This commit is contained in:
Infinite
2020-01-29 15:31:57 +01:00
parent 247ad0a470
commit 1ccf8d1586
20 changed files with 442 additions and 418 deletions

View File

@@ -74,9 +74,9 @@
except: ["after-single-line-comment", "first-nested" ], except: ["after-single-line-comment", "first-nested" ],
ignore: ["after-comment"], ignore: ["after-comment"],
}], }],
"selector-max-id": 0, "selector-max-id": 1,
"selector-max-type": 1, "selector-max-type": 2,
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z]+)*$", "selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$",
"selector-combinator-space-after": "always", "selector-combinator-space-after": "always",
"selector-combinator-space-before": "always", "selector-combinator-space-before": "always",
"selector-list-comma-newline-after": "always", "selector-list-comma-newline-after": "always",

View File

@@ -13,7 +13,7 @@ import { Settings } from "./components/Settings/Settings";
import { Stats } from "./components/Stats"; import { Stats } from "./components/Stats";
import Studios from "./components/Studios/Studios"; import Studios from "./components/Studios/Studios";
import { TagList } from "./components/Tags/TagList"; import { TagList } from "./components/Tags/TagList";
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser"; import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser";
library.add(fas); library.add(fas);

View 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
];
}

View 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 &apos;\\&apos; 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>
);
}

View File

