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" ],
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",

View File

@@ -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);

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,
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 &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 {
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()}

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 (
<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)}

View File

@@ -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,

View File

@@ -40,12 +40,21 @@
}
}
.duration-button {
.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 {

View File

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

View File

@@ -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%;
}
}
}

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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>
);

View File

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

View File

@@ -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

View File

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

View File

@@ -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 */

View File

@@ -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,

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;