mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Add filtering to folder select (#4277)
This commit is contained in:
parent
61f4d8bd12
commit
a0f33e3dab
10 changed files with 120 additions and 85 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
)
|
||||
|
|
@ -25,13 +26,27 @@ func (s dirLister) Bytes(i int) []byte {
|
|||
// listDir will return the contents of a given directory path as a string slice
|
||||
func listDir(col *collate.Collator, path string) ([]string, error) {
|
||||
var dirPaths []string
|
||||
dirPath := path
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
path = filepath.Dir(path)
|
||||
files, err = os.ReadDir(path)
|
||||
dirPath = filepath.Dir(path)
|
||||
dirFiles, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return dirPaths, err
|
||||
}
|
||||
|
||||
// Filter dir contents by last path fragment if the dir isn't an exact match
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if base != "." && base != string(filepath.Separator) {
|
||||
for _, file := range dirFiles {
|
||||
if strings.HasPrefix(strings.ToLower(file.Name()), base) {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = dirFiles
|
||||
}
|
||||
}
|
||||
|
||||
if col != nil {
|
||||
|
|
@ -42,7 +57,7 @@ func listDir(col *collate.Collator, path string) ([]string, error) {
|
|||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
dirPaths = append(dirPaths, filepath.Join(path, file.Name()))
|
||||
dirPaths = append(dirPaths, filepath.Join(dirPath, file.Name()))
|
||||
}
|
||||
return dirPaths, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func getDir(path string) string {
|
|||
}
|
||||
|
||||
func getParent(path string) *string {
|
||||
isRoot := path[len(path)-1:] == "/"
|
||||
isRoot := path == "/"
|
||||
if isRoot {
|
||||
return nil
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ export const PathFilter: React.FC<IInputFilterProps> = ({
|
|||
) : (
|
||||
<FolderSelect
|
||||
currentDirectory={criterion.value ? criterion.value.toString() : ""}
|
||||
setCurrentDirectory={(v) => onValueChanged(v)}
|
||||
onChangeDirectory={onValueChanged}
|
||||
collapsible
|
||||
quoteSpaced
|
||||
quotePath
|
||||
hideError
|
||||
defaultDirectories={libraryPaths}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const CleanDialog: React.FC<ICleanDialog> = ({
|
|||
{pathSelection ? (
|
||||
<FolderSelect
|
||||
currentDirectory={currentDirectory}
|
||||
setCurrentDirectory={(v) => setCurrentDirectory(v)}
|
||||
onChangeDirectory={setCurrentDirectory}
|
||||
defaultDirectories={libraryPaths}
|
||||
appendButton={
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const DirectorySelectionDialog: React.FC<
|
|||
|
||||
<FolderSelect
|
||||
currentDirectory={currentDirectory}
|
||||
setCurrentDirectory={(v) => setCurrentDirectory(v)}
|
||||
onChangeDirectory={setCurrentDirectory}
|
||||
defaultDirectories={libraryPaths}
|
||||
appendButton={
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,89 +1,76 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, InputGroup, Form, Collapse } from "react-bootstrap";
|
||||
import { Icon } from "../Icon";
|
||||
import { LoadingIndicator } from "../LoadingIndicator";
|
||||
import { useDirectory } from "src/core/StashService";
|
||||
import { faEllipsis, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { useDirectoryPaths } from "./useDirectoryPaths";
|
||||
|
||||
interface IProps {
|
||||
currentDirectory: string;
|
||||
setCurrentDirectory: (value: string) => void;
|
||||
onChangeDirectory: (value: string) => void;
|
||||
defaultDirectories?: string[];
|
||||
appendButton?: JSX.Element;
|
||||
collapsible?: boolean;
|
||||
quoteSpaced?: boolean;
|
||||
quotePath?: boolean;
|
||||
hideError?: boolean;
|
||||
}
|
||||
|
||||
export const FolderSelect: React.FC<IProps> = ({
|
||||
currentDirectory,
|
||||
setCurrentDirectory,
|
||||
defaultDirectories,
|
||||
onChangeDirectory,
|
||||
defaultDirectories = [],
|
||||
appendButton,
|
||||
collapsible = false,
|
||||
quoteSpaced = false,
|
||||
quotePath = false,
|
||||
hideError = false,
|
||||
}) => {
|
||||
const [showBrowser, setShowBrowser] = React.useState(false);
|
||||
const [directory, setDirectory] = useState(currentDirectory);
|
||||
const intl = useIntl();
|
||||
const [showBrowser, setShowBrowser] = useState(false);
|
||||
const [path, setPath] = useState(currentDirectory);
|
||||
|
||||
const isQuoted =
|
||||
quoteSpaced && directory.startsWith('"') && directory.endsWith('"');
|
||||
const { data, error, loading } = useDirectory(
|
||||
isQuoted ? directory.slice(1, -1) : directory
|
||||
const normalizedPath = quotePath ? TextUtils.stripQuotes(path) : path;
|
||||
const { directories, parent, error, loading } = useDirectoryPaths(
|
||||
normalizedPath,
|
||||
hideError
|
||||
);
|
||||
|
||||
const intl = useIntl();
|
||||
const selectableDirectories =
|
||||
(currentDirectory ? directories : defaultDirectories) ?? defaultDirectories;
|
||||
|
||||
const defaultDirectoriesOrEmpty = defaultDirectories ?? [];
|
||||
|
||||
const selectableDirectories: string[] = currentDirectory
|
||||
? data?.directory.directories ??
|
||||
(error && hideError ? [] : defaultDirectoriesOrEmpty)
|
||||
: defaultDirectoriesOrEmpty;
|
||||
|
||||
const debouncedSetDirectory = useDebounce(setDirectory, 250);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDirectory !== directory) {
|
||||
debouncedSetDirectory(currentDirectory);
|
||||
}
|
||||
}, [currentDirectory, directory, debouncedSetDirectory]);
|
||||
const debouncedSetDirectory = useDebounce(setPath, 250);
|
||||
|
||||
function setInstant(value: string) {
|
||||
if (quoteSpaced && value.includes(" ")) {
|
||||
value = `"${value}"`;
|
||||
}
|
||||
|
||||
setCurrentDirectory(value);
|
||||
setDirectory(value);
|
||||
const normalizedValue =
|
||||
quotePath && value.includes(" ") ? TextUtils.addQuotes(value) : value;
|
||||
onChangeDirectory(normalizedValue);
|
||||
setPath(normalizedValue);
|
||||
}
|
||||
|
||||
function setDebounced(value: string) {
|
||||
setCurrentDirectory(value);
|
||||
onChangeDirectory(value);
|
||||
debouncedSetDirectory(value);
|
||||
}
|
||||
|
||||
function goUp() {
|
||||
if (defaultDirectories?.includes(currentDirectory)) {
|
||||
setInstant("");
|
||||
} else if (data?.directory.parent) {
|
||||
setInstant(data.directory.parent);
|
||||
} else if (parent) {
|
||||
setInstant(parent);
|
||||
}
|
||||
}
|
||||
|
||||
const topDirectory =
|
||||
currentDirectory && data?.directory?.parent ? (
|
||||
const topDirectory = currentDirectory && parent && (
|
||||
<li className="folder-list-parent folder-list-item">
|
||||
<Button variant="link" onClick={() => goUp()}>
|
||||
<Button variant="link" onClick={() => goUp()} disabled={loading}>
|
||||
<span>
|
||||
<FormattedMessage id="setup.folder.up_dir" />
|
||||
</span>
|
||||
</Button>
|
||||
</li>
|
||||
) : null;
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -91,16 +78,16 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||
<Form.Control
|
||||
className="btn-secondary"
|
||||
placeholder={intl.formatMessage({ id: "setup.folder.file_path" })}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange={(e) => {
|
||||
setDebounced(e.currentTarget.value);
|
||||
}}
|
||||
value={currentDirectory}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{appendButton ? (
|
||||
<InputGroup.Append>{appendButton}</InputGroup.Append>
|
||||
) : undefined}
|
||||
{collapsible ? (
|
||||
|
||||
{appendButton && <InputGroup.Append>{appendButton}</InputGroup.Append>}
|
||||
|
||||
{collapsible && (
|
||||
<InputGroup.Append>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -109,32 +96,37 @@ export const FolderSelect: React.FC<IProps> = ({
|
|||
<Icon icon={faEllipsis} />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
) : undefined}
|
||||
{!data || !data.directory || loading ? (
|
||||
)}
|
||||
|
||||
{(loading || error) && (
|
||||
<InputGroup.Append className="align-self-center">
|
||||
{loading ? (
|
||||
<LoadingIndicator inline small message="" />
|
||||
) : !hideError ? (
|
||||
<Icon icon={faTimes} color="red" className="ml-3" />
|
||||
) : undefined}
|
||||
) : (
|
||||
!hideError && <Icon icon={faTimes} color="red" className="ml-3" />
|
||||
)}
|
||||
</InputGroup.Append>
|
||||
) : undefined}
|
||||
)}
|
||||
</InputGroup>
|
||||
|
||||
{!hideError && error !== undefined && (
|
||||
<h5 className="mt-4 text-break">Error: {error.message}</h5>
|
||||
)}
|
||||
|
||||
<Collapse in={!collapsible || showBrowser}>
|
||||
<ul className="folder-list">
|
||||
{topDirectory}
|
||||
{selectableDirectories.map((path) => {
|
||||
return (
|
||||
<li key={path} className="folder-list-item">
|
||||
<Button variant="link" onClick={() => setInstant(path)}>
|
||||
<span>{path}</span>
|
||||
{selectableDirectories.map((dir) => (
|
||||
<li key={dir} className="folder-list-item">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => setInstant(dir)}
|
||||
disabled={loading}
|
||||
>
|
||||
<span>{dir}</span>
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</ul>
|
||||
</Collapse>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const FolderSelectDialog: React.FC<IProps> = ({
|
|||
<div className="dialog-content">
|
||||
<FolderSelect
|
||||
currentDirectory={currentDirectory}
|
||||
setCurrentDirectory={(v) => setCurrentDirectory(v)}
|
||||
onChangeDirectory={setCurrentDirectory}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { useRef } from "react";
|
||||
import { useDirectory } from "src/core/StashService";
|
||||
|
||||
export const useDirectoryPaths = (path: string, hideError: boolean) => {
|
||||
const { data, loading, error } = useDirectory(path);
|
||||
const prevData = useRef<typeof data | undefined>(undefined);
|
||||
|
||||
if (!loading) prevData.current = data;
|
||||
|
||||
const currentData = loading ? prevData.current : data;
|
||||
const directories =
|
||||
error && hideError ? [] : currentData?.directory.directories;
|
||||
const parent = currentData?.directory.parent;
|
||||
|
||||
return { directories, parent, loading, error };
|
||||
};
|
||||
|
|
@ -80,10 +80,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
margin-top: 0.5rem 0 0 0;
|
||||
max-height: 30vw;
|
||||
overflow-x: auto;
|
||||
// z-index gets set on button groups for some reason
|
||||
.multi-set .btn-group > button.btn {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
|
|
@ -92,14 +91,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
// z-index gets set on button groups for some reason
|
||||
.multi-set .btn-group > button.btn {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
max-height: 30vw;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
&-item {
|
||||
|
|
|
|||
|
|
@ -455,6 +455,19 @@ const abbreviateCounter = (counter: number = 0) => {
|
|||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Trims quotes if the text has leading/trailing quotes
|
||||
*/
|
||||
const stripQuotes = (text: string) => {
|
||||
if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1);
|
||||
return text;
|
||||
};
|
||||
|
||||
/*
|
||||
* Wraps string in quotes
|
||||
*/
|
||||
const addQuotes = (text: string) => `"${text}"`;
|
||||
|
||||
const TextUtils = {
|
||||
fileSize,
|
||||
formatFileSizeUnit,
|
||||
|
|
@ -478,6 +491,8 @@ const TextUtils = {
|
|||
formatDateTime,
|
||||
secondsAsTimeString,
|
||||
abbreviateCounter,
|
||||
stripQuotes,
|
||||
addQuotes,
|
||||
};
|
||||
|
||||
export default TextUtils;
|
||||
|
|
|
|||
Loading…
Reference in a new issue