@@ -5,19 +5,18 @@ import {
Badge, Badge,
Button, Button,
Card, Card,
Collapse,
Dropdown,
DropdownButton,
Form, Form,
Table Table
} from "react-bootstrap"; } from "react-bootstrap";
import _ from "lodash"; import _ from "lodash";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; 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 { TextUtils } from "src/utils";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { Pagination } from "../list/Pagination"; import { Pagination } from "../list/Pagination";
import { IParserInput, ParserInput } from './ParserInput';
import { ParserField } from './ParserField';
class ParserResult<T> { class ParserResult<T> {
public value: GQL.Maybe<T> = null; 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 { class SceneParserResult {
public id: string; public id: string;
public filename: 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 = { const initialParserInput = {
pattern: "{title}.{ext}", pattern: "{title}.{ext}",
ignoreWords: [], ignoreWords: [],
@@ -518,181 +388,6 @@ export const SceneFilenameParser: React.FC = () => {
setAllStudioSet(selected); 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 &apos;\\&apos; 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 { interface ISceneParserFieldProps {
parserResult: ParserResult<any>; parserResult: ParserResult<any>;
className?: string; className?: string;
@@ -1062,7 +757,13 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<Card id="parser-container"> <Card id="parser-container">
<h4>Scene Filename Parser</h4> <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 />} {isLoading && <LoadingIndicator />}
{renderTable()} {renderTable()}

View 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>
);
}

View File

@@ -62,9 +62,10 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
} }
return ( return (
<Form.Group className={props.className}> <Form.Group className={`duration-input ${props.className}`}>
<InputGroup> <InputGroup>
<Form.Control <Form.Control
className="duration-control"
disabled={props.disabled} disabled={props.disabled}
value={value} value={value}
onChange={(e: any) => setValue(e.target.value)} onChange={(e: any) => setValue(e.target.value)}

View File

@@ -239,9 +239,11 @@ export const TagSelect: React.FC<IFilterProps> = props => {
const Toast = useToast(); const Toast = useToast();
const placeholder = props.noSelectionString ?? "Select tags..."; const placeholder = props.noSelectionString ?? "Select tags...";
const selectedTags = props.ids ?? selectedIds;
const tags = data?.allTags ?? []; const tags = data?.allTags ?? [];
const selected = tags 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 })); .map(tag => ({ value: tag.id, label: tag.name }));
const items: Option[] = tags.map(item => ({ const items: Option[] = tags.map(item => ({
value: item.id, value: item.id,

View File

@@ -40,12 +40,21 @@
} }
} }
.duration-button { .duration-input {
.duration-control {
min-width: 3rem;
}
.duration-button {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-top-left-radius: 0; border-top-left-radius: 0;
line-height: 10px; line-height: 10px;
margin-left: 0 !important;
padding: 1px 7px; padding: 1px 7px;
}
.btn + .btn {
margin-left: 0;
}
} }
.folder-list { .folder-list {

View File

@@ -1,4 +1,3 @@
#tag-list-container { #tag-list-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -24,5 +23,3 @@
text-decoration: underline; text-decoration: underline;
} }
} }

View File

@@ -85,17 +85,19 @@
z-index: -1; z-index: -1;
} }
.wall-item video, .wall {
.wall-item img { .wall-item {
line-height: 0;
overflow: visible;
padding: 0;
position: relative;
width: 20%;
video,
img {
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
width: 100%; width: 100%;
} }
}
.wall-item {
line-height: 0;
overflow: visible;
padding: 0 !important;
position: relative;
width: 20%;
} }

View File

@@ -228,7 +228,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
if (props.onChangeZoom) { if (props.onChangeZoom) {
return ( return (
<Form.Control <Form.Control
className="zoom-slider" className="zoom-slider col-1"
type="range" type="range"
min={0} min={0}
max={3} max={3}

View File

@@ -1,7 +1,7 @@
.performer.image { .performer.image {
background-position: center !important; background-position: center;
background-repeat: no-repeat !important; background-repeat: no-repeat;
background-size: cover !important; background-size: cover;
height: 50vh; height: 50vh;
min-height: 400px; min-height: 400px;
} }
@@ -20,10 +20,6 @@
width: 100%; width: 100%;
} }
#url-field {
line-height: 30px;
}
.scrape-url-button { .scrape-url-button {
color: $text-color; color: $text-color;
float: right; float: right;
@@ -38,6 +34,10 @@
} }
} }
#url-field {
line-height: 30px;
}
#performer-page { #performer-page {
flex-direction: row; flex-direction: row;
margin: 10px auto; margin: 10px auto;
@@ -57,11 +57,11 @@
margin-left: 10px; margin-left: 10px;
.not-favorite { .not-favorite {
color: rgba(191, 204, 214, .5) !important; color: rgba(191, 204, 214, .5);
} }
.favorite { .favorite {
color: #ff7373 !important; color: #ff7373;
} }
.link { .link {

View File

@@ -93,7 +93,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal"> <Button className="minimal">
<Icon icon="tag" /> <Icon icon="tag" />
{props.scene.tags.length} <span>{props.scene.tags.length}</span>
</Button> </Button>
</HoverPopover> </HoverPopover>
); );
@@ -117,7 +117,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal"> <Button className="minimal">
<Icon icon="user" /> <Icon icon="user" />
{props.scene.performers.length} <span>{props.scene.performers.length}</span>
</Button> </Button>
</HoverPopover> </HoverPopover>
); );
@@ -135,7 +135,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal"> <Button className="minimal">
<Icon icon="map-marker-alt" /> <Icon icon="map-marker-alt" />
{props.scene.scene_markers.length} <span>{props.scene.scene_markers.length}</span>
</Button> </Button>
</HoverPopover> </HoverPopover>
); );

View File

@@ -128,4 +128,3 @@
display: inline-block; display: inline-block;
width: 100%; width: 100%;
} }

View File

