This commit is contained in:
Infinite 2020-02-09 13:22:19 +01:00
parent a7df23c54d
commit a43cae43c0
33 changed files with 246 additions and 194 deletions

View file

@ -11,7 +11,7 @@
"lint": "yarn lint:css && yarn lint:js", "lint": "yarn lint:css && yarn lint:js",
"lint:js": "eslint --cache src/**/*.{ts,tsx}", "lint:js": "eslint --cache src/**/*.{ts,tsx}",
"lint:css": "stylelint 'src/**/*.scss'", "lint:css": "stylelint 'src/**/*.scss'",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", "format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx}\"",
"gqlgen": "gql-gen --config codegen.yml", "gqlgen": "gql-gen --config codegen.yml",
"extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'" "extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'"
}, },

View file

@ -1,13 +1,13 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { IntlProvider } from 'react-intl'; import { IntlProvider } from "react-intl";
import { ToastProvider } from "src/hooks/Toast"; import { ToastProvider } from "src/hooks/Toast";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons"; import { fas } from "@fortawesome/free-solid-svg-icons";
import locales from 'src/locale'; import locales from "src/locale";
import { StashService } from 'src/core/StashService'; import { StashService } from "src/core/StashService";
import { flattenMessages } from 'src/utils'; import { flattenMessages } from "src/utils";
import { ErrorBoundary } from "./components/ErrorBoundary"; import { ErrorBoundary } from "./components/ErrorBoundary";
import Galleries from "./components/Galleries/Galleries"; import Galleries from "./components/Galleries/Galleries";
import { MainNavbar } from "./components/MainNavbar"; import { MainNavbar } from "./components/MainNavbar";
@ -20,13 +20,12 @@ import Studios from "./components/Studios/Studios";
import { TagList } from "./components/Tags/TagList"; import { TagList } from "./components/Tags/TagList";
import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser"; import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser";
library.add(fas); library.add(fas);
export const App: React.FC = () => { export const App: React.FC = () => {
const config = StashService.useConfiguration(); const config = StashService.useConfiguration();
const language = config.data?.configuration?.interface?.language ?? 'en-US'; const language = config.data?.configuration?.interface?.language ?? "en-US";
const messageLanguage = language.slice(0,2); const messageLanguage = language.slice(0, 2);
const messages = flattenMessages((locales as any)[messageLanguage]); const messages = flattenMessages((locales as any)[messageLanguage]);
return ( return (

View file

@ -1,6 +1,6 @@
import { debounce } from "lodash"; import { debounce } from "lodash";
import React, { SyntheticEvent, useCallback, useState } from "react"; import React, { SyntheticEvent, useCallback, useState } from "react";
import { SortDirectionEnum } from 'src/core/generated-graphql'; import { SortDirectionEnum } from "src/core/generated-graphql";
import { import {
Badge, Badge,
Button, Button,

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from "react-intl";
import { Nav, Navbar, Button } from "react-bootstrap"; import { Nav, Navbar, Button } from "react-bootstrap";
import { IconName } from "@fortawesome/fontawesome-svg-core"; import { IconName } from "@fortawesome/fontawesome-svg-core";
import { LinkContainer } from "react-router-bootstrap"; import { LinkContainer } from "react-router-bootstrap";
@ -60,7 +60,9 @@ export const MainNavbar: React.FC = () => {
"" ""
) : ( ) : (
<LinkContainer to={path}> <LinkContainer to={path}>
<Button variant="primary"><FormattedMessage id="new" defaultMessage="New" /></Button> <Button variant="primary">
<FormattedMessage id="new" defaultMessage="New" />
</Button>
</LinkContainer> </LinkContainer>
); );

View file

@ -9,13 +9,12 @@ interface IPerformerCardProps {
ageFromDate?: string; ageFromDate?: string;
} }
export const PerformerCard: React.FC<IPerformerCardProps> = ( export const PerformerCard: React.FC<IPerformerCardProps> = ({
{ performer, ageFromDate } performer,
) => { ageFromDate
}) => {
const age = TextUtils.age(performer.birthdate, ageFromDate); const age = TextUtils.age(performer.birthdate, ageFromDate);
const ageString = `${age} years old${ const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`;
ageFromDate ? " in this scene." : "."
}`;
function maybeRenderFavoriteBanner() { function maybeRenderFavoriteBanner() {
if (performer.favorite === false) { if (performer.favorite === false) {
@ -26,12 +25,12 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (
return ( return (
<Card className="performer-card"> <Card className="performer-card">
<Link <Link to={`/performers/${performer.id}`}>
to={`/performers/${performer.id}`}
>
<img <img
className="image-thumbnail card-image" className="image-thumbnail card-image"
alt={performer.name ?? ''} src={performer.image_path ?? ''} /> alt={performer.name ?? ""}
src={performer.image_path ?? ""}
/>
{maybeRenderFavoriteBanner()} {maybeRenderFavoriteBanner()}
</Link> </Link>
<div className="card-section"> <div className="card-section">
@ -39,10 +38,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""} {age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<div className="text-muted"> <div className="text-muted">
Stars in {performer.scene_count}{" "} Stars in {performer.scene_count}{" "}
<Link to={NavUtils.makePerformerScenesUrl(performer)}> <Link to={NavUtils.makePerformerScenesUrl(performer)}>scenes</Link>.
scenes
</Link>
.
</div> </div>
</div> </div>
</Card> </Card>

View file

@ -296,7 +296,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
</td> </td>
<td> <td>
<Form.Control <Form.Control
value={url ?? ''} value={url ?? ""}
readOnly={!isEditing} readOnly={!isEditing}
plaintext={!isEditing} plaintext={!isEditing}
placeholder="URL" placeholder="URL"

View file

@ -17,10 +17,12 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
const renderPerformerRow = (performer: GQL.PerformerDataFragment) => ( const renderPerformerRow = (performer: GQL.PerformerDataFragment) => (
<tr key={performer.id}> <tr key={performer.id}>
<td> <td>
<Link <Link to={`/performers/${performer.id}`}>
to={`/performers/${performer.id}`} <img
> className="image-thumbnail"
<img className="image-thumbnail" alt={performer.name ?? ""} src={performer.image_path ?? ''} /> alt={performer.name ?? ""}
src={performer.image_path ?? ""}
/>
</Link> </Link>
</td> </td>
<td className="text-left"> <td className="text-left">
@ -29,13 +31,12 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
</Link> </Link>
</td> </td>
<td>{performer.aliases ? performer.aliases : ""}</td> <td>{performer.aliases ? performer.aliases : ""}</td>
<td>{ <td>
performer.favorite && ( {performer.favorite && (
<Button disabled className="favorite"> <Button disabled className="favorite">
<Icon icon="heart" /> <Icon icon="heart" />
</Button> </Button>
) )}
}
</td> </td>
<td> <td>
<Link to={NavUtils.makePerformerScenesUrl(performer)}> <Link to={NavUtils.makePerformerScenesUrl(performer)}>

View file

@ -192,9 +192,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
checked={capitalizeTitle} checked={capitalizeTitle}
onChange={() => setCapitalizeTitle(!capitalizeTitle)} onChange={() => setCapitalizeTitle(!capitalizeTitle)}
/> />
<Form.Label htmlFor="capitalize-title"> <Form.Label htmlFor="capitalize-title">Capitalize title</Form.Label>
Capitalize title
</Form.Label>
</Form.Group> </Form.Group>
{/* TODO - mapping stuff will go here */} {/* TODO - mapping stuff will go here */}
@ -238,7 +236,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
className="col-1 filter-item" className="col-1 filter-item"
> >
{PAGE_SIZE_OPTIONS.map(val => ( {PAGE_SIZE_OPTIONS.map(val => (
<option key={val} value={val}>{val}</option> <option key={val} value={val}>
{val}
</option>
))} ))}
</Form.Control> </Form.Control>
</Form.Group> </Form.Group>

View file

@ -493,7 +493,9 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<div> <div>
{elements.map((name: string) => ( {elements.map((name: string) => (
<Badge key={name} variant="secondary">{name}</Badge> <Badge key={name} variant="secondary">
{name}
</Badge>
))} ))}
</div> </div>
); );
@ -591,7 +593,9 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<tr className="scene-parser-row"> <tr className="scene-parser-row">
<td className="text-left parser-field-filename">{props.scene.filename}</td> <td className="text-left parser-field-filename">
{props.scene.filename}
</td>
<SceneParserField <SceneParserField
key="title" key="title"
fieldName="Title" fieldName="Title"

View file

@ -205,7 +205,11 @@ export class ScenePlayerImpl extends React.Component<
public render() { public render() {
return ( return (
<HotKeys keyMap={KeyMap} handlers={this.KeyHandlers} className="row scene-player"> <HotKeys
keyMap={KeyMap}
handlers={this.KeyHandlers}
className="row scene-player"
>
<div <div
id="jwplayer-container" id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter" className="w-100 col-sm-9 m-sm-auto no-gutter"

View file

@ -1 +1 @@
export { ScenePlayer } from './ScenePlayer'; export { ScenePlayer } from "./ScenePlayer";

View file

@ -66,10 +66,15 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<div className="scene-studio-overlay"> <div className="scene-studio-overlay">
<Link to={`/studios/${props.scene.studio.id}`}> <Link to={`/studios/${props.scene.studio.id}`}>
{ showStudioAsText {showStudioAsText ? (
? props.scene.studio.name props.scene.studio.name
: <img className="image-thumbnail" alt={props.scene.studio.name} src={props.scene.studio.image_path ?? ''} /> ) : (
} <img
className="image-thumbnail"
alt={props.scene.studio.name}
src={props.scene.studio.image_path ?? ""}
/>
)}
</Link> </Link>
</div> </div>
); );
@ -101,7 +106,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
to={`/performers/${performer.id}`} to={`/performers/${performer.id}`}
className="performer-tag col m-auto zoom-2" className="performer-tag col m-auto zoom-2"
> >
<img className="image-thumbnail" alt={performer.name ?? ''} src={performer.image_path ?? ''} /> <img
className="image-thumbnail"
alt={performer.name ?? ""}
src={performer.image_path ?? ""}
/>
</Link> </Link>
<TagLink key={performer.id} performer={performer} className="d-block" /> <TagLink key={performer.id} performer={performer} className="d-block" />
</div> </div>
@ -144,7 +153,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
<span>{props.scene.o_counter}</span> <span>{props.scene.o_counter}</span>
</Button> </Button>
</div> </div>
) );
} }
} }
@ -207,15 +216,12 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}} }}
/> />
{maybeRenderSceneStudioOverlay()} {maybeRenderSceneStudioOverlay()}
<Link <Link to={`/scenes/${props.scene.id}`} className="scene-card-link">
to={`/scenes/${props.scene.id}`}
className="scene-card-link"
>
{maybeRenderRatingBanner()} {maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()} {maybeRenderSceneSpecsOverlay()}
<video <video
loop loop
className={cx('scene-card-video', { portrait: isPortrait() })} className={cx("scene-card-video", { portrait: isPortrait() })}
poster={props.scene.paths.screenshot || ""} poster={props.scene.paths.screenshot || ""}
ref={videoHoverHook.videoEl} ref={videoHoverHook.videoEl}
> >
@ -229,13 +235,9 @@ export const SceneCard: React.FC<ISceneCardProps> = (
: TextUtils.fileNameFromPath(props.scene.path)} : TextUtils.fileNameFromPath(props.scene.path)}
</h5> </h5>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
{ props.scene.details && ( {props.scene.details && (
<p> <p>
{TextUtils.truncate( {TextUtils.truncate(props.scene.details, 100, "... (continued)")}
props.scene.details,
100,
"... (continued)"
)}
</p> </p>
)} )}
</div> </div>

View file

@ -1,27 +1,24 @@
import React from "react"; import React from "react";
import { Button, Spinner } from 'react-bootstrap'; import { Button, Spinner } from "react-bootstrap";
import { Icon, HoverPopover, SweatDrops } from 'src/components/Shared'; import { Icon, HoverPopover, SweatDrops } from "src/components/Shared";
export interface IOCounterButtonProps { export interface IOCounterButtonProps {
loading: boolean loading: boolean;
value: number value: number;
onIncrement: () => void onIncrement: () => void;
onDecrement: () => void onDecrement: () => void;
onReset: () => void onReset: () => void;
onMenuOpened?: () => void onMenuOpened?: () => void;
onMenuClosed?: () => void onMenuClosed?: () => void;
} }
export const OCounterButton: React.FC<IOCounterButtonProps> = (props: IOCounterButtonProps) => { export const OCounterButton: React.FC<IOCounterButtonProps> = (
if(props.loading) props: IOCounterButtonProps
return <Spinner animation="border" role="status" />; ) => {
if (props.loading) return <Spinner animation="border" role="status" />;
const renderButton = () => ( const renderButton = () => (
<Button <Button className="minimal" onClick={props.onIncrement} variant="secondary">
className="minimal"
onClick={props.onIncrement}
variant="secondary"
>
<SweatDrops /> <SweatDrops />
<span className="ml-2">{props.value}</span> <span className="ml-2">{props.value}</span>
</Button> </Button>
@ -59,9 +56,9 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (props: IOCounterB
onOpen={props.onMenuOpened} onOpen={props.onMenuOpened}
onClose={props.onMenuClosed} onClose={props.onMenuClosed}
> >
{ renderButton() } {renderButton()}
</HoverPopover> </HoverPopover>
); );
} }
return renderButton(); return renderButton();
} };

View file

@ -6,14 +6,14 @@ 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 { useToast } from 'src/hooks'; import { useToast } from "src/hooks";
import { ScenePlayer } from "src/components/ScenePlayer"; import { ScenePlayer } from "src/components/ScenePlayer";
import { ScenePerformerPanel } from "./ScenePerformerPanel"; import { ScenePerformerPanel } from "./ScenePerformerPanel";
import { SceneMarkersPanel } from "./SceneMarkersPanel"; import { SceneMarkersPanel } from "./SceneMarkersPanel";
import { SceneFileInfoPanel } from "./SceneFileInfoPanel"; import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
import { SceneEditPanel } from "./SceneEditPanel"; import { SceneEditPanel } from "./SceneEditPanel";
import { SceneDetailPanel } from "./SceneDetailPanel"; import { SceneDetailPanel } from "./SceneDetailPanel";
import { OCounterButton } from './OCounterButton'; import { OCounterButton } from "./OCounterButton";
export const Scene: React.FC = () => { export const Scene: React.FC = () => {
const { id = "new" } = useParams(); const { id = "new" } = useParams();
@ -48,46 +48,43 @@ export const Scene: React.FC = () => {
const modifiedScene = { ...scene } as GQL.SceneDataFragment; const modifiedScene = { ...scene } as GQL.SceneDataFragment;
modifiedScene.o_counter = newValue; modifiedScene.o_counter = newValue;
setScene(modifiedScene); setScene(modifiedScene);
} };
const onIncrementClick = async () => { const onIncrementClick = async () => {
try { try {
setOLoading(true); setOLoading(true);
const result = await incrementO(); const result = await incrementO();
if(result.data) if (result.data) updateOCounter(result.data.sceneIncrementO);
updateOCounter(result.data.sceneIncrementO);
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
} finally { } finally {
setOLoading(false); setOLoading(false);
} }
} };
const onDecrementClick = async () => { const onDecrementClick = async () => {
try { try {
setOLoading(true); setOLoading(true);
const result = await decrementO(); const result = await decrementO();
if(result.data) if (result.data) updateOCounter(result.data.sceneDecrementO);
updateOCounter(result.data.sceneDecrementO);
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
} finally { } finally {
setOLoading(false); setOLoading(false);
} }
} };
const onResetClick = async () => { const onResetClick = async () => {
try { try {
setOLoading(true); setOLoading(true);
const result = await resetO(); const result = await resetO();
if(result.data) if (result.data) updateOCounter(result.data.sceneResetO);
updateOCounter(result.data.sceneResetO);
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
} finally { } finally {
setOLoading(false); setOLoading(false);
} }
} };
function onClickMarker(marker: GQL.SceneMarkerDataFragment) { function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
setTimestamp(marker.seconds); setTimestamp(marker.seconds);

View file

@ -48,9 +48,7 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
</div> </div>
<div className="col-4 offset-2"> <div className="col-4 offset-2">
{props.scene.studio && ( {props.scene.studio && (
<Link <Link to={`/studios/${props.scene.studio.id}`}>
to={`/studios/${props.scene.studio.id}`}
>
<img <img
src={props.scene.studio.image_path ?? ""} src={props.scene.studio.image_path ?? ""}
alt={`${props.scene.studio.name} logo`} alt={`${props.scene.studio.name} logo`}

View file

@ -157,7 +157,12 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
Submit Submit
</Button> </Button>
<Button variant="secondary" type="button" onClick={onClose} className="ml-2"> <Button
variant="secondary"
type="button"
onClick={onClose}
className="ml-2"
>
Cancel Cancel
</Button> </Button>
{editingMarker && ( {editingMarker && (

View file

@ -11,27 +11,30 @@ interface ISceneListTableProps {
export const SceneListTable: React.FC<ISceneListTableProps> = ( export const SceneListTable: React.FC<ISceneListTableProps> = (
props: ISceneListTableProps props: ISceneListTableProps
) => { ) => {
const renderTags = (tags: GQL.Tag[]) => ( const renderTags = (tags: GQL.Tag[]) =>
tags.map(tag => ( tags.map(tag => (
<Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}> <Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}>
<h6>{tag.name}</h6> <h6>{tag.name}</h6>
</Link> </Link>
)) ));
);
const renderPerformers = (performers: Partial<GQL.Performer>[]) => ( const renderPerformers = (performers: Partial<GQL.Performer>[]) =>
performers.map(performer => ( performers.map(performer => (
<Link key={performer.id} to={NavUtils.makePerformerScenesUrl(performer)} /> <Link
)) key={performer.id}
); to={NavUtils.makePerformerScenesUrl(performer)}
/>
));
const renderSceneRow = (scene: GQL.SlimSceneDataFragment) => ( const renderSceneRow = (scene: GQL.SlimSceneDataFragment) => (
<tr key={scene.id}> <tr key={scene.id}>
<td> <td>
<Link <Link to={`/scenes/${scene.id}`}>
to={`/scenes/${scene.id}`} <img
> className="image-thumbnail"
<img className="image-thumbnail" alt={scene.title ?? ''} src={scene.paths.screenshot ?? ''} /> alt={scene.title ?? ""}
src={scene.paths.screenshot ?? ""}
/>
</Link> </Link>
</td> </td>
<td className="text-left"> <td className="text-left">
@ -42,17 +45,21 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
</Link> </Link>
</td> </td>
<td>{scene.rating ? scene.rating : ""}</td> <td>{scene.rating ? scene.rating : ""}</td>
<td>{scene.file.duration && TextUtils.secondsToTimestamp(scene.file.duration) }</td> <td>
{scene.file.duration &&
TextUtils.secondsToTimestamp(scene.file.duration)}
</td>
<td>{renderTags(scene.tags)}</td> <td>{renderTags(scene.tags)}</td>
<td>{renderPerformers(scene.performers)}</td> <td>{renderPerformers(scene.performers)}</td>
<td>{ scene.studio && ( <td>
<Link to={NavUtils.makeStudioScenesUrl(scene.studio)}> {scene.studio && (
<h6>{scene.studio.name}</h6> <Link to={NavUtils.makeStudioScenesUrl(scene.studio)}>
</Link> <h6>{scene.studio.name}</h6>
)} </Link>
)}
</td> </td>
</tr> </tr>
) );
return ( return (
<div className="row table-list col col-lg-8 mx-auto"> <div className="row table-list col col-lg-8 mx-auto">

View file

@ -14,7 +14,7 @@ export const SettingsInterfacePanel: React.FC = () => {
const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false); const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false);
const [css, setCSS] = useState<string>(); const [css, setCSS] = useState<string>();
const [cssEnabled, setCSSEnabled] = useState<boolean>(false); const [cssEnabled, setCSSEnabled] = useState<boolean>(false);
const [language, setLanguage] = useState<string>('en'); const [language, setLanguage] = useState<string>("en");
const [updateInterfaceConfig] = StashService.useConfigureInterface({ const [updateInterfaceConfig] = StashService.useConfigureInterface({
soundOnPreview, soundOnPreview,
@ -36,7 +36,7 @@ export const SettingsInterfacePanel: React.FC = () => {
setShowStudioAsText(iCfg?.showStudioAsText ?? false); setShowStudioAsText(iCfg?.showStudioAsText ?? false);
setCSS(iCfg?.css ?? ""); setCSS(iCfg?.css ?? "");
setCSSEnabled(iCfg?.cssEnabled ?? false); setCSSEnabled(iCfg?.cssEnabled ?? false);
setLanguage(iCfg?.language ?? 'en-US'); setLanguage(iCfg?.language ?? "en-US");
}, [config]); }, [config]);
async function onSave() { async function onSave() {
@ -50,10 +50,8 @@ export const SettingsInterfacePanel: React.FC = () => {
} }
} }
if(error) if (error) return <h1>{error.message}</h1>;
return <h1>{error.message}</h1>; if (loading) return <LoadingIndicator />;
if(loading)
return <LoadingIndicator />;
return ( return (
<> <>
@ -64,7 +62,9 @@ export const SettingsInterfacePanel: React.FC = () => {
as="select" as="select"
className="col-4" className="col-4"
value={language} value={language}
onChange={(e:React.FormEvent<HTMLSelectElement>) => setLanguage(e.currentTarget.value)} onChange={(e: React.FormEvent<HTMLSelectElement>) =>
setLanguage(e.currentTarget.value)
}
> >
<option value="en-US">English (United States)</option> <option value="en-US">English (United States)</option>
<option value="en-GB">English (United Kingdom)</option> <option value="en-GB">English (United Kingdom)</option>

View file

@ -29,7 +29,7 @@ export const HoverPopover: React.FC<IHoverPopover> = ({
const handleMouseEnter = useCallback(() => { const handleMouseEnter = useCallback(() => {
window.clearTimeout(leaveTimer.current); window.clearTimeout(leaveTimer.current);
enterTimer.current = window.setTimeout(() => { enterTimer.current = window.setTimeout(() => {
setShow(true) setShow(true);
onOpen?.(); onOpen?.();
}, enterDelay); }, enterDelay);
}, [enterDelay, onOpen]); }, [enterDelay, onOpen]);
@ -37,7 +37,7 @@ export const HoverPopover: React.FC<IHoverPopover> = ({
const handleMouseLeave = useCallback(() => { const handleMouseLeave = useCallback(() => {
window.clearTimeout(enterTimer.current); window.clearTimeout(enterTimer.current);
leaveTimer.current = window.setTimeout(() => { leaveTimer.current = window.setTimeout(() => {
setShow(false) setShow(false);
onClose?.(); onClose?.();
}, leaveDelay); }, leaveDelay);
}, [leaveDelay, onClose]); }, [leaveDelay, onClose]);

View file

@ -9,7 +9,11 @@ interface IIcon {
} }
const Icon: React.FC<IIcon> = ({ icon, className, color }) => ( const Icon: React.FC<IIcon> = ({ icon, className, color }) => (
<FontAwesomeIcon icon={icon} className={`fa-icon ${className}`} color={color} /> <FontAwesomeIcon
icon={icon}
className={`fa-icon ${className}`}
color={color}
/>
); );
export default Icon; export default Icon;

View file

@ -343,7 +343,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
}), }),
multiValueRemove: (base: CSSProperties, state: any) => ({ multiValueRemove: (base: CSSProperties, state: any) => ({
...base, ...base,
color: state.isFocused ? base.color: '#333333' color: state.isFocused ? base.color : "#333333"
}) })
}; };

View file

@ -1,9 +1,22 @@
import React from 'react'; import React from "react";
export const SweatDrops = () => ( export const SweatDrops = () => (
<span> <span>
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" style={{transform: "rotate(360deg)"}} preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"> <svg
<path fill="currentColor" d="M22.855.758L7.875 7.024l12.537 9.733c2.633 2.224 6.377 2.937 9.77 1.518c4.826-2.018 7.096-7.576 5.072-12.413C33.232 1.024 27.68-1.261 22.855.758zm-9.962 17.924L2.05 10.284L.137 23.529a7.993 7.993 0 0 0 2.958 7.803a8.001 8.001 0 0 0 9.798-12.65zm15.339 7.015l-8.156-4.69l-.033 9.223c-.088 2 .904 3.98 2.75 5.041a5.462 5.462 0 0 0 7.479-2.051c1.499-2.644.589-6.013-2.04-7.523z" /> xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
focusable="false"
width="1em"
height="1em"
style={{ transform: "rotate(360deg)" }}
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 36 36"
>
<path
fill="currentColor"
d="M22.855.758L7.875 7.024l12.537 9.733c2.633 2.224 6.377 2.937 9.77 1.518c4.826-2.018 7.096-7.576 5.072-12.413C33.232 1.024 27.68-1.261 22.855.758zm-9.962 17.924L2.05 10.284L.137 23.529a7.993 7.993 0 0 0 2.958 7.803a8.001 8.001 0 0 0 9.798-12.65zm15.339 7.015l-8.156-4.69l-.033 9.223c-.088 2 .904 3.98 2.75 5.041a5.462 5.462 0 0 0 7.479-2.051c1.499-2.644.589-6.013-2.04-7.523z"
/>
<rect x="0" y="0" width="36" height="36" fill="rgba(0, 0, 0, 0)" /> <rect x="0" y="0" width="36" height="36" fill="rgba(0, 0, 0, 0)" />
</svg> </svg>
</span> </span>

View file

@ -16,4 +16,4 @@ 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";
export { SweatDrops } from './SweatDrops'; export { SweatDrops } from "./SweatDrops";

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { FormattedMessage, FormattedNumber } from 'react-intl'; import { FormattedMessage, FormattedNumber } from "react-intl";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
export const Stats: React.FC = () => { export const Stats: React.FC = () => {
@ -17,9 +17,7 @@ export const Stats: React.FC = () => {
<div className="flex-grow-1"> <div className="flex-grow-1">
<div> <div>
<p className="heading"> <p className="heading">
<FormattedMessage <FormattedMessage id="scenes" defaultMessage="Scenes" />
id="scenes"
defaultMessage="Scenes" />
</p> </p>
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.scene_count} /> <FormattedNumber value={data.stats.scene_count} />
@ -29,9 +27,7 @@ export const Stats: React.FC = () => {
<div className="flex-grow-1"> <div className="flex-grow-1">
<div> <div>
<p className="heading"> <p className="heading">
<FormattedMessage <FormattedMessage id="galleries" defaultMessage="Galleries" />
id="galleries"
defaultMessage="Galleries" />
</p> </p>
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.gallery_count} /> <FormattedNumber value={data.stats.gallery_count} />
@ -41,9 +37,7 @@ export const Stats: React.FC = () => {
<div className="flex-grow-1"> <div className="flex-grow-1">
<div> <div>
<p className="heading"> <p className="heading">
<FormattedMessage <FormattedMessage id="performers" defaultMessage="Performers" />
id="performers"
defaultMessage="Performers" />
</p> </p>
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.performer_count} /> <FormattedNumber value={data.stats.performer_count} />
@ -53,9 +47,7 @@ export const Stats: React.FC = () => {
<div className="flex-grow-1"> <div className="flex-grow-1">
<div> <div>
<p className="heading"> <p className="heading">
<FormattedMessage <FormattedMessage id="studios" defaultMessage="Studios" />
id="studios"
defaultMessage="Studios" />
</p> </p>
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.studio_count} /> <FormattedNumber value={data.stats.studio_count} />
@ -65,9 +57,7 @@ export const Stats: React.FC = () => {
<div className="flex-grow-1"> <div className="flex-grow-1">
<div> <div>
<p className="heading"> <p className="heading">
<FormattedMessage <FormattedMessage id="tags" defaultMessage="Tags" />
id="tags"
defaultMessage="Tags" />
</p> </p>
<p className="title"> <p className="title">
<FormattedNumber value={data.stats.tag_count} /> <FormattedNumber value={data.stats.tag_count} />

View file

@ -11,7 +11,11 @@ export const StudioCard: React.FC<IProps> = ({ studio }) => {
return ( return (
<Card className="studio-card"> <Card className="studio-card">
<Link to={`/studios/${studio.id}`} className="studio-card-header"> <Link to={`/studios/${studio.id}`} className="studio-card-header">
<img className="studio-card-image" alt={studio.name} src={studio.image_path ?? ""} /> <img
className="studio-card-image"
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

@ -103,16 +103,26 @@ export const TagList: React.FC = () => {
{tag.name} {tag.name}
</Button> </Button>
<div className="ml-auto"> <div className="ml-auto">
<Button variant="secondary" className="tag-list-button" onClick={() => onAutoTag(tag)}> <Button
variant="secondary"
className="tag-list-button"
onClick={() => onAutoTag(tag)}
>
Auto Tag Auto Tag
</Button> </Button>
<Button variant="secondary" className="tag-list-button"> <Button variant="secondary" className="tag-list-button">
<Link to={NavUtils.makeTagScenesUrl(tag)} className="tag-list-anchor"> <Link
to={NavUtils.makeTagScenesUrl(tag)}
className="tag-list-anchor"
>
Scenes: {tag.scene_count} Scenes: {tag.scene_count}
</Link> </Link>
</Button> </Button>
<Button variant="secondary" className="tag-list-button"> <Button variant="secondary" className="tag-list-button">
<Link to={NavUtils.makeTagSceneMarkersUrl(tag)} className="tag-list-anchor"> <Link
to={NavUtils.makeTagSceneMarkersUrl(tag)}
className="tag-list-anchor"
>
Markers: {tag.scene_marker_count} Markers: {tag.scene_marker_count}
</Link> </Link>
</Button> </Button>

View file

@ -87,7 +87,9 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
)}` )}`
); );
const thisTags = props.sceneMarker.tags.map(tag => ( const thisTags = props.sceneMarker.tags.map(tag => (
<span key={tag.id} className="wall-tag">{tag.name}</span> <span key={tag.id} className="wall-tag">
{tag.name}
</span>
)); ));
thisTags.unshift( thisTags.unshift(
<span key={props.sceneMarker.primary_tag.id} className="wall-tag"> <span key={props.sceneMarker.primary_tag.id} className="wall-tag">

View file

@ -371,19 +371,19 @@ export class StashService {
public static useSceneIncrementO(id: string) { public static useSceneIncrementO(id: string) {
return GQL.useSceneIncrementOMutation({ return GQL.useSceneIncrementOMutation({
variables: {id} variables: { id }
}); });
} }
public static useSceneDecrementO(id: string) { public static useSceneDecrementO(id: string) {
return GQL.useSceneDecrementOMutation({ return GQL.useSceneDecrementOMutation({
variables: {id} variables: { id }
}); });
} }
public static useSceneResetO(id: string) { public static useSceneResetO(id: string) {
return GQL.useSceneResetOMutation({ return GQL.useSceneResetOMutation({
variables: {id} variables: { id }
}); });
} }

View file

@ -16,7 +16,7 @@ import {
FindStudiosQueryResult, FindStudiosQueryResult,
FindPerformersQueryResult FindPerformersQueryResult
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { useInterfaceLocalForage } from 'src/hooks/LocalForage'; import { useInterfaceLocalForage } from "src/hooks/LocalForage";
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";
@ -94,19 +94,16 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const items = options.getData(result); const items = options.getData(result);
useEffect(() => { useEffect(() => {
if(!forageInitialised.current && !interfaceForage.loading) { if (!forageInitialised.current && !interfaceForage.loading) {
forageInitialised.current = true; forageInitialised.current = true;
// Don't use query parameters for sub-components // Don't use query parameters for sub-components
if(options.subComponent) if (options.subComponent) return;
return;
// Don't read localForage if page already had query parameters // Don't read localForage if page already had query parameters
if(history.location.search) if (history.location.search) return;
return;
const queryData = interfaceForage.data?.queries[options.filterMode]; const queryData = interfaceForage.data?.queries[options.filterMode];
if(!queryData) if (!queryData) return;
return;
const newFilter = new ListFilterModel( const newFilter = new ListFilterModel(
options.filterMode, options.filterMode,
@ -119,11 +116,16 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
newLocation.search = queryData.filter; newLocation.search = queryData.filter;
history.replace(newLocation); history.replace(newLocation);
} }
}, [interfaceForage.data, interfaceForage.loading, history, options.subComponent, options.filterMode]); }, [
interfaceForage.data,
interfaceForage.loading,
history,
options.subComponent,
options.filterMode
]);
useEffect(() => { useEffect(() => {
if(options.subComponent) if (options.subComponent) return;
return;
const newFilter = new ListFilterModel( const newFilter = new ListFilterModel(
options.filterMode, options.filterMode,
@ -131,8 +133,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
); );
setFilter(newFilter); setFilter(newFilter);
if(forageInitialised.current) { if (forageInitialised.current) {
setInterfaceForage((d) => { setInterfaceForage(d => {
const dataClone = _.cloneDeep(d); const dataClone = _.cloneDeep(d);
dataClone!.queries[options.filterMode] = { dataClone!.queries[options.filterMode] = {
filter: location.search, filter: location.search,
@ -323,9 +325,9 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
: undefined; : undefined;
let template; let template;
if(result.loading || !forageInitialised.current) { if (result.loading || !forageInitialised.current) {
template = <LoadingIndicator />; template = <LoadingIndicator />;
} else if(result.error) { } else if (result.error) {
template = <h1>{result.error.message}</h1>; template = <h1>{result.error.message}</h1>;
} else { } else {
template = ( template = (

View file

@ -50,14 +50,17 @@ function useLocalForage(item: string): ILocalForage<ValidTypes> {
runAsync(); runAsync();
}); });
return {data: json, setData: setJson, error: err, loading: !loaded}; return { data: json, setData: setJson, error: err, loading: !loaded };
} }
export function useInterfaceLocalForage(): [ILocalForage<IInterfaceConfig | undefined>, Dispatch<SetStateAction<IInterfaceConfig | undefined>>] { export function useInterfaceLocalForage(): [
ILocalForage<IInterfaceConfig | undefined>,
Dispatch<SetStateAction<IInterfaceConfig | undefined>>
] {
const result = useLocalForage("interface"); const result = useLocalForage("interface");
let returnVal = result; let returnVal = result;
if(!result.data?.queries) { if (!result.data?.queries) {
returnVal = { returnVal = {
...result, ...result,
data: { data: {
@ -67,5 +70,5 @@ export function useInterfaceLocalForage(): [ILocalForage<IInterfaceConfig | unde
}; };
} }
return [returnVal, result.setData];; return [returnVal, result.setData];
} }

View file

@ -1,5 +1,5 @@
import en from './en.json'; import en from "./en.json";
import de from './de.json'; import de from "./de.json";
export default { export default {
en, en,

View file

@ -64,7 +64,6 @@ const DEFAULT_PARAMS = {
itemsPerPage: 40 itemsPerPage: 40
}; };
// TODO: handle customCriteria // TODO: handle customCriteria
export class ListFilterModel { export class ListFilterModel {
public filterMode: FilterMode = FilterMode.Scenes; public filterMode: FilterMode = FilterMode.Scenes;
@ -209,9 +208,10 @@ export class ListFilterModel {
} }
} }
} }
this.sortDirection = params.sortdir === "desc" this.sortDirection =
? SortDirectionEnum.Desc params.sortdir === "desc"
: SortDirectionEnum.Asc; ? SortDirectionEnum.Desc
: SortDirectionEnum.Asc;
if (params.disp) { if (params.disp) {
this.displayMode = Number.parseInt(params.disp, 10); this.displayMode = Number.parseInt(params.disp, 10);
} }
@ -221,8 +221,7 @@ export class ListFilterModel {
if (params.p) { if (params.p) {
this.currentPage = Number.parseInt(params.p, 10); this.currentPage = Number.parseInt(params.p, 10);
} }
if (params.items) if (params.items) this.itemsPerPage = Number.parseInt(params.items, 10);
this.itemsPerPage = Number.parseInt(params.items, 10);
if (params.c !== undefined) { if (params.c !== undefined) {
this.criteria = []; this.criteria = [];
@ -249,7 +248,7 @@ export class ListFilterModel {
// #321 - set the random seed if it is not set // #321 - set the random seed if it is not set
if (this.randomSeed === -1) { if (this.randomSeed === -1) {
// generate 8-digit seed // generate 8-digit seed
this.randomSeed = Math.floor(Math.random() * (10 ** 8)); this.randomSeed = Math.floor(Math.random() * 10 ** 8);
} }
} else { } else {
this.randomSeed = -1; this.randomSeed = -1;
@ -278,12 +277,22 @@ export class ListFilterModel {
}); });
const result = { const result = {
items: this.itemsPerPage !== DEFAULT_PARAMS.itemsPerPage ? this.itemsPerPage : undefined, items:
this.itemsPerPage !== DEFAULT_PARAMS.itemsPerPage
? this.itemsPerPage
: undefined,
sortby: this.getSortBy(), sortby: this.getSortBy(),
sortdir: this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined, sortdir:
disp: this.displayMode !== DEFAULT_PARAMS.displayMode ? this.displayMode : undefined, this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined,
disp:
this.displayMode !== DEFAULT_PARAMS.displayMode
? this.displayMode
: undefined,
q: this.searchTerm, q: this.searchTerm,
p: this.currentPage !== DEFAULT_PARAMS.currentPage ? this.currentPage : undefined, p:
this.currentPage !== DEFAULT_PARAMS.currentPage
? this.currentPage
: undefined,
c: encodedCriteria c: encodedCriteria
}; };
return queryString.stringify(result, { encode: false }); return queryString.stringify(result, { encode: false });
@ -315,7 +324,10 @@ export class ListFilterModel {
} }
case "o_counter": { case "o_counter": {
const oCounterCrit = criterion as NumberCriterion; const oCounterCrit = criterion as NumberCriterion;
result.o_counter = { value: oCounterCrit.value, modifier: oCounterCrit.modifier }; result.o_counter = {
value: oCounterCrit.value,
modifier: oCounterCrit.modifier
};
break; break;
} }
case "resolution": { case "resolution": {

View file

@ -1,19 +1,19 @@
const flattenMessages = ((nestedMessages:any, prefix = '') => { const flattenMessages = (nestedMessages: any, prefix = "") => {
if (nestedMessages === null) { if (nestedMessages === null) {
return {} return {};
} }
return Object.keys(nestedMessages).reduce((messages, key) => { return Object.keys(nestedMessages).reduce((messages, key) => {
const value = nestedMessages[key] const value = nestedMessages[key];
const prefixedKey = prefix ? `${prefix}.${key}` : key const prefixedKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'string') { if (typeof value === "string") {
Object.assign(messages, { [prefixedKey]: value }) Object.assign(messages, { [prefixedKey]: value });
} else { } else {
Object.assign(messages, flattenMessages(value, prefixedKey)) Object.assign(messages, flattenMessages(value, prefixedKey));
} }
return messages return messages;
}, {}) }, {});
}) };
export default flattenMessages; export default flattenMessages;