From 3212a35efbfb5564ba6907d3336905e64a266689 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 20 Nov 2025 20:54:14 -0800 Subject: [PATCH] add LibraryContainer for max-width and background overlay --- .../components/album-detail-content.tsx | 7 +- .../albums/components/album-detail-header.tsx | 143 ++++++++---------- .../albums/routes/album-detail-route.tsx | 34 +++-- .../album-artist-detail-content.tsx | 8 +- .../components/album-artist-detail-header.tsx | 11 +- .../routes/album-artist-detail-route.tsx | 38 +++-- .../library-background-overlay.module.css | 25 ++- .../components/library-background-overlay.tsx | 73 ++++++++- .../components/library-container.module.css | 11 ++ .../shared/components/library-container.tsx | 15 ++ .../components/library-header.module.css | 25 --- .../shared/components/library-header.tsx | 19 +-- src/renderer/hooks/use-fast-average-color.tsx | 3 + 13 files changed, 237 insertions(+), 175 deletions(-) create mode 100644 src/renderer/features/shared/components/library-container.module.css create mode 100644 src/renderer/features/shared/components/library-container.tsx diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 4f54d50fc..e66dc6450 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -12,7 +12,6 @@ import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/ import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel'; -import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { searchLibraryItems } from '/@/renderer/features/shared/utils'; import { useContainerQuery } from '/@/renderer/hooks'; @@ -49,9 +48,6 @@ import { } from '/@/shared/types/domain-types'; import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; -interface AlbumDetailContentProps { - background?: string; -} interface AlbumMetadataTagsProps { album: Album | undefined; @@ -316,7 +312,7 @@ const AlbumMetadataExternalLinks = ({ ); }; -export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => { +export const AlbumDetailContent = () => { const { t } = useTranslation(); const { albumId } = useParams() as { albumId: string }; const server = useCurrentServer(); @@ -370,7 +366,6 @@ export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => { return (
-
{comment && {replaceURLWithHTMLLinks(comment)}}
diff --git a/src/renderer/features/albums/components/album-detail-header.tsx b/src/renderer/features/albums/components/album-detail-header.tsx index b0a527f90..8e669b56b 100644 --- a/src/renderer/features/albums/components/album-detail-header.tsx +++ b/src/renderer/features/albums/components/album-detail-header.tsx @@ -16,93 +16,82 @@ import { Stack } from '/@/shared/components/stack/stack'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; -interface AlbumDetailHeaderProps { - background: { - background?: string; - blur: number; - loading: boolean; - }; -} +export const AlbumDetailHeader = forwardRef((_props, ref) => { + const { albumId } = useParams() as { albumId: string }; + const server = useCurrentServer(); + const detailQuery = useQuery( + albumQueries.detail({ query: { id: albumId }, serverId: server?.id }), + ); -export const AlbumDetailHeader = forwardRef( - ({ background }, ref) => { - const { albumId } = useParams() as { albumId: string }; - const server = useCurrentServer(); - const detailQuery = useQuery( - albumQueries.detail({ query: { id: albumId }, serverId: server?.id }), + const showRating = + detailQuery?.data?._serverType === ServerType.NAVIDROME || + detailQuery?.data?._serverType === ServerType.SUBSONIC; + + const { addToQueueByFetch, setFavorite, setRating } = usePlayer(); + const playButtonBehavior = usePlayButtonBehavior(); + + const handleFavorite = () => { + if (!detailQuery?.data) return; + setFavorite( + detailQuery.data._serverId, + [detailQuery.data.id], + LibraryItem.ALBUM, + !detailQuery.data.userFavorite, ); + }; - const showRating = - detailQuery?.data?._serverType === ServerType.NAVIDROME || - detailQuery?.data?._serverType === ServerType.SUBSONIC; - - const { addToQueueByFetch, setFavorite, setRating } = usePlayer(); - const playButtonBehavior = usePlayButtonBehavior(); - - const handleFavorite = () => { - if (!detailQuery?.data) return; - setFavorite( - detailQuery.data._serverId, - [detailQuery.data.id], - LibraryItem.ALBUM, - !detailQuery.data.userFavorite, - ); - }; - - const handleUpdateRating = showRating - ? (rating: number) => { - if (!detailQuery?.data) return; - - if (detailQuery.data.userRating === rating) { - return setRating( - detailQuery.data._serverId, - [detailQuery.data.id], - LibraryItem.ALBUM, - 0, - ); - } + const handleUpdateRating = showRating + ? (rating: number) => { + if (!detailQuery?.data) return; + if (detailQuery.data.userRating === rating) { return setRating( detailQuery.data._serverId, [detailQuery.data.id], LibraryItem.ALBUM, - rating, + 0, ); } - : undefined; - const handlePlay = (type?: Play) => { - if (!server?.id || !albumId) return; - addToQueueByFetch(server.id, [albumId], LibraryItem.ALBUM, type || playButtonBehavior); - }; + return setRating( + detailQuery.data._serverId, + [detailQuery.data.id], + LibraryItem.ALBUM, + rating, + ); + } + : undefined; - const handleMoreOptions = (e: React.MouseEvent) => { - if (!detailQuery?.data) return; - ContextMenuController.call({ - cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM }, - event: e, - }); - }; + const handlePlay = (type?: Play) => { + if (!server?.id || !albumId) return; + addToQueueByFetch(server.id, [albumId], LibraryItem.ALBUM, type || playButtonBehavior); + }; - return ( - - - handlePlay(Play.NOW)} - onRating={handleUpdateRating} - onShuffle={() => handlePlay(Play.SHUFFLE)} - rating={detailQuery?.data?.userRating || 0} - /> - - - ); - }, -); + const handleMoreOptions = (e: React.MouseEvent) => { + if (!detailQuery?.data) return; + ContextMenuController.call({ + cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM }, + event: e, + }); + }; + + return ( + + + handlePlay(Play.NOW)} + onRating={handleUpdateRating} + onShuffle={() => handlePlay(Play.SHUFFLE)} + rating={detailQuery?.data?.userRating || 0} + /> + + + ); +}); diff --git a/src/renderer/features/albums/routes/album-detail-route.tsx b/src/renderer/features/albums/routes/album-detail-route.tsx index d4e3ee35f..e04adfbce 100644 --- a/src/renderer/features/albums/routes/album-detail-route.tsx +++ b/src/renderer/features/albums/routes/album-detail-route.tsx @@ -7,6 +7,11 @@ import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content'; import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-detail-header'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; +import { + LibraryBackgroundImage, + LibraryBackgroundOverlay, +} from '/@/renderer/features/shared/components/library-background-overlay'; +import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { useFastAverageColor } from '/@/renderer/hooks'; import { useCurrentServer, useGeneralSettings } from '/@/renderer/store'; @@ -28,14 +33,15 @@ const AlbumDetailRoute = () => { staleTime: 0, }); - const { background: backgroundColor, colorId } = useFastAverageColor({ + const { background: backgroundColor } = useFastAverageColor({ id: albumId, src: detailQuery.data?.imageUrl, srcLoaded: !detailQuery.isLoading, }); - const backgroundUrl = detailQuery.data?.imageUrl || ''; - const background = (albumBackground && `url(${backgroundUrl})`) || backgroundColor; + const background = backgroundColor; + + const showBlurredImage = Boolean(detailQuery.data?.imageUrl) && albumBackground; return ( @@ -59,15 +65,19 @@ const AlbumDetailRoute = () => { }} ref={scrollAreaRef} > - - + {showBlurredImage ? ( + + ) : ( + + )} + + } /> + + ); diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 98b6ed4bd..614729a24 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -10,7 +10,6 @@ import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel'; import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; -import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay'; import { PlayButton } from '/@/renderer/features/shared/components/play-button'; import { useContainerQuery } from '/@/renderer/hooks'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; @@ -35,11 +34,7 @@ import { } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; -interface AlbumArtistDetailContentProps { - background?: string; -} - -export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => { +export const AlbumArtistDetailContent = () => { const { t } = useTranslation(); const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings(); const { albumArtistId, artistId } = useParams() as { @@ -230,7 +225,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten return (
-
) => { + (_props, ref: Ref) => { const { albumArtistId, artistId } = useParams() as { albumArtistId?: string; artistId?: string; @@ -95,7 +87,6 @@ export const AlbumArtistDetailHeader = forwardRef( item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }} ref={ref} title={detailQuery?.data?.name || ''} - {...background} > diff --git a/src/renderer/features/artists/routes/album-artist-detail-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-route.tsx index e6be89530..bee8a4525 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-route.tsx @@ -7,10 +7,14 @@ import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content'; import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; +import { + LibraryBackgroundImage, + LibraryBackgroundOverlay, +} from '/@/renderer/features/shared/components/library-background-overlay'; +import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { useFastAverageColor } from '/@/renderer/hooks'; -import { useCurrentServer } from '/@/renderer/store'; -import { useGeneralSettings } from '/@/renderer/store/settings.store'; +import { useCurrentServer, useGeneralSettings } from '/@/renderer/store'; import { LibraryItem } from '/@/shared/types/domain-types'; const AlbumArtistDetailRoute = () => { @@ -37,20 +41,20 @@ const AlbumArtistDetailRoute = () => { staleTime: 0, }); - const { background: backgroundColor, colorId } = useFastAverageColor({ + const { background: backgroundColor } = useFastAverageColor({ id: artistId, src: detailQuery.data?.imageUrl, srcLoaded: !detailQuery.isLoading, }); - const backgroundUrl = detailQuery.data?.imageUrl || ''; - const background = (artistBackground && `url(${backgroundUrl})`) || backgroundColor; + const background = backgroundColor; + const showBlurredImage = Boolean(detailQuery.data?.imageUrl) && artistBackground; return ( { }} ref={scrollAreaRef} > - - + {showBlurredImage && detailQuery.data?.imageUrl ? ( + + ) : ( + + )} + + } /> + + ); diff --git a/src/renderer/features/shared/components/library-background-overlay.module.css b/src/renderer/features/shared/components/library-background-overlay.module.css index d76f0133a..76488a966 100644 --- a/src/renderer/features/shared/components/library-background-overlay.module.css +++ b/src/renderer/features/shared/components/library-background-overlay.module.css @@ -1,11 +1,30 @@ -.root { +.overlay { position: absolute; z-index: -1; width: 100%; - height: 20vh; min-height: 200px; pointer-events: none; user-select: none; background-image: var(--theme-overlay-subheader); - opacity: 0.3; + opacity: 0.7; +} + +.background-image { + position: absolute; + top: 0; + z-index: 0; + width: 100%; + background-position: center !important; + background-size: cover !important; + opacity: 0.9; +} + +.background-image-overlay { + position: absolute; + top: 0; + left: 0; + z-index: 0; + width: 100%; + background: var(--theme-overlay-subheader); + opacity: 0.5; } diff --git a/src/renderer/features/shared/components/library-background-overlay.tsx b/src/renderer/features/shared/components/library-background-overlay.tsx index 0c0b30108..804e9f880 100644 --- a/src/renderer/features/shared/components/library-background-overlay.tsx +++ b/src/renderer/features/shared/components/library-background-overlay.tsx @@ -1,9 +1,78 @@ +import { useEffect, useState } from 'react'; + import styles from './library-background-overlay.module.css'; interface LibraryBackgroundOverlayProps { backgroundColor?: string; + headerRef: React.RefObject; } -export const LibraryBackgroundOverlay = ({ backgroundColor }: LibraryBackgroundOverlayProps) => { - return
; +export const LibraryBackgroundOverlay = ({ + backgroundColor, + headerRef, +}: LibraryBackgroundOverlayProps) => { + const height = useHeaderHeight(headerRef); + return ( +
+ ); +}; + +interface LibraryBackgroundProps { + blur?: number; + headerRef: React.RefObject; + imageUrl?: string; +} + +export const LibraryBackgroundImage = ({ blur, headerRef, imageUrl }: LibraryBackgroundProps) => { + const url = `url(${imageUrl})`; + const height = useHeaderHeight(headerRef); + return ( + <> +
+
+ + ); +}; + +const useHeaderHeight = (headerRef: React.RefObject) => { + const [headerHeight, setHeaderHeight] = useState(0); + + useEffect(() => { + if (!headerRef?.current) return; + + const updateHeight = () => { + if (headerRef?.current) { + setHeaderHeight(headerRef.current.offsetHeight); + } + }; + + updateHeight(); + + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(headerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [headerRef]); + + return headerHeight; }; diff --git a/src/renderer/features/shared/components/library-container.module.css b/src/renderer/features/shared/components/library-container.module.css new file mode 100644 index 000000000..e6ecb274b --- /dev/null +++ b/src/renderer/features/shared/components/library-container.module.css @@ -0,0 +1,11 @@ +.container { + position: relative; + width: 100%; + max-width: 1600px; + margin: 0 auto; +} + +.content { + position: relative; + z-index: 0; +} diff --git a/src/renderer/features/shared/components/library-container.tsx b/src/renderer/features/shared/components/library-container.tsx new file mode 100644 index 000000000..d0d6b81c5 --- /dev/null +++ b/src/renderer/features/shared/components/library-container.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from 'react'; + +import styles from './library-container.module.css'; + +interface LibraryContainerProps { + children: ReactNode; +} + +export const LibraryContainer = ({ children }: LibraryContainerProps) => { + return ( +
+
{children}
+
+ ); +}; diff --git a/src/renderer/features/shared/components/library-header.module.css b/src/renderer/features/shared/components/library-header.module.css index a8b3128e4..9036549cb 100644 --- a/src/renderer/features/shared/components/library-header.module.css +++ b/src/renderer/features/shared/components/library-header.module.css @@ -96,31 +96,6 @@ border-radius: 5px; } -.background { - position: absolute; - top: 0; - z-index: 0; - width: 100%; - height: 100%; - background-position: center !important; - background-size: cover !important; - opacity: 0.9; -} - -.background-overlay { - position: absolute; - top: 0; - left: 0; - z-index: 0; - width: 100%; - height: 100%; - background: var(--theme-overlay-header); -} - -.opaque-overlay { - opacity: 0.5; -} - .title { display: flex; align-items: center !important; diff --git a/src/renderer/features/shared/components/library-header.tsx b/src/renderer/features/shared/components/library-header.tsx index facfeb66d..e4fa2a88e 100644 --- a/src/renderer/features/shared/components/library-header.tsx +++ b/src/renderer/features/shared/components/library-header.tsx @@ -1,6 +1,5 @@ import { closeAllModals, openModal } from '@mantine/modals'; import { AutoTextSize } from 'auto-text-size'; -import clsx from 'clsx'; import { forwardRef, ReactNode, Ref, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; @@ -11,7 +10,6 @@ import { WidePlayButton, WideShuffleButton, } from '/@/renderer/features/shared/components/play-button'; -import { useGeneralSettings } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Center } from '/@/shared/components/center/center'; import { Group } from '/@/shared/components/group/group'; @@ -21,8 +19,6 @@ import { Text } from '/@/shared/components/text/text'; import { LibraryItem } from '/@/shared/types/domain-types'; interface LibraryHeaderProps { - background?: string; - blur?: number; children?: ReactNode; imagePlaceholderUrl?: null | string; imageUrl?: null | string; @@ -32,13 +28,9 @@ interface LibraryHeaderProps { } export const LibraryHeader = forwardRef( - ( - { background, blur, children, imageUrl, item, title }: LibraryHeaderProps, - ref: Ref, - ) => { + ({ children, imageUrl, item, title }: LibraryHeaderProps, ref: Ref) => { const { t } = useTranslation(); const [isImageError, setIsImageError] = useState(false); - const { albumBackground } = useGeneralSettings(); const onImageError = () => { setIsImageError(true); @@ -92,15 +84,6 @@ export const LibraryHeader = forwardRef( return (
-
-
openImage()} diff --git a/src/renderer/hooks/use-fast-average-color.tsx b/src/renderer/hooks/use-fast-average-color.tsx index 3f3fd8b23..b3e58c308 100644 --- a/src/renderer/hooks/use-fast-average-color.tsx +++ b/src/renderer/hooks/use-fast-average-color.tsx @@ -10,6 +10,9 @@ export const getFastAverageColor = async (args: { algorithm: args.algorithm || 'dominant', ignoredColor: [ [255, 255, 255, 255, 90], // White + [255, 255, 255, 255, 50], // Light gray + [255, 255, 255, 255, 30], // Very light gray + [255, 255, 255, 255, 10], // Very very light gray [0, 0, 0, 255, 30], // Black [0, 0, 0, 0, 40], // Transparent ],