Rename Movie to Group in UI (#4963)

* Replace movies with groups in the UI
* Massage menu items
* Change view names
* Rename Movie components to Group
* Refactor movie to group variable names
* Rename movie class names to group
This commit is contained in:
WithoutPants 2024-06-26 11:39:31 +10:00 committed by GitHub
parent d986a9eb4f
commit af6841be49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 643 additions and 612 deletions

View File

@ -66,7 +66,7 @@ const Galleries = lazyComponent(
() => import("./components/Galleries/Galleries")
);
const Movies = lazyComponent(() => import("./components/Movies/Movies"));
const Groups = lazyComponent(() => import("./components/Movies/Movies"));
const Tags = lazyComponent(() => import("./components/Tags/Tags"));
const Images = lazyComponent(() => import("./components/Images/Images"));
const Setup = lazyComponent(() => import("./components/Setup/Setup"));
@ -312,7 +312,7 @@ export const App: React.FC = () => {
<Route path="/performers" component={Performers} />
<Route path="/tags" component={Tags} />
<Route path="/studios" component={Studios} />
<Route path="/movies" component={Movies} />
<Route path="/groups" component={Groups} />
<Route path="/stats" component={Stats} />
<Route path="/settings" component={Settings} />
<Route

View File

@ -7,7 +7,7 @@ import { ConfigurationContext } from "src/hooks/Config";
import { ListFilterModel } from "src/models/list-filter/filter";
import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow";
import { ImageRecommendationRow } from "../Images/ImageRecommendationRow";
import { MovieRecommendationRow } from "../Movies/MovieRecommendationRow";
import { GroupRecommendationRow } from "../Movies/MovieRecommendationRow";
import { PerformerRecommendationRow } from "../Performers/PerformerRecommendationRow";
import { SceneRecommendationRow } from "../Scenes/SceneRecommendationRow";
import { StudioRecommendationRow } from "../Studios/StudioRecommendationRow";
@ -45,7 +45,7 @@ const RecommendationRow: React.FC<IFilter> = ({ mode, filter, header }) => {
);
case GQL.FilterMode.Movies:
return (
<MovieRecommendationRow
<GroupRecommendationRow
isTouch={isTouch}
filter={filter}
header={header}

View File

@ -22,7 +22,7 @@ interface IAddSavedFilterModalProps {
const FilterModeToMessageID = {
[GQL.FilterMode.Galleries]: "galleries",
[GQL.FilterMode.Images]: "images",
[GQL.FilterMode.Movies]: "movies",
[GQL.FilterMode.Movies]: "groups",
[GQL.FilterMode.Performers]: "performers",
[GQL.FilterMode.SceneMarkers]: "markers",
[GQL.FilterMode.Scenes]: "scenes",

View File

@ -111,7 +111,7 @@
}
}
.movie-skeleton {
.group-skeleton {
max-width: 240px;
min-height: 540px;
min-width: 240px;
@ -313,7 +313,7 @@
width: 20rem;
}
.slick-list .movie-card.card {
.slick-list .group-card.card {
width: 16rem;
}

View File

@ -193,7 +193,7 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
const FilterModeToConfigKey = {
[FilterMode.Galleries]: "galleries",
[FilterMode.Images]: "images",
[FilterMode.Movies]: "movies",
[FilterMode.Movies]: "groups",
[FilterMode.Performers]: "performers",
[FilterMode.SceneMarkers]: "sceneMarkers",
[FilterMode.Scenes]: "scenes",

View File

@ -24,7 +24,7 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
inputType !== "performer_tags" &&
inputType !== "tags" &&
inputType !== "scenes" &&
inputType !== "movies" &&
inputType !== "groups" &&
inputType !== "galleries"
) {
return null;

View File

@ -2,7 +2,7 @@ export enum View {
Galleries = "galleries",
Images = "images",
Scenes = "scenes",
Movies = "movies",
Groups = "groups",
Performers = "performers",
Tags = "tags",
SceneMarkers = "scene_markers",
@ -17,7 +17,7 @@ export enum View {
PerformerScenes = "performer_scenes",
PerformerGalleries = "performer_galleries",
PerformerImages = "performer_images",
PerformerMovies = "performer_movies",
PerformerGroups = "performer_groups",
PerformerAppearsWith = "performer_appears_with",
StudioGalleries = "studio_galleries",
@ -26,9 +26,9 @@ export enum View {
GalleryImages = "gallery_images",
StudioScenes = "studio_scenes",
StudioMovies = "studio_movies",
StudioGroups = "studio_groups",
StudioPerformers = "studio_performers",
StudioChildren = "studio_children",
MovieScenes = "movie_scenes",
GroupScenes = "group_scenes",
}

View File

@ -1,4 +1,10 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import React, {
useEffect,
useRef,
useState,
useCallback,
useMemo,
} from "react";
import {
defineMessages,
FormattedMessage,
@ -52,9 +58,9 @@ const messages = defineMessages({
id: "images",
defaultMessage: "Images",
},
movies: {
id: "movies",
defaultMessage: "Movies",
groups: {
id: "groups",
defaultMessage: "Groups",
},
markers: {
id: "markers",
@ -107,9 +113,9 @@ const allMenuItems: IMenuItem[] = [
hotkey: "g i",
},
{
name: "movies",
message: messages.movies,
href: "/movies",
name: "groups",
message: messages.groups,
href: "/groups",
icon: faFilm,
hotkey: "g v",
userCreatable: true,
@ -179,20 +185,26 @@ export const MainNavbar: React.FC = () => {
const { configuration, loading } = React.useContext(ConfigurationContext);
const { openManual } = React.useContext(ManualStateContext);
// Show all menu items by default, unless config says otherwise
const [menuItems, setMenuItems] = useState<IMenuItem[]>(allMenuItems);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const iCfg = configuration?.interface;
if (iCfg?.menuItems) {
setMenuItems(
allMenuItems.filter((menuItem) =>
iCfg.menuItems!.includes(menuItem.name)
)
);
// Show all menu items by default, unless config says otherwise
const menuItems = useMemo(() => {
let cfgMenuItems = configuration?.interface.menuItems;
if (!cfgMenuItems) {
return allMenuItems;
}
// translate old movies menu item to groups
cfgMenuItems = cfgMenuItems.map((item) => {
if (item === "movies") {
return "groups";
}
return item;
});
return allMenuItems.filter((menuItem) =>
cfgMenuItems!.includes(menuItem.name)
);
}, [configuration]);
// react-bootstrap typing bug

View File

@ -24,7 +24,7 @@ interface IListOperationProps {
onClose: (applied: boolean) => void;
}
export const EditMoviesDialog: React.FC<IListOperationProps> = (
export const EditGroupsDialog: React.FC<IListOperationProps> = (
props: IListOperationProps
) => {
const intl = useIntl();
@ -69,7 +69,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl.formatMessage({ id: "movies" }).toLocaleLowerCase(),
entity: intl.formatMessage({ id: "groups" }).toLocaleLowerCase(),
}
)
);
@ -126,7 +126,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
icon={faPencilAlt}
header={intl.formatMessage(
{ id: "actions.edit_entity" },
{ entityType: intl.formatMessage({ id: "movies" }) }
{ entityType: intl.formatMessage({ id: "groups" }) }
)}
accept={{
onClick: onSave,

View File

@ -12,7 +12,7 @@ import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
import ScreenUtils from "src/utils/screen";
interface IProps {
movie: GQL.MovieDataFragment;
group: GQL.MovieDataFragment;
containerWidth?: number;
sceneIndex?: number;
selecting?: boolean;
@ -20,8 +20,8 @@ interface IProps {
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}
export const MovieCard: React.FC<IProps> = ({
movie,
export const GroupCard: React.FC<IProps> = ({
group,
sceneIndex,
containerWidth,
selecting,
@ -47,7 +47,7 @@ export const MovieCard: React.FC<IProps> = ({
return (
<>
<hr />
<span className="movie-scene-number">
<span className="group-scene-number">
<FormattedMessage id="scene" /> #{sceneIndex}
</span>
</>
@ -55,9 +55,9 @@ export const MovieCard: React.FC<IProps> = ({
}
function maybeRenderScenesPopoverButton() {
if (movie.scenes.length === 0) return;
if (group.scenes.length === 0) return;
const popoverContent = movie.scenes.map((scene) => (
const popoverContent = group.scenes.map((scene) => (
<SceneLink key={scene.id} scene={scene} />
));
@ -69,31 +69,31 @@ export const MovieCard: React.FC<IProps> = ({
>
<Button className="minimal">
<Icon icon={faPlayCircle} />
<span>{movie.scenes.length}</span>
<span>{group.scenes.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderTagPopoverButton() {
if (movie.tags.length <= 0) return;
if (group.tags.length <= 0) return;
const popoverContent = movie.tags.map((tag) => (
<TagLink key={tag.id} linkType="movie" tag={tag} />
const popoverContent = group.tags.map((tag) => (
<TagLink key={tag.id} linkType="group" tag={tag} />
));
return (
<HoverPopover placement="bottom" content={popoverContent}>
<Button className="minimal tag-count">
<Icon icon={faTag} />
<span>{movie.tags.length}</span>
<span>{group.tags.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderPopoverButtonGroup() {
if (sceneIndex || movie.scenes.length > 0 || movie.tags.length > 0) {
if (sceneIndex || group.scenes.length > 0 || group.tags.length > 0) {
return (
<>
{maybeRenderSceneNumber()}
@ -109,28 +109,28 @@ export const MovieCard: React.FC<IProps> = ({
return (
<GridCard
className="movie-card"
url={`/movies/${movie.id}`}
className="group-card"
url={`/groups/${group.id}`}
width={cardWidth}
title={movie.name}
linkClassName="movie-card-header"
title={group.name}
linkClassName="group-card-header"
image={
<>
<img
loading="lazy"
className="movie-card-image"
alt={movie.name ?? ""}
src={movie.front_image_path ?? ""}
className="group-card-image"
alt={group.name ?? ""}
src={group.front_image_path ?? ""}
/>
<RatingBanner rating={movie.rating100} />
<RatingBanner rating={group.rating100} />
</>
}
details={
<div className="movie-card__details">
<span className="movie-card__date">{movie.date}</span>
<div className="group-card__details">
<span className="group-card__date">{group.date}</span>
<TruncatedText
className="movie-card__description"
text={movie.synopsis}
className="group-card__description"
text={group.synopsis}
lineCount={3}
/>
</div>

View File

@ -1,27 +1,27 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieCard } from "./MovieCard";
import { GroupCard } from "./MovieCard";
import { useContainerDimensions } from "../Shared/GridCard/GridCard";
interface IMovieCardGrid {
movies: GQL.MovieDataFragment[];
interface IGroupCardGrid {
groups: GQL.MovieDataFragment[];
selectedIds: Set<string>;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const MovieCardGrid: React.FC<IMovieCardGrid> = ({
movies,
export const GroupCardGrid: React.FC<IGroupCardGrid> = ({
groups,
selectedIds,
onSelectChange,
}) => {
const [componentRef, { width }] = useContainerDimensions();
return (
<div className="row justify-content-center" ref={componentRef}>
{movies.map((p) => (
<MovieCard
{groups.map((p) => (
<GroupCard
key={p.id}
containerWidth={width}
movie={p}
group={p}
selecting={selectedIds.size > 0}
selected={selectedIds.has(p.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>

View File

@ -17,12 +17,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useLightbox } from "src/hooks/Lightbox/hooks";
import { ModalComponent } from "src/components/Shared/Modal";
import { useToast } from "src/hooks/Toast";
import { MovieScenesPanel } from "./MovieScenesPanel";
import { GroupScenesPanel } from "./MovieScenesPanel";
import {
CompressedMovieDetailsPanel,
MovieDetailsPanel,
GroupDetailsPanel,
} from "./MovieDetailsPanel";
import { MovieEditPanel } from "./MovieEditPanel";
import { GroupEditPanel } from "./MovieEditPanel";
import {
faChevronDown,
faChevronUp,
@ -38,14 +38,14 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton";
interface IProps {
movie: GQL.MovieDataFragment;
group: GQL.MovieDataFragment;
}
interface IMovieParams {
interface IGroupParams {
id: string;
}
const MoviePage: React.FC<IProps> = ({ movie }) => {
const GroupPage: React.FC<IProps> = ({ group }) => {
const intl = useIntl();
const history = useHistory();
const Toast = useToast();
@ -70,35 +70,35 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
const [encodingImage, setEncodingImage] = useState<boolean>(false);
const defaultImage =
movie.front_image_path && movie.front_image_path.includes("default=true")
group.front_image_path && group.front_image_path.includes("default=true")
? true
: false;
const lightboxImages = useMemo(() => {
const covers = [
...(movie.front_image_path && !defaultImage
...(group.front_image_path && !defaultImage
? [
{
paths: {
thumbnail: movie.front_image_path,
image: movie.front_image_path,
thumbnail: group.front_image_path,
image: group.front_image_path,
},
},
]
: []),
...(movie.back_image_path
...(group.back_image_path
? [
{
paths: {
thumbnail: movie.back_image_path,
image: movie.back_image_path,
thumbnail: group.back_image_path,
image: group.back_image_path,
},
},
]
: []),
];
return covers;
}, [movie.front_image_path, movie.back_image_path, defaultImage]);
}, [group.front_image_path, group.back_image_path, defaultImage]);
const index = lightboxImages.length;
@ -108,7 +108,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
const [updateMovie, { loading: updating }] = useMovieUpdate();
const [deleteMovie, { loading: deleting }] = useMovieDestroy({
id: movie.id,
id: group.id,
});
// set up hotkeys
@ -135,7 +135,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
await updateMovie({
variables: {
input: {
id: movie.id,
id: group.id,
...input,
},
},
@ -144,7 +144,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
Toast.success(
intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
{ entity: intl.formatMessage({ id: "group" }).toLocaleLowerCase() }
)
);
}
@ -157,7 +157,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
}
// redirect to movies page
history.push(`/movies`);
history.push(`/groups`);
}
function toggleEditing(value?: boolean) {
@ -187,8 +187,8 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
id="dialogs.delete_confirm"
values={{
entityName:
movie.name ??
intl.formatMessage({ id: "movie" }).toLocaleLowerCase(),
group.name ??
intl.formatMessage({ id: "group" }).toLocaleLowerCase(),
}}
/>
</p>
@ -216,7 +216,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
}
function renderFrontImage() {
let image = movie.front_image_path;
let image = group.front_image_path;
if (isEditing) {
if (frontImage === null && image) {
const imageURL = new URL(image);
@ -229,14 +229,14 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
if (image && defaultImage) {
return (
<div className="movie-image-container">
<div className="group-image-container">
<DetailImage alt="Front Cover" src={image} />
</div>
);
} else if (image) {
return (
<Button
className="movie-image-container"
className="group-image-container"
variant="link"
onClick={() => showLightbox()}
>
@ -247,7 +247,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
}
function renderBackImage() {
let image = movie.back_image_path;
let image = group.back_image_path;
if (isEditing) {
if (backImage === null) {
image = undefined;
@ -259,7 +259,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
if (image) {
return (
<Button
className="movie-image-container"
className="group-image-container"
variant="link"
onClick={() => showLightbox(index - 1)}
>
@ -271,26 +271,26 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
const renderClickableIcons = () => (
<span className="name-icons">
{movie.urls.length > 0 && <ExternalLinksButton urls={movie.urls} />}
{group.urls.length > 0 && <ExternalLinksButton urls={group.urls} />}
</span>
);
function maybeRenderAliases() {
if (movie?.aliases) {
if (group?.aliases) {
return (
<div>
<span className="alias-head">{movie?.aliases}</span>
<span className="alias-head">{group?.aliases}</span>
</div>
);
}
}
function setRating(v: number | null) {
if (movie.id) {
if (group.id) {
updateMovie({
variables: {
input: {
id: movie.id,
id: group.id,
rating100: v,
},
},
@ -298,13 +298,13 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
}
}
const renderTabs = () => <MovieScenesPanel active={true} movie={movie} />;
const renderTabs = () => <GroupScenesPanel active={true} group={group} />;
function maybeRenderDetails() {
if (!isEditing) {
return (
<MovieDetailsPanel
movie={movie}
<GroupDetailsPanel
group={group}
collapsed={collapsed}
fullWidth={!collapsed && !compactExpandedDetails}
/>
@ -315,8 +315,8 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
function maybeRenderEditPanel() {
if (isEditing) {
return (
<MovieEditPanel
movie={movie}
<GroupEditPanel
group={group}
onSubmit={onSave}
onCancel={() => toggleEditing()}
onDelete={onDelete}
@ -329,7 +329,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
{
return (
<DetailsEditNavbar
objectName={movie.name}
objectName={group.name}
isNew={false}
isEditing={isEditing}
onToggleEdit={() => toggleEditing()}
@ -343,12 +343,12 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
function maybeRenderCompressedDetails() {
if (!isEditing && loadStickyHeader) {
return <CompressedMovieDetailsPanel movie={movie} />;
return <CompressedMovieDetailsPanel group={group} />;
}
}
function maybeRenderHeaderBackgroundImage() {
let image = movie.front_image_path;
let image = group.front_image_path;
if (enableBackgroundImage && !isEditing && image) {
const imageURL = new URL(image);
let isDefaultImage = imageURL.searchParams.get("default");
@ -360,7 +360,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
<img
className="background-image"
src={image}
alt={`${movie.name} background`}
alt={`${group.name} background`}
/>
</picture>
</div>
@ -384,9 +384,9 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
});
return (
<div id="movie-page" className="row">
<div id="group-page" className="row">
<Helmet>
<title>{movie?.name}</title>
<title>{group?.name}</title>
</Helmet>
<div className={headerClassName}>
@ -399,7 +399,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
message={intl.formatMessage({ id: "actions.encoding_image" })}
/>
) : (
<div className="movie-images">
<div className="group-images">
{renderFrontImage()}
{renderBackImage()}
</div>
@ -407,15 +407,15 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
</div>
</div>
<div className="row">
<div className="movie-head col">
<div className="group-head col">
<h2>
<span className="movie-name">{movie.name}</span>
<span className="group-name">{group.name}</span>
{maybeRenderShowCollapseButton()}
{renderClickableIcons()}
</h2>
{maybeRenderAliases()}
<RatingSystem
value={movie.rating100}
value={group.rating100}
onSetRating={(value) => setRating(value)}
clickToRate
withoutContext
@ -428,8 +428,8 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
</div>
{maybeRenderCompressedDetails()}
<div className="detail-body">
<div className="movie-body">
<div className="movie-tabs">{maybeRenderTab()}</div>
<div className="group-body">
<div className="group-tabs">{maybeRenderTab()}</div>
</div>
</div>
{renderDeleteAlert()}
@ -437,7 +437,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
);
};
const MovieLoader: React.FC<RouteComponentProps<IMovieParams>> = ({
const GroupLoader: React.FC<RouteComponentProps<IGroupParams>> = ({
match,
}) => {
const { id } = match.params;
@ -450,7 +450,7 @@ const MovieLoader: React.FC<RouteComponentProps<IMovieParams>> = ({
if (!data?.findMovie)
return <ErrorMessage error={`No movie found with id ${id}.`} />;
return <MoviePage movie={data.findMovie} />;
return <GroupPage group={data.findMovie} />;
};
export default MovieLoader;
export default GroupLoader;

View File

@ -5,16 +5,16 @@ import { useHistory, useLocation } from "react-router-dom";
import { useIntl } from "react-intl";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useToast } from "src/hooks/Toast";
import { MovieEditPanel } from "./MovieEditPanel";
import { GroupEditPanel } from "./MovieEditPanel";
const MovieCreate: React.FC = () => {
const GroupCreate: React.FC = () => {
const history = useHistory();
const intl = useIntl();
const Toast = useToast();
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const movie = {
const group = {
name: query.get("q") ?? undefined,
};
@ -30,7 +30,7 @@ const MovieCreate: React.FC = () => {
variables: { input },
});
if (result.data?.movieCreate?.id) {
history.push(`/movies/${result.data.movieCreate.id}`);
history.push(`/groups/${result.data.movieCreate.id}`);
Toast.success(
intl.formatMessage(
{ id: "toast.created_entity" },
@ -43,7 +43,7 @@ const MovieCreate: React.FC = () => {
function renderFrontImage() {
if (frontImage) {
return (
<div className="movie-image-container">
<div className="group-image-container">
<img alt="Front Cover" src={frontImage} />
</div>
);
@ -53,7 +53,7 @@ const MovieCreate: React.FC = () => {
function renderBackImage() {
if (backImage) {
return (
<div className="movie-image-container">
<div className="group-image-container">
<img alt="Back Cover" src={backImage} />
</div>
);
@ -63,24 +63,24 @@ const MovieCreate: React.FC = () => {
// TODO: CSS class
return (
<div className="row">
<div className="movie-details mb-3 col">
<div className="group-details mb-3 col">
<div className="logo w-100">
{encodingImage ? (
<LoadingIndicator
message={intl.formatMessage({ id: "actions.encoding_image" })}
/>
) : (
<div className="movie-images">
<div className="group-images">
{renderFrontImage()}
{renderBackImage()}
</div>
)}
</div>
<MovieEditPanel
movie={movie}
<GroupEditPanel
group={group}
onSubmit={onSave}
onCancel={() => history.push("/movies")}
onCancel={() => history.push("/groups")}
onDelete={() => {}}
setFrontImage={setFrontImage}
setBackImage={setBackImage}
@ -91,4 +91,4 @@ const MovieCreate: React.FC = () => {
);
};
export default MovieCreate;
export default GroupCreate;

View File

@ -7,14 +7,14 @@ import { Link } from "react-router-dom";
import { DirectorLink } from "src/components/Shared/Link";
import { TagLink } from "src/components/Shared/TagLink";
interface IMovieDetailsPanel {
movie: GQL.MovieDataFragment;
interface IGroupDetailsPanel {
group: GQL.MovieDataFragment;
collapsed?: boolean;
fullWidth?: boolean;
}
export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
movie,
export const GroupDetailsPanel: React.FC<IGroupDetailsPanel> = ({
group,
collapsed,
fullWidth,
}) => {
@ -22,13 +22,13 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
const intl = useIntl();
function renderTagsField() {
if (!movie.tags.length) {
if (!group.tags.length) {
return;
}
return (
<ul className="pl-0">
{(movie.tags ?? []).map((tag) => (
<TagLink key={tag.id} linkType="movie" tag={tag} />
{(group.tags ?? []).map((tag) => (
<TagLink key={tag.id} linkType="group" tag={tag} />
))}
</ul>
);
@ -40,7 +40,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
<>
<DetailItem
id="synopsis"
value={movie.synopsis}
value={group.synopsis}
fullWidth={fullWidth}
/>
<DetailItem
@ -58,21 +58,21 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
<DetailItem
id="duration"
value={
movie.duration ? TextUtils.secondsToTimestamp(movie.duration) : ""
group.duration ? TextUtils.secondsToTimestamp(group.duration) : ""
}
fullWidth={fullWidth}
/>
<DetailItem
id="date"
value={movie.date ? TextUtils.formatDate(intl, movie.date) : ""}
value={group.date ? TextUtils.formatDate(intl, group.date) : ""}
fullWidth={fullWidth}
/>
<DetailItem
id="studio"
value={
movie.studio?.id ? (
<Link to={`/studios/${movie.studio?.id}`}>
{movie.studio?.name}
group.studio?.id ? (
<Link to={`/studios/${group.studio?.id}`}>
{group.studio?.name}
</Link>
) : (
""
@ -84,8 +84,8 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
<DetailItem
id="director"
value={
movie.director ? (
<DirectorLink director={movie.director} linkType="movie" />
group.director ? (
<DirectorLink director={group.director} linkType="group" />
) : (
""
)
@ -97,8 +97,8 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
);
};
export const CompressedMovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
movie,
export const CompressedMovieDetailsPanel: React.FC<IGroupDetailsPanel> = ({
group,
}) => {
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
@ -107,13 +107,13 @@ export const CompressedMovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
return (
<div className="sticky detail-header">
<div className="sticky detail-header-group">
<a className="movie-name" onClick={() => scrollToTop()}>
{movie.name}
<a className="group-name" onClick={() => scrollToTop()}>
{group.name}
</a>
{movie?.studio?.name ? (
{group?.studio?.name ? (
<>
<span className="detail-divider">/</span>
<span className="movie-studio">{movie?.studio?.name}</span>
<span className="group-studio">{group?.studio?.name}</span>
</>
) : (
""

View File

@ -15,7 +15,7 @@ import TextUtils from "src/utils/text";
import ImageUtils from "src/utils/image";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import { MovieScrapeDialog } from "./MovieScrapeDialog";
import { GroupScrapeDialog } from "./MovieScrapeDialog";
import isEqual from "lodash-es/isEqual";
import { handleUnsavedChanges } from "src/utils/navigation";
import { formikUtils } from "src/utils/form";
@ -27,8 +27,8 @@ import {
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
import { useTagsEdit } from "src/hooks/tagsEdit";
interface IMovieEditPanel {
movie: Partial<GQL.MovieDataFragment>;
interface IGroupEditPanel {
group: Partial<GQL.MovieDataFragment>;
onSubmit: (movie: GQL.MovieCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
@ -37,8 +37,8 @@ interface IMovieEditPanel {
setEncodingImage: (loading: boolean) => void;
}
export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
movie,
export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
group,
onSubmit,
onCancel,
onDelete,
@ -49,7 +49,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
const intl = useIntl();
const Toast = useToast();
const isNew = movie.id === undefined;
const isNew = group.id === undefined;
const [isLoading, setIsLoading] = useState(false);
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
@ -57,7 +57,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
const [imageClipboard, setImageClipboard] = useState<string>();
const Scrapers = useListMovieScrapers();
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
const [scrapedGroup, setScrapedGroup] = useState<GQL.ScrapedMovie>();
const [studio, setStudio] = useState<Studio | null>(null);
@ -76,15 +76,15 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
});
const initialValues = {
name: movie?.name ?? "",
aliases: movie?.aliases ?? "",
duration: movie?.duration ?? null,
date: movie?.date ?? "",
studio_id: movie?.studio?.id ?? null,
tag_ids: (movie?.tags ?? []).map((t) => t.id),
director: movie?.director ?? "",
urls: movie?.urls ?? [],
synopsis: movie?.synopsis ?? "",
name: group?.name ?? "",
aliases: group?.aliases ?? "",
duration: group?.duration ?? null,
date: group?.date ?? "",
studio_id: group?.studio?.id ?? null,
tag_ids: (group?.tags ?? []).map((t) => t.id),
director: group?.director ?? "",
urls: group?.urls ?? [],
synopsis: group?.synopsis ?? "",
};
type InputValues = yup.InferType<typeof schema>;
@ -97,7 +97,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
});
const { tags, updateTagsStateFromScraper, tagsControl } = useTagsEdit(
movie.tags,
group.tags,
(ids) => formik.setFieldValue("tag_ids", ids)
);
@ -107,8 +107,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
}
useEffect(() => {
setStudio(movie.studio ?? null);
}, [movie.studio]);
setStudio(group.studio ?? null);
}, [group.studio]);
// set up hotkeys
useEffect(() => {
@ -128,7 +128,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
};
});
function updateMovieEditStateFromScraper(
function updateGroupEditStateFromScraper(
state: Partial<GQL.ScrapedMovieDataFragment>
) {
if (state.name) {
@ -200,11 +200,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
return;
}
// if this is a new movie, just dump the data
// if this is a new group, just dump the data
if (isNew) {
updateMovieEditStateFromScraper(result.data.scrapeMovieURL);
updateGroupEditStateFromScraper(result.data.scrapeMovieURL);
} else {
setScrapedMovie(result.data.scrapeMovieURL);
setScrapedGroup(result.data.scrapeMovieURL);
}
} catch (e) {
Toast.error(e);
@ -223,25 +223,25 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
}
function maybeRenderScrapeDialog() {
if (!scrapedMovie) {
if (!scrapedGroup) {
return;
}
const currentMovie = {
id: movie.id!,
const currentGroup = {
id: group.id!,
...formik.values,
};
// Get image paths for scrape gui
currentMovie.front_image = movie?.front_image_path;
currentMovie.back_image = movie?.back_image_path;
currentGroup.front_image = group?.front_image_path;
currentGroup.back_image = group?.back_image_path;
return (
<MovieScrapeDialog
movie={currentMovie}
movieStudio={studio}
movieTags={tags}
scraped={scrapedMovie}
<GroupScrapeDialog
group={currentGroup}
groupStudio={studio}
groupTags={tags}
scraped={scrapedGroup}
onClose={(m) => {
onScrapeDialogClosed(m);
}}
@ -251,9 +251,9 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
function onScrapeDialogClosed(p?: GQL.ScrapedMovieDataFragment) {
if (p) {
updateMovieEditStateFromScraper(p);
updateGroupEditStateFromScraper(p);
}
setScrapedMovie(undefined);
setScrapedGroup(undefined);
}
const encodingImage = ImageUtils.usePasteImage(showImageAlert);
@ -373,7 +373,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
<h2>
{intl.formatMessage(
{ id: "actions.add_entity" },
{ entityType: intl.formatMessage({ id: "movie" }) }
{ entityType: intl.formatMessage({ id: "group" }) }
)}
</h2>
)}
@ -382,14 +382,14 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
when={formik.dirty}
message={(location, action) => {
// Check if it's a redirect after movie creation
if (action === "PUSH" && location.pathname.startsWith("/movies/"))
if (action === "PUSH" && location.pathname.startsWith("/groups/"))
return true;
return handleUnsavedChanges(intl, "movies", movie.id)(location);
return handleUnsavedChanges(intl, "groups", group.id)(location);
}}
/>
<Form noValidate onSubmit={formik.handleSubmit} id="movie-edit">
<Form noValidate onSubmit={formik.handleSubmit} id="group-edit">
{renderInputField("name")}
{renderInputField("aliases")}
{renderDurationField("duration")}
@ -402,7 +402,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
</Form>
<DetailsEditNavbar
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
objectName={group?.name ?? intl.formatMessage({ id: "group" })}
isNew={isNew}
classNames="col-xl-9 mt-3"
isEditing

View File

@ -5,17 +5,17 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneList } from "src/components/Scenes/SceneList";
import { View } from "src/components/List/views";
interface IMovieScenesPanel {
interface IGroupScenesPanel {
active: boolean;
movie: GQL.MovieDataFragment;
group: GQL.MovieDataFragment;
}
export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({
export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
active,
movie,
group,
}) => {
function filterHook(filter: ListFilterModel) {
const movieValue = { id: movie.id, label: movie.name };
const movieValue = { id: group.id, label: group.name };
// if movie is already present, then we modify it, otherwise add
let movieCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "movies";
@ -29,7 +29,7 @@ export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({
// add the movie if not present
if (
!movieCriterion.value.find((p) => {
return p.id === movie.id;
return p.id === group.id;
})
) {
movieCriterion.value.push(movieValue);
@ -46,13 +46,13 @@ export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({
return filter;
}
if (movie && movie.id) {
if (group && group.id) {
return (
<SceneList
filterHook={filterHook}
defaultSort="movie_scene_number"
alterQuery={active}
view={View.MovieScenes}
view={View.GroupScenes}
/>
);
}

View File

@ -20,33 +20,33 @@ import { uniq } from "lodash-es";
import { Tag } from "src/components/Tags/TagSelect";
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
interface IMovieScrapeDialogProps {
movie: Partial<GQL.MovieUpdateInput>;
movieStudio: Studio | null;
movieTags: Tag[];
interface IGroupScrapeDialogProps {
group: Partial<GQL.MovieUpdateInput>;
groupStudio: Studio | null;
groupTags: Tag[];
scraped: GQL.ScrapedMovie;
onClose: (scrapedMovie?: GQL.ScrapedMovie) => void;
}
export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = ({
movie,
movieStudio,
movieTags,
export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
group,
groupStudio: groupStudio,
groupTags: groupTags,
scraped,
onClose,
}) => {
const intl = useIntl();
const [name, setName] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.name, scraped.name)
new ScrapeResult<string>(group.name, scraped.name)
);
const [aliases, setAliases] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.aliases, scraped.aliases)
new ScrapeResult<string>(group.aliases, scraped.aliases)
);
const [duration, setDuration] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(
TextUtils.secondsToTimestamp(movie.duration || 0),
TextUtils.secondsToTimestamp(group.duration || 0),
// convert seconds to string if it's a number
scraped.duration && !isNaN(+scraped.duration)
? TextUtils.secondsToTimestamp(parseInt(scraped.duration, 10))
@ -54,20 +54,20 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = ({
)
);
const [date, setDate] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.date, scraped.date)
new ScrapeResult<string>(group.date, scraped.date)
);
const [director, setDirector] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.director, scraped.director)
new ScrapeResult<string>(group.director, scraped.director)
);
const [synopsis, setSynopsis] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.synopsis, scraped.synopsis)
new ScrapeResult<string>(group.synopsis, scraped.synopsis)
);
const [studio, setStudio] = useState<ObjectScrapeResult<GQL.ScrapedStudio>>(
new ObjectScrapeResult<GQL.ScrapedStudio>(
movieStudio
groupStudio
? {
stored_id: movieStudio.id,
name: movieStudio.name,
stored_id: groupStudio.id,
name: groupStudio.name,
}
: undefined,
scraped.studio?.stored_id ? scraped.studio : undefined
@ -75,17 +75,17 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = ({
);
const [urls, setURLs] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string[]>(
movie.urls,
group.urls,
scraped.urls
? uniq((movie.urls ?? []).concat(scraped.urls ?? []))
? uniq((group.urls ?? []).concat(scraped.urls ?? []))
: undefined
)
);
const [frontImage, setFrontImage] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.front_image, scraped.front_image)
new ScrapeResult<string>(group.front_image, scraped.front_image)
);
const [backImage, setBackImage] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(movie.back_image, scraped.back_image)
new ScrapeResult<string>(group.back_image, scraped.back_image)
);
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
@ -99,7 +99,7 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = ({
});
const { tags, newTags, scrapedTagsRow } = useScrapedTags(
movieTags,
groupTags,
scraped.tags
);
@ -194,13 +194,13 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = ({
{scrapedTagsRow}
<ScrapedImageRow
title="Front Image"
className="movie-image"
className="group-image"
result={frontImage}
onChange={(value) => setFrontImage(value)}
/>
<ScrapedImageRow
title="Back Image"
className="movie-image"
className="group-image"
result={backImage}
onChange={(value) => setBackImage(value)}
/>
@ -212,7 +212,7 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = ({
<ScrapeDialog
title={intl.formatMessage(
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "movie" }) }
{ entity_type: intl.formatMessage({ id: "group" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {

View File

@ -14,11 +14,11 @@ import {
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ExportDialog } from "../Shared/ExportDialog";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { MovieCardGrid } from "./MovieCardGrid";
import { EditMoviesDialog } from "./EditMoviesDialog";
import { GroupCardGrid } from "./MovieCardGrid";
import { EditGroupsDialog } from "./EditMoviesDialog";
import { View } from "../List/views";
const MovieItemList = makeItemList({
const GroupItemList = makeItemList({
filterMode: GQL.FilterMode.Movies,
useResult: useFindMovies,
getItems(result: GQL.FindMoviesQueryResult) {
@ -29,13 +29,13 @@ const MovieItemList = makeItemList({
},
});
interface IMovieList {
interface IGroupList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View;
alterQuery?: boolean;
}
export const MovieList: React.FC<IMovieList> = ({
export const GroupList: React.FC<IGroupList> = ({
filterHook,
alterQuery,
view,
@ -90,7 +90,7 @@ export const MovieList: React.FC<IMovieList> = ({
if (singleResult.data.findMovies.movies.length === 1) {
const { id } = singleResult.data.findMovies.movies[0];
// navigate to the movie page
history.push(`/movies/${id}`);
history.push(`/groups/${id}`);
}
}
}
@ -111,7 +111,7 @@ export const MovieList: React.FC<IMovieList> = ({
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
function maybeRenderMovieExportDialog() {
function maybeRenderGroupExportDialog() {
if (isExportDialogOpen) {
return (
<ExportDialog
@ -127,13 +127,13 @@ export const MovieList: React.FC<IMovieList> = ({
}
}
function renderMovies() {
function renderGroups() {
if (!result.data?.findMovies) return;
if (filter.displayMode === DisplayMode.Grid) {
return (
<MovieCardGrid
movies={result.data.findMovies.movies}
<GroupCardGrid
groups={result.data.findMovies.movies}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
@ -145,36 +145,36 @@ export const MovieList: React.FC<IMovieList> = ({
}
return (
<>
{maybeRenderMovieExportDialog()}
{renderMovies()}
{maybeRenderGroupExportDialog()}
{renderGroups()}
</>
);
}
function renderEditDialog(
selectedMovies: GQL.MovieDataFragment[],
selectedGroups: GQL.MovieDataFragment[],
onClose: (applied: boolean) => void
) {
return <EditMoviesDialog selected={selectedMovies} onClose={onClose} />;
return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />;
}
function renderDeleteDialog(
selectedMovies: GQL.SlimMovieDataFragment[],
selectedGroups: GQL.SlimMovieDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedMovies}
selected={selectedGroups}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "movie" })}
pluralEntity={intl.formatMessage({ id: "movies" })}
singularEntity={intl.formatMessage({ id: "group" })}
pluralEntity={intl.formatMessage({ id: "groups" })}
destroyMutation={useMoviesDestroy}
/>
);
}
return (
<MovieItemList
<GroupItemList
selectable
filterHook={filterHook}
view={view}

View File

@ -2,7 +2,7 @@ import React from "react";
import { Link } from "react-router-dom";
import { useFindMovies } from "src/core/StashService";
import Slider from "@ant-design/react-slick";
import { MovieCard } from "./MovieCard";
import { GroupCard } from "./MovieCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
@ -14,7 +14,7 @@ interface IProps {
header: string;
}
export const MovieRecommendationRow: React.FC<IProps> = (props: IProps) => {
export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
const result = useFindMovies(props.filter);
const cardCount = result.data?.findMovies.count;
@ -24,10 +24,10 @@ export const MovieRecommendationRow: React.FC<IProps> = (props: IProps) => {
return (
<RecommendationRow
className="movie-recommendations"
className="group-recommendations"
header={props.header}
link={
<Link to={`/movies?${props.filter.makeQueryParameters()}`}>
<Link to={`/groups?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
@ -40,10 +40,10 @@ export const MovieRecommendationRow: React.FC<IProps> = (props: IProps) => {
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="movie-skeleton skeleton-card"></div>
<div key={`_${i}`} className="group-skeleton skeleton-card"></div>
))
: result.data?.findMovies.movies.map((m) => (
<MovieCard key={m.id} movie={m} />
<GroupCard key={m.id} group={m} />
))}
</Slider>
</RecommendationRow>

View File

@ -30,13 +30,13 @@ import { sortByRelevance } from "src/utils/query";
import { PatchComponent, PatchFunction } from "src/patch";
import { TruncatedText } from "../Shared/TruncatedText";
export type Movie = Pick<
export type Group = Pick<
GQL.Movie,
"id" | "name" | "date" | "front_image_path" | "aliases"
> & {
studio?: Pick<GQL.Studio, "name"> | null;
};
type Option = SelectOption<Movie>;
type Option = SelectOption<Group>;
type FindMoviesResult = Awaited<
ReturnType<typeof queryFindMoviesForSelect>
@ -56,9 +56,9 @@ const movieSelectSort = PatchFunction(
sortMoviesByRelevance
);
const _MovieSelect: React.FC<
const _GroupSelect: React.FC<
IFilterProps &
IFilterValueProps<Movie> & {
IFilterValueProps<Group> & {
hoverPlacement?: Placement;
excludeIds?: string[];
}
@ -94,7 +94,7 @@ const _MovieSelect: React.FC<
}));
}
const MovieOption: React.FC<OptionProps<Option, boolean>> = (optionProps) => {
const GroupOption: React.FC<OptionProps<Option, boolean>> = (optionProps) => {
let thisOptionProps = optionProps;
const { object } = optionProps.data;
@ -111,24 +111,24 @@ const _MovieSelect: React.FC<
thisOptionProps = {
...optionProps,
children: (
<span className="movie-select-option">
<span className="movie-select-row">
<span className="group-select-option">
<span className="group-select-row">
{object.front_image_path && (
<img
className="movie-select-image"
className="group-select-image"
src={object.front_image_path}
loading="lazy"
/>
)}
<span className="movie-select-details">
<span className="group-select-details">
<TruncatedText
className="movie-select-title"
className="group-select-title"
text={
<span>
{title}
{alias && (
<span className="movie-select-alias">{` (${alias})`}</span>
<span className="group-select-alias">{` (${alias})`}</span>
)}
</span>
}
@ -136,13 +136,13 @@ const _MovieSelect: React.FC<
/>
{object.studio?.name && (
<span className="movie-select-studio">
<span className="group-select-studio">
{object.studio?.name}
</span>
)}
{object.date && (
<span className="movie-select-date">{object.date}</span>
<span className="group-select-date">{object.date}</span>
)}
</span>
</span>
@ -153,7 +153,7 @@ const _MovieSelect: React.FC<
return <reactSelectComponents.Option {...thisOptionProps} />;
};
const MovieMultiValueLabel: React.FC<
const GroupMultiValueLabel: React.FC<
MultiValueGenericProps<Option, boolean>
> = (optionProps) => {
let thisOptionProps = optionProps;
@ -168,7 +168,7 @@ const _MovieSelect: React.FC<
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
};
const MovieValueLabel: React.FC<SingleValueProps<Option, boolean>> = (
const GroupValueLabel: React.FC<SingleValueProps<Option, boolean>> = (
optionProps
) => {
let thisOptionProps = optionProps;
@ -190,7 +190,7 @@ const _MovieSelect: React.FC<
return {
value: result.data!.movieCreate!.id,
item: result.data!.movieCreate!,
message: "Created movie",
message: "Created group",
};
};
@ -201,7 +201,7 @@ const _MovieSelect: React.FC<
};
};
const isValidNewOption = (inputValue: string, options: Movie[]) => {
const isValidNewOption = (inputValue: string, options: Group[]) => {
if (!inputValue) {
return false;
}
@ -221,12 +221,12 @@ const _MovieSelect: React.FC<
};
return (
<FilterSelectComponent<Movie, boolean>
<FilterSelectComponent<Group, boolean>
{...props}
className={cx(
"movie-select",
"group-select",
{
"movie-select-active": props.active,
"group-select-active": props.active,
},
props.className
)}
@ -234,9 +234,9 @@ const _MovieSelect: React.FC<
getNamedObject={getNamedObject}
isValidNewOption={isValidNewOption}
components={{
Option: MovieOption,
MultiValueLabel: MovieMultiValueLabel,
SingleValue: MovieValueLabel,
Option: GroupOption,
MultiValueLabel: GroupMultiValueLabel,
SingleValue: GroupValueLabel,
}}
isMulti={props.isMulti ?? false}
creatable={props.creatable ?? defaultCreatable}
@ -247,7 +247,7 @@ const _MovieSelect: React.FC<
{ id: "actions.select_entity" },
{
entityType: intl.formatMessage({
id: props.isMulti ? "movies" : "movie",
id: props.isMulti ? "groups" : "group",
}),
}
)
@ -257,22 +257,22 @@ const _MovieSelect: React.FC<
);
};
export const MovieSelect = PatchComponent("MovieSelect", _MovieSelect);
export const GroupSelect = PatchComponent("GroupSelect", _GroupSelect);
const _MovieIDSelect: React.FC<IFilterProps & IFilterIDProps<Movie>> = (
const _GroupIDSelect: React.FC<IFilterProps & IFilterIDProps<Group>> = (
props
) => {
const { ids, onSelect: onSelectValues } = props;
const [values, setValues] = useState<Movie[]>([]);
const [values, setValues] = useState<Group[]>([]);
const idsChanged = useCompare(ids);
function onSelect(items: Movie[]) {
function onSelect(items: Group[]) {
setValues(items);
onSelectValues?.(items);
}
async function loadObjectsByID(idsToLoad: string[]): Promise<Movie[]> {
async function loadObjectsByID(idsToLoad: string[]): Promise<Group[]> {
const query = await queryFindMoviesByIDForSelect(idsToLoad);
const { movies: loadedMovies } = query.data.findMovies;
@ -303,7 +303,7 @@ const _MovieIDSelect: React.FC<IFilterProps & IFilterIDProps<Movie>> = (
load();
}, [ids, idsChanged, values]);
return <MovieSelect {...props} values={values} onSelect={onSelect} />;
return <GroupSelect {...props} values={values} onSelect={onSelect} />;
};
export const MovieIDSelect = PatchComponent("MovieIDSelect", _MovieIDSelect);
export const GroupIDSelect = PatchComponent("GroupIDSelect", _GroupIDSelect);

View File

@ -2,30 +2,30 @@ import React from "react";
import { Route, Switch } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useTitleProps } from "src/hooks/title";
import Movie from "./MovieDetails/Movie";
import MovieCreate from "./MovieDetails/MovieCreate";
import { MovieList } from "./MovieList";
import Group from "./MovieDetails/Movie";
import GroupCreate from "./MovieDetails/MovieCreate";
import { GroupList } from "./MovieList";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const Movies: React.FC = () => {
const Groups: React.FC = () => {
useScrollToTopOnMount();
return <MovieList view={View.Movies} />;
return <GroupList view={View.Groups} />;
};
const MovieRoutes: React.FC = () => {
const titleProps = useTitleProps({ id: "movies" });
const GroupRoutes: React.FC = () => {
const titleProps = useTitleProps({ id: "groups" });
return (
<>
<Helmet {...titleProps} />
<Switch>
<Route exact path="/movies" component={Movies} />
<Route exact path="/movies/new" component={MovieCreate} />
<Route path="/movies/:id/:tab?" component={Movie} />
<Route exact path="/groups" component={Groups} />
<Route exact path="/groups/new" component={GroupCreate} />
<Route path="/groups/:id/:tab?" component={Group} />
</Switch>
</>
);
};
export default MovieRoutes;
export default GroupRoutes;

View File

@ -1,4 +1,4 @@
.movie-card {
.group-card {
width: 240px;
@media (max-width: 576px) {
@ -14,7 +14,7 @@
width: 100%;
}
.movie-scene-number {
.group-scene-number {
text-align: center;
}
@ -23,14 +23,14 @@
}
}
.movie-images {
.group-images {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-evenly;
max-width: 100%;
.movie-image-container {
.group-image-container {
box-shadow: none;
}
@ -40,17 +40,17 @@
}
}
#movie-page .rating-number .text-input {
#group-page .rating-number .text-input {
width: auto;
}
.movie-select-option {
.movie-select-row {
.group-select-option {
.group-select-row {
align-items: center;
display: flex;
width: 100%;
.movie-select-image {
.group-select-image {
background-color: $body-bg;
margin-right: 0.4em;
max-height: 50px;
@ -59,26 +59,26 @@
object-position: center;
}
.movie-select-details {
.group-select-details {
display: flex;
flex-direction: column;
justify-content: flex-start;
max-height: 4.1rem;
overflow: hidden;
.movie-select-title {
.group-select-title {
flex-shrink: 0;
white-space: pre-wrap;
word-break: break-all;
.movie-select-alias {
.group-select-alias {
font-size: 0.8rem;
font-weight: bold;
}
}
.movie-select-date,
.movie-select-studio {
.group-select-date,
.group-select-studio {
color: $text-muted;
flex-shrink: 0;
font-size: 0.9rem;

View File

@ -178,15 +178,15 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
);
}
function maybeRenderMoviesPopoverButton() {
function maybeRenderGroupsPopoverButton() {
if (!performer.movie_count) return;
return (
<PopoverCountButton
className="movie-count"
type="movie"
className="group-count"
type="group"
count={performer.movie_count}
url={NavUtils.makePerformerMoviesUrl(
url={NavUtils.makePerformerGroupsUrl(
performer,
extraCriteria?.performer,
extraCriteria?.movies
@ -209,7 +209,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderScenesPopoverButton()}
{maybeRenderMoviesPopoverButton()}
{maybeRenderGroupsPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderTagPopoverButton()}

View File

@ -27,7 +27,7 @@ import {
} from "./PerformerDetailsPanel";
import { PerformerScenesPanel } from "./PerformerScenesPanel";
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
import { PerformerGroupsPanel } from "./PerformerMoviesPanel";
import { PerformerImagesPanel } from "./PerformerImagesPanel";
import { PerformerAppearsWithPanel } from "./performerAppearsWithPanel";
import { PerformerEditPanel } from "./PerformerEditPanel";
@ -60,7 +60,7 @@ const validTabs = [
"scenes",
"galleries",
"images",
"movies",
"groups",
"appearswith",
] as const;
type TabKey = (typeof validTabs)[number];
@ -146,7 +146,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
} else if (performer.image_count != 0) {
ret = "images";
} else if (performer.movie_count != 0) {
ret = "movies";
ret = "groups";
}
}
@ -191,7 +191,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
Mousetrap.bind("e", () => toggleEditing());
Mousetrap.bind("c", () => setTabKey("scenes"));
Mousetrap.bind("g", () => setTabKey("galleries"));
Mousetrap.bind("m", () => setTabKey("movies"));
Mousetrap.bind("m", () => setTabKey("groups"));
Mousetrap.bind("f", () => setFavorite(!performer.favorite));
Mousetrap.bind(",", () => setCollapsed(!collapsed));
@ -319,10 +319,10 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
/>
</Tab>
<Tab
eventKey="movies"
eventKey="groups"
title={
<>
{intl.formatMessage({ id: "movies" })}
{intl.formatMessage({ id: "groups" })}
<Counter
abbreviateCounter={abbreviateCounter}
count={performer.movie_count}
@ -331,8 +331,8 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
</>
}
>
<PerformerMoviesPanel
active={tabKey === "movies"}
<PerformerGroupsPanel
active={tabKey === "groups"}
performer={performer}
/>
</Tab>

View File

@ -1,6 +1,6 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieList } from "src/components/Movies/MovieList";
import { GroupList } from "src/components/Movies/MovieList";
import { usePerformerFilterHook } from "src/core/performers";
import { View } from "src/components/List/views";
@ -9,16 +9,16 @@ interface IPerformerDetailsProps {
performer: GQL.PerformerDataFragment;
}
export const PerformerMoviesPanel: React.FC<IPerformerDetailsProps> = ({
export const PerformerGroupsPanel: React.FC<IPerformerDetailsProps> = ({
active,
performer,
}) => {
const filterHook = usePerformerFilterHook(performer);
return (
<MovieList
<GroupList
filterHook={filterHook}
alterQuery={active}
view={View.PerformerMovies}
view={View.PerformerGroups}
/>
);
};

View File

@ -21,7 +21,7 @@ import { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon";
import {
GalleryLink,
MovieLink,
GroupLink,
SceneMarkerLink,
TagLink,
} from "../Shared/TagLink";
@ -386,14 +386,14 @@ export const SceneDuplicateChecker: React.FC = () => {
return <PerformerPopoverButton performers={scene.performers} />;
}
function maybeRenderMoviePopoverButton(scene: GQL.SlimSceneDataFragment) {
function maybeRenderGroupPopoverButton(scene: GQL.SlimSceneDataFragment) {
if (scene.movies.length <= 0) return;
const popoverContent = scene.movies.map((sceneMovie) => (
<div className="movie-tag-container row" key="movie">
<div className="group-tag-container row" key={sceneMovie.movie.id}>
<Link
to={`/movies/${sceneMovie.movie.id}`}
className="movie-tag col m-auto zoom-2"
to={`/groups/${sceneMovie.movie.id}`}
className="group-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
@ -401,9 +401,9 @@ export const SceneDuplicateChecker: React.FC = () => {
src={sceneMovie.movie.front_image_path ?? ""}
/>
</Link>
<MovieLink
<GroupLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
group={sceneMovie.movie}
className="d-block"
/>
</div>
@ -523,7 +523,7 @@ export const SceneDuplicateChecker: React.FC = () => {
<ButtonGroup className="flex-wrap">
{maybeRenderTagPopoverButton(scene)}
{maybeRenderPerformerPopoverButton(scene)}
{maybeRenderMoviePopoverButton(scene)}
{maybeRenderGroupPopoverButton(scene)}
{maybeRenderSceneMarkerPopoverButton(scene)}
{maybeRenderOCounter(scene)}
{maybeRenderGallery(scene)}

View File

@ -13,7 +13,7 @@ import { RatingSystem } from "../Shared/Rating/RatingSystem";
import {
getAggregateInputIDs,
getAggregateInputValue,
getAggregateMovieIds,
getAggregateGroupIds,
getAggregatePerformerIds,
getAggregateRating,
getAggregateStudioId,
@ -42,11 +42,11 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
);
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [movieMode, setMovieMode] = React.useState<GQL.BulkUpdateIdMode>(
const [groupMode, setGroupMode] = React.useState<GQL.BulkUpdateIdMode>(
GQL.BulkUpdateIdMode.Add
);
const [movieIds, setMovieIds] = useState<string[]>();
const [existingMovieIds, setExistingMovieIds] = useState<string[]>();
const [groupIds, setGroupIds] = useState<string[]>();
const [existingGroupIds, setExistingGroupIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
@ -62,7 +62,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
const aggregateStudioId = getAggregateStudioId(props.selected);
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
const aggregateTagIds = getAggregateTagIds(props.selected);
const aggregateMovieIds = getAggregateMovieIds(props.selected);
const aggregateGroupIds = getAggregateGroupIds(props.selected);
const sceneInput: GQL.BulkSceneUpdateInput = {
ids: props.selected.map((scene) => {
@ -80,9 +80,9 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
);
sceneInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
sceneInput.movie_ids = getAggregateInputIDs(
movieMode,
movieIds,
aggregateMovieIds
groupMode,
groupIds,
aggregateGroupIds
);
if (organized !== undefined) {
@ -115,7 +115,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateMovieIds: string[] = [];
let updateGroupIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
@ -126,14 +126,14 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
.map((p) => p.id)
.sort();
const sceneTagIDs = (scene.tags ?? []).map((p) => p.id).sort();
const sceneMovieIDs = (scene.movies ?? []).map((m) => m.movie.id).sort();
const sceneGroupIDs = (scene.movies ?? []).map((m) => m.movie.id).sort();
if (first) {
updateRating = sceneRating ?? undefined;
updateStudioID = sceneStudioID;
updatePerformerIds = scenePerformerIDs;
updateTagIds = sceneTagIDs;
updateMovieIds = sceneMovieIDs;
updateGroupIds = sceneGroupIDs;
first = false;
updateOrganized = scene.organized;
} else {
@ -149,8 +149,8 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
if (!isEqual(sceneTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (!isEqual(sceneMovieIDs, updateMovieIds)) {
updateMovieIds = [];
if (!isEqual(sceneGroupIDs, updateGroupIds)) {
updateGroupIds = [];
}
if (scene.organized !== updateOrganized) {
updateOrganized = undefined;
@ -162,7 +162,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
setStudioId(updateStudioID);
setExistingPerformerIds(updatePerformerIds);
setExistingTagIds(updateTagIds);
setExistingMovieIds(updateMovieIds);
setExistingGroupIds(updateGroupIds);
setOrganized(updateOrganized);
}, [props.selected]);
@ -173,7 +173,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags" | "movies",
type: "performers" | "tags" | "groups",
ids: string[] | undefined
) {
let mode = GQL.BulkUpdateIdMode.Add;
@ -187,9 +187,9 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
mode = tagMode;
existingIds = existingTagIds;
break;
case "movies":
mode = movieMode;
existingIds = existingMovieIds;
case "groups":
mode = groupMode;
existingIds = existingGroupIds;
break;
}
@ -205,8 +205,8 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
case "tags":
setTagIds(itemIDs);
break;
case "movies":
setMovieIds(itemIDs);
case "groups":
setGroupIds(itemIDs);
break;
}
}}
@ -218,8 +218,8 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
case "tags":
setTagMode(newMode);
break;
case "movies":
setMovieMode(newMode);
case "groups":
setGroupMode(newMode);
break;
}
}}
@ -306,11 +306,11 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="movies">
<Form.Group controlId="groups">
<Form.Label>
<FormattedMessage id="movies" />
<FormattedMessage id="groups" />
</Form.Label>
{renderMultiSelect("movies", movieIds)}
{renderMultiSelect("groups", groupIds)}
</Form.Group>
<Form.Group controlId="organized">

View File

@ -7,7 +7,7 @@ import { Icon } from "../Shared/Icon";
import {
GalleryLink,
TagLink,
MovieLink,
GroupLink,
SceneMarkerLink,
} from "../Shared/TagLink";
import { HoverPopover } from "../Shared/HoverPopover";
@ -143,24 +143,24 @@ const SceneCardPopovers = PatchComponent(
return <PerformerPopoverButton performers={props.scene.performers} />;
}
function maybeRenderMoviePopoverButton() {
function maybeRenderGroupPopoverButton() {
if (props.scene.movies.length <= 0) return;
const popoverContent = props.scene.movies.map((sceneMovie) => (
<div className="movie-tag-container row" key="movie">
const popoverContent = props.scene.movies.map((sceneGroup) => (
<div className="group-tag-container row" key={sceneGroup.movie.id}>
<Link
to={`/movies/${sceneMovie.movie.id}`}
className="movie-tag col m-auto zoom-2"
to={`/groups/${sceneGroup.movie.id}`}
className="group-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
alt={sceneMovie.movie.name ?? ""}
src={sceneMovie.movie.front_image_path ?? ""}
alt={sceneGroup.movie.name ?? ""}
src={sceneGroup.movie.front_image_path ?? ""}
/>
</Link>
<MovieLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
<GroupLink
key={sceneGroup.movie.id}
group={sceneGroup.movie}
className="d-block"
/>
</div>
@ -170,7 +170,7 @@ const SceneCardPopovers = PatchComponent(
<HoverPopover
placement="bottom"
content={popoverContent}
className="movie-count tag-tooltip"
className="group-count tag-tooltip"
>
<Button className="minimal">
<Icon icon={faFilm} />
@ -291,7 +291,7 @@ const SceneCardPopovers = PatchComponent(
<ButtonGroup className="card-popovers">
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderMoviePopoverButton()}
{maybeRenderGroupPopoverButton()}
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}

View File

@ -70,7 +70,7 @@ const SceneMarkersPanel = lazyComponent(() => import("./SceneMarkersPanel"));
const SceneFileInfoPanel = lazyComponent(() => import("./SceneFileInfoPanel"));
const SceneDetailPanel = lazyComponent(() => import("./SceneDetailPanel"));
const SceneHistoryPanel = lazyComponent(() => import("./SceneHistoryPanel"));
const SceneMoviePanel = lazyComponent(() => import("./SceneMoviePanel"));
const SceneGroupPanel = lazyComponent(() => import("./SceneMoviePanel"));
const SceneGalleriesPanel = lazyComponent(
() => import("./SceneGalleriesPanel")
);
@ -443,9 +443,9 @@ const ScenePage: React.FC<IProps> = ({
</Nav.Item>
{scene.movies.length > 0 ? (
<Nav.Item>
<Nav.Link eventKey="scene-movie-panel">
<Nav.Link eventKey="scene-group-panel">
<FormattedMessage
id="countables.movies"
id="countables.groups"
values={{ count: scene.movies.length }}
/>
</Nav.Link>
@ -514,8 +514,8 @@ const ScenePage: React.FC<IProps> = ({
isVisible={activeTabKey === "scene-markers-panel"}
/>
</Tab.Pane>
<Tab.Pane eventKey="scene-movie-panel">
<SceneMoviePanel scene={scene} />
<Tab.Pane eventKey="scene-group-panel">
<SceneGroupPanel scene={scene} />
</Tab.Pane>
{scene.galleries.length >= 1 && (
<Tab.Pane eventKey="scene-galleries-panel">

View File

@ -29,7 +29,7 @@ import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import { ConfigurationContext } from "src/hooks/Config";
import { stashboxDisplayName } from "src/utils/stashbox";
import { IMovieEntry, SceneMovieTable } from "./SceneMovieTable";
import { IGroupEntry, SceneGroupTable } from "./SceneMovieTable";
import { faSearch, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
@ -47,7 +47,7 @@ import {
import { formikUtils } from "src/utils/form";
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
import { Movie } from "src/components/Movies/MovieSelect";
import { Group } from "src/components/Movies/MovieSelect";
import { useTagsEdit } from "src/hooks/tagsEdit";
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
@ -75,7 +75,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
const [galleries, setGalleries] = useState<Gallery[]>([]);
const [performers, setPerformers] = useState<Performer[]>([]);
const [movies, setMovies] = useState<Movie[]>([]);
const [groups, setGroups] = useState<Group[]>([]);
const [studio, setStudio] = useState<Studio | null>(null);
const Scrapers = useListSceneScrapers();
@ -104,7 +104,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
}, [scene.performers]);
useEffect(() => {
setMovies(scene.movies?.map((m) => m.movie) ?? []);
setGroups(scene.movies?.map((m) => m.movie) ?? []);
}, [scene.movies]);
useEffect(() => {
@ -191,12 +191,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
return formik.values.movies
.map((m) => {
return {
movie: movies.find((mm) => mm.id === m.movie_id),
movie: groups.find((mm) => mm.id === m.movie_id),
scene_index: m.scene_index,
};
})
.filter((m) => m.movie !== undefined) as IMovieEntry[];
}, [formik.values.movies, movies]);
.filter((m) => m.movie !== undefined) as IGroupEntry[];
}, [formik.values.movies, groups]);
function onSetGalleries(items: Gallery[]) {
setGalleries(items);
@ -253,8 +253,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
setQueryableScrapers(newQueryableScrapers);
}, [Scrapers, stashConfig]);
function onSetMovies(items: Movie[]) {
setMovies(items);
function onSetGroups(items: Group[]) {
setGroups(items);
const existingMovies = formik.values.movies;
@ -386,7 +386,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
sceneStudio={studio}
sceneTags={tags}
scenePerformers={performers}
sceneMovies={movies}
sceneGroups={groups}
scraped={scrapedScene}
endpoint={endpoint}
onClose={(s) => onScrapeDialogClosed(s)}
@ -574,7 +574,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
});
if (idMovis.length > 0) {
onSetMovies(
onSetGroups(
idMovis.map((p) => {
return {
id: p.stored_id!,
@ -725,8 +725,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
return renderField("performer_ids", title, control, fullWidthProps);
}
function onSetMovieEntries(input: IMovieEntry[]) {
setMovies(input.map((m) => m.movie));
function onSetMovieEntries(input: IGroupEntry[]) {
setGroups(input.map((m) => m.movie));
const newMovies = input.map((m) => ({
movie_id: m.movie.id,
@ -737,9 +737,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
}
function renderMoviesField() {
const title = intl.formatMessage({ id: "movies" });
const title = intl.formatMessage({ id: "groups" });
const control = (
<SceneMovieTable value={movieEntries} onUpdate={onSetMovieEntries} />
<SceneGroupTable value={movieEntries} onUpdate={onSetMovieEntries} />
);
return renderField("movies", title, control, fullWidthProps);

View File

@ -1,19 +1,19 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieCard } from "src/components/Movies/MovieCard";
import { GroupCard } from "src/components/Movies/MovieCard";
interface ISceneMoviePanelProps {
interface ISceneGroupPanelProps {
scene: GQL.SceneDataFragment;
}
export const SceneMoviePanel: React.FC<ISceneMoviePanelProps> = (
props: ISceneMoviePanelProps
export const SceneGroupPanel: React.FC<ISceneGroupPanelProps> = (
props: ISceneGroupPanelProps
) => {
const cards = props.scene.movies.map((sceneMovie) => (
<MovieCard
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
sceneIndex={sceneMovie.scene_index ?? undefined}
const cards = props.scene.movies.map((sceneGroup) => (
<GroupCard
key={sceneGroup.movie.id}
group={sceneGroup.movie}
sceneIndex={sceneGroup.scene_index ?? undefined}
/>
));
@ -24,4 +24,4 @@ export const SceneMoviePanel: React.FC<ISceneMoviePanelProps> = (
);
};
export default SceneMoviePanel;
export default SceneGroupPanel;

View File

@ -2,27 +2,27 @@ import React, { useMemo } from "react";
import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { Form, Row, Col } from "react-bootstrap";
import { Movie, MovieSelect } from "src/components/Movies/MovieSelect";
import { Group, GroupSelect } from "src/components/Movies/MovieSelect";
import cx from "classnames";
export type MovieSceneIndexMap = Map<string, number | undefined>;
export interface IMovieEntry {
movie: Movie;
export interface IGroupEntry {
movie: Group;
scene_index?: GQL.InputMaybe<number> | undefined;
}
export interface IProps {
value: IMovieEntry[];
onUpdate: (input: IMovieEntry[]) => void;
value: IGroupEntry[];
onUpdate: (input: IGroupEntry[]) => void;
}
export const SceneMovieTable: React.FC<IProps> = (props) => {
export const SceneGroupTable: React.FC<IProps> = (props) => {
const { value, onUpdate } = props;
const intl = useIntl();
const movieIDs = useMemo(() => value.map((m) => m.movie.id), [value]);
const groupIDs = useMemo(() => value.map((m) => m.movie.id), [value]);
const updateFieldChanged = (index: number, sceneIndex: number | null) => {
const newValues = value.map((existing, i) => {
@ -38,21 +38,21 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
onUpdate(newValues);
};
function onMovieSet(index: number, movies: Movie[]) {
if (!movies.length) {
function onGroupSet(index: number, groups: Group[]) {
if (!groups.length) {
// remove this entry
const newValues = value.filter((_, i) => i !== index);
onUpdate(newValues);
return;
}
const movie = movies[0];
const group = groups[0];
const newValues = value.map((existing, i) => {
if (i === index) {
return {
...existing,
movie: movie,
movie: group,
};
}
return existing;
@ -61,17 +61,17 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
onUpdate(newValues);
}
function onNewMovieSet(movies: Movie[]) {
if (!movies.length) {
function onNewGroupSet(groups: Group[]) {
if (!groups.length) {
return;
}
const movie = movies[0];
const group = groups[0];
const newValues = [
...value,
{
movie: movie,
movie: group,
scene_index: null,
},
];
@ -83,12 +83,12 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
return (
<>
{value.map((m, i) => (
<Row key={m.movie.id} className="movie-row">
<Row key={m.movie.id} className="group-row">
<Col xs={9}>
<MovieSelect
onSelect={(items) => onMovieSet(i, items)}
<GroupSelect
onSelect={(items) => onGroupSet(i, items)}
values={[m.movie!]}
excludeIds={movieIDs}
excludeIds={groupIDs}
/>
</Col>
<Col xs={3}>
@ -108,12 +108,12 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
</Col>
</Row>
))}
<Row className="movie-row">
<Row className="group-row">
<Col xs={12}>
<MovieSelect
onSelect={(items) => onNewMovieSet(items)}
<GroupSelect
onSelect={(items) => onNewGroupSet(items)}
values={[]}
excludeIds={movieIDs}
excludeIds={groupIDs}
/>
</Col>
</Row>
@ -122,11 +122,11 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
}
return (
<div className={cx("movie-table", { "no-movies": !value.length })}>
<Row className="movie-table-header">
<div className={cx("group-table", { "no-groups": !value.length })}>
<Row className="group-table-header">
<Col xs={9}></Col>
<Form.Label column xs={3} className="movie-scene-number-header">
{intl.formatMessage({ id: "movie_scene_number" })}
<Form.Label column xs={3} className="group-scene-number-header">
{intl.formatMessage({ id: "group_scene_number" })}
</Form.Label>
</Row>
{renderTableData()}

View File

@ -17,18 +17,18 @@ import {
ScrapeResult,
} from "src/components/Shared/ScrapeDialog/scrapeResult";
import {
ScrapedMoviesRow,
ScrapedGroupsRow,
ScrapedPerformersRow,
ScrapedStudioRow,
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
import {
useCreateScrapedMovie,
useCreateScrapedGroup,
useCreateScrapedPerformer,
useCreateScrapedStudio,
} from "src/components/Shared/ScrapeDialog/createObjects";
import { Tag } from "src/components/Tags/TagSelect";
import { Studio } from "src/components/Studios/StudioSelect";
import { Movie } from "src/components/Movies/MovieSelect";
import { Group } from "src/components/Movies/MovieSelect";
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
interface ISceneScrapeDialogProps {
@ -36,7 +36,7 @@ interface ISceneScrapeDialogProps {
sceneStudio: Studio | null;
scenePerformers: Performer[];
sceneTags: Tag[];
sceneMovies: Movie[];
sceneGroups: Group[];
scraped: GQL.ScrapedScene;
endpoint?: string;
@ -48,7 +48,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
sceneStudio,
scenePerformers,
sceneTags,
sceneMovies,
sceneGroups,
scraped,
onClose,
endpoint,
@ -114,12 +114,12 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
scraped.performers?.filter((t) => !t.stored_id) ?? []
);
const [movies, setMovies] = useState<
const [groups, setGroups] = useState<
ObjectListScrapeResult<GQL.ScrapedMovie>
>(
new ObjectListScrapeResult<GQL.ScrapedMovie>(
sortStoredIdObjects(
sceneMovies.map((p) => ({
sceneGroups.map((p) => ({
stored_id: p.id,
name: p.name,
}))
@ -127,7 +127,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
sortStoredIdObjects(scraped.movies ?? undefined)
)
);
const [newMovies, setNewMovies] = useState<GQL.ScrapedMovie[]>(
const [newGroups, setNewGroups] = useState<GQL.ScrapedMovie[]>(
scraped.movies?.filter((t) => !t.stored_id) ?? []
);
@ -157,11 +157,11 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
setNewObjects: setNewPerformers,
});
const createNewMovie = useCreateScrapedMovie({
scrapeResult: movies,
setScrapeResult: setMovies,
newObjects: newMovies,
setNewObjects: setNewMovies,
const createNewGroup = useCreateScrapedGroup({
scrapeResult: groups,
setScrapeResult: setGroups,
newObjects: newGroups,
setNewObjects: setNewGroups,
});
const intl = useIntl();
@ -176,7 +176,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
director,
studio,
performers,
movies,
groups,
tags,
details,
image,
@ -184,7 +184,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
].every((r) => !r.scraped) &&
newTags.length === 0 &&
newPerformers.length === 0 &&
newMovies.length === 0 &&
newGroups.length === 0 &&
!newStudio
) {
onClose();
@ -202,7 +202,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
director: director.getNewValue(),
studio: newStudioValue,
performers: performers.getNewValue(),
movies: movies.getNewValue(),
movies: groups.getNewValue(),
tags: tags.getNewValue(),
details: details.getNewValue(),
image: image.getNewValue(),
@ -253,12 +253,12 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
newObjects={newPerformers}
onCreateNew={createNewPerformer}
/>
<ScrapedMoviesRow
title={intl.formatMessage({ id: "movies" })}
result={movies}
onChange={(value) => setMovies(value)}
newObjects={newMovies}
onCreateNew={createNewMovie}
<ScrapedGroupsRow
title={intl.formatMessage({ id: "groups" })}
result={groups}
onChange={(value) => setGroups(value)}
newObjects={newGroups}
onCreateNew={createNewGroup}
/>
{scrapedTagsRow}
<ScrapedTextAreaRow

View File

@ -124,12 +124,12 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
}
};
const MovieCell = (scene: GQL.SlimSceneDataFragment) => (
const GroupCell = (scene: GQL.SlimSceneDataFragment) => (
<ul className="comma-list overflowable">
{scene.movies.map((sceneMovie) => (
<li key={sceneMovie.movie.id}>
<Link to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}>
<span className="ellips-data">{sceneMovie.movie.name}</span>
{scene.movies.map((sceneGroup) => (
<li key={sceneGroup.movie.id}>
<Link to={NavUtils.makeGroupScenesUrl(sceneGroup.movie)}>
<span className="ellips-data">{sceneGroup.movie.name}</span>
</Link>
</li>
))}
@ -322,10 +322,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
render: TagCell,
},
{
value: "movies",
label: intl.formatMessage({ id: "movies" }),
value: "groups",
label: intl.formatMessage({ id: "groups" }),
defaultShow: true,
render: MovieCell,
render: GroupCell,
},
{
value: "galleries",

View File

@ -30,7 +30,7 @@ import {
hasScrapedValues,
} from "../Shared/ScrapeDialog/scrapeResult";
import {
ScrapedMoviesRow,
ScrapedGroupsRow,
ScrapedPerformersRow,
ScrapedStudioRow,
ScrapedTagsRow,
@ -100,7 +100,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
};
}
function movieToStoredID(o: { movie: { id: string; name: string } }) {
function groupToStoredID(o: { movie: { id: string; name: string } }) {
return {
stored_id: o.movie.id,
name: o.movie.name,
@ -141,11 +141,11 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
)
);
const [movies, setMovies] = useState<
const [groups, setGroups] = useState<
ObjectListScrapeResult<GQL.ScrapedMovie>
>(
new ObjectListScrapeResult<GQL.ScrapedMovie>(
sortStoredIdObjects(dest.movies.map(movieToStoredID))
sortStoredIdObjects(dest.movies.map(groupToStoredID))
)
);
@ -252,10 +252,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
)
);
setMovies(
setGroups(
new ObjectListScrapeResult<GQL.ScrapedMovie>(
sortStoredIdObjects(dest.movies.map(movieToStoredID)),
uniqIDStoredIDs(all.map((s) => s.movies.map(movieToStoredID)).flat())
sortStoredIdObjects(dest.movies.map(groupToStoredID)),
uniqIDStoredIDs(all.map((s) => s.movies.map(groupToStoredID)).flat())
)
);
@ -331,7 +331,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
galleries,
studio,
performers,
movies,
groups,
tags,
details,
organized,
@ -348,7 +348,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
galleries,
studio,
performers,
movies,
groups,
tags,
details,
organized,
@ -508,10 +508,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
result={performers}
onChange={(value) => setPerformers(value)}
/>
<ScrapedMoviesRow
title={intl.formatMessage({ id: "movies" })}
result={movies}
onChange={(value) => setMovies(value)}
<ScrapedGroupsRow
title={intl.formatMessage({ id: "groups" })}
result={groups}
onChange={(value) => setGroups(value)}
/>
<ScrapedTagsRow
title={intl.formatMessage({ id: "tags" })}
@ -585,7 +585,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
gallery_ids: galleries.getNewValue(),
studio_id: studio.getNewValue()?.stored_id,
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
movies: movies.getNewValue()?.map((m) => {
movies: groups.getNewValue()?.map((m) => {
// find the equivalent movie in the original scenes
const found = all
.map((s) => s.movies)

View File

@ -24,7 +24,7 @@
}
.performer-tag-container,
.movie-tag-container {
.group-tag-container {
display: inline-block;
margin: 5px;
}
@ -34,7 +34,7 @@
}
.performer-tag.image,
.movie-tag.image {
.group-tag.image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
@ -288,19 +288,19 @@ textarea.scene-description {
max-width: 100%;
}
.movie-image {
.group-image {
max-width: 100%;
}
.movie-table {
.group-table {
width: 100%;
.movie-row {
.group-row {
align-items: center;
margin-bottom: 0.25rem;
}
.movie-scene-number-header {
.group-scene-number-header {
color: $text-muted;
font-size: 0.8em;
padding-bottom: 0;
@ -308,7 +308,7 @@ textarea.scene-description {
}
}
.movie-table.no-movies .movie-table-header {
.group-table.no-groups .group-table-header {
display: none;
}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback, useMemo } from "react";
import { Button, Form } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { DurationInput } from "src/components/Shared/DurationInput";
@ -47,7 +47,7 @@ import { defaultMaxOptionsShown } from "src/core/config";
const allMenuItems = [
{ id: "scenes", headingID: "scenes" },
{ id: "images", headingID: "images" },
{ id: "movies", headingID: "movies" },
{ id: "groups", headingID: "groups" },
{ id: "markers", headingID: "markers" },
{ id: "galleries", headingID: "galleries" },
{ id: "performers", headingID: "performers" },
@ -67,6 +67,22 @@ export const SettingsInterfacePanel: React.FC = () => {
error,
} = useSettings();
// convert old movies menu item to groups
const massageMenuItems = useCallback((menuItems: string[]) => {
return menuItems.map((item) => {
if (item === "movies") {
return "groups";
}
return item;
});
}, []);
const massagedMenuItems = useMemo(() => {
if (!iface.menuItems) return iface.menuItems;
return massageMenuItems(iface.menuItems);
}, [iface.menuItems, massageMenuItems]);
const {
interactive,
state: interactiveState,
@ -231,8 +247,8 @@ export const SettingsInterfacePanel: React.FC = () => {
<CheckboxGroup
groupId="menu-items"
items={allMenuItems}
checkedIds={iface.menuItems ?? undefined}
onChange={(v) => saveInterface({ menuItems: v })}
checkedIds={massagedMenuItems ?? undefined}
onChange={(v) => saveInterface({ menuItems: massageMenuItems(v) })}
/>
</div>
@ -563,7 +579,7 @@ export const SettingsInterfacePanel: React.FC = () => {
</div>
<BooleanSetting
id="enableMovieBackgroundImage"
headingID="movie"
headingID="group"
checked={ui.enableMovieBackgroundImage ?? undefined}
onChange={(v) => saveUI({ enableMovieBackgroundImage: v })}
/>
@ -659,8 +675,8 @@ export const SettingsInterfacePanel: React.FC = () => {
}
/>
<BooleanSetting
id="disableDropdownCreate_movie"
headingID="movie"
id="disableDropdownCreate_group"
headingID="group"
checked={iface.disableDropdownCreate?.movie ?? undefined}
onChange={(v) =>
saveInterface({

View File

@ -79,7 +79,7 @@ export const SettingsScrapingPanel: React.FC = () => {
useListSceneScrapers();
const { data: galleryScrapers, loading: loadingGalleries } =
useListGalleryScrapers();
const { data: movieScrapers, loading: loadingMovies } =
const { data: groupScrapers, loading: loadingGroups } =
useListMovieScrapers();
const { general, scraping, loading, error, saveGeneral, saveScraping } =
@ -158,13 +158,13 @@ export const SettingsScrapingPanel: React.FC = () => {
);
}
function renderMovieScrapeTypes(types: ScrapeType[]) {
function renderGroupScrapeTypes(types: ScrapeType[]) {
const typeStrings = types.map((t) => {
switch (t) {
case ScrapeType.Fragment:
return intl.formatMessage(
{ id: "config.scraping.entity_metadata" },
{ entityType: intl.formatMessage({ id: "movie" }) }
{ entityType: intl.formatMessage({ id: "group" }) }
);
default:
return t;
@ -246,12 +246,12 @@ export const SettingsScrapingPanel: React.FC = () => {
);
}
function renderMovieScrapers() {
const elements = (movieScrapers?.listScrapers ?? []).map((scraper) => (
function renderGroupScrapers() {
const elements = (groupScrapers?.listScrapers ?? []).map((scraper) => (
<tr key={scraper.id}>
<td>{scraper.name}</td>
<td>
{renderMovieScrapeTypes(scraper.movie?.supported_scrapes ?? [])}
{renderGroupScrapeTypes(scraper.movie?.supported_scrapes ?? [])}
</td>
<td>{renderURLs(scraper.movie?.urls ?? [])}</td>
</tr>
@ -260,7 +260,7 @@ export const SettingsScrapingPanel: React.FC = () => {
return renderTable(
intl.formatMessage(
{ id: "config.scraping.entity_scrapers" },
{ entityType: intl.formatMessage({ id: "movie" }) }
{ entityType: intl.formatMessage({ id: "group" }) }
),
elements
);
@ -297,7 +297,7 @@ export const SettingsScrapingPanel: React.FC = () => {
loadingScenes ||
loadingGalleries ||
loadingPerformers ||
loadingMovies
loadingGroups
)
return <LoadingIndicator />;
@ -361,7 +361,7 @@ export const SettingsScrapingPanel: React.FC = () => {
{renderSceneScrapers()}
{renderGalleryScrapers()}
{renderPerformerScrapers()}
{renderMovieScrapers()}
{renderGroupScrapers()}
</div>
</SettingSection>
</>

View File

@ -6,14 +6,14 @@ import NavUtils from "src/utils/navigation";
export const DirectorLink: React.FC<{
director: string;
linkType: "scene" | "movie";
linkType: "scene" | "group";
}> = ({ director: director, linkType = "scene" }) => {
const link = useMemo(() => {
switch (linkType) {
case "scene":
return NavUtils.makeDirectorScenesUrl(director);
case "movie":
return NavUtils.makeDirectorMoviesUrl(director);
case "group":
return NavUtils.makeDirectorGroupsUrl(director);
}
}, [director, linkType]);

View File

@ -10,7 +10,7 @@ import {
} from "../Galleries/GallerySelect";
interface IMultiSetProps {
type: "performers" | "studios" | "tags" | "movies" | "galleries";
type: "performers" | "studios" | "tags" | "groups" | "galleries";
existingIds?: string[];
ids?: string[];
mode: GQL.BulkUpdateIdMode;

View File

@ -20,7 +20,7 @@ type PopoverLinkType =
| "image"
| "gallery"
| "marker"
| "movie"
| "group"
| "performer"
| "studio";
@ -52,7 +52,7 @@ export const PopoverCountButton: React.FC<IProps> = ({
return faImages;
case "marker":
return faMapMarkerAlt;
case "movie":
case "group":
return faFilm;
case "performer":
return faUser;
@ -83,10 +83,10 @@ export const PopoverCountButton: React.FC<IProps> = ({
one: "marker",
other: "markers",
};
case "movie":
case "group":
return {
one: "movie",
other: "movies",
one: "group",
other: "groups",
};
case "performer":
return {

View File

@ -8,7 +8,7 @@ import {
} from "src/components/Shared/ScrapeDialog/scrapeResult";
import { TagSelect } from "src/components/Tags/TagSelect";
import { StudioSelect } from "src/components/Studios/StudioSelect";
import { MovieSelect } from "src/components/Movies/MovieSelect";
import { GroupSelect } from "src/components/Movies/MovieSelect";
interface IScrapedStudioRow {
title: string;
@ -196,10 +196,10 @@ export const ScrapedPerformersRow: React.FC<
);
};
export const ScrapedMoviesRow: React.FC<
export const ScrapedGroupsRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedMovie>
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
const moviesCopy = useMemo(() => {
const groupsCopy = useMemo(() => {
return (
newObjects?.map((p) => {
const name: string = p.name ?? "";
@ -208,7 +208,7 @@ export const ScrapedMoviesRow: React.FC<
);
}, [newObjects]);
function renderScrapedMovies(
function renderScrapedGroups(
scrapeResult: ScrapeResult<GQL.ScrapedMovie[]>,
isNew?: boolean,
onChangeFn?: (value: GQL.ScrapedMovie[]) => void
@ -228,7 +228,7 @@ export const ScrapedMoviesRow: React.FC<
});
return (
<MovieSelect
<GroupSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
@ -247,9 +247,9 @@ export const ScrapedMoviesRow: React.FC<
<ScrapedObjectsRow<GQL.ScrapedMovie>
title={title}
result={result}
renderObjects={renderScrapedMovies}
renderObjects={renderScrapedGroups}
onChange={onChange}
newObjects={moviesCopy}
newObjects={groupsCopy}
onCreateNew={onCreateNew}
getName={(value) => value.name ?? ""}
/>

View File

@ -9,7 +9,7 @@ import {
import { ObjectScrapeResult, ScrapeResult } from "./scrapeResult";
import { useIntl } from "react-intl";
import { scrapedPerformerToCreateInput } from "src/core/performers";
import { scrapedMovieToCreateInput } from "src/core/movies";
import { scrapedGroupToCreateInput } from "src/core/movies";
function useCreateObject<T>(
entityTypeID: string,
@ -123,16 +123,16 @@ export function useCreateScrapedPerformer(
return useCreateObject("performer", createNewPerformer);
}
export function useCreateScrapedMovie(
export function useCreateScrapedGroup(
props: IUseCreateNewObjectProps<GQL.ScrapedMovie>
) {
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
const [createMovie] = useMovieCreate();
const [createGroup] = useMovieCreate();
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
const input = scrapedMovieToCreateInput(toCreate);
async function createNewGroup(toCreate: GQL.ScrapedMovie) {
const input = scrapedGroupToCreateInput(toCreate);
const result = await createMovie({
const result = await createGroup({
variables: { input: input },
});
@ -150,14 +150,14 @@ export function useCreateScrapedMovie(
// remove the object from the list
const newObjectsClone = newObjects.concat();
const pIndex = newObjectsClone.findIndex((p) => p.name === toCreate.name);
if (pIndex === -1) throw new Error("Could not find movie to remove");
if (pIndex === -1) throw new Error("Could not find group to remove");
newObjectsClone.splice(pIndex, 1);
setNewObjects(newObjectsClone);
}
return useCreateObject("movie", createNewMovie);
return useCreateObject("group", createNewGroup);
}
export function useCreateScrapedTag(

View File

@ -26,7 +26,7 @@ import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
import { TagIDSelect } from "../Tags/TagSelect";
import { StudioIDSelect } from "../Studios/StudioSelect";
import { GalleryIDSelect } from "../Galleries/GallerySelect";
import { MovieIDSelect } from "../Movies/MovieSelect";
import { GroupIDSelect } from "../Movies/MovieSelect";
import { SceneIDSelect } from "../Scenes/SceneSelect";
export type SelectObject = {
@ -44,7 +44,7 @@ interface ITypeProps {
| "scene_tags"
| "performer_tags"
| "scenes"
| "movies"
| "groups"
| "galleries";
}
interface IFilterProps {
@ -364,8 +364,8 @@ export const StudioSelect: React.FC<
return <StudioIDSelect {...props} />;
};
export const MovieSelect: React.FC<IFilterProps> = (props) => {
return <MovieIDSelect {...props} />;
export const GroupSelect: React.FC<IFilterProps> = (props) => {
return <GroupIDSelect {...props} />;
};
export const TagSelect: React.FC<
@ -382,8 +382,8 @@ export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
return <StudioSelect {...props} creatable={false} />;
case "scenes":
return <SceneSelect {...props} creatable={false} />;
case "movies":
return <MovieSelect {...props} creatable={false} />;
case "groups":
return <GroupSelect {...props} creatable={false} />;
case "galleries":
return <GallerySelect {...props} />;
default:

View File

@ -71,25 +71,25 @@ export const PerformerLink: React.FC<IPerformerLinkProps> = ({
);
};
interface IMovieLinkProps {
movie: INamedObject;
interface IGroupLinkProps {
group: INamedObject;
linkType?: "scene";
className?: string;
}
export const MovieLink: React.FC<IMovieLinkProps> = ({
movie,
export const GroupLink: React.FC<IGroupLinkProps> = ({
group,
linkType = "scene",
className,
}) => {
const link = useMemo(() => {
switch (linkType) {
case "scene":
return NavUtils.makeMovieScenesUrl(movie);
return NavUtils.makeGroupScenesUrl(group);
}
}, [movie, linkType]);
}, [group, linkType]);
const title = movie.name || "";
const title = group.name || "";
return (
<CommonLinkComponent link={link} className={className}>
@ -197,7 +197,7 @@ interface ITagLinkProps {
| "image"
| "details"
| "performer"
| "movie"
| "group"
| "studio";
className?: string;
hoverPlacement?: Placement;
@ -225,8 +225,8 @@ export const TagLink: React.FC<ITagLinkProps> = ({
return NavUtils.makeTagGalleriesUrl(tag);
case "image":
return NavUtils.makeTagImagesUrl(tag);
case "movie":
return NavUtils.makeTagMoviesUrl(tag);
case "group":
return NavUtils.makeTagGroupsUrl(tag);
case "details":
return NavUtils.makeTagUrl(tag.id ?? "");
}

View File

@ -53,7 +53,7 @@ export const Stats: React.FC = () => {
<FormattedNumber value={data.stats.movie_count} />
</p>
<p className="heading">
<FormattedMessage id="movies" />
<FormattedMessage id="groups" />
</p>
</div>
<div className="stats-element">

View File

@ -142,15 +142,15 @@ export const StudioCard: React.FC<IProps> = ({
);
}
function maybeRenderMoviesPopoverButton() {
function maybeRenderGroupsPopoverButton() {
if (!studio.movie_count) return;
return (
<PopoverCountButton
className="movie-count"
type="movie"
className="group-count"
type="group"
count={studio.movie_count}
url={NavUtils.makeStudioMoviesUrl(studio)}
url={NavUtils.makeStudioGroupsUrl(studio)}
/>
);
}
@ -199,7 +199,7 @@ export const StudioCard: React.FC<IProps> = ({
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderScenesPopoverButton()}
{maybeRenderMoviesPopoverButton()}
{maybeRenderGroupsPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderPerformersPopoverButton()}

View File

@ -31,7 +31,7 @@ import {
CompressedStudioDetailsPanel,
StudioDetailsPanel,
} from "./StudioDetailsPanel";
import { StudioMoviesPanel } from "./StudioMoviesPanel";
import { StudioGroupsPanel } from "./StudioMoviesPanel";
import {
faTrashAlt,
faLink,
@ -63,7 +63,7 @@ const validTabs = [
"galleries",
"images",
"performers",
"movies",
"groups",
"childstudios",
] as const;
type TabKey = (typeof validTabs)[number];
@ -108,7 +108,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
(showAllCounts ? studio.image_count_all : studio.image_count) ?? 0;
const performerCount =
(showAllCounts ? studio.performer_count_all : studio.performer_count) ?? 0;
const movieCount =
const groupCount =
(showAllCounts ? studio.movie_count_all : studio.movie_count) ?? 0;
const populatedDefaultTab = useMemo(() => {
@ -120,8 +120,8 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
ret = "images";
} else if (performerCount != 0) {
ret = "performers";
} else if (movieCount != 0) {
ret = "movies";
} else if (groupCount != 0) {
ret = "groups";
} else if (studio.child_studios.length != 0) {
ret = "childstudios";
}
@ -133,7 +133,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
galleryCount,
imageCount,
performerCount,
movieCount,
groupCount,
studio,
]);
@ -437,19 +437,19 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
/>
</Tab>
<Tab
eventKey="movies"
eventKey="groups"
title={
<>
{intl.formatMessage({ id: "movies" })}
{intl.formatMessage({ id: "groups" })}
<Counter
abbreviateCounter={abbreviateCounter}
count={movieCount}
count={groupCount}
hideZero
/>
</>
}
>
<StudioMoviesPanel active={tabKey === "movies"} studio={studio} />
<StudioGroupsPanel active={tabKey === "groups"} studio={studio} />
</Tab>
<Tab
eventKey="childstudios"

View File

@ -1,24 +1,24 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieList } from "src/components/Movies/MovieList";
import { GroupList } from "src/components/Movies/MovieList";
import { useStudioFilterHook } from "src/core/studios";
import { View } from "src/components/List/views";
interface IStudioMoviesPanel {
interface IStudioGroupsPanel {
active: boolean;
studio: GQL.StudioDataFragment;
}
export const StudioMoviesPanel: React.FC<IStudioMoviesPanel> = ({
export const StudioGroupsPanel: React.FC<IStudioGroupsPanel> = ({
active,
studio,
}) => {
const filterHook = useStudioFilterHook(studio);
return (
<MovieList
<GroupList
filterHook={filterHook}
alterQuery={active}
view={View.StudioMovies}
view={View.StudioGroups}
/>
);
};

View File

@ -236,15 +236,15 @@ export const TagCard: React.FC<IProps> = ({
);
}
function maybeRenderMoviesPopoverButton() {
function maybeRenderGroupsPopoverButton() {
if (!tag.movie_count) return;
return (
<PopoverCountButton
className="movie-count"
type="movie"
className="group-count"
type="group"
count={tag.movie_count}
url={NavUtils.makeTagMoviesUrl(tag)}
url={NavUtils.makeTagGroupsUrl(tag)}
/>
);
}
@ -258,7 +258,7 @@ export const TagCard: React.FC<IProps> = ({
{maybeRenderScenesPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderMoviesPopoverButton()}
{maybeRenderGroupsPopoverButton()}
{maybeRenderSceneMarkersPopoverButton()}
{maybeRenderPerformersPopoverButton()}
{maybeRenderStudiosPopoverButton()}

View File

@ -42,7 +42,7 @@ import {
import { DetailImage } from "src/components/Shared/DetailImage";
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { TagMoviesPanel } from "./TagMoviesPanel";
import { TagGroupsPanel } from "./TagMoviesPanel";
interface IProps {
tag: GQL.TagDataFragment;
@ -59,7 +59,7 @@ const validTabs = [
"scenes",
"images",
"galleries",
"movies",
"groups",
"markers",
"performers",
"studios",
@ -105,7 +105,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
(showAllCounts ? tag.image_count_all : tag.image_count) ?? 0;
const galleryCount =
(showAllCounts ? tag.gallery_count_all : tag.gallery_count) ?? 0;
const movieCount =
const groupCount =
(showAllCounts ? tag.movie_count_all : tag.movie_count) ?? 0;
const sceneMarkerCount =
(showAllCounts ? tag.scene_marker_count_all : tag.scene_marker_count) ?? 0;
@ -121,8 +121,8 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
ret = "images";
} else if (galleryCount != 0) {
ret = "galleries";
} else if (movieCount != 0) {
ret = "movies";
} else if (groupCount != 0) {
ret = "groups";
} else if (sceneMarkerCount != 0) {
ret = "markers";
} else if (performerCount != 0) {
@ -140,7 +140,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
sceneMarkerCount,
performerCount,
studioCount,
movieCount,
groupCount,
]);
const setTabKey = useCallback(
@ -484,19 +484,19 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
<TagGalleriesPanel active={tabKey === "galleries"} tag={tag} />
</Tab>
<Tab
eventKey="movies"
eventKey="groups"
title={
<>
{intl.formatMessage({ id: "movies" })}
{intl.formatMessage({ id: "groups" })}
<Counter
abbreviateCounter={abbreviateCounter}
count={movieCount}
count={groupCount}
hideZero
/>
</>
}
>
<TagMoviesPanel active={tabKey === "movies"} tag={tag} />
<TagGroupsPanel active={tabKey === "groups"} tag={tag} />
</Tab>
<Tab
eventKey="markers"

View File

@ -1,12 +1,12 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { useTagFilterHook } from "src/core/tags";
import { MovieList } from "src/components/Movies/MovieList";
import { GroupList } from "src/components/Movies/MovieList";
export const TagMoviesPanel: React.FC<{
export const TagGroupsPanel: React.FC<{
active: boolean;
tag: GQL.TagDataFragment;
}> = ({ active, tag }) => {
const filterHook = useTagFilterHook(tag);
return <MovieList filterHook={filterHook} alterQuery={active} />;
return <GroupList filterHook={filterHook} alterQuery={active} />;
};

View File

@ -143,7 +143,7 @@ export function generateDefaultFrontPageContent(intl: IntlShape) {
return [
recentlyReleased(intl, FilterMode.Scenes, "scenes"),
recentlyAdded(intl, FilterMode.Studios, "studios"),
recentlyReleased(intl, FilterMode.Movies, "movies"),
recentlyReleased(intl, FilterMode.Movies, "groups"),
recentlyAdded(intl, FilterMode.Performers, "performers"),
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
];
@ -156,8 +156,8 @@ export function generatePremadeFrontPageContent(intl: IntlShape) {
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
recentlyAdded(intl, FilterMode.Galleries, "galleries"),
recentlyAdded(intl, FilterMode.Images, "images"),
recentlyReleased(intl, FilterMode.Movies, "movies"),
recentlyAdded(intl, FilterMode.Movies, "movies"),
recentlyReleased(intl, FilterMode.Movies, "groups"),
recentlyAdded(intl, FilterMode.Movies, "groups"),
recentlyAdded(intl, FilterMode.Studios, "studios"),
recentlyAdded(intl, FilterMode.Performers, "performers"),
];

View File

@ -1,7 +1,7 @@
import * as GQL from "src/core/generated-graphql";
import TextUtils from "src/utils/text";
export const scrapedMovieToCreateInput = (toCreate: GQL.ScrapedMovie) => {
export const scrapedGroupToCreateInput = (toCreate: GQL.ScrapedMovie) => {
const input: GQL.MovieCreateInput = {
name: toCreate.name ?? "",
url: toCreate.url,

View File

@ -54,7 +54,7 @@ body {
}
}
#movie-page,
#group-page,
#performer-page,
#studio-page,
#tag-page {
@ -83,7 +83,7 @@ dd {
display: none;
}
.movie-name,
.group-name,
.performer-name,
.studio-name,
.tag-name {
@ -93,7 +93,7 @@ dd {
.sticky.detail-header-group {
padding: 1rem 2.5rem;
a.movie-name,
a.group-name,
a.performer-name,
a.studio-name,
a.tag-name {
@ -313,7 +313,7 @@ dd {
justify-content: center;
padding: 0 1rem;
.movie-images {
.group-images {
height: 100%;
}
@ -322,7 +322,7 @@ dd {
height: auto;
padding: 0;
.movie-images {
.group-images {
.img {
max-width: 100%;
}
@ -335,18 +335,18 @@ dd {
transition: 0.5s;
}
.movie-images img {
.group-images img {
@media (max-width: 576px) {
max-width: 100%;
}
}
}
#movie-page .detail-header-image .movie-images img {
#group-page .detail-header-image .group-images img {
max-width: 13rem;
}
#movie-page .detail-header-image img,
#group-page .detail-header-image img,
#performer-page .detail-header-image img,
#tag-page .detail-header-image img {
border-radius: 0.5rem;

View File

@ -798,9 +798,9 @@
"countables": {
"files": "{count, plural, one {File} other {Files}}",
"galleries": "{count, plural, one {Gallery} other {Galleries}}",
"groups": "{count, plural, one {Group} other {Groups}}",
"images": "{count, plural, one {Image} other {Images}}",
"markers": "{count, plural, one {Marker} other {Markers}}",
"movies": "{count, plural, one {Movie} other {Movies}}",
"performers": "{count, plural, one {Performer} other {Performers}}",
"scenes": "{count, plural, one {Scene} other {Scenes}}",
"studios": "{count, plural, one {Studio} other {Studios}}",
@ -1060,6 +1060,10 @@
"TRANSGENDER_FEMALE": "Transgender Female",
"TRANSGENDER_MALE": "Transgender Male"
},
"group": "Group",
"group_count": "Group Count",
"group_scene_number": "Scene Number",
"groups": "Groups",
"hair_color": "Hair Colour",
"handy_connection_status": {
"connecting": "Connecting",
@ -1117,10 +1121,6 @@
},
"megabits_per_second": "{value} mbps",
"metadata": "Metadata",
"movie": "Movie",
"movie_count": "Movie Count",
"movie_scene_number": "Scene Number",
"movies": "Movies",
"name": "Name",
"new": "New",
"none": "None",

View File

@ -173,7 +173,7 @@ export type InputType =
| "performer_tags"
| "scenes"
| "scene_tags"
| "movies"
| "groups"
| "galleries"
| undefined;

View File

@ -1,9 +1,9 @@
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "movies";
const inputType = "groups";
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
"movies",
"groups",
"movies",
false,
inputType,

View File

@ -47,7 +47,6 @@ const sortByOptions = [
"resume_time",
"play_duration",
"play_count",
"movie_scene_number",
"interactive",
"interactive_speed",
"perceptual_similarity",
@ -59,6 +58,10 @@ const sortByOptions = [
messageID: "o_count",
value: "o_counter",
},
{
messageID: "group_scene_number",
value: "movie_scene_number",
},
]);
const displayModeOptions = [
DisplayMode.Grid,

View File

@ -36,7 +36,7 @@ const sortByOptions = ["name", "random"]
value: "scenes_count",
},
{
messageID: "movie_count",
messageID: "group_count",
value: "movies_count",
},
{
@ -62,7 +62,7 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("gallery_count"),
createMandatoryNumberCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("studio_count"),
createMandatoryNumberCriterionOption("movie_count"),
createMandatoryNumberCriterionOption("movie_count", "group_count"),
createMandatoryNumberCriterionOption("marker_count"),
ParentTagsCriterionOption,
new MandatoryNumberCriterionOption("parent_tag_count", "parent_count"),

View File

@ -693,12 +693,12 @@ declare namespace PluginApi {
function makePerformerScenesUrl(...args: any[]): any;
function makePerformerImagesUrl(...args: any[]): any;
function makePerformerGalleriesUrl(...args: any[]): any;
function makePerformerMoviesUrl(...args: any[]): any;
function makePerformerGroupsUrl(...args: any[]): any;
function makePerformersCountryUrl(...args: any[]): any;
function makeStudioScenesUrl(...args: any[]): any;
function makeStudioImagesUrl(...args: any[]): any;
function makeStudioGalleriesUrl(...args: any[]): any;
function makeStudioMoviesUrl(...args: any[]): any;
function makeStudioGroupsUrl(...args: any[]): any;
function makeStudioPerformersUrl(...args: any[]): any;
function makeTagUrl(...args: any[]): any;
function makeParentTagsUrl(...args: any[]): any;
@ -710,7 +710,7 @@ declare namespace PluginApi {
function makeTagImagesUrl(...args: any[]): any;
function makeScenesPHashMatchUrl(...args: any[]): any;
function makeSceneMarkerUrl(...args: any[]): any;
function makeMovieScenesUrl(...args: any[]): any;
function makeGroupScenesUrl(...args: any[]): any;
function makeChildStudiosUrl(...args: any[]): any;
function makeGalleryImagesUrl(...args: any[]): any;
}

View File

@ -81,11 +81,11 @@ export function getAggregateTagIds(state: { tags: IHasID[] }[]) {
return getAggregateIds(sortedLists);
}
interface IMovie {
interface IGroup {
movie: IHasID;
}
export function getAggregateMovieIds(state: { movies: IMovie[] }[]) {
export function getAggregateGroupIds(state: { movies: IGroup[] }[]) {
const sortedLists = state.map((o) =>
o.movies.map((oo) => oo.movie.id).sort()
);

View File

@ -103,7 +103,7 @@ const makePerformerGalleriesUrl = (
return `/galleries?${filter.makeQueryParameters()}`;
};
const makePerformerMoviesUrl = (
const makePerformerGroupsUrl = (
performer: Partial<GQL.PerformerDataFragment>,
extraPerformer?: ILabeledId,
extraCriteria?: Criterion<CriterionValue>[]
@ -121,7 +121,7 @@ const makePerformerMoviesUrl = (
filter.criteria.push(criterion);
addExtraCriteria(filter.criteria, extraCriteria);
return `/movies?${filter.makeQueryParameters()}`;
return `/groups?${filter.makeQueryParameters()}`;
};
const makePerformersCountryUrl = (
@ -174,7 +174,7 @@ const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
return `/galleries?${filter.makeQueryParameters()}`;
};
const makeStudioMoviesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
const makeStudioGroupsUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#";
const filter = new ListFilterModel(GQL.FilterMode.Movies, undefined);
const criterion = new StudiosCriterion();
@ -184,7 +184,7 @@ const makeStudioMoviesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
depth: 0,
};
filter.criteria.push(criterion);
return `/movies?${filter.makeQueryParameters()}`;
return `/groups?${filter.makeQueryParameters()}`;
};
const makeStudioPerformersUrl = (studio: Partial<GQL.StudioDataFragment>) => {
@ -211,12 +211,12 @@ const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
return `/studios?${filter.makeQueryParameters()}`;
};
const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
if (!movie.id) return "#";
const makeGroupScenesUrl = (group: Partial<GQL.MovieDataFragment>) => {
if (!group.id) return "#";
const filter = new ListFilterModel(GQL.FilterMode.Scenes, undefined);
const criterion = new MoviesCriterion();
criterion.value = [
{ id: movie.id, label: movie.name || `Movie ${movie.id}` },
{ id: group.id, label: group.name || `Group ${group.id}` },
];
filter.criteria.push(criterion);
return `/scenes?${filter.makeQueryParameters()}`;
@ -298,8 +298,8 @@ const makeTagImagesUrl = (tag: INamedObject) => {
return `/images?${makeTagFilter(GQL.FilterMode.Images, tag)}`;
};
const makeTagMoviesUrl = (tag: INamedObject) => {
return `/movies?${makeTagFilter(GQL.FilterMode.Movies, tag)}`;
const makeTagGroupsUrl = (tag: INamedObject) => {
return `/groups?${makeTagFilter(GQL.FilterMode.Movies, tag)}`;
};
type SceneMarkerDataFragment = Pick<GQL.SceneMarker, "id" | "seconds"> & {
@ -349,13 +349,13 @@ const makeDirectorScenesUrl = (director: string) => {
return `/scenes?${filter.makeQueryParameters()}`;
};
const makeDirectorMoviesUrl = (director: string) => {
const makeDirectorGroupsUrl = (director: string) => {
if (director.length == 0) return "#";
const filter = new ListFilterModel(GQL.FilterMode.Movies, undefined);
filter.criteria.push(
stringEqualsCriterion(createStringCriterionOption("director"), director)
);
return `/movies?${filter.makeQueryParameters()}`;
return `/groups?${filter.makeQueryParameters()}`;
};
const makePhotographerGalleriesUrl = (photographer: string) => {
@ -401,12 +401,12 @@ const NavUtils = {
makePerformerScenesUrl,
makePerformerImagesUrl,
makePerformerGalleriesUrl,
makePerformerMoviesUrl,
makePerformerGroupsUrl,
makePerformersCountryUrl,
makeStudioScenesUrl,
makeStudioImagesUrl,
makeStudioGalleriesUrl,
makeStudioMoviesUrl,
makeStudioGroupsUrl: makeStudioGroupsUrl,
makeStudioPerformersUrl,
makeTagUrl,
makeParentTagsUrl,
@ -417,16 +417,16 @@ const NavUtils = {
makeTagStudiosUrl,
makeTagGalleriesUrl,
makeTagImagesUrl,
makeTagMoviesUrl,
makeTagGroupsUrl,
makeScenesPHashMatchUrl,
makeSceneMarkerUrl,
makeMovieScenesUrl,
makeGroupScenesUrl,
makeChildStudiosUrl,
makeGalleryImagesUrl,
makeDirectorScenesUrl,
makePhotographerGalleriesUrl,
makePhotographerImagesUrl,
makeDirectorMoviesUrl,
makeDirectorGroupsUrl,
};
export default NavUtils;