mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add folder browser to path filter field (#3570)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
DateCriterion,
|
DateCriterion,
|
||||||
TimestampCriterion,
|
TimestampCriterion,
|
||||||
BooleanCriterion,
|
BooleanCriterion,
|
||||||
|
PathCriterionOption,
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +37,7 @@ import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
|||||||
import { RatingFilter } from "./Filters/RatingFilter";
|
import { RatingFilter } from "./Filters/RatingFilter";
|
||||||
import { BooleanFilter } from "./Filters/BooleanFilter";
|
import { BooleanFilter } from "./Filters/BooleanFilter";
|
||||||
import { OptionsListFilter } from "./Filters/OptionsListFilter";
|
import { OptionsListFilter } from "./Filters/OptionsListFilter";
|
||||||
|
import { PathFilter } from "./Filters/PathFilter";
|
||||||
|
|
||||||
interface IGenericCriterionEditor {
|
interface IGenericCriterionEditor {
|
||||||
criterion: Criterion<CriterionValue>;
|
criterion: Criterion<CriterionValue>;
|
||||||
@@ -137,6 +139,11 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
|||||||
// <OptionsFilter criterion={criterion} onValueChanged={onValueChanged} />
|
// <OptionsFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
if (criterion.criterionOption instanceof PathCriterionOption) {
|
||||||
|
return (
|
||||||
|
<PathFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
if (criterion instanceof DurationCriterion) {
|
if (criterion instanceof DurationCriterion) {
|
||||||
return (
|
return (
|
||||||
<DurationFilter criterion={criterion} onValueChanged={onValueChanged} />
|
<DurationFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
|||||||
32
ui/v2.5/src/components/List/Filters/PathFilter.tsx
Normal file
32
ui/v2.5/src/components/List/Filters/PathFilter.tsx
Normal file
@@ -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<CriterionValue>;
|
||||||
|
onValueChanged: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PathFilter: React.FC<IInputFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const libraryPaths = configuration?.general.stashes.map((s) => s.path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<FolderSelect
|
||||||
|
currentDirectory={criterion.value ? criterion.value.toString() : ""}
|
||||||
|
setCurrentDirectory={(v) => onValueChanged(v)}
|
||||||
|
collapsible
|
||||||
|
defaultDirectories={libraryPaths}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
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 { Icon } from "../Icon";
|
||||||
import { LoadingIndicator } from "../LoadingIndicator";
|
import { LoadingIndicator } from "../LoadingIndicator";
|
||||||
import { useDirectory } from "src/core/StashService";
|
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";
|
import { useDebouncedSetState } from "src/hooks/debounce";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -12,6 +12,7 @@ interface IProps {
|
|||||||
setCurrentDirectory: (value: string) => void;
|
setCurrentDirectory: (value: string) => void;
|
||||||
defaultDirectories?: string[];
|
defaultDirectories?: string[];
|
||||||
appendButton?: JSX.Element;
|
appendButton?: JSX.Element;
|
||||||
|
collapsible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FolderSelect: React.FC<IProps> = ({
|
export const FolderSelect: React.FC<IProps> = ({
|
||||||
@@ -19,7 +20,9 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||||||
setCurrentDirectory,
|
setCurrentDirectory,
|
||||||
defaultDirectories,
|
defaultDirectories,
|
||||||
appendButton,
|
appendButton,
|
||||||
|
collapsible = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [showBrowser, setShowBrowser] = React.useState(false);
|
||||||
const [directory, setDirectory] = useState(currentDirectory);
|
const [directory, setDirectory] = useState(currentDirectory);
|
||||||
const { data, error, loading } = useDirectory(directory);
|
const { data, error, loading } = useDirectory(directory);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -31,9 +34,10 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||||||
const debouncedSetDirectory = useDebouncedSetState(setDirectory, 250);
|
const debouncedSetDirectory = useDebouncedSetState(setDirectory, 250);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentDirectory === "" && !defaultDirectories && data?.directory.path)
|
if (currentDirectory !== directory) {
|
||||||
setCurrentDirectory(data.directory.path);
|
debouncedSetDirectory(currentDirectory);
|
||||||
}, [currentDirectory, setCurrentDirectory, data, defaultDirectories]);
|
}
|
||||||
|
}, [currentDirectory, directory, debouncedSetDirectory]);
|
||||||
|
|
||||||
function setInstant(value: string) {
|
function setInstant(value: string) {
|
||||||
setCurrentDirectory(value);
|
setCurrentDirectory(value);
|
||||||
@@ -66,6 +70,7 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||||||
<>
|
<>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
placeholder={intl.formatMessage({ id: "setup.folder.file_path" })}
|
placeholder={intl.formatMessage({ id: "setup.folder.file_path" })}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setDebounced(e.currentTarget.value);
|
setDebounced(e.currentTarget.value);
|
||||||
@@ -76,6 +81,16 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||||||
{appendButton ? (
|
{appendButton ? (
|
||||||
<InputGroup.Append>{appendButton}</InputGroup.Append>
|
<InputGroup.Append>{appendButton}</InputGroup.Append>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
{collapsible ? (
|
||||||
|
<InputGroup.Append>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setShowBrowser(!showBrowser)}
|
||||||
|
>
|
||||||
|
<Icon icon={faEllipsis} />
|
||||||
|
</Button>
|
||||||
|
</InputGroup.Append>
|
||||||
|
) : undefined}
|
||||||
{!data || !data.directory || loading ? (
|
{!data || !data.directory || loading ? (
|
||||||
<InputGroup.Append className="align-self-center">
|
<InputGroup.Append className="align-self-center">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -89,18 +104,20 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||||||
{error !== undefined && (
|
{error !== undefined && (
|
||||||
<h5 className="mt-4 text-break">Error: {error.message}</h5>
|
<h5 className="mt-4 text-break">Error: {error.message}</h5>
|
||||||
)}
|
)}
|
||||||
<ul className="folder-list">
|
<Collapse in={!collapsible || showBrowser}>
|
||||||
{topDirectory}
|
<ul className="folder-list">
|
||||||
{selectableDirectories.map((path) => {
|
{topDirectory}
|
||||||
return (
|
{selectableDirectories.map((path) => {
|
||||||
<li key={path} className="folder-list-item">
|
return (
|
||||||
<Button variant="link" onClick={() => setInstant(path)}>
|
<li key={path} className="folder-list-item">
|
||||||
{path}
|
<Button variant="link" onClick={() => setInstant(path)}>
|
||||||
</Button>
|
{path}
|
||||||
</li>
|
</Button>
|
||||||
);
|
</li>
|
||||||
})}
|
);
|
||||||
</ul>
|
})}
|
||||||
|
</ul>
|
||||||
|
</Collapse>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
export class BooleanCriterionOption extends CriterionOption {
|
||||||
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
||||||
super({
|
super({
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
DateCriterionOption,
|
DateCriterionOption,
|
||||||
TimestampCriterion,
|
TimestampCriterion,
|
||||||
MandatoryTimestampCriterionOption,
|
MandatoryTimestampCriterionOption,
|
||||||
|
PathCriterionOption,
|
||||||
} from "./criterion";
|
} from "./criterion";
|
||||||
import { OrganizedCriterion } from "./organized";
|
import { OrganizedCriterion } from "./organized";
|
||||||
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
|
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
|
||||||
@@ -65,9 +66,7 @@ export function makeCriteria(
|
|||||||
return new NoneCriterion();
|
return new NoneCriterion();
|
||||||
case "name":
|
case "name":
|
||||||
case "path":
|
case "path":
|
||||||
return new StringCriterion(
|
return new StringCriterion(new PathCriterionOption(type, type));
|
||||||
new MandatoryStringCriterionOption(type, type)
|
|
||||||
);
|
|
||||||
case "checksum":
|
case "checksum":
|
||||||
return new StringCriterion(
|
return new StringCriterion(
|
||||||
new MandatoryStringCriterionOption("media_info.checksum", type, type)
|
new MandatoryStringCriterionOption("media_info.checksum", type, type)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
NullNumberCriterionOption,
|
NullNumberCriterionOption,
|
||||||
createDateCriterionOption,
|
createDateCriterionOption,
|
||||||
createMandatoryTimestampCriterionOption,
|
createMandatoryTimestampCriterionOption,
|
||||||
|
createPathCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
|
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
@@ -43,7 +44,7 @@ const displayModeOptions = [
|
|||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
createStringCriterionOption("title"),
|
createStringCriterionOption("title"),
|
||||||
createStringCriterionOption("details"),
|
createStringCriterionOption("details"),
|
||||||
createStringCriterionOption("path"),
|
createPathCriterionOption("path"),
|
||||||
createStringCriterionOption(
|
createStringCriterionOption(
|
||||||
"galleryChecksum",
|
"galleryChecksum",
|
||||||
"media_info.checksum",
|
"media_info.checksum",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
NullNumberCriterionOption,
|
NullNumberCriterionOption,
|
||||||
createMandatoryTimestampCriterionOption,
|
createMandatoryTimestampCriterionOption,
|
||||||
createDateCriterionOption,
|
createDateCriterionOption,
|
||||||
|
createPathCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
@@ -33,7 +34,7 @@ const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
|||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
createStringCriterionOption("title"),
|
createStringCriterionOption("title"),
|
||||||
createMandatoryStringCriterionOption("checksum", "media_info.checksum"),
|
createMandatoryStringCriterionOption("checksum", "media_info.checksum"),
|
||||||
createMandatoryStringCriterionOption("path"),
|
createPathCriterionOption("path"),
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
createMandatoryNumberCriterionOption("o_counter"),
|
createMandatoryNumberCriterionOption("o_counter"),
|
||||||
ResolutionCriterionOption,
|
ResolutionCriterionOption,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
NullNumberCriterionOption,
|
NullNumberCriterionOption,
|
||||||
createDateCriterionOption,
|
createDateCriterionOption,
|
||||||
createMandatoryTimestampCriterionOption,
|
createMandatoryTimestampCriterionOption,
|
||||||
|
createPathCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
import { HasMarkersCriterionOption } from "./criteria/has-markers";
|
import { HasMarkersCriterionOption } from "./criteria/has-markers";
|
||||||
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
|
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
@@ -59,7 +60,7 @@ const displayModeOptions = [
|
|||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
createStringCriterionOption("title"),
|
createStringCriterionOption("title"),
|
||||||
createStringCriterionOption("scene_code"),
|
createStringCriterionOption("scene_code"),
|
||||||
createMandatoryStringCriterionOption("path"),
|
createPathCriterionOption("path"),
|
||||||
createStringCriterionOption("details"),
|
createStringCriterionOption("details"),
|
||||||
createStringCriterionOption("director"),
|
createStringCriterionOption("director"),
|
||||||
createMandatoryStringCriterionOption("oshash", "media_info.hash"),
|
createMandatoryStringCriterionOption("oshash", "media_info.hash"),
|
||||||
|
|||||||
Reference in New Issue
Block a user