This commit is contained in:
Infinite
2020-01-29 22:28:13 +01:00
parent 3f9f32c356
commit c1ce6d539d
45 changed files with 419 additions and 310 deletions

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { LoadingIndicator } from 'src/components/Shared'; import { LoadingIndicator } from "src/components/Shared";
import { GalleryViewer } from "./GalleryViewer"; import { GalleryViewer } from "./GalleryViewer";
export const Gallery: React.FC = () => { export const Gallery: React.FC = () => {
@@ -10,8 +10,7 @@ export const Gallery: React.FC = () => {
const { data, error, loading } = StashService.useFindGallery(id); const { data, error, loading } = StashService.useFindGallery(id);
const gallery = data?.findGallery; const gallery = data?.findGallery;
if (loading || !gallery) if (loading || !gallery) return <LoadingIndicator />;
return <LoadingIndicator />;
if (error) return <div>{error.message}</div>; if (error) return <div>{error.message}</div>;
return ( return (

View File

@@ -91,9 +91,7 @@ export const MainNavbar: React.FC = () => {
))} ))}
</Nav> </Nav>
<Nav> <Nav>
<div className="d-none d-sm-block"> <div className="d-none d-sm-block">{newButton}</div>
{newButton}
</div>
<LinkContainer exact to="/settings"> <LinkContainer exact to="/settings">
<Button className="minimal"> <Button className="minimal">
<Icon icon="cog" /> <Icon icon="cog" />

View File

