diff --git a/ui/v2.5/src/components/List/ItemList.tsx b/ui/v2.5/src/components/List/ItemList.tsx index 7ff256cb33e..cfde8fdf30a 100644 --- a/ui/v2.5/src/components/List/ItemList.tsx +++ b/ui/v2.5/src/components/List/ItemList.tsx @@ -466,7 +466,7 @@ export function makeItemList({ } return ( -
+
= ({ movie }) => { const history = useHistory(); const Toast = useToast(); + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const uiConfig = configuration?.ui as IUIConfig | undefined; + const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false; + const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; + const showAllDetails = uiConfig?.showAllDetails ?? true; + + const [collapsed, setCollapsed] = useState(!showAllDetails); + const [loadStickyHeader, setLoadStickyHeader] = useState(false); + // Editing state const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); @@ -87,6 +112,7 @@ const MoviePage: React.FC = ({ movie }) => { Mousetrap.bind("d d", () => { onDelete(); }); + Mousetrap.bind(",", () => setCollapsed(!collapsed)); return () => { Mousetrap.unbind("e"); @@ -94,6 +120,27 @@ const MoviePage: React.FC = ({ movie }) => { }; }); + useRatingKeybinds( + true, + configuration?.ui?.ratingSystemOptions?.type, + setRating + ); + + useEffect(() => { + const f = () => { + if (document.documentElement.scrollTop <= 50) { + setLoadStickyHeader(false); + } else { + setLoadStickyHeader(true); + } + }; + + window.addEventListener("scroll", f); + return () => { + window.removeEventListener("scroll", f); + }; + }); + async function onSave(input: GQL.MovieCreateInput) { await updateMovie({ variables: { @@ -159,6 +206,25 @@ const MoviePage: React.FC = ({ movie }) => { ); } + function getCollapseButtonIcon() { + return collapsed ? faChevronDown : faChevronUp; + } + + function maybeRenderShowCollapseButton() { + if (!isEditing) { + return ( + + + + ); + } + } + function renderFrontImage() { let image = movie.front_image_path; if (isEditing) { @@ -174,7 +240,11 @@ const MoviePage: React.FC = ({ movie }) => { if (image && defaultImage) { return (
- Front Cover + Front Cover
); } else if (image) { @@ -184,7 +254,11 @@ const MoviePage: React.FC = ({ movie }) => { variant="link" onClick={() => showLightbox()} > - Front Cover + Front Cover ); } @@ -207,62 +281,180 @@ const MoviePage: React.FC = ({ movie }) => { variant="link" onClick={() => showLightbox(index - 1)} > - Back Cover + Back Cover + + ); + } + } + + const renderClickableIcons = () => ( + + {movie.url && ( + + )} + + ); + + function maybeRenderAliases() { + if (movie?.aliases) { + return ( +
+ {movie?.aliases} +
); } } + function setRating(v: number | null) { + if (movie.id) { + updateMovie({ + variables: { + input: { + id: movie.id, + rating100: v, + }, + }, + }); + } + } + + const renderTabs = () => ; + + function maybeRenderDetails() { + if (!isEditing) { + return ( + + ); + } + } + + function maybeRenderEditPanel() { + if (isEditing) { + return ( + toggleEditing()} + onDelete={onDelete} + setFrontImage={setFrontImage} + setBackImage={setBackImage} + setEncodingImage={setEncodingImage} + /> + ); + } + { + return ( + toggleEditing()} + onSave={() => {}} + onImageChange={() => {}} + onDelete={onDelete} + /> + ); + } + } + + function maybeRenderCompressedDetails() { + if (!isEditing && loadStickyHeader) { + return ; + } + } + + function maybeRenderHeaderBackgroundImage() { + let image = movie.front_image_path; + if (enableBackgroundImage && !isEditing && image) { + return ( +
+ + + {`${movie.name} + +
+ ); + } + } + + function maybeRenderTab() { + if (!isEditing) { + return renderTabs(); + } + } + if (updating || deleting) return ; - // TODO: CSS class return ( -
+
{movie?.name} -
-
- {encodingImage ? ( - - ) : ( -
- {renderFrontImage()} - {renderBackImage()} +
+ {maybeRenderHeaderBackgroundImage()} +
+
+
+ {encodingImage ? ( + + ) : ( +
+ {renderFrontImage()} + {renderBackImage()} +
+ )}
- )} +
+
+
+

+ {movie.name} + {maybeRenderShowCollapseButton()} + {renderClickableIcons()} +

+ {maybeRenderAliases()} + setRating(value ?? null)} + /> + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
+
- - {!isEditing ? ( - <> - - {/* HACK - this is also rendered in the MovieEditPanel */} - toggleEditing()} - onSave={() => {}} - onImageChange={() => {}} - onDelete={onDelete} - /> - - ) : ( - toggleEditing()} - onDelete={onDelete} - setFrontImage={setFrontImage} - setBackImage={setBackImage} - setEncodingImage={setEncodingImage} - /> - )}
- -
- + {maybeRenderCompressedDetails()} +
+
+
{maybeRenderTab()}
+
{renderDeleteAlert()}
diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx index 0fd9506fc55..344e6bde0dc 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx @@ -66,7 +66,9 @@ const MovieCreate: React.FC = () => {
{encodingImage ? ( - + ) : (
{renderFrontImage()} diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx index bccbe36b64d..08af10fa899 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx @@ -3,82 +3,73 @@ import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import DurationUtils from "src/utils/duration"; import TextUtils from "src/utils/text"; -import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; -import { TextField, URLField } from "src/utils/field"; +import { DetailItem } from "src/components/Shared/DetailItem"; interface IMovieDetailsPanel { movie: GQL.MovieDataFragment; + fullWidth?: boolean; } -export const MovieDetailsPanel: React.FC = ({ movie }) => { +export const MovieDetailsPanel: React.FC = ({ + movie, + fullWidth, +}) => { // Network state const intl = useIntl(); - function maybeRenderAliases() { - if (movie.aliases) { - return ( -
- - {intl.formatMessage({ id: "also_known_as" })}{" "} - - {movie.aliases} -
- ); - } - } + return ( +
+ + + + {movie.studio?.name} + + ) : ( + "" + ) + } + fullWidth={fullWidth} + /> - function renderRatingField() { - if (!movie.rating100) { - return; - } + + +
+ ); +}; - return ( - <> -
{intl.formatMessage({ id: "rating" })}
-
- -
- - ); +export const CompressedMovieDetailsPanel: React.FC = ({ + movie, +}) => { + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }); } - // TODO: CSS class return ( -
-
-

{movie.name}

+
+
+ scrollToTop()}> + {movie.name} + + {movie?.studio?.name ? ( + {movie?.studio?.name} + ) : ( + "" + )}
- - {maybeRenderAliases()} - -
- - - - - - {renderRatingField()} - - - - -
); }; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index 561cab69da3..60717ad28ae 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -15,14 +15,10 @@ import { URLField } from "src/components/Shared/URLField"; import { useToast } from "src/hooks/Toast"; import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap"; import DurationUtils from "src/utils/duration"; -import FormUtils from "src/utils/form"; import ImageUtils from "src/utils/image"; -import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { MovieScrapeDialog } from "./MovieScrapeDialog"; -import { useRatingKeybinds } from "src/hooks/keybinds"; -import { ConfigurationContext } from "src/hooks/Config"; import isEqual from "lodash-es/isEqual"; import { DateInput } from "src/components/Shared/DateInput"; import { handleUnsavedChanges } from "src/utils/navigation"; @@ -48,7 +44,6 @@ export const MovieEditPanel: React.FC = ({ }) => { const intl = useIntl(); const Toast = useToast(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); const isNew = movie.id === undefined; @@ -60,6 +55,11 @@ export const MovieEditPanel: React.FC = ({ const Scrapers = useListMovieScrapers(); const [scrapedMovie, setScrapedMovie] = useState(); + const labelXS = 3; + const labelXL = 2; + const fieldXS = 9; + const fieldXL = 7; + const schema = yup.object({ name: yup.string().required(), aliases: yup.string().ensure(), @@ -79,7 +79,6 @@ export const MovieEditPanel: React.FC = ({ }), studio_id: yup.string().required().nullable(), director: yup.string().ensure(), - rating100: yup.number().nullable().defined(), url: yup.string().ensure(), synopsis: yup.string().ensure(), front_image: yup.string().nullable().optional(), @@ -93,7 +92,6 @@ export const MovieEditPanel: React.FC = ({ date: movie?.date ?? "", studio_id: movie?.studio?.id ?? null, director: movie?.director ?? "", - rating100: movie?.rating100 ?? null, url: movie?.url ?? "", synopsis: movie?.synopsis ?? "", }; @@ -107,16 +105,6 @@ export const MovieEditPanel: React.FC = ({ onSubmit: (values) => onSave(values), }); - function setRating(v: number) { - formik.setFieldValue("rating100", v); - } - - useRatingKeybinds( - true, - stashConfig?.ui?.ratingSystemOptions?.type, - setRating - ); - // set up hotkeys useEffect(() => { // Mousetrap.bind("u", (e) => { @@ -347,10 +335,10 @@ export const MovieEditPanel: React.FC = ({ function renderTextField(field: string, title: string, placeholder?: string) { return ( - {FormUtils.renderLabel({ - title, - })} - + + + + = ({
- {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "name" }), - })} - + + + + = ({ {renderTextField("aliases", intl.formatMessage({ id: "aliases" }))} - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "duration" }), - })} - + + + + { @@ -423,10 +411,10 @@ export const MovieEditPanel: React.FC = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "date" }), - })} - + + + + formik.setFieldValue("date", value)} @@ -436,10 +424,10 @@ export const MovieEditPanel: React.FC = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "studio" }), - })} - + + + + formik.setFieldValue( @@ -454,24 +442,11 @@ export const MovieEditPanel: React.FC = ({ {renderTextField("director", intl.formatMessage({ id: "director" }))} - - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "rating" }), - })} - - - formik.setFieldValue("rating100", value ?? null) - } - /> - - - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "url" }), - })} - + + + + = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "synopsis" }), - })} - + + + + = ({ = ({ performer }) => { const intl = useIntl(); const { tab = "details" } = useParams(); - const [collapsed, setCollapsed] = useState(false); - // Configuration settings const { configuration } = React.useContext(ConfigurationContext); - const abbreviateCounter = - (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; - + const uiConfig = configuration?.ui as IUIConfig | undefined; + const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const enableBackgroundImage = + uiConfig?.enablePerformerBackgroundImage ?? false; + const showAllDetails = uiConfig?.showAllDetails ?? false; + const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; + + const [collapsed, setCollapsed] = useState(!showAllDetails); const [isEditing, setIsEditing] = useState(false); const [image, setImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); + const [loadStickyHeader, setLoadStickyHeader] = useState(false); const activeImage = useMemo(() => { const performerImage = performer.image_path; @@ -99,10 +105,10 @@ const PerformerPage: React.FC = ({ performer }) => { tab === "movies" || tab == "appearswith" ? tab - : "details"; + : "scenes"; const setActiveTabKey = (newTab: string | null) => { if (tab !== newTab) { - const tabParam = newTab === "details" ? "" : `/${newTab}`; + const tabParam = newTab === "scenes" ? "" : `/${newTab}`; history.replace(`/performers/${performer.id}${tabParam}`); } }; @@ -126,7 +132,6 @@ const PerformerPage: React.FC = ({ performer }) => { // set up hotkeys useEffect(() => { - Mousetrap.bind("a", () => setActiveTabKey("details")); Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("c", () => setActiveTabKey("scenes")); Mousetrap.bind("g", () => setActiveTabKey("galleries")); @@ -186,44 +191,24 @@ const PerformerPage: React.FC = ({ performer }) => { if (activeImage) { return ( ); } } const renderTabs = () => ( - - - toggleEditing()} - onDelete={onDelete} - onAutoTag={onAutoTag} - isNew={false} - isEditing={false} - onSave={() => {}} - onImageChange={() => {}} - classNames="mb-2" - customButtons={ -
- -
- } - >
-
- - - - = ({ performer }) => {
); - function renderTabsOrEditPanel() { + function maybeRenderHeaderBackgroundImage() { + if (enableBackgroundImage && !isEditing && activeImage) { + return ( +
+ + + {`${performer.name} + +
+ ); + } + } + + function maybeRenderEditPanel() { if (isEditing) { return ( = ({ performer }) => { setEncodingImage={setEncodingImage} /> ); - } else { - return renderTabs(); } + { + return ( + + + toggleEditing()} + onDelete={onDelete} + onAutoTag={onAutoTag} + isNew={false} + isEditing={false} + onSave={() => {}} + onImageChange={() => {}} + classNames="mb-2" + customButtons={ +
+ +
+ } + >
+
+ + ); + } + } + + function getCollapseButtonIcon() { + return collapsed ? faChevronDown : faChevronUp; } - function maybeRenderAge() { - if (performer?.birthdate) { - // calculate the age from birthdate. In future, this should probably be - // provided by the server + useEffect(() => { + const f = () => { + if (document.documentElement.scrollTop <= 50) { + setLoadStickyHeader(false); + } else { + setLoadStickyHeader(true); + } + }; + + window.addEventListener("scroll", f); + return () => { + window.removeEventListener("scroll", f); + }; + }); + + function maybeRenderDetails() { + if (!isEditing) { return ( -
- - {TextUtils.age(performer.birthdate, performer.death_date)} - - - {" "} - - -
+ ); } } + function maybeRenderCompressedDetails() { + if (!isEditing && loadStickyHeader) { + return ; + } + } + + function maybeRenderTab() { + if (!isEditing) { + return renderTabs(); + } + } + function maybeRenderAliases() { if (performer?.alias_list?.length) { return (
- - {" "} - - {performer.alias_list?.join(", ")} + {performer.alias_list?.join(", ")}
); } @@ -392,61 +440,95 @@ const PerformerPage: React.FC = ({ performer }) => { } } - const renderClickableIcons = () => ( - - - {performer.url && ( - - )} - {performer.twitter && ( - - )} - {performer.instagram && ( - + + ); + } + } + + function renderClickableIcons() { + /* Collect urls adding into details */ + /* This code can be removed once multple urls are supported for performers */ + const detailURLsRegex = /\[((?:http|www\.)[^\n\]]+)\]/gm; + let urls = performer?.details?.match(detailURLsRegex); + + return ( + + - )} - - ); + {performer.url && ( + + )} + {(urls ?? []).map((url, index) => ( + + ))} + {performer.twitter && ( + + )} + {performer.instagram && ( + + )} + + ); + } if (isDestroying) return ( @@ -455,10 +537,6 @@ const PerformerPage: React.FC = ({ performer }) => { /> ); - function getCollapseButtonIcon() { - return collapsed ? faChevronRight : faChevronLeft; - } - return (
@@ -466,48 +544,48 @@ const PerformerPage: React.FC = ({ performer }) => {
- {encodingImage ? ( - - ) : ( - renderImage() - )} -
-
- -
-
-
-
-

- +
+ {encodingImage ? ( + - - {performer.name} - {performer.disambiguation && ( - - {` (${performer.disambiguation})`} - - )} - {renderClickableIcons()} -

- setRating(value ?? null)} - /> - {maybeRenderAliases()} - {maybeRenderAge()} + ) : ( + renderImage() + )} +
+
+
+

+ {performer.name} + {performer.disambiguation && ( + + {` (${performer.disambiguation})`} + + )} + {maybeRenderShowCollapseButton()} + {renderClickableIcons()} +

+ {maybeRenderAliases()} + setRating(value ?? null)} + /> + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
+
+ {maybeRenderCompressedDetails()} +
-
{renderTabsOrEditPanel()}
+
{maybeRenderTab()}
diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx index 26b3f88f0f8..bd594b2f635 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx @@ -42,7 +42,11 @@ const PerformerCreate: React.FC = () => { function renderPerformerImage() { if (encodingImage) { - return ; + return ( + + ); } if (image) { return ( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 514258a3811..0c30efcef1b 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -1,19 +1,23 @@ import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; import { getStashboxBase } from "src/utils/stashbox"; -import { getCountryByISO } from "src/utils/country"; -import { TextField, URLField } from "src/utils/field"; import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units"; +import { DetailItem } from "src/components/Shared/DetailItem"; +import { CountryFlag } from "src/components/Shared/CountryFlag"; interface IPerformerDetails { performer: GQL.PerformerDataFragment; + collapsed?: boolean; + fullWidth?: boolean; } export const PerformerDetailsPanel: React.FC = ({ performer, + collapsed, + fullWidth, }) => { // Network state const intl = useIntl(); @@ -22,20 +26,12 @@ export const PerformerDetailsPanel: React.FC = ({ if (!performer.tags.length) { return; } - return ( - <> -
- -
-
-
    - {(performer.tags ?? []).map((tag) => ( - - ))} -
-
- +
    + {(performer.tags ?? []).map((tag) => ( + + ))} +
); } @@ -45,32 +41,27 @@ export const PerformerDetailsPanel: React.FC = ({ } return ( - <> -
StashIDs
-
-
    - {performer.stash_ids.map((stashID) => { - const base = getStashboxBase(stashID.endpoint); - const link = base ? ( - - {stashID.stash_id} - - ) : ( - stashID.stash_id - ); - return ( -
  • - {link} -
  • - ); - })} -
-
- +
    + {performer.stash_ids.map((stashID) => { + const base = getStashboxBase(stashID.endpoint); + const link = base ? ( + + {stashID.stash_id} + + ) : ( + stashID.stash_id + ); + return ( +
  • + {link} +
  • + ); + })} +
); } @@ -176,92 +167,169 @@ export const PerformerDetailsPanel: React.FC = ({ ); }; + function maybeRenderExtraDetails() { + if (!collapsed) { + /* Remove extra urls provided in details since they will be present by perfomr name */ + /* This code can be removed once multple urls are supported for performers */ + let details = performer?.details + ?.replace(/\[((?:http|www\.)[^\n\]]+)\]/gm, "") + .trim(); + return ( + <> + + + + + + + ); + } + } + return ( -
- + {performer.gender ? ( + + ) : ( + "" + )} + + + {performer.country ? ( + + } + fullWidth={fullWidth} + /> + ) : ( + "" + )} + + + + + - - - - - - + + {maybeRenderExtraDetails()} +
+ ); +}; - {!!performer.height_cm && ( - <> -
- -
-
{formatHeight(performer.height_cm)}
- - )} +export const CompressedPerformerDetailsPanel: React.FC = ({ + performer, +}) => { + // Network state + const intl = useIntl(); - {!!performer.weight && ( - <> -
- -
-
{formatWeight(performer.weight)}
- - )} + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }); + } - {(performer.penis_length || performer.circumcised) && ( - <> -
- : -
-
- {formatPenisLength(performer.penis_length)} - {formatCircumcised(performer.circumcised)} -
- - )} - - - - - - - - +
+ scrollToTop()}> + {performer.name} + + {performer.gender ? ( + + {intl.formatMessage({ id: "gender_types." + performer.gender })} + + ) : ( + "" )} - /> - + {TextUtils.age(performer.birthdate, performer.death_date)} + + ) : ( + "" )} - /> - {renderTagsField()} - {renderStashIDs()} - + {performer.country ? ( + + + + ) : ( + "" + )} +
+
); }; diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss index 43c27a39a59..cf356fd3c1d 100644 --- a/ui/v2.5/src/components/Performers/styles.scss +++ b/ui/v2.5/src/components/Performers/styles.scss @@ -16,12 +16,9 @@ .performer-head { display: inline-block; - margin-bottom: 2rem; vertical-align: top; .name-icons { - margin-left: 10px; - .not-favorite { color: rgba(191, 204, 214, 0.5); } @@ -213,4 +210,5 @@ /* stylelint-disable */ font-size: 0.875em; /* stylelint-enable */ + padding-right: 0.5rem; } diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index ea4e3bc1e58..15989fa3cb6 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -683,7 +683,11 @@ export const SceneEditPanel: React.FC = ({ const image = useMemo(() => { if (encodingImage) { - return ; + return ( + + ); } if (coverImagePreview) { diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index df20ecf0f56..6b6bf69bc0d 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -493,6 +493,64 @@ export const SettingsInterfacePanel: React.FC = () => { /> + +
+
+
+

+ {intl.formatMessage({ + id: "config.ui.detail.enable_background_image.heading", + })} +

+
+ {intl.formatMessage({ + id: "config.ui.detail.enable_background_image.description", + })} +
+
+
+
+ saveUI({ enableMovieBackgroundImage: v })} + /> + saveUI({ enablePerformerBackgroundImage: v })} + /> + saveUI({ enableStudioBackgroundImage: v })} + /> + saveUI({ enableTagBackgroundImage: v })} + /> +
+ saveUI({ showAllDetails: v })} + /> + saveUI({ compactExpandedDetails: v })} + /> + +
diff --git a/ui/v2.5/src/components/Shared/CountryFlag.tsx b/ui/v2.5/src/components/Shared/CountryFlag.tsx index c5f5959f74d..66c6c89ed1b 100644 --- a/ui/v2.5/src/components/Shared/CountryFlag.tsx +++ b/ui/v2.5/src/components/Shared/CountryFlag.tsx @@ -5,11 +5,13 @@ import { getCountryByISO } from "src/utils/country"; interface ICountryFlag { country?: string | null; className?: string; + includeName?: boolean; } export const CountryFlag: React.FC = ({ className, country: isoCountry, + includeName, }) => { const { locale } = useIntl(); @@ -18,9 +20,12 @@ export const CountryFlag: React.FC = ({ if (!isoCountry || !country) return <>; return ( - + <> + {includeName ? country : ""} + + ); }; diff --git a/ui/v2.5/src/components/Shared/DetailItem.tsx b/ui/v2.5/src/components/Shared/DetailItem.tsx new file mode 100644 index 00000000000..a5988f5a748 --- /dev/null +++ b/ui/v2.5/src/components/Shared/DetailItem.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +interface IDetailItem { + id?: string | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value?: any; + title?: string; + fullWidth?: boolean; +} + +export const DetailItem: React.FC = ({ + id, + value, + title, + fullWidth, +}) => { + if (!id || !value || value === "Na") { + return <>; + } + + const message = ; + + return ( + // according to linter rule CSS classes shouldn't use underscores +
+ + {message} + {fullWidth ? ":" : ""} + + + {value} + +
+ ); +}; diff --git a/ui/v2.5/src/components/Shared/GridCard.tsx b/ui/v2.5/src/components/Shared/GridCard.tsx index 332bcaf0422..a96bded86f3 100644 --- a/ui/v2.5/src/components/Shared/GridCard.tsx +++ b/ui/v2.5/src/components/Shared/GridCard.tsx @@ -35,6 +35,7 @@ export const GridCard: React.FC = (props: ICardProps) => { props.onSelectedChanged(!props.selected, shiftKey); event.preventDefault(); } + window.scrollTo(0, 0); } function handleDrag(event: React.DragEvent) { diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index fb7c51c2bad..6ff2ed658a9 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -46,11 +46,11 @@ margin-right: 0.5rem; white-space: nowrap; } +} - div:nth-last-child(2) { - flex: 1; - max-width: 100%; - } +.detail-header.edit .details-edit div:nth-last-child(2) { + flex: 1; + max-width: 100%; } .select-suggest { diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 027d3adc9d8..921b3d6f7d2 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -26,14 +26,22 @@ import { StudioImagesPanel } from "./StudioImagesPanel"; import { StudioChildrenPanel } from "./StudioChildrenPanel"; import { StudioPerformersPanel } from "./StudioPerformersPanel"; import { StudioEditPanel } from "./StudioEditPanel"; -import { StudioDetailsPanel } from "./StudioDetailsPanel"; +import { + CompressedStudioDetailsPanel, + StudioDetailsPanel, +} from "./StudioDetailsPanel"; import { StudioMoviesPanel } from "./StudioMoviesPanel"; import { faTrashAlt, - faChevronRight, - faChevronLeft, + faLink, + faChevronDown, + faChevronUp, } from "@fortawesome/free-solid-svg-icons"; import { IUIConfig } from "src/core/config"; +import TextUtils from "src/utils/text"; +import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; +import ImageUtils from "src/utils/image"; +import { useRatingKeybinds } from "src/hooks/keybinds"; interface IProps { studio: GQL.StudioDataFragment; @@ -49,12 +57,16 @@ const StudioPage: React.FC = ({ studio }) => { const intl = useIntl(); const { tab = "details" } = useParams(); - const [collapsed, setCollapsed] = useState(false); - // Configuration settings const { configuration } = React.useContext(ConfigurationContext); - const abbreviateCounter = - (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; + const uiConfig = configuration?.ui as IUIConfig | undefined; + const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; + const showAllDetails = uiConfig?.showAllDetails ?? false; + const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; + + const [collapsed, setCollapsed] = useState(!showAllDetails); + const [loadStickyHeader, setLoadStickyHeader] = useState(false); // Editing state const [isEditing, setIsEditing] = useState(false); @@ -95,6 +107,27 @@ const StudioPage: React.FC = ({ studio }) => { }; }); + useRatingKeybinds( + true, + configuration?.ui?.ratingSystemOptions?.type, + setRating + ); + + useEffect(() => { + const f = () => { + if (document.documentElement.scrollTop <= 50) { + setLoadStickyHeader(false); + } else { + setLoadStickyHeader(true); + } + }; + + window.addEventListener("scroll", f); + return () => { + window.removeEventListener("scroll", f); + }; + }); + async function onSave(input: GQL.StudioCreateInput) { await updateStudio({ variables: { @@ -162,6 +195,20 @@ const StudioPage: React.FC = ({ studio }) => { ); } + function maybeRenderAliases() { + if (studio?.aliases?.length) { + return ( +
+ {studio?.aliases?.join(", ")} +
+ ); + } + } + + function getCollapseButtonIcon() { + return collapsed ? faChevronDown : faChevronUp; + } + function toggleEditing(value?: boolean) { if (value !== undefined) { setIsEditing(value); @@ -184,7 +231,14 @@ const StudioPage: React.FC = ({ studio }) => { } if (studioImage) { - return {studio.name}; + return ( + {studio.name} + ); } } @@ -203,175 +257,289 @@ const StudioPage: React.FC = ({ studio }) => { } }; - function getCollapseButtonIcon() { - return collapsed ? faChevronRight : faChevronLeft; + const renderClickableIcons = () => ( + + {studio.url && ( + + )} + + ); + + function setRating(v: number | null) { + if (studio.id) { + updateStudio({ + variables: { + input: { + id: studio.id, + rating100: v, + }, + }, + }); + } } - return ( -
-
+ ); + } + } + + function maybeRenderShowCollapseButton() { + if (!isEditing) { + return ( + + + + ); + } + } + + function maybeRenderCompressedDetails() { + if (!isEditing && loadStickyHeader) { + return ; + } + } + + const renderTabs = () => ( + + -
- {encodingImage ? ( - - ) : ( - renderImage() - )} -
- {!isEditing ? ( - <> - - - {studio.name ?? intl.formatMessage({ id: "studio" })} - - - - toggleEditing()} - onSave={() => {}} - onImageChange={() => {}} - onClearImage={() => {}} - onAutoTag={onAutoTag} - onDelete={onDelete} - /> - - ) : ( - + {intl.formatMessage({ id: "scenes" })} + + + } + > + toggleEditing()} - onDelete={onDelete} - setImage={setImage} - setEncodingImage={setEncodingImage} /> - )} -
-
- -
-
- + + {intl.formatMessage({ id: "galleries" })} + + + } > - - {intl.formatMessage({ id: "scenes" })} - - - } - > - - - - {intl.formatMessage({ id: "galleries" })} - - - } - > - - - - {intl.formatMessage({ id: "images" })} - - - } - > - - - - {intl.formatMessage({ id: "performers" })} - - - } - > - - - - {intl.formatMessage({ id: "movies" })} - - - } - > - - - - {intl.formatMessage({ id: "subsidiary_studios" })} - - - } - > - + + + {intl.formatMessage({ id: "images" })} + + + } + > + + + + {intl.formatMessage({ id: "performers" })} + + + } + > + + + + {intl.formatMessage({ id: "movies" })} + + + } + > + + + + {intl.formatMessage({ id: "subsidiary_studios" })} + + + } + > + + + + + ); + + function maybeRenderHeaderBackgroundImage() { + let studioImage = studio.image_path; + if (enableBackgroundImage && !isEditing && studioImage) { + return ( +
+ + + {`${studio.name} - - + +
+ ); + } + } + + function maybeRenderTab() { + if (!isEditing) { + return renderTabs(); + } + } + + function maybeRenderEditPanel() { + if (isEditing) { + return ( + toggleEditing()} + onDelete={onDelete} + setImage={setImage} + setEncodingImage={setEncodingImage} + /> + ); + } + { + return ( + toggleEditing()} + onSave={() => {}} + onImageChange={() => {}} + onClearImage={() => {}} + onAutoTag={onAutoTag} + onDelete={onDelete} + /> + ); + } + } + + return ( +
+ + {studio.name ?? intl.formatMessage({ id: "studio" })} + + +
+ {maybeRenderHeaderBackgroundImage()} +
+
+ {encodingImage ? ( + + ) : ( + renderImage() + )} +
+
+
+

+ {studio.name} + {maybeRenderShowCollapseButton()} + {renderClickableIcons()} +

+ {maybeRenderAliases()} + setRating(value ?? null)} + /> + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
+
+
+
+ {maybeRenderCompressedDetails()} +
+
+
{maybeRenderTab()}
+
{renderDeleteAlert()}
diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx index 250c85d445d..002c07849f7 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioCreate.tsx @@ -58,7 +58,9 @@ const StudioCreate: React.FC = () => {
{encodingImage ? ( - + ) : ( renderImage() )} diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx index 54d6fab5d95..bfaa9b4f435 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx @@ -1,119 +1,100 @@ import React from "react"; -import { Badge } from "react-bootstrap"; -import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import TextUtils from "src/utils/text"; -import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; -import { TextField, URLField } from "src/utils/field"; +import { DetailItem } from "src/components/Shared/DetailItem"; interface IStudioDetailsPanel { studio: GQL.StudioDataFragment; + collapsed?: boolean; + fullWidth?: boolean; } export const StudioDetailsPanel: React.FC = ({ studio, + collapsed, + fullWidth, }) => { - const intl = useIntl(); - function renderRatingField() { - if (!studio.rating100) { + function renderStashIDs() { + if (!studio.stash_ids?.length) { return; } return ( - <> -
{intl.formatMessage({ id: "rating" })}
-
- -
- +
    + {studio.stash_ids.map((stashID) => { + const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; + const link = base ? ( + + {stashID.stash_id} + + ) : ( + stashID.stash_id + ); + return ( +
  • + {link} +
  • + ); + })} +
); } - function renderTagsList() { - if (!studio.aliases?.length) { - return; + function maybeRenderExtraDetails() { + if (!collapsed) { + return ( + + ); } - - return ( - <> -
- -
-
- {studio.aliases.map((a) => ( - - {a} - - ))} -
- - ); } - function renderStashIDs() { - if (!studio.stash_ids?.length) { - return; - } + return ( +
+ + + {studio.parent_studio.name} + + ) : ( + "" + ) + } + fullWidth={fullWidth} + /> + {maybeRenderExtraDetails()} +
+ ); +}; - return ( - <> -
- -
-
-
    - {studio.stash_ids.map((stashID) => { - const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; - const link = base ? ( - - {stashID.stash_id} - - ) : ( - stashID.stash_id - ); - return ( -
  • - {link} -
  • - ); - })} -
-
- - ); +export const CompressedStudioDetailsPanel: React.FC = ({ + studio, +}) => { + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }); } return ( -
-
-

{studio.name}

+
+
+ scrollToTop()}> + {studio.name} + + {studio?.parent_studio?.name ? ( + {studio?.parent_studio?.name} + ) : ( + "" + )}
- -
- - - - - - - {renderRatingField()} - {renderTagsList()} - {renderStashIDs()} -
); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index 24cfd96494f..fdc31c1610e 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -8,16 +8,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { StudioSelect } from "src/components/Shared/Select"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { Button, Form, Col, Row } from "react-bootstrap"; -import FormUtils from "src/utils/form"; import ImageUtils from "src/utils/image"; import { getStashIDs } from "src/utils/stashIds"; -import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { StringListInput } from "../../Shared/StringListInput"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; -import { useRatingKeybinds } from "src/hooks/keybinds"; -import { ConfigurationContext } from "src/hooks/Config"; import isEqual from "lodash-es/isEqual"; import { useToast } from "src/hooks/Toast"; import { handleUnsavedChanges } from "src/utils/navigation"; @@ -43,7 +39,11 @@ export const StudioEditPanel: React.FC = ({ const Toast = useToast(); const isNew = studio.id === undefined; - const { configuration } = React.useContext(ConfigurationContext); + + const labelXS = 3; + const labelXL = 2; + const fieldXS = 9; + const fieldXL = 7; // Network state const [isLoading, setIsLoading] = useState(false); @@ -53,7 +53,6 @@ export const StudioEditPanel: React.FC = ({ url: yup.string().ensure(), details: yup.string().ensure(), parent_id: yup.string().required().nullable(), - rating100: yup.number().nullable().defined(), aliases: yup .array(yup.string().required()) .defined() @@ -85,7 +84,6 @@ export const StudioEditPanel: React.FC = ({ url: studio.url ?? "", details: studio.details ?? "", parent_id: studio.parent_studio?.id ?? null, - rating100: studio.rating100 ?? null, aliases: studio.aliases ?? [], ignore_auto_tag: studio.ignore_auto_tag ?? false, stash_ids: getStashIDs(studio.stash_ids), @@ -112,16 +110,6 @@ export const StudioEditPanel: React.FC = ({ setEncodingImage(encodingImage); }, [setEncodingImage, encodingImage]); - function setRating(v: number) { - formik.setFieldValue("rating100", v); - } - - useRatingKeybinds( - true, - configuration?.ui?.ratingSystemOptions?.type, - setRating - ); - // set up hotkeys useEffect(() => { Mousetrap.bind("s s", () => { @@ -171,8 +159,10 @@ export const StudioEditPanel: React.FC = ({ return ( - StashIDs - + + StashIDs + +
    {formik.values.stash_ids.map((stashID) => { const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; @@ -235,10 +225,10 @@ export const StudioEditPanel: React.FC = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "name" }), - })} - + + + + = ({ + + + + + + formik.setFieldValue("aliases", value)} + errors={aliasErrorMsg} + errorIdx={aliasErrorIdx} + /> + + + - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "url" }), - })} - + + + + = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "details" }), - })} - + + + + = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "parent_studios" }), - })} - + + + + formik.setFieldValue( @@ -301,44 +305,16 @@ export const StudioEditPanel: React.FC = ({ - - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "rating" }), - })} - - - formik.setFieldValue("rating100", value ?? null) - } - /> - - - {renderStashIDs()} - - - - - - - formik.setFieldValue("aliases", value)} - errors={aliasErrorMsg} - errorIdx={aliasErrorIdx} - /> - -
    - + - + = ({ = ({ tag }) => { const Toast = useToast(); const intl = useIntl(); - const [collapsed, setCollapsed] = useState(false); - // Configuration settings const { configuration } = React.useContext(ConfigurationContext); - const abbreviateCounter = - (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; + const uiConfig = configuration?.ui as IUIConfig | undefined; + const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false; + const showAllDetails = uiConfig?.showAllDetails ?? false; + const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; + + const [collapsed, setCollapsed] = useState(!showAllDetails); + const [loadStickyHeader, setLoadStickyHeader] = useState(false); const { tab = "scenes" } = useParams(); @@ -117,6 +122,21 @@ const TagPage: React.FC = ({ tag }) => { }; }); + useEffect(() => { + const f = () => { + if (document.documentElement.scrollTop <= 50) { + setLoadStickyHeader(false); + } else { + setLoadStickyHeader(true); + } + }; + + window.addEventListener("scroll", f); + return () => { + window.removeEventListener("scroll", f); + }; + }); + async function onSave(input: GQL.TagCreateInput) { const oldRelations = { parents: tag.parents ?? [], @@ -203,6 +223,35 @@ const TagPage: React.FC = ({ tag }) => { ); } + function getCollapseButtonIcon() { + return collapsed ? faChevronDown : faChevronUp; + } + + function maybeRenderShowCollapseButton() { + if (!isEditing) { + return ( + + + + ); + } + } + + function maybeRenderAliases() { + if (tag?.aliases?.length) { + return ( +
    + {tag?.aliases?.join(", ")} +
    + ); + } + } + function toggleEditing(value?: boolean) { if (value !== undefined) { setIsEditing(value); @@ -225,7 +274,14 @@ const TagPage: React.FC = ({ tag }) => { } if (tagImage) { - return {tag.name}; + return ( + {tag.name} + ); } } @@ -270,157 +326,211 @@ const TagPage: React.FC = ({ tag }) => { ); } - function getCollapseButtonIcon() { - return collapsed ? faChevronRight : faChevronLeft; + function maybeRenderDetails() { + if (!isEditing) { + return ( + + ); + } + } + + function maybeRenderEditPanel() { + if (isEditing) { + return ( + toggleEditing()} + onDelete={onDelete} + setImage={setImage} + setEncodingImage={setEncodingImage} + /> + ); + } + { + return ( + toggleEditing()} + onSave={() => {}} + onImageChange={() => {}} + onClearImage={() => {}} + onAutoTag={onAutoTag} + onDelete={onDelete} + classNames="mb-2" + customButtons={renderMergeButton()} + /> + ); + } + } + + const renderTabs = () => ( + + + + {intl.formatMessage({ id: "scenes" })} + + + } + > + + + + {intl.formatMessage({ id: "images" })} + + + } + > + + + + {intl.formatMessage({ id: "galleries" })} + + + } + > + + + + {intl.formatMessage({ id: "markers" })} + + + } + > + + + + {intl.formatMessage({ id: "performers" })} + + + } + > + + + + + ); + + function maybeRenderHeaderBackgroundImage() { + let tagImage = tag.image_path; + if (enableBackgroundImage && !isEditing && tagImage) { + return ( +
    + + + {`${tag.name} + +
    + ); + } + } + + function maybeRenderTab() { + if (!isEditing) { + return renderTabs(); + } + } + + function maybeRenderCompressedDetails() { + if (!isEditing && loadStickyHeader) { + return ; + } } return ( - <> +
    {tag.name} -
    -
    -
    + +
    + {maybeRenderHeaderBackgroundImage()} +
    +
    {encodingImage ? ( - + ) : ( renderImage() )} -

    {tag.name}

    -

    {tag.description}

    - {!isEditing ? ( - <> - - {/* HACK - this is also rendered in the TagEditPanel */} - toggleEditing()} - onSave={() => {}} - onImageChange={() => {}} - onClearImage={() => {}} - onAutoTag={onAutoTag} - onDelete={onDelete} - classNames="mb-2" - customButtons={renderMergeButton()} - /> - - ) : ( - toggleEditing()} - onDelete={onDelete} - setImage={setImage} - setEncodingImage={setEncodingImage} - /> - )} -
    -
    - +
    +
    +

    + {tag.name} + {maybeRenderShowCollapseButton()} +

    + {maybeRenderAliases()} + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
    +
    -
    - - - {intl.formatMessage({ id: "scenes" })} - - - } - > - - - - {intl.formatMessage({ id: "images" })} - - - } - > - - - - {intl.formatMessage({ id: "galleries" })} - - - } - > - - - - {intl.formatMessage({ id: "markers" })} - - - } - > - - - - {intl.formatMessage({ id: "performers" })} - - - } - > - - - +
    + {maybeRenderCompressedDetails()} +
    +
    +
    {maybeRenderTab()}
    - {renderDeleteAlert()} - {renderMergeDialog()}
    - + {renderDeleteAlert()} + {renderMergeDialog()} +
    ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx index 4b4c6dfc16e..b2f0c2af1f2 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagCreate.tsx @@ -60,7 +60,9 @@ const TagCreate: React.FC = () => {
    {encodingImage ? ( - + ) : ( renderImage() )} diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index 19b921a55ee..2f0df5ac80f 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -1,53 +1,28 @@ import React from "react"; import { Badge } from "react-bootstrap"; -import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; +import { DetailItem } from "src/components/Shared/DetailItem"; import * as GQL from "src/core/generated-graphql"; interface ITagDetails { tag: GQL.TagDataFragment; + fullWidth?: boolean; } -export const TagDetailsPanel: React.FC = ({ tag }) => { - function renderAliasesField() { - if (!tag.aliases.length) { - return; - } - - return ( -
    -
    - -
    -
    - {tag.aliases.map((a) => ( - - {a} - - ))} -
    -
    - ); - } - +export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { function renderParentsField() { if (!tag.parents?.length) { return; } return ( -
    -
    - -
    -
    - {tag.parents.map((p) => ( - - {p.name} - - ))} -
    -
    + <> + {tag.parents.map((p) => ( + + {p.name} + + ))} + ); } @@ -57,26 +32,54 @@ export const TagDetailsPanel: React.FC = ({ tag }) => { } return ( -
    -
    - -
    -
    - {tag.children.map((c) => ( - - {c.name} - - ))} -
    -
    + <> + {tag.children.map((c) => ( + + {c.name} + + ))} + ); } return ( - <> - {renderAliasesField()} - {renderParentsField()} - {renderChildrenField()} - +
    + + + +
    + ); +}; + +export const CompressedTagDetailsPanel: React.FC = ({ tag }) => { + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }); + } + + return ( +
    +
    + scrollToTop()}> + {tag.name} + + {tag.description ? ( + {tag.description} + ) : ( + "" + )} +
    +
    ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx index 7a6e4a6081e..bec63643f80 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx @@ -5,7 +5,6 @@ import * as yup from "yup"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { TagSelect } from "src/components/Shared/Select"; import { Form, Col, Row } from "react-bootstrap"; -import FormUtils from "src/utils/form"; import ImageUtils from "src/utils/image"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; @@ -42,9 +41,9 @@ export const TagEditPanel: React.FC = ({ const [isLoading, setIsLoading] = useState(false); const labelXS = 3; - const labelXL = 3; + const labelXL = 2; const fieldXS = 9; - const fieldXL = 9; + const fieldXL = 7; const schema = yup.object({ name: yup.string().required(), @@ -204,10 +203,10 @@ export const TagEditPanel: React.FC = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "description" }), - })} - + + + + = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "parent_tags" }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - + + + + @@ -247,15 +241,10 @@ export const TagEditPanel: React.FC = ({ - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "sub_tags" }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - + + + + @@ -294,6 +283,7 @@ export const TagEditPanel: React.FC = ({ .btn-primary.dropdown-toggle, + .btn-primary:hover { + background: rgba(138, 155, 168, 0.15); + background-color: rgba(138, 155, 168, 0.15); + border-color: rgba(138, 155, 168, 0.15); + box-shadow: unset; + color: #f5f8fa; + } +} + +.detail-header { + background-color: #192127; + min-height: 15rem; + overflow: hidden; + padding: 1rem; + position: relative; + transition: 0.3s; + width: 100%; + z-index: 11; + + .detail-group, + .col { + transition: 0.2s; + + @media (max-width: 576px) { + padding-top: 0.5rem; + } + } + + .background-image-container { + bottom: -0.2rem; + left: 0; + opacity: 0.2; + position: absolute; + right: 0; + top: -0.2rem; + z-index: auto; + + .background-image { + filter: blur(16px); + height: 100%; + object-fit: cover; + object-position: 50% 30%; + width: 100%; + } + } + + .detail-container { + height: 100%; + position: relative; + z-index: 20; + + .detail-item-value.age { + border-bottom: 1px dotted #f5f8fa; + margin-right: auto; + } + } + + h2 { + margin-bottom: 0; + } + + .country, + .performer-country { + .mr-2.fi { + margin-left: 0.5rem; + } + } + + .alias-head { + color: #868791; + } + + .detail-expand-collapse, + .name-icons { + margin-left: 10px; + } +} + +.detail-header.edit { + background-color: unset; + + form { + padding-top: 0.5rem; + } + + .details-edit { + padding-top: 1rem; + } + + .detail-header-image { + height: auto; + } +} + +.detail-header.collapsed { + .detail-header-image img { + max-width: 11rem; + transition: 0.5s; + } +} + +.detail-body { + margin-left: 15px; + margin-right: 15px; + width: 100%; + + nav { + align-content: center; + border-bottom: solid 2px #192127; + display: flex; + justify-content: center; + margin: 0; + padding: 5px 0; + } +} + +.collapsed .detail-item-value { + -webkit-box-orient: vertical; + display: -webkit-box; + -webkit-line-clamp: 3; + overflow: hidden; +} + +.full-width { + .detail-header-image { + height: auto; + + img { + max-width: 22rem; + } + } + + .detail-item { + flex-direction: unset; + padding-right: 0; + width: 100%; + + .detail-item-title { + display: table-cell; + width: 100px; + } + + .detail-item-value { + padding-left: 0.5rem; + } + } + + .detail-item-title.tags, + .detail-item-title.parent-tags, + .detail-item-title.sub-tags { + padding-top: 4px; + } +} + +.detail-header-image { + display: flex; + float: left; + height: 100%; + justify-content: center; + padding: 0 1rem; + + .movie-images { + height: 100%; + } + + @media (max-width: 576px) { + float: unset; + height: auto; + padding: 0; + + .movie-images { + .img { + max-width: 100%; + } + } + } + + img { + margin: auto; + max-width: 14rem; + transition: 0.5s; + } + + .movie-images img { + @media (max-width: 576px) { + max-width: 100%; + } + } +} + +#movie-page .detail-header-image .movie-images img { + max-width: 13rem; +} + +#movie-page .detail-header-image img, +#performer-page .detail-header-image img, +#tag-page .detail-header-image img { + border-radius: 0.5rem; +} + +#tag-page .full-width .detail-header-image img { + max-width: 22rem; + + @media (max-width: 576px) { + max-width: 100%; + } +} + +#tag-page .detail-header-image img { + max-width: 18rem; + + @media (max-width: 576px) { + max-width: 100%; + } +} + +.detail-item.tags .pl-0 { + margin-bottom: 0; +} + +.detail-group { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 1rem 0; +} + +.detail-item { + align-items: left; + display: inline-flex; + flex-direction: column; + padding-bottom: 0.5rem; + padding-right: 4rem; + + @media (max-width: 576px) { + padding-right: 2rem; + } +} + +.detail-item-title { + color: #868791; + font-weight: 700; +} + +.detail-item-value { + align-items: center; + display: flex; + flex-direction: row; + white-space: pre-line; +} + .input-control, .text-input { border: 0; @@ -130,6 +428,12 @@ textarea.text-input { } } +@media (max-width: 576px) { + .row.justify-content-center { + margin-left: 0; + margin-right: 0; + } +} @media (min-width: 576px) { .zoom-0 { width: 240px; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index c6d1e98796a..30b12fee8e5 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -40,6 +40,7 @@ "download_backup": "Download Backup", "edit": "Edit", "edit_entity": "Edit {entityType}", + "encoding_image": "Encoding image", "export": "Export", "export_all": "Export all…", "find": "Find", @@ -580,6 +581,21 @@ } } }, + "detail": { + "enable_background_image": { + "description": "Display background image on detail page.", + "heading": "Enable background image" + }, + "heading": "Detail Page", + "compact_expanded_details": { + "description": "When enabled, this option will present expanded details while maintaining a compact presentation", + "heading": "Compact expanded details" + }, + "show_all_details": { + "description": "When enabled, all content details will be shown by default and each detail item will fit under a single column", + "heading": "Show all details" + } + }, "funscript_offset": { "description": "Time offset in milliseconds for interactive scripts playback.", "heading": "Funscript Offset (ms)" diff --git a/ui/v2.5/src/utils/image.tsx b/ui/v2.5/src/utils/image.tsx index b31387e830f..53443c0b3eb 100644 --- a/ui/v2.5/src/utils/image.tsx +++ b/ui/v2.5/src/utils/image.tsx @@ -70,6 +70,17 @@ const ImageUtils = { onImageChange, usePasteImage, imageToDataURL, + verifyImageSize, }; +function verifyImageSize(e: React.UIEvent) { + const img = e.target as HTMLImageElement; + // set width = 200px if zero-sized image (SVG w/o intrinsic size) + if (img.width === 0 && img.height === 0) { + img.setAttribute("width", "200"); + } else { + img.removeAttribute("width"); + } +} + export default ImageUtils;