mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Split out entity creation from view pages (#1884)
* Split performerCreate page into separate page * Split studioCreate into a separate page * Remove Partial types from performer/studio * Split tagCreate into a separate page * Split movieCreate into a separate page * Split out galleryCreate into its own page * Add loader to scene page * Fix performer name fallback * Fix movie layout shift * Fix prompt comment and switch studio prompt to localized string
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { Gallery } from "./GalleryDetails/Gallery";
|
||||
import Gallery from "./GalleryDetails/Gallery";
|
||||
import GalleryCreate from "./GalleryDetails/GalleryCreate";
|
||||
import { GalleryList } from "./GalleryList";
|
||||
|
||||
const Galleries = () => (
|
||||
@@ -13,6 +14,7 @@ const Galleries = () => (
|
||||
<GalleryList {...props} persistState={PersistanceLevel.ALL} />
|
||||
)}
|
||||
/>
|
||||
<Route exact path="/galleries/new" component={GalleryCreate} />
|
||||
<Route path="/galleries/:id/:tab?" component={Gallery} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory, Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
mutateMetadataScan,
|
||||
useFindGallery,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from "src/core/StashService";
|
||||
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import * as Mousetrap from "mousetrap";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { useToast } from "src/hooks";
|
||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||
@@ -20,27 +21,26 @@ import { GalleryAddPanel } from "./GalleryAddPanel";
|
||||
import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel";
|
||||
import { GalleryScenesPanel } from "./GalleryScenesPanel";
|
||||
|
||||
interface IProps {
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
}
|
||||
|
||||
interface IGalleryParams {
|
||||
id?: string;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
export const Gallery: React.FC = () => {
|
||||
const { tab = "images", id = "new" } = useParams<IGalleryParams>();
|
||||
export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
||||
const { tab = "images" } = useParams<IGalleryParams>();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const isNew = id === "new";
|
||||
|
||||
const { data, error, loading } = useFindGallery(id);
|
||||
const gallery = data?.findGallery;
|
||||
|
||||
const [activeTabKey, setActiveTabKey] = useState("gallery-details-panel");
|
||||
const activeRightTabKey = tab === "images" || tab === "add" ? tab : "images";
|
||||
const setActiveRightTabKey = (newTab: string | null) => {
|
||||
if (tab !== newTab) {
|
||||
const tabParam = newTab === "images" ? "" : `/${newTab}`;
|
||||
history.replace(`/galleries/${id}${tabParam}`);
|
||||
history.replace(`/galleries/${gallery.id}${tabParam}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,8 +54,8 @@ export const Gallery: React.FC = () => {
|
||||
await updateGallery({
|
||||
variables: {
|
||||
input: {
|
||||
id: gallery?.id ?? "",
|
||||
organized: !gallery?.organized,
|
||||
id: gallery.id,
|
||||
organized: !gallery.organized,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -118,7 +118,7 @@ export const Gallery: React.FC = () => {
|
||||
<Icon icon="ellipsis-v" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="bg-secondary text-white">
|
||||
{gallery?.path ? (
|
||||
{gallery.path ? (
|
||||
<Dropdown.Item
|
||||
key="rescan"
|
||||
className="bg-secondary text-white"
|
||||
@@ -268,35 +268,6 @@ export const Gallery: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
|
||||
if (isNew)
|
||||
return (
|
||||
<div className="row new-view">
|
||||
<div className="col-md-6">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="actions.create_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</h2>
|
||||
<GalleryEditPanel
|
||||
isNew
|
||||
gallery={undefined}
|
||||
isVisible
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!gallery)
|
||||
return <ErrorMessage error={`No gallery with id ${id} found.`} />;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
{maybeRenderDeleteDialog()}
|
||||
@@ -323,3 +294,17 @@ export const Gallery: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GalleryLoader: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { data, loading, error } = useFindGallery(id ?? "");
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!data?.findGallery)
|
||||
return <ErrorMessage error={`No gallery found with id ${id}.`} />;
|
||||
|
||||
return <GalleryPage gallery={data.findGallery} />;
|
||||
};
|
||||
|
||||
export default GalleryLoader;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useIntl } from "react-intl";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface IGalleryAddProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
}
|
||||
|
||||
export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||
@@ -20,7 +20,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
const galleryValue = {
|
||||
id: gallery.id!,
|
||||
id: gallery.id,
|
||||
label: gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? ""),
|
||||
};
|
||||
// if galleries is already present, then we modify it, otherwise add
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||
|
||||
const GalleryCreate: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="row new-view">
|
||||
<div className="col-md-6">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="actions.create_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</h2>
|
||||
<GalleryEditPanel
|
||||
isNew
|
||||
gallery={undefined}
|
||||
isVisible
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryCreate;
|
||||
@@ -9,23 +9,25 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
|
||||
interface IGalleryDetailProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
}
|
||||
|
||||
export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = (props) => {
|
||||
export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = ({
|
||||
gallery,
|
||||
}) => {
|
||||
function renderDetails() {
|
||||
if (!props.gallery.details || props.gallery.details === "") return;
|
||||
if (!gallery.details) return;
|
||||
return (
|
||||
<>
|
||||
<h6>Details</h6>
|
||||
<p className="pre">{props.gallery.details}</p>
|
||||
<p className="pre">{gallery.details}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTags() {
|
||||
if (!props.gallery.tags || props.gallery.tags.length === 0) return;
|
||||
const tags = props.gallery.tags.map((tag) => (
|
||||
if (gallery.tags.length === 0) return;
|
||||
const tags = gallery.tags.map((tag) => (
|
||||
<TagLink key={tag.id} tag={tag} tagType="gallery" />
|
||||
));
|
||||
return (
|
||||
@@ -37,14 +39,13 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = (props) => {
|
||||
}
|
||||
|
||||
function renderPerformers() {
|
||||
if (!props.gallery.performers || props.gallery.performers.length === 0)
|
||||
return;
|
||||
const performers = sortPerformers(props.gallery.performers);
|
||||
if (gallery.performers.length === 0) return;
|
||||
const performers = sortPerformers(gallery.performers);
|
||||
const cards = performers.map((performer) => (
|
||||
<PerformerCard
|
||||
key={performer.id}
|
||||
performer={performer}
|
||||
ageFromDate={props.gallery.date ?? undefined}
|
||||
ageFromDate={gallery.date ?? undefined}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -59,9 +60,8 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = (props) => {
|
||||
}
|
||||
|
||||
// filename should use entire row if there is no studio
|
||||
const galleryDetailsWidth = props.gallery.studio ? "col-9" : "col-12";
|
||||
const title =
|
||||
props.gallery.title ?? TextUtils.fileNameFromPath(props.gallery.path ?? "");
|
||||
const galleryDetailsWidth = gallery.studio ? "col-9" : "col-12";
|
||||
const title = gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? "");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -70,29 +70,29 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = (props) => {
|
||||
<h3 className="gallery-header d-xl-none">
|
||||
<TruncatedText text={title} />
|
||||
</h3>
|
||||
{props.gallery.date ? (
|
||||
{gallery.date ? (
|
||||
<h5>
|
||||
<FormattedDate
|
||||
value={props.gallery.date}
|
||||
value={gallery.date}
|
||||
format="long"
|
||||
timeZone="utc"
|
||||
/>
|
||||
</h5>
|
||||
) : undefined}
|
||||
{props.gallery.rating ? (
|
||||
{gallery.rating ? (
|
||||
<h6>
|
||||
Rating: <RatingStars value={props.gallery.rating} />
|
||||
Rating: <RatingStars value={gallery.rating} />
|
||||
</h6>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
{props.gallery.studio && (
|
||||
{gallery.studio && (
|
||||
<div className="col-3 d-xl-none">
|
||||
<Link to={`/studios/${props.gallery.studio.id}`}>
|
||||
<Link to={`/studios/${gallery.studio.id}`}>
|
||||
<img
|
||||
src={props.gallery.studio.image_path ?? ""}
|
||||
alt={`${props.gallery.studio.name} logo`}
|
||||
src={gallery.studio.image_path ?? ""}
|
||||
alt={`${gallery.studio.name} logo`}
|
||||
className="studio-logo float-right"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
useFindMovie,
|
||||
useMovieUpdate,
|
||||
useMovieCreate,
|
||||
useMovieDestroy,
|
||||
} from "src/core/StashService";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import {
|
||||
DetailsEditNavbar,
|
||||
ErrorMessage,
|
||||
LoadingIndicator,
|
||||
Modal,
|
||||
} from "src/components/Shared";
|
||||
@@ -20,19 +19,17 @@ import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
||||
import { MovieEditPanel } from "./MovieEditPanel";
|
||||
|
||||
interface IMovieParams {
|
||||
id?: string;
|
||||
interface IProps {
|
||||
movie: GQL.MovieDataFragment;
|
||||
}
|
||||
|
||||
export const Movie: React.FC = () => {
|
||||
const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const { id = "new" } = useParams<IMovieParams>();
|
||||
const isNew = id === "new";
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing movie state
|
||||
@@ -44,14 +41,10 @@ export const Movie: React.FC = () => {
|
||||
);
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
// Network state
|
||||
const { data, error, loading } = useFindMovie(id);
|
||||
const movie = data?.findMovie;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [updateMovie] = useMovieUpdate();
|
||||
const [createMovie] = useMovieCreate();
|
||||
const [deleteMovie] = useMovieDestroy({ id });
|
||||
const [updateMovie, { loading: updating }] = useMovieUpdate();
|
||||
const [deleteMovie, { loading: deleting }] = useMovieDestroy({
|
||||
id: movie.id,
|
||||
});
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
@@ -66,23 +59,14 @@ export const Movie: React.FC = () => {
|
||||
|
||||
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data || !data.findMovie || loading) return <LoadingIndicator />;
|
||||
if (error) {
|
||||
return <>{error!.message}</>;
|
||||
}
|
||||
}
|
||||
|
||||
function getMovieInput(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||
...input,
|
||||
id: movie.id,
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
(ret as GQL.MovieUpdateInput).id = id;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -90,42 +74,25 @@ export const Movie: React.FC = () => {
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!isNew) {
|
||||
const result = await updateMovie({
|
||||
variables: {
|
||||
input: getMovieInput(input) as GQL.MovieUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.movieUpdate) {
|
||||
setIsEditing(false);
|
||||
history.push(`/movies/${result.data.movieUpdate.id}`);
|
||||
}
|
||||
} else {
|
||||
const result = await createMovie({
|
||||
variables: getMovieInput(input) as GQL.MovieCreateInput,
|
||||
});
|
||||
if (result.data?.movieCreate?.id) {
|
||||
history.push(`/movies/${result.data.movieCreate.id}`);
|
||||
setIsEditing(false);
|
||||
}
|
||||
const result = await updateMovie({
|
||||
variables: {
|
||||
input: getMovieInput(input) as GQL.MovieUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.movieUpdate) {
|
||||
setIsEditing(false);
|
||||
history.push(`/movies/${result.data.movieUpdate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await deleteMovie();
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
// redirect to movies page
|
||||
@@ -155,7 +122,7 @@ export const Movie: React.FC = () => {
|
||||
id="dialogs.delete_confirm"
|
||||
values={{
|
||||
entityName:
|
||||
movie?.name ??
|
||||
movie.name ??
|
||||
intl.formatMessage({ id: "movie" }).toLocaleLowerCase(),
|
||||
}}
|
||||
/>
|
||||
@@ -165,7 +132,7 @@ export const Movie: React.FC = () => {
|
||||
}
|
||||
|
||||
function renderFrontImage() {
|
||||
let image = movie?.front_image_path;
|
||||
let image = movie.front_image_path;
|
||||
if (isEditing) {
|
||||
if (frontImage === null) {
|
||||
image = `${image}&default=true`;
|
||||
@@ -184,7 +151,7 @@ export const Movie: React.FC = () => {
|
||||
}
|
||||
|
||||
function renderBackImage() {
|
||||
let image = movie?.back_image_path;
|
||||
let image = movie.back_image_path;
|
||||
if (isEditing) {
|
||||
if (backImage === null) {
|
||||
image = undefined;
|
||||
@@ -202,16 +169,12 @@ export const Movie: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
if (updating || deleting) return <LoadingIndicator />;
|
||||
|
||||
// TODO: CSS class
|
||||
return (
|
||||
<div className="row">
|
||||
<div
|
||||
className={cx("movie-details mb-3 col", {
|
||||
"col-xl-4 col-lg-6": !isNew,
|
||||
})}
|
||||
>
|
||||
<div className="movie-details mb-3 col col-xl-4 col-lg-6">
|
||||
<div className="logo w-100">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
@@ -223,13 +186,13 @@ export const Movie: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isEditing && movie ? (
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<MovieDetailsPanel movie={movie} />
|
||||
{/* HACK - this is also rendered in the MovieEditPanel */}
|
||||
<DetailsEditNavbar
|
||||
objectName={movie?.name ?? "movie"}
|
||||
isNew={isNew}
|
||||
objectName={movie.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={() => {}}
|
||||
@@ -239,7 +202,7 @@ export const Movie: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<MovieEditPanel
|
||||
movie={movie ?? undefined}
|
||||
movie={movie}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
@@ -250,12 +213,24 @@ export const Movie: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isNew && movie && (
|
||||
<div className="col-xl-8 col-lg-6">
|
||||
<MovieScenesPanel movie={movie} />
|
||||
</div>
|
||||
)}
|
||||
<div className="col-xl-8 col-lg-6">
|
||||
<MovieScenesPanel movie={movie} />
|
||||
</div>
|
||||
{renderDeleteAlert()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MovieLoader: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { data, loading, error } = useFindMovie(id ?? "");
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!data?.findMovie)
|
||||
return <ErrorMessage error={`No movie found with id ${id}.`} />;
|
||||
|
||||
return <MoviePage movie={data.findMovie} />;
|
||||
};
|
||||
|
||||
export default MovieLoader;
|
||||
|
||||
99
ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx
Normal file
99
ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useMovieCreate } from "src/core/StashService";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { MovieEditPanel } from "./MovieEditPanel";
|
||||
|
||||
export const MovieCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
|
||||
// Editing movie state
|
||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
||||
|
||||
function getMovieInput(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||
...input,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) {
|
||||
try {
|
||||
const result = await createMovie({
|
||||
variables: getMovieInput(input) as GQL.MovieCreateInput,
|
||||
});
|
||||
if (result.data?.movieCreate?.id) {
|
||||
history.push(`/movies/${result.data.movieCreate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFrontImage() {
|
||||
if (frontImage) {
|
||||
return (
|
||||
<div className="movie-image-container">
|
||||
<img alt="Front Cover" src={frontImage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderBackImage() {
|
||||
if (backImage) {
|
||||
return (
|
||||
<div className="movie-image-container">
|
||||
<img alt="Back Cover" src={backImage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: CSS class
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="movie-details mb-3 col">
|
||||
<div className="logo w-100">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<div className="movie-images">
|
||||
{renderFrontImage()}
|
||||
{renderBackImage()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MovieEditPanel
|
||||
onSubmit={onSave}
|
||||
onCancel={() => history.push("/movies")}
|
||||
onDelete={() => {}}
|
||||
setFrontImage={setFrontImage}
|
||||
setBackImage={setBackImage}
|
||||
onImageEncoding={onImageEncoding}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MovieCreate;
|
||||
@@ -6,7 +6,7 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IMovieDetailsPanel {
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
movie: GQL.MovieDataFragment;
|
||||
}
|
||||
|
||||
export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
||||
|
||||
@@ -386,7 +386,12 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
|
||||
<Prompt
|
||||
when={formik.dirty}
|
||||
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
|
||||
message={(location, action) => {
|
||||
// Check if it's a redirect after movie creation
|
||||
if (action === "PUSH" && location.pathname.startsWith("/movies/"))
|
||||
return true;
|
||||
return intl.formatMessage({ id: "dialogs.unsaved_changes" });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form noValidate onSubmit={formik.handleSubmit} id="movie-edit">
|
||||
|
||||
@@ -5,12 +5,12 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { SceneList } from "src/components/Scenes/SceneList";
|
||||
|
||||
interface IMovieScenesPanel {
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
movie: GQL.MovieDataFragment;
|
||||
}
|
||||
|
||||
export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({ movie }) => {
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
const movieValue = { id: movie.id!, label: movie.name! };
|
||||
const movieValue = { id: movie.id, label: movie.name };
|
||||
// if movie is already present, then we modify it, otherwise add
|
||||
let movieCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.type === "movies";
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { Movie } from "./MovieDetails/Movie";
|
||||
import Movie from "./MovieDetails/Movie";
|
||||
import MovieCreate from "./MovieDetails/MovieCreate";
|
||||
import { MovieList } from "./MovieList";
|
||||
|
||||
const Movies = () => (
|
||||
<Switch>
|
||||
<Route exact path="/movies" component={MovieList} />
|
||||
<Route exact path="/movies/new" component={MovieCreate} />
|
||||
<Route path="/movies/:id/:tab?" component={Movie} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.movie-details {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.movie-card {
|
||||
width: 240px;
|
||||
|
||||
|
||||
@@ -27,23 +27,21 @@ import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
|
||||
import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
|
||||
interface IProps {
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
interface IPerformerParams {
|
||||
id?: string;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
export const Performer: React.FC = () => {
|
||||
const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const { tab = "details", id = "new" } = useParams<IPerformerParams>();
|
||||
const isNew = id === "new";
|
||||
const { tab = "details" } = useParams<IPerformerParams>();
|
||||
|
||||
// Performer state
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
const { data, loading: performerLoading, error } = useFindPerformer(id);
|
||||
const performer = data?.findPerformer || ({} as Partial<GQL.Performer>);
|
||||
|
||||
// if undefined then get the existing image
|
||||
// if null then get the default (no) image
|
||||
@@ -51,7 +49,7 @@ export const Performer: React.FC = () => {
|
||||
const activeImage =
|
||||
imagePreview === undefined
|
||||
? performer.image_path ?? ""
|
||||
: imagePreview ?? (isNew ? "" : `${performer.image_path}&default=true`);
|
||||
: imagePreview ?? `${performer.image_path}&default=true`;
|
||||
const lightboxImages = useMemo(
|
||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||
[activeImage]
|
||||
@@ -61,12 +59,8 @@ export const Performer: React.FC = () => {
|
||||
images: lightboxImages,
|
||||
});
|
||||
|
||||
// Network state
|
||||
const [loading, setIsLoading] = useState(false);
|
||||
const isLoading = performerLoading || loading;
|
||||
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [deletePerformer] = usePerformerDestroy();
|
||||
const [deletePerformer, { loading: isDestroying }] = usePerformerDestroy();
|
||||
|
||||
const activeTabKey =
|
||||
tab === "scenes" ||
|
||||
@@ -80,7 +74,7 @@ export const Performer: React.FC = () => {
|
||||
const setActiveTabKey = (newTab: string | null) => {
|
||||
if (tab !== newTab) {
|
||||
const tabParam = newTab === "details" ? "" : `/${newTab}`;
|
||||
history.replace(`/performers/${id}${tabParam}`);
|
||||
history.replace(`/performers/${performer.id}${tabParam}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,19 +125,12 @@ export const Performer: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!performer.id && !isNew)
|
||||
return <ErrorMessage error={`No performer found with id ${id}.`} />;
|
||||
|
||||
async function onDelete() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await deletePerformer({ variables: { id } });
|
||||
await deletePerformer({ variables: { id: performer.id } });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
||||
// redirect to performers page
|
||||
history.push("/performers");
|
||||
@@ -175,7 +162,7 @@ export const Performer: React.FC = () => {
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={activeTabKey === "edit"}
|
||||
isNew={isNew}
|
||||
isNew={false}
|
||||
onDelete={onDelete}
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
@@ -303,39 +290,13 @@ export const Performer: React.FC = () => {
|
||||
</span>
|
||||
);
|
||||
|
||||
function renderPerformerImage() {
|
||||
if (imageEncoding) {
|
||||
return <LoadingIndicator message="Encoding image..." />;
|
||||
}
|
||||
if (activeImage) {
|
||||
return <img className="performer" src={activeImage} alt="Performer" />;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
if (isDestroying)
|
||||
return (
|
||||
<div className="row new-view" id="performer-page">
|
||||
<div className="performer-image-container col-md-4 text-center">
|
||||
{renderPerformerImage()}
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<h2>Create Performer</h2>
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible
|
||||
isNew
|
||||
onDelete={onDelete}
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingIndicator
|
||||
message={`Deleting performer ${performer.id}: ${performer.name}`}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!performer.id) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="performer-page" className="row">
|
||||
<div className="performer-image-container col-md-4 text-center">
|
||||
@@ -370,3 +331,17 @@ export const Performer: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PerformerLoader: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { data, loading, error } = useFindPerformer(id ?? "");
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!data?.findPerformer)
|
||||
return <ErrorMessage error={`No performer found with id ${id}.`} />;
|
||||
|
||||
return <PerformerPage performer={data.findPerformer} />;
|
||||
};
|
||||
|
||||
export default PerformerLoader;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from "react";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
|
||||
const PerformerCreate: React.FC = () => {
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
|
||||
const activeImage = imagePreview ?? "";
|
||||
|
||||
const onImageChange = (image?: string | null) => setImagePreview(image);
|
||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
||||
|
||||
function renderPerformerImage() {
|
||||
if (imageEncoding) {
|
||||
return <LoadingIndicator message="Encoding image..." />;
|
||||
}
|
||||
if (activeImage) {
|
||||
return <img className="performer" src={activeImage} alt="Performer" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row new-view" id="performer-page">
|
||||
<div className="performer-image-container col-md-4 text-center">
|
||||
{renderPerformerImage()}
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<h2>Create Performer</h2>
|
||||
<PerformerEditPanel
|
||||
performer={{}}
|
||||
isVisible
|
||||
isNew
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformerCreate;
|
||||
@@ -7,7 +7,7 @@ import { TextField, URLField } from "src/utils/field";
|
||||
import { genderToString } from "src/utils/gender";
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
@@ -17,7 +17,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
const intl = useIntl();
|
||||
|
||||
function renderTagsField() {
|
||||
if (!performer.tags?.length) {
|
||||
if (!performer.tags.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
}
|
||||
|
||||
function renderStashIDs() {
|
||||
if (!performer.stash_ids?.length) {
|
||||
if (!performer.stash_ids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { performerFilterHook } from "src/core/performers";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerGalleriesPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ImageList } from "src/components/Images/ImageList";
|
||||
import { performerFilterHook } from "src/core/performers";
|
||||
|
||||
interface IPerformerImagesPanel {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> = ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MovieList } from "src/components/Movies/MovieList";
|
||||
import { performerFilterHook } from "src/core/performers";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerMoviesPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { mutateMetadataAutoTag } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
interface IPerformerOperationsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
|
||||
@@ -15,9 +15,6 @@ export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
|
||||
const Toast = useToast();
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!performer?.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await mutateMetadataAutoTag({ performers: [performer.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SceneList } from "src/components/Scenes/SceneList";
|
||||
import { performerFilterHook } from "src/core/performers";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { Performer } from "./PerformerDetails/Performer";
|
||||
import Performer from "./PerformerDetails/Performer";
|
||||
import PerformerCreate from "./PerformerDetails/PerformerCreate";
|
||||
import { PerformerList } from "./PerformerList";
|
||||
|
||||
const Performers = () => (
|
||||
@@ -13,6 +14,7 @@ const Performers = () => (
|
||||
<PerformerList persistState={PersistanceLevel.ALL} {...props} />
|
||||
)}
|
||||
/>
|
||||
<Route path="/performers/new" component={PerformerCreate} />
|
||||
<Route path="/performers/:id/:tab?" component={Performer} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -38,12 +38,12 @@ import { SceneGenerateDialog } from "../SceneGenerateDialog";
|
||||
import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel";
|
||||
import { OrganizedButton } from "./OrganizedButton";
|
||||
|
||||
interface ISceneParams {
|
||||
id?: string;
|
||||
interface IProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export const Scene: React.FC = () => {
|
||||
const { id = "new" } = useParams<ISceneParams>();
|
||||
const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
@@ -53,18 +53,16 @@ export const Scene: React.FC = () => {
|
||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const { data, error, loading, refetch } = useFindScene(id);
|
||||
const scene = data?.findScene;
|
||||
const {
|
||||
data: sceneStreams,
|
||||
error: streamableError,
|
||||
loading: streamableLoading,
|
||||
} = useSceneStreams(id);
|
||||
} = useSceneStreams(scene.id);
|
||||
|
||||
const [oLoading, setOLoading] = useState(false);
|
||||
const [incrementO] = useSceneIncrementO(scene?.id ?? "0");
|
||||
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
|
||||
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
||||
const [incrementO] = useSceneIncrementO(scene.id);
|
||||
const [decrementO] = useSceneDecrementO(scene.id);
|
||||
const [resetO] = useSceneResetO(scene.id);
|
||||
|
||||
const [organizedLoading, setOrganizedLoading] = useState(false);
|
||||
|
||||
@@ -85,7 +83,7 @@ export const Scene: React.FC = () => {
|
||||
|
||||
const queryParams = queryString.parse(location.search);
|
||||
const autoplay = queryParams?.autoplay === "true";
|
||||
const currentQueueIndex = queueScenes.findIndex((s) => s.id === id);
|
||||
const currentQueueIndex = queueScenes.findIndex((s) => s.id === scene.id);
|
||||
|
||||
async function getQueueFilterScenes(filter: ListFilterModel) {
|
||||
const query = await queryFindScenes(filter);
|
||||
@@ -113,7 +111,7 @@ export const Scene: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setRerenderPlayer(true);
|
||||
}, [id]);
|
||||
}, [scene.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setSceneQueue(SceneQueue.fromQueryParameters(location.search));
|
||||
@@ -142,8 +140,8 @@ export const Scene: React.FC = () => {
|
||||
await updateScene({
|
||||
variables: {
|
||||
input: {
|
||||
id: scene?.id ?? "",
|
||||
organized: !scene?.organized,
|
||||
id: scene.id,
|
||||
organized: !scene.organized,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -192,10 +190,6 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
|
||||
async function onRescan() {
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateMetadataScan({
|
||||
paths: [scene.path],
|
||||
});
|
||||
@@ -214,10 +208,6 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
|
||||
async function onGenerateScreenshot(at?: number) {
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
await generateScreenshot({
|
||||
variables: {
|
||||
id: scene.id,
|
||||
@@ -323,7 +313,7 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
|
||||
function maybeRenderDeleteDialog() {
|
||||
if (isDeleteAlertOpen && scene) {
|
||||
if (isDeleteAlertOpen) {
|
||||
return (
|
||||
<DeleteScenesDialog selected={[scene]} onClose={onDeleteDialogClosed} />
|
||||
);
|
||||
@@ -331,7 +321,7 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
|
||||
function maybeRenderSceneGenerateDialog() {
|
||||
if (isGenerateDialogOpen && scene) {
|
||||
if (isGenerateDialogOpen) {
|
||||
return (
|
||||
<SceneGenerateDialog
|
||||
selectedIds={[scene.id]}
|
||||
@@ -343,215 +333,204 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function renderOperations() {
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
variant="secondary"
|
||||
id="operation-menu"
|
||||
className="minimal"
|
||||
title="Operations"
|
||||
>
|
||||
<Icon icon="ellipsis-v" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="bg-secondary text-white">
|
||||
<Dropdown.Item
|
||||
key="rescan"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onRescan()}
|
||||
>
|
||||
<FormattedMessage id="actions.rescan" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsGenerateDialogOpen(true)}
|
||||
>
|
||||
<FormattedMessage id="actions.generate" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-screenshot"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() =>
|
||||
onGenerateScreenshot(JWUtils.getPlayer().getPosition())
|
||||
}
|
||||
>
|
||||
<FormattedMessage id="actions.generate_thumb_from_current" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-default"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onGenerateScreenshot()}
|
||||
>
|
||||
<FormattedMessage id="actions.generate_thumb_default" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="delete-scene"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="actions.delete_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "scene" }) }}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab.Container
|
||||
activeKey={activeTabKey}
|
||||
onSelect={(k) => k && setActiveTabKey(k)}
|
||||
const renderOperations = () => (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
variant="secondary"
|
||||
id="operation-menu"
|
||||
className="minimal"
|
||||
title="Operations"
|
||||
>
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-details-panel">
|
||||
<FormattedMessage id="scenes" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{(queueScenes ?? []).length > 0 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-queue-panel">
|
||||
<FormattedMessage id="queue" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-markers-panel">
|
||||
<FormattedMessage id="markers" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{scene.movies.length > 0 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-movie-panel">
|
||||
<FormattedMessage
|
||||
id="countables.movies"
|
||||
values={{ count: scene.movies.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{scene.galleries.length >= 1 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-galleries-panel">
|
||||
<FormattedMessage
|
||||
id="countables.galleries"
|
||||
values={{ count: scene.galleries.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-video-filter-panel">
|
||||
<FormattedMessage id="effect_filters.name" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-file-info-panel">
|
||||
<FormattedMessage id="file_info" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-edit-panel">
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Nav.Item className="ml-auto">
|
||||
<ExternalPlayerButton scene={scene} />
|
||||
</Nav.Item>
|
||||
<Nav.Item className="ml-auto">
|
||||
<OCounterButton
|
||||
loading={oLoading}
|
||||
value={scene.o_counter || 0}
|
||||
onIncrement={onIncrementClick}
|
||||
onDecrement={onDecrementClick}
|
||||
onReset={onResetClick}
|
||||
/>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<OrganizedButton
|
||||
loading={organizedLoading}
|
||||
organized={scene.organized}
|
||||
onClick={onOrganizedClick}
|
||||
/>
|
||||
</Nav.Item>
|
||||
<Nav.Item>{renderOperations()}</Nav.Item>
|
||||
</ButtonGroup>
|
||||
</Nav>
|
||||
</div>
|
||||
<Icon icon="ellipsis-v" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="bg-secondary text-white">
|
||||
<Dropdown.Item
|
||||
key="rescan"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onRescan()}
|
||||
>
|
||||
<FormattedMessage id="actions.rescan" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsGenerateDialogOpen(true)}
|
||||
>
|
||||
<FormattedMessage id="actions.generate" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-screenshot"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() =>
|
||||
onGenerateScreenshot(JWUtils.getPlayer().getPosition())
|
||||
}
|
||||
>
|
||||
<FormattedMessage id="actions.generate_thumb_from_current" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-default"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onGenerateScreenshot()}
|
||||
>
|
||||
<FormattedMessage id="actions.generate_thumb_default" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="delete-scene"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="actions.delete_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "scene" }) }}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
<Tab.Content>
|
||||
<Tab.Pane eventKey="scene-details-panel">
|
||||
<SceneDetailPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-queue-panel">
|
||||
<QueueViewer
|
||||
scenes={queueScenes}
|
||||
currentID={scene.id}
|
||||
onSceneClicked={(sceneID) => playScene(sceneID)}
|
||||
onNext={onQueueNext}
|
||||
onPrevious={onQueuePrevious}
|
||||
onRandom={onQueueRandom}
|
||||
start={queueStart}
|
||||
hasMoreScenes={queueHasMoreScenes()}
|
||||
onLessScenes={() => onQueueLessScenes()}
|
||||
onMoreScenes={() => onQueueMoreScenes()}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-markers-panel">
|
||||
<SceneMarkersPanel
|
||||
scene={scene}
|
||||
onClickMarker={onClickMarker}
|
||||
isVisible={activeTabKey === "scene-markers-panel"}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-movie-panel">
|
||||
<SceneMoviePanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
{scene.galleries.length === 1 && (
|
||||
<Tab.Pane eventKey="scene-galleries-panel">
|
||||
<GalleryViewer galleryId={scene.galleries[0].id} />
|
||||
</Tab.Pane>
|
||||
const renderTabs = () => (
|
||||
<Tab.Container
|
||||
activeKey={activeTabKey}
|
||||
onSelect={(k) => k && setActiveTabKey(k)}
|
||||
>
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-details-panel">
|
||||
<FormattedMessage id="scenes" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{(queueScenes ?? []).length > 0 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-queue-panel">
|
||||
<FormattedMessage id="queue" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{scene.galleries.length > 1 && (
|
||||
<Tab.Pane eventKey="scene-galleries-panel">
|
||||
<SceneGalleriesPanel galleries={scene.galleries} />
|
||||
</Tab.Pane>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-markers-panel">
|
||||
<FormattedMessage id="markers" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{scene.movies.length > 0 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-movie-panel">
|
||||
<FormattedMessage
|
||||
id="countables.movies"
|
||||
values={{ count: scene.movies.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Tab.Pane eventKey="scene-video-filter-panel">
|
||||
<SceneVideoFilterPanel scene={scene} />
|
||||
{scene.galleries.length >= 1 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-galleries-panel">
|
||||
<FormattedMessage
|
||||
id="countables.galleries"
|
||||
values={{ count: scene.galleries.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-video-filter-panel">
|
||||
<FormattedMessage id="effect_filters.name" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-file-info-panel">
|
||||
<FormattedMessage id="file_info" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-edit-panel">
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Nav.Item className="ml-auto">
|
||||
<ExternalPlayerButton scene={scene} />
|
||||
</Nav.Item>
|
||||
<Nav.Item className="ml-auto">
|
||||
<OCounterButton
|
||||
loading={oLoading}
|
||||
value={scene.o_counter || 0}
|
||||
onIncrement={onIncrementClick}
|
||||
onDecrement={onDecrementClick}
|
||||
onReset={onResetClick}
|
||||
/>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<OrganizedButton
|
||||
loading={organizedLoading}
|
||||
organized={scene.organized}
|
||||
onClick={onOrganizedClick}
|
||||
/>
|
||||
</Nav.Item>
|
||||
<Nav.Item>{renderOperations()}</Nav.Item>
|
||||
</ButtonGroup>
|
||||
</Nav>
|
||||
</div>
|
||||
|
||||
<Tab.Content>
|
||||
<Tab.Pane eventKey="scene-details-panel">
|
||||
<SceneDetailPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-queue-panel">
|
||||
<QueueViewer
|
||||
scenes={queueScenes}
|
||||
currentID={scene.id}
|
||||
onSceneClicked={(sceneID) => playScene(sceneID)}
|
||||
onNext={onQueueNext}
|
||||
onPrevious={onQueuePrevious}
|
||||
onRandom={onQueueRandom}
|
||||
start={queueStart}
|
||||
hasMoreScenes={queueHasMoreScenes()}
|
||||
onLessScenes={() => onQueueLessScenes()}
|
||||
onMoreScenes={() => onQueueMoreScenes()}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-markers-panel">
|
||||
<SceneMarkersPanel
|
||||
scene={scene}
|
||||
onClickMarker={onClickMarker}
|
||||
isVisible={activeTabKey === "scene-markers-panel"}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-movie-panel">
|
||||
<SceneMoviePanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
{scene.galleries.length === 1 && (
|
||||
<Tab.Pane eventKey="scene-galleries-panel">
|
||||
<GalleryViewer galleryId={scene.galleries[0].id} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane
|
||||
className="file-info-panel"
|
||||
eventKey="scene-file-info-panel"
|
||||
>
|
||||
<SceneFileInfoPanel scene={scene} />
|
||||
)}
|
||||
{scene.galleries.length > 1 && (
|
||||
<Tab.Pane eventKey="scene-galleries-panel">
|
||||
<SceneGalleriesPanel galleries={scene.galleries} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-edit-panel">
|
||||
<SceneEditPanel
|
||||
isVisible={activeTabKey === "scene-edit-panel"}
|
||||
scene={scene}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
onUpdate={() => refetch()}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
</Tab.Content>
|
||||
</Tab.Container>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<Tab.Pane eventKey="scene-video-filter-panel">
|
||||
<SceneVideoFilterPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane className="file-info-panel" eventKey="scene-file-info-panel">
|
||||
<SceneFileInfoPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-edit-panel">
|
||||
<SceneEditPanel
|
||||
isVisible={activeTabKey === "scene-edit-panel"}
|
||||
scene={scene}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
onUpdate={() => refetch()}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
</Tab.Content>
|
||||
</Tab.Container>
|
||||
);
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
@@ -582,10 +561,8 @@ export const Scene: React.FC = () => {
|
||||
return collapsed ? ">" : "<";
|
||||
}
|
||||
|
||||
if (loading || streamableLoading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (streamableLoading) return <LoadingIndicator />;
|
||||
if (streamableError) return <ErrorMessage error={streamableError.message} />;
|
||||
if (!scene) return <ErrorMessage error={`No scene found with id ${id}.`} />;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
@@ -638,3 +615,17 @@ export const Scene: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SceneLoader: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { data, loading, error, refetch } = useFindScene(id ?? "");
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!data?.findScene)
|
||||
return <ErrorMessage error={`No scene found with id ${id}.`} />;
|
||||
|
||||
return <ScenePage scene={data.findScene} refetch={refetch} />;
|
||||
};
|
||||
|
||||
export default SceneLoader;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { Scene } from "./SceneDetails/Scene";
|
||||
import Scene from "./SceneDetails/Scene";
|
||||
import { SceneList } from "./SceneList";
|
||||
import { SceneMarkerList } from "./SceneMarkerList";
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ import { Tabs, Tab } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
useFindStudio,
|
||||
useStudioUpdate,
|
||||
useStudioCreate,
|
||||
useStudioDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
@@ -30,32 +28,29 @@ import { StudioEditPanel } from "./StudioEditPanel";
|
||||
import { StudioDetailsPanel } from "./StudioDetailsPanel";
|
||||
import { StudioMoviesPanel } from "./StudioMoviesPanel";
|
||||
|
||||
interface IProps {
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
interface IStudioParams {
|
||||
id?: string;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
export const Studio: React.FC = () => {
|
||||
const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { tab = "details", id = "new" } = useParams<IStudioParams>();
|
||||
const isNew = id === "new";
|
||||
const { tab = "details" } = useParams<IStudioParams>();
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
|
||||
const { data, loading: studioLoading, error } = useFindStudio(id);
|
||||
const studio = data?.findStudio;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [updateStudio] = useStudioUpdate();
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [deleteStudio] = useStudioDestroy({ id });
|
||||
const [deleteStudio] = useStudioDestroy({ id: studio.id });
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
@@ -68,53 +63,29 @@ export const Studio: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.findStudio) {
|
||||
setImage(undefined);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
||||
) {
|
||||
async function onSave(input: Partial<GQL.StudioUpdateInput>) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!isNew) {
|
||||
const result = await updateStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioUpdate) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
} else {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioCreateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioCreate?.id) {
|
||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||
setIsEditing(false);
|
||||
}
|
||||
const result = await updateStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioUpdate) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!studio?.id) return;
|
||||
if (!studio.id) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ studios: [studio.id] });
|
||||
Toast.success({
|
||||
@@ -153,7 +124,7 @@ export const Studio: React.FC = () => {
|
||||
id="dialogs.delete_confirm"
|
||||
values={{
|
||||
entityName:
|
||||
studio?.name ??
|
||||
studio.name ??
|
||||
intl.formatMessage({ id: "studio" }).toLocaleLowerCase(),
|
||||
}}
|
||||
/>
|
||||
@@ -167,7 +138,7 @@ export const Studio: React.FC = () => {
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
let studioImage = studio?.image_path;
|
||||
let studioImage = studio.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null) {
|
||||
studioImage = `${studioImage}&default=true`;
|
||||
@@ -177,9 +148,7 @@ export const Studio: React.FC = () => {
|
||||
}
|
||||
|
||||
if (studioImage) {
|
||||
return (
|
||||
<img className="logo" alt={studio?.name ?? ""} src={studioImage} />
|
||||
);
|
||||
return <img className="logo" alt={studio.name} src={studioImage} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,31 +163,13 @@ export const Studio: React.FC = () => {
|
||||
const setActiveTabKey = (newTab: string | null) => {
|
||||
if (tab !== newTab) {
|
||||
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||
history.replace(`/studios/${id}${tabParam}`);
|
||||
history.replace(`/studios/${studio.id}${tabParam}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading || studioLoading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!studio?.id && !isNew)
|
||||
return <ErrorMessage error={`No studio found with id ${id}.`} />;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div
|
||||
className={cx("studio-details", {
|
||||
"col-md-4": !isNew,
|
||||
"col-md-8": isNew,
|
||||
})}
|
||||
>
|
||||
{isNew && (
|
||||
<h2>
|
||||
{intl.formatMessage(
|
||||
{ id: "actions.add_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "studio" }) }
|
||||
)}
|
||||
</h2>
|
||||
)}
|
||||
<div className="studio-detils col-md-4">
|
||||
<div className="text-center">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
@@ -226,12 +177,12 @@ export const Studio: React.FC = () => {
|
||||
renderImage()
|
||||
)}
|
||||
</div>
|
||||
{!isEditing && !isNew && studio ? (
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<StudioDetailsPanel studio={studio} />
|
||||
<DetailsEditNavbar
|
||||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||
isNew={isNew}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={() => {}}
|
||||
@@ -243,7 +194,7 @@ export const Studio: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<StudioEditPanel
|
||||
studio={studio ?? ({} as Partial<GQL.Studio>)}
|
||||
studio={studio}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
@@ -251,46 +202,58 @@ export const Studio: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{studio?.id && (
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="studio-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={activeTabKey}
|
||||
onSelect={setActiveTabKey}
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="studio-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={activeTabKey}
|
||||
onSelect={setActiveTabKey}
|
||||
>
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<StudioScenesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
>
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<StudioScenesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
>
|
||||
<StudioGalleriesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<StudioImagesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
>
|
||||
<StudioPerformersPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="movies" title={intl.formatMessage({ id: "movies" })}>
|
||||
<StudioMoviesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="childstudios"
|
||||
title={intl.formatMessage({ id: "subsidiary_studios" })}
|
||||
>
|
||||
<StudioChildrenPanel studio={studio} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
<StudioGalleriesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<StudioImagesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
>
|
||||
<StudioPerformersPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="movies" title={intl.formatMessage({ id: "movies" })}>
|
||||
<StudioMoviesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="childstudios"
|
||||
title={intl.formatMessage({ id: "subsidiary_studios" })}
|
||||
>
|
||||
<StudioChildrenPanel studio={studio} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
{renderDeleteAlert()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StudioLoader: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { data, loading, error } = useFindStudio(id ?? "");
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!data?.findStudio)
|
||||
return <ErrorMessage error={`No studio found with id ${id}.`} />;
|
||||
|
||||
return <StudioPage studio={data.findStudio} />;
|
||||
};
|
||||
|
||||
export default StudioLoader;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { StudioList } from "../StudioList";
|
||||
|
||||
interface IStudioChildrenPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useStudioCreate } from "src/core/StashService";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { StudioEditPanel } from "./StudioEditPanel";
|
||||
|
||||
const StudioCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
// Studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
|
||||
const [createStudio] = useStudioCreate();
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
||||
) {
|
||||
try {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioCreateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioCreate?.id) {
|
||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
if (image) {
|
||||
return <img className="logo" alt="" src={image} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="studio-details col-md-8">
|
||||
<h2>
|
||||
{intl.formatMessage(
|
||||
{ id: "actions.add_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "studio" }) }
|
||||
)}
|
||||
</h2>
|
||||
<div className="text-center">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
)}
|
||||
</div>
|
||||
<StudioEditPanel
|
||||
studio={{}}
|
||||
onSubmit={onSave}
|
||||
onImageChange={setImage}
|
||||
onCancel={() => history.push("/studios")}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StudioCreate;
|
||||
@@ -7,7 +7,7 @@ import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IStudioDetailsPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
||||
@@ -31,7 +31,7 @@ export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
||||
}
|
||||
|
||||
function renderTagsList() {
|
||||
if (!studio?.aliases?.length) {
|
||||
if (!studio.aliases?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,12 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||
<>
|
||||
<Prompt
|
||||
when={formik.dirty}
|
||||
message="Unsaved changes. Are you sure you want to leave?"
|
||||
message={(location, action) => {
|
||||
// Check if it's a redirect after studio creation
|
||||
if (action === "PUSH" && location.pathname.startsWith("/studios/"))
|
||||
return true;
|
||||
return intl.formatMessage({ id: "dialogs.unsaved_changes" });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { studioFilterHook } from "src/core/studios";
|
||||
|
||||
interface IStudioGalleriesPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioGalleriesPanel: React.FC<IStudioGalleriesPanel> = ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { studioFilterHook } from "src/core/studios";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
|
||||
interface IStudioImagesPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioImagesPanel: React.FC<IStudioImagesPanel> = ({ studio }) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MovieList } from "src/components/Movies/MovieList";
|
||||
import { studioFilterHook } from "src/core/studios";
|
||||
|
||||
interface IStudioMoviesPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioMoviesPanel: React.FC<IStudioMoviesPanel> = ({ studio }) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PerformerList } from "src/components/Performers/PerformerList";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
|
||||
interface IStudioPerformersPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SceneList } from "src/components/Scenes/SceneList";
|
||||
import { studioFilterHook } from "src/core/studios";
|
||||
|
||||
interface IStudioScenesPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
studio: GQL.StudioDataFragment;
|
||||
}
|
||||
|
||||
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { Studio } from "./StudioDetails/Studio";
|
||||
import Studio from "./StudioDetails/Studio";
|
||||
import StudioCreate from "./StudioDetails/StudioCreate";
|
||||
import { StudioList } from "./StudioList";
|
||||
|
||||
const Studios = () => (
|
||||
<Switch>
|
||||
<Route exact path="/studios" component={StudioList} />
|
||||
<Route exact path="/studios/new" component={StudioCreate} />
|
||||
<Route path="/studios/:id/:tab?" component={Studio} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -2,20 +2,19 @@ import { Tabs, Tab, Dropdown } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
useFindTag,
|
||||
useTagUpdate,
|
||||
useTagCreate,
|
||||
useTagDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import {
|
||||
DetailsEditNavbar,
|
||||
ErrorMessage,
|
||||
Modal,
|
||||
LoadingIndicator,
|
||||
Icon,
|
||||
@@ -31,33 +30,30 @@ import { TagDetailsPanel } from "./TagDetailsPanel";
|
||||
import { TagEditPanel } from "./TagEditPanel";
|
||||
import { TagMergeModal } from "./TagMergeDialog";
|
||||
|
||||
interface IProps {
|
||||
tag: GQL.TagDataFragment;
|
||||
}
|
||||
|
||||
interface ITabParams {
|
||||
id?: string;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
export const Tag: React.FC = () => {
|
||||
const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { tab = "scenes", id = "new" } = useParams<ITabParams>();
|
||||
const isNew = id === "new";
|
||||
const { tab = "scenes" } = useParams<ITabParams>();
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
const [mergeType, setMergeType] = useState<"from" | "into" | undefined>();
|
||||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
|
||||
// Tag state
|
||||
const { data, error, loading } = useFindTag(id);
|
||||
const tag = data?.findTag;
|
||||
|
||||
const [updateTag] = useTagUpdate();
|
||||
const [createTag] = useTagCreate();
|
||||
const [deleteTag] = useTagDestroy({ id });
|
||||
const [deleteTag] = useTagDestroy({ id: tag.id });
|
||||
|
||||
const activeTabKey =
|
||||
tab === "markers" ||
|
||||
@@ -69,7 +65,7 @@ export const Tag: React.FC = () => {
|
||||
const setActiveTabKey = (newTab: string | null) => {
|
||||
if (tab !== newTab) {
|
||||
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||
history.replace(`/tags/${id}${tabParam}`);
|
||||
history.replace(`/tags/${tag.id}${tabParam}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,35 +84,21 @@ export const Tag: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.findTag) {
|
||||
setImage(undefined);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data?.findTag || loading) return <LoadingIndicator />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
}
|
||||
|
||||
function getTagInput(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
||||
...input,
|
||||
image,
|
||||
id: tag.id,
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
(ret as GQL.TagUpdateInput).id = id;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -125,39 +107,22 @@ export const Tag: React.FC = () => {
|
||||
) {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: tag?.parents ?? [],
|
||||
children: tag?.children ?? [],
|
||||
parents: tag.parents ?? [],
|
||||
children: tag.children ?? [],
|
||||
};
|
||||
if (!isNew) {
|
||||
const result = await updateTag({
|
||||
variables: {
|
||||
input: getTagInput(input) as GQL.TagUpdateInput,
|
||||
},
|
||||
const result = await updateTag({
|
||||
variables: {
|
||||
input: getTagInput(input) as GQL.TagUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.tagUpdate) {
|
||||
setIsEditing(false);
|
||||
const updated = result.data.tagUpdate;
|
||||
tagRelationHook(updated, oldRelations, {
|
||||
parents: updated.parents,
|
||||
children: updated.children,
|
||||
});
|
||||
if (result.data?.tagUpdate) {
|
||||
setIsEditing(false);
|
||||
const updated = result.data.tagUpdate;
|
||||
tagRelationHook(updated, oldRelations, {
|
||||
parents: updated.parents,
|
||||
children: updated.children,
|
||||
});
|
||||
return updated.id;
|
||||
}
|
||||
} else {
|
||||
const result = await createTag({
|
||||
variables: {
|
||||
input: getTagInput(input) as GQL.TagCreateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.tagCreate?.id) {
|
||||
setIsEditing(false);
|
||||
const created = result.data.tagCreate;
|
||||
tagRelationHook(created, oldRelations, {
|
||||
parents: created.parents,
|
||||
children: created.children,
|
||||
});
|
||||
return created.id;
|
||||
}
|
||||
return updated.id;
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -165,7 +130,7 @@ export const Tag: React.FC = () => {
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!tag?.id) return;
|
||||
if (!tag.id) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||
Toast.success({
|
||||
@@ -179,8 +144,8 @@ export const Tag: React.FC = () => {
|
||||
async function onDelete() {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: tag?.parents ?? [],
|
||||
children: tag?.children ?? [],
|
||||
parents: tag.parents ?? [],
|
||||
children: tag.children ?? [],
|
||||
};
|
||||
await deleteTag();
|
||||
tagRelationHook(tag as GQL.TagDataFragment, oldRelations, {
|
||||
@@ -212,7 +177,7 @@ export const Tag: React.FC = () => {
|
||||
id="dialogs.delete_confirm"
|
||||
values={{
|
||||
entityName:
|
||||
tag?.name ??
|
||||
tag.name ??
|
||||
intl.formatMessage({ id: "tag" }).toLocaleLowerCase(),
|
||||
}}
|
||||
/>
|
||||
@@ -227,7 +192,7 @@ export const Tag: React.FC = () => {
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
let tagImage = tag?.image_path;
|
||||
let tagImage = tag.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null) {
|
||||
tagImage = `${tagImage}&default=true`;
|
||||
@@ -237,7 +202,7 @@ export const Tag: React.FC = () => {
|
||||
}
|
||||
|
||||
if (tagImage) {
|
||||
return <img className="logo" alt={tag?.name ?? ""} src={tagImage} />;
|
||||
return <img className="logo" alt={tag.name} src={tagImage} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,27 +249,22 @@ export const Tag: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div
|
||||
className={cx("tag-details", {
|
||||
"col-md-4": !isNew,
|
||||
"col-md-8": isNew,
|
||||
})}
|
||||
>
|
||||
<div className="tag-details col-md-4">
|
||||
<div className="text-center logo-container">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
)}
|
||||
{!isNew && tag && <h2>{tag.name}</h2>}
|
||||
<h2>{tag.name}</h2>
|
||||
</div>
|
||||
{!isEditing && !isNew && tag ? (
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<TagDetailsPanel tag={tag} />
|
||||
{/* HACK - this is also rendered in the TagEditPanel */}
|
||||
<DetailsEditNavbar
|
||||
objectName={tag.name ?? "tag"}
|
||||
isNew={isNew}
|
||||
objectName={tag.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={() => {}}
|
||||
@@ -317,7 +277,7 @@ export const Tag: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<TagEditPanel
|
||||
tag={tag ?? undefined}
|
||||
tag={tag}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
@@ -325,43 +285,52 @@ export const Tag: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isNew && tag && (
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="tag-tabs"
|
||||
mountOnEnter
|
||||
activeKey={activeTabKey}
|
||||
onSelect={setActiveTabKey}
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="tag-tabs"
|
||||
mountOnEnter
|
||||
activeKey={activeTabKey}
|
||||
onSelect={setActiveTabKey}
|
||||
>
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<TagScenesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<TagImagesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
>
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<TagScenesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<TagImagesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
>
|
||||
<TagGalleriesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="markers"
|
||||
title={intl.formatMessage({ id: "markers" })}
|
||||
>
|
||||
<TagMarkersPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
>
|
||||
<TagPerformersPanel tag={tag} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
<TagGalleriesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="markers" title={intl.formatMessage({ id: "markers" })}>
|
||||
<TagMarkersPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
>
|
||||
<TagPerformersPanel tag={tag} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
{renderDeleteAlert()}
|
||||
{renderMergeDialog()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TagLoader: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { data, loading, error } = useFindTag(id ?? "");
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!data?.findTag)
|
||||
return <ErrorMessage error={`No tag found with id ${id}.`} />;
|
||||
|
||||
return <TagPage tag={data.findTag} />;
|
||||
};
|
||||
|
||||
export default TagLoader;
|
||||
|
||||
91
ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx
Normal file
91
ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useTagCreate } from "src/core/StashService";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { tagRelationHook } from "src/core/tags";
|
||||
import { TagEditPanel } from "./TagEditPanel";
|
||||
|
||||
const TagCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
function getTagInput(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
const ret: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
||||
...input,
|
||||
image,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async function onSave(
|
||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
||||
) {
|
||||
try {
|
||||
const oldRelations = {
|
||||
parents: [],
|
||||
children: [],
|
||||
};
|
||||
const result = await createTag({
|
||||
variables: {
|
||||
input: getTagInput(input) as GQL.TagCreateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.tagCreate?.id) {
|
||||
const created = result.data.tagCreate;
|
||||
tagRelationHook(created, oldRelations, {
|
||||
parents: created.parents,
|
||||
children: created.children,
|
||||
});
|
||||
return created.id;
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
if (image) {
|
||||
return <img className="logo" alt="" src={image} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="tag-details col-md-8">
|
||||
<div className="text-center logo-container">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
renderImage()
|
||||
)}
|
||||
</div>
|
||||
<TagEditPanel
|
||||
onSubmit={onSave}
|
||||
onCancel={() => history.push("/tags")}
|
||||
onDelete={() => {}}
|
||||
setImage={setImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagCreate;
|
||||
@@ -5,12 +5,12 @@ import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
interface ITagDetails {
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
tag: GQL.TagDataFragment;
|
||||
}
|
||||
|
||||
export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag }) => {
|
||||
function renderAliasesField() {
|
||||
if (!tag.aliases?.length) {
|
||||
if (!tag.aliases.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
import { SceneMarkerList } from "src/components/Scenes/SceneMarkerList";
|
||||
|
||||
interface ITagMarkersPanel {
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
tag: GQL.TagDataFragment;
|
||||
}
|
||||
|
||||
export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({ tag }) => {
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
const tagValue = { id: tag.id!, label: tag.name! };
|
||||
const tagValue = { id: tag.id, label: tag.name };
|
||||
// if tag is already present, then we modify it, otherwise add
|
||||
let tagCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.type === "tags";
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { Tag } from "./TagDetails/Tag";
|
||||
import Tag from "./TagDetails/Tag";
|
||||
import TagCreate from "./TagDetails/TagCreate";
|
||||
import { TagList } from "./TagList";
|
||||
|
||||
const Tags = () => (
|
||||
<Switch>
|
||||
<Route exact path="/tags" component={TagList} />
|
||||
<Route exact path="/tags/new" component={TagCreate} />
|
||||
<Route path="/tags/:id/:tab?" component={Tag} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,12 @@ import { PerformersCriterion } from "src/models/list-filter/criteria/performers"
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
||||
export const performerFilterHook = (
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
) => {
|
||||
export const performerFilterHook = (performer: GQL.PerformerDataFragment) => {
|
||||
return (filter: ListFilterModel) => {
|
||||
const performerValue = { id: performer.id!, label: performer.name! };
|
||||
const performerValue = {
|
||||
id: performer.id,
|
||||
label: performer.name ?? `Performer ${performer.id}`,
|
||||
};
|
||||
// if performers is already present, then we modify it, otherwise add
|
||||
let performerCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.type === "performers";
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
||||
export const studioFilterHook = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
export const studioFilterHook = (studio: GQL.StudioDataFragment) => {
|
||||
return (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
|
||||
let studioCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.type === "studios";
|
||||
|
||||
Reference in New Issue
Block a user