Compare commits

...

15 Commits

Author SHA1 Message Date
WithoutPants
95a2c8d13f Update changelog for bugfix release 2024-01-10 11:21:06 +11:00
DingDongSoLong4
0b131f76df Fix scene marker merging (#4446) 2024-01-10 10:25:05 +11:00
WithoutPants
6271f18979 Fix error when creating/updating performer with alias == name (#4443)
* Filter out performer aliases that match the name
* Validate when creating/updating performer in stash-box task
2024-01-09 14:57:49 +11:00
WithoutPants
ca976a0994 Don't bail on error when scraping all (#4442) 2024-01-09 11:39:00 +11:00
DingDongSoLong4
9859ec61fb Calculate DetailImage fallback width using rem (#4441) 2024-01-09 11:11:46 +11:00
DingDongSoLong4
a998497004 Hide tag input when set tags is disabled (#4440) 2024-01-09 11:09:42 +11:00
bayured
f5e3fe77b7 Update FieldStrategyOverwrite to work when scene has no existing URL (#4412) 2024-01-09 10:23:29 +11:00
WithoutPants
743ab9a52c Sort plugin settings (#4435) 2024-01-09 09:32:26 +11:00
WithoutPants
d23cecfc18 Disable select all checkbox for plugin sources (#4434) 2024-01-09 09:32:16 +11:00
DingDongSoLong4
d8990e655d Fix settings tab links (#4430) 2024-01-08 12:08:09 +11:00
DingDongSoLong4
5b9a96b843 Scene queue autoplay (#4428)
* Remove unnecessary undefined checks
* Respect autostartVideoOnPlaySelected in scene queue
2024-01-08 12:04:30 +11:00
CJ
b968aa3f31 Fixes package manger head border (#4420) 2024-01-08 11:54:14 +11:00
DingDongSoLong4
910c7025dc Fix scraped performer alias matching (#4432) 2024-01-08 11:50:31 +11:00
bayured
ea503833c5 Add join to intCriterionHandler (#4414)
* Add join to intCriterionHandler
* Add join to floatCriterionHandler
2024-01-08 11:48:16 +11:00
cc1234475
6848dec5f4 Add CSP to plugin from the yaml file (#4424) 2024-01-08 11:45:55 +11:00
22 changed files with 314 additions and 164 deletions

View File

@@ -392,7 +392,7 @@ func getScenePartial(scene *models.Scene, scraped *scraper.ScrapedScene, fieldOp
switch getFieldStrategy(fieldOptions["url"]) {
case FieldStrategyOverwrite:
// only overwrite if not equal
if len(sliceutil.Exclude(scene.URLs.List(), scraped.URLs)) != 0 {
if len(sliceutil.Exclude(scraped.URLs, scene.URLs.List())) != 0 {
partial.URLs = &models.UpdateStrings{
Values: scraped.URLs,
Mode: models.RelationshipUpdateModeSet,

View File

@@ -7,6 +7,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/scraper/stashbox"
"github.com/stashapp/stash/pkg/studio"
)
@@ -155,6 +156,10 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
partial := p.ToPartial(t.box.Endpoint, excluded, existingStashIDs)
if err := performer.ValidateUpdate(ctx, t.performer.ID, partial, qb); err != nil {
return err
}
if _, err := qb.UpdatePartial(ctx, t.performer.ID, partial); err != nil {
return err
}
@@ -185,6 +190,10 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
err = r.WithTxn(ctx, func(ctx context.Context) error {
qb := r.Performer
if err := performer.ValidateCreate(ctx, *newPerformer, qb); err != nil {
return err
}
if err := qb.Create(ctx, newPerformer); err != nil {
return err
}

View File

@@ -44,22 +44,21 @@ func ScrapedPerformer(ctx context.Context, qb PerformerFinder, p *models.Scraped
}
performers, err := qb.FindByNames(ctx, []string{*p.Name}, true)
if err != nil {
return err
}
if performers == nil || len(performers) != 1 {
// try matching a single performer by exact alias
if len(performers) == 0 {
// if no names matched, try match an exact alias
performers, err = performer.ByAlias(ctx, qb, *p.Name)
if err != nil {
return err
}
}
if performers == nil || len(performers) != 1 {
// ignore - cannot match
return nil
}
if len(performers) != 1 {
// ignore - cannot match
return nil
}
id := strconv.Itoa(performers[0].ID)

View File

@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/stashapp/stash/pkg/utils"
@@ -206,7 +207,15 @@ func convertHooks(hooks []HookTriggerEnum) []string {
func (c Config) getPluginSettings() []PluginSetting {
ret := []PluginSetting{}
for k, o := range c.Settings {
var keys []string
for k := range c.Settings {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
o := c.Settings[k]
t := o.Type
if t == "" {
t = PluginSettingTypeEnumString
@@ -248,6 +257,7 @@ func (c Config) toPlugin() *Plugin {
ExternalCSS: c.UI.getExternalCSS(),
Javascript: c.UI.getJavascriptFiles(c),
CSS: c.UI.getCSSFiles(c),
CSP: c.UI.CSP,
Assets: c.UI.Assets,
},
Settings: c.getPluginSettings(),

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
@@ -129,6 +130,12 @@ func (s *Service) mergeSceneMarkers(ctx context.Context, dest *models.Scene, src
destExists, _ := fsutil.FileExists(e.dest)
if srcExists && !destExists {
destDir := filepath.Dir(e.dest)
if err := fsutil.EnsureDir(destDir); err != nil {
logger.Errorf("Error creating generated marker folder %s: %v", destDir, err)
continue
}
if err := os.Rename(e.src, e.dest); err != nil {
logger.Errorf("Error renaming generated marker file from %s to %s: %v", e.src, e.dest, err)
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/scraper/stashbox/graphql"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
@@ -669,6 +670,12 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc
}
if len(p.Aliases) > 0 {
// #4437 - stash-box may return aliases that are equal to the performer name
// filter these out
p.Aliases = sliceutil.Filter(p.Aliases, func(s string) bool {
return !strings.EqualFold(s, p.Name)
})
alias := strings.Join(p.Aliases, ", ")
sp.Aliases = &alias
}

View File

@@ -542,6 +542,9 @@ func getPathSearchClauseMany(pathColumn, basenameColumn, p string, addWildcards,
func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
clause, args := getIntCriterionWhereClause(column, *c)
f.addWhere(clause, args...)
}
@@ -551,6 +554,9 @@ func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn f
func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
clause, args := getFloatCriterionWhereClause(column, *c)
f.addWhere(clause, args...)
}

View File

@@ -15,7 +15,7 @@ import { objectTitle } from "src/core/files";
import { QueuedScene } from "src/models/sceneQueue";
export interface IPlaylistViewer {
scenes?: QueuedScene[];
scenes: QueuedScene[];
currentID?: string;
start?: number;
continue?: boolean;
@@ -47,7 +47,7 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
const [lessLoading, setLessLoading] = useState(false);
const [moreLoading, setMoreLoading] = useState(false);
const currentIndex = scenes?.findIndex((s) => s.id === currentID) ?? 0;
const currentIndex = scenes.findIndex((s) => s.id === currentID);
useEffect(() => {
setLessLoading(false);
@@ -130,7 +130,7 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
) : (
""
)}
{currentIndex < (scenes ?? []).length - 1 || hasMoreScenes ? (
{currentIndex < scenes.length - 1 || hasMoreScenes ? (
<Button
className="minimal"
variant="secondary"
@@ -162,7 +162,7 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
</Button>
</div>
) : undefined}
<ol start={start}>{(scenes ?? []).map(renderPlaylistEntry)}</ol>
<ol start={start}>{scenes.map(renderPlaylistEntry)}</ol>
{hasMoreScenes ? (
<div className="d-flex justify-content-center">
<Button onClick={() => moreClicked()} disabled={moreLoading}>

View File

@@ -81,9 +81,9 @@ interface IProps {
onQueueNext: () => void;
onQueuePrevious: () => void;
onQueueRandom: () => void;
onQueueSceneClicked: (sceneID: string) => void;
onDelete: () => void;
continuePlaylist: boolean;
loadScene: (sceneID: string) => void;
queueHasMoreScenes: boolean;
onQueueMoreScenes: () => void;
onQueueLessScenes: () => void;
@@ -104,9 +104,9 @@ const ScenePage: React.FC<IProps> = ({
onQueueNext,
onQueuePrevious,
onQueueRandom,
onQueueSceneClicked,
onDelete,
continuePlaylist,
loadScene,
queueHasMoreScenes,
onQueueMoreScenes,
onQueueLessScenes,
@@ -359,7 +359,7 @@ const ScenePage: React.FC<IProps> = ({
<FormattedMessage id="details" />
</Nav.Link>
</Nav.Item>
{(queueScenes ?? []).length > 0 ? (
{queueScenes.length > 0 ? (
<Nav.Item>
<Nav.Link eventKey="scene-queue-panel">
<FormattedMessage id="queue" />
@@ -445,7 +445,7 @@ const ScenePage: React.FC<IProps> = ({
currentID={scene.id}
continue={continuePlaylist}
setContinue={setContinuePlaylist}
onSceneClicked={loadScene}
onSceneClicked={onQueueSceneClicked}
onNext={onQueueNext}
onPrevious={onQueuePrevious}
onRandom={onQueueRandom}
@@ -594,8 +594,11 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
const [queueStart, setQueueStart] = useState(1);
const autoplay = queryParams.get("autoplay") === "true";
const autoPlayOnSelected =
configuration?.interface.autostartVideoOnPlaySelected ?? false;
const currentQueueIndex = useMemo(
() => (queueScenes ? queueScenes.findIndex((s) => s.id === id) : -1),
() => queueScenes.findIndex((s) => s.id === id),
[queueScenes, id]
);
@@ -692,61 +695,46 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
history.replace(sceneLink);
}
function onDelete() {
if (
continuePlaylist &&
queueScenes &&
currentQueueIndex >= 0 &&
currentQueueIndex < queueScenes.length - 1
) {
loadScene(queueScenes[currentQueueIndex + 1].id);
} else {
history.push("/scenes");
}
}
async function queueNext(autoPlay: boolean) {
if (currentQueueIndex === -1) return;
async function onQueueNext() {
if (!queueScenes) return;
if (currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1) {
loadScene(queueScenes[currentQueueIndex + 1].id, true);
if (currentQueueIndex < queueScenes.length - 1) {
loadScene(queueScenes[currentQueueIndex + 1].id, autoPlay);
} else {
// if we're at the end of the queue, load more scenes
if (
currentQueueIndex >= 0 &&
currentQueueIndex === queueScenes.length - 1 &&
queueHasMoreScenes
) {
if (currentQueueIndex === queueScenes.length - 1 && queueHasMoreScenes) {
const loadedScenes = await onQueueMoreScenes();
if (loadedScenes && loadedScenes.length > 0) {
// set the page to the next page
const newPage = (sceneQueue.query?.currentPage ?? 0) + 1;
loadScene(loadedScenes[0].id, true, newPage);
loadScene(loadedScenes[0].id, autoPlay, newPage);
}
}
}
}
async function onQueuePrevious() {
if (!queueScenes) return;
async function queuePrevious(autoPlay: boolean) {
if (currentQueueIndex === -1) return;
if (currentQueueIndex > 0) {
loadScene(queueScenes[currentQueueIndex - 1].id, true);
loadScene(queueScenes[currentQueueIndex - 1].id, autoPlay);
} else {
// if we're at the beginning of the queue, load the previous page
if (currentQueueIndex === 0 && queueStart > 1) {
if (queueStart > 1) {
const loadedScenes = await onQueueLessScenes();
if (loadedScenes && loadedScenes.length > 0) {
const newPage = (sceneQueue.query?.currentPage ?? 0) - 1;
loadScene(loadedScenes[loadedScenes.length - 1].id, true, newPage);
loadScene(
loadedScenes[loadedScenes.length - 1].id,
autoPlay,
newPage
);
}
}
}
}
async function onQueueRandom() {
if (!queueScenes) return;
async function queueRandom(autoPlay: boolean) {
if (sceneQueue.query) {
const { query } = sceneQueue;
const pages = Math.ceil(queueTotal / query.itemsPerPage);
@@ -760,20 +748,30 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
if (queryResults.data.findScenes.scenes.length > index) {
const { id: sceneID } = queryResults.data.findScenes.scenes[index];
// navigate to the image player page
loadScene(sceneID, undefined, page);
loadScene(sceneID, autoPlay, page);
}
} else {
} else if (queueTotal !== 0) {
const index = Math.floor(Math.random() * queueTotal);
loadScene(queueScenes[index].id);
loadScene(queueScenes[index].id, autoPlay);
}
}
function onComplete() {
if (!queueScenes) return;
// load the next scene if we're continuing
if (continuePlaylist) {
onQueueNext();
queueNext(true);
}
}
function onDelete() {
if (
continuePlaylist &&
currentQueueIndex >= 0 &&
currentQueueIndex < queueScenes.length - 1
) {
loadScene(queueScenes[currentQueueIndex + 1].id);
} else {
history.push("/scenes");
}
}
@@ -789,8 +787,8 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
return Math.floor((index + queueStart - 1) / perPage) + 1;
}
function onSceneClicked(sceneID: string) {
loadScene(sceneID, true, getScenePage(sceneID));
function onQueueSceneClicked(sceneID: string) {
loadScene(sceneID, autoPlayOnSelected, getScenePage(sceneID));
}
if (!scene) {
@@ -804,14 +802,14 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
<ScenePage
scene={scene}
setTimestamp={setTimestamp}
queueScenes={queueScenes ?? []}
queueScenes={queueScenes}
queueStart={queueStart}
onDelete={onDelete}
onQueueNext={onQueueNext}
onQueuePrevious={onQueuePrevious}
onQueueRandom={onQueueRandom}
onQueueNext={() => queueNext(autoPlayOnSelected)}
onQueuePrevious={() => queuePrevious(autoPlayOnSelected)}
onQueueRandom={() => queueRandom(autoPlayOnSelected)}
onQueueSceneClicked={onQueueSceneClicked}
continuePlaylist={continuePlaylist}
loadScene={onSceneClicked}
queueHasMoreScenes={queueHasMoreScenes}
onQueueLessScenes={onQueueLessScenes}
onQueueMoreScenes={onQueueMoreScenes}
@@ -829,8 +827,8 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
initialTimestamp={initialTimestamp}
sendSetTimestamp={getSetTimestamp}
onComplete={onComplete}
onNext={onQueueNext}
onPrevious={onQueuePrevious}
onNext={() => queueNext(true)}
onPrevious={() => queuePrevious(true)}
/>
</div>
</div>

View File

@@ -161,6 +161,7 @@ export const AvailableScraperPackages: React.FC = () => {
addSource={addSource}
editSource={editSource}
deleteSource={deleteSource}
allowSelectAll
/>
</div>
</SettingSection>

View File

@@ -1,6 +1,7 @@
import React from "react";
import { Tab, Nav, Row, Col } from "react-bootstrap";
import { useHistory, useLocation } from "react-router-dom";
import { Redirect, RouteComponentProps } from "react-router-dom";
import { LinkContainer } from "react-router-bootstrap";
import { FormattedMessage } from "react-intl";
import { Helmet } from "react-helmet";
import { useTitleProps } from "src/hooks/title";
@@ -18,83 +19,133 @@ import { SettingsLibraryPanel } from "./SettingsLibraryPanel";
import { SettingsSecurityPanel } from "./SettingsSecurityPanel";
import Changelog from "../Changelog/Changelog";
export const Settings: React.FC = () => {
const location = useLocation();
const history = useHistory();
const defaultTab = new URLSearchParams(location.search).get("tab") ?? "tasks";
const validTabs = [
"tasks",
"library",
"interface",
"security",
"metadata-providers",
"services",
"system",
"plugins",
"logs",
"tools",
"changelog",
"about",
] as const;
type TabKey = (typeof validTabs)[number];
const onSelect = (val: string) => history.push(`?tab=${val}`);
const defaultTab: TabKey = "tasks";
function isTabKey(tab: string | null): tab is TabKey {
return validTabs.includes(tab as TabKey);
}
const Settings: React.FC<RouteComponentProps> = ({ location }) => {
const tab = new URLSearchParams(location.search).get("tab");
const titleProps = useTitleProps({ id: "settings" });
if (!isTabKey(tab)) {
return (
<Redirect
to={{
...location,
search: `tab=${defaultTab}`,
}}
/>
);
}
return (
<Tab.Container
activeKey={defaultTab}
id="configuration-tabs"
onSelect={(tab) => tab && onSelect(tab)}
>
<Tab.Container activeKey={tab} id="configuration-tabs">
<Helmet {...titleProps} />
<Row>
<Col id="settings-menu-container" sm={3} md={3} xl={2}>
<Nav variant="pills" className="flex-column">
<Nav.Item>
<Nav.Link eventKey="tasks">
<FormattedMessage id="config.categories.tasks" />
</Nav.Link>
<LinkContainer to="/settings?tab=tasks">
<Nav.Link eventKey="tasks">
<FormattedMessage id="config.categories.tasks" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="library">
<FormattedMessage id="library" />
</Nav.Link>
<LinkContainer to="/settings?tab=library">
<Nav.Link eventKey="library">
<FormattedMessage id="library" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="interface">
<FormattedMessage id="config.categories.interface" />
</Nav.Link>
<LinkContainer to="/settings?tab=interface">
<Nav.Link eventKey="interface">
<FormattedMessage id="config.categories.interface" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="security">
<FormattedMessage id="config.categories.security" />
</Nav.Link>
<LinkContainer to="/settings?tab=security">
<Nav.Link eventKey="security">
<FormattedMessage id="config.categories.security" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="metadata-providers">
<FormattedMessage id="config.categories.metadata_providers" />
</Nav.Link>
<LinkContainer to="/settings?tab=metadata-providers">
<Nav.Link eventKey="metadata-providers">
<FormattedMessage id="config.categories.metadata_providers" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="services">
<FormattedMessage id="config.categories.services" />
</Nav.Link>
<LinkContainer to="/settings?tab=services">
<Nav.Link eventKey="services">
<FormattedMessage id="config.categories.services" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="system">
<FormattedMessage id="config.categories.system" />
</Nav.Link>
<LinkContainer to="/settings?tab=system">
<Nav.Link eventKey="system">
<FormattedMessage id="config.categories.system" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="plugins">
<FormattedMessage id="config.categories.plugins" />
</Nav.Link>
<LinkContainer to="/settings?tab=plugins">
<Nav.Link eventKey="plugins">
<FormattedMessage id="config.categories.plugins" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="logs">
<FormattedMessage id="config.categories.logs" />
</Nav.Link>
<LinkContainer to="/settings?tab=logs">
<Nav.Link eventKey="logs">
<FormattedMessage id="config.categories.logs" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="tools">
<FormattedMessage id="config.categories.tools" />
</Nav.Link>
<LinkContainer to="/settings?tab=tools">
<Nav.Link eventKey="tools">
<FormattedMessage id="config.categories.tools" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="changelog">
<FormattedMessage id="config.categories.changelog" />
</Nav.Link>
<LinkContainer to="/settings?tab=changelog">
<Nav.Link eventKey="changelog">
<FormattedMessage id="config.categories.changelog" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="about">
<FormattedMessage id="config.categories.about" />
</Nav.Link>
<LinkContainer to="/settings?tab=about">
<Nav.Link eventKey="about">
<FormattedMessage id="config.categories.about" />
</Nav.Link>
</LinkContainer>
</Nav.Item>
<hr className="d-sm-none" />
</Nav>

View File

@@ -1,6 +1,7 @@
import { useLayoutEffect, useRef } from "react";
import { remToPx } from "src/utils/units";
const DEFAULT_WIDTH = "200";
const DEFAULT_WIDTH = Math.round(remToPx(30));
// Props used by the <img> element
type IDetailImageProps = JSX.IntrinsicElements["img"];
@@ -17,7 +18,7 @@ export const DetailImage = (props: IDetailImageProps) => {
// If the naturalWidth is zero, it means the image either hasn't loaded yet
// or we're on Firefox and it is an SVG w/o an intrinsic size.
// So set the width to our fallback width.
img.setAttribute("width", DEFAULT_WIDTH);
img.setAttribute("width", String(DEFAULT_WIDTH));
} else {
// If we have a `naturalWidth`, this could either be the actual intrinsic width
// of the image, or the image is an SVG w/o an intrinsic size and we're on Chrome or Safari,
@@ -26,7 +27,7 @@ export const DetailImage = (props: IDetailImageProps) => {
// so we need to clone the image to disconnect it from the DOM, and then get the `naturalWidth` of the clone,
// in order to always return the same `naturalWidth` for a given src.
const i = img.cloneNode() as HTMLImageElement;
img.setAttribute("width", (i.naturalWidth || DEFAULT_WIDTH).toString());
img.setAttribute("width", String(i.naturalWidth || DEFAULT_WIDTH));
}
}

View File

@@ -224,6 +224,9 @@ const InstalledPackagesList: React.FC<{
</th>
) : undefined}
</tr>
<tr>
<th className="border-row" colSpan={100}></th>
</tr>
</thead>
<tbody>{renderBody()}</tbody>
</Table>
@@ -620,6 +623,7 @@ const SourcePackagesList: React.FC<{
loadSource: () => Promise<RemotePackage[]>;
selectedOnly: boolean;
selectedPackages: RemotePackage[];
allowSelectAll?: boolean;
setSelectedPackages: React.Dispatch<React.SetStateAction<RemotePackage[]>>;
renderDescription?: (pkg: RemotePackage) => React.ReactNode;
editSource: () => void;
@@ -627,6 +631,7 @@ const SourcePackagesList: React.FC<{
}> = ({
source,
loadSource,
allowSelectAll,
selectedOnly,
selectedPackages,
setSelectedPackages,
@@ -785,7 +790,7 @@ const SourcePackagesList: React.FC<{
<>
<tr className="package-source">
<td>
{packages !== undefined ? (
{allowSelectAll && packages !== undefined ? (
<Form.Check
checked={sourceChecked ?? false}
onChange={() => toggleSource()}
@@ -844,6 +849,7 @@ const AvailablePackagesList: React.FC<{
React.SetStateAction<Record<string, RemotePackage[]>>
>;
selectedOnly: boolean;
allowSourceSelectAll?: boolean;
addSource: (src: GQL.PackageSource) => void;
editSource: (existing: GQL.PackageSource, changed: GQL.PackageSource) => void;
deleteSource: (source: GQL.PackageSource) => void;
@@ -859,6 +865,7 @@ const AvailablePackagesList: React.FC<{
addSource,
editSource,
deleteSource,
allowSourceSelectAll,
}) => {
const [deletingSource, setDeletingSource] = useState<GQL.PackageSource>();
const [editingSource, setEditingSource] = useState<GQL.PackageSource>();
@@ -920,6 +927,7 @@ const AvailablePackagesList: React.FC<{
setSelectedPackages={(v) => setSelectedSourcePackages(src, v)}
editSource={() => setEditingSource(src)}
deleteSource={() => setDeletingSource(src)}
allowSelectAll={allowSourceSelectAll}
/>
))}
<tr className="add-package-source">
@@ -983,6 +991,9 @@ const AvailablePackagesList: React.FC<{
<FormattedMessage id="package_manager.description" />
</th>
</tr>
<tr>
<th className="border-row" colSpan={100}></th>
</tr>
</thead>
<tbody>{renderBody()}</tbody>
</Table>
@@ -1000,6 +1011,7 @@ export const AvailablePackages: React.FC<{
addSource: (src: GQL.PackageSource) => void;
editSource: (existing: GQL.PackageSource, changed: GQL.PackageSource) => void;
deleteSource: (source: GQL.PackageSource) => void;
allowSelectAll?: boolean;
}> = ({
sources,
loadSource,
@@ -1009,6 +1021,7 @@ export const AvailablePackages: React.FC<{
addSource,
editSource,
deleteSource,
allowSelectAll,
}) => {
const [checkedPackages, setCheckedPackages] = useState<
Record<string, RemotePackage[]>
@@ -1060,6 +1073,7 @@ export const AvailablePackages: React.FC<{
addSource={addSource}
editSource={editSource}
deleteSource={deleteSource}
allowSourceSelectAll={allowSelectAll}
/>
</div>
);

View File

@@ -29,6 +29,10 @@
.package-manager-table-container {
max-height: 300px;
overflow-y: auto;
th {
border: none;
}
}
table thead {

View File

@@ -24,6 +24,7 @@ import { useLocalForage } from "src/hooks/LocalForage";
import { useToast } from "src/hooks/Toast";
import { ConfigurationContext } from "src/hooks/Config";
import { ITaggerSource, SCRAPER_PREFIX, STASH_BOX_PREFIX } from "./constants";
import { errorToString } from "src/utils";
export interface ITaggerContextState {
config: ITaggerConfig;
@@ -293,21 +294,29 @@ export const TaggerContext: React.FC = ({ children }) => {
return;
}
const results = await queryScrapeScene(currentSource.sourceInput, sceneID);
let newResult: ISceneQueryResult;
if (results.error) {
newResult = { error: results.error.message };
} else if (results.errors) {
newResult = { error: results.errors.toString() };
} else {
newResult = {
results: results.data.scrapeSingleScene.map((r) => ({
...r,
// scenes are already resolved if they are scraped via fragment
resolved: true,
})),
};
try {
const results = await queryScrapeScene(
currentSource.sourceInput,
sceneID
);
if (results.error) {
newResult = { error: results.error.message };
} else if (results.errors) {
newResult = { error: results.errors.toString() };
} else {
newResult = {
results: results.data.scrapeSingleScene.map((r) => ({
...r,
// scenes are already resolved if they are scraped via fragment
resolved: true,
})),
};
}
} catch (err: unknown) {
newResult = { error: errorToString(err) };
}
setSearchResults((current) => {

View File

@@ -690,27 +690,30 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
}
}
const renderTagsField = () => (
<div className="mt-2">
<div>
<Form.Group controlId="tags" as={Row}>
{FormUtils.renderLabel({
title: `${intl.formatMessage({ id: "tags" })}:`,
})}
<Col sm={9} xl={12}>
<TagSelect
isMulti
onSelect={(items) => {
setTagIDs(items.map((i) => i.id));
}}
ids={tagIDs}
/>
</Col>
</Form.Group>
</div>
{scene.tags
?.filter((t) => !t.stored_id)
.map((t) => (
function maybeRenderTagsField() {
if (!config.setTags) return;
const createTags = scene.tags?.filter((t) => !t.stored_id);
return (
<div className="mt-2">
<div>
<Form.Group controlId="tags" as={Row}>
{FormUtils.renderLabel({
title: `${intl.formatMessage({ id: "tags" })}:`,
})}
<Col sm={9} xl={12}>
<TagSelect
isMulti
onSelect={(items) => {
setTagIDs(items.map((i) => i.id));
}}
ids={tagIDs}
/>
</Col>
</Form.Group>
</div>
{createTags?.map((t) => (
<Badge
className="tag-item"
variant="secondary"
@@ -725,8 +728,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
</Button>
</Badge>
))}
</div>
);
</div>
);
}
if (loading) {
return <LoadingIndicator card />;
@@ -769,7 +773,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
<div className="col-lg-6">
{maybeRenderStudioField()}
{renderPerformerField()}
{renderTagsField()}
{maybeRenderTagsField()}
<div className="row no-gutters mt-2 align-items-center justify-content-end">
<OperationButton operation={handleSave}>

View File

@@ -10,6 +10,7 @@
* Added option to Duplicate Checker to select all files except the highest resolution. ([#4286](https://github.com/stashapp/stash/pull/4286))
### 🎨 Improvements
* **[0.24.2]** Hide Tags input in Tagger when Set Tags is disabled. ([#4440](https://github.com/stashapp/stash/pull/4440))
* Show Performer image in Performer select list. ([#4227](https://github.com/stashapp/stash/pull/4227))
* Match Performers by alias during scraping and tagging if no Performer is found with the exact name (only if a single performer is found with the alias). ([#4182](https://github.com/stashapp/stash/pull/4182))
* Show Performer disambiguation and add stash-box links to Studio in tagger results. ([#4180](https://github.com/stashapp/stash/pull/4180))
@@ -21,6 +22,16 @@
* Added support for setting plugins path from the UI. ([#4382](https://github.com/stashapp/stash/pull/4382))
### 🐛 Bug fixes
* **[0.24.2]** Fixed error when renaming marker files during scene merge operation ([#4446](https://github.com/stashapp/stash/pull/4446))
* **[0.24.2]** Fixed error when creating/updating a Performer where an alias is the same as the Performer name. ([#4443](https://github.com/stashapp/stash/pull/4443))
* **[0.24.2]** Errors during the tagger Scrape All operation now output to the scene card and no longer stop the operation. ([#4442](https://github.com/stashapp/stash/pull/4442))
* **[0.24.2]** Fixed studio image sizing on details pages. ([#4441](https://github.com/stashapp/stash/pull/4441))
* **[0.24.2]** Fixed URL not being overwritten when specified during Identify ([#4412](https://github.com/stashapp/stash/pull/4412))
* **[0.24.2]** Fixed plugin settings to be sorted alphabetically, instead of being displayed in a random order. ([#4435](https://github.com/stashapp/stash/pull/4435))
* **[0.24.2]** Fixed scene queue not respecting the Auto-start video setting. ([#4428](https://github.com/stashapp/stash/pull/4428))
* **[0.24.2]** Fixed performers incorrectly being matched by alias during scraping. ([#4432](https://github.com/stashapp/stash/pull/4432))
* **[0.24.2]** Fixed error when filtering on Scene interactive speed. ([#4414](https://github.com/stashapp/stash/pull/4414))
* **[0.24.2]** Fixed plugin CSP not being enacted. ([#4424](https://github.com/stashapp/stash/pull/4424))
* **[0.24.1]** Fixed external player button not working correctly. ([#4403](https://github.com/stashapp/stash/pull/4403))
* **[0.24.1]** Fixed image thumbnail generation on arm devices. ([#4402](https://github.com/stashapp/stash/pull/4402))
* **[0.24.1]** Reverted change to modal button order. ([#4400](https://github.com/stashapp/stash/pull/4400))

View File

@@ -6,6 +6,7 @@ import React, {
useMemo,
} from "react";
import { Toast } from "react-bootstrap";
import { errorToString } from "src/utils";
interface IToast {
header?: string;
@@ -67,16 +68,7 @@ export const useToast = () => {
});
},
error(error: unknown) {
let message;
if (error instanceof Error) {
message = error.message;
}
if (!message) {
message = String(error);
}
if (!message) {
message = "Unknown error";
}
const message = errorToString(error);
console.error(error);
addToast({

View File

@@ -491,6 +491,14 @@ textarea.text-input {
}
}
/* stylelint-disable declaration-no-important */
.border-row {
background-color: #414c53;
height: 1px;
padding: 0 !important;
}
/* stylelint-enable declaration-no-important */
@media (max-width: 576px) {
.row.justify-content-center {
margin-left: 0;

View File

@@ -112,8 +112,8 @@ export class SceneQueue {
let params = [
this.makeQueryParameters(options.sceneIndex, options.newPage),
];
if (options.autoPlay !== undefined) {
params.push("autoplay=" + options.autoPlay);
if (options.autoPlay) {
params.push("autoplay=true");
}
if (options.continue !== undefined) {
params.push("continue=" + options.continue);

View File

@@ -2,3 +2,18 @@ import { ApolloError } from "@apollo/client";
export const apolloError = (error: unknown) =>
error instanceof ApolloError ? error.message : "";
export function errorToString(error: unknown) {
let message;
if (error instanceof Error) {
message = error.message;
}
if (!message) {
message = String(error);
}
if (!message) {
message = "Unknown error";
}
return message;
}

View File

@@ -15,3 +15,7 @@ export function cmToInches(cm: number) {
const inches = cm * cmInInches;
return inches;
}
export function remToPx(rem: number) {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
}