improve loading state

This commit is contained in:
jeffvli
2026-02-09 12:43:19 -08:00
parent b4c45f0956
commit 9a2540f954
2 changed files with 123 additions and 38 deletions
@@ -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;
@@ -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 ? (
<CellComponent
columnId={col.id}
controls={controls}
internalState={internalState}
isMutatingFavorite={isMutatingFavorite}
isRowHovered={isRowHovered}
onFavoriteClick={onFavoriteClick}
rowIndex={rowIndex}
size={size}
song={song}
@@ -447,6 +451,88 @@ const MetadataSection = memo(
MetadataSection.displayName = 'MetadataSection';
interface ItemDetailSkeletonRowProps {
defaultRowHeight: number;
enableAlternateRowColors: boolean;
enableHorizontalBorders: boolean;
enableVerticalBorders: boolean;
trackTableSize: 'compact' | 'default' | 'large';
}
const ItemDetailSkeletonRow = memo(
({
defaultRowHeight,
enableAlternateRowColors,
enableHorizontalBorders,
enableVerticalBorders,
trackTableSize,
}: ItemDetailSkeletonRowProps) => {
const heightStyle = {
height: defaultRowHeight,
minHeight: defaultRowHeight,
overflow: 'hidden' as const,
};
return (
<>
<div className={styles.skeletonColumnWrapper} style={heightStyle}>
<div className={styles.left}>
<div className={styles.metadata}>
<Skeleton
className={styles.skeletonImage}
containerClassName={styles.skeletonImageContainer}
/>
<Skeleton
className={styles.skeletonTitle}
containerClassName={styles.skeletonTitleContainer}
/>
<Skeleton
className={styles.skeletonArtist}
containerClassName={styles.skeletonArtistContainer}
/>
</div>
</div>
</div>
<div className={styles.skeletonColumnWrapper} style={heightStyle}>
<div className={styles.right}>
<div className={styles.tracksTable} role="table">
{Array.from({ length: SKELETON_TRACK_ROW_COUNT }).map((_, i) => (
<div
className={clsx(styles.trackRow, {
[styles.trackRowAlternateEven]:
enableAlternateRowColors && i % 2 === 0,
[styles.trackRowAlternateOdd]:
enableAlternateRowColors && i % 2 === 1,
[styles.trackRowHorizontalBorderVisible]:
enableHorizontalBorders && i > 0,
[styles.trackRowSizeCompact]: trackTableSize === 'compact',
[styles.trackRowSizeDefault]: trackTableSize === 'default',
[styles.trackRowSizeLarge]: trackTableSize === 'large',
[styles.trackRowWithHorizontalBorder]: i > 0,
})}
key={i}
role="row"
>
<div
className={clsx(styles.trackCell, {
[styles.trackCellVerticalBorderVisible]:
enableVerticalBorders,
[styles.trackCellWithVerticalBorder]: true,
})}
role="cell"
style={{ flex: 1, minWidth: 0 }}
/>
</div>
))}
</div>
</div>
</div>
</>
);
},
);
ItemDetailSkeletonRow.displayName = 'ItemDetailSkeletonRow';
type RowContentProps = Omit<RowComponentProps<RowData>, '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 (
<>
<div className={styles.left}>
<div className={styles.metadata}>
<Skeleton className={styles.skeletonImage} />
<Skeleton className={styles.skeletonTitle} />
<Skeleton className={styles.skeletonArtist} />
</div>
</div>
<div className={styles.right}>
<div
className={clsx(styles.skeletonTracks, {
[styles.skeletonTracksSizeCompact]: trackTableSize === 'compact',
[styles.skeletonTracksSizeDefault]: trackTableSize === 'default',
[styles.skeletonTracksSizeLarge]: trackTableSize === 'large',
})}
>
{Array.from({ length: 10 }).map((_, i) => (
<div className={styles.skeletonTrackRow} key={i}>
<Skeleton className={styles.skeletonTrackCell} />
<Skeleton className={styles.skeletonTrackCellTitle} />
<Skeleton className={styles.skeletonTrackCell} />
</div>
))}
</div>
</div>
</>
<ItemDetailSkeletonRow
defaultRowHeight={defaultRowHeight}
enableAlternateRowColors={enableAlternateRowColors}
enableHorizontalBorders={enableHorizontalBorders}
enableVerticalBorders={enableVerticalBorders}
trackTableSize={trackTableSize}
/>
);
}
@@ -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,