mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add new grid carousels
This commit is contained in:
+1
-1
@@ -115,7 +115,6 @@
|
|||||||
"nuqs": "^2.7.1",
|
"nuqs": "^2.7.1",
|
||||||
"overlayscrollbars": "^2.11.1",
|
"overlayscrollbars": "^2.11.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-call": "^1.8.1",
|
"react-call": "^1.8.1",
|
||||||
@@ -167,6 +166,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"i18next-parser": "^9.0.2",
|
"i18next-parser": "^9.0.2",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-packagejson": "^2.5.14",
|
"prettier-plugin-packagejson": "^2.5.14",
|
||||||
"sass-embedded": "^1.89.0",
|
"sass-embedded": "^1.89.0",
|
||||||
|
|||||||
Generated
+3
-3
@@ -173,9 +173,6 @@ importers:
|
|||||||
overlayscrollbars-react:
|
overlayscrollbars-react:
|
||||||
specifier: ^0.5.6
|
specifier: ^0.5.6
|
||||||
version: 0.5.6(overlayscrollbars@2.11.3)(react@19.1.0)
|
version: 0.5.6(overlayscrollbars@2.11.3)(react@19.1.0)
|
||||||
postcss-simple-vars:
|
|
||||||
specifier: ^7.0.1
|
|
||||||
version: 7.0.1(postcss@8.5.3)
|
|
||||||
qs:
|
qs:
|
||||||
specifier: ^6.14.0
|
specifier: ^6.14.0
|
||||||
version: 6.14.0
|
version: 6.14.0
|
||||||
@@ -324,6 +321,9 @@ importers:
|
|||||||
postcss-preset-mantine:
|
postcss-preset-mantine:
|
||||||
specifier: ^1.17.0
|
specifier: ^1.17.0
|
||||||
version: 1.17.0(postcss@8.5.3)
|
version: 1.17.0(postcss@8.5.3)
|
||||||
|
postcss-simple-vars:
|
||||||
|
specifier: ^7.0.1
|
||||||
|
version: 7.0.1(postcss@8.5.3)
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.5.3
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
|
|||||||
+7
-7
@@ -3,13 +3,13 @@ module.exports = {
|
|||||||
'postcss-preset-mantine': {},
|
'postcss-preset-mantine': {},
|
||||||
'postcss-simple-vars': {
|
'postcss-simple-vars': {
|
||||||
variables: {
|
variables: {
|
||||||
'breakpoint-xs': '36em',
|
'mantine-breakpoint-xs': '36em',
|
||||||
'breakpoint-sm': '48em',
|
'mantine-breakpoint-sm': '48em',
|
||||||
'breakpoint-md': '62em',
|
'mantine-breakpoint-md': '62em',
|
||||||
'breakpoint-lg': '75em',
|
'mantine-breakpoint-lg': '75em',
|
||||||
'breakpoint-xl': '88em',
|
'mantine-breakpoint-xl': '88em',
|
||||||
'breakpoint-2xl': '120em',
|
'mantine-breakpoint-2xl': '120em',
|
||||||
'breakpoint-3xl': '160em',
|
'mantine-breakpoint-3xl': '160em',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import type { Variants } from 'motion/react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { AnimatePresence, motion, useMotionValue } from 'motion/react';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import styles from './grid-carousel.module.css';
|
||||||
|
|
||||||
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
|
|
||||||
|
interface Card {
|
||||||
|
content: ReactNode;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GridCarouselProps {
|
||||||
|
cards: Card[];
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
loadNextPage?: () => void;
|
||||||
|
onNextPage: (page: number) => void;
|
||||||
|
onPrevPage: (page: number) => void;
|
||||||
|
rowCount?: number;
|
||||||
|
title?: ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoizedCard = memo(({ content }: { content: ReactNode }) => (
|
||||||
|
<div className={styles.card}>{content}</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
MemoizedCard.displayName = 'MemoizedCard';
|
||||||
|
|
||||||
|
const pageVariants: Variants = {
|
||||||
|
animate: { opacity: 1, transition: { duration: 0.3, ease: 'easeOut' }, x: 0 },
|
||||||
|
exit: (custom: { isNext: boolean }) => ({
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.3, ease: 'easeIn' },
|
||||||
|
x: custom.isNext ? -100 : 100,
|
||||||
|
}),
|
||||||
|
initial: (custom: { isNext: boolean }) => ({ opacity: 0, x: custom.isNext ? 100 : -100 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GridCarousel(props: GridCarouselProps) {
|
||||||
|
const { cards, hasNextPage, loadNextPage, onNextPage, onPrevPage, rowCount = 1, title } = props;
|
||||||
|
const cq = useContainerQuery({
|
||||||
|
lg: 900,
|
||||||
|
md: 600,
|
||||||
|
sm: 360,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState({
|
||||||
|
isNext: false,
|
||||||
|
page: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePrevPage = useCallback(() => {
|
||||||
|
setCurrentPage((prev) => ({
|
||||||
|
isNext: false,
|
||||||
|
page: prev.page > 0 ? prev.page - 1 : 0,
|
||||||
|
}));
|
||||||
|
onPrevPage(currentPage.page);
|
||||||
|
}, [currentPage, onPrevPage]);
|
||||||
|
|
||||||
|
const handleNextPage = useCallback(() => {
|
||||||
|
setCurrentPage((prev) => ({
|
||||||
|
isNext: true,
|
||||||
|
page: prev.page + 1,
|
||||||
|
}));
|
||||||
|
onNextPage(currentPage.page);
|
||||||
|
}, [currentPage, onNextPage]);
|
||||||
|
|
||||||
|
const cardsToShow = getCardsToShow({
|
||||||
|
isLargerThanLg: cq.isLg,
|
||||||
|
isLargerThanMd: cq.isMd,
|
||||||
|
isLargerThanSm: cq.isSm,
|
||||||
|
isLargerThanXl: cq.isXl,
|
||||||
|
isLargerThanXxl: cq.is2xl,
|
||||||
|
isLargerThanXxxl: cq.is3xl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleCards = useMemo(() => {
|
||||||
|
return cards.slice(
|
||||||
|
currentPage.page * cardsToShow * rowCount,
|
||||||
|
(currentPage.page + 1) * cardsToShow * rowCount,
|
||||||
|
);
|
||||||
|
}, [cards, currentPage, cardsToShow, rowCount]);
|
||||||
|
|
||||||
|
const shouldLoadNextPage = visibleCards.length < cardsToShow * rowCount;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldLoadNextPage) {
|
||||||
|
loadNextPage?.();
|
||||||
|
}
|
||||||
|
}, [loadNextPage, shouldLoadNextPage]);
|
||||||
|
|
||||||
|
const isPrevDisabled = currentPage.page === 0;
|
||||||
|
const hasMoreCards = (currentPage.page + 1) * cardsToShow * rowCount < cards.length;
|
||||||
|
const isNextDisabled = !hasMoreCards && (hasNextPage === false || hasNextPage === undefined);
|
||||||
|
|
||||||
|
const indicatorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const x = useMotionValue(0);
|
||||||
|
const dragThreshold = 1;
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
const dragDistance = x.get();
|
||||||
|
|
||||||
|
if (Math.abs(dragDistance) > dragThreshold) {
|
||||||
|
if (dragDistance > 0 && !isPrevDisabled) {
|
||||||
|
// Dragged right, go to previous page
|
||||||
|
handlePrevPage();
|
||||||
|
} else if (dragDistance < 0 && !isNextDisabled) {
|
||||||
|
// Dragged left, go to next page
|
||||||
|
handleNextPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x.set(0);
|
||||||
|
}, [handleNextPage, handlePrevPage, isNextDisabled, isPrevDisabled, x]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.gridCarousel} ref={cq.ref}>
|
||||||
|
{cq.isCalculated && (
|
||||||
|
<>
|
||||||
|
<div className={styles.navigation}>
|
||||||
|
{typeof title === 'string' ? (
|
||||||
|
<TextTitle order={4}>{title}</TextTitle>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
<Group gap="xs" justify="end">
|
||||||
|
<ActionIcon
|
||||||
|
disabled={isPrevDisabled}
|
||||||
|
icon="arrowLeftS"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={handlePrevPage}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={isNextDisabled}
|
||||||
|
icon="arrowRightS"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={handleNextPage}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
||||||
|
<motion.div
|
||||||
|
animate="animate"
|
||||||
|
className={styles.grid}
|
||||||
|
custom={currentPage}
|
||||||
|
exit="exit"
|
||||||
|
initial="initial"
|
||||||
|
key={currentPage.page}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
'--cards-to-show': cardsToShow,
|
||||||
|
'--row-count': rowCount,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
variants={pageVariants}
|
||||||
|
>
|
||||||
|
{visibleCards.map((card) => (
|
||||||
|
<MemoizedCard content={card.content} key={card.id} />
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
className={styles.pageIndicator}
|
||||||
|
drag="x"
|
||||||
|
dragConstraints={{ left: -20, right: 20 }}
|
||||||
|
dragElastic={0.3}
|
||||||
|
dragSnapToOrigin={true}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
ref={indicatorRef}
|
||||||
|
style={{ x }}
|
||||||
|
>
|
||||||
|
<motion.div className={styles.indicatorTrack} />
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardsToShow(breakpoints: {
|
||||||
|
isLargerThanLg: boolean;
|
||||||
|
isLargerThanMd: boolean;
|
||||||
|
isLargerThanSm: boolean;
|
||||||
|
isLargerThanXl: boolean;
|
||||||
|
isLargerThanXxl: boolean;
|
||||||
|
isLargerThanXxxl: boolean;
|
||||||
|
}) {
|
||||||
|
if (breakpoints.isLargerThanXxxl) {
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakpoints.isLargerThanXxl) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakpoints.isLargerThanXl) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakpoints.isLargerThanLg) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakpoints.isLargerThanMd) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakpoints.isLargerThanSm) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
.grid-carousel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
container-name: grid-carousel;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
|
height: calc(var(--row-count) * (100cqw / var(--cards-to-show, 2) + 3rem));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--theme-spacing-sm) 0;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-indicator:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-track {
|
||||||
|
width: 20px;
|
||||||
|
height: 4px;
|
||||||
|
touch-action: none;
|
||||||
|
cursor: grab;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: darken(var(--theme-colors-background), 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
background-color: lighten(var(--theme-colors-background), 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-track:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
import throttle from 'lodash/throttle';
|
|
||||||
import {
|
|
||||||
isValidElement,
|
|
||||||
memo,
|
|
||||||
ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { SwiperOptions, Virtual } from 'swiper';
|
|
||||||
import 'swiper/css';
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
||||||
import { Swiper as SwiperCore } from 'swiper/types';
|
|
||||||
|
|
||||||
import { PosterCard } from '/@/renderer/components/card/poster-card';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
|
||||||
import { Group } from '/@/shared/components/group/group';
|
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
|
||||||
import {
|
|
||||||
Album,
|
|
||||||
AlbumArtist,
|
|
||||||
Artist,
|
|
||||||
LibraryItem,
|
|
||||||
RelatedArtist,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { CardRoute, CardRow } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const getSlidesPerView = (windowWidth: number) => {
|
|
||||||
if (windowWidth < 400) return 2;
|
|
||||||
if (windowWidth < 700) return 3;
|
|
||||||
if (windowWidth < 900) return 4;
|
|
||||||
if (windowWidth < 1100) return 5;
|
|
||||||
if (windowWidth < 1300) return 6;
|
|
||||||
if (windowWidth < 1500) return 7;
|
|
||||||
if (windowWidth < 1920) return 8;
|
|
||||||
return 10;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface TitleProps {
|
|
||||||
handleNext?: () => void;
|
|
||||||
handlePrev?: () => void;
|
|
||||||
label?: ReactNode | string;
|
|
||||||
pagination: {
|
|
||||||
hasNextPage: boolean;
|
|
||||||
hasPreviousPage: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
|
|
||||||
return (
|
|
||||||
<Group justify="space-between">
|
|
||||||
{isValidElement(label) ? (
|
|
||||||
label
|
|
||||||
) : (
|
|
||||||
<TextTitle order={3} weight={700}>
|
|
||||||
{label}
|
|
||||||
</TextTitle>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Group gap="sm">
|
|
||||||
<Button
|
|
||||||
disabled={!pagination.hasPreviousPage}
|
|
||||||
onClick={handlePrev}
|
|
||||||
size="compact-md"
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
<Icon icon="arrowLeftS" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={!pagination.hasNextPage}
|
|
||||||
onClick={handleNext}
|
|
||||||
size="compact-md"
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
<Icon icon="arrowRightS" />
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SwiperGridCarouselProps {
|
|
||||||
cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
|
||||||
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
|
|
||||||
isLoading?: boolean;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
route: CardRoute;
|
|
||||||
swiperProps?: SwiperOptions;
|
|
||||||
title?: {
|
|
||||||
children?: ReactNode;
|
|
||||||
hasPagination?: boolean;
|
|
||||||
icon?: ReactNode;
|
|
||||||
label: ReactNode | string;
|
|
||||||
};
|
|
||||||
uniqueId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SwiperGridCarousel = ({
|
|
||||||
cardRows,
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
itemType,
|
|
||||||
route,
|
|
||||||
swiperProps,
|
|
||||||
title,
|
|
||||||
uniqueId,
|
|
||||||
}: SwiperGridCarouselProps) => {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const swiperRef = useRef<any | SwiperCore>(null);
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
const [slideCount, setSlideCount] = useState(4);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
swiperRef.current?.slideTo(0, 0);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
hasNextPage: (data?.length || 0) > Math.round(3),
|
|
||||||
hasPreviousPage: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const createFavoriteMutation = useCreateFavorite({});
|
|
||||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
|
||||||
|
|
||||||
const handleFavorite = useCallback(
|
|
||||||
(options: {
|
|
||||||
id: string[];
|
|
||||||
isFavorite: boolean;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
serverId: string;
|
|
||||||
}) => {
|
|
||||||
const { id, isFavorite, itemType, serverId } = options;
|
|
||||||
if (isFavorite) {
|
|
||||||
deleteFavoriteMutation.mutate({
|
|
||||||
apiClientProps: { serverId },
|
|
||||||
query: {
|
|
||||||
id,
|
|
||||||
type: itemType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createFavoriteMutation.mutate({
|
|
||||||
apiClientProps: { serverId },
|
|
||||||
query: {
|
|
||||||
id,
|
|
||||||
type: itemType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[createFavoriteMutation, deleteFavoriteMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const slides = useMemo(() => {
|
|
||||||
if (!data) return [];
|
|
||||||
|
|
||||||
return data.map((el) => (
|
|
||||||
<PosterCard
|
|
||||||
controls={{
|
|
||||||
cardRows,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
itemType,
|
|
||||||
playButtonBehavior,
|
|
||||||
route,
|
|
||||||
}}
|
|
||||||
data={el}
|
|
||||||
isLoading={isLoading}
|
|
||||||
key={`${uniqueId}-${el.id}`}
|
|
||||||
uniqueId={uniqueId}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}, [
|
|
||||||
cardRows,
|
|
||||||
data,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
isLoading,
|
|
||||||
itemType,
|
|
||||||
playButtonBehavior,
|
|
||||||
route,
|
|
||||||
uniqueId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
|
||||||
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
|
||||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
|
|
||||||
swiperRef?.current?.slideTo(activeIndex + slidesPerView);
|
|
||||||
}, [slideCount, swiperProps?.slidesPerView]);
|
|
||||||
|
|
||||||
const handlePrev = useCallback(() => {
|
|
||||||
const activeIndex = swiperRef?.current?.activeIndex || 0;
|
|
||||||
const slidesPerView = Math.round(Number(swiperProps?.slidesPerView || slideCount));
|
|
||||||
swiperRef?.current?.slideTo(activeIndex - slidesPerView);
|
|
||||||
}, [slideCount, swiperProps?.slidesPerView]);
|
|
||||||
|
|
||||||
const handleOnSlideChange = useCallback((e: SwiperCore) => {
|
|
||||||
const { isBeginning, isEnd, params, slides } = e;
|
|
||||||
if (isEnd || isBeginning) return;
|
|
||||||
|
|
||||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
|
||||||
setPagination({
|
|
||||||
hasNextPage: slideCount < slides.length,
|
|
||||||
hasPreviousPage: slideCount < slides.length,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnZoomChange = useCallback((e: SwiperCore) => {
|
|
||||||
const { isBeginning, isEnd, params, slides } = e;
|
|
||||||
if (isEnd || isBeginning) return;
|
|
||||||
|
|
||||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
|
||||||
setPagination({
|
|
||||||
hasNextPage: slideCount < slides.length,
|
|
||||||
hasPreviousPage: slideCount < slides.length,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnReachEnd = useCallback((e: SwiperCore) => {
|
|
||||||
const { params, slides } = e;
|
|
||||||
|
|
||||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
|
||||||
setPagination({
|
|
||||||
hasNextPage: false,
|
|
||||||
hasPreviousPage: slideCount < slides.length,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
|
|
||||||
const { params, slides } = e;
|
|
||||||
|
|
||||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
|
||||||
setPagination({
|
|
||||||
hasNextPage: slideCount < slides.length,
|
|
||||||
hasPreviousPage: false,
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
// Use the container div ref and not swiper width, as this value is more accurate
|
|
||||||
const width = containerRef.current?.clientWidth;
|
|
||||||
const { activeIndex, params, slides } =
|
|
||||||
(swiperRef.current as SwiperCore | undefined) ?? {};
|
|
||||||
|
|
||||||
if (width) {
|
|
||||||
const slidesPerView = getSlidesPerView(width);
|
|
||||||
setSlideCount(slidesPerView);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeIndex !== undefined && slides && params?.slidesPerView) {
|
|
||||||
const slideCount = (params.slidesPerView as number | undefined) || 4;
|
|
||||||
setPagination({
|
|
||||||
hasNextPage: activeIndex + slideCount < slides.length,
|
|
||||||
hasPreviousPage: activeIndex > 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleResize();
|
|
||||||
|
|
||||||
const throttledResize = throttle(handleResize, 200);
|
|
||||||
window.addEventListener('resize', throttledResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', throttledResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack className="grid-carousel" gap="md" ref={containerRef as any}>
|
|
||||||
{title ? (
|
|
||||||
<Title
|
|
||||||
{...title}
|
|
||||||
handleNext={handleNext}
|
|
||||||
handlePrev={handlePrev}
|
|
||||||
pagination={pagination}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<Swiper
|
|
||||||
modules={[Virtual]}
|
|
||||||
onBeforeInit={(swiper) => {
|
|
||||||
swiperRef.current = swiper;
|
|
||||||
}}
|
|
||||||
onReachBeginning={handleOnReachBeginning}
|
|
||||||
onReachEnd={handleOnReachEnd}
|
|
||||||
onSlideChange={handleOnSlideChange}
|
|
||||||
onZoomChange={handleOnZoomChange}
|
|
||||||
ref={swiperRef}
|
|
||||||
resizeObserver
|
|
||||||
slidesPerView={slideCount}
|
|
||||||
spaceBetween={20}
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
{...swiperProps}
|
|
||||||
>
|
|
||||||
{slides.map((slideContent, index) => {
|
|
||||||
return (
|
|
||||||
<SwiperSlide
|
|
||||||
key={`${uniqueId}-${slideContent?.props?.data?.id}-${index}`}
|
|
||||||
virtualIndex={index}
|
|
||||||
>
|
|
||||||
{slideContent}
|
|
||||||
</SwiperSlide>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Swiper>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MemoizedSwiperGridCarousel = memo(
|
|
||||||
function Carousel(props: SwiperGridCarouselProps) {
|
|
||||||
return <SwiperGridCarousel {...props} />;
|
|
||||||
},
|
|
||||||
(oldProps, newProps) => {
|
|
||||||
const uniqueIdIsEqual = oldProps.uniqueId === newProps.uniqueId;
|
|
||||||
const dataIsEqual = oldProps.data === newProps.data;
|
|
||||||
return uniqueIdIsEqual && dataIsEqual;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -460,12 +460,14 @@ const PosterItemCard = ({
|
|||||||
},
|
},
|
||||||
itemType,
|
itemType,
|
||||||
onDragStart: () => {
|
onDragStart: () => {
|
||||||
if (!data || !internalState) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const draggedItems = getDraggedItems(data, internalState);
|
const draggedItems = getDraggedItems(data, internalState);
|
||||||
internalState.setDragging(draggedItems);
|
if (internalState) {
|
||||||
|
internalState.setDragging(draggedItems);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDrop: () => {
|
onDrop: () => {
|
||||||
if (internalState) {
|
if (internalState) {
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ const hasRequiredDragProperties = (
|
|||||||
* Gets the items that should be dragged based on the current data and selection state.
|
* Gets the items that should be dragged based on the current data and selection state.
|
||||||
* If the current item is already selected, drag all selected items.
|
* If the current item is already selected, drag all selected items.
|
||||||
* Otherwise, select and drag only the current item.
|
* Otherwise, select and drag only the current item.
|
||||||
|
* If internalState is not provided, returns the single item wrapped in an array.
|
||||||
*
|
*
|
||||||
* @param data - The item data to drag (Album, AlbumArtist, Artist, Playlist, or Song)
|
* @param data - The item data to drag (Album, AlbumArtist, Artist, Playlist, or Song)
|
||||||
* @param itemType - The type of library item
|
|
||||||
* @param internalState - The item list state actions (optional)
|
* @param internalState - The item list state actions (optional)
|
||||||
* @param updateSelection - Whether to update the selection state (default: true)
|
* @param updateSelection - Whether to update the selection state (default: true)
|
||||||
* @returns Array of items that should be dragged (with original values, asserting id, itemType, and _serverId)
|
* @returns Array of items that should be dragged (with original values, asserting id, itemType, and _serverId)
|
||||||
@@ -38,7 +38,7 @@ export const getDraggedItems = (
|
|||||||
internalState?: ItemListStateActions,
|
internalState?: ItemListStateActions,
|
||||||
updateSelection: boolean = true,
|
updateSelection: boolean = true,
|
||||||
): ItemListStateItemWithRequiredProperties[] => {
|
): ItemListStateItemWithRequiredProperties[] => {
|
||||||
if (!data || !internalState) {
|
if (!data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,14 +46,18 @@ export const getDraggedItems = (
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draggedItem = data as ItemListStateItemWithRequiredProperties;
|
||||||
|
|
||||||
|
if (!internalState) {
|
||||||
|
return [draggedItem];
|
||||||
|
}
|
||||||
|
|
||||||
const rowId = internalState.extractRowId(data);
|
const rowId = internalState.extractRowId(data);
|
||||||
|
|
||||||
if (!rowId) {
|
if (!rowId) {
|
||||||
return [];
|
return [draggedItem];
|
||||||
}
|
}
|
||||||
|
|
||||||
const draggedItem = data as ItemListStateItemWithRequiredProperties;
|
|
||||||
|
|
||||||
const previouslySelected = internalState.getSelected();
|
const previouslySelected = internalState.getSelected();
|
||||||
const isDraggingSelectedItem = previouslySelected.some((selected) => {
|
const isDraggingSelectedItem = previouslySelected.some((selected) => {
|
||||||
if (hasRequiredDragProperties(selected)) {
|
if (hasRequiredDragProperties(selected)) {
|
||||||
|
|||||||
@@ -102,13 +102,14 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
|
|||||||
},
|
},
|
||||||
itemType: props.itemType,
|
itemType: props.itemType,
|
||||||
onDragStart: () => {
|
onDragStart: () => {
|
||||||
if (!item || !isDataRow || !props.internalState) {
|
if (!item || !isDataRow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const draggedItems = getDraggedItems(item as any, props.internalState);
|
const draggedItems = getDraggedItems(item as any, props.internalState);
|
||||||
|
if (props.internalState) {
|
||||||
props.internalState.setDragging(draggedItems);
|
props.internalState.setDragging(draggedItems);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDrop: () => {
|
onDrop: () => {
|
||||||
if (props.internalState) {
|
if (props.internalState) {
|
||||||
|
|||||||
@@ -1,75 +1,33 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
|
|
||||||
import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
|
|
||||||
import { useSetState } from '@mantine/hooks';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, useParams } from 'react-router';
|
import { generatePath, Link, useParams } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
|
||||||
|
|
||||||
import styles from './album-detail-content.module.css';
|
import styles from './album-detail-content.module.css';
|
||||||
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
|
||||||
import {
|
|
||||||
getColumnDefs,
|
|
||||||
TableConfigDropdown,
|
|
||||||
VirtualTable,
|
|
||||||
} from '/@/renderer/components/virtual-table';
|
|
||||||
import { FullWidthDiscCell } from '/@/renderer/components/virtual-table/cells/full-width-disc-cell';
|
|
||||||
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import {
|
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
|
||||||
ALBUM_CONTEXT_MENU_ITEMS,
|
|
||||||
SONG_CONTEXT_MENU_ITEMS,
|
|
||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import {
|
|
||||||
useHandleGeneralContextMenu,
|
|
||||||
useHandleTableContextMenu,
|
|
||||||
} from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
|
||||||
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
||||||
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
|
||||||
import { useAppFocus, useContainerQuery } from '/@/renderer/hooks';
|
|
||||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { useCurrentServer, usePlayerSong, usePlayerStatus } from '/@/renderer/store';
|
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import {
|
|
||||||
PersistedTableColumn,
|
|
||||||
useGeneralSettings,
|
|
||||||
usePlayButtonBehavior,
|
|
||||||
useSettingsStoreActions,
|
|
||||||
useTableSettings,
|
|
||||||
} from '/@/renderer/store/settings.store';
|
|
||||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Popover } from '/@/shared/components/popover/popover';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import {
|
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
AlbumListQuery,
|
|
||||||
AlbumListSort,
|
|
||||||
LibraryItem,
|
|
||||||
QueueSong,
|
|
||||||
SortOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
const isFullWidthRow = (node: RowNode) => {
|
|
||||||
return node.id?.startsWith('disc-');
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AlbumDetailContentProps {
|
interface AlbumDetailContentProps {
|
||||||
background?: string;
|
background?: string;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentProps) => {
|
export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { albumId } = useParams() as { albumId: string };
|
const { albumId } = useParams() as { albumId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
@@ -82,267 +40,59 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||||||
);
|
);
|
||||||
|
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
const tableConfig = useTableSettings('albumDetail');
|
|
||||||
const { setTable } = useSettingsStoreActions();
|
|
||||||
const status = usePlayerStatus();
|
|
||||||
const isFocused = useAppFocus();
|
|
||||||
const currentSong = usePlayerSong();
|
|
||||||
const { externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
const { externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
||||||
const genreRoute = useGenreRoute();
|
const genreRoute = useGenreRoute();
|
||||||
|
|
||||||
const columnDefs = useMemo(
|
const carousels = useMemo(
|
||||||
() => getColumnDefs(tableConfig.columns, false, 'albumDetail'),
|
() => [
|
||||||
[tableConfig.columns],
|
{
|
||||||
);
|
excludeIds: detail?.id ? [detail.id] : undefined,
|
||||||
|
isHidden: !detail?.albumArtists?.[0]?.id,
|
||||||
const getRowHeight = useCallback(
|
query: {
|
||||||
(params: RowHeightParams) => {
|
_custom: {
|
||||||
if (isFullWidthRow(params.node)) {
|
jellyfin: {
|
||||||
return 45;
|
ExcludeItemIds: detail?.id,
|
||||||
}
|
},
|
||||||
|
|
||||||
return tableConfig.rowHeight;
|
|
||||||
},
|
|
||||||
[tableConfig.rowHeight],
|
|
||||||
);
|
|
||||||
|
|
||||||
const songsRowData = useMemo(() => {
|
|
||||||
if (!detail?.songs) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let discNumber = -1;
|
|
||||||
let discSubtitle: null | string = null;
|
|
||||||
|
|
||||||
const rowData: (QueueSong | { id: string; name: string })[] = [];
|
|
||||||
const discTranslated = t('common.disc', { postProcess: 'upperCase' });
|
|
||||||
|
|
||||||
for (const song of detail.songs) {
|
|
||||||
if (song.discNumber !== discNumber || song.discSubtitle !== discSubtitle) {
|
|
||||||
discNumber = song.discNumber;
|
|
||||||
discSubtitle = song.discSubtitle;
|
|
||||||
|
|
||||||
let id = `disc-${discNumber}`;
|
|
||||||
let name = `${discTranslated} ${discNumber}`;
|
|
||||||
|
|
||||||
if (discSubtitle) {
|
|
||||||
id += `-${discSubtitle}`;
|
|
||||||
name += `: ${discSubtitle}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
rowData.push({ id, name });
|
|
||||||
}
|
|
||||||
rowData.push(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rowData;
|
|
||||||
}, [detail?.songs, t]);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useSetState({
|
|
||||||
artist: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleNextPage = useCallback(
|
|
||||||
(key: 'artist') => {
|
|
||||||
setPagination({
|
|
||||||
[key]: pagination[key as keyof typeof pagination] + 1,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[pagination, setPagination],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePreviousPage = useCallback(
|
|
||||||
(key: 'artist') => {
|
|
||||||
setPagination({
|
|
||||||
[key]: pagination[key as keyof typeof pagination] - 1,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[pagination, setPagination],
|
|
||||||
);
|
|
||||||
|
|
||||||
const artistQuery = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
query: {
|
|
||||||
_custom: {
|
|
||||||
jellyfin: {
|
|
||||||
ExcludeItemIds: detail?.id,
|
|
||||||
},
|
},
|
||||||
|
artistIds: detail?.albumArtists.length
|
||||||
|
? [detail.albumArtists[0].id]
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
artistIds: detail?.albumArtists.length ? [detail?.albumArtists[0].id] : undefined,
|
|
||||||
limit: 15,
|
|
||||||
sortBy: AlbumListSort.YEAR,
|
sortBy: AlbumListSort.YEAR,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
startIndex: 0,
|
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
|
||||||
|
uniqueId: 'moreFromArtist',
|
||||||
},
|
},
|
||||||
serverId: server.id,
|
{
|
||||||
}),
|
excludeIds: detail?.id ? [detail.id] : undefined,
|
||||||
|
isHidden: !detailQuery?.data?.genres?.[0],
|
||||||
|
query: {
|
||||||
|
genres: detailQuery.data?.genres.length
|
||||||
|
? [detailQuery.data.genres[0].id]
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
title: `${t('page.albumDetail.moreFromGeneric', {
|
||||||
|
item: '',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})} ${detailQuery?.data?.genres?.[0]?.name}`,
|
||||||
|
uniqueId: 'relatedGenres',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[detail?.id, detail?.albumArtists, detailQuery?.data?.genres, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
// const artistQuery = useAlbumList({
|
|
||||||
// options: {
|
|
||||||
// enabled: detail?.albumArtists[0]?.id !== undefined,
|
|
||||||
// gcTime: 1000 * 60,
|
|
||||||
// placeholderData: true,
|
|
||||||
// },
|
|
||||||
// query: {
|
|
||||||
// _custom: {
|
|
||||||
// jellyfin: {
|
|
||||||
// ExcludeItemIds: detailQuery?.data?.id,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// artistIds: detailQuery?.data?.albumArtists.length
|
|
||||||
// ? [detailQuery?.data?.albumArtists[0].id]
|
|
||||||
// : undefined,
|
|
||||||
// limit: 15,
|
|
||||||
// sortBy: AlbumListSort.YEAR,
|
|
||||||
// sortOrder: SortOrder.DESC,
|
|
||||||
// startIndex: 0,
|
|
||||||
// },
|
|
||||||
// serverId: server?.id,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const relatedAlbumGenresRequest: AlbumListQuery = {
|
|
||||||
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
|
|
||||||
limit: 15,
|
|
||||||
sortBy: AlbumListSort.RANDOM,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
startIndex: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const relatedAlbumGenresQuery = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
options: {
|
|
||||||
enabled: !!detailQuery?.data?.genres?.[0],
|
|
||||||
gcTime: 1000 * 60,
|
|
||||||
queryKey: queryKeys.albums.related(
|
|
||||||
server?.id || '',
|
|
||||||
albumId,
|
|
||||||
relatedAlbumGenresRequest,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
query: relatedAlbumGenresRequest,
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const carousels = [
|
|
||||||
{
|
|
||||||
data: artistQuery?.data?.items.filter((a) => a.id !== detailQuery?.data?.id),
|
|
||||||
isHidden: !artistQuery?.data?.items.filter((a) => a.id !== detailQuery?.data?.id)
|
|
||||||
.length,
|
|
||||||
loading: artistQuery?.isLoading || artistQuery.isFetching,
|
|
||||||
pagination: {
|
|
||||||
handleNextPage: () => handleNextPage('artist'),
|
|
||||||
handlePreviousPage: () => handlePreviousPage('artist'),
|
|
||||||
hasPreviousPage: pagination.artist > 0,
|
|
||||||
},
|
|
||||||
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
|
|
||||||
uniqueId: 'mostPlayed',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: relatedAlbumGenresQuery?.data?.items.filter(
|
|
||||||
(a) => a.id !== detailQuery?.data?.id,
|
|
||||||
),
|
|
||||||
isHidden: !relatedAlbumGenresQuery?.data?.items.filter(
|
|
||||||
(a) => a.id !== detailQuery?.data?.id,
|
|
||||||
).length,
|
|
||||||
loading: relatedAlbumGenresQuery?.isLoading || relatedAlbumGenresQuery.isFetching,
|
|
||||||
title: `${t('page.albumDetail.moreFromGeneric', {
|
|
||||||
item: '',
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
})} ${detailQuery?.data?.genres?.[0]?.name}`,
|
|
||||||
uniqueId: 'relatedGenres',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const handlePlay = async (playType?: Play) => {
|
const handlePlay = async (playType?: Play) => {};
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byData: detailQuery?.data?.songs,
|
|
||||||
playType: playType || playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCellContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
|
||||||
|
|
||||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
|
||||||
if (!e.data || e.node.isFullWidthCell()) return;
|
|
||||||
|
|
||||||
const rowData: QueueSong[] = [];
|
|
||||||
e.api.forEachNode((node) => {
|
|
||||||
if (!node.data || node.isFullWidthCell()) return;
|
|
||||||
rowData.push(node.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byData: rowData,
|
|
||||||
initialSongId: e.data.id,
|
|
||||||
playType: playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFavoriteMutation = useCreateFavorite({});
|
|
||||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
|
||||||
|
|
||||||
const handleFavorite = () => {
|
const handleFavorite = () => {
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery?.data) return;
|
||||||
|
|
||||||
if (detailQuery.data.userFavorite) {
|
|
||||||
deleteFavoriteMutation.mutate({
|
|
||||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
|
||||||
query: {
|
|
||||||
id: [detailQuery.data.id],
|
|
||||||
type: LibraryItem.ALBUM,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createFavoriteMutation.mutate({
|
|
||||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
|
||||||
query: {
|
|
||||||
id: [detailQuery.data.id],
|
|
||||||
type: LibraryItem.ALBUM,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||||
const comment = detailQuery?.data?.comment;
|
const comment = detailQuery?.data?.comment;
|
||||||
|
|
||||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
|
||||||
LibraryItem.ALBUM,
|
|
||||||
ALBUM_CONTEXT_MENU_ITEMS,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onColumnMoved = useCallback(() => {
|
|
||||||
const { columnApi } = tableRef?.current || {};
|
|
||||||
const columnsOrder = columnApi?.getAllGridColumns();
|
|
||||||
|
|
||||||
if (!columnsOrder) return;
|
|
||||||
|
|
||||||
const columnsInSettings = tableConfig.columns;
|
|
||||||
const updatedColumns: PersistedTableColumn[] = [];
|
|
||||||
for (const column of columnsOrder) {
|
|
||||||
const columnInSettings = columnsInSettings.find(
|
|
||||||
(c) => c.column === column.getColDef().colId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (columnInSettings) {
|
|
||||||
updatedColumns.push({
|
|
||||||
...columnInSettings,
|
|
||||||
...(!tableConfig.autoFit && {
|
|
||||||
width: column.getActualWidth(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTable('albumDetail', { ...tableConfig, columns: updatedColumns });
|
|
||||||
}, [setTable, tableConfig, tableRef]);
|
|
||||||
|
|
||||||
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
|
||||||
|
|
||||||
const mbzId = detailQuery?.data?.mbzId;
|
const mbzId = detailQuery?.data?.mbzId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -361,10 +111,6 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||||||
? 'primary'
|
? 'primary'
|
||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
loading={
|
|
||||||
createFavoriteMutation.isPending ||
|
|
||||||
deleteFavoriteMutation.isPending
|
|
||||||
}
|
|
||||||
onClick={handleFavorite}
|
onClick={handleFavorite}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
@@ -373,29 +119,12 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||||||
icon="ellipsisHorizontal"
|
icon="ellipsisHorizontal"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery?.data) return;
|
||||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Popover position="bottom-end">
|
|
||||||
<Popover.Target>
|
|
||||||
<ActionIcon
|
|
||||||
icon="settings"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (!detailQuery?.data) return;
|
|
||||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
variant="transparent"
|
|
||||||
/>
|
|
||||||
</Popover.Target>
|
|
||||||
<Popover.Dropdown>
|
|
||||||
<TableConfigDropdown type="albumDetail" />
|
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
</Group>
|
</Group>
|
||||||
</section>
|
</section>
|
||||||
{showGenres && (
|
{showGenres && (
|
||||||
@@ -468,91 +197,26 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||||||
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
|
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<div style={{ minHeight: '300px' }}>
|
|
||||||
<VirtualTable
|
|
||||||
autoFitColumns={tableConfig.autoFit}
|
|
||||||
autoHeight
|
|
||||||
columnDefs={columnDefs}
|
|
||||||
context={{
|
|
||||||
currentSong,
|
|
||||||
isFocused,
|
|
||||||
itemType: LibraryItem.SONG,
|
|
||||||
onCellContextMenu,
|
|
||||||
status,
|
|
||||||
}}
|
|
||||||
enableCellChangeFlash={false}
|
|
||||||
fullWidthCellRenderer={FullWidthDiscCell}
|
|
||||||
getRowHeight={getRowHeight}
|
|
||||||
getRowId={(data) => data.data.id}
|
|
||||||
isFullWidthRow={(data) => {
|
|
||||||
return isFullWidthRow(data.rowNode) || false;
|
|
||||||
}}
|
|
||||||
isRowSelectable={(data) => {
|
|
||||||
if (isFullWidthRow(data.data)) return false;
|
|
||||||
return true;
|
|
||||||
}}
|
|
||||||
key={`table-${tableConfig.rowHeight}`}
|
|
||||||
onCellContextMenu={onCellContextMenu}
|
|
||||||
onColumnMoved={onColumnMoved}
|
|
||||||
onRowDoubleClicked={handleRowDoubleClick}
|
|
||||||
ref={tableRef}
|
|
||||||
rowClassRules={rowClassRules}
|
|
||||||
rowData={songsRowData}
|
|
||||||
rowSelection="multiple"
|
|
||||||
shouldUpdateSong
|
|
||||||
stickyHeader
|
|
||||||
suppressCellFocus
|
|
||||||
suppressLoadingOverlay
|
|
||||||
suppressRowDrag
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Stack gap="lg" mt="3rem" ref={cq.ref}>
|
<Stack gap="lg" mt="3rem" ref={cq.ref}>
|
||||||
{cq.height || cq.width ? (
|
{cq.height || cq.width ? (
|
||||||
<>
|
<>
|
||||||
{carousels
|
{carousels
|
||||||
.filter((c) => !c.isHidden)
|
.filter((c) => !c.isHidden)
|
||||||
.map((carousel, index) => (
|
.map((carousel) => (
|
||||||
<MemoizedSwiperGridCarousel
|
<Suspense
|
||||||
cardRows={[
|
fallback={<Spinner container />}
|
||||||
{
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
property: 'name',
|
>
|
||||||
route: {
|
<AlbumInfiniteCarousel
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
excludeIds={carousel.excludeIds}
|
||||||
slugs: [
|
query={carousel.query}
|
||||||
{
|
rowCount={1}
|
||||||
idProperty: 'id',
|
sortBy={carousel.sortBy}
|
||||||
slugProperty: 'albumId',
|
sortOrder={carousel.sortOrder}
|
||||||
},
|
title={carousel.title}
|
||||||
],
|
/>
|
||||||
},
|
</Suspense>
|
||||||
},
|
|
||||||
{
|
|
||||||
arrayProperty: 'name',
|
|
||||||
property: 'albumArtists',
|
|
||||||
route: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
|
||||||
slugs: [
|
|
||||||
{
|
|
||||||
idProperty: 'id',
|
|
||||||
slugProperty: 'albumArtistId',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={carousel.data}
|
|
||||||
isLoading={carousel.loading}
|
|
||||||
itemType={LibraryItem.ALBUM}
|
|
||||||
key={`carousel-${carousel.uniqueId}-${index}`}
|
|
||||||
route={{
|
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
||||||
}}
|
|
||||||
title={{
|
|
||||||
label: carousel.title,
|
|
||||||
}}
|
|
||||||
uniqueId={carousel.uniqueId}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
||||||
|
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
|
import { Album, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface AlbumGridCarouselProps {
|
||||||
|
data: Album[];
|
||||||
|
excludeIds?: string[];
|
||||||
|
rowCount?: number;
|
||||||
|
title: React.ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlbumGridCarousel(props: AlbumGridCarouselProps) {
|
||||||
|
const { data, excludeIds, rowCount = 1, title } = props;
|
||||||
|
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||||
|
const controls = useDefaultItemListControls();
|
||||||
|
|
||||||
|
const cards = useMemo(() => {
|
||||||
|
// Filter out excluded IDs if provided
|
||||||
|
const filteredItems = excludeIds
|
||||||
|
? data.filter((album) => !excludeIds.includes(album.id))
|
||||||
|
: data;
|
||||||
|
|
||||||
|
return filteredItems.map((album: Album) => ({
|
||||||
|
content: (
|
||||||
|
<MemoizedItemCard
|
||||||
|
controls={controls}
|
||||||
|
data={album}
|
||||||
|
enableDrag
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
rows={rows}
|
||||||
|
type="poster"
|
||||||
|
withControls
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: album.id,
|
||||||
|
}));
|
||||||
|
}, [data, excludeIds, controls, rows]);
|
||||||
|
|
||||||
|
const handleNextPage = () => {};
|
||||||
|
const handlePrevPage = () => {};
|
||||||
|
|
||||||
|
if (cards.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCarousel
|
||||||
|
cards={cards}
|
||||||
|
onNextPage={handleNextPage}
|
||||||
|
onPrevPage={handlePrevPage}
|
||||||
|
rowCount={rowCount}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
||||||
|
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
|
import {
|
||||||
|
Album,
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListResponse,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface AlbumCarouselProps {
|
||||||
|
excludeIds?: string[];
|
||||||
|
query?: Partial<Omit<AlbumListQuery, 'startIndex'>>;
|
||||||
|
rowCount?: number;
|
||||||
|
sortBy: AlbumListSort;
|
||||||
|
sortOrder: SortOrder;
|
||||||
|
title: React.ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlbumInfiniteCarousel(props: AlbumCarouselProps) {
|
||||||
|
const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props;
|
||||||
|
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||||
|
const {
|
||||||
|
data: albums,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
} = useAlbumListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||||
|
|
||||||
|
const controls = useDefaultItemListControls();
|
||||||
|
|
||||||
|
const cards = useMemo(() => {
|
||||||
|
// Flatten all pages and filter excluded IDs
|
||||||
|
const allItems = albums.pages.flatMap((page: AlbumListResponse) => page.items);
|
||||||
|
const filteredItems = excludeIds
|
||||||
|
? allItems.filter((album) => !excludeIds.includes(album.id))
|
||||||
|
: allItems;
|
||||||
|
|
||||||
|
return filteredItems.map((album: Album) => ({
|
||||||
|
content: (
|
||||||
|
<MemoizedItemCard
|
||||||
|
controls={controls}
|
||||||
|
data={album}
|
||||||
|
enableDrag
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
rows={rows}
|
||||||
|
type="poster"
|
||||||
|
withControls
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: album.id,
|
||||||
|
}));
|
||||||
|
}, [albums.pages, controls, excludeIds, rows]);
|
||||||
|
|
||||||
|
const handleNextPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
const handlePrevPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
const firstPageItems = excludeIds
|
||||||
|
? albums.pages[0]?.items.filter((album) => !excludeIds.includes(album.id)) || []
|
||||||
|
: albums.pages[0]?.items || [];
|
||||||
|
|
||||||
|
if (firstPageItems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCarousel
|
||||||
|
cards={cards}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
loadNextPage={fetchNextPage}
|
||||||
|
onNextPage={handleNextPage}
|
||||||
|
onPrevPage={handlePrevPage}
|
||||||
|
rowCount={rowCount}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAlbumListInfinite(
|
||||||
|
sortBy: AlbumListSort,
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
itemLimit: number,
|
||||||
|
additionalQuery?: Partial<Omit<AlbumListQuery, 'startIndex'>>,
|
||||||
|
) {
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
|
const query = useSuspenseInfiniteQuery<AlbumListResponse>({
|
||||||
|
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
||||||
|
if (lastPage.items.length < itemLimit) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPageParam = Number(lastPageParam) + itemLimit;
|
||||||
|
|
||||||
|
return String(nextPageParam);
|
||||||
|
},
|
||||||
|
initialPageParam: '0',
|
||||||
|
queryFn: ({ pageParam, signal }) => {
|
||||||
|
return api.controller.getAlbumList({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: {
|
||||||
|
limit: itemLimit,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
startIndex: Number(pageParam),
|
||||||
|
...additionalQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.albums.list(serverId, {
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
...additionalQuery,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
@@ -8,8 +8,6 @@ import styles from './dummy-album-detail-route.module.css';
|
|||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { SONG_ALBUM_PAGE } from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header';
|
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header';
|
||||||
@@ -97,8 +95,6 @@ const DummyAlbumDetailRoute = () => {
|
|||||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||||
const comment = detailQuery?.data?.comment;
|
const comment = detailQuery?.data?.comment;
|
||||||
|
|
||||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(LibraryItem.SONG, SONG_ALBUM_PAGE);
|
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
byItemType: {
|
byItemType: {
|
||||||
@@ -190,7 +186,6 @@ const DummyAlbumDetailRoute = () => {
|
|||||||
icon="ellipsisHorizontal"
|
icon="ellipsisHorizontal"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery?.data) return;
|
||||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
|
||||||
}}
|
}}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, useParams } from 'react-router';
|
import { createSearchParams, generatePath, Link, useParams } from 'react-router';
|
||||||
import { createSearchParams, Link } from 'react-router';
|
|
||||||
|
|
||||||
import styles from './album-artist-detail-content.module.css';
|
import styles from './album-artist-detail-content.module.css';
|
||||||
|
|
||||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
|
||||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import {
|
import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel';
|
||||||
ARTIST_CONTEXT_MENU_ITEMS,
|
|
||||||
SONG_CONTEXT_MENU_ITEMS,
|
|
||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import {
|
|
||||||
useHandleGeneralContextMenu,
|
|
||||||
useHandleTableContextMenu,
|
|
||||||
} from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||||
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
|
||||||
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||||
@@ -34,11 +24,11 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Grid } from '/@/shared/components/grid/grid';
|
import { Grid } from '/@/shared/components/grid/grid';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
import {
|
import {
|
||||||
Album,
|
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
@@ -46,7 +36,7 @@ import {
|
|||||||
ServerType,
|
ServerType,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { CardRow, Play, TableColumn } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistDetailContentProps {
|
interface AlbumArtistDetailContentProps {
|
||||||
background?: string;
|
background?: string;
|
||||||
@@ -101,40 +91,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
artistName: detailQuery?.data?.name || '',
|
artistName: detailQuery?.data?.name || '',
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
const recentAlbumsQuery = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
options: {
|
|
||||||
enabled: enabledItem.recentAlbums,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
artistIds: [routeId],
|
|
||||||
compilation: false,
|
|
||||||
limit: 15,
|
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const compilationAlbumsQuery = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
options: {
|
|
||||||
enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
artistIds: [routeId],
|
|
||||||
compilation: true,
|
|
||||||
limit: 15,
|
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const topSongsQuery = useQuery(
|
const topSongsQuery = useQuery(
|
||||||
artistsQueries.topSongs({
|
artistsQueries.topSongs({
|
||||||
options: {
|
options: {
|
||||||
@@ -148,68 +104,18 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const topSongsColumnDefs: ColDef[] = useMemo(
|
|
||||||
() =>
|
|
||||||
getColumnDefs([
|
|
||||||
{ column: TableColumn.ROW_INDEX, width: 0 },
|
|
||||||
{ column: TableColumn.TITLE_COMBINED, width: 0 },
|
|
||||||
{ column: TableColumn.DURATION, width: 0 },
|
|
||||||
{ column: TableColumn.ALBUM, width: 0 },
|
|
||||||
{ column: TableColumn.YEAR, width: 0 },
|
|
||||||
{ column: TableColumn.PLAY_COUNT, width: 0 },
|
|
||||||
{ column: TableColumn.USER_FAVORITE, width: 0 },
|
|
||||||
]),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const cardRows: Record<string, CardRow<Album>[] | CardRow<AlbumArtist>[]> = {
|
|
||||||
album: [
|
|
||||||
{
|
|
||||||
property: 'name',
|
|
||||||
route: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
arrayProperty: 'name',
|
|
||||||
property: 'albumArtists',
|
|
||||||
route: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
albumArtist: [
|
|
||||||
{
|
|
||||||
property: 'name',
|
|
||||||
route: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardRoutes = {
|
|
||||||
album: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
||||||
},
|
|
||||||
albumArtist: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const carousels = useMemo(() => {
|
const carousels = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
data: recentAlbumsQuery?.data?.items,
|
isHidden: !enabledItem.recentAlbums || !routeId,
|
||||||
isHidden: !recentAlbumsQuery?.data?.items?.length || !enabledItem.recentAlbums,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
loading: recentAlbumsQuery?.isLoading || recentAlbumsQuery.isFetching,
|
|
||||||
order: itemOrder.recentAlbums,
|
order: itemOrder.recentAlbums,
|
||||||
|
query: {
|
||||||
|
artistIds: routeId ? [routeId] : undefined,
|
||||||
|
compilation: false,
|
||||||
|
},
|
||||||
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
title: (
|
title: (
|
||||||
<Group align="flex-end">
|
<Group align="flex-end">
|
||||||
<TextTitle fw={700} order={2}>
|
<TextTitle fw={700} order={2}>
|
||||||
@@ -230,14 +136,16 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
uniqueId: 'recentReleases',
|
uniqueId: 'recentReleases',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: compilationAlbumsQuery?.data?.items,
|
|
||||||
isHidden:
|
isHidden:
|
||||||
!compilationAlbumsQuery?.data?.items?.length ||
|
!enabledItem.compilations || server?.type === ServerType.SUBSONIC || !routeId,
|
||||||
!enabledItem.compilations ||
|
|
||||||
server?.type === ServerType.SUBSONIC,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
|
||||||
order: itemOrder.compilations,
|
order: itemOrder.compilations,
|
||||||
|
query: {
|
||||||
|
artistIds: routeId ? [routeId] : undefined,
|
||||||
|
compilation: true,
|
||||||
|
},
|
||||||
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
title: (
|
title: (
|
||||||
<TextTitle fw={700} order={2}>
|
<TextTitle fw={700} order={2}>
|
||||||
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
||||||
@@ -246,7 +154,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
uniqueId: 'compilationAlbums',
|
uniqueId: 'compilationAlbums',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: detailQuery?.data?.similarArtists || [],
|
data: (detailQuery?.data?.similarArtists || []) as AlbumArtist[],
|
||||||
isHidden: !detailQuery?.data?.similarArtists || !enabledItem.similarArtists,
|
isHidden: !detailQuery?.data?.similarArtists || !enabledItem.similarArtists,
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
order: itemOrder.similarArtists,
|
order: itemOrder.similarArtists,
|
||||||
@@ -262,9 +170,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
artistDiscographyLink,
|
artistDiscographyLink,
|
||||||
compilationAlbumsQuery?.data?.items,
|
|
||||||
compilationAlbumsQuery.isFetching,
|
|
||||||
compilationAlbumsQuery?.isLoading,
|
|
||||||
detailQuery?.data?.similarArtists,
|
detailQuery?.data?.similarArtists,
|
||||||
enabledItem.compilations,
|
enabledItem.compilations,
|
||||||
enabledItem.recentAlbums,
|
enabledItem.recentAlbums,
|
||||||
@@ -272,9 +177,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
itemOrder.compilations,
|
itemOrder.compilations,
|
||||||
itemOrder.recentAlbums,
|
itemOrder.recentAlbums,
|
||||||
itemOrder.similarArtists,
|
itemOrder.similarArtists,
|
||||||
recentAlbumsQuery?.data?.items,
|
routeId,
|
||||||
recentAlbumsQuery.isFetching,
|
|
||||||
recentAlbumsQuery?.isLoading,
|
|
||||||
server?.type,
|
server?.type,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
@@ -291,16 +194,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
|
|
||||||
|
|
||||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
||||||
if (!e.data || !topSongsQuery?.data) return;
|
if (!e.data || !topSongsQuery?.data) return;
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byData: topSongsQuery?.data?.items || [],
|
|
||||||
initialSongId: e.data.id,
|
|
||||||
playType: playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFavoriteMutation = useCreateFavorite({});
|
const createFavoriteMutation = useCreateFavorite({});
|
||||||
@@ -311,7 +206,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
|
|
||||||
if (detailQuery.data.userFavorite) {
|
if (detailQuery.data.userFavorite) {
|
||||||
deleteFavoriteMutation.mutate({
|
deleteFavoriteMutation.mutate({
|
||||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||||
query: {
|
query: {
|
||||||
id: [detailQuery.data.id],
|
id: [detailQuery.data.id],
|
||||||
type: LibraryItem.ALBUM_ARTIST,
|
type: LibraryItem.ALBUM_ARTIST,
|
||||||
@@ -319,7 +214,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
createFavoriteMutation.mutate({
|
createFavoriteMutation.mutate({
|
||||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||||
query: {
|
query: {
|
||||||
id: [detailQuery.data.id],
|
id: [detailQuery.data.id],
|
||||||
type: LibraryItem.ALBUM_ARTIST,
|
type: LibraryItem.ALBUM_ARTIST,
|
||||||
@@ -329,17 +224,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
};
|
};
|
||||||
|
|
||||||
const albumCount = detailQuery?.data?.albumCount;
|
const albumCount = detailQuery?.data?.albumCount;
|
||||||
const artistContextItems =
|
|
||||||
(albumCount ?? 1) > 0
|
|
||||||
? ARTIST_CONTEXT_MENU_ITEMS
|
|
||||||
: ARTIST_CONTEXT_MENU_ITEMS.filter((item) => !item.id.toLowerCase().includes('play'));
|
|
||||||
|
|
||||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
|
||||||
LibraryItem.ALBUM_ARTIST,
|
|
||||||
artistContextItems,
|
|
||||||
);
|
|
||||||
|
|
||||||
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
|
||||||
|
|
||||||
const biography = useMemo(() => {
|
const biography = useMemo(() => {
|
||||||
const bio = detailQuery?.data?.biography;
|
const bio = detailQuery?.data?.biography;
|
||||||
@@ -384,7 +268,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
icon="ellipsisHorizontal"
|
icon="ellipsisHorizontal"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery?.data) return;
|
||||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
// handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
@@ -512,28 +396,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<VirtualTable
|
|
||||||
autoFitColumns
|
|
||||||
autoHeight
|
|
||||||
columnDefs={topSongsColumnDefs}
|
|
||||||
context={{
|
|
||||||
itemType: LibraryItem.SONG,
|
|
||||||
}}
|
|
||||||
deselectOnClickOutside
|
|
||||||
enableCellChangeFlash={false}
|
|
||||||
getRowId={(data) => data.data.uniqueId}
|
|
||||||
onCellContextMenu={handleContextMenu}
|
|
||||||
onRowDoubleClicked={handleRowDoubleClick}
|
|
||||||
rowData={topSongs}
|
|
||||||
rowHeight={60}
|
|
||||||
rowSelection="multiple"
|
|
||||||
shouldUpdateSong
|
|
||||||
stickyHeader
|
|
||||||
suppressCellFocus
|
|
||||||
suppressHorizontalScroll
|
|
||||||
suppressLoadingOverlay
|
|
||||||
suppressRowDrag
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -548,28 +410,30 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
>
|
>
|
||||||
<section>
|
<section>
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<MemoizedSwiperGridCarousel
|
{carousel.itemType === LibraryItem.ALBUM ? (
|
||||||
cardRows={
|
'query' in carousel &&
|
||||||
cardRows[carousel.itemType as keyof typeof cardRows]
|
carousel.query &&
|
||||||
}
|
carousel.sortBy &&
|
||||||
data={carousel.data}
|
carousel.sortOrder ? (
|
||||||
isLoading={carousel.loading}
|
<Suspense fallback={<Spinner container />}>
|
||||||
itemType={carousel.itemType}
|
<AlbumInfiniteCarousel
|
||||||
route={
|
query={carousel.query}
|
||||||
cardRoutes[
|
rowCount={1}
|
||||||
carousel.itemType as keyof typeof cardRoutes
|
sortBy={carousel.sortBy}
|
||||||
]
|
sortOrder={carousel.sortOrder}
|
||||||
}
|
title={carousel.title}
|
||||||
swiperProps={{
|
/>
|
||||||
grid: {
|
</Suspense>
|
||||||
rows: 2,
|
) : null
|
||||||
},
|
) : carousel.itemType === LibraryItem.ALBUM_ARTIST ? (
|
||||||
}}
|
'data' in carousel && carousel.data ? (
|
||||||
title={{
|
<AlbumArtistGridCarousel
|
||||||
label: carousel.title,
|
data={carousel.data}
|
||||||
}}
|
rowCount={1}
|
||||||
uniqueId={carousel.uniqueId}
|
title={carousel.title}
|
||||||
/>
|
/>
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</section>
|
</section>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
||||||
|
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
|
import { AlbumArtist, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface AlbumArtistGridCarouselProps {
|
||||||
|
data: AlbumArtist[];
|
||||||
|
excludeIds?: string[];
|
||||||
|
rowCount?: number;
|
||||||
|
title: React.ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
|
||||||
|
const { data, excludeIds, rowCount = 1, title } = props;
|
||||||
|
const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST);
|
||||||
|
const controls = useDefaultItemListControls();
|
||||||
|
|
||||||
|
const cards = useMemo(() => {
|
||||||
|
// Filter out excluded IDs if provided
|
||||||
|
const filteredItems = excludeIds
|
||||||
|
? data.filter((albumArtist) => !excludeIds.includes(albumArtist.id))
|
||||||
|
: data;
|
||||||
|
|
||||||
|
return filteredItems.map((albumArtist: AlbumArtist) => ({
|
||||||
|
content: (
|
||||||
|
<MemoizedItemCard
|
||||||
|
controls={controls}
|
||||||
|
data={albumArtist}
|
||||||
|
enableDrag
|
||||||
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
|
rows={rows}
|
||||||
|
type="poster"
|
||||||
|
withControls
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: albumArtist.id,
|
||||||
|
}));
|
||||||
|
}, [data, excludeIds, controls, rows]);
|
||||||
|
|
||||||
|
const handleNextPage = () => {};
|
||||||
|
const handlePrevPage = () => {};
|
||||||
|
|
||||||
|
if (cards.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCarousel
|
||||||
|
cards={cards}
|
||||||
|
onNextPage={handleNextPage}
|
||||||
|
onPrevPage={handlePrevPage}
|
||||||
|
rowCount={rowCount}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
|
||||||
|
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
|
||||||
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
|
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
|
import {
|
||||||
|
AlbumArtist,
|
||||||
|
AlbumArtistListQuery,
|
||||||
|
AlbumArtistListResponse,
|
||||||
|
AlbumArtistListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface AlbumArtistCarouselProps {
|
||||||
|
excludeIds?: string[];
|
||||||
|
query?: Partial<Omit<AlbumArtistListQuery, 'startIndex'>>;
|
||||||
|
rowCount?: number;
|
||||||
|
sortBy: AlbumArtistListSort;
|
||||||
|
sortOrder: SortOrder;
|
||||||
|
title: React.ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlbumArtistInfiniteCarousel(props: AlbumArtistCarouselProps) {
|
||||||
|
const { excludeIds, query: additionalQuery, rowCount = 1, sortBy, sortOrder, title } = props;
|
||||||
|
const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST);
|
||||||
|
const {
|
||||||
|
data: albumArtists,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
} = useAlbumArtistListInfinite(sortBy, sortOrder, 20, additionalQuery);
|
||||||
|
|
||||||
|
const controls = useDefaultItemListControls();
|
||||||
|
|
||||||
|
const cards = useMemo(() => {
|
||||||
|
// Flatten all pages and filter excluded IDs
|
||||||
|
const allItems = albumArtists.pages.flatMap((page: AlbumArtistListResponse) => page.items);
|
||||||
|
const filteredItems = excludeIds
|
||||||
|
? allItems.filter((albumArtist) => !excludeIds.includes(albumArtist.id))
|
||||||
|
: allItems;
|
||||||
|
|
||||||
|
return filteredItems.map((albumArtist: AlbumArtist) => ({
|
||||||
|
content: (
|
||||||
|
<MemoizedItemCard
|
||||||
|
controls={controls}
|
||||||
|
data={albumArtist}
|
||||||
|
enableDrag
|
||||||
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
|
rows={rows}
|
||||||
|
type="poster"
|
||||||
|
withControls
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: albumArtist.id,
|
||||||
|
}));
|
||||||
|
}, [albumArtists.pages, controls, excludeIds, rows]);
|
||||||
|
|
||||||
|
const handleNextPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
const handlePrevPage = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
const firstPageItems = excludeIds
|
||||||
|
? albumArtists.pages[0]?.items.filter(
|
||||||
|
(albumArtist) => !excludeIds.includes(albumArtist.id),
|
||||||
|
) || []
|
||||||
|
: albumArtists.pages[0]?.items || [];
|
||||||
|
|
||||||
|
if (firstPageItems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridCarousel
|
||||||
|
cards={cards}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
loadNextPage={fetchNextPage}
|
||||||
|
onNextPage={handleNextPage}
|
||||||
|
onPrevPage={handlePrevPage}
|
||||||
|
rowCount={rowCount}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAlbumArtistListInfinite(
|
||||||
|
sortBy: AlbumArtistListSort,
|
||||||
|
sortOrder: SortOrder,
|
||||||
|
itemLimit: number,
|
||||||
|
additionalQuery?: Partial<Omit<AlbumArtistListQuery, 'startIndex'>>,
|
||||||
|
) {
|
||||||
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
|
const query = useSuspenseInfiniteQuery<AlbumArtistListResponse>({
|
||||||
|
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
||||||
|
if (lastPage.items.length < itemLimit) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPageParam = Number(lastPageParam) + itemLimit;
|
||||||
|
|
||||||
|
return String(nextPageParam);
|
||||||
|
},
|
||||||
|
initialPageParam: '0',
|
||||||
|
queryFn: ({ pageParam, signal }) => {
|
||||||
|
return api.controller.getAlbumArtistList({
|
||||||
|
apiClientProps: { serverId, signal },
|
||||||
|
query: {
|
||||||
|
limit: itemLimit,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
startIndex: Number(pageParam),
|
||||||
|
...additionalQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.albumArtists.list(serverId, {
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
...additionalQuery,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
@@ -1,28 +1,22 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useRef } from 'react';
|
import { Suspense, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
|
import { FeatureCarousel } from '/@/renderer/components/feature-carousel/feature-carousel';
|
||||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
|
||||||
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import { homeQueries } from '/@/renderer/features/home/api/home-api';
|
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
|
||||||
import {
|
import {
|
||||||
HomeItem,
|
HomeItem,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
useWindowSettings,
|
useWindowSettings,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|
||||||
import { Group } from '/@/shared/components/group/group';
|
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
|
||||||
import {
|
import {
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
@@ -32,12 +26,6 @@ import {
|
|||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
const BASE_QUERY_ARGS = {
|
|
||||||
limit: 15,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
startIndex: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const HomeRoute = () => {
|
const HomeRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -45,16 +33,9 @@ const HomeRoute = () => {
|
|||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const { homeFeature, homeItems } = useGeneralSettings();
|
const { homeFeature, homeItems } = useGeneralSettings();
|
||||||
|
|
||||||
const queriesEnabled = useMemo(() => {
|
const isJellyfin = server?.type === ServerType.JELLYFIN;
|
||||||
return homeItems.reduce(
|
|
||||||
(previous: Record<HomeItem, boolean>, current) => ({
|
|
||||||
...previous,
|
|
||||||
[current.id]: !current.disabled,
|
|
||||||
}),
|
|
||||||
{} as Record<HomeItem, boolean>,
|
|
||||||
);
|
|
||||||
}, [homeItems]);
|
|
||||||
|
|
||||||
|
// Only keep queries for FeatureCarousel and songs carousel (which still uses old carousel)
|
||||||
const feature = useQuery(
|
const feature = useQuery(
|
||||||
albumQueries.list({
|
albumQueries.list({
|
||||||
options: {
|
options: {
|
||||||
@@ -72,83 +53,15 @@ const HomeRoute = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isJellyfin = server?.type === ServerType.JELLYFIN;
|
|
||||||
|
|
||||||
const featureItemsWithImage = useMemo(() => {
|
|
||||||
return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
|
|
||||||
}, [feature.data?.items]);
|
|
||||||
|
|
||||||
const random = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
options: {
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...BASE_QUERY_ARGS,
|
|
||||||
sortBy: AlbumListSort.RANDOM,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const recentlyPlayed = useQuery(
|
|
||||||
homeQueries.recentlyPlayed({
|
|
||||||
options: {
|
|
||||||
staleTime: 0,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...BASE_QUERY_ARGS,
|
|
||||||
sortBy: AlbumListSort.RECENTLY_PLAYED,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const recentlyAdded = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
options: {
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...BASE_QUERY_ARGS,
|
|
||||||
sortBy: AlbumListSort.RECENTLY_ADDED,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const mostPlayedAlbums = useQuery(
|
|
||||||
albumQueries.list({
|
|
||||||
options: {
|
|
||||||
enabled:
|
|
||||||
server?.type === ServerType.SUBSONIC || server?.type === ServerType.NAVIDROME,
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...BASE_QUERY_ARGS,
|
|
||||||
sortBy: AlbumListSort.PLAY_COUNT,
|
|
||||||
sortOrder: SortOrder.DESC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const mostPlayedSongs = useQuery(
|
const mostPlayedSongs = useQuery(
|
||||||
songsQueries.list(
|
songsQueries.list(
|
||||||
{
|
{
|
||||||
options: {
|
options: {
|
||||||
enabled: server?.type === ServerType.JELLYFIN,
|
enabled: isJellyfin,
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
...BASE_QUERY_ARGS,
|
limit: 15,
|
||||||
sortBy: SongListSort.PLAY_COUNT,
|
sortBy: SongListSort.PLAY_COUNT,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
@@ -159,62 +72,42 @@ const HomeRoute = () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentlyReleased = useQuery(
|
const featureItemsWithImage = useMemo(() => {
|
||||||
albumQueries.list({
|
return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
|
||||||
options: {
|
}, [feature.data?.items]);
|
||||||
enabled: queriesEnabled[HomeItem.RECENTLY_RELEASED],
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...BASE_QUERY_ARGS,
|
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
(random.isLoading && queriesEnabled[HomeItem.RANDOM]) ||
|
|
||||||
(recentlyPlayed.isLoading && queriesEnabled[HomeItem.RECENTLY_PLAYED] && !isJellyfin) ||
|
|
||||||
(recentlyAdded.isLoading && queriesEnabled[HomeItem.RECENTLY_ADDED]) ||
|
|
||||||
(recentlyReleased.isLoading && queriesEnabled[HomeItem.RECENTLY_RELEASED]) ||
|
|
||||||
(((isJellyfin && mostPlayedSongs.isLoading) ||
|
|
||||||
(!isJellyfin && mostPlayedAlbums.isLoading)) &&
|
|
||||||
queriesEnabled[HomeItem.MOST_PLAYED]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Spinner container />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Carousel configuration - queries are now handled inside AlbumInfiniteCarousel
|
||||||
const carousels = {
|
const carousels = {
|
||||||
[HomeItem.MOST_PLAYED]: {
|
[HomeItem.MOST_PLAYED]: {
|
||||||
data: isJellyfin ? mostPlayedSongs?.data?.items : mostPlayedAlbums?.data?.items,
|
data: mostPlayedSongs?.data?.items,
|
||||||
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
|
itemType: isJellyfin ? LibraryItem.SONG : LibraryItem.ALBUM,
|
||||||
query: isJellyfin ? mostPlayedSongs : mostPlayedAlbums,
|
query: mostPlayedSongs,
|
||||||
|
sortBy: AlbumListSort.PLAY_COUNT,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }),
|
title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
[HomeItem.RANDOM]: {
|
[HomeItem.RANDOM]: {
|
||||||
data: random?.data?.items,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
query: random,
|
sortBy: AlbumListSort.RANDOM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
title: t('page.home.explore', { postProcess: 'sentenceCase' }),
|
title: t('page.home.explore', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
[HomeItem.RECENTLY_ADDED]: {
|
[HomeItem.RECENTLY_ADDED]: {
|
||||||
data: recentlyAdded?.data?.items,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
query: recentlyAdded,
|
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }),
|
title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
[HomeItem.RECENTLY_PLAYED]: {
|
[HomeItem.RECENTLY_PLAYED]: {
|
||||||
data: recentlyPlayed?.data?.items,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
query: recentlyPlayed,
|
sortBy: AlbumListSort.RECENTLY_PLAYED,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }),
|
title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
[HomeItem.RECENTLY_RELEASED]: {
|
[HomeItem.RECENTLY_RELEASED]: {
|
||||||
data: recentlyReleased?.data?.items,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
query: recentlyReleased,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
title: t('page.home.recentlyReleased', { postProcess: 'sentenceCase' }),
|
title: t('page.home.recentlyReleased', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -257,70 +150,31 @@ const HomeRoute = () => {
|
|||||||
px="2rem"
|
px="2rem"
|
||||||
>
|
>
|
||||||
{homeFeature && <FeatureCarousel data={featureItemsWithImage} />}
|
{homeFeature && <FeatureCarousel data={featureItemsWithImage} />}
|
||||||
{sortedCarousel.map((carousel) => (
|
{sortedCarousel.map((carousel) => {
|
||||||
<MemoizedSwiperGridCarousel
|
if (carousel.itemType === LibraryItem.ALBUM) {
|
||||||
cardRows={[
|
return (
|
||||||
{
|
<Suspense
|
||||||
property: 'name',
|
fallback={<Spinner container />}
|
||||||
route: {
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
>
|
||||||
slugs: [
|
<AlbumInfiniteCarousel
|
||||||
{
|
rowCount={1}
|
||||||
idProperty:
|
sortBy={carousel.sortBy}
|
||||||
isJellyfin &&
|
sortOrder={carousel.sortOrder}
|
||||||
carousel.itemType === LibraryItem.SONG
|
title={carousel.title}
|
||||||
? 'albumId'
|
/>
|
||||||
: 'id',
|
</Suspense>
|
||||||
slugProperty: 'albumId',
|
);
|
||||||
},
|
}
|
||||||
],
|
|
||||||
},
|
// Songs carousel (only for Jellyfin most played) - keep using old carousel for now
|
||||||
},
|
if ('data' in carousel && 'query' in carousel) {
|
||||||
{
|
// TODO: Create SongInfiniteCarousel
|
||||||
arrayProperty: 'name',
|
return null;
|
||||||
property: 'albumArtists',
|
}
|
||||||
route: {
|
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
return null;
|
||||||
slugs: [
|
})}
|
||||||
{
|
|
||||||
idProperty: 'id',
|
|
||||||
slugProperty: 'albumArtistId',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={carousel.data}
|
|
||||||
itemType={carousel.itemType}
|
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
|
||||||
route={{
|
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
||||||
slugs: [
|
|
||||||
{
|
|
||||||
idProperty:
|
|
||||||
isJellyfin && carousel.itemType === LibraryItem.SONG
|
|
||||||
? 'albumId'
|
|
||||||
: 'id',
|
|
||||||
slugProperty: 'albumId',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
title={{
|
|
||||||
label: (
|
|
||||||
<Group>
|
|
||||||
<TextTitle order={3}>{carousel.title}</TextTitle>
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => carousel.query.refetch()}
|
|
||||||
variant="transparent"
|
|
||||||
>
|
|
||||||
<Icon icon="refresh" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
uniqueId={carousel.uniqueId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</NativeScrollArea>
|
</NativeScrollArea>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
|
|||||||
@@ -21,5 +21,7 @@ export const useContainerQuery = (props?: UseContainerQueryProps) => {
|
|||||||
const is2xl = width >= (xxl || 1920);
|
const is2xl = width >= (xxl || 1920);
|
||||||
const is3xl = width >= (xxxl || 2560);
|
const is3xl = width >= (xxxl || 2560);
|
||||||
|
|
||||||
return { height, is2xl, is3xl, isLg, isMd, isSm, isXl, isXs, ref, width };
|
const isCalculated = width !== 0;
|
||||||
|
|
||||||
|
return { height, is2xl, is3xl, isCalculated, isLg, isMd, isSm, isXl, isXs, ref, width };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user