add LibraryContainer for max-width and background overlay

This commit is contained in:
jeffvli
2025-11-20 20:54:14 -08:00
parent c4f94495a8
commit 3212a35efb
13 changed files with 237 additions and 175 deletions
@@ -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 (
<div className={styles.contentContainer} ref={ref}>
<LibraryBackgroundOverlay backgroundColor={background} />
<div className={styles.detailContainer}>
{comment && <Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>}
<div className={styles.contentLayout}>
@@ -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<HTMLDivElement>((_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<HTMLDivElement, AlbumDetailHeaderProps>(
({ 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<HTMLButtonElement>) => {
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 (
<Stack ref={ref}>
<LibraryHeader
imageUrl={detailQuery?.data?.imageUrl}
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
title={detailQuery?.data?.name || ''}
{...background}
>
<LibraryHeaderMenu
favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite}
onMore={handleMoreOptions}
onPlay={() => handlePlay(Play.NOW)}
onRating={handleUpdateRating}
onShuffle={() => handlePlay(Play.SHUFFLE)}
rating={detailQuery?.data?.userRating || 0}
/>
</LibraryHeader>
</Stack>
);
},
);
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!detailQuery?.data) return;
ContextMenuController.call({
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM },
event: e,
});
};
return (
<Stack ref={ref}>
<LibraryHeader
imageUrl={detailQuery?.data?.imageUrl}
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
title={detailQuery?.data?.name || ''}
>
<LibraryHeaderMenu
favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite}
onMore={handleMoreOptions}
onPlay={() => handlePlay(Play.NOW)}
onRating={handleUpdateRating}
onShuffle={() => handlePlay(Play.SHUFFLE)}
rating={detailQuery?.data?.userRating || 0}
/>
</LibraryHeader>
</Stack>
);
});
@@ -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 (
<AnimatedPage key={`album-detail-${albumId}`}>
@@ -59,15 +65,19 @@ const AlbumDetailRoute = () => {
}}
ref={scrollAreaRef}
>
<AlbumDetailHeader
background={{
background,
blur: (albumBackground && albumBackgroundBlur) || 0,
loading: !backgroundColor || colorId !== albumId,
}}
ref={headerRef}
/>
<AlbumDetailContent background={background} />
{showBlurredImage ? (
<LibraryBackgroundImage
blur={albumBackgroundBlur}
headerRef={headerRef}
imageUrl={detailQuery.data.imageUrl!}
/>
) : (
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
)}
<LibraryContainer>
<AlbumDetailHeader ref={headerRef as React.Ref<HTMLDivElement>} />
<AlbumDetailContent />
</LibraryContainer>
</NativeScrollArea>
</AnimatedPage>
);
@@ -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 (
<div className={styles.contentContainer} ref={ref}>
<LibraryBackgroundOverlay backgroundColor={background} />
<div className={styles.detailContainer}>
<Group gap="md">
<PlayButton
@@ -15,16 +15,8 @@ import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
interface AlbumArtistDetailHeaderProps {
background: {
background?: string;
blur: number;
loading: boolean;
};
}
export const AlbumArtistDetailHeader = forwardRef(
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
(_props, ref: Ref<HTMLDivElement>) => {
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}
>
<Stack>
<Group>
@@ -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 (
<AnimatedPage key={`album-artist-detail-${routeId}`}>
<NativeScrollArea
pageHeaderProps={{
backgroundColor: background,
backgroundColor: backgroundColor || undefined,
children: (
<LibraryHeaderBar>
<LibraryHeaderBar.PlayButton
@@ -67,15 +71,19 @@ const AlbumArtistDetailRoute = () => {
}}
ref={scrollAreaRef}
>
<AlbumArtistDetailHeader
background={{
background,
blur: (artistBackground && artistBackgroundBlur) || 0,
loading: !backgroundColor || colorId !== artistId,
}}
ref={headerRef}
/>
<AlbumArtistDetailContent background={background} />
{showBlurredImage && detailQuery.data?.imageUrl ? (
<LibraryBackgroundImage
blur={artistBackgroundBlur}
headerRef={headerRef}
imageUrl={detailQuery.data.imageUrl}
/>
) : (
<LibraryBackgroundOverlay backgroundColor={background} headerRef={headerRef} />
)}
<LibraryContainer>
<AlbumArtistDetailHeader ref={headerRef as React.Ref<HTMLDivElement>} />
<AlbumArtistDetailContent />
</LibraryContainer>
</NativeScrollArea>
</AnimatedPage>
);
@@ -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;
}
@@ -1,9 +1,78 @@
import { useEffect, useState } from 'react';
import styles from './library-background-overlay.module.css';
interface LibraryBackgroundOverlayProps {
backgroundColor?: string;
headerRef: React.RefObject<HTMLDivElement | null>;
}
export const LibraryBackgroundOverlay = ({ backgroundColor }: LibraryBackgroundOverlayProps) => {
return <div className={styles.root} style={{ backgroundColor }} />;
export const LibraryBackgroundOverlay = ({
backgroundColor,
headerRef,
}: LibraryBackgroundOverlayProps) => {
const height = useHeaderHeight(headerRef);
return (
<div
className={styles.overlay}
style={{
backgroundColor,
height: height ? `${height + 64}px` : undefined,
}}
/>
);
};
interface LibraryBackgroundProps {
blur?: number;
headerRef: React.RefObject<HTMLDivElement | null>;
imageUrl?: string;
}
export const LibraryBackgroundImage = ({ blur, headerRef, imageUrl }: LibraryBackgroundProps) => {
const url = `url(${imageUrl})`;
const height = useHeaderHeight(headerRef);
return (
<>
<div
className={styles.backgroundImage}
style={{
background: url,
filter: `blur(${blur ?? 0}rem)`,
height: height ? `${height - 64}px` : undefined,
}}
/>
<div
className={styles.backgroundImageOverlay}
style={{
height: height ? `${height + 64}px` : undefined,
}}
/>
</>
);
};
const useHeaderHeight = (headerRef: React.RefObject<HTMLDivElement | null>) => {
const [headerHeight, setHeaderHeight] = useState<number>(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;
};
@@ -0,0 +1,11 @@
.container {
position: relative;
width: 100%;
max-width: 1600px;
margin: 0 auto;
}
.content {
position: relative;
z-index: 0;
}
@@ -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 (
<div className={styles.container}>
<div className={styles.content}>{children}</div>
</div>
);
};
@@ -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;
@@ -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<HTMLDivElement>,
) => {
({ children, imageUrl, item, title }: LibraryHeaderProps, ref: Ref<HTMLDivElement>) => {
const { t } = useTranslation();
const [isImageError, setIsImageError] = useState<boolean | null>(false);
const { albumBackground } = useGeneralSettings();
const onImageError = () => {
setIsImageError(true);
@@ -92,15 +84,6 @@ export const LibraryHeader = forwardRef(
return (
<div className={styles.libraryHeader} ref={ref}>
<div
className={styles.background}
style={{ background, filter: `blur(${blur ?? 0}rem)` }}
/>
<div
className={clsx(styles.backgroundOverlay, {
[styles.opaqueOverlay]: albumBackground,
})}
/>
<div
className={styles.imageSection}
onClick={() => openImage()}
@@ -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
],