mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Changes
This commit is contained in:
@@ -74,9 +74,9 @@
|
||||
except: ["after-single-line-comment", "first-nested" ],
|
||||
ignore: ["after-comment"],
|
||||
}],
|
||||
"selector-max-id": 0,
|
||||
"selector-max-type": 1,
|
||||
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z]+)*$",
|
||||
"selector-max-id": 1,
|
||||
"selector-max-type": 2,
|
||||
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-list-comma-newline-after": "always",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Settings } from "./components/Settings/Settings";
|
||||
import { Stats } from "./components/Stats";
|
||||
import Studios from "./components/Studios/Studios";
|
||||
import { TagList } from "./components/Tags/TagList";
|
||||
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
|
||||
import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser";
|
||||
|
||||
library.add(fas);
|
||||
|
||||
|
||||
67
ui/v2.5/src/components/SceneFilenameParser/ParserField.ts
Normal file
67
ui/v2.5/src/components/SceneFilenameParser/ParserField.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
export class ParserField {
|
||||
public field: string;
|
||||
public helperText?: string;
|
||||
|
||||
constructor(field: string, helperText?: string) {
|
||||
this.field = field;
|
||||
this.helperText = helperText;
|
||||
}
|
||||
|
||||
public getFieldPattern() {
|
||||
return `{${this.field}}`;
|
||||
}
|
||||
|
||||
static Title = new ParserField("title");
|
||||
static Ext = new ParserField("ext", "File extension");
|
||||
|
||||
static I = new ParserField("i", "Matches any ignored word");
|
||||
static D = new ParserField("d", "Matches any delimiter (.-_)");
|
||||
|
||||
static Performer = new ParserField("performer");
|
||||
static Studio = new ParserField("studio");
|
||||
static Tag = new ParserField("tag");
|
||||
|
||||
// date fields
|
||||
static Date = new ParserField("date", "YYYY-MM-DD");
|
||||
static YYYY = new ParserField("yyyy", "Year");
|
||||
static YY = new ParserField("yy", "Year (20YY)");
|
||||
static MM = new ParserField("mm", "Two digit month");
|
||||
static DD = new ParserField("dd", "Two digit date");
|
||||
static YYYYMMDD = new ParserField("yyyymmdd");
|
||||
static YYMMDD = new ParserField("yymmdd");
|
||||
static DDMMYYYY = new ParserField("ddmmyyyy");
|
||||
static DDMMYY = new ParserField("ddmmyy");
|
||||
static MMDDYYYY = new ParserField("mmddyyyy");
|
||||
static MMDDYY = new ParserField("mmddyy");
|
||||
|
||||
static validFields = [
|
||||
ParserField.Title,
|
||||
ParserField.Ext,
|
||||
ParserField.D,
|
||||
ParserField.I,
|
||||
ParserField.Performer,
|
||||
ParserField.Studio,
|
||||
ParserField.Tag,
|
||||
ParserField.Date,
|
||||
ParserField.YYYY,
|
||||
ParserField.YY,
|
||||
ParserField.MM,
|
||||
ParserField.DD,
|
||||
ParserField.YYYYMMDD,
|
||||
ParserField.YYMMDD,
|
||||
ParserField.DDMMYYYY,
|
||||
ParserField.DDMMYY,
|
||||
ParserField.MMDDYYYY,
|
||||
ParserField.MMDDYY
|
||||
];
|
||||
|
||||
static fullDateFields = [
|
||||
ParserField.YYYYMMDD,
|
||||
ParserField.YYMMDD,
|
||||
ParserField.DDMMYYYY,
|
||||
ParserField.DDMMYY,
|
||||
ParserField.MMDDYYYY,
|
||||
ParserField.MMDDYY
|
||||
];
|
||||
}
|
||||
221
ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx
Normal file
221
ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Form,
|
||||
InputGroup,
|
||||
} from 'react-bootstrap';
|
||||
import { ParserField } from './ParserField';
|
||||
import { ShowFields } from './ShowFields';
|
||||
|
||||
const builtInRecipes = [
|
||||
{
|
||||
pattern: "{title}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: "",
|
||||
capitalizeTitle: false,
|
||||
description: "Filename"
|
||||
},
|
||||
{
|
||||
pattern: "{title}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: "",
|
||||
capitalizeTitle: false,
|
||||
description: "Without extension"
|
||||
},
|
||||
{
|
||||
pattern: "{}.{yy}.{mm}.{dd}.{title}.XXX.{}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
pattern: "{}.{yy}.{mm}.{dd}.{title}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
pattern: "{title}.XXX.{}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
pattern: "{}.{yy}.{mm}.{dd}.{title}.{i}.{ext}",
|
||||
ignoreWords: ["cz", "fr"],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: "Foreign language"
|
||||
}
|
||||
];
|
||||
|
||||
export interface IParserInput {
|
||||
pattern: string;
|
||||
ignoreWords: string[];
|
||||
whitespaceCharacters: string;
|
||||
capitalizeTitle: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
findClicked: boolean;
|
||||
}
|
||||
|
||||
interface IParserRecipe {
|
||||
pattern: string;
|
||||
ignoreWords: string[];
|
||||
whitespaceCharacters: string;
|
||||
capitalizeTitle: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface IParserInputProps {
|
||||
input: IParserInput;
|
||||
onFind: (input: IParserInput) => void;
|
||||
onPageSizeChanged: (newSize: number) => void;
|
||||
showFields: Map<string, boolean>;
|
||||
setShowFields: (fields: Map<string, boolean>) => void;
|
||||
}
|
||||
|
||||
export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProps) => {
|
||||
const [pattern, setPattern] = useState<string>(props.input.pattern);
|
||||
const [ignoreWords, setIgnoreWords] = useState<string>(
|
||||
props.input.ignoreWords.join(" ")
|
||||
);
|
||||
const [whitespaceCharacters, setWhitespaceCharacters] = useState<string>(
|
||||
props.input.whitespaceCharacters
|
||||
);
|
||||
const [capitalizeTitle, setCapitalizeTitle] = useState<boolean>(
|
||||
props.input.capitalizeTitle
|
||||
);
|
||||
|
||||
function onFind() {
|
||||
props.onFind({
|
||||
pattern,
|
||||
ignoreWords: ignoreWords.split(" "),
|
||||
whitespaceCharacters,
|
||||
capitalizeTitle,
|
||||
page: 1,
|
||||
pageSize: props.input.pageSize,
|
||||
findClicked: props.input.findClicked
|
||||
});
|
||||
}
|
||||
|
||||
function setParserRecipe(recipe: IParserRecipe) {
|
||||
setPattern(recipe.pattern);
|
||||
setIgnoreWords(recipe.ignoreWords.join(" "));
|
||||
setWhitespaceCharacters(recipe.whitespaceCharacters);
|
||||
setCapitalizeTitle(recipe.capitalizeTitle);
|
||||
}
|
||||
|
||||
const validFields = [new ParserField("", "Wildcard")].concat(
|
||||
ParserField.validFields
|
||||
);
|
||||
|
||||
function addParserField(field: ParserField) {
|
||||
setPattern(pattern + field.getFieldPattern());
|
||||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Group className="row">
|
||||
<Form.Label htmlFor="filename-pattern" className="col-2">Filename Pattern</Form.Label>
|
||||
<InputGroup className="col-8">
|
||||
<Form.Control
|
||||
id="filename-pattern"
|
||||
onChange={(newValue: any) => setPattern(newValue.target.value)}
|
||||
value={pattern}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
<DropdownButton id="parser-field-select" title="Add Field">
|
||||
{validFields.map(item => (
|
||||
<Dropdown.Item onSelect={() => addParserField(item)}>
|
||||
<span>{item.field}</span>
|
||||
<span className="ml-auto">{item.helperText}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
<Form.Text className="text-muted row col-10 offset-2">Use '\\' to escape literal {} characters</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="row" controlId="ignored-words">
|
||||
<Form.Label className="col-2">Ignored words</Form.Label>
|
||||
<Form.Control
|
||||
className="col-8"
|
||||
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}
|
||||
value={ignoreWords}
|
||||
/>
|
||||
<Form.Text className="text-muted col-10 offset-2">Matches with {"{i}"}</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<h5>Title</h5>
|
||||
<Form.Group className="row">
|
||||
<Form.Label htmlFor="whitespace-characters" className="col-2">Whitespace characters:</Form.Label>
|
||||
<Form.Control
|
||||
className="col-8"
|
||||
onChange={(newValue: any) =>
|
||||
setWhitespaceCharacters(newValue.target.value)
|
||||
}
|
||||
value={whitespaceCharacters}
|
||||
/>
|
||||
<Form.Text className="text-muted col-10 offset-2">
|
||||
These characters will be replaced with whitespace in the title
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group className="row">
|
||||
<Form.Label htmlFor="capitalize-title" className="col-2">Capitalize title</Form.Label>
|
||||
<Form.Control
|
||||
className="col-8"
|
||||
type="checkbox"
|
||||
checked={capitalizeTitle}
|
||||
onChange={() => setCapitalizeTitle(!capitalizeTitle)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{/* TODO - mapping stuff will go here */}
|
||||
|
||||
<Form.Group>
|
||||
<DropdownButton variant="secondary" id="recipe-select" title="Select Parser Recipe">
|
||||
{builtInRecipes.map(item => (
|
||||
<Dropdown.Item onSelect={() => setParserRecipe(item)}>
|
||||
<span>{item.pattern}</span>
|
||||
<span className="mr-auto">{item.description}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<ShowFields
|
||||
fields={props.showFields}
|
||||
onShowFieldsChanged={fields => props.setShowFields(fields)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="row">
|
||||
<Button variant="secondary" className="col-1" onClick={onFind}>Find</Button>
|
||||
<Form.Control
|
||||
as="select"
|
||||
style={{ flexBasis: "min-content" }}
|
||||
options={PAGE_SIZE_OPTIONS}
|
||||
onChange={(event: any) =>
|
||||
props.onPageSizeChanged(parseInt(event.target.value, 10))
|
||||
}
|
||||
defaultValue={props.input.pageSize}
|
||||
className="col-1 filter-item"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map(val => (
|
||||
<option value="val">{val}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
@@ -5,19 +5,18 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Form,
|
||||
Table
|
||||
} from "react-bootstrap";
|
||||
import _ from "lodash";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FilterSelect, Icon, StudioSelect, LoadingIndicator } from "src/components/Shared";
|
||||
import { FilterSelect, StudioSelect, LoadingIndicator } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks";
|
||||
import { Pagination } from "../list/Pagination";
|
||||
import { IParserInput, ParserInput } from './ParserInput';
|
||||
import { ParserField } from './ParserField';
|
||||
|
||||
class ParserResult<T> {
|
||||
public value: GQL.Maybe<T> = null;
|
||||
@@ -37,72 +36,6 @@ class ParserResult<T> {
|
||||
}
|
||||
}
|
||||
|
||||
class ParserField {
|
||||
public field: string;
|
||||
public helperText?: string;
|
||||
|
||||
constructor(field: string, helperText?: string) {
|
||||
this.field = field;
|
||||
this.helperText = helperText;
|
||||
}
|
||||
|
||||
public getFieldPattern() {
|
||||
return `{${this.field}}`;
|
||||
}
|
||||
|
||||
static Title = new ParserField("title");
|
||||
static Ext = new ParserField("ext", "File extension");
|
||||
|
||||
static I = new ParserField("i", "Matches any ignored word");
|
||||
static D = new ParserField("d", "Matches any delimiter (.-_)");
|
||||
|
||||
static Performer = new ParserField("performer");
|
||||
static Studio = new ParserField("studio");
|
||||
static Tag = new ParserField("tag");
|
||||
|
||||
// date fields
|
||||
static Date = new ParserField("date", "YYYY-MM-DD");
|
||||
static YYYY = new ParserField("yyyy", "Year");
|
||||
static YY = new ParserField("yy", "Year (20YY)");
|
||||
static MM = new ParserField("mm", "Two digit month");
|
||||
static DD = new ParserField("dd", "Two digit date");
|
||||
static YYYYMMDD = new ParserField("yyyymmdd");
|
||||
static YYMMDD = new ParserField("yymmdd");
|
||||
static DDMMYYYY = new ParserField("ddmmyyyy");
|
||||
static DDMMYY = new ParserField("ddmmyy");
|
||||
static MMDDYYYY = new ParserField("mmddyyyy");
|
||||
static MMDDYY = new ParserField("mmddyy");
|
||||
|
||||
static validFields = [
|
||||
ParserField.Title,
|
||||
ParserField.Ext,
|
||||
ParserField.D,
|
||||
ParserField.I,
|
||||
ParserField.Performer,
|
||||
ParserField.Studio,
|
||||
ParserField.Tag,
|
||||
ParserField.Date,
|
||||
ParserField.YYYY,
|
||||
ParserField.YY,
|
||||
ParserField.MM,
|
||||
ParserField.DD,
|
||||
ParserField.YYYYMMDD,
|
||||
ParserField.YYMMDD,
|
||||
ParserField.DDMMYYYY,
|
||||
ParserField.DDMMYY,
|
||||
ParserField.MMDDYYYY,
|
||||
ParserField.MMDDYY
|
||||
];
|
||||
|
||||
static fullDateFields = [
|
||||
ParserField.YYYYMMDD,
|
||||
ParserField.YYMMDD,
|
||||
ParserField.DDMMYYYY,
|
||||
ParserField.DDMMYY,
|
||||
ParserField.MMDDYYYY,
|
||||
ParserField.MMDDYY
|
||||
];
|
||||
}
|
||||
class SceneParserResult {
|
||||
public id: string;
|
||||
public filename: string;
|
||||
@@ -219,69 +152,6 @@ class SceneParserResult {
|
||||
}
|
||||
}
|
||||
|
||||
interface IParserInput {
|
||||
pattern: string;
|
||||
ignoreWords: string[];
|
||||
whitespaceCharacters: string;
|
||||
capitalizeTitle: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
findClicked: boolean;
|
||||
}
|
||||
|
||||
interface IParserRecipe {
|
||||
pattern: string;
|
||||
ignoreWords: string[];
|
||||
whitespaceCharacters: string;
|
||||
capitalizeTitle: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const builtInRecipes = [
|
||||
{
|
||||
pattern: "{title}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: "",
|
||||
capitalizeTitle: false,
|
||||
description: "Filename"
|
||||
},
|
||||
{
|
||||
pattern: "{title}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: "",
|
||||
capitalizeTitle: false,
|
||||
description: "Without extension"
|
||||
},
|
||||
{
|
||||
pattern: "{}.{yy}.{mm}.{dd}.{title}.XXX.{}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
pattern: "{}.{yy}.{mm}.{dd}.{title}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
pattern: "{title}.XXX.{}.{ext}",
|
||||
ignoreWords: [],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
pattern: "{}.{yy}.{mm}.{dd}.{title}.{i}.{ext}",
|
||||
ignoreWords: ["cz", "fr"],
|
||||
whitespaceCharacters: ".",
|
||||
capitalizeTitle: true,
|
||||
description: "Foreign language"
|
||||
}
|
||||
];
|
||||
|
||||
const initialParserInput = {
|
||||
pattern: "{title}.{ext}",
|
||||
ignoreWords: [],
|
||||
@@ -518,181 +388,6 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setAllStudioSet(selected);
|
||||
}
|
||||
|
||||
interface IShowFieldsProps {
|
||||
fields: Map<string, boolean>;
|
||||
onShowFieldsChanged: (fields: Map<string, boolean>) => void;
|
||||
}
|
||||
|
||||
function ShowFields(props: IShowFieldsProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
function handleClick(label: string) {
|
||||
const copy = new Map<string, boolean>(props.fields);
|
||||
copy.set(label, !props.fields.get(label));
|
||||
props.onShowFieldsChanged(copy);
|
||||
}
|
||||
|
||||
const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (
|
||||
<div
|
||||
key={label}
|
||||
onClick={() => {
|
||||
handleClick(label);
|
||||
}}
|
||||
>
|
||||
<Icon icon={enabled ? "check" : "times"} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => setOpen(!open)}>
|
||||
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
||||
<span>Display fields</span>
|
||||
</div>
|
||||
<Collapse in={open}>
|
||||
<div>{fieldRows}</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface IParserInputProps {
|
||||
input: IParserInput;
|
||||
onFind: (input: IParserInput) => void;
|
||||
}
|
||||
|
||||
function ParserInput(props: IParserInputProps) {
|
||||
const [pattern, setPattern] = useState<string>(props.input.pattern);
|
||||
const [ignoreWords, setIgnoreWords] = useState<string>(
|
||||
props.input.ignoreWords.join(" ")
|
||||
);
|
||||
const [whitespaceCharacters, setWhitespaceCharacters] = useState<string>(
|
||||
props.input.whitespaceCharacters
|
||||
);
|
||||
const [capitalizeTitle, setCapitalizeTitle] = useState<boolean>(
|
||||
props.input.capitalizeTitle
|
||||
);
|
||||
|
||||
function onFind() {
|
||||
props.onFind({
|
||||
pattern,
|
||||
ignoreWords: ignoreWords.split(" "),
|
||||
whitespaceCharacters,
|
||||
capitalizeTitle,
|
||||
page: 1,
|
||||
pageSize: props.input.pageSize,
|
||||
findClicked: props.input.findClicked
|
||||
});
|
||||
}
|
||||
|
||||
function setParserRecipe(recipe: IParserRecipe) {
|
||||
setPattern(recipe.pattern);
|
||||
setIgnoreWords(recipe.ignoreWords.join(" "));
|
||||
setWhitespaceCharacters(recipe.whitespaceCharacters);
|
||||
setCapitalizeTitle(recipe.capitalizeTitle);
|
||||
}
|
||||
|
||||
const validFields = [new ParserField("", "Wildcard")].concat(
|
||||
ParserField.validFields
|
||||
);
|
||||
|
||||
function addParserField(field: ParserField) {
|
||||
setPattern(pattern + field.getFieldPattern());
|
||||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
onChange={(newValue: any) => setPattern(newValue.target.value)}
|
||||
value={pattern}
|
||||
/>
|
||||
<DropdownButton id="parser-field-select" title="Add Field">
|
||||
{validFields.map(item => (
|
||||
<Dropdown.Item onSelect={() => addParserField(item)}>
|
||||
<span>{item.field}</span>
|
||||
<span className="ml-auto">{item.helperText}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
<div>Use '\\' to escape literal {} characters</div>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Ignored words::</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}
|
||||
value={ignoreWords}
|
||||
/>
|
||||
<div>Matches with {"{i}"}</div>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Title</h5>
|
||||
<Form.Label>Whitespace characters:</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(newValue: any) =>
|
||||
setWhitespaceCharacters(newValue.target.value)
|
||||
}
|
||||
value={whitespaceCharacters}
|
||||
/>
|
||||
<Form.Group>
|
||||
<Form.Label>Capitalize title</Form.Label>
|
||||
<Form.Control
|
||||
type="checkbox"
|
||||
checked={capitalizeTitle}
|
||||
onChange={() => setCapitalizeTitle(!capitalizeTitle)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<div>
|
||||
These characters will be replaced with whitespace in the title
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
{/* TODO - mapping stuff will go here */}
|
||||
|
||||
<Form.Group>
|
||||
<DropdownButton id="recipe-select" title="Select Parser Recipe">
|
||||
{builtInRecipes.map(item => (
|
||||
<Dropdown.Item onSelect={() => setParserRecipe(item)}>
|
||||
<span>{item.pattern}</span>
|
||||
<span className="mr-auto">{item.description}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<ShowFields
|
||||
fields={showFields}
|
||||
onShowFieldsChanged={fields => setShowFields(fields)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Button onClick={onFind}>Find</Button>
|
||||
<Form.Control
|
||||
as="select"
|
||||
style={{ flexBasis: "min-content" }}
|
||||
options={PAGE_SIZE_OPTIONS}
|
||||
onChange={(event: any) =>
|
||||
onPageSizeChanged(parseInt(event.target.value, 10))
|
||||
}
|
||||
defaultValue={props.input.pageSize}
|
||||
className="filter-item"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map(val => (
|
||||
<option value="val">{val}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
interface ISceneParserFieldProps {
|
||||
parserResult: ParserResult<any>;
|
||||
className?: string;
|
||||
@@ -1062,7 +757,13 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
return (
|
||||
<Card id="parser-container">
|
||||
<h4>Scene Filename Parser</h4>
|
||||
<ParserInput input={parserInput} onFind={input => onFindClicked(input)} />
|
||||
<ParserInput
|
||||
input={parserInput}
|
||||
onFind={input => onFindClicked(input)}
|
||||
onPageSizeChanged={onPageSizeChanged}
|
||||
showFields={showFields}
|
||||
setShowFields={setShowFields}
|
||||
/>
|
||||
|
||||
{isLoading && <LoadingIndicator />}
|
||||
{renderTable()}
|
||||
45
ui/v2.5/src/components/SceneFilenameParser/ShowFields.tsx
Normal file
45
ui/v2.5/src/components/SceneFilenameParser/ShowFields.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Collapse
|
||||
} from 'react-bootstrap';
|
||||
import { Icon } from 'src/components/Shared';
|
||||
|
||||
interface IShowFieldsProps {
|
||||
fields: Map<string, boolean>;
|
||||
onShowFieldsChanged: (fields: Map<string, boolean>) => void;
|
||||
}
|
||||
|
||||
export const ShowFields = (props: IShowFieldsProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
function handleClick(label: string) {
|
||||
const copy = new Map<string, boolean>(props.fields);
|
||||
copy.set(label, !props.fields.get(label));
|
||||
props.onShowFieldsChanged(copy);
|
||||
}
|
||||
|
||||
const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (
|
||||
<div
|
||||
key={label}
|
||||
onClick={() => {
|
||||
handleClick(label);
|
||||
}}
|
||||
>
|
||||
<Icon icon={enabled ? "check" : "times"} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => setOpen(!open)} className="minimal">
|
||||
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
||||
<span>Display fields</span>
|
||||
</Button>
|
||||
<Collapse in={open}>
|
||||
<div>{fieldRows}</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,9 +62,10 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group className={props.className}>
|
||||
<Form.Group className={`duration-input ${props.className}`}>
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
className="duration-control"
|
||||
disabled={props.disabled}
|
||||
value={value}
|
||||
onChange={(e: any) => setValue(e.target.value)}
|
||||
|
||||
@@ -239,9 +239,11 @@ export const TagSelect: React.FC<IFilterProps> = props => {
|
||||
const Toast = useToast();
|
||||
const placeholder = props.noSelectionString ?? "Select tags...";
|
||||
|
||||
const selectedTags = props.ids ?? selectedIds;
|
||||
|
||||
const tags = data?.allTags ?? [];
|
||||
const selected = tags
|
||||
.filter(tag => selectedIds.indexOf(tag.id) !== -1)
|
||||
.filter(tag => selectedTags.indexOf(tag.id) !== -1)
|
||||
.map(tag => ({ value: tag.id, label: tag.name }));
|
||||
const items: Option[] = tags.map(item => ({
|
||||
value: item.id,
|
||||
|
||||
@@ -40,14 +40,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.duration-input {
|
||||
.duration-control {
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
.duration-button {
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
line-height: 10px;
|
||||
margin-left: 0 !important;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
|
||||
.btn + .btn {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
margin-top: .5rem 0 0 0;
|
||||
max-height: 30vw;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
#tag-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -24,5 +23,3 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -85,17 +85,19 @@
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.wall-item video,
|
||||
.wall-item img {
|
||||
.wall {
|
||||
.wall-item {
|
||||
line-height: 0;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 20%;
|
||||
|
||||
video,
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wall-item {
|
||||
line-height: 0;
|
||||
overflow: visible;
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
if (props.onChangeZoom) {
|
||||
return (
|
||||
<Form.Control
|
||||
className="zoom-slider"
|
||||
className="zoom-slider col-1"
|
||||
type="range"
|
||||
min={0}
|
||||
max={3}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.performer.image {
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
height: 50vh;
|
||||
min-height: 400px;
|
||||
}
|
||||
@@ -20,10 +20,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#url-field {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.scrape-url-button {
|
||||
color: $text-color;
|
||||
float: right;
|
||||
@@ -38,6 +34,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#url-field {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#performer-page {
|
||||
flex-direction: row;
|
||||
margin: 10px auto;
|
||||
@@ -57,11 +57,11 @@
|
||||
margin-left: 10px;
|
||||
|
||||
.not-favorite {
|
||||
color: rgba(191, 204, 214, .5) !important;
|
||||
color: rgba(191, 204, 214, .5);
|
||||
}
|
||||
|
||||
.favorite {
|
||||
color: #ff7373 !important;
|
||||
color: #ff7373;
|
||||
}
|
||||
|
||||
.link {
|
||||
|
||||
@@ -93,7 +93,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="tag" />
|
||||
{props.scene.tags.length}
|
||||
<span>{props.scene.tags.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
@@ -117,7 +117,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="user" />
|
||||
{props.scene.performers.length}
|
||||
<span>{props.scene.performers.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
@@ -135,7 +135,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal">
|
||||
<Icon icon="map-marker-alt" />
|
||||
{props.scene.scene_markers.length}
|
||||
<span>{props.scene.scene_markers.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
|
||||
@@ -128,4 +128,3 @@
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -180,58 +180,50 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||
return ret;
|
||||
}
|
||||
|
||||
function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) {
|
||||
useEffect(() => {
|
||||
const state = props.selected;
|
||||
let updateRating = "";
|
||||
let updateStudioId: string|undefined;
|
||||
let updateStudioID: string|undefined;
|
||||
let updatePerformerIds: string[] = [];
|
||||
let updateTagIds: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
const thisRating = scene.rating?.toString() ?? "";
|
||||
const thisStudio = scene?.studio?.id;
|
||||
const sceneRating = scene.rating?.toString() ?? "";
|
||||
const sceneStudioID = scene?.studio?.id;
|
||||
const scenePerformerIDs = (scene.performers ?? []).map(p => p.id).sort();
|
||||
const sceneTagIDs = (scene.tags ?? []).map(p => p.id).sort();
|
||||
|
||||
if (first) {
|
||||
updateRating = thisRating;
|
||||
updateStudioId = thisStudio;
|
||||
updatePerformerIds = scene.performers
|
||||
? scene.performers.map(p => p.id).sort()
|
||||
: [];
|
||||
updateTagIds = scene.tags ? scene.tags.map(p => p.id).sort() : [];
|
||||
updateRating = sceneRating;
|
||||
updateStudioID = sceneStudioID;
|
||||
updatePerformerIds = scenePerformerIDs;
|
||||
updateTagIds = sceneTagIDs;
|
||||
first = false;
|
||||
} else {
|
||||
if (rating !== thisRating) {
|
||||
if (sceneRating !== updateRating) {
|
||||
updateRating = "";
|
||||
}
|
||||
if (studioId !== thisStudio) {
|
||||
updateStudioId = undefined;
|
||||
if (sceneStudioID !== updateStudioID) {
|
||||
updateStudioID = undefined;
|
||||
}
|
||||
const perfIds = scene.performers
|
||||
? scene.performers.map(p => p.id).sort()
|
||||
: [];
|
||||
const tIds = scene.tags ? scene.tags.map(t => t.id).sort() : [];
|
||||
|
||||
if (!_.isEqual(performerIds, perfIds)) {
|
||||
if (!_.isEqual(scenePerformerIDs, updatePerformerIds)) {
|
||||
updatePerformerIds = [];
|
||||
}
|
||||
|
||||
if (!_.isEqual(tagIds, tIds)) {
|
||||
if (!_.isEqual(sceneTagIDs, updateTagIds)) {
|
||||
updateTagIds = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setRating(updateRating);
|
||||
setStudioId(updateStudioId);
|
||||
setStudioId(updateStudioID);
|
||||
setPerformerIds(updatePerformerIds);
|
||||
setTagIds(updateTagIds);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateScenesEditState(props.selected);
|
||||
setIsLoading(false);
|
||||
}, [props.selected]);
|
||||
|
||||
|
||||
function renderMultiSelect(
|
||||
type: "performers" | "tags",
|
||||
ids: string[] | undefined
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#details {
|
||||
min-height: 150px;
|
||||
}
|
||||
@@ -104,4 +105,3 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
@import "styles/theme";
|
||||
@import "styles/range";
|
||||
@import "styles/scrollbars";
|
||||
@import "styles/variables";
|
||||
@import "./components/**/*.scss";
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
"Fira Sans",
|
||||
"Droid Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0;
|
||||
@@ -70,6 +57,7 @@ code {
|
||||
max-height: 11.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.zoom-1 {
|
||||
width: 20rem;
|
||||
|
||||
@@ -81,6 +69,7 @@ code {
|
||||
height: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.zoom-2 {
|
||||
width: 30rem;
|
||||
|
||||
@@ -92,6 +81,7 @@ code {
|
||||
height: 22.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.zoom-3 {
|
||||
width: 40rem;
|
||||
|
||||
@@ -156,7 +146,7 @@ video.preview.portrait {
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
background-color: #bfccd6;
|
||||
background-color: $muted-gray;
|
||||
color: #182026;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
@@ -214,7 +204,7 @@ video.preview.portrait {
|
||||
}
|
||||
|
||||
.rating-5 {
|
||||
background: #FF2F39;
|
||||
background: #ff2f39;
|
||||
}
|
||||
|
||||
.rating-4 {
|
||||
@@ -224,6 +214,7 @@ video.preview.portrait {
|
||||
.rating-3 {
|
||||
background: $orange1;
|
||||
}
|
||||
|
||||
.rating-2 {
|
||||
background: $sepia1;
|
||||
}
|
||||
@@ -250,7 +241,7 @@ video.preview.portrait {
|
||||
|
||||
.scene-specs-overlay {
|
||||
bottom: 1rem;
|
||||
color: #f5f8fa;
|
||||
color: $text-color;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
letter-spacing: -.03rem;
|
||||
@@ -274,7 +265,7 @@ video.preview.portrait {
|
||||
background-position: right top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
color: #f5f8fa;
|
||||
color: $text-color;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
letter-spacing: -.03rem;
|
||||
@@ -332,20 +323,13 @@ video.preview.portrait {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.studio {
|
||||
.image {
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: contain !important;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.react-photo-gallery--gallery {
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
|
||||
#parser-container {
|
||||
margin: 10px auto;
|
||||
@@ -413,11 +397,11 @@ video.preview.portrait {
|
||||
}
|
||||
|
||||
.main {
|
||||
color: #f5f8fa;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.table {
|
||||
color: #f5f8fa;
|
||||
color: $text-color;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
@@ -425,7 +409,7 @@ video.preview.portrait {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-child(odd) td {
|
||||
.table-striped tr:nth-child(odd) td {
|
||||
background: rgba(92, 112, 128, .15);
|
||||
}
|
||||
|
||||
@@ -455,7 +439,7 @@ video.preview.portrait {
|
||||
.button-link {
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
color: #48aff0;
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
padding: 0;
|
||||
@@ -467,7 +451,7 @@ video.preview.portrait {
|
||||
|
||||
.scrubber-button {
|
||||
background-color: transparent;
|
||||
color: #48aff0;
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
.scrubber-button:hover {
|
||||
@@ -532,6 +516,7 @@ video.preview.portrait {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.svg-inline--fa {
|
||||
margin: 0 .4rem;
|
||||
}
|
||||
@@ -541,3 +526,4 @@ video.preview.portrait {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* Blueprint dark theme */
|
||||
|
||||
$secondary: #394b59;
|
||||
$muted-gray: #bfccd6;
|
||||
|
||||
$theme-colors: (
|
||||
primary: #137cbd,
|
||||
@@ -13,7 +14,7 @@ $theme-colors: (
|
||||
);
|
||||
|
||||
$body-bg: #202b33;
|
||||
$text-muted: #bfccd6;
|
||||
$text-muted: $muted-gray;
|
||||
$link-color: #48aff0;
|
||||
$link-hover-color: #48aff0;
|
||||
$text-color: #f5f8fa;
|
||||
@@ -23,10 +24,19 @@ $popover-bg: $secondary;
|
||||
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
$red1: #a82a2a;
|
||||
$orange1: #a66321;
|
||||
$sepia1: #63411e;
|
||||
$dark-gray2: #202b33;
|
||||
$dark-gray5: #394b59;
|
||||
|
||||
$pt-grid-size: 10px;
|
||||
$pt-navbar-height: 4rem;
|
||||
|
||||
.btn.active:not(.disabled),
|
||||
.btn.active.minimal:not(.disabled) {
|
||||
background-color: rgba(138, 155, 168, .3);
|
||||
color: #f5f8fa;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
a.minimal,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
$red1: #a82a2a;
|
||||
$orange1: #a66321;
|
||||
$sepia1: #63411e;
|
||||
$dark-gray2: #202b33;
|
||||
$dark-gray5: #394b59;
|
||||
|
||||
$pt-grid-size: 10px;
|
||||
$pt-navbar-height: 4rem;
|
||||
Reference in New Issue
Block a user