Update album/playlist headers with shared styles
This commit is contained in:
parent
d49bba42ef
commit
a3804808b4
4 changed files with 230 additions and 296 deletions
|
@ -1,72 +1,14 @@
|
||||||
import { Center, Group } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useMergedRef } from '@mantine/hooks';
|
|
||||||
import { forwardRef, Fragment, Ref } from 'react';
|
import { forwardRef, Fragment, Ref } from 'react';
|
||||||
import { RiAlbumFill } from 'react-icons/ri';
|
|
||||||
import { generatePath, useParams } from 'react-router';
|
import { generatePath, useParams } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import { Text } from '/@/renderer/components';
|
||||||
import { Text, TextTitle } from '/@/renderer/components';
|
|
||||||
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
|
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
|
||||||
|
import { LibraryHeader } from '/@/renderer/features/shared';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { LibraryItem } from '/@/renderer/types';
|
||||||
const HeaderContainer = styled.div`
|
import { formatDurationString } from '/@/renderer/utils';
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
grid-template-areas: 'image info';
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
grid-template-columns: 250px minmax(0, 1fr);
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 30vh;
|
|
||||||
min-height: 340px;
|
|
||||||
max-height: 500px;
|
|
||||||
padding: 5rem 2rem 2rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CoverImageWrapper = styled.div`
|
|
||||||
z-index: 15;
|
|
||||||
display: flex;
|
|
||||||
grid-area: image;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
filter: drop-shadow(0 0 8px rgb(0, 0, 0, 50%));
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MetadataWrapper = styled.div`
|
|
||||||
z-index: 15;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
grid-area: info;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledImage = styled.img`
|
|
||||||
object-fit: cover;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BackgroundImage = styled.div<{ background: string }>`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: ${(props) => props.background};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BackgroundImageOverlay = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(180deg, rgba(25, 26, 28, 5%), var(--main-bg)), var(--background-noise);
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface AlbumDetailHeaderProps {
|
interface AlbumDetailHeaderProps {
|
||||||
background: string;
|
background: string;
|
||||||
|
@ -77,75 +19,46 @@ export const AlbumDetailHeader = forwardRef(
|
||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const detailQuery = useAlbumDetail({ id: albumId });
|
const detailQuery = useAlbumDetail({ id: albumId });
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const mergedRef = useMergedRef(ref, cq.ref);
|
|
||||||
|
|
||||||
const titleSize = cq.isXl
|
const metadataItems = [
|
||||||
? '6rem'
|
{
|
||||||
: cq.isLg
|
id: 'releaseYear',
|
||||||
? '5.5rem'
|
secondary: false,
|
||||||
: cq.isMd
|
value: detailQuery?.data?.releaseYear,
|
||||||
? '4.5rem'
|
},
|
||||||
: cq.isSm
|
{
|
||||||
? '3.5rem'
|
id: 'songCount',
|
||||||
: '2rem';
|
secondary: false,
|
||||||
|
value: `${detailQuery?.data?.songCount} songs`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duration',
|
||||||
|
secondary: true,
|
||||||
|
value: detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('detailQuery?.data?.duration :>> ', detailQuery?.data?.duration);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderContainer ref={mergedRef}>
|
<Stack ref={cq.ref}>
|
||||||
<BackgroundImage background={background} />
|
<LibraryHeader
|
||||||
<BackgroundImageOverlay />
|
ref={ref}
|
||||||
<CoverImageWrapper>
|
background={background}
|
||||||
{detailQuery?.data?.imageUrl ? (
|
imageUrl={detailQuery?.data?.imageUrl}
|
||||||
<StyledImage
|
item={{ route: AppRoute.PLAYLISTS, type: LibraryItem.ALBUM }}
|
||||||
alt="cover"
|
title={detailQuery?.data?.name || ''}
|
||||||
height={225}
|
|
||||||
src={detailQuery?.data.imageUrl}
|
|
||||||
width={225}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-default-radius)',
|
|
||||||
height: `${225}px`,
|
|
||||||
width: `${225}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RiAlbumFill
|
<Stack mt="1rem">
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</CoverImageWrapper>
|
|
||||||
<MetadataWrapper>
|
|
||||||
<Group>
|
<Group>
|
||||||
<Text
|
{metadataItems.map((item, index) => (
|
||||||
$link
|
<Fragment key={`item-${item.id}-${index}`}>
|
||||||
component={Link}
|
{index > 0 && <Text $noSelect>•</Text>}
|
||||||
fw="600"
|
<Text $secondary={item.secondary}>{item.value}</Text>
|
||||||
sx={{ textTransform: 'uppercase' }}
|
</Fragment>
|
||||||
to={AppRoute.LIBRARY_ALBUMS}
|
))}
|
||||||
>
|
|
||||||
Album
|
|
||||||
</Text>
|
|
||||||
{detailQuery?.data?.releaseYear && (
|
|
||||||
<>
|
|
||||||
<Text>•</Text>
|
|
||||||
<Text>{detailQuery?.data?.releaseYear}</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
<TextTitle
|
|
||||||
fw="900"
|
|
||||||
lh="1"
|
|
||||||
mb="0.12em"
|
|
||||||
mt=".08em"
|
|
||||||
sx={{ fontSize: titleSize }}
|
|
||||||
>
|
|
||||||
{detailQuery?.data?.name}
|
|
||||||
</TextTitle>
|
|
||||||
<Group
|
<Group
|
||||||
spacing="xs"
|
|
||||||
sx={{
|
sx={{
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
|
@ -157,7 +70,6 @@ export const AlbumDetailHeader = forwardRef(
|
||||||
<Fragment key={`artist-${artist.id}`}>
|
<Fragment key={`artist-${artist.id}`}>
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<Text
|
<Text
|
||||||
$noSelect
|
|
||||||
sx={{
|
sx={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
padding: '0 0.5rem',
|
padding: '0 0.5rem',
|
||||||
|
@ -179,8 +91,9 @@ export const AlbumDetailHeader = forwardRef(
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
</MetadataWrapper>
|
</Stack>
|
||||||
</HeaderContainer>
|
</LibraryHeader>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,65 +1,60 @@
|
||||||
import { PageHeader, ScrollArea, TextTitle } from '/@/renderer/components';
|
import { NativeScrollArea } from '/@/renderer/components';
|
||||||
import { AnimatedPage, PlayButton } from '/@/renderer/features/shared';
|
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
|
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useFastAverageColor, useShouldPadTitlebar } from '/@/renderer/hooks';
|
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||||
import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content';
|
import { AlbumDetailContent } from '/@/renderer/features/albums/components/album-detail-content';
|
||||||
import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-detail-header';
|
import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-detail-header';
|
||||||
import { useIntersection } from '@mantine/hooks';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { Group } from '@mantine/core';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
|
import { LibraryItem } from '/@/renderer/types';
|
||||||
|
|
||||||
const AlbumDetailRoute = () => {
|
const AlbumDetailRoute = () => {
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const detailQuery = useAlbumDetail({ id: albumId });
|
const detailQuery = useAlbumDetail({ id: albumId });
|
||||||
const background = useFastAverageColor(detailQuery.data?.imageUrl);
|
const background = useFastAverageColor(detailQuery.data?.imageUrl);
|
||||||
const padTop = useShouldPadTitlebar();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const containerRef = useRef();
|
const handlePlay = () => {
|
||||||
const { ref, entry } = useIntersection({
|
handlePlayQueueAdd?.({
|
||||||
root: containerRef.current,
|
byItemType: {
|
||||||
threshold: 0,
|
id: [albumId],
|
||||||
|
type: LibraryItem.ALBUM,
|
||||||
|
},
|
||||||
|
play: playButtonBehavior,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!background) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`album-detail-${albumId}`}>
|
<AnimatedPage key={`album-detail-${albumId}`}>
|
||||||
<PageHeader
|
<NativeScrollArea
|
||||||
backgroundColor="black"
|
ref={scrollAreaRef}
|
||||||
isHidden={entry?.isIntersecting}
|
pageHeaderProps={{
|
||||||
position="absolute"
|
backgroundColor: background,
|
||||||
|
children: (
|
||||||
|
<LibraryHeaderBar>
|
||||||
|
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||||
|
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
||||||
|
</LibraryHeaderBar>
|
||||||
|
),
|
||||||
|
target: headerRef,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Group p="1rem">
|
|
||||||
<PlayButton
|
|
||||||
h={40}
|
|
||||||
w={40}
|
|
||||||
/>
|
|
||||||
<TextTitle
|
|
||||||
fw="bold"
|
|
||||||
order={2}
|
|
||||||
>
|
|
||||||
{detailQuery?.data?.name}
|
|
||||||
</TextTitle>
|
|
||||||
</Group>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<ScrollArea
|
|
||||||
ref={containerRef}
|
|
||||||
h="100%"
|
|
||||||
offsetScrollbars={false}
|
|
||||||
styles={{ scrollbar: { marginTop: padTop ? '35px' : 0 } }}
|
|
||||||
>
|
|
||||||
{background && (
|
|
||||||
<>
|
|
||||||
<AlbumDetailHeader
|
<AlbumDetailHeader
|
||||||
ref={ref}
|
ref={headerRef}
|
||||||
background={background}
|
background={background}
|
||||||
/>
|
/>
|
||||||
<AlbumDetailContent tableRef={tableRef} />
|
<AlbumDetailContent tableRef={tableRef} />
|
||||||
</>
|
</NativeScrollArea>
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
|
import { forwardRef, Ref } from 'react';
|
||||||
import { RiMoreFill } from 'react-icons/ri';
|
import { RiMoreFill } from 'react-icons/ri';
|
||||||
import { generatePath, useParams } from 'react-router';
|
import { generatePath, useParams } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -16,11 +17,11 @@ interface PlaylistDetailHeaderProps {
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailHeader = ({
|
export const PlaylistDetailHeader = forwardRef(
|
||||||
background,
|
(
|
||||||
imageUrl,
|
{ background, imageUrl, imagePlaceholderUrl }: PlaylistDetailHeaderProps,
|
||||||
imagePlaceholderUrl,
|
ref: Ref<HTMLDivElement>,
|
||||||
}: PlaylistDetailHeaderProps) => {
|
) => {
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
@ -39,6 +40,7 @@ export const PlaylistDetailHeader = ({
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
|
ref={ref}
|
||||||
background={background}
|
background={background}
|
||||||
imagePlaceholderUrl={imagePlaceholderUrl}
|
imagePlaceholderUrl={imagePlaceholderUrl}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
|
@ -89,4 +91,5 @@ export const PlaylistDetailHeader = ({
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -1,41 +1,64 @@
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { PageHeader, ScrollArea } from '/@/renderer/components';
|
import { NativeScrollArea } from '/@/renderer/components';
|
||||||
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { PlaylistDetailContent } from '/@/renderer/features/playlists/components/playlist-detail-content';
|
import { PlaylistDetailContent } from '/@/renderer/features/playlists/components/playlist-detail-content';
|
||||||
import { PlaylistDetailHeader } from '/@/renderer/features/playlists/components/playlist-detail-header';
|
import { PlaylistDetailHeader } from '/@/renderer/features/playlists/components/playlist-detail-header';
|
||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||||
import { useFastAverageColor, useShouldPadTitlebar } from '/@/renderer/hooks';
|
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||||
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
|
import { LibraryItem } from '/@/renderer/types';
|
||||||
|
|
||||||
const PlaylistDetailRoute = () => {
|
const PlaylistDetailRoute = () => {
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const padTitlebar = useShouldPadTitlebar();
|
|
||||||
|
|
||||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||||
const background = useFastAverageColor(detailQuery?.data?.imageUrl, 'dominant');
|
const background = useFastAverageColor(detailQuery?.data?.imageUrl, 'dominant');
|
||||||
|
|
||||||
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
handlePlayQueueAdd?.({
|
||||||
|
byItemType: {
|
||||||
|
id: [playlistId],
|
||||||
|
type: LibraryItem.PLAYLIST,
|
||||||
|
},
|
||||||
|
play: playButtonBehavior,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!background) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<PageHeader position="absolute" />
|
|
||||||
{background && (
|
|
||||||
<AnimatedPage key={`playlist-detail-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-${playlistId}`}>
|
||||||
<ScrollArea
|
<NativeScrollArea
|
||||||
h="100%"
|
ref={scrollAreaRef}
|
||||||
offsetScrollbars={false}
|
pageHeaderProps={{
|
||||||
styles={{ scrollbar: { marginTop: padTitlebar ? '35px' : 0 } }}
|
backgroundColor: background,
|
||||||
|
children: (
|
||||||
|
<LibraryHeaderBar>
|
||||||
|
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||||
|
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
||||||
|
</LibraryHeaderBar>
|
||||||
|
),
|
||||||
|
target: headerRef,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlaylistDetailHeader
|
<PlaylistDetailHeader
|
||||||
|
ref={headerRef}
|
||||||
background={background}
|
background={background}
|
||||||
imagePlaceholderUrl={detailQuery?.data?.imageUrl}
|
imagePlaceholderUrl={detailQuery?.data?.imageUrl}
|
||||||
imageUrl={detailQuery?.data?.imageUrl}
|
imageUrl={detailQuery?.data?.imageUrl}
|
||||||
/>
|
/>
|
||||||
<PlaylistDetailContent tableRef={tableRef} />
|
<PlaylistDetailContent tableRef={tableRef} />
|
||||||
</ScrollArea>
|
</NativeScrollArea>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Reference in a new issue