diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 073304358da..5d329606b0c 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -26,6 +26,9 @@ import { faLink, faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import TextUtils from "src/utils/text"; import { Icon } from "src/components/Shared/Icon"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; +import { ConfigurationContext } from "src/hooks/Config"; +import { IUIConfig } from "src/core/config"; +import ImageUtils from "src/utils/image"; interface IProps { movie: GQL.MovieDataFragment; @@ -36,6 +39,11 @@ const MoviePage: React.FC = ({ movie }) => { const history = useHistory(); const Toast = useToast(); + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const uiConfig = configuration?.ui as IUIConfig | undefined; + const enableBackgroundImage = uiConfig?.enableBackgroundImage ?? false; + // Editing state const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); @@ -180,7 +188,11 @@ const MoviePage: React.FC = ({ movie }) => { if (image && defaultImage) { return (
- Front Cover + Front Cover
); } else if (image) { @@ -190,7 +202,11 @@ const MoviePage: React.FC = ({ movie }) => { variant="link" onClick={() => showLightbox()} > - Front Cover + Front Cover ); } @@ -213,7 +229,11 @@ const MoviePage: React.FC = ({ movie }) => { variant="link" onClick={() => showLightbox(index - 1)} > - Back Cover + Back Cover ); } @@ -302,6 +322,23 @@ const MoviePage: React.FC = ({ movie }) => { } } + function maybeRenderHeaderBackgroundImage() { + if (enableBackgroundImage && !isEditing && frontImage) { + return ( +
+ + + {`${movie.name} + +
+ ); + } + } + function maybeRenderTab() { if (!isEditing) { return renderTabs(); @@ -317,31 +354,34 @@ const MoviePage: React.FC = ({ movie }) => {
-
-
- {encodingImage ? ( - - ) : ( -
- {renderFrontImage()} - {renderBackImage()} -
- )} + {maybeRenderHeaderBackgroundImage()} +
+
+
+ {encodingImage ? ( + + ) : ( +
+ {renderFrontImage()} + {renderBackImage()} +
+ )} +
-
-
-
-

- {movie.name} - {renderClickableIcons()} -

- {maybeRenderAliases()} - setRating(value ?? null)} - /> - {maybeRenderDetails()} - {maybeRenderEditPanel()} +
+
+

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

+ {maybeRenderAliases()} + setRating(value ?? null)} + /> + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 9457bc28921..3f31b3c68b6 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -42,6 +42,7 @@ import { import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons"; import { IUIConfig } from "src/core/config"; import { useRatingKeybinds } from "src/hooks/keybinds"; +import ImageUtils from "src/utils/image"; interface IProps { performer: GQL.PerformerDataFragment; @@ -60,6 +61,7 @@ const PerformerPage: React.FC = ({ performer }) => { const { configuration } = React.useContext(ConfigurationContext); const uiConfig = configuration?.ui as IUIConfig | undefined; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const enableBackgroundImage = uiConfig?.enableBackgroundImage ?? false; const showAllDetails = uiConfig?.showAllDetails ?? false; const [collapsed, setCollapsed] = useState(!showAllDetails); @@ -186,7 +188,12 @@ const PerformerPage: React.FC = ({ performer }) => { if (activeImage) { return ( ); } @@ -293,6 +300,23 @@ const PerformerPage: React.FC = ({ performer }) => { ); + function maybeRenderHeaderBackgroundImage() { + if (enableBackgroundImage && !isEditing && activeImage) { + return ( +
+ + + {`${performer.name} + +
+ ); + } + } + function maybeRenderEditPanel() { if (isEditing) { return ( @@ -502,32 +526,35 @@ const PerformerPage: React.FC = ({ performer }) => { collapsed ? "collapsed" : "" }`} > -
- {encodingImage ? ( - - ) : ( - renderImage() - )} -
-
-
-

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

- {maybeRenderAliases()} - setRating(value ?? null)} - /> - {maybeRenderDetails()} - {maybeRenderEditPanel()} + {maybeRenderHeaderBackgroundImage()} +
+
+ {encodingImage ? ( + + ) : ( + renderImage() + )} +
+
+
+

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

+ {maybeRenderAliases()} + setRating(value ?? null)} + /> + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 682ac118e63..98674437b06 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -22,11 +22,17 @@ export const PerformerDetailsPanel: React.FC = ({ function renderTagsField() { return ( -
    - {(performer.tags ?? []).map((tag) => ( - - ))} -
+ <> + {performer.tags ? ( +
    + {(performer.tags ?? []).map((tag) => ( + + ))} +
+ ) : ( + "" + )} + ); } diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss index 5aaa64b6500..cf356fd3c1d 100644 --- a/ui/v2.5/src/components/Performers/styles.scss +++ b/ui/v2.5/src/components/Performers/styles.scss @@ -19,8 +19,6 @@ vertical-align: top; .name-icons { - margin-left: 10px; - .not-favorite { color: rgba(191, 204, 214, 0.5); } diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index af9fa932992..f6010fc26c1 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -556,6 +556,13 @@ export const SettingsInterfacePanel: React.FC = () => { value={ui.maxOptionsShown ?? defaultMaxOptionsShown} onChange={(v) => saveUI({ maxOptionsShown: v })} /> + saveUI({ enableBackgroundImage: v })} + /> = ({ studio }) => { // 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?.enableBackgroundImage ?? false; // Editing state const [isEditing, setIsEditing] = useState(false); @@ -193,10 +195,10 @@ const StudioPage: React.FC = ({ studio }) => { if (studioImage) { return ( {studio.name} ); } @@ -380,6 +382,24 @@ const StudioPage: React.FC = ({ studio }) => { ); + function maybeRenderHeaderBackgroundImage() { + let studioImage = studio.image_path; + if (enableBackgroundImage && !isEditing && studioImage) { + return ( +
+ + + {`${studio.name} + +
+ ); + } + } + function maybeRenderTab() { if (!isEditing) { return renderTabs(); @@ -423,26 +443,29 @@ const StudioPage: React.FC = ({ studio }) => {
-
- {encodingImage ? ( - - ) : ( - renderImage() - )} -
-
-
-

- {studio.name} - {renderClickableIcons()} -

- {maybeRenderAliases()} - setRating(value ?? null)} - /> - {maybeRenderDetails()} - {maybeRenderEditPanel()} + {maybeRenderHeaderBackgroundImage()} +
+
+ {encodingImage ? ( + + ) : ( + renderImage() + )} +
+
+
+

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

+ {maybeRenderAliases()} + setRating(value ?? null)} + /> + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx index 3b758770f56..fdc31c1610e 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx @@ -159,7 +159,9 @@ export const StudioEditPanel: React.FC = ({ return ( - StashIDs + + StashIDs +
    {formik.values.stash_ids.map((stashID) => { diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 8715d86b136..fd4c7673bfd 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -35,6 +35,7 @@ import { faTrashAlt, } from "@fortawesome/free-solid-svg-icons"; import { IUIConfig } from "src/core/config"; +import ImageUtils from "src/utils/image"; interface IProps { tag: GQL.TagDataFragment; @@ -51,8 +52,9 @@ const TagPage: React.FC = ({ tag }) => { // 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?.enableBackgroundImage ?? false; const { tab = "scenes" } = useParams(); @@ -230,7 +232,14 @@ const TagPage: React.FC = ({ tag }) => { } if (tagImage) { - return {tag.name}; + return ( + {tag.name} + ); } } @@ -401,6 +410,24 @@ const TagPage: React.FC = ({ tag }) => { ); + function maybeRenderHeaderBackgroundImage() { + let tagImage = tag.image_path; + if (enableBackgroundImage && !isEditing && tagImage) { + return ( +
    + + + {`${tag.name} + +
    + ); + } + } + function maybeRenderTab() { if (!isEditing) { return renderTabs(); @@ -420,21 +447,24 @@ const TagPage: React.FC = ({ tag }) => {
    -
    - {encodingImage ? ( - - ) : ( - renderImage() - )} -
    -
    -
    -

    - {tag.name} -

    - {maybeRenderAliases()} - {maybeRenderDetails()} - {maybeRenderEditPanel()} + {maybeRenderHeaderBackgroundImage()} +
    +
    + {encodingImage ? ( + + ) : ( + renderImage() + )} +
    +
    +
    +

    + {tag.name} +

    + {maybeRenderAliases()} + {maybeRenderDetails()} + {maybeRenderEditPanel()} +
    diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index 6154836ba06..330643c5d21 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -43,6 +43,8 @@ export interface IUIConfig { ratingSystemOptions?: RatingSystemOptions; + // if true a background image will be display on header + enableBackgroundImage?: boolean; // if true show all content details by default showAllDetails?: boolean; // if true the chromecast option will enabled diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index a43be5ed9b1..6b8e742adc3 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -139,7 +139,9 @@ dd { .detail-header { background-color: #192127; min-height: 15rem; + overflow: hidden; padding: 1rem; + position: relative; transition: 0.3s; width: 100%; z-index: 11; @@ -153,6 +155,28 @@ dd { } } + .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%; + } + h2 { margin-bottom: 0; } @@ -168,6 +192,7 @@ dd { color: #868791; } + .detail-expand-collapse, .name-icons { margin-left: 10px; } @@ -238,7 +263,6 @@ dd { } img { - border-radius: 0.5rem; margin: auto; max-width: 14rem; transition: 0.5s; @@ -251,6 +275,12 @@ dd { } } +#movie-page .detail-header-image img, +#performer-page .detail-header-image img, +#tag-page .detail-header-image img { + border-radius: 0.5rem; +} + #tag-page .detail-header-image img { max-width: 18rem; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 6e4dd3f82a0..eac2cae9be9 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -556,6 +556,10 @@ "max_options_shown": { "label": "Maximum number of items to show in select dropdowns" }, + "enable_background_image": { + "description": "Display background image on detail page header.", + "heading": "Enable background image" + }, "show_all_details": { "description": "When enabled, all content details will be shown by default.", "heading": "Show all details" 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;