From b1608128d60b4adf1a38d8d014adb6edb353e4a2 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 22 Mar 2023 11:17:31 +1100 Subject: [PATCH] Add folder browser to path filter field (#3570) --- .../src/components/List/CriterionEditor.tsx | 7 +++ .../components/List/Filters/PathFilter.tsx | 32 ++++++++++++ .../Shared/FolderSelect/FolderSelect.tsx | 51 ++++++++++++------- .../models/list-filter/criteria/criterion.ts | 14 +++++ .../models/list-filter/criteria/factory.ts | 5 +- ui/v2.5/src/models/list-filter/galleries.ts | 3 +- ui/v2.5/src/models/list-filter/images.ts | 3 +- ui/v2.5/src/models/list-filter/scenes.ts | 3 +- 8 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 ui/v2.5/src/components/List/Filters/PathFilter.tsx diff --git a/ui/v2.5/src/components/List/CriterionEditor.tsx b/ui/v2.5/src/components/List/CriterionEditor.tsx index bdb09b221..7e7e5c636 100644 --- a/ui/v2.5/src/components/List/CriterionEditor.tsx +++ b/ui/v2.5/src/components/List/CriterionEditor.tsx @@ -12,6 +12,7 @@ import { DateCriterion, TimestampCriterion, BooleanCriterion, + PathCriterionOption, } from "src/models/list-filter/criteria/criterion"; import { useIntl } from "react-intl"; import { @@ -36,6 +37,7 @@ import { RatingCriterion } from "../../models/list-filter/criteria/rating"; import { RatingFilter } from "./Filters/RatingFilter"; import { BooleanFilter } from "./Filters/BooleanFilter"; import { OptionsListFilter } from "./Filters/OptionsListFilter"; +import { PathFilter } from "./Filters/PathFilter"; interface IGenericCriterionEditor { criterion: Criterion; @@ -137,6 +139,11 @@ const GenericCriterionEditor: React.FC = ({ // // ); } + if (criterion.criterionOption instanceof PathCriterionOption) { + return ( + + ); + } if (criterion instanceof DurationCriterion) { return ( diff --git a/ui/v2.5/src/components/List/Filters/PathFilter.tsx b/ui/v2.5/src/components/List/Filters/PathFilter.tsx new file mode 100644 index 000000000..5ab23de9d --- /dev/null +++ b/ui/v2.5/src/components/List/Filters/PathFilter.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Form } from "react-bootstrap"; +import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; +import { ConfigurationContext } from "src/hooks/Config"; +import { + Criterion, + CriterionValue, +} from "../../../models/list-filter/criteria/criterion"; + +interface IInputFilterProps { + criterion: Criterion; + onValueChanged: (value: string) => void; +} + +export const PathFilter: React.FC = ({ + criterion, + onValueChanged, +}) => { + const { configuration } = React.useContext(ConfigurationContext); + const libraryPaths = configuration?.general.stashes.map((s) => s.path); + + return ( + + onValueChanged(v)} + collapsible + defaultDirectories={libraryPaths} + /> + + ); +}; diff --git a/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx b/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx index 01a937bfd..cc89eaeb9 100644 --- a/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx +++ b/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, InputGroup, Form } from "react-bootstrap"; +import { Button, InputGroup, Form, Collapse } from "react-bootstrap"; import { Icon } from "../Icon"; import { LoadingIndicator } from "../LoadingIndicator"; import { useDirectory } from "src/core/StashService"; -import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis, faTimes } from "@fortawesome/free-solid-svg-icons"; import { useDebouncedSetState } from "src/hooks/debounce"; interface IProps { @@ -12,6 +12,7 @@ interface IProps { setCurrentDirectory: (value: string) => void; defaultDirectories?: string[]; appendButton?: JSX.Element; + collapsible?: boolean; } export const FolderSelect: React.FC = ({ @@ -19,7 +20,9 @@ export const FolderSelect: React.FC = ({ setCurrentDirectory, defaultDirectories, appendButton, + collapsible = false, }) => { + const [showBrowser, setShowBrowser] = React.useState(false); const [directory, setDirectory] = useState(currentDirectory); const { data, error, loading } = useDirectory(directory); const intl = useIntl(); @@ -31,9 +34,10 @@ export const FolderSelect: React.FC = ({ const debouncedSetDirectory = useDebouncedSetState(setDirectory, 250); useEffect(() => { - if (currentDirectory === "" && !defaultDirectories && data?.directory.path) - setCurrentDirectory(data.directory.path); - }, [currentDirectory, setCurrentDirectory, data, defaultDirectories]); + if (currentDirectory !== directory) { + debouncedSetDirectory(currentDirectory); + } + }, [currentDirectory, directory, debouncedSetDirectory]); function setInstant(value: string) { setCurrentDirectory(value); @@ -66,6 +70,7 @@ export const FolderSelect: React.FC = ({ <> ) => { setDebounced(e.currentTarget.value); @@ -76,6 +81,16 @@ export const FolderSelect: React.FC = ({ {appendButton ? ( {appendButton} ) : undefined} + {collapsible ? ( + + + + ) : undefined} {!data || !data.directory || loading ? ( {loading ? ( @@ -89,18 +104,20 @@ export const FolderSelect: React.FC = ({ {error !== undefined && (
Error: {error.message}
)} -
    - {topDirectory} - {selectableDirectories.map((path) => { - return ( -
  • - -
  • - ); - })} -
