mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
optimize detail columns
This commit is contained in:
@@ -1,3 +1,38 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const ActionsColumn = (_props: ItemDetailListCellProps) => null;
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const ActionsColumn = ({ controls, internalState, song }: ItemDetailListCellProps) => {
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
const index = internalState?.findItemIndex(song.id) ?? -1;
|
||||||
|
controls?.onMore?.({
|
||||||
|
event,
|
||||||
|
index,
|
||||||
|
internalState: internalState ?? undefined,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
icon="ellipsisHorizontal"
|
||||||
|
iconProps={{
|
||||||
|
color: 'muted',
|
||||||
|
size: 'xs',
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const AlbumArtistColumn = ({ song }: ItemDetailListCellProps) =>
|
export const AlbumArtistColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.albumArtistName ?? '—';
|
song.albumArtistName ?? <> </>;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const AlbumColumn = ({ song }: ItemDetailListCellProps) => song.album ?? '—';
|
export const AlbumColumn = ({ song }: ItemDetailListCellProps) => song.album ?? <> </>;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const ArtistColumn = ({ song }: ItemDetailListCellProps) => song.artistName ?? '—';
|
export const ArtistColumn = ({ song }: ItemDetailListCellProps) => song.artistName ?? <> </>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const BitDepthColumn = ({ song }: ItemDetailListCellProps) =>
|
export const BitDepthColumn = ({ song }: ItemDetailListCellProps) => song.bitDepth;
|
||||||
song.bitDepth != null ? String(song.bitDepth) : '—';
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const BitRateColumn = ({ song }: ItemDetailListCellProps) =>
|
export const BitRateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.bitRate != null ? `${song.bitRate} kbps` : '—';
|
song.bitRate != null ? `${song.bitRate} kbps` : <> </>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const BpmColumn = ({ song }: ItemDetailListCellProps) =>
|
export const BpmColumn = ({ song }: ItemDetailListCellProps) => song.bpm ?? <> </>;
|
||||||
song.bpm != null ? String(song.bpm) : '—';
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>
|
export const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.channels != null ? String(song.channels) : '—';
|
song.channels != null ? String(song.channels) : <> </>;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const CodecColumn = ({ song }: ItemDetailListCellProps) => song.container ?? '—';
|
export const CodecColumn = ({ song }: ItemDetailListCellProps) => song.container ?? <> </>;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const CommentColumn = ({ song }: ItemDetailListCellProps) => song.comment ?? '—';
|
export const CommentColumn = ({ song }: ItemDetailListCellProps) => song.comment ?? <> </>;
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import { ItemDetailListCellProps } from './types';
|
|||||||
|
|
||||||
export const ComposerColumn = ({ song }: ItemDetailListCellProps) => {
|
export const ComposerColumn = ({ song }: ItemDetailListCellProps) => {
|
||||||
const composers = song.participants?.composer;
|
const composers = song.participants?.composer;
|
||||||
if (!composers?.length) return '—';
|
if (!composers?.length) return <> </>;
|
||||||
return composers.map((a) => a.name).join(', ');
|
return composers.map((a) => a.name).join(', ');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
import { formatDateAbsolute } from '/@/renderer/utils/format';
|
import { formatDateAbsolute } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
export const DateAddedColumn = ({ song }: ItemDetailListCellProps) =>
|
export const DateAddedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.createdAt ? formatDateAbsolute(song.createdAt) : '—';
|
song.createdAt ? formatDateAbsolute(song.createdAt) : <> </>;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
import { TableColumn } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
interface DefaultColumnProps extends ItemDetailListCellProps {
|
interface DefaultColumnProps extends ItemDetailListCellProps {
|
||||||
columnId: string;
|
columnId: string;
|
||||||
@@ -7,6 +6,6 @@ interface DefaultColumnProps extends ItemDetailListCellProps {
|
|||||||
|
|
||||||
export const DefaultColumn = ({ columnId, song }: DefaultColumnProps) => {
|
export const DefaultColumn = ({ columnId, song }: DefaultColumnProps) => {
|
||||||
const raw = (song as Record<string, unknown>)[columnId];
|
const raw = (song as Record<string, unknown>)[columnId];
|
||||||
if (raw === undefined || raw === null || typeof raw === 'object') return '—';
|
if (raw === undefined || raw === null || typeof raw === 'object') return <> </>;
|
||||||
return String(raw);
|
return String(raw);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const DiscNumberColumn = ({ song }: ItemDetailListCellProps) =>
|
export const DiscNumberColumn = ({ song }: ItemDetailListCellProps) => String(song.discNumber ?? 1);
|
||||||
String(song.discNumber ?? 1);
|
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
|
||||||
|
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||||
|
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const FavoriteColumn = ({
|
export const FavoriteColumn = ({
|
||||||
|
controls,
|
||||||
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
onFavoriteClick,
|
onFavoriteClick,
|
||||||
song,
|
song,
|
||||||
}: ItemDetailListCellProps) => (
|
}: ItemDetailListCellProps) => {
|
||||||
<div
|
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||||
aria-disabled={isMutatingFavorite}
|
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||||
onClick={(event) => {
|
const isMutating = isMutatingFavorite ?? (isMutatingCreateFavorite || isMutatingDeleteFavorite);
|
||||||
event.stopPropagation();
|
const isFavorite = song.userFavorite ?? false;
|
||||||
event.preventDefault();
|
|
||||||
onFavoriteClick?.(song);
|
return (
|
||||||
}}
|
<ActionIcon
|
||||||
onDoubleClick={(event) => {
|
disabled={isMutating}
|
||||||
event.stopPropagation();
|
icon="favorite"
|
||||||
event.preventDefault();
|
iconProps={{
|
||||||
}}
|
color: isFavorite ? 'primary' : 'muted',
|
||||||
role="button"
|
fill: isFavorite ? 'primary' : undefined,
|
||||||
>
|
size: 'xs',
|
||||||
<Icon icon="favorite" size="xs" />
|
}}
|
||||||
</div>
|
onClick={(event) => {
|
||||||
);
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
const index = internalState?.findItemIndex(song.id) ?? -1;
|
||||||
|
if (controls?.onFavorite) {
|
||||||
|
controls.onFavorite({
|
||||||
|
event,
|
||||||
|
favorite: !isFavorite,
|
||||||
|
index,
|
||||||
|
internalState: internalState ?? undefined,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onFavoriteClick?.(song);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDoubleClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const GenreBadgeColumn = ({ song }: ItemDetailListCellProps) =>
|
export const GenreBadgeColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.genres?.length ? song.genres.map((g) => g.name).join(', ') : '—';
|
song.genres?.length ? song.genres.map((g) => g.name).join(', ') : <> </>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const GenreColumn = ({ song }: ItemDetailListCellProps) =>
|
export const GenreColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.genres?.length ? song.genres.map((g) => g.name).join(', ') : '—';
|
song.genres?.length ? song.genres.map((g) => g.name).join(', ') : <> </>;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.compact-container {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
aspect-ratio: unset;
|
||||||
|
padding-top: var(--theme-spacing-xs);
|
||||||
|
padding-bottom: var(--theme-spacing-xs);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import styles from './image-column.module.css';
|
||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const ImageColumn = ({ song }: ItemDetailListCellProps) =>
|
export const ImageColumn = ({ song }: ItemDetailListCellProps) => (
|
||||||
song.imageId ? (
|
<ItemImage
|
||||||
<ItemImage id={song.imageId} itemType={LibraryItem.SONG} type="itemCard" />
|
className={styles.compactImage}
|
||||||
) : (
|
containerClassName={styles.compactContainer}
|
||||||
'—'
|
id={song.imageId}
|
||||||
);
|
itemType={LibraryItem.SONG}
|
||||||
|
type="table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
import { formatDateRelative } from '/@/renderer/utils/format';
|
import { formatDateRelative } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
export const LastPlayedColumn = ({ song }: ItemDetailListCellProps) =>
|
export const LastPlayedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.lastPlayedAt ? formatDateRelative(song.lastPlayedAt) : '—';
|
song.lastPlayedAt ? formatDateRelative(song.lastPlayedAt) : <> </>;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const PathColumn = ({ song }: ItemDetailListCellProps) => song.path ?? '—';
|
export const PathColumn = ({ song }: ItemDetailListCellProps) => song.path ?? <> </>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const PlayCountColumn = ({ song }: ItemDetailListCellProps) =>
|
export const PlayCountColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
String(song.playCount ?? 0);
|
song.playCount ? String(song.playCount) : <> </>;
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
import { ReadOnlyRating } from '/@/shared/components/read-only-rating/read-only-rating';
|
|
||||||
|
|
||||||
export const RatingColumn = ({ song }: ItemDetailListCellProps) => (
|
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||||
<ReadOnlyRating size="md" value={song.userRating ?? undefined} />
|
import { Rating } from '/@/shared/components/rating/rating';
|
||||||
);
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const RatingColumn = ({ controls, internalState, song }: ItemDetailListCellProps) => {
|
||||||
|
const isMutatingRating = useIsMutatingRating();
|
||||||
|
const value = song.userRating ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rating
|
||||||
|
onChange={(rating) => {
|
||||||
|
const index = internalState?.findItemIndex(song.id) ?? -1;
|
||||||
|
controls?.onRating?.({
|
||||||
|
event: null,
|
||||||
|
index,
|
||||||
|
internalState: internalState ?? undefined,
|
||||||
|
item: song,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
rating,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
readOnly={isMutatingRating}
|
||||||
|
size="xs"
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
|
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
|
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : '—';
|
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <> </>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const SampleRateColumn = ({ song }: ItemDetailListCellProps) =>
|
export const SampleRateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.sampleRate != null ? `${song.sampleRate} Hz` : '—';
|
song.sampleRate ? `${song.sampleRate} Hz` : <> </>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
import { formatSizeString } from '/@/renderer/utils/format';
|
import { formatSizeString } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
export const SizeColumn = ({ song }: ItemDetailListCellProps) =>
|
export const SizeColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.size != null ? formatSizeString(song.size) : '—';
|
song.size ? formatSizeString(song.size) : <> </>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const TitleArtistColumn = ({ song }: ItemDetailListCellProps) =>
|
export const TitleArtistColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
[song.name, song.artistName].filter(Boolean).join(' — ') || '—';
|
[song.name, song.artistName].filter(Boolean).join(' — ') ?? <> </>;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const TitleColumn = ({ song }: ItemDetailListCellProps) => song.name ?? '—';
|
export const TitleColumn = ({ song }: ItemDetailListCellProps) => song.name ?? <> </>;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const TitleCombinedColumn = ({ song }: ItemDetailListCellProps) =>
|
export const TitleCombinedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
[song.name, song.artistName].filter(Boolean).join(' — ') || '—';
|
[song.name, song.artistName].filter(Boolean).join(' — ') ?? <> </>;
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { ItemDetailListCellProps } from './types';
|
|||||||
export const TrackNumberColumn = ({ song }: ItemDetailListCellProps) => {
|
export const TrackNumberColumn = ({ song }: ItemDetailListCellProps) => {
|
||||||
const disc = song.discNumber ?? 1;
|
const disc = song.discNumber ?? 1;
|
||||||
const track = song.trackNumber.toString().padStart(2, '0');
|
const track = song.trackNumber.toString().padStart(2, '0');
|
||||||
return `${disc} - ${track}`;
|
return `${disc}-${track}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export interface ItemDetailListCellProps {
|
export interface ItemDetailListCellProps {
|
||||||
|
controls?: ItemControls;
|
||||||
|
internalState?: ItemListStateActions;
|
||||||
isMutatingFavorite?: boolean;
|
isMutatingFavorite?: boolean;
|
||||||
onFavoriteClick?: (song: Song) => void;
|
onFavoriteClick?: (song: Song) => void;
|
||||||
rowIndex?: number;
|
rowIndex?: number;
|
||||||
|
size?: 'compact' | 'default' | 'large';
|
||||||
song: Song;
|
song: Song;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
|
||||||
export const YearColumn = ({ song }: ItemDetailListCellProps) =>
|
export const YearColumn = ({ song }: ItemDetailListCellProps) =>
|
||||||
song.releaseYear != null ? String(song.releaseYear) : '—';
|
song.releaseYear ? String(song.releaseYear) : <> </>;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--theme-spacing-xs);
|
gap: var(--theme-spacing-xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: var(--theme-spacing-sm);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: var(--theme-font-size-md);
|
font-size: var(--theme-font-size-md);
|
||||||
@@ -88,9 +89,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row .tracks-table {
|
.row .tracks-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: var(--theme-font-size-sm);
|
font-size: var(--theme-font-size-sm);
|
||||||
table-layout: fixed;
|
}
|
||||||
|
|
||||||
|
.row .track-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row .track-header-cell {
|
.row .track-header-cell {
|
||||||
@@ -100,6 +110,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row .track-cell {
|
.row .track-cell {
|
||||||
|
min-width: 0;
|
||||||
padding-right: var(--theme-spacing-sm);
|
padding-right: var(--theme-spacing-sm);
|
||||||
padding-left: var(--theme-spacing-sm);
|
padding-left: var(--theme-spacing-sm);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -107,25 +118,38 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row .track-row-size-compact .track-cell {
|
.row .track-row-size-compact {
|
||||||
padding-top: var(--theme-spacing-xs);
|
height: 32px;
|
||||||
padding-bottom: var(--theme-spacing-xs);
|
min-height: 32px;
|
||||||
|
max-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row .track-row-size-default .track-cell {
|
.row .track-row-size-default {
|
||||||
padding-top: var(--theme-spacing-sm);
|
height: 40px;
|
||||||
padding-bottom: var(--theme-spacing-sm);
|
min-height: 40px;
|
||||||
|
max-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row .track-row-size-large .track-cell {
|
.row .track-row-size-large {
|
||||||
padding-top: var(--theme-spacing-md);
|
height: 48px;
|
||||||
padding-bottom: var(--theme-spacing-md);
|
min-height: 48px;
|
||||||
|
max-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row .track-cell-muted {
|
.row .track-cell-muted {
|
||||||
color: var(--theme-colors-foreground-muted);
|
color: var(--theme-colors-foreground-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row .track-cell-image {
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
padding-right: var(--theme-spacing-sm);
|
||||||
|
padding-left: var(--theme-spacing-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.track-row-selected {
|
.track-row-selected {
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: lighten(var(--theme-colors-surface), 5%);
|
background-color: lighten(var(--theme-colors-surface), 5%);
|
||||||
@@ -172,18 +196,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-tracks-size-compact .skeleton-track-row {
|
.skeleton-tracks-size-compact .skeleton-track-row {
|
||||||
padding-top: var(--theme-spacing-xs);
|
height: 32px;
|
||||||
padding-bottom: var(--theme-spacing-xs);
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-tracks-size-default .skeleton-track-row {
|
.skeleton-tracks-size-default .skeleton-track-row {
|
||||||
padding-top: var(--theme-spacing-sm);
|
height: 40px;
|
||||||
padding-bottom: var(--theme-spacing-sm);
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-tracks-size-large .skeleton-track-row {
|
.skeleton-tracks-size-large .skeleton-track-row {
|
||||||
padding-top: var(--theme-spacing-md);
|
height: 48px;
|
||||||
padding-bottom: var(--theme-spacing-md);
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-track-cell {
|
.skeleton-track-cell {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import {
|
|||||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
||||||
import { getDetailListCellComponent } from '/@/renderer/components/item-list/item-detail-list/columns';
|
import { getDetailListCellComponent } from '/@/renderer/components/item-list/item-detail-list/columns';
|
||||||
|
import {
|
||||||
|
getTrackColumnFixed,
|
||||||
|
shouldShowHoverOnlyColumnContent,
|
||||||
|
} from '/@/renderer/components/item-list/item-detail-list/utils';
|
||||||
import {
|
import {
|
||||||
pickTableColumns,
|
pickTableColumns,
|
||||||
SONG_TABLE_COLUMNS,
|
SONG_TABLE_COLUMNS,
|
||||||
@@ -62,6 +66,7 @@ interface RowData {
|
|||||||
interface TrackRowProps {
|
interface TrackRowProps {
|
||||||
columns: ItemTableListColumnConfig[];
|
columns: ItemTableListColumnConfig[];
|
||||||
columnWidthPercents: number[];
|
columnWidthPercents: number[];
|
||||||
|
controls?: ItemControls;
|
||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
isMutatingFavorite: boolean;
|
isMutatingFavorite: boolean;
|
||||||
onFavoriteClick: (song: Song) => void;
|
onFavoriteClick: (song: Song) => void;
|
||||||
@@ -77,6 +82,7 @@ const TrackRow = memo(
|
|||||||
({
|
({
|
||||||
columns,
|
columns,
|
||||||
columnWidthPercents,
|
columnWidthPercents,
|
||||||
|
controls,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
onFavoriteClick,
|
onFavoriteClick,
|
||||||
@@ -85,7 +91,7 @@ const TrackRow = memo(
|
|||||||
song,
|
song,
|
||||||
}: TrackRowProps) => {
|
}: TrackRowProps) => {
|
||||||
const playerContext = usePlayer();
|
const playerContext = usePlayer();
|
||||||
const { dragRef, isDragging } = useItemDragDropState<HTMLTableRowElement>({
|
const { dragRef, isDragging } = useItemDragDropState<HTMLDivElement>({
|
||||||
enableDrag: true,
|
enableDrag: true,
|
||||||
internalState,
|
internalState,
|
||||||
isDataRow: true,
|
isDataRow: true,
|
||||||
@@ -93,6 +99,7 @@ const TrackRow = memo(
|
|||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
playerContext,
|
playerContext,
|
||||||
});
|
});
|
||||||
|
const [isRowHovered, setIsRowHovered] = useState(false);
|
||||||
const isSelected = useItemSelectionState(internalState, song.id);
|
const isSelected = useItemSelectionState(internalState, song.id);
|
||||||
|
|
||||||
const handleRowClick = useCallback(
|
const handleRowClick = useCallback(
|
||||||
@@ -176,8 +183,8 @@ const TrackRow = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<div
|
||||||
className={clsx({
|
className={clsx(styles.trackRow, {
|
||||||
[styles.trackRowDragging]: isDragging,
|
[styles.trackRowDragging]: isDragging,
|
||||||
[styles.trackRowSelected]: isSelected,
|
[styles.trackRowSelected]: isSelected,
|
||||||
[styles.trackRowSizeCompact]: size === 'compact',
|
[styles.trackRowSizeCompact]: size === 'compact',
|
||||||
@@ -185,45 +192,62 @@ const TrackRow = memo(
|
|||||||
[styles.trackRowSizeLarge]: size === 'large',
|
[styles.trackRowSizeLarge]: size === 'large',
|
||||||
})}
|
})}
|
||||||
onClick={handleRowClick}
|
onClick={handleRowClick}
|
||||||
|
onMouseEnter={() => setIsRowHovered(true)}
|
||||||
|
onMouseLeave={() => setIsRowHovered(false)}
|
||||||
ref={dragRef ?? undefined}
|
ref={dragRef ?? undefined}
|
||||||
|
role="row"
|
||||||
>
|
>
|
||||||
{columns.map((col, colIndex) => {
|
{columns.map((col, colIndex) => {
|
||||||
const percent = columnWidthPercents[colIndex] ?? 0;
|
const percent = columnWidthPercents[colIndex] ?? 0;
|
||||||
|
const { fixedWidth, isFixedColumn } = getTrackColumnFixed(col.id);
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
|
flex: isFixedColumn ? `0 0 ${fixedWidth}px` : `${percent} 1 0`,
|
||||||
fontFamily:
|
fontFamily:
|
||||||
col.id === TableColumn.DURATION || col.id === TableColumn.TRACK_NUMBER
|
col.id === TableColumn.DURATION || col.id === TableColumn.TRACK_NUMBER
|
||||||
? 'monospace'
|
? 'monospace'
|
||||||
: undefined,
|
: undefined,
|
||||||
minWidth: 0,
|
minWidth: isFixedColumn ? fixedWidth : 0,
|
||||||
textAlign: textAlignFromAlign(col.align),
|
textAlign: textAlignFromAlign(col.align),
|
||||||
width: `${percent}%`,
|
|
||||||
};
|
};
|
||||||
const CellComponent = getDetailListCellComponent(col.id);
|
const CellComponent = getDetailListCellComponent(col.id);
|
||||||
const content = (
|
const isTitleColumn = col.id === TableColumn.TITLE;
|
||||||
|
const isImageColumn = col.id === TableColumn.IMAGE;
|
||||||
|
const showHoverContent = shouldShowHoverOnlyColumnContent(
|
||||||
|
col.id,
|
||||||
|
isRowHovered,
|
||||||
|
song,
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = showHoverContent ? (
|
||||||
<CellComponent
|
<CellComponent
|
||||||
columnId={col.id}
|
columnId={col.id}
|
||||||
|
controls={controls}
|
||||||
|
internalState={internalState}
|
||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
onFavoriteClick={onFavoriteClick}
|
onFavoriteClick={onFavoriteClick}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
|
size={size}
|
||||||
song={song}
|
song={song}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
'\u00A0'
|
||||||
);
|
);
|
||||||
|
|
||||||
const isTitleColumn = col.id === TableColumn.TITLE;
|
|
||||||
return (
|
return (
|
||||||
<td
|
<div
|
||||||
className={clsx(
|
className={clsx(styles.trackCell, {
|
||||||
styles.trackCell,
|
[styles.trackCellImage]: isImageColumn,
|
||||||
!isTitleColumn && styles.trackCellMuted,
|
[styles.trackCellMuted]: !isTitleColumn,
|
||||||
)}
|
})}
|
||||||
key={col.id}
|
key={col.id}
|
||||||
|
role="cell"
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</td>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -357,23 +381,22 @@ const RowContent = memo(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.right}>
|
<div className={styles.right}>
|
||||||
<table className={styles.tracksTable}>
|
<div className={styles.tracksTable} role="table">
|
||||||
<tbody>
|
{songs.map((song, rowIndex) => (
|
||||||
{songs.map((song, rowIndex) => (
|
<TrackRow
|
||||||
<TrackRow
|
columns={trackColumns}
|
||||||
columns={trackColumns}
|
columnWidthPercents={columnWidthPercents}
|
||||||
columnWidthPercents={columnWidthPercents}
|
controls={controls}
|
||||||
internalState={internalState}
|
internalState={internalState}
|
||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
key={song.id}
|
key={song.id}
|
||||||
onFavoriteClick={onFavoriteClick}
|
onFavoriteClick={onFavoriteClick}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
size={trackTableSize}
|
size={trackTableSize}
|
||||||
song={song as Song}
|
song={song as Song}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
|
||||||
|
[TableColumn.ACTIONS]: 48,
|
||||||
|
[TableColumn.TRACK_NUMBER]: 56,
|
||||||
|
[TableColumn.USER_FAVORITE]: 48,
|
||||||
|
[TableColumn.USER_RATING]: 76,
|
||||||
|
};
|
||||||
|
|
||||||
|
const HOVER_ONLY_COLUMNS: TableColumn[] = [
|
||||||
|
TableColumn.ACTIONS,
|
||||||
|
TableColumn.USER_FAVORITE,
|
||||||
|
TableColumn.USER_RATING,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getTrackColumnFixed(columnId: TableColumn): {
|
||||||
|
fixedWidth: number;
|
||||||
|
isFixedColumn: boolean;
|
||||||
|
} {
|
||||||
|
const width = FIXED_TRACK_COLUMN_WIDTHS[columnId];
|
||||||
|
return width !== undefined
|
||||||
|
? { fixedWidth: width, isFixedColumn: true }
|
||||||
|
: { fixedWidth: 0, isFixedColumn: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTrackColumnHoverOnly(columnId: TableColumn): boolean {
|
||||||
|
return HOVER_ONLY_COLUMNS.includes(columnId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowHoverOnlyColumnContent(
|
||||||
|
columnId: TableColumn,
|
||||||
|
isRowHovered: boolean,
|
||||||
|
song: { userFavorite?: boolean | null; userRating?: null | number },
|
||||||
|
): boolean {
|
||||||
|
if (!HOVER_ONLY_COLUMNS.includes(columnId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isRowHovered ||
|
||||||
|
(columnId === TableColumn.USER_FAVORITE && song.userFavorite !== false) ||
|
||||||
|
(columnId === TableColumn.USER_RATING && song.userRating != null)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user