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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -493,7 +493,9 @@ export const SceneFilenameParser: React.FC = () => {
return (
<div>
{elements.map((name: string) => (
<Badge key={name} variant="secondary">{name}</Badge>
<Badge key={name} variant="secondary">
{name}
</Badge>
))}
</div>
);
@ -591,7 +593,9 @@ export const SceneFilenameParser: React.FC = () => {
return (
<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
key="title"
fieldName="Title"

View file

@ -205,7 +205,11 @@ export class ScenePlayerImpl extends React.Component<
public render() {
return (
<HotKeys keyMap={KeyMap} handlers={this.KeyHandlers} className="row scene-player">
<HotKeys
keyMap={KeyMap}
handlers={this.KeyHandlers}
className="row scene-player"
>
<div
id="jwplayer-container"
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 (
<div className="scene-studio-overlay">
<Link to={`/studios/${props.scene.studio.id}`}>
{ showStudioAsText
? props.scene.studio.name
: <img className="image-thumbnail" alt={props.scene.studio.name} src={props.scene.studio.image_path ?? ''} />
}
{showStudioAsText ? (
props.scene.studio.name
) : (
<img
className="image-thumbnail"
alt={props.scene.studio.name}
src={props.scene.studio.image_path ?? ""}
/>
)}
</Link>
</div>
);
@ -101,7 +106,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
to={`/performers/${performer.id}`}
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>
<TagLink key={performer.id} performer={performer} className="d-block" />
</div>
@ -144,7 +153,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
<span>{props.scene.o_counter}</span>
</Button>
</div>
)
);
}
}
@ -207,15 +216,12 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}}
/>
{maybeRenderSceneStudioOverlay()}
<Link
to={`/scenes/${props.scene.id}`}
className="scene-card-link"
>
<Link to={`/scenes/${props.scene.id}`} className="scene-card-link">
{maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()}
<video
loop
className={cx('scene-card-video', { portrait: isPortrait() })}
className={cx("scene-card-video", { portrait: isPortrait() })}
poster={props.scene.paths.screenshot || ""}
ref={videoHoverHook.videoEl}
>
@ -229,13 +235,9 @@ export const SceneCard: React.FC<ISceneCardProps> = (
: TextUtils.fileNameFromPath(props.scene.path)}
</h5>
<span>{props.scene.date}</span>
{ props.scene.details && (
{props.scene.details && (
<p>
{TextUtils.truncate(
props.scene.details,
100,
"... (continued)"
)}
{TextUtils.truncate(props.scene.details, 100, "... (continued)")}
</p>
)}
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -11,27 +11,30 @@ interface ISceneListTableProps {
export const SceneListTable: React.FC<ISceneListTableProps> = (
props: ISceneListTableProps
) => {
const renderTags = (tags: GQL.Tag[]) => (
const renderTags = (tags: GQL.Tag[]) =>
tags.map(tag => (
<Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}>
<h6>{tag.name}</h6>
</Link>
))
);
));
const renderPerformers = (performers: Partial<GQL.Performer>[]) => (
const renderPerformers = (performers: Partial<GQL.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) => (
<tr key={scene.id}>
<td>
<Link
to={`/scenes/${scene.id}`}
>
<img className="image-thumbnail" alt={scene.title ?? ''} src={scene.paths.screenshot ?? ''} />
<Link to={`/scenes/${scene.id}`}>
<img
className="image-thumbnail"
alt={scene.title ?? ""}
src={scene.paths.screenshot ?? ""}
/>
</Link>
</td>
<td className="text-left">
@ -42,17 +45,21 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
</Link>
</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>{renderPerformers(scene.performers)}</td>
<td>{ scene.studio && (
<Link to={NavUtils.makeStudioScenesUrl(scene.studio)}>
<h6>{scene.studio.name}</h6>
</Link>
)}
<td>
{scene.studio && (
<Link to={NavUtils.makeStudioScenesUrl(scene.studio)}>
<h6>{scene.studio.name}</h6>
</Link>
)}
</td>
</tr>
)
);
return (
<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 [css, setCSS] = useState<string>();
const [cssEnabled, setCSSEnabled] = useState<boolean>(false);
const [language, setLanguage] = useState<string>('en');
const [language, setLanguage] = useState<string>("en");
const [updateInterfaceConfig] = StashService.useConfigureInterface({
soundOnPreview,
@ -36,7 +36,7 @@ export const SettingsInterfacePanel: React.FC = () => {
setShowStudioAsText(iCfg?.showStudioAsText ?? false);
setCSS(iCfg?.css ?? "");
setCSSEnabled(iCfg?.cssEnabled ?? false);
setLanguage(iCfg?.language ?? 'en-US');
setLanguage(iCfg?.language ?? "en-US");
}, [config]);
async function onSave() {
@ -50,10 +50,8 @@ export const SettingsInterfacePanel: React.FC = () => {
}
}
if(error)
return <h1>{error.message}</h1>;
if(loading)
return <LoadingIndicator />;
if (error) return <h1>{error.message}</h1>;
if (loading) return <LoadingIndicator />;
return (
<>
@ -64,7 +62,9 @@ export const SettingsInterfacePanel: React.FC = () => {
as="select"
className="col-4"
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-GB">English (United Kingdom)</option>

View file

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

View file

@ -9,7 +9,11 @@ interface IIcon {
}
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;

View file

@ -343,7 +343,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
}),
multiValueRemove: (base: CSSProperties, state: any) => ({
...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 = () => (
<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">
<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" />
<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"
>
<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)" />
</svg>
</span>

View file

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

View file

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

View file

@ -11,7 +11,11 @@ export const StudioCard: React.FC<IProps> = ({ studio }) => {
return (
<Card className="studio-card">
<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>
<div className="card-section">
<h5 className="text-truncate">{studio.name}</h5>

View file

@ -103,16 +103,26 @@ export const TagList: React.FC = () => {
{tag.name}
</Button>
<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
</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}
</Link>
</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}
</Link>
</Button>

View file

@ -87,7 +87,9 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
)}`
);
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(
<span key={props.sceneMarker.primary_tag.id} className="wall-tag">

View file

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

View file

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

View file

@ -50,14 +50,17 @@ function useLocalForage(item: string): ILocalForage<ValidTypes> {
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");
let returnVal = result;
if(!result.data?.queries) {
if (!result.data?.queries) {
returnVal = {
...result,
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 de from './de.json';
import en from "./en.json";
import de from "./de.json";
export default {
en,

View file

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

View file

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