From 9a2540f954f89f9db6937a2edb0da88463d9d30c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 9 Feb 2026 12:43:19 -0800 Subject: [PATCH] improve loading state --- .../item-detail-list/item-detail.module.css | 18 +++ .../item-detail-list/item-detail.tsx | 143 +++++++++++++----- 2 files changed, 123 insertions(+), 38 deletions(-) diff --git a/src/renderer/components/item-list/item-detail-list/item-detail.module.css b/src/renderer/components/item-list/item-detail-list/item-detail.module.css index edc8b11ae..e388325b8 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail.module.css +++ b/src/renderer/components/item-list/item-detail-list/item-detail.module.css @@ -104,6 +104,12 @@ padding: 0 var(--theme-spacing-md) var(--theme-spacing-xl) var(--theme-spacing-md); } +/* Height constraint for skeleton columns; row grid gets two direct children (left, right) */ +.skeleton-column-wrapper { + box-sizing: border-box; + min-width: 0; +} + .image-wrapper { position: relative; display: block; @@ -395,17 +401,29 @@ opacity: 0.7; } +.skeleton-image-container { + justify-content: center; +} + .skeleton-image { width: 100%; aspect-ratio: 1; border-radius: var(--theme-radius-md); } +.skeleton-title-container { + justify-content: center; +} + .skeleton-title { width: 75%; height: 1.25rem; } +.skeleton-artist-container { + justify-content: center; +} + .skeleton-artist { width: 50%; height: 1rem; diff --git a/src/renderer/components/item-list/item-detail-list/item-detail.tsx b/src/renderer/components/item-list/item-detail-list/item-detail.tsx index 7ab0c4fbb..6ac41ec95 100644 --- a/src/renderer/components/item-list/item-detail-list/item-detail.tsx +++ b/src/renderer/components/item-list/item-detail-list/item-detail.tsx @@ -58,6 +58,10 @@ import { Text } from '/@/shared/components/text/text'; import { Album, LibraryItem, Song } from '/@/shared/types/domain-types'; import { ItemListKey, TableColumn } from '/@/shared/types/types'; +const DEFAULT_ROW_HEIGHT = 300; + +const SKELETON_TRACK_ROW_COUNT = 6; + interface ItemDetailListProps { currentPage?: number; data?: unknown[]; @@ -74,6 +78,7 @@ interface RowData { columnWidthPercents: number[]; controls?: ItemControls; data: unknown[]; + defaultRowHeight: number; enableAlternateRowColors: boolean; enableHorizontalBorders: boolean; enableRowHoverHighlight: boolean; @@ -96,7 +101,7 @@ interface TrackRowProps { enableVerticalBorders: boolean; internalState: ItemListStateActions; isMutatingFavorite: boolean; - onFavoriteClick: (song: Song) => void; + isSongsLoading?: boolean; rowIndex: number; size: 'compact' | 'default' | 'large'; song: Song; @@ -116,7 +121,7 @@ const TrackRow = memo( enableVerticalBorders, internalState, isMutatingFavorite, - onFavoriteClick, + isSongsLoading, rowIndex, size, song, @@ -260,14 +265,13 @@ const TrackRow = memo( song, ); - const content = showHoverContent ? ( + const content = isSongsLoading ? null : showHoverContent ? ( { + const heightStyle = { + height: defaultRowHeight, + minHeight: defaultRowHeight, + overflow: 'hidden' as const, + }; + return ( + <> +
+
+
+ + + +
+
+
+
+
+
+ {Array.from({ length: SKELETON_TRACK_ROW_COUNT }).map((_, i) => ( +
0, + [styles.trackRowSizeCompact]: trackTableSize === 'compact', + [styles.trackRowSizeDefault]: trackTableSize === 'default', + [styles.trackRowSizeLarge]: trackTableSize === 'large', + [styles.trackRowWithHorizontalBorder]: i > 0, + })} + key={i} + role="row" + > +
+
+ ))} +
+
+
+ + ); + }, +); + +ItemDetailSkeletonRow.displayName = 'ItemDetailSkeletonRow'; + type RowContentProps = Omit, 'style'>; const RowContent = memo( @@ -454,6 +540,7 @@ const RowContent = memo( columnWidthPercents, controls, data, + defaultRowHeight, enableAlternateRowColors, enableHorizontalBorders, enableRowHoverHighlight, @@ -474,7 +561,7 @@ const RowContent = memo( return (data?.[index] as Album | undefined) || undefined; }, [data, getItem, index]); - const { data: songData } = useQuery({ + const { data: songData, isLoading: isSongsQueryLoading } = useQuery({ enabled: !!item && !!item.id, ...albumQueries.detail({ query: { @@ -484,6 +571,8 @@ const RowContent = memo( }), }); + const isSongsLoading = !!item && isSongsQueryLoading && !songData; + const songs = useMemo(() => { return ( songData?.songs || @@ -502,39 +591,15 @@ const RowContent = memo( } }, [item?.id, registerSongs, songData?.songs]); - const onFavoriteClick = useCallback((song: Song) => { - // TODO: toggle favorite for song - void song; - }, []); - if (!item) { return ( - <> -
-
- - - -
-
-
-
- {Array.from({ length: 10 }).map((_, i) => ( -
- - - -
- ))} -
-
- + ); } @@ -561,8 +626,8 @@ const RowContent = memo( enableVerticalBorders={enableVerticalBorders} internalState={internalState} isMutatingFavorite={isMutatingFavorite} + isSongsLoading={isSongsLoading} key={song.id} - onFavoriteClick={onFavoriteClick} rowIndex={rowIndex} size={trackTableSize} song={song as Song} @@ -577,6 +642,7 @@ const RowContent = memo( prev.index === next.index && prev.data === next.data && prev.columnWidthPercents === next.columnWidthPercents && + prev.defaultRowHeight === next.defaultRowHeight && prev.enableAlternateRowColors === next.enableAlternateRowColors && prev.enableHorizontalBorders === next.enableHorizontalBorders && prev.enableRowHoverHighlight === next.enableRowHoverHighlight && @@ -706,7 +772,7 @@ export const ItemDetailList = ({ const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite; const rowHeight = useDynamicRowHeight({ - defaultRowHeight: 300, + defaultRowHeight: DEFAULT_ROW_HEIGHT, }); const isInfinite = data !== undefined || getItem !== undefined; @@ -817,6 +883,7 @@ export const ItemDetailList = ({ columnWidthPercents, controls, data: dataSource, + defaultRowHeight: DEFAULT_ROW_HEIGHT, enableAlternateRowColors, enableHorizontalBorders, enableRowHoverHighlight,