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