@@ -180,58 +180,50 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
return ret; return ret;
} }
function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) { useEffect(() => {
const state = props.selected;
let updateRating = ""; let updateRating = "";
let updateStudioId: string|undefined; let updateStudioID: string|undefined;
let updatePerformerIds: string[] = []; let updatePerformerIds: string[] = [];
let updateTagIds: string[] = []; let updateTagIds: string[] = [];
let first = true; let first = true;
state.forEach((scene: GQL.SlimSceneDataFragment) => { state.forEach((scene: GQL.SlimSceneDataFragment) => {
const thisRating = scene.rating?.toString() ?? ""; const sceneRating = scene.rating?.toString() ?? "";
const thisStudio = scene?.studio?.id; 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) { if (first) {
updateRating = thisRating; updateRating = sceneRating;
updateStudioId = thisStudio; updateStudioID = sceneStudioID;
updatePerformerIds = scene.performers updatePerformerIds = scenePerformerIDs;
? scene.performers.map(p => p.id).sort() updateTagIds = sceneTagIDs;
: [];
updateTagIds = scene.tags ? scene.tags.map(p => p.id).sort() : [];
first = false; first = false;
} else { } else {
if (rating !== thisRating) { if (sceneRating !== updateRating) {
updateRating = ""; updateRating = "";
} }
if (studioId !== thisStudio) { if (sceneStudioID !== updateStudioID) {
updateStudioId = undefined; updateStudioID = undefined;
} }
const perfIds = scene.performers if (!_.isEqual(scenePerformerIDs, updatePerformerIds)) {
? scene.performers.map(p => p.id).sort()
: [];
const tIds = scene.tags ? scene.tags.map(t => t.id).sort() : [];
if (!_.isEqual(performerIds, perfIds)) {
updatePerformerIds = []; updatePerformerIds = [];
} }
if (!_.isEqual(sceneTagIDs, updateTagIds)) {
if (!_.isEqual(tagIds, tIds)) {
updateTagIds = []; updateTagIds = [];
} }
} }
}); });
setRating(updateRating); setRating(updateRating);
setStudioId(updateStudioId); setStudioId(updateStudioID);
setPerformerIds(updatePerformerIds); setPerformerIds(updatePerformerIds);
setTagIds(updateTagIds); setTagIds(updateTagIds);
}
useEffect(() => {
updateScenesEditState(props.selected);
setIsLoading(false); setIsLoading(false);
}, [props.selected]); }, [props.selected]);
function renderMultiSelect( function renderMultiSelect(
type: "performers" | "tags", type: "performers" | "tags",
ids: string[] | undefined ids: string[] | undefined

View File

@@ -88,6 +88,7 @@
width: 100%; width: 100%;
} }
} }
#details { #details {
min-height: 150px; min-height: 150px;
} }
@@ -104,4 +105,3 @@
overflow-y: auto; overflow-y: auto;
} }
} }

View File