+ +
    + {topDirectory} + {selectableDirectories.map((path) => { + return ( +
  • + +
  • + ); + })} +
+
); }; diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index a65638346..a4b53dec7 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -279,6 +279,20 @@ export function createMandatoryStringCriterionOption( ); } +export class PathCriterionOption extends StringCriterionOption {} + +export function createPathCriterionOption( + value: CriterionType, + messageID?: string, + parameterName?: string +) { + return new PathCriterionOption( + messageID ?? value, + value, + parameterName ?? messageID ?? value + ); +} + export class BooleanCriterionOption extends CriterionOption { constructor(messageID: string, value: CriterionType, parameterName?: string) { super({ diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index d3f57e12d..28bec371b 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -15,6 +15,7 @@ import { DateCriterionOption, TimestampCriterion, MandatoryTimestampCriterionOption, + PathCriterionOption, } from "./criterion"; import { OrganizedCriterion } from "./organized"; import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite"; @@ -65,9 +66,7 @@ export function makeCriteria( return new NoneCriterion(); case "name": case "path": - return new StringCriterion( - new MandatoryStringCriterionOption(type, type) - ); + return new StringCriterion(new PathCriterionOption(type, type)); case "checksum": return new StringCriterion( new MandatoryStringCriterionOption("media_info.checksum", type, type) diff --git a/ui/v2.5/src/models/list-filter/galleries.ts b/ui/v2.5/src/models/list-filter/galleries.ts index 1a0ab8688..36bb65de6 100644 --- a/ui/v2.5/src/models/list-filter/galleries.ts +++ b/ui/v2.5/src/models/list-filter/galleries.ts @@ -4,6 +4,7 @@ import { NullNumberCriterionOption, createDateCriterionOption, createMandatoryTimestampCriterionOption, + createPathCriterionOption, } from "./criteria/criterion"; import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { GalleryIsMissingCriterionOption } from "./criteria/is-missing"; @@ -43,7 +44,7 @@ const displayModeOptions = [ const criterionOptions = [ createStringCriterionOption("title"), createStringCriterionOption("details"), - createStringCriterionOption("path"), + createPathCriterionOption("path"), createStringCriterionOption( "galleryChecksum", "media_info.checksum", diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index 7e0e0fce3..372ca21ca 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -5,6 +5,7 @@ import { NullNumberCriterionOption, createMandatoryTimestampCriterionOption, createDateCriterionOption, + createPathCriterionOption, } from "./criteria/criterion"; import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { ImageIsMissingCriterionOption } from "./criteria/is-missing"; @@ -33,7 +34,7 @@ const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall]; const criterionOptions = [ createStringCriterionOption("title"), createMandatoryStringCriterionOption("checksum", "media_info.checksum"), - createMandatoryStringCriterionOption("path"), + createPathCriterionOption("path"), OrganizedCriterionOption, createMandatoryNumberCriterionOption("o_counter"), ResolutionCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index b894628d8..f4be9f78a 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -5,6 +5,7 @@ import { NullNumberCriterionOption, createDateCriterionOption, createMandatoryTimestampCriterionOption, + createPathCriterionOption, } from "./criteria/criterion"; import { HasMarkersCriterionOption } from "./criteria/has-markers"; import { SceneIsMissingCriterionOption } from "./criteria/is-missing"; @@ -59,7 +60,7 @@ const displayModeOptions = [ const criterionOptions = [ createStringCriterionOption("title"), createStringCriterionOption("scene_code"), - createMandatoryStringCriterionOption("path"), + createPathCriterionOption("path"), createStringCriterionOption("details"), createStringCriterionOption("director"), createMandatoryStringCriterionOption("oshash", "media_info.hash"),