@@ -1,4 +1,3 @@
export class ParserField { export class ParserField {
public field: string; public field: string;
public helperText?: string; public helperText?: string;

View File

@@ -1,13 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { import {
Button, Button,
Dropdown, Dropdown,
DropdownButton, DropdownButton,
Form, Form,
InputGroup, InputGroup
} from 'react-bootstrap'; } from "react-bootstrap";
import { ParserField } from './ParserField'; import { ParserField } from "./ParserField";
import { ShowFields } from './ShowFields'; import { ShowFields } from "./ShowFields";
const builtInRecipes = [ const builtInRecipes = [
{ {
@@ -80,7 +80,9 @@ interface IParserInputProps {
setShowFields: (fields: Map<string, boolean>) => void; setShowFields: (fields: Map<string, boolean>) => void;
} }
export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProps) => { export const ParserInput: React.FC<IParserInputProps> = (
props: IParserInputProps
) => {
const [pattern, setPattern] = useState<string>(props.input.pattern); const [pattern, setPattern] = useState<string>(props.input.pattern);
const [ignoreWords, setIgnoreWords] = useState<string>( const [ignoreWords, setIgnoreWords] = useState<string>(
props.input.ignoreWords.join(" ") props.input.ignoreWords.join(" ")
@@ -124,7 +126,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
return ( return (
<Form.Group> <Form.Group>
<Form.Group className="row"> <Form.Group className="row">
<Form.Label htmlFor="filename-pattern" className="col-2">Filename Pattern</Form.Label> <Form.Label htmlFor="filename-pattern" className="col-2">
Filename Pattern
</Form.Label>
<InputGroup className="col-8"> <InputGroup className="col-8">
<Form.Control <Form.Control
id="filename-pattern" id="filename-pattern"
@@ -134,7 +138,10 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
<InputGroup.Append> <InputGroup.Append>
<DropdownButton id="parser-field-select" title="Add Field"> <DropdownButton id="parser-field-select" title="Add Field">
{validFields.map(item => ( {validFields.map(item => (
<Dropdown.Item key={item.field} onSelect={() => addParserField(item)}> <Dropdown.Item
key={item.field}
onSelect={() => addParserField(item)}
>
<span>{item.field}</span> <span>{item.field}</span>
<span className="ml-auto">{item.helperText}</span> <span className="ml-auto">{item.helperText}</span>
</Dropdown.Item> </Dropdown.Item>
@@ -142,7 +149,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</DropdownButton> </DropdownButton>
</InputGroup.Append> </InputGroup.Append>
</InputGroup> </InputGroup>
<Form.Text className="text-muted row col-10 offset-2">Use &apos;\\&apos; to escape literal {} characters</Form.Text> <Form.Text className="text-muted row col-10 offset-2">
Use &apos;\\&apos; to escape literal {} characters
</Form.Text>
</Form.Group> </Form.Group>
<Form.Group className="row" controlId="ignored-words"> <Form.Group className="row" controlId="ignored-words">
@@ -152,12 +161,16 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)} onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}
value={ignoreWords} value={ignoreWords}
/> />
<Form.Text className="text-muted col-10 offset-2">Matches with {"{i}"}</Form.Text> <Form.Text className="text-muted col-10 offset-2">
Matches with {"{i}"}
</Form.Text>
</Form.Group> </Form.Group>
<h5>Title</h5> <h5>Title</h5>
<Form.Group className="row"> <Form.Group className="row">
<Form.Label htmlFor="whitespace-characters" className="col-2">Whitespace characters:</Form.Label> <Form.Label htmlFor="whitespace-characters" className="col-2">
Whitespace characters:
</Form.Label>
<Form.Control <Form.Control
className="col-8" className="col-8"
onChange={(newValue: any) => onChange={(newValue: any) =>
@@ -170,7 +183,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</Form.Text> </Form.Text>
</Form.Group> </Form.Group>
<Form.Group className="row"> <Form.Group className="row">
<Form.Label htmlFor="capitalize-title" className="col-2">Capitalize title</Form.Label> <Form.Label htmlFor="capitalize-title" className="col-2">
Capitalize title
</Form.Label>
<Form.Control <Form.Control
className="col-8" className="col-8"
type="checkbox" type="checkbox"
@@ -182,9 +197,16 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
{/* TODO - mapping stuff will go here */} {/* TODO - mapping stuff will go here */}
<Form.Group> <Form.Group>
<DropdownButton variant="secondary" id="recipe-select" title="Select Parser Recipe"> <DropdownButton
variant="secondary"
id="recipe-select"
title="Select Parser Recipe"
>
{builtInRecipes.map(item => ( {builtInRecipes.map(item => (
<Dropdown.Item key={item.pattern} onSelect={() => setParserRecipe(item)}> <Dropdown.Item
key={item.pattern}
onSelect={() => setParserRecipe(item)}
>
<span>{item.pattern}</span> <span>{item.pattern}</span>
<span className="mr-auto">{item.description}</span> <span className="mr-auto">{item.description}</span>
</Dropdown.Item> </Dropdown.Item>
@@ -200,7 +222,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</Form.Group> </Form.Group>
<Form.Group className="row"> <Form.Group className="row">
<Button variant="secondary" className="col-1" onClick={onFind}>Find</Button> <Button variant="secondary" className="col-1" onClick={onFind}>
Find
</Button>
<Form.Control <Form.Control
as="select" as="select"
style={{ flexBasis: "min-content" }} style={{ flexBasis: "min-content" }}
@@ -218,4 +242,4 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</Form.Group> </Form.Group>
</Form.Group> </Form.Group>
); );
} };

View File

@@ -1,22 +1,20 @@
/* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ /* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { import { Badge, Button, Card, Form, Table } from "react-bootstrap";
Badge,
Button,
Card,
Form,
Table
} 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, 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 { IParserInput, ParserInput } from "./ParserInput";
import { ParserField } from './ParserField'; import { ParserField } from "./ParserField";
class ParserResult<T> { class ParserResult<T> {
public value: GQL.Maybe<T> = null; public value: GQL.Maybe<T> = null;

View File

@@ -1,9 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { import { Button, Collapse } from "react-bootstrap";
Button, import { Icon } from "src/components/Shared";
Collapse
} from 'react-bootstrap';
import { Icon } from 'src/components/Shared';
interface IShowFieldsProps { interface IShowFieldsProps {
fields: Map<string, boolean>; fields: Map<string, boolean>;
@@ -43,4 +40,4 @@ export const ShowFields = (props: IShowFieldsProps) => {
</Collapse> </Collapse>
</div> </div>
); );
} };

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Button, Table } from "react-bootstrap"; import { Button, Table } from "react-bootstrap";
import { LoadingIndicator } from 'src/components/Shared'; import { LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
export const SettingsAboutPanel: React.FC = () => { export const SettingsAboutPanel: React.FC = () => {

View File

@@ -153,8 +153,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
} }
if (error) return <h1>{error.message}</h1>; if (error) return <h1>{error.message}</h1>;
if (!data?.configuration || loading) if (!data?.configuration || loading) return <LoadingIndicator />;
return <LoadingIndicator />;
return ( return (
<> <>
@@ -199,8 +198,8 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group> <Form.Group>
<h6>Excluded Patterns</h6> <h6>Excluded Patterns</h6>
<Form.Group> <Form.Group>
{excludes {excludes &&
&& excludes.map((regexp, i) => ( excludes.map((regexp, i) => (
<InputGroup> <InputGroup>
<Form.Control <Form.Control
className="col col-sm-6" className="col col-sm-6"
@@ -218,8 +217,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
</Button> </Button>
</InputGroup.Append> </InputGroup.Append>
</InputGroup> </InputGroup>
)) ))}
}
</Form.Group> </Form.Group>
<Button className="minimal" onClick={() => excludeAddRegex()}> <Button className="minimal" onClick={() => excludeAddRegex()}>
<Icon icon="plus" /> <Icon icon="plus" />

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { DurationInput, LoadingIndicator } from 'src/components/Shared'; import { DurationInput, LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -108,7 +108,8 @@ export const SettingsInterfacePanel: React.FC = () => {
onValueChange={duration => setMaximumLoopDuration(duration)} onValueChange={duration => setMaximumLoopDuration(duration)}
/> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">
Maximum scene duration where scene player will loop the video - 0 to disable Maximum scene duration where scene player will loop the video - 0 to
disable
</Form.Text> </Form.Text>
</Form.Group> </Form.Group>
</Form.Group> </Form.Group>

View File

@@ -53,7 +53,12 @@ export const GenerateButton: React.FC = () => {
/> />
</Form.Group> </Form.Group>
<Form.Group> <Form.Group>
<Button id="generate" variant="secondary" type="submit" onClick={() => onGenerate()}> <Button
id="generate"
variant="secondary"
type="submit"
onClick={() => onGenerate()}
>
Generate Generate
</Button> </Button>
<Form.Text className="text-muted"> <Form.Text className="text-muted">

View File

@@ -242,9 +242,7 @@ export const SettingsTasksPanel: React.FC = () => {
<Form.Group> <Form.Group>
<Link to="/sceneFilenameParser"> <Link to="/sceneFilenameParser">
<Button variant="secondary"> <Button variant="secondary">Scene Filename Parser</Button>
Scene Filename Parser
</Button>
</Link> </Link>
</Form.Group> </Form.Group>

View File

@@ -1,10 +1,7 @@
import { import { Button, Modal } from "react-bootstrap";
Button,
Modal,
} from "react-bootstrap";
import React, { useState } from "react"; import React, { useState } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ImageInput } from 'src/components/Shared'; import { ImageInput } from "src/components/Shared";
interface IProps { interface IProps {
performer?: Partial<GQL.PerformerDataFragment>; performer?: Partial<GQL.PerformerDataFragment>;
@@ -24,7 +21,11 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
function renderEditButton() { function renderEditButton() {
if (props.isNew) return; if (props.isNew) return;
return ( return (
<Button variant="primary" className="edit" onClick={() => props.onToggleEdit()}> <Button
variant="primary"
className="edit"
onClick={() => props.onToggleEdit()}
>
{props.isEditing ? "Cancel" : "Edit"} {props.isEditing ? "Cancel" : "Edit"}
</Button> </Button>
); );
@@ -43,7 +44,11 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
function renderDeleteButton() { function renderDeleteButton() {
if (props.isNew || props.isEditing) return; if (props.isNew || props.isEditing) return;
return ( return (
<Button variant="danger" className="delete d-none d-sm-block" onClick={() => setIsDeleteAlertOpen(true)}> <Button
variant="danger"
className="delete d-none d-sm-block"
onClick={() => setIsDeleteAlertOpen(true)}
>
Delete Delete
</Button> </Button>
); );
@@ -92,7 +97,10 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
return ( return (
<div className="details-edit"> <div className="details-edit">
{renderEditButton()} {renderEditButton()}
<ImageInput isEditing={props.isEditing} onImageChange={props.onImageChange} /> <ImageInput
isEditing={props.isEditing}
onImageChange={props.onImageChange}
/>
{renderAutoTagButton()} {renderAutoTagButton()}
{renderSaveButton()} {renderSaveButton()}
{renderDeleteButton()} {renderDeleteButton()}

View File

@@ -35,10 +35,20 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
function renderButtons() { function renderButtons() {
return ( return (
<ButtonGroup vertical> <ButtonGroup vertical>
<Button variant="secondary" className="duration-button" disabled={props.disabled} onClick={() => increment()}> <Button
variant="secondary"
className="duration-button"
disabled={props.disabled}
onClick={() => increment()}
>
<Icon icon="chevron-up" /> <Icon icon="chevron-up" />
</Button> </Button>
<Button variant="secondary" className="duration-button" disabled={props.disabled} onClick={() => decrement()}> <Button
variant="secondary"
className="duration-button"
disabled={props.disabled}
onClick={() => decrement()}
>
<Icon icon="chevron-down" /> <Icon icon="chevron-down" />
</Button> </Button>
</ButtonGroup> </ButtonGroup>

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, InputGroup, Form, Modal } from "react-bootstrap"; import { Button, InputGroup, Form, Modal } from "react-bootstrap";
import { LoadingIndicator } from 'src/components/Shared'; import { LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
interface IProps { interface IProps {
@@ -80,7 +80,9 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button variant="success" onClick={() => onSelectDirectory()}>Add</Button> <Button variant="success" onClick={() => onSelectDirectory()}>
Add
</Button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
); );
@@ -103,7 +105,9 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
})} })}
</Form.Group> </Form.Group>
<Button variant="secondary" onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button> <Button variant="secondary" onClick={() => setIsDisplayingDialog(true)}>
Add Directory
</Button>
</> </>
); );
}; };

View File

@@ -1,12 +1,15 @@
import React from "react"; import React from "react";
import { Button, Form } from 'react-bootstrap'; import { Button, Form } from "react-bootstrap";
interface IImageInput { interface IImageInput {
isEditing: boolean; isEditing: boolean;
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void; onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
} }
export const ImageInput: React.FC<IImageInput> = ({ isEditing, onImageChange }) => { export const ImageInput: React.FC<IImageInput> = ({
isEditing,
onImageChange
}) => {
if (!isEditing) return <div />; if (!isEditing) return <div />;
return ( return (
@@ -19,5 +22,4 @@ export const ImageInput: React.FC<IImageInput> = ({ isEditing, onImageChange })
/> />
</Form.Label> </Form.Label>
); );
} };

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Spinner } from "react-bootstrap"; import { Spinner } from "react-bootstrap";
import cx from 'classnames'; import cx from "classnames";
interface ILoadingProps { interface ILoadingProps {
message?: string; message?: string;
@@ -10,8 +10,11 @@ interface ILoadingProps {
const CLASSNAME = "LoadingIndicator"; const CLASSNAME = "LoadingIndicator";
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`; const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
const LoadingIndicator: React.FC<ILoadingProps> = ({ message, inline = false }) => ( const LoadingIndicator: React.FC<ILoadingProps> = ({
<div className={cx(CLASSNAME, { inline }) }> message,
inline = false
}) => (
<div className={cx(CLASSNAME, { inline })}>
<Spinner animation="border" role="status"> <Spinner animation="border" role="status">
<span className="sr-only">Loading...</span> <span className="sr-only">Loading...</span>
</Spinner> </Spinner>

View File

@@ -35,7 +35,7 @@ interface ISelectProps {
onChange: (item: ValueType<Option>) => void; onChange: (item: ValueType<Option>) => void;
initialIds?: string[]; initialIds?: string[];
isMulti?: boolean; isMulti?: boolean;
isClearable?: boolean, isClearable?: boolean;
onInputChange?: (input: string) => void; onInputChange?: (input: string) => void;
placeholder?: string; placeholder?: string;
showDropdown?: boolean; showDropdown?: boolean;
@@ -53,10 +53,11 @@ interface ISceneGallerySelect {
} }
const getSelectedValues = (selectedItems: ValueType<Option>) => const getSelectedValues = (selectedItems: ValueType<Option>) =>
selectedItems ? selectedItems
(Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map( ? (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
item => item.value item => item.value
) : []; )
: [];
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => { export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
const { data, loading } = StashService.useValidGalleriesForScene( const { data, loading } = StashService.useValidGalleriesForScene(
@@ -176,8 +177,9 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
label: item.name ?? "" label: item.name ?? ""
})); }));
const placeholder = props.noSelectionString ?? "Select performer..."; const placeholder = props.noSelectionString ?? "Select performer...";
const selectedOptions:Option[] = props.ids ? const selectedOptions: Option[] = props.ids
items.filter(item => props.ids?.indexOf(item.value) !== -1) : []; ? items.filter(item => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems); const selectedIds = getSelectedValues(selectedItems);
@@ -208,8 +210,9 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
label: item.name label: item.name
})); }));
const placeholder = props.noSelectionString ?? "Select studio..."; const placeholder = props.noSelectionString ?? "Select studio...";
const selectedOptions:Option[] = props.ids ? const selectedOptions: Option[] = props.ids
items.filter(item => props.ids?.indexOf(item.value) !== -1) : []; ? items.filter(item => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems); const selectedIds = getSelectedValues(selectedItems);
@@ -320,17 +323,21 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
const defaultValue = const defaultValue =
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null; items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
const options = groupHeader ? [{ const options = groupHeader
? [
{
label: groupHeader, label: groupHeader,
options: items options: items
}] : items; }
]
: items;
const styles = { const styles = {
option: (base:CSSProperties) => ({ option: (base: CSSProperties) => ({
...base, ...base,
color: "#000" color: "#000"
}), }),
container: (base:CSSProperties, state:any) => ({ container: (base: CSSProperties, state: any) => ({
...base, ...base,
zIndex: state.isFocused ? 10 : base.zIndex zIndex: state.isFocused ? 10 : base.zIndex
}) })

View File

@@ -15,4 +15,4 @@ export { DurationInput } from "./DurationInput";
export { TagLink } from "./TagLink"; export { TagLink } from "./TagLink";
export { HoverPopover } from "./HoverPopover"; export { HoverPopover } from "./HoverPopover";
export { default as LoadingIndicator } from "./LoadingIndicator"; export { default as LoadingIndicator } from "./LoadingIndicator";
export { ImageInput } from './ImageInput'; export { ImageInput } from "./ImageInput";

View File

@@ -46,7 +46,10 @@ export const Stats: React.FC = () => {
</nav> </nav>
<h5>Notes</h5> <h5>Notes</h5>
<em>This is still an early version, some things are still a work in progress.</em> <em>
This is still an early version, some things are still a work in
progress.
</em>
</div> </div>
</div> </div>
); );

View File

@@ -10,11 +10,8 @@ interface IProps {
export const StudioCard: React.FC<IProps> = ({ studio }) => { export const StudioCard: React.FC<IProps> = ({ studio }) => {
return ( return (
<Card className="studio-card"> <Card className="studio-card">
<Link <Link to={`/studios/${studio.id}`} className="studio-image">
to={`/studios/${studio.id}`} <img alt={studio.name} src={studio.image_path ?? ""} />
className="studio-image"
>
<img alt={studio.name} src={studio.image_path ?? ''} />
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h5 className="text-truncate">{studio.name}</h5> <h5 className="text-truncate">{studio.name}</h5>

View File

@@ -3,14 +3,18 @@
import { Table } from "react-bootstrap"; import { Table } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import cx from 'classnames'; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { ImageUtils, TableUtils } from "src/utils"; import { ImageUtils, TableUtils } from "src/utils";
import { DetailsEditNavbar, Modal, LoadingIndicator } from "src/components/Shared"; import {
DetailsEditNavbar,
Modal,
LoadingIndicator
} from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { StudioScenesPanel } from './StudioScenesPanel'; import { StudioScenesPanel } from "./StudioScenesPanel";
export const Studio: React.FC = () => { export const Studio: React.FC = () => {
const history = useHistory(); const history = useHistory();
@@ -71,8 +75,7 @@ export const Studio: React.FC = () => {
ImageUtils.usePasteImage(onImageLoad); ImageUtils.usePasteImage(onImageLoad);
if (!isNew && !isEditing) { if (!isNew && !isEditing) {
if (!data?.findStudio || loading) if (!data?.findStudio || loading) return <LoadingIndicator />;
return <LoadingIndicator />;
if (error) return <div>{error.message}</div>; if (error) return <div>{error.message}</div>;
} }
@@ -142,21 +145,26 @@ export const Studio: React.FC = () => {
accept={{ text: "Delete", variant: "danger", onClick: onDelete }} accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }} cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
> >
<p>Are you sure you want to delete {studio.name ?? 'studio'}?</p> <p>Are you sure you want to delete {studio.name ?? "studio"}?</p>
</Modal> </Modal>
); );
} }
return ( return (
<div className="row"> <div className="row">
<div className={cx('studio-details', { 'col ml-sm-5': !isNew, 'col-8': isNew})}> <div
{ isNew && <h2>Add Studio</h2> } className={cx("studio-details", {
"col ml-sm-5": !isNew,
"col-8": isNew
})}
>
{isNew && <h2>Add Studio</h2>}
<img className="logo" alt={name} src={imagePreview} /> <img className="logo" alt={name} src={imagePreview} />
<Table id="performer-details" style={{ width: "100%" }}> <Table id="performer-details" style={{ width: "100%" }}>
<tbody> <tbody>
{TableUtils.renderInputGroup({ {TableUtils.renderInputGroup({
title: "Name", title: "Name",
value: studio.name ?? '', value: studio.name ?? "",
isEditing: !!isEditing, isEditing: !!isEditing,
onChange: setName onChange: setName
})} })}
@@ -179,7 +187,7 @@ export const Studio: React.FC = () => {
onDelete={onDelete} onDelete={onDelete}
/> />
</div> </div>
{ !isNew && ( {!isNew && (
<div className="col-12 col-sm-8"> <div className="col-12 col-sm-8">
<StudioScenesPanel studio={studio} /> <StudioScenesPanel studio={studio} />
</div> </div>

View File

@@ -8,9 +8,7 @@ interface IStudioScenesPanel {
studio: Partial<GQL.StudioDataFragment>; studio: Partial<GQL.StudioDataFragment>;
} }
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
studio
}) => {
function filterHook(filter: ListFilterModel) { function filterHook(filter: ListFilterModel) {
const studioValue = { id: studio.id!, label: studio.name! }; const studioValue = { id: studio.id!, label: studio.name! };
// if studio is already present, then we modify it, otherwise add // if studio is already present, then we modify it, otherwise add

View File

@@ -14,8 +14,7 @@ export const StudioList: React.FC = () => {
result: FindStudiosQueryResult, result: FindStudiosQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
if (!result.data?.findStudios) if (!result.data?.findStudios) return;
return;
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (

View File

@@ -93,8 +93,7 @@ export const TagList: React.FC = () => {
</Modal> </Modal>
); );
if (!data?.allTags) if (!data?.allTags) return <LoadingIndicator />;
return <LoadingIndicator />;
if (error) return <div>{error.message}</div>; if (error) return <div>{error.message}</div>;
const tagElements = data.allTags.map(tag => { const tagElements = data.allTags.map(tag => {
@@ -104,7 +103,9 @@ export const TagList: React.FC = () => {
{tag.name} {tag.name}
</Button> </Button>
<div style={{ float: "right" }}> <div style={{ float: "right" }}>
<Button variant="secondary" onClick={() => onAutoTag(tag)}>Auto Tag</Button> <Button variant="secondary" onClick={() => onAutoTag(tag)}>
Auto Tag
</Button>
<Button variant="secondary"> <Button variant="secondary">
<Link to={NavUtils.makeTagScenesUrl(tag)}> <Link to={NavUtils.makeTagScenesUrl(tag)}>
Scenes: {tag.scene_count} Scenes: {tag.scene_count}

View File

@@ -16,9 +16,7 @@ interface IWallItemProps {
) => void; ) => void;
} }
export const WallItem: React.FC<IWallItemProps> = ( export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
props: IWallItemProps
) => {
const [videoPath, setVideoPath] = useState<string>(); const [videoPath, setVideoPath] = useState<string>();
const [previewPath, setPreviewPath] = useState<string>(""); const [previewPath, setPreviewPath] = useState<string>("");
const [screenshotPath, setScreenshotPath] = useState<string>(""); const [screenshotPath, setScreenshotPath] = useState<string>("");
@@ -28,7 +26,8 @@ export const WallItem: React.FC<IWallItemProps> = (
const videoHoverHook = VideoHoverHook.useVideoHover({ const videoHoverHook = VideoHoverHook.useVideoHover({
resetOnMouseLeave: true resetOnMouseLeave: true
}); });
const showTextContainer = config.data?.configuration.interface.wallShowTitle ?? true; const showTextContainer =
config.data?.configuration.interface.wallShowTitle ?? true;
function onMouseEnter() { function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook); VideoHoverHook.onMouseEnter(videoHoverHook);

View File

@@ -123,7 +123,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
value={criterion.modifier} value={criterion.modifier}
> >
{criterion.modifierOptions.map(c => ( {criterion.modifierOptions.map(c => (
<option key={c.value} value={c.value}>{c.label}</option> <option key={c.value} value={c.value}>
{c.label}
</option>
))} ))}
</Form.Control> </Form.Control>
); );
@@ -156,7 +158,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (
isMulti isMulti
onSelect={items => { onSelect={items => {
const newCriterion = _.cloneDeep(criterion); const newCriterion = _.cloneDeep(criterion);
newCriterion.value = items.map(i => ({ id: i.id, label: i.name! })); newCriterion.value = items.map(i => ({
id: i.id,
label: i.name!
}));
setCriterion(newCriterion); setCriterion(newCriterion);
}} }}
ids={criterion.value.map((labeled: any) => labeled.id)} ids={criterion.value.map((labeled: any) => labeled.id)}
@@ -172,7 +177,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
value={criterion.value} value={criterion.value}
> >
{criterion.options.map(c => ( {criterion.options.map(c => (
<option key={c} value={c}>{c}</option> <option key={c} value={c}>
{c}
</option>
))} ))}
</Form.Control> </Form.Control>
); );
@@ -216,7 +223,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
value={criterion.type} value={criterion.type}
> >
{props.filter.criterionOptions.map(c => ( {props.filter.criterionOptions.map(c => (
<option key={c.value} value={c.value}>{c.label}</option> <option key={c.value} value={c.value}>
{c.label}
</option>
))} ))}
</Form.Control> </Form.Control>
</Form.Group> </Form.Group>

View File

@@ -158,7 +158,10 @@ export const ListFilter: React.FC<IListFilterProps> = (
onClick={() => onClickCriterionTag(criterion)} onClick={() => onClickCriterionTag(criterion)}
> >
{criterion.getLabel()} {criterion.getLabel()}
<Button variant="secondary" onClick={() => onRemoveCriterionTag(criterion)}> <Button
variant="secondary"
onClick={() => onRemoveCriterionTag(criterion)}
>
<Icon icon="times" /> <Icon icon="times" />
</Button> </Button>
</Badge> </Badge>
@@ -180,7 +183,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSelectAll() { function renderSelectAll() {
if (props.onSelectAll) { if (props.onSelectAll) {
return ( return (
<Dropdown.Item key="select-all" onClick={() => onSelectAll()}>Select All</Dropdown.Item> <Dropdown.Item key="select-all" onClick={() => onSelectAll()}>
Select All
</Dropdown.Item>
); );
} }
} }
@@ -201,7 +206,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
if (props.otherOperations) { if (props.otherOperations) {
props.otherOperations.forEach(o => { props.otherOperations.forEach(o => {
options.push( options.push(
<Dropdown.Item key={o.text} onClick={o.onClick}>{o.text}</Dropdown.Item> <Dropdown.Item key={o.text} onClick={o.onClick}>
{o.text}
</Dropdown.Item>
); );
}); });
} }
@@ -259,7 +266,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
className="filter-item col-1 d-none d-sm-inline" className="filter-item col-1 d-none d-sm-inline"
> >
{PAGE_SIZE_OPTIONS.map(s => ( {PAGE_SIZE_OPTIONS.map(s => (
<option value={s} key={s}>{s}</option> <option value={s} key={s}>
{s}
</option>
))} ))}
</Form.Control> </Form.Control>
<ButtonGroup className="filter-item"> <ButtonGroup className="filter-item">
@@ -288,7 +297,6 @@ export const ListFilter: React.FC<IListFilterProps> = (
</Button> </Button>
</OverlayTrigger> </OverlayTrigger>
</Dropdown> </Dropdown>
</ButtonGroup> </ButtonGroup>
<AddFilter <AddFilter
@@ -304,7 +312,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
{maybeRenderZoom()} {maybeRenderZoom()}
<ButtonGroup className="filter-item d-none d-sm-inline-flex">{renderMore()}</ButtonGroup> <ButtonGroup className="filter-item d-none d-sm-inline-flex">
{renderMore()}
</ButtonGroup>
</div> </div>
<div <div
style={{ style={{

View File

@@ -37,15 +37,15 @@ export const Pagination: React.FC<IPaginationProps> = ({
i => startPage + i i => startPage + i
); );
const calculatePageClass = (buttonPage:number) => { const calculatePageClass = (buttonPage: number) => {
if(pages.length <= 4) return ''; if (pages.length <= 4) return "";
if(currentPage === 1 && buttonPage <= 4) return ''; if (currentPage === 1 && buttonPage <= 4) return "";
const maxPage = pages[pages.length - 1]; const maxPage = pages[pages.length - 1];
if(currentPage === maxPage && buttonPage > (maxPage - 3)) return ''; if (currentPage === maxPage && buttonPage > maxPage - 3) return "";
if(Math.abs(buttonPage - currentPage) <= 1) return ''; if (Math.abs(buttonPage - currentPage) <= 1) return "";
return 'd-none d-sm-block' return "d-none d-sm-block";
} };
const pageButtons = pages.map((page: number) => ( const pageButtons = pages.map((page: number) => (
<Button <Button
@@ -59,12 +59,15 @@ export const Pagination: React.FC<IPaginationProps> = ({
</Button> </Button>
)); ));
if(pages.length <= 1) if (pages.length <= 1) return <div />;
return <div />;
return ( return (
<ButtonGroup className="filter-container pagination"> <ButtonGroup className="filter-container pagination">
<Button variant="secondary" disabled={currentPage === 1} onClick={() => onChangePage(1)}> <Button
variant="secondary"
disabled={currentPage === 1}
onClick={() => onChangePage(1)}
>
<span className="d-none d-sm-inline">First</span> <span className="d-none d-sm-inline">First</span>
<span className="d-inline d-sm-none">&#x300a;</span> <span className="d-inline d-sm-none">&#x300a;</span>
</Button> </Button>

View File

@@ -3,7 +3,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Tabs, Tab } from "react-bootstrap"; import { Button, Tabs, Tab } from "react-bootstrap";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import cx from 'classnames' import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { Icon, LoadingIndicator } from "src/components/Shared"; import { Icon, LoadingIndicator } from "src/components/Shared";
@@ -166,34 +166,52 @@ export const Performer: React.FC = () => {
const renderIcons = () => ( const renderIcons = () => (
<span className="name-icons d-block d-sm-inline"> <span className="name-icons d-block d-sm-inline">
<Button <Button
className={cx('minimal', performer.favorite ? "favorite" : "not-favorite")} className={cx(
"minimal",
performer.favorite ? "favorite" : "not-favorite"
)}
onClick={() => setFavorite(!performer.favorite)} onClick={() => setFavorite(!performer.favorite)}
> >
<Icon icon="heart" /> <Icon icon="heart" />
</Button> </Button>
{ performer.url && ( {performer.url && (
<Button className="minimal"> <Button className="minimal">
<a href={performer.url} className="link" target="_blank" rel="noopener noreferrer"> <a
href={performer.url}
className="link"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="link" /> <Icon icon="link" />
</a> </a>
</Button> </Button>
)} )}
{ performer.twitter && ( {performer.twitter && (
<Button className="minimal"> <Button className="minimal">
<a href={`https://www.twitter.com/${performer.twitter}`} className="twitter" target="_blank" rel="noopener noreferrer"> <a
href={`https://www.twitter.com/${performer.twitter}`}
className="twitter"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="dove" /> <Icon icon="dove" />
</a> </a>
</Button> </Button>
)} )}
{ performer.instagram && ( {performer.instagram && (
<Button className="minimal"> <Button className="minimal">
<a href={`https://www.instagram.com/${performer.instagram}`} className="instagram" target="_blank" rel="noopener noreferrer"> <a
href={`https://www.instagram.com/${performer.instagram}`}
className="instagram"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="camera" /> <Icon icon="camera" />
</a> </a>
</Button> </Button>
)} )}
</span> </span>
) );
function renderNewView() { function renderNewView() {
return ( return (

View File

@@ -1,16 +1,16 @@
/* eslint-disable react/no-this-in-sfc */ /* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import { Button, Form, Popover, OverlayTrigger, Table } from "react-bootstrap";
Button,
Form,
Popover,
OverlayTrigger,
Table
} from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { Icon, Modal, ImageInput, ScrapePerformerSuggest, LoadingIndicator } from "src/components/Shared"; import {
Icon,
Modal,
ImageInput,
ScrapePerformerSuggest,
LoadingIndicator
} from "src/components/Shared";
import { ImageUtils, TableUtils } from "src/utils"; import { ImageUtils, TableUtils } from "src/utils";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -276,7 +276,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
return undefined; return undefined;
} }
return ( return (
<Button className="minimal scrape-url-button" onClick={() => onScrapePerformerURL()}> <Button
className="minimal scrape-url-button"
onClick={() => onScrapePerformerURL()}
>
<Icon icon="file-upload" /> <Icon icon="file-upload" />
</Button> </Button>
); );
@@ -446,7 +449,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
isEditing: !!isEditing, isEditing: !!isEditing,
onChange: setInstagram onChange: setInstagram
})} })}
<ImageInput isEditing={!!isEditing} onImageChange={onImageChangeHandler} /> <ImageInput
isEditing={!!isEditing}
onImageChange={onImageChangeHandler}
/>
</tbody> </tbody>
</Table> </Table>

View File

@@ -1,22 +1,25 @@
import React from "react"; import React from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { Button, Badge, Card } from 'react-bootstrap'; import { Button, Badge, Card } from "react-bootstrap";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
interface IPrimaryTags { interface IPrimaryTags {
sceneMarkers: GQL.SceneMarkerDataFragment[]; sceneMarkers: GQL.SceneMarkerDataFragment[];
onClickMarker: (marker:GQL.SceneMarkerDataFragment) => void; onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
onEdit: (marker:GQL.SceneMarkerDataFragment) =>void; onEdit: (marker: GQL.SceneMarkerDataFragment) => void;
} }
export const PrimaryTags: React.FC<IPrimaryTags> = ({ sceneMarkers, onClickMarker, onEdit }) => { export const PrimaryTags: React.FC<IPrimaryTags> = ({
sceneMarkers,
onClickMarker,
onEdit
}) => {
if (!sceneMarkers?.length) return <div />; if (!sceneMarkers?.length) return <div />;
const primaries:Record<string, GQL.Tag> = {}; const primaries: Record<string, GQL.Tag> = {};
const primaryTags:Record<string, GQL.SceneMarkerDataFragment[]> = {}; const primaryTags: Record<string, GQL.SceneMarkerDataFragment[]> = {};
sceneMarkers.forEach(m => { sceneMarkers.forEach(m => {
if(primaryTags[m.primary_tag.id]) if (primaryTags[m.primary_tag.id]) primaryTags[m.primary_tag.id].push(m);
primaryTags[m.primary_tag.id].push(m);
else { else {
primaryTags[m.primary_tag.id] = [m]; primaryTags[m.primary_tag.id] = [m];
primaries[m.primary_tag.id] = m.primary_tag; primaries[m.primary_tag.id] = m.primary_tag;
@@ -55,16 +58,10 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({ sceneMarkers, onClickMarke
return ( return (
<Card className="primary-card col-12 col-sm-3" key={id}> <Card className="primary-card col-12 col-sm-3" key={id}>
<h3>{primaries[id].name}</h3> <h3>{primaries[id].name}</h3>
<Card.Body className="primary-card-body"> <Card.Body className="primary-card-body">{markers}</Card.Body>
{ markers }
</Card.Body>
</Card> </Card>
); );
}); });
return ( return <div className="primary-tag row">{primaryCards}</div>;
<div className="primary-tag row">
{ primaryCards }
</div>
);
}; };

View File

@@ -5,7 +5,7 @@ import { useParams, useLocation, useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
import { LoadingIndicator } from 'src/components/Shared'; import { LoadingIndicator } from "src/components/Shared";
import { ScenePlayer } from "src/components/scenes/ScenePlayer/ScenePlayer"; import { ScenePlayer } from "src/components/scenes/ScenePlayer/ScenePlayer";
import { ScenePerformerPanel } from "./ScenePerformerPanel"; import { ScenePerformerPanel } from "./ScenePerformerPanel";
import { SceneMarkersPanel } from "./SceneMarkersPanel"; import { SceneMarkersPanel } from "./SceneMarkersPanel";
@@ -25,8 +25,7 @@ export const Scene: React.FC = () => {
const autoplay = queryParams?.autoplay === "true"; const autoplay = queryParams?.autoplay === "true";
useEffect(() => { useEffect(() => {
if(data?.findScene) if (data?.findScene) setScene(data.findScene);
setScene(data.findScene)
}, [data]); }, [data]);
function getInitialTimestamp() { function getInitialTimestamp() {
@@ -50,21 +49,14 @@ export const Scene: React.FC = () => {
return ( return (
<> <>
<ScenePlayer <ScenePlayer scene={scene} timestamp={timestamp} autoplay={autoplay} />
scene={scene}
timestamp={timestamp}
autoplay={autoplay}
/>
<div id="details-container" className="col col-sm-9 m-sm-auto"> <div id="details-container" className="col col-sm-9 m-sm-auto">
<Tabs id="scene-tabs" mountOnEnter> <Tabs id="scene-tabs" mountOnEnter>
<Tab eventKey="scene-details-panel" title="Details"> <Tab eventKey="scene-details-panel" title="Details">
<SceneDetailPanel scene={scene} /> <SceneDetailPanel scene={scene} />
</Tab> </Tab>
<Tab eventKey="scene-markers-panel" title="Markers"> <Tab eventKey="scene-markers-panel" title="Markers">
<SceneMarkersPanel <SceneMarkersPanel scene={scene} onClickMarker={onClickMarker} />
scene={scene}
onClickMarker={onClickMarker}
/>
</Tab> </Tab>
{scene.performers.length > 0 ? ( {scene.performers.length > 0 ? (
<Tab eventKey="scene-performer-panel" title="Performers"> <Tab eventKey="scene-performer-panel" title="Performers">
@@ -80,10 +72,18 @@ export const Scene: React.FC = () => {
) : ( ) : (
"" ""
)} )}
<Tab className="file-info-panel" eventKey="scene-file-info-panel" title="File Info"> <Tab
className="file-info-panel"
eventKey="scene-file-info-panel"
title="File Info"
>
<SceneFileInfoPanel scene={scene} /> <SceneFileInfoPanel scene={scene} />
</Tab> </Tab>
<Tab eventKey="scene-edit-panel" title="Edit" tabClassName="d-none d-sm-block"> <Tab
eventKey="scene-edit-panel"
title="Edit"
tabClassName="d-none d-sm-block"
>
<SceneEditPanel <SceneEditPanel
scene={scene} scene={scene}
onUpdate={newScene => setScene(newScene)} onUpdate={newScene => setScene(newScene)}

View File

@@ -10,7 +10,7 @@ interface ISceneDetailProps {
export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => { export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
function renderDetails() { function renderDetails() {
if (!props.scene.details || props.scene.details === "")return; if (!props.scene.details || props.scene.details === "") return;
return ( return (
<> <>
<h6>Details</h6> <h6>Details</h6>
@@ -38,7 +38,7 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
{props.scene.title ?? TextUtils.fileNameFromPath(props.scene.path)} {props.scene.title ?? TextUtils.fileNameFromPath(props.scene.path)}
</h3> </h3>
<div className="col-6 scene-details"> <div className="col-6 scene-details">
<h4>{props.scene.date ?? ''}</h4> <h4>{props.scene.date ?? ""}</h4>
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""} {props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
{props.scene.file.height && ( {props.scene.file.height && (
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
@@ -47,9 +47,15 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
{renderTags()} {renderTags()}
</div> </div>
<div className="col-4 offset-2"> <div className="col-4 offset-2">
{ props.scene.studio && ( {props.scene.studio && (
<Link className="studio-logo" to={`/studios/${props.scene.studio.id}`}> <Link
<img src={props.scene.studio.image_path ?? ''} alt={`${props.scene.studio.name} logo`} /> className="studio-logo"
to={`/studios/${props.scene.studio.id}`}
>
<img
src={props.scene.studio.image_path ?? ""}
alt={`${props.scene.studio.name} logo`}
/>
</Link> </Link>
)} )}
</div> </div>

View File

@@ -1,13 +1,7 @@
/* eslint-disable react/no-this-in-sfc */ /* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import { Button, Dropdown, DropdownButton, Form, Table } from "react-bootstrap";
Button,
Dropdown,
DropdownButton,
Form,
Table
} from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { import {
@@ -294,8 +288,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
); );
} }
if(isLoading) if (isLoading) return <LoadingIndicator />;
return <LoadingIndicator />;
return ( return (
<div className="form-container row"> <div className="form-container row">
@@ -329,8 +322,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
title: "Rating", title: "Rating",
value: rating, value: rating,
isEditing: true, isEditing: true,
onChange: (value: string) => setRating(Number.parseInt(value, 10)), onChange: (value: string) =>
selectOptions: ['', 1, 2, 3, 4, 5] setRating(Number.parseInt(value, 10)),
selectOptions: ["", 1, 2, 3, 4, 5]
})} })}
<tr> <tr>
<td>Gallery</td> <td>Gallery</td>
@@ -356,7 +350,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<td> <td>
<PerformerSelect <PerformerSelect
isMulti isMulti
onSelect={items => setPerformerIds(items.map(item => item.id))} onSelect={items =>
setPerformerIds(items.map(item => item.id))
}
ids={performerIds} ids={performerIds}
/> />
</td> </td>

View File

@@ -94,7 +94,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Frame Rate</span> <span className="col-4">Frame Rate</span>
<span className="col-8 text-truncate">{props.scene.file.framerate} frames per second</span> <span className="col-8 text-truncate">
{props.scene.file.framerate} frames per second
</span>
</div> </div>
); );
} }
@@ -106,7 +108,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Bit Rate</span> <span className="col-4">Bit Rate</span>
<span className="col-8 text-truncate">{TextUtils.bitRate(props.scene.file.bitrate ?? 0)}</span> <span className="col-8 text-truncate">
{TextUtils.bitRate(props.scene.file.bitrate ?? 0)}
</span>
</div> </div>
); );
} }
@@ -118,7 +122,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Video Codec</span> <span className="col-4">Video Codec</span>
<span className="col-8 text-truncate">{props.scene.file.video_codec}</span> <span className="col-8 text-truncate">
{props.scene.file.video_codec}
</span>
</div> </div>
); );
} }
@@ -130,7 +136,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Audio Codec</span> <span className="col-4">Audio Codec</span>
<span className="col-8 text-truncate">{props.scene.file.audio_codec}</span> <span className="col-8 text-truncate">
{props.scene.file.audio_codec}
</span>
</div> </div>
); );
} }

View File

@@ -1,8 +1,5 @@
import React from "react"; import React from "react";
import { import { Button, Form } from "react-bootstrap";
Button,
Form
} from "react-bootstrap";
import { Field, FieldProps, Form as FormikForm, Formik } from "formik"; import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
@@ -27,16 +24,19 @@ interface ISceneMarkerForm {
onClose: () => void; onClose: () => void;
} }
export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMarker, playerPosition, onClose }) => { export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
sceneID,
editingMarker,
playerPosition,
onClose
}) => {
const [sceneMarkerCreate] = StashService.useSceneMarkerCreate(); const [sceneMarkerCreate] = StashService.useSceneMarkerCreate();
const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate(); const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate();
const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy(); const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy();
const Toast = useToast(); const Toast = useToast();
const onSubmit = (values: IFormFields) => { const onSubmit = (values: IFormFields) => {
const variables: const variables: GQL.SceneMarkerUpdateInput | GQL.SceneMarkerCreateInput = {
| GQL.SceneMarkerUpdateInput
| GQL.SceneMarkerCreateInput = {
title: values.title, title: values.title,
seconds: parseFloat(values.seconds), seconds: parseFloat(values.seconds),
scene_id: sceneID, scene_id: sceneID,
@@ -54,7 +54,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
.then(onClose) .then(onClose)
.catch(err => Toast.error(err)); .catch(err => Toast.error(err));
} }
} };
const onDelete = () => { const onDelete = () => {
if (!editingMarker) return; if (!editingMarker) return;
@@ -62,7 +62,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
sceneMarkerDestroy({ variables: { id: editingMarker.id } }) sceneMarkerDestroy({ variables: { id: editingMarker.id } })
.then(onClose) .then(onClose)
.catch(err => Toast.error(err)); .catch(err => Toast.error(err));
} };
const renderTitleField = (fieldProps: FieldProps<string>) => ( const renderTitleField = (fieldProps: FieldProps<string>) => (
<div className="col-10"> <div className="col-10">
<MarkerTitleSuggest <MarkerTitleSuggest
@@ -84,7 +84,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
Math.round(playerPosition ?? 0) Math.round(playerPosition ?? 0)
) )
} }
numericValue={Number.parseInt(fieldProps.field.value ?? '0', 10)} numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
/> />
</div> </div>
); );
@@ -113,48 +113,43 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
/> />
); );
const values:IFormFields = { const values: IFormFields = {
title: editingMarker?.title ?? '', title: editingMarker?.title ?? "",
seconds: (editingMarker?.seconds ?? Math.round(playerPosition ?? 0)).toString(), seconds: (
primaryTagId: editingMarker?.primary_tag.id ?? '', editingMarker?.seconds ?? Math.round(playerPosition ?? 0)
).toString(),
primaryTagId: editingMarker?.primary_tag.id ?? "",
tagIds: editingMarker?.tags.map(tag => tag.id) ?? [] tagIds: editingMarker?.tags.map(tag => tag.id) ?? []
}; };
return ( return (
<Formik <Formik initialValues={values} onSubmit={onSubmit}>
initialValues={values}
onSubmit={onSubmit}
>
<FormikForm> <FormikForm>
<div> <div>
<Form.Group className="row"> <Form.Group className="row">
<Form.Label htmlFor="title" className="col-2"> <Form.Label htmlFor="title" className="col-2">
Scene Marker Title Scene Marker Title
</Form.Label> </Form.Label>
<Field name="title"> <Field name="title">{renderTitleField}</Field>
{renderTitleField}
</Field>
</Form.Group> </Form.Group>
<Form.Group className="row"> <Form.Group className="row">
<Form.Label htmlFor="primaryTagId" className="col-2"> <Form.Label htmlFor="primaryTagId" className="col-2">
Primary Tag Primary Tag
</Form.Label> </Form.Label>
<div className="col-6"> <div className="col-6">
<Field name="primaryTagId"> <Field name="primaryTagId">{renderPrimaryTagField}</Field>
{renderPrimaryTagField}
</Field>
</div> </div>
<Form.Label htmlFor="seconds" className="col-1">Time</Form.Label> <Form.Label htmlFor="seconds" className="col-1">
<Field name="seconds"> Time
{renderSecondsField} </Form.Label>
</Field> <Field name="seconds">{renderSecondsField}</Field>
</Form.Group> </Form.Group>
<Form.Group className="row"> <Form.Group className="row">
<Form.Label htmlFor="tagIds" className="col-2">Tags</Form.Label> <Form.Label htmlFor="tagIds" className="col-2">
Tags
</Form.Label>
<div className="col-10"> <div className="col-10">
<Field name="tagIds"> <Field name="tagIds">{renderTagsField}</Field>
{renderTagsField}
</Field>
</div> </div>
</Form.Group> </Form.Group>
</div> </div>
@@ -165,7 +160,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
<Button type="button" onClick={onClose}> <Button type="button" onClick={onClose}>
Cancel Cancel
</Button> </Button>
{ editingMarker && ( {editingMarker && (
<Button <Button
variant="danger" variant="danger"
style={{ float: "right", marginRight: "10px" }} style={{ float: "right", marginRight: "10px" }}
@@ -178,4 +173,4 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
</FormikForm> </FormikForm>
</Formik> </Formik>
); );
} };

View File

@@ -1,12 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { import { Button } from "react-bootstrap";
Button,
} from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { WallPanel } from "src/components/Wall/WallPanel"; import { WallPanel } from "src/components/Wall/WallPanel";
import { JWUtils } from "src/utils"; import { JWUtils } from "src/utils";
import { PrimaryTags } from './PrimaryTags'; import { PrimaryTags } from "./PrimaryTags";
import { SceneMarkerForm } from './SceneMarkerForm'; import { SceneMarkerForm } from "./SceneMarkerForm";
interface ISceneMarkersPanelProps { interface ISceneMarkersPanelProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
@@ -17,10 +15,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
props: ISceneMarkersPanelProps props: ISceneMarkersPanelProps
) => { ) => {
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false); const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
const [ const [editingMarker, setEditingMarker] = useState<
editingMarker, GQL.SceneMarkerDataFragment
setEditingMarker >();
] = useState<GQL.SceneMarkerDataFragment>();
const jwplayer = JWUtils.getPlayer(); const jwplayer = JWUtils.getPlayer();
@@ -38,7 +35,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
setIsEditorOpen(false); setIsEditorOpen(false);
}; };
if(isEditorOpen) if (isEditorOpen)
return ( return (
<SceneMarkerForm <SceneMarkerForm
sceneID={props.scene.id} sceneID={props.scene.id}

View File

@@ -36,9 +36,7 @@ export const SceneMarkerList: React.FC = () => {
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindSceneMarkers(filterCopy); const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
if ( if (singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1) {
singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1
) {
// navigate to the scene player page // navigate to the scene player page
const url = NavUtils.makeSceneMarkerUrl( const url = NavUtils.makeSceneMarkerUrl(
singleResult.data.findSceneMarkers.scene_markers[0] singleResult.data.findSceneMarkers.scene_markers[0]

View File

@@ -3,7 +3,7 @@ import ReactJWPlayer from "react-jw-player";
import { HotKeys } from "react-hotkeys"; import { HotKeys } from "react-hotkeys";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { JWUtils } from 'src/utils'; import { JWUtils } from "src/utils";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
interface IScenePlayerProps { interface IScenePlayerProps {
@@ -206,7 +206,10 @@ export class ScenePlayerImpl extends React.Component<
public render() { public render() {
return ( return (
<HotKeys keyMap={KeyMap} handlers={this.KeyHandlers} className="row"> <HotKeys keyMap={KeyMap} handlers={this.KeyHandlers} className="row">
<div id="jwplayer-container" className="w-100 col-sm-9 m-sm-auto no-gutter" > <div
id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter"
>
{this.renderPlayer()} {this.renderPlayer()}
<ScenePlayerScrubber <ScenePlayerScrubber
scene={this.props.scene} scene={this.props.scene}

View File

@@ -3,7 +3,11 @@ import { Button, Form } 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, StudioSelect, LoadingIndicator } from "src/components/Shared"; import {
FilterSelect,
StudioSelect,
LoadingIndicator
} from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
interface IListOperationProps { interface IListOperationProps {
@@ -183,7 +187,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
useEffect(() => { useEffect(() => {
const state = props.selected; 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;
@@ -223,7 +227,6 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
setIsLoading(false); setIsLoading(false);
}, [props.selected]); }, [props.selected]);
function renderMultiSelect( function renderMultiSelect(
type: "performers" | "tags", type: "performers" | "tags",
ids: string[] | undefined ids: string[] | undefined
@@ -249,19 +252,21 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
); );
} }
if(isLoading) if (isLoading) return <LoadingIndicator />;
return <LoadingIndicator />;
function render() { function render() {
return ( return (
<div className="operation-container"> <div className="operation-container">
<Form.Group controlId="rating" className="operation-item rating-operation"> <Form.Group
controlId="rating"
className="operation-item rating-operation"
>
<Form.Label>Rating</Form.Label> <Form.Label>Rating</Form.Label>
<Form.Control <Form.Control
as="select" as="select"
onChange={(event: any) => setRating(event.target.value)} onChange={(event: any) => setRating(event.target.value)}
> >
{["", '1', '2', '3', '4', '5'].map(opt => ( {["", "1", "2", "3", "4", "5"].map(opt => (
<option selected={opt === rating} value={opt}> <option selected={opt === rating} value={opt}>
{opt} {opt}
</option> </option>

View File

@@ -15,7 +15,7 @@ import {
FindStudiosQueryResult, FindStudiosQueryResult,
FindPerformersQueryResult FindPerformersQueryResult
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { LoadingIndicator } from 'src/components/Shared'; import { LoadingIndicator } from "src/components/Shared";
import { ListFilter } from "src/components/list/ListFilter"; import { ListFilter } from "src/components/list/ListFilter";
import { Pagination } from "src/components/list/Pagination"; import { Pagination } from "src/components/list/Pagination";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";

View File

@@ -3,4 +3,4 @@ export { default as NavUtils } from "./navigation";
export { default as TableUtils } from "./table"; export { default as TableUtils } from "./table";
export { default as TextUtils } from "./text"; export { default as TextUtils } from "./text";
export { default as DurationUtils } from "./duration"; export { default as DurationUtils } from "./duration";
export { default as JWUtils } from './jwplayer'; export { default as JWUtils } from "./jwplayer";

View File

@@ -1,7 +1,5 @@
const playerID = "main-jwplayer"; const playerID = "main-jwplayer";
const getPlayer = () => ( const getPlayer = () => (window as any).jwplayer(playerID);
(window as any).jwplayer(playerID)
)
export default { export default {
playerID, playerID,

View File

@@ -94,7 +94,11 @@ const renderHtmlSelect = (options: {
options.onChange(event.currentTarget.value) options.onChange(event.currentTarget.value)
} }
> >
{ options.selectOptions.map(opt => <option value={opt} key={opt}>{opt}</option>)} {options.selectOptions.map(opt => (
<option value={opt} key={opt}>
{opt}
</option>
))}
</Form.Control> </Form.Control>
</td> </td>
</tr> </tr>