@@ -1,22 +1,9 @@
@import "styles/theme"; @import "styles/theme";
@import "styles/range"; @import "styles/range";
@import "styles/scrollbars"; @import "styles/scrollbars";
@import "styles/variables";
@import "./components/**/*.scss"; @import "./components/**/*.scss";
body { body {
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
margin: 0; margin: 0;
@@ -70,6 +57,7 @@ code {
max-height: 11.25rem; max-height: 11.25rem;
} }
} }
&.zoom-1 { &.zoom-1 {
width: 20rem; width: 20rem;
@@ -81,6 +69,7 @@ code {
height: 15rem; height: 15rem;
} }
} }
&.zoom-2 { &.zoom-2 {
width: 30rem; width: 30rem;
@@ -92,6 +81,7 @@ code {
height: 22.5rem; height: 22.5rem;
} }
} }
&.zoom-3 { &.zoom-3 {
width: 40rem; width: 40rem;
@@ -156,7 +146,7 @@ video.preview.portrait {
} }
.tag-item { .tag-item {
background-color: #bfccd6; background-color: $muted-gray;
color: #182026; color: #182026;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
@@ -214,7 +204,7 @@ video.preview.portrait {
} }
.rating-5 { .rating-5 {
background: #FF2F39; background: #ff2f39;
} }
.rating-4 { .rating-4 {
@@ -224,6 +214,7 @@ video.preview.portrait {
.rating-3 { .rating-3 {
background: $orange1; background: $orange1;
} }
.rating-2 { .rating-2 {
background: $sepia1; background: $sepia1;
} }
@@ -250,7 +241,7 @@ video.preview.portrait {
.scene-specs-overlay { .scene-specs-overlay {
bottom: 1rem; bottom: 1rem;
color: #f5f8fa; color: $text-color;
display: block; display: block;
font-weight: 400; font-weight: 400;
letter-spacing: -.03rem; letter-spacing: -.03rem;
@@ -274,7 +265,7 @@ video.preview.portrait {
background-position: right top; background-position: right top;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
color: #f5f8fa; color: $text-color;
display: inline-block; display: inline-block;
height: 100%; height: 100%;
letter-spacing: -.03rem; letter-spacing: -.03rem;
@@ -332,20 +323,13 @@ video.preview.portrait {
white-space: pre-line; white-space: pre-line;
} }
.studio { /* stylelint-disable selector-class-pattern */
.image {
background-position: center !important;
background-repeat: no-repeat !important;
background-size: contain !important;
height: 100px;
}
}
.react-photo-gallery--gallery { .react-photo-gallery--gallery {
img { img {
object-fit: contain; object-fit: contain;
} }
} }
/* stylelint-enable selector-class-pattern */
#parser-container { #parser-container {
margin: 10px auto; margin: 10px auto;
@@ -413,11 +397,11 @@ video.preview.portrait {
} }
.main { .main {
color: #f5f8fa; color: $text-color;
} }
.table { .table {
color: #f5f8fa; color: $text-color;
width: inherit; width: inherit;
} }
@@ -425,7 +409,7 @@ video.preview.portrait {
border: none; border: none;
} }
.table-striped tbody tr:nth-child(odd) td { .table-striped tr:nth-child(odd) td {
background: rgba(92, 112, 128, .15); background: rgba(92, 112, 128, .15);
} }
@@ -455,7 +439,7 @@ video.preview.portrait {
.button-link { .button-link {
background-color: transparent; background-color: transparent;
border-width: 0; border-width: 0;
color: #48aff0; color: $link-color;
cursor: pointer; cursor: pointer;
display: inline; display: inline;
padding: 0; padding: 0;
@@ -467,7 +451,7 @@ video.preview.portrait {
.scrubber-button { .scrubber-button {
background-color: transparent; background-color: transparent;
color: #48aff0; color: $link-color;
} }
.scrubber-button:hover { .scrubber-button:hover {
@@ -532,6 +516,7 @@ video.preview.portrait {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
/* stylelint-disable selector-class-pattern */
.svg-inline--fa { .svg-inline--fa {
margin: 0 .4rem; margin: 0 .4rem;
} }
@@ -541,3 +526,4 @@ video.preview.portrait {
margin: 0; margin: 0;
} }
} }
/* stylelint-enable */

View File

@@ -2,6 +2,7 @@
/* Blueprint dark theme */ /* Blueprint dark theme */
$secondary: #394b59; $secondary: #394b59;
$muted-gray: #bfccd6;
$theme-colors: ( $theme-colors: (
primary: #137cbd, primary: #137cbd,
@@ -13,7 +14,7 @@ $theme-colors: (
); );
$body-bg: #202b33; $body-bg: #202b33;
$text-muted: #bfccd6; $text-muted: $muted-gray;
$link-color: #48aff0; $link-color: #48aff0;
$link-hover-color: #48aff0; $link-hover-color: #48aff0;
$text-color: #f5f8fa; $text-color: #f5f8fa;
@@ -23,10 +24,19 @@ $popover-bg: $secondary;
@import "node_modules/bootstrap/scss/bootstrap"; @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:not(.disabled),
.btn.active.minimal:not(.disabled) { .btn.active.minimal:not(.disabled) {
background-color: rgba(138, 155, 168, .3); background-color: rgba(138, 155, 168, .3);
color: #f5f8fa; color: $text-color;
} }
a.minimal, a.minimal,

View File

@@ -1,8 +0,0 @@
$red1: #a82a2a;
$orange1: #a66321;
$sepia1: #63411e;
$dark-gray2: #202b33;
$dark-gray5: #394b59;
$pt-grid-size: 10px;
$pt-navbar-height: 4rem;