mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
improve loading state
This commit is contained in:
@@ -104,6 +104,12 @@
|
|||||||
padding: 0 var(--theme-spacing-md) var(--theme-spacing-xl) var(--theme-spacing-md);
|
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 {
|
.image-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -395,17 +401,29 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-image-container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-image {
|
.skeleton-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: var(--theme-radius-md);
|
border-radius: var(--theme-radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-title-container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-title {
|
.skeleton-title {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-artist-container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-artist {
|
.skeleton-artist {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ import { Text } from '/@/shared/components/text/text';
|
|||||||
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const DEFAULT_ROW_HEIGHT = 300;
|
||||||
|
|
||||||
|
const SKELETON_TRACK_ROW_COUNT = 6;
|
||||||
|
|
||||||
interface ItemDetailListProps {
|
interface ItemDetailListProps {
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
data?: unknown[];
|
data?: unknown[];
|
||||||
@@ -74,6 +78,7 @@ interface RowData {
|
|||||||
columnWidthPercents: number[];
|
columnWidthPercents: number[];
|
||||||
controls?: ItemControls;
|
controls?: ItemControls;
|
||||||
data: unknown[];
|
data: unknown[];
|
||||||
|
defaultRowHeight: number;
|
||||||
enableAlternateRowColors: boolean;
|
enableAlternateRowColors: boolean;
|
||||||
enableHorizontalBorders: boolean;
|
enableHorizontalBorders: boolean;
|
||||||
enableRowHoverHighlight: boolean;
|
enableRowHoverHighlight: boolean;
|
||||||
@@ -96,7 +101,7 @@ interface TrackRowProps {
|
|||||||
enableVerticalBorders: boolean;
|
enableVerticalBorders: boolean;
|
||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
isMutatingFavorite: boolean;
|
isMutatingFavorite: boolean;
|
||||||
onFavoriteClick: (song: Song) => void;
|
isSongsLoading?: boolean;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
size: 'compact' | 'default' | 'large';
|
size: 'compact' | 'default' | 'large';
|
||||||
song: Song;
|
song: Song;
|
||||||
@@ -116,7 +121,7 @@ const TrackRow = memo(
|
|||||||
enableVerticalBorders,
|
enableVerticalBorders,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
onFavoriteClick,
|
isSongsLoading,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
size,
|
size,
|
||||||
song,
|
song,
|
||||||
@@ -260,14 +265,13 @@ const TrackRow = memo(
|
|||||||
song,
|
song,
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = showHoverContent ? (
|
const content = isSongsLoading ? null : showHoverContent ? (
|
||||||
<CellComponent
|
<CellComponent
|
||||||
columnId={col.id}
|
columnId={col.id}
|
||||||
controls={controls}
|
controls={controls}
|
||||||
internalState={internalState}
|
internalState={internalState}
|
||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
isRowHovered={isRowHovered}
|
isRowHovered={isRowHovered}
|
||||||
onFavoriteClick={onFavoriteClick}
|
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
size={size}
|
size={size}
|
||||||
song={song}
|
song={song}
|
||||||
@@ -447,6 +451,88 @@ const MetadataSection = memo(
|
|||||||
|
|
||||||
MetadataSection.displayName = 'MetadataSection';
|
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'>;
|
type RowContentProps = Omit<RowComponentProps<RowData>, 'style'>;
|
||||||
|
|
||||||
const RowContent = memo(
|
const RowContent = memo(
|
||||||
@@ -454,6 +540,7 @@ const RowContent = memo(
|
|||||||
columnWidthPercents,
|
columnWidthPercents,
|
||||||
controls,
|
controls,
|
||||||
data,
|
data,
|
||||||
|
defaultRowHeight,
|
||||||
enableAlternateRowColors,
|
enableAlternateRowColors,
|
||||||
enableHorizontalBorders,
|
enableHorizontalBorders,
|
||||||
enableRowHoverHighlight,
|
enableRowHoverHighlight,
|
||||||
@@ -474,7 +561,7 @@ const RowContent = memo(
|
|||||||
return (data?.[index] as Album | undefined) || undefined;
|
return (data?.[index] as Album | undefined) || undefined;
|
||||||
}, [data, getItem, index]);
|
}, [data, getItem, index]);
|
||||||
|
|
||||||
const { data: songData } = useQuery({
|
const { data: songData, isLoading: isSongsQueryLoading } = useQuery({
|
||||||
enabled: !!item && !!item.id,
|
enabled: !!item && !!item.id,
|
||||||
...albumQueries.detail({
|
...albumQueries.detail({
|
||||||
query: {
|
query: {
|
||||||
@@ -484,6 +571,8 @@ const RowContent = memo(
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isSongsLoading = !!item && isSongsQueryLoading && !songData;
|
||||||
|
|
||||||
const songs = useMemo(() => {
|
const songs = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
songData?.songs ||
|
songData?.songs ||
|
||||||
@@ -502,39 +591,15 @@ const RowContent = memo(
|
|||||||
}
|
}
|
||||||
}, [item?.id, registerSongs, songData?.songs]);
|
}, [item?.id, registerSongs, songData?.songs]);
|
||||||
|
|
||||||
const onFavoriteClick = useCallback((song: Song) => {
|
|
||||||
// TODO: toggle favorite for song
|
|
||||||
void song;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return (
|
return (
|
||||||
<>
|
<ItemDetailSkeletonRow
|
||||||
<div className={styles.left}>
|
defaultRowHeight={defaultRowHeight}
|
||||||
<div className={styles.metadata}>
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
<Skeleton className={styles.skeletonImage} />
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
<Skeleton className={styles.skeletonTitle} />
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
<Skeleton className={styles.skeletonArtist} />
|
trackTableSize={trackTableSize}
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,8 +626,8 @@ const RowContent = memo(
|
|||||||
enableVerticalBorders={enableVerticalBorders}
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
internalState={internalState}
|
internalState={internalState}
|
||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
|
isSongsLoading={isSongsLoading}
|
||||||
key={song.id}
|
key={song.id}
|
||||||
onFavoriteClick={onFavoriteClick}
|
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
size={trackTableSize}
|
size={trackTableSize}
|
||||||
song={song as Song}
|
song={song as Song}
|
||||||
@@ -577,6 +642,7 @@ const RowContent = memo(
|
|||||||
prev.index === next.index &&
|
prev.index === next.index &&
|
||||||
prev.data === next.data &&
|
prev.data === next.data &&
|
||||||
prev.columnWidthPercents === next.columnWidthPercents &&
|
prev.columnWidthPercents === next.columnWidthPercents &&
|
||||||
|
prev.defaultRowHeight === next.defaultRowHeight &&
|
||||||
prev.enableAlternateRowColors === next.enableAlternateRowColors &&
|
prev.enableAlternateRowColors === next.enableAlternateRowColors &&
|
||||||
prev.enableHorizontalBorders === next.enableHorizontalBorders &&
|
prev.enableHorizontalBorders === next.enableHorizontalBorders &&
|
||||||
prev.enableRowHoverHighlight === next.enableRowHoverHighlight &&
|
prev.enableRowHoverHighlight === next.enableRowHoverHighlight &&
|
||||||
@@ -706,7 +772,7 @@ export const ItemDetailList = ({
|
|||||||
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
|
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
|
||||||
|
|
||||||
const rowHeight = useDynamicRowHeight({
|
const rowHeight = useDynamicRowHeight({
|
||||||
defaultRowHeight: 300,
|
defaultRowHeight: DEFAULT_ROW_HEIGHT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isInfinite = data !== undefined || getItem !== undefined;
|
const isInfinite = data !== undefined || getItem !== undefined;
|
||||||
@@ -817,6 +883,7 @@ export const ItemDetailList = ({
|
|||||||
columnWidthPercents,
|
columnWidthPercents,
|
||||||
controls,
|
controls,
|
||||||
data: dataSource,
|
data: dataSource,
|
||||||
|
defaultRowHeight: DEFAULT_ROW_HEIGHT,
|
||||||
enableAlternateRowColors,
|
enableAlternateRowColors,
|
||||||
enableHorizontalBorders,
|
enableHorizontalBorders,
|
||||||
enableRowHoverHighlight,
|
enableRowHoverHighlight,
|
||||||
|
|||||||
Reference in New Issue
Block a user