diff --git a/package.json b/package.json
index fefba49c0..04dee183d 100644
--- a/package.json
+++ b/package.json
@@ -115,7 +115,6 @@
"nuqs": "^2.7.1",
"overlayscrollbars": "^2.11.1",
"overlayscrollbars-react": "^0.5.6",
- "postcss-simple-vars": "^7.0.1",
"qs": "^6.14.0",
"react": "^19.1.0",
"react-call": "^1.8.1",
@@ -167,6 +166,7 @@
"eslint-plugin-react-refresh": "^0.4.19",
"i18next-parser": "^9.0.2",
"postcss-preset-mantine": "^1.17.0",
+ "postcss-simple-vars": "^7.0.1",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14",
"sass-embedded": "^1.89.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eb72415fa..ca27fbe6d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -173,9 +173,6 @@ importers:
overlayscrollbars-react:
specifier: ^0.5.6
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:
specifier: ^6.14.0
version: 6.14.0
@@ -324,6 +321,9 @@ importers:
postcss-preset-mantine:
specifier: ^1.17.0
version: 1.17.0(postcss@8.5.3)
+ postcss-simple-vars:
+ specifier: ^7.0.1
+ version: 7.0.1(postcss@8.5.3)
prettier:
specifier: ^3.5.3
version: 3.5.3
diff --git a/postcss.config.cjs b/postcss.config.cjs
index 62f77aa46..0a1980dc6 100644
--- a/postcss.config.cjs
+++ b/postcss.config.cjs
@@ -3,13 +3,13 @@ module.exports = {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
- 'breakpoint-xs': '36em',
- 'breakpoint-sm': '48em',
- 'breakpoint-md': '62em',
- 'breakpoint-lg': '75em',
- 'breakpoint-xl': '88em',
- 'breakpoint-2xl': '120em',
- 'breakpoint-3xl': '160em',
+ 'mantine-breakpoint-xs': '36em',
+ 'mantine-breakpoint-sm': '48em',
+ 'mantine-breakpoint-md': '62em',
+ 'mantine-breakpoint-lg': '75em',
+ 'mantine-breakpoint-xl': '88em',
+ 'mantine-breakpoint-2xl': '120em',
+ 'mantine-breakpoint-3xl': '160em',
},
},
},
diff --git a/src/renderer/components/grid-carousel/grid-carousel-v2.tsx b/src/renderer/components/grid-carousel/grid-carousel-v2.tsx
new file mode 100644
index 000000000..72f63566e
--- /dev/null
+++ b/src/renderer/components/grid-carousel/grid-carousel-v2.tsx
@@ -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 }) => (
+
{content}
+));
+
+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(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 (
+
+ {cq.isCalculated && (
+ <>
+
+ {typeof title === 'string' ? (
+
{title}
+ ) : (
+ title
+ )}
+
+
+
+
+
+
+
+ {visibleCards.map((card) => (
+
+ ))}
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+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;
+}
diff --git a/src/renderer/components/grid-carousel/grid-carousel.module.css b/src/renderer/components/grid-carousel/grid-carousel.module.css
new file mode 100644
index 000000000..50a3d1350
--- /dev/null
+++ b/src/renderer/components/grid-carousel/grid-carousel.module.css
@@ -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;
+}
diff --git a/src/renderer/components/grid-carousel/grid-carousel.tsx b/src/renderer/components/grid-carousel/grid-carousel.tsx
deleted file mode 100644
index 22326f555..000000000
--- a/src/renderer/components/grid-carousel/grid-carousel.tsx
+++ /dev/null
@@ -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 (
-
- {isValidElement(label) ? (
- label
- ) : (
-
- {label}
-
- )}
-
-
-
-
-
-
- );
-};
-
-export interface SwiperGridCarouselProps {
- cardRows: CardRow[] | CardRow[] | CardRow[];
- 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(null);
- const swiperRef = useRef(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) => (
-
- ));
- }, [
- 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 (
-
- {title ? (
-
- ) : null}
- {
- 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 (
-
- {slideContent}
-
- );
- })}
-
-
- );
-};
-
-export const MemoizedSwiperGridCarousel = memo(
- function Carousel(props: SwiperGridCarouselProps) {
- return ;
- },
- (oldProps, newProps) => {
- const uniqueIdIsEqual = oldProps.uniqueId === newProps.uniqueId;
- const dataIsEqual = oldProps.data === newProps.data;
- return uniqueIdIsEqual && dataIsEqual;
- },
-);
diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx
index 8e1099268..16c1c969e 100644
--- a/src/renderer/components/item-card/item-card.tsx
+++ b/src/renderer/components/item-card/item-card.tsx
@@ -460,12 +460,14 @@ const PosterItemCard = ({
},
itemType,
onDragStart: () => {
- if (!data || !internalState) {
+ if (!data) {
return;
}
const draggedItems = getDraggedItems(data, internalState);
- internalState.setDragging(draggedItems);
+ if (internalState) {
+ internalState.setDragging(draggedItems);
+ }
},
onDrop: () => {
if (internalState) {
diff --git a/src/renderer/components/item-list/helpers/get-dragged-items.ts b/src/renderer/components/item-list/helpers/get-dragged-items.ts
index 7f830e197..4c6fac94a 100644
--- a/src/renderer/components/item-list/helpers/get-dragged-items.ts
+++ b/src/renderer/components/item-list/helpers/get-dragged-items.ts
@@ -26,9 +26,9 @@ const hasRequiredDragProperties = (
* 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.
* 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 itemType - The type of library item
* @param internalState - The item list state actions (optional)
* @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)
@@ -38,7 +38,7 @@ export const getDraggedItems = (
internalState?: ItemListStateActions,
updateSelection: boolean = true,
): ItemListStateItemWithRequiredProperties[] => {
- if (!data || !internalState) {
+ if (!data) {
return [];
}
@@ -46,14 +46,18 @@ export const getDraggedItems = (
return [];
}
+ const draggedItem = data as ItemListStateItemWithRequiredProperties;
+
+ if (!internalState) {
+ return [draggedItem];
+ }
+
const rowId = internalState.extractRowId(data);
if (!rowId) {
- return [];
+ return [draggedItem];
}
- const draggedItem = data as ItemListStateItemWithRequiredProperties;
-
const previouslySelected = internalState.getSelected();
const isDraggingSelectedItem = previouslySelected.some((selected) => {
if (hasRequiredDragProperties(selected)) {
diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
index 2e38b3f24..7e855dc05 100644
--- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
+++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
@@ -102,13 +102,14 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
},
itemType: props.itemType,
onDragStart: () => {
- if (!item || !isDataRow || !props.internalState) {
+ if (!item || !isDataRow) {
return;
}
const draggedItems = getDraggedItems(item as any, props.internalState);
-
- props.internalState.setDragging(draggedItems);
+ if (props.internalState) {
+ props.internalState.setDragging(draggedItems);
+ }
},
onDrop: () => {
if (props.internalState) {
diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx
index 90a029a4b..5fce0ef9c 100644
--- a/src/renderer/features/albums/components/album-detail-content.tsx
+++ b/src/renderer/features/albums/components/album-detail-content.tsx
@@ -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 { MutableRefObject, useCallback, useMemo } from 'react';
+import { Suspense, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { generatePath, useParams } from 'react-router';
-import { Link } from 'react-router';
+import { generatePath, Link, useParams } from 'react-router';
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 {
- 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 { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
-import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
-import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
-import { useAppFocus, useContainerQuery } from '/@/renderer/hooks';
+import { useContainerQuery } from '/@/renderer/hooks';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
-import { AppRoute } from '/@/renderer/router/routes';
-import { useCurrentServer, usePlayerSong, usePlayerStatus } from '/@/renderer/store';
-import {
- PersistedTableColumn,
- useGeneralSettings,
- usePlayButtonBehavior,
- useSettingsStoreActions,
- useTableSettings,
-} from '/@/renderer/store/settings.store';
+import { useCurrentServer } from '/@/renderer/store';
+import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
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 { Stack } from '/@/shared/components/stack/stack';
-import {
- AlbumListQuery,
- AlbumListSort,
- LibraryItem,
- QueueSong,
- SortOrder,
-} from '/@/shared/types/domain-types';
+import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
-const isFullWidthRow = (node: RowNode) => {
- return node.id?.startsWith('disc-');
-};
-
interface AlbumDetailContentProps {
background?: string;
- tableRef: MutableRefObject;
}
-export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentProps) => {
+export const AlbumDetailContent = ({ background }: AlbumDetailContentProps) => {
const { t } = useTranslation();
const { albumId } = useParams() as { albumId: string };
const server = useCurrentServer();
@@ -82,267 +40,59 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
);
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 genreRoute = useGenreRoute();
- const columnDefs = useMemo(
- () => getColumnDefs(tableConfig.columns, false, 'albumDetail'),
- [tableConfig.columns],
- );
-
- const getRowHeight = useCallback(
- (params: RowHeightParams) => {
- if (isFullWidthRow(params.node)) {
- return 45;
- }
-
- 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,
+ const carousels = useMemo(
+ () => [
+ {
+ excludeIds: detail?.id ? [detail.id] : undefined,
+ isHidden: !detail?.albumArtists?.[0]?.id,
+ 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,
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 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) => {
- 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 handlePlay = async (playType?: Play) => {};
const handleFavorite = () => {
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 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;
return (
@@ -361,10 +111,6 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
? 'primary'
: undefined,
}}
- loading={
- createFavoriteMutation.isPending ||
- deleteFavoriteMutation.isPending
- }
onClick={handleFavorite}
size="lg"
variant="transparent"
@@ -373,29 +119,12 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
icon="ellipsisHorizontal"
onClick={(e) => {
if (!detailQuery?.data) return;
- handleGeneralContextMenu(e, [detailQuery.data!]);
}}
size="lg"
variant="transparent"
/>
-
-
- {
- if (!detailQuery?.data) return;
- handleGeneralContextMenu(e, [detailQuery.data!]);
- }}
- size="lg"
- variant="transparent"
- />
-
-
-
-
-
{showGenres && (
@@ -468,91 +197,26 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
{replaceURLWithHTMLLinks(comment)}
)}
-
- 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
- />
-
+
{cq.height || cq.width ? (
<>
{carousels
.filter((c) => !c.isHidden)
- .map((carousel, index) => (
-
+ .map((carousel) => (
+ }
+ key={`carousel-${carousel.uniqueId}`}
+ >
+
+
))}
>
) : null}
diff --git a/src/renderer/features/albums/components/album-grid-carousel.tsx b/src/renderer/features/albums/components/album-grid-carousel.tsx
new file mode 100644
index 000000000..509461078
--- /dev/null
+++ b/src/renderer/features/albums/components/album-grid-carousel.tsx
@@ -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: (
+
+ ),
+ id: album.id,
+ }));
+ }, [data, excludeIds, controls, rows]);
+
+ const handleNextPage = () => {};
+ const handlePrevPage = () => {};
+
+ if (cards.length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/renderer/features/albums/components/album-infinite-carousel.tsx b/src/renderer/features/albums/components/album-infinite-carousel.tsx
new file mode 100644
index 000000000..57716ba4f
--- /dev/null
+++ b/src/renderer/features/albums/components/album-infinite-carousel.tsx
@@ -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>;
+ 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: (
+
+ ),
+ 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 (
+
+ );
+}
+
+function useAlbumListInfinite(
+ sortBy: AlbumListSort,
+ sortOrder: SortOrder,
+ itemLimit: number,
+ additionalQuery?: Partial>,
+) {
+ const serverId = useCurrentServerId();
+
+ const query = useSuspenseInfiniteQuery({
+ 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;
+}
diff --git a/src/renderer/features/albums/routes/dummy-album-detail-route.tsx b/src/renderer/features/albums/routes/dummy-album-detail-route.tsx
index e3ed8a2ce..e16c264b6 100644
--- a/src/renderer/features/albums/routes/dummy-album-detail-route.tsx
+++ b/src/renderer/features/albums/routes/dummy-album-detail-route.tsx
@@ -8,8 +8,6 @@ import styles from './dummy-album-detail-route.module.css';
import { api } from '/@/renderer/api';
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 { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
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 comment = detailQuery?.data?.comment;
- const handleGeneralContextMenu = useHandleGeneralContextMenu(LibraryItem.SONG, SONG_ALBUM_PAGE);
-
const handlePlay = () => {
handlePlayQueueAdd?.({
byItemType: {
@@ -190,7 +186,6 @@ const DummyAlbumDetailRoute = () => {
icon="ellipsisHorizontal"
onClick={(e) => {
if (!detailQuery?.data) return;
- handleGeneralContextMenu(e, [detailQuery.data!]);
}}
variant="subtle"
/>
diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx
index 9c85a5224..17a0e3606 100644
--- a/src/renderer/features/artists/components/album-artist-detail-content.tsx
+++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx
@@ -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 { useMemo } from 'react';
+import { Suspense, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { generatePath, useParams } from 'react-router';
-import { createSearchParams, Link } from 'react-router';
+import { createSearchParams, generatePath, Link, useParams } from 'react-router';
import styles from './album-artist-detail-content.module.css';
-import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
-import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
-import { albumQueries } from '/@/renderer/features/albums/api/album-api';
+import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
-import {
- 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 { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components/library-background-overlay';
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 { Grid } from '/@/shared/components/grid/grid';
import { Group } from '/@/shared/components/group/group';
+import { Spinner } from '/@/shared/components/spinner/spinner';
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import {
- Album,
AlbumArtist,
AlbumListSort,
LibraryItem,
@@ -46,7 +36,7 @@ import {
ServerType,
SortOrder,
} from '/@/shared/types/domain-types';
-import { CardRow, Play, TableColumn } from '/@/shared/types/types';
+import { Play } from '/@/shared/types/types';
interface AlbumArtistDetailContentProps {
background?: string;
@@ -101,40 +91,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
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(
artistsQueries.topSongs({
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[] | CardRow[]> = {
- 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(() => {
return [
{
- data: recentAlbumsQuery?.data?.items,
- isHidden: !recentAlbumsQuery?.data?.items?.length || !enabledItem.recentAlbums,
+ isHidden: !enabledItem.recentAlbums || !routeId,
itemType: LibraryItem.ALBUM,
- loading: recentAlbumsQuery?.isLoading || recentAlbumsQuery.isFetching,
order: itemOrder.recentAlbums,
+ query: {
+ artistIds: routeId ? [routeId] : undefined,
+ compilation: false,
+ },
+ sortBy: AlbumListSort.RELEASE_DATE,
+ sortOrder: SortOrder.DESC,
title: (
@@ -230,14 +136,16 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
uniqueId: 'recentReleases',
},
{
- data: compilationAlbumsQuery?.data?.items,
isHidden:
- !compilationAlbumsQuery?.data?.items?.length ||
- !enabledItem.compilations ||
- server?.type === ServerType.SUBSONIC,
+ !enabledItem.compilations || server?.type === ServerType.SUBSONIC || !routeId,
itemType: LibraryItem.ALBUM,
- loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
order: itemOrder.compilations,
+ query: {
+ artistIds: routeId ? [routeId] : undefined,
+ compilation: true,
+ },
+ sortBy: AlbumListSort.RELEASE_DATE,
+ sortOrder: SortOrder.DESC,
title: (
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
@@ -246,7 +154,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
uniqueId: 'compilationAlbums',
},
{
- data: detailQuery?.data?.similarArtists || [],
+ data: (detailQuery?.data?.similarArtists || []) as AlbumArtist[],
isHidden: !detailQuery?.data?.similarArtists || !enabledItem.similarArtists,
itemType: LibraryItem.ALBUM_ARTIST,
order: itemOrder.similarArtists,
@@ -262,9 +170,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
];
}, [
artistDiscographyLink,
- compilationAlbumsQuery?.data?.items,
- compilationAlbumsQuery.isFetching,
- compilationAlbumsQuery?.isLoading,
detailQuery?.data?.similarArtists,
enabledItem.compilations,
enabledItem.recentAlbums,
@@ -272,9 +177,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
itemOrder.compilations,
itemOrder.recentAlbums,
itemOrder.similarArtists,
- recentAlbumsQuery?.data?.items,
- recentAlbumsQuery.isFetching,
- recentAlbumsQuery?.isLoading,
+ routeId,
server?.type,
t,
]);
@@ -291,16 +194,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
});
};
- const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS);
-
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
if (!e.data || !topSongsQuery?.data) return;
-
- handlePlayQueueAdd?.({
- byData: topSongsQuery?.data?.items || [],
- initialSongId: e.data.id,
- playType: playButtonBehavior,
- });
};
const createFavoriteMutation = useCreateFavorite({});
@@ -311,7 +206,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
if (detailQuery.data.userFavorite) {
deleteFavoriteMutation.mutate({
- apiClientProps: { serverId: detailQuery.data.serverId },
+ apiClientProps: { serverId: detailQuery.data._serverId },
query: {
id: [detailQuery.data.id],
type: LibraryItem.ALBUM_ARTIST,
@@ -319,7 +214,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
});
} else {
createFavoriteMutation.mutate({
- apiClientProps: { serverId: detailQuery.data.serverId },
+ apiClientProps: { serverId: detailQuery.data._serverId },
query: {
id: [detailQuery.data.id],
type: LibraryItem.ALBUM_ARTIST,
@@ -329,17 +224,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
};
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 bio = detailQuery?.data?.biography;
@@ -384,7 +268,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
icon="ellipsisHorizontal"
onClick={(e) => {
if (!detailQuery?.data) return;
- handleGeneralContextMenu(e, [detailQuery.data!]);
+ // handleGeneralContextMenu(e, [detailQuery.data!]);
}}
size="lg"
variant="transparent"
@@ -512,28 +396,6 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
- data.data.uniqueId}
- onCellContextMenu={handleContextMenu}
- onRowDoubleClicked={handleRowDoubleClick}
- rowData={topSongs}
- rowHeight={60}
- rowSelection="multiple"
- shouldUpdateSong
- stickyHeader
- suppressCellFocus
- suppressHorizontalScroll
- suppressLoadingOverlay
- suppressRowDrag
- />
) : null}
@@ -548,28 +410,30 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
>
-
+ {carousel.itemType === LibraryItem.ALBUM ? (
+ 'query' in carousel &&
+ carousel.query &&
+ carousel.sortBy &&
+ carousel.sortOrder ? (
+ }>
+
+
+ ) : null
+ ) : carousel.itemType === LibraryItem.ALBUM_ARTIST ? (
+ 'data' in carousel && carousel.data ? (
+
+ ) : null
+ ) : null}
diff --git a/src/renderer/features/artists/components/album-artist-grid-carousel.tsx b/src/renderer/features/artists/components/album-artist-grid-carousel.tsx
new file mode 100644
index 000000000..8f21a7838
--- /dev/null
+++ b/src/renderer/features/artists/components/album-artist-grid-carousel.tsx
@@ -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: (
+
+ ),
+ id: albumArtist.id,
+ }));
+ }, [data, excludeIds, controls, rows]);
+
+ const handleNextPage = () => {};
+ const handlePrevPage = () => {};
+
+ if (cards.length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx b/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx
new file mode 100644
index 000000000..3a5208d36
--- /dev/null
+++ b/src/renderer/features/artists/components/album-artist-infinite-carousel.tsx
@@ -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>;
+ 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: (
+
+ ),
+ 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 (
+
+ );
+}
+
+function useAlbumArtistListInfinite(
+ sortBy: AlbumArtistListSort,
+ sortOrder: SortOrder,
+ itemLimit: number,
+ additionalQuery?: Partial>,
+) {
+ const serverId = useCurrentServerId();
+
+ const query = useSuspenseInfiniteQuery({
+ 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;
+}
diff --git a/src/renderer/features/home/routes/home-route.tsx b/src/renderer/features/home/routes/home-route.tsx
index f5af8e358..a009aeab8 100644
--- a/src/renderer/features/home/routes/home-route.tsx
+++ b/src/renderer/features/home/routes/home-route.tsx
@@ -1,28 +1,22 @@
import { useQuery } from '@tanstack/react-query';
-import { useMemo, useRef } from 'react';
+import { Suspense, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
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 { 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 { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
-import { AppRoute } from '/@/renderer/router/routes';
import {
HomeItem,
useCurrentServer,
useGeneralSettings,
useWindowSettings,
} 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 { Stack } from '/@/shared/components/stack/stack';
-import { TextTitle } from '/@/shared/components/text-title/text-title';
import {
AlbumListSort,
LibraryItem,
@@ -32,12 +26,6 @@ import {
} from '/@/shared/types/domain-types';
import { Platform } from '/@/shared/types/types';
-const BASE_QUERY_ARGS = {
- limit: 15,
- sortOrder: SortOrder.DESC,
- startIndex: 0,
-};
-
const HomeRoute = () => {
const { t } = useTranslation();
const scrollAreaRef = useRef(null);
@@ -45,16 +33,9 @@ const HomeRoute = () => {
const { windowBarStyle } = useWindowSettings();
const { homeFeature, homeItems } = useGeneralSettings();
- const queriesEnabled = useMemo(() => {
- return homeItems.reduce(
- (previous: Record, current) => ({
- ...previous,
- [current.id]: !current.disabled,
- }),
- {} as Record,
- );
- }, [homeItems]);
+ const isJellyfin = server?.type === ServerType.JELLYFIN;
+ // Only keep queries for FeatureCarousel and songs carousel (which still uses old carousel)
const feature = useQuery(
albumQueries.list({
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(
songsQueries.list(
{
options: {
- enabled: server?.type === ServerType.JELLYFIN,
+ enabled: isJellyfin,
staleTime: 1000 * 60 * 5,
},
query: {
- ...BASE_QUERY_ARGS,
+ limit: 15,
sortBy: SongListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC,
startIndex: 0,
@@ -159,62 +72,42 @@ const HomeRoute = () => {
),
);
- const recentlyReleased = useQuery(
- albumQueries.list({
- options: {
- 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 ;
- }
+ const featureItemsWithImage = useMemo(() => {
+ return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
+ }, [feature.data?.items]);
+ // Carousel configuration - queries are now handled inside AlbumInfiniteCarousel
const carousels = {
[HomeItem.MOST_PLAYED]: {
- data: isJellyfin ? mostPlayedSongs?.data?.items : mostPlayedAlbums?.data?.items,
+ data: mostPlayedSongs?.data?.items,
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' }),
},
[HomeItem.RANDOM]: {
- data: random?.data?.items,
itemType: LibraryItem.ALBUM,
- query: random,
+ sortBy: AlbumListSort.RANDOM,
+ sortOrder: SortOrder.ASC,
title: t('page.home.explore', { postProcess: 'sentenceCase' }),
},
[HomeItem.RECENTLY_ADDED]: {
- data: recentlyAdded?.data?.items,
itemType: LibraryItem.ALBUM,
- query: recentlyAdded,
+ sortBy: AlbumListSort.RECENTLY_ADDED,
+ sortOrder: SortOrder.DESC,
title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }),
},
[HomeItem.RECENTLY_PLAYED]: {
- data: recentlyPlayed?.data?.items,
itemType: LibraryItem.ALBUM,
- query: recentlyPlayed,
+ sortBy: AlbumListSort.RECENTLY_PLAYED,
+ sortOrder: SortOrder.DESC,
title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }),
},
[HomeItem.RECENTLY_RELEASED]: {
- data: recentlyReleased?.data?.items,
itemType: LibraryItem.ALBUM,
- query: recentlyReleased,
+ sortBy: AlbumListSort.RELEASE_DATE,
+ sortOrder: SortOrder.DESC,
title: t('page.home.recentlyReleased', { postProcess: 'sentenceCase' }),
},
};
@@ -257,70 +150,31 @@ const HomeRoute = () => {
px="2rem"
>
{homeFeature && }
- {sortedCarousel.map((carousel) => (
-
- {carousel.title}
- carousel.query.refetch()}
- variant="transparent"
- >
-
-
-
- ),
- }}
- uniqueId={carousel.uniqueId}
- />
- ))}
+ {sortedCarousel.map((carousel) => {
+ if (carousel.itemType === LibraryItem.ALBUM) {
+ return (
+ }
+ key={`carousel-${carousel.uniqueId}`}
+ >
+
+
+ );
+ }
+
+ // Songs carousel (only for Jellyfin most played) - keep using old carousel for now
+ if ('data' in carousel && 'query' in carousel) {
+ // TODO: Create SongInfiniteCarousel
+ return null;
+ }
+
+ return null;
+ })}
diff --git a/src/renderer/hooks/use-container-query.ts b/src/renderer/hooks/use-container-query.ts
index d9ec53260..b1e9438cb 100644
--- a/src/renderer/hooks/use-container-query.ts
+++ b/src/renderer/hooks/use-container-query.ts
@@ -21,5 +21,7 @@ export const useContainerQuery = (props?: UseContainerQueryProps) => {
const is2xl = width >= (xxl || 1920);
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 };
};