mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-08 11:40:03 +02:00
Add album detail list view (#1681)
This commit is contained in:
@@ -1,84 +0,0 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: var(--theme-spacing-sm);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--theme-spacing-sm);
|
||||
container-type: inline-size;
|
||||
background: var(--theme-colors-surface);
|
||||
border-radius: var(--theme-radius-md);
|
||||
|
||||
@container (min-width: 500px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: none;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
aspect-ratio: 1/1;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background-color: rgb(0 0 0);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 500px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.metadata-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-sm);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--theme-spacing-xs) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metadata-container .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.metadata-container .header .title {
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metadata-container .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-xs);
|
||||
}
|
||||
|
||||
.metadata-container .content .tags {
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// import { AnimatePresence } from 'motion/react';
|
||||
// import { MouseEvent, useMemo, useState } from 'react';
|
||||
// import { Link } from 'react-router';
|
||||
|
||||
// import styles from './item-detail.module.css';
|
||||
|
||||
// import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||
// import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
// import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
// import { Badge } from '/@/shared/components/badge/badge';
|
||||
// import { Divider } from '/@/shared/components/divider/divider';
|
||||
// import { Group } from '/@/shared/components/group/group';
|
||||
// import { Image } from '/@/shared/components/image/image';
|
||||
// import { Rating } from '/@/shared/components/rating/rating';
|
||||
// import { Text } from '/@/shared/components/text/text';
|
||||
// import {
|
||||
// Album,
|
||||
// AlbumArtist,
|
||||
// Artist,
|
||||
// LibraryItem,
|
||||
// Playlist,
|
||||
// Song,
|
||||
// } from '/@/shared/types/domain-types';
|
||||
// import { stringToColor } from '/@/shared/utils/string-to-color';
|
||||
|
||||
// interface ItemDetailProps {
|
||||
// data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
|
||||
// itemHeight: number;
|
||||
// itemType: LibraryItem;
|
||||
// onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
|
||||
// withControls?: boolean;
|
||||
// }
|
||||
|
||||
// export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
|
||||
// const imageUrl = getImageUrl(data);
|
||||
|
||||
// const [showControls, setShowControls] = useState(false);
|
||||
|
||||
// const { background } = useFastAverageColor({
|
||||
// algorithm: 'simple',
|
||||
// src: imageUrl,
|
||||
// srcLoaded: false,
|
||||
// });
|
||||
|
||||
// // const tags = [...(data?.genres ?? [])];
|
||||
|
||||
// const tags = useMemo(() => {
|
||||
// if (!data) {
|
||||
// return [];
|
||||
// }
|
||||
|
||||
// const items: {
|
||||
// color?: string;
|
||||
// id: string;
|
||||
// isLight?: boolean;
|
||||
// itemType: LibraryItem;
|
||||
// name: string;
|
||||
// }[] = [];
|
||||
|
||||
// if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
|
||||
// data.albumArtists?.forEach((tag: { id: string; name: string }) => {
|
||||
// items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
|
||||
// });
|
||||
// }
|
||||
|
||||
// if ('genres' in data && Array.isArray(data.genres)) {
|
||||
// data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
|
||||
// const { color, isLight } = stringToColor(tag.name);
|
||||
// items.push({ ...tag, color, isLight });
|
||||
// });
|
||||
// }
|
||||
|
||||
// // if ('tags' in data && typeof data.tags === 'object') {
|
||||
// // console.log('data.tags :>> ', data.tags);
|
||||
// // Object.entries(data.tags).forEach(([key, value]) => {
|
||||
// // items.push({ id: key, itemType: LibraryItem.TAG, name: value });
|
||||
// // });
|
||||
// // }
|
||||
|
||||
// return items;
|
||||
// }, [data]);
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// className={styles.container}
|
||||
// onClick={(e) => onClick?.(e, data, itemType)}
|
||||
// style={{ backgroundColor: background }}
|
||||
// >
|
||||
// <div
|
||||
// className={styles.imageContainer}
|
||||
// onMouseEnter={() => withControls && setShowControls(true)}
|
||||
// onMouseLeave={() => withControls && setShowControls(false)}
|
||||
// >
|
||||
// <Image alt={data?.name} src={imageUrl} />
|
||||
// <AnimatePresence>
|
||||
// {withControls && showControls && <ItemCardControls type="compact" />}
|
||||
// </AnimatePresence>
|
||||
// </div>
|
||||
// <div className={styles.metadataContainer}>
|
||||
// <div className={styles.header}>
|
||||
// <Text className={styles.title} component={Link} isLink size="lg" weight={500}>
|
||||
// {data?.name}
|
||||
// </Text>
|
||||
// <Group>
|
||||
// {data && 'userRating' in data && (
|
||||
// <Rating size="xs" value={data?.userRating ?? 0} />
|
||||
// )}
|
||||
// {data && 'userFavorite' in data && (
|
||||
// <ActionIcon
|
||||
// icon="favorite"
|
||||
// iconProps={{
|
||||
// fill: data?.userFavorite ? 'primary' : 'default',
|
||||
// }}
|
||||
// size="xs"
|
||||
// />
|
||||
// )}
|
||||
// </Group>
|
||||
// </div>
|
||||
// <Divider />
|
||||
// <div className={styles.content}>
|
||||
// <Group className={styles.tags} gap="xs">
|
||||
// {tags.map((tag) => (
|
||||
// <Badge
|
||||
// key={tag.id}
|
||||
// style={{
|
||||
// backgroundColor: tag.color,
|
||||
// color: tag.isLight ? 'black' : 'white',
|
||||
// }}
|
||||
// >
|
||||
// {tag.name}
|
||||
// </Badge>
|
||||
// ))}
|
||||
// </Group>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
|
||||
// if (data && 'imageUrl' in data) {
|
||||
// return data.imageUrl || undefined;
|
||||
// }
|
||||
|
||||
// return undefined;
|
||||
// };
|
||||
@@ -192,9 +192,10 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
onColumnReordered?.(columnIdFrom, columnIdTo, edge);
|
||||
},
|
||||
|
||||
onColumnResized: ({ columnId, width }: { columnId: TableColumn; width: number }) => {
|
||||
onColumnResized?.(columnId, width);
|
||||
},
|
||||
onColumnResized: onColumnResized
|
||||
? ({ columnId, width }: { columnId: TableColumn; width: number }) =>
|
||||
onColumnResized(columnId, width)
|
||||
: undefined,
|
||||
|
||||
onDoubleClick: ({ internalState, item, itemType, meta }: DefaultItemControlProps) => {
|
||||
if (!item || !internalState) {
|
||||
@@ -241,11 +242,13 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const singleSongOnly = meta?.singleSongOnly === true;
|
||||
|
||||
// For NEXT, LAST, NEXT_SHUFFLE, and LAST_SHUFFLE, only add the clicked song
|
||||
// For NOW and SHUFFLE, add a range of songs around the clicked song
|
||||
// For single-song actions (e.g. image play button), or NEXT/LAST/..., only add the clicked song
|
||||
// For row double-click with NOW/SHUFFLE, add a range of songs around the clicked song
|
||||
let songsToAdd: Song[];
|
||||
if (
|
||||
singleSongOnly ||
|
||||
playType === Play.NEXT ||
|
||||
playType === Play.LAST ||
|
||||
playType === Play.NEXT_SHUFFLE ||
|
||||
|
||||
@@ -7,14 +7,19 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
interface UseItemListColumnReorderProps {
|
||||
itemListKey: ItemListKey;
|
||||
tableKey?: 'detail' | 'main';
|
||||
}
|
||||
|
||||
export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReorderProps) => {
|
||||
export const useItemListColumnReorder = ({
|
||||
itemListKey,
|
||||
tableKey = 'main',
|
||||
}: UseItemListColumnReorderProps) => {
|
||||
const { setList } = useSettingsStoreActions();
|
||||
|
||||
const handleColumnReordered = useCallback(
|
||||
(columnIdFrom: TableColumn, columnIdTo: TableColumn, edge: Edge | null) => {
|
||||
const columns = useSettingsStore.getState().lists[itemListKey]?.table.columns;
|
||||
const list = useSettingsStore.getState().lists[itemListKey];
|
||||
const columns = tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
|
||||
|
||||
if (!columns) {
|
||||
return;
|
||||
@@ -83,13 +88,20 @@ export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReord
|
||||
// Insert the column at the new position
|
||||
newColumns.splice(newIndex, 0, updatedMovedColumn);
|
||||
|
||||
setList(itemListKey, {
|
||||
table: {
|
||||
columns: newColumns,
|
||||
},
|
||||
});
|
||||
if (tableKey === 'detail') {
|
||||
type SetListData = Parameters<
|
||||
ReturnType<typeof useSettingsStoreActions>['setList']
|
||||
>[1];
|
||||
setList(itemListKey, { detail: { columns: newColumns } } as SetListData);
|
||||
} else {
|
||||
setList(itemListKey, {
|
||||
table: {
|
||||
columns: newColumns,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[itemListKey, setList],
|
||||
[itemListKey, setList, tableKey],
|
||||
);
|
||||
|
||||
return { handleColumnReordered };
|
||||
|
||||
@@ -5,11 +5,18 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
interface UseItemListColumnResizeProps {
|
||||
itemListKey: ItemListKey;
|
||||
tableKey?: 'detail' | 'main';
|
||||
}
|
||||
|
||||
export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResizeProps) => {
|
||||
export const useItemListColumnResize = ({
|
||||
itemListKey,
|
||||
tableKey = 'main',
|
||||
}: UseItemListColumnResizeProps) => {
|
||||
const { setList } = useSettingsStoreActions();
|
||||
const columns = useSettingsStore((state) => state.lists[itemListKey]?.table.columns);
|
||||
const columns = useSettingsStore((state) => {
|
||||
const list = state.lists[itemListKey];
|
||||
return tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
|
||||
});
|
||||
|
||||
const handleColumnResized = useCallback(
|
||||
(columnId: TableColumn, width: number) => {
|
||||
@@ -19,13 +26,20 @@ export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResize
|
||||
column.id === columnId ? { ...column, width } : column,
|
||||
);
|
||||
|
||||
setList(itemListKey, {
|
||||
table: {
|
||||
columns: updatedColumns,
|
||||
},
|
||||
});
|
||||
if (tableKey === 'detail') {
|
||||
type SetListData = Parameters<
|
||||
ReturnType<typeof useSettingsStoreActions>['setList']
|
||||
>[1];
|
||||
setList(itemListKey, { detail: { columns: updatedColumns } } as SetListData);
|
||||
} else {
|
||||
setList(itemListKey, {
|
||||
table: {
|
||||
columns: updatedColumns,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[columns, itemListKey, setList],
|
||||
[columns, itemListKey, setList, tableKey],
|
||||
);
|
||||
|
||||
return { handleColumnResized };
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { useLocation, useNavigationType } from 'react-router';
|
||||
|
||||
import { parseIntParam, setSearchParam } from '/@/renderer/utils/query-params';
|
||||
import { useScrollStore } from '/@/renderer/store/scroll.store';
|
||||
|
||||
interface UseItemListScrollPersistProps {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistProps) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigationType = useNavigationType();
|
||||
const setOffset = useScrollStore((s) => s.setOffset);
|
||||
const getOffset = useScrollStore((s) => s.getOffset);
|
||||
|
||||
const scrollOffset = useMemo(() => parseIntParam(searchParams, 'scrollOffset'), [searchParams]);
|
||||
const scrollOffset = useMemo(() => {
|
||||
if (navigationType !== 'POP') return undefined;
|
||||
return getOffset(location.key);
|
||||
}, [getOffset, location.key, navigationType]);
|
||||
|
||||
const handleOnScrollEnd = useCallback(
|
||||
(offset: number) => {
|
||||
if (!enabled) return;
|
||||
|
||||
setSearchParams((prev) => setSearchParam(prev, 'scrollOffset', offset), {
|
||||
replace: true,
|
||||
});
|
||||
setOffset(location.key, offset);
|
||||
},
|
||||
[enabled, setSearchParams],
|
||||
[enabled, location.key, setOffset],
|
||||
);
|
||||
|
||||
return { handleOnScrollEnd, scrollOffset };
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import {
|
||||
JOINED_ARTISTS_MUTED_PROPS,
|
||||
JoinedArtists,
|
||||
} from '/@/renderer/features/albums/components/joined-artists';
|
||||
|
||||
export const AlbumArtistColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
|
||||
const name = song.albumArtistName?.trim() ?? '';
|
||||
const hasArtists = name.length > 0 || (song.albumArtists?.length ?? 0) > 0;
|
||||
|
||||
if (!hasArtists) return <> </>;
|
||||
|
||||
return (
|
||||
<JoinedArtists
|
||||
artistName={song.albumArtistName ?? ''}
|
||||
artists={song.albumArtists ?? []}
|
||||
linkProps={JOINED_ARTISTS_MUTED_PROPS.linkProps}
|
||||
readOnly={!isRowHovered}
|
||||
rootTextProps={JOINED_ARTISTS_MUTED_PROPS.rootTextProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const AlbumColumn = ({ song }: ItemDetailListCellProps) => song.album ?? <> </>;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import {
|
||||
JOINED_ARTISTS_MUTED_PROPS,
|
||||
JoinedArtists,
|
||||
} from '/@/renderer/features/albums/components/joined-artists';
|
||||
|
||||
export const ArtistColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
|
||||
const name = song.artistName?.trim() ?? '';
|
||||
const hasArtists = name.length > 0 || (song.artists?.length ?? 0) > 0;
|
||||
|
||||
if (!hasArtists) return <> </>;
|
||||
|
||||
return (
|
||||
<JoinedArtists
|
||||
artistName={song.artistName ?? ''}
|
||||
artists={song.artists ?? []}
|
||||
linkProps={JOINED_ARTISTS_MUTED_PROPS.linkProps}
|
||||
readOnly={!isRowHovered}
|
||||
rootTextProps={JOINED_ARTISTS_MUTED_PROPS.rootTextProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const BitDepthColumn = ({ song }: ItemDetailListCellProps) => song.bitDepth;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const BitRateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.bitRate != null ? `${song.bitRate} kbps` : <> </>;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const BpmColumn = ({ song }: ItemDetailListCellProps) => song.bpm ?? <> </>;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.channels != null ? String(song.channels) : <> </>;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const CodecColumn = ({ song }: ItemDetailListCellProps) => song.container ?? <> </>;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const CommentColumn = ({ song }: ItemDetailListCellProps) => song.comment ?? <> </>;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const ComposerColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const composers = song.participants?.composer;
|
||||
if (!composers?.length) return <> </>;
|
||||
return composers.map((a) => a.name).join(', ');
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { formatDateAbsolute } from '/@/renderer/utils/format';
|
||||
|
||||
export const DateAddedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.createdAt ? formatDateAbsolute(song.createdAt) : <> </>;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
interface DefaultColumnProps extends ItemDetailListCellProps {
|
||||
columnId: string;
|
||||
}
|
||||
|
||||
export const DefaultColumn = ({ columnId, song }: DefaultColumnProps) => {
|
||||
const raw = (song as Record<string, unknown>)[columnId];
|
||||
if (raw === undefined || raw === null || typeof raw === 'object') return <> </>;
|
||||
return String(raw);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const DiscNumberColumn = ({ song }: ItemDetailListCellProps) => String(song.discNumber ?? 1);
|
||||
@@ -0,0 +1,5 @@
|
||||
import formatDuration from 'format-duration';
|
||||
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const DurationColumn = ({ song }: ItemDetailListCellProps) => formatDuration(song.duration);
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
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 = ({
|
||||
controls,
|
||||
internalState,
|
||||
isMutatingFavorite,
|
||||
onFavoriteClick,
|
||||
song,
|
||||
}: ItemDetailListCellProps) => {
|
||||
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||
const isMutating = isMutatingFavorite ?? (isMutatingCreateFavorite || isMutatingDeleteFavorite);
|
||||
const isFavorite = song.userFavorite ?? false;
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
disabled={isMutating}
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
color: isFavorite ? 'primary' : 'muted',
|
||||
fill: isFavorite ? 'primary' : undefined,
|
||||
size: 'xs',
|
||||
}}
|
||||
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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
.group {
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--theme-spacing-sm) var(--theme-spacing-xs);
|
||||
min-width: 0;
|
||||
padding: var(--theme-spacing-xs) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group a {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import styles from './genre-badge-column.module.css';
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { stringToColor } from '/@/shared/utils/string-to-color';
|
||||
|
||||
const MAX_GENRES = 4;
|
||||
|
||||
export const GenreBadgeColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const genres = song.genres;
|
||||
|
||||
const genresWithStyle = useMemo(() => {
|
||||
if (!genres) return [];
|
||||
return genres.slice(0, MAX_GENRES).map((genre) => {
|
||||
const { color, isLight } = stringToColor(genre.name);
|
||||
const path = generatePath(AppRoute.LIBRARY_GENRES_DETAIL, { genreId: genre.id });
|
||||
return { ...genre, color, isLight, path };
|
||||
});
|
||||
}, [genres]);
|
||||
|
||||
if (!genresWithStyle.length) return <> </>;
|
||||
|
||||
return (
|
||||
<Group className={styles.group} wrap="nowrap">
|
||||
{genresWithStyle.map((genre) => (
|
||||
<Badge
|
||||
component={Link}
|
||||
key={genre.id}
|
||||
state={{ item: genre }}
|
||||
style={{
|
||||
backgroundColor: genre.color,
|
||||
color: genre.isLight ? 'black' : 'white',
|
||||
}}
|
||||
to={genre.path}
|
||||
>
|
||||
{genre.name}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Fragment } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
const TEXT_PROPS = { isMuted: true, isNoSelect: true, size: 'sm' as const } as const;
|
||||
|
||||
export const GenreColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
|
||||
const genres = song.genres ?? [];
|
||||
if (!genres.length) return <> </>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{genres.map((genre, index) => (
|
||||
<Fragment key={genre.id}>
|
||||
{isRowHovered ? (
|
||||
<Text
|
||||
component={Link}
|
||||
isLink
|
||||
state={{ item: genre }}
|
||||
to={generatePath(AppRoute.LIBRARY_GENRES_DETAIL, {
|
||||
genreId: genre.id,
|
||||
})}
|
||||
{...TEXT_PROPS}
|
||||
>
|
||||
{genre.name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text component="span" {...TEXT_PROPS}>
|
||||
{genre.name}
|
||||
</Text>
|
||||
)}
|
||||
{index < genres.length - 1 && ', '}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.play-button-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
opacity: 0.6;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.play-button-overlay:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.play-button-overlay button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.compact-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import styles from './image-column.module.css';
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||
import {
|
||||
LONG_PRESS_PLAY_BEHAVIOR,
|
||||
PlayTooltip,
|
||||
} from '/@/renderer/features/shared/components/play-button-group';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const ImageColumn = ({
|
||||
controls,
|
||||
internalState,
|
||||
rowIndex = 0,
|
||||
song,
|
||||
}: ItemDetailListCellProps) => {
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handlePlay = (playType: Play) => {
|
||||
if (!song || !controls?.onDoubleClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.onDoubleClick({
|
||||
event: null,
|
||||
index: rowIndex,
|
||||
internalState,
|
||||
item: song,
|
||||
itemType: LibraryItem.SONG,
|
||||
meta: { playType, singleSongOnly: true },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<ItemImage
|
||||
className={styles.compactImage}
|
||||
containerClassName={styles.compactContainer}
|
||||
explicitStatus={song.explicitStatus}
|
||||
id={song.imageId}
|
||||
itemType={LibraryItem.SONG}
|
||||
serverId={song._serverId}
|
||||
type="table"
|
||||
/>
|
||||
{isHovered && (
|
||||
<div className={clsx(styles.playButtonOverlay)}>
|
||||
<PlayTooltip disabled={false} type={playButtonBehavior}>
|
||||
<PlayButton
|
||||
fill
|
||||
onClick={() => handlePlay(playButtonBehavior)}
|
||||
onLongPress={() =>
|
||||
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior])
|
||||
}
|
||||
/>
|
||||
</PlayTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import type { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { ActionsColumn } from './actions-column';
|
||||
import { AlbumArtistColumn } from './album-artist-column';
|
||||
import { AlbumColumn } from './album-column';
|
||||
import { ArtistColumn } from './artist-column';
|
||||
import { BitDepthColumn } from './bit-depth-column';
|
||||
import { BitRateColumn } from './bit-rate-column';
|
||||
import { BpmColumn } from './bpm-column';
|
||||
import { ChannelsColumn } from './channels-column';
|
||||
import { CodecColumn } from './codec-column';
|
||||
import { CommentColumn } from './comment-column';
|
||||
import { ComposerColumn } from './composer-column';
|
||||
import { DateAddedColumn } from './date-added-column';
|
||||
import { DefaultColumn } from './default-column';
|
||||
import { DiscNumberColumn } from './disc-number-column';
|
||||
import { DurationColumn } from './duration-column';
|
||||
import { FavoriteColumn } from './favorite-column';
|
||||
import { GenreBadgeColumn } from './genre-badge-column';
|
||||
import { GenreColumn } from './genre-column';
|
||||
import { ImageColumn } from './image-column';
|
||||
import { LastPlayedColumn } from './last-played-column';
|
||||
import { PathColumn } from './path-column';
|
||||
import { PlayCountColumn } from './play-count-column';
|
||||
import { RatingColumn } from './rating-column';
|
||||
import { ReleaseDateColumn } from './release-date-column';
|
||||
import { RowIndexColumn } from './row-index-column';
|
||||
import { SampleRateColumn } from './sample-rate-column';
|
||||
import { SizeColumn } from './size-column';
|
||||
import { TitleArtistColumn } from './title-artist-column';
|
||||
import { TitleColumn } from './title-column';
|
||||
import { TitleCombinedColumn } from './title-combined-column';
|
||||
import { TrackNumberColumn } from './track-number-column';
|
||||
import { YearColumn } from './year-column';
|
||||
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
type CellComponent = (props: ItemDetailListCellProps) => ReactNode;
|
||||
|
||||
const COLUMN_MAP: Partial<Record<TableColumn, CellComponent>> = {
|
||||
[TableColumn.ACTIONS]: ActionsColumn,
|
||||
[TableColumn.ALBUM]: AlbumColumn,
|
||||
[TableColumn.ALBUM_ARTIST]: AlbumArtistColumn,
|
||||
[TableColumn.ARTIST]: ArtistColumn,
|
||||
[TableColumn.BIT_DEPTH]: BitDepthColumn,
|
||||
[TableColumn.BIT_RATE]: BitRateColumn,
|
||||
[TableColumn.BPM]: BpmColumn,
|
||||
[TableColumn.CHANNELS]: ChannelsColumn,
|
||||
[TableColumn.CODEC]: CodecColumn,
|
||||
[TableColumn.COMMENT]: CommentColumn,
|
||||
[TableColumn.COMPOSER]: ComposerColumn,
|
||||
[TableColumn.DATE_ADDED]: DateAddedColumn,
|
||||
[TableColumn.DISC_NUMBER]: DiscNumberColumn,
|
||||
[TableColumn.DURATION]: DurationColumn,
|
||||
[TableColumn.GENRE]: GenreColumn,
|
||||
[TableColumn.GENRE_BADGE]: GenreBadgeColumn,
|
||||
[TableColumn.IMAGE]: ImageColumn,
|
||||
[TableColumn.LAST_PLAYED]: LastPlayedColumn,
|
||||
[TableColumn.PATH]: PathColumn,
|
||||
[TableColumn.PLAY_COUNT]: PlayCountColumn,
|
||||
[TableColumn.RELEASE_DATE]: ReleaseDateColumn,
|
||||
[TableColumn.ROW_INDEX]: RowIndexColumn,
|
||||
[TableColumn.SAMPLE_RATE]: SampleRateColumn,
|
||||
[TableColumn.SIZE]: SizeColumn,
|
||||
[TableColumn.TITLE]: TitleColumn,
|
||||
[TableColumn.TITLE_ARTIST]: TitleArtistColumn,
|
||||
[TableColumn.TITLE_COMBINED]: TitleCombinedColumn,
|
||||
[TableColumn.TRACK_NUMBER]: TrackNumberColumn,
|
||||
[TableColumn.USER_FAVORITE]: FavoriteColumn,
|
||||
[TableColumn.USER_RATING]: RatingColumn,
|
||||
[TableColumn.YEAR]: YearColumn,
|
||||
};
|
||||
|
||||
export type DetailListCellComponentProps = ItemDetailListCellProps & { columnId?: string };
|
||||
|
||||
export function getDetailListCellComponent(
|
||||
columnId: string | TableColumn,
|
||||
): (props: DetailListCellComponentProps) => ReactNode {
|
||||
const Component = COLUMN_MAP[columnId as TableColumn];
|
||||
if (Component) {
|
||||
return Component as (props: DetailListCellComponentProps) => ReactNode;
|
||||
}
|
||||
return (props: DetailListCellComponentProps) =>
|
||||
React.createElement(DefaultColumn, {
|
||||
columnId: props.columnId ?? (columnId as string),
|
||||
song: props.song,
|
||||
});
|
||||
}
|
||||
|
||||
export type { ItemDetailListCellProps } from './types';
|
||||
|
||||
export {
|
||||
ActionsColumn,
|
||||
AlbumArtistColumn,
|
||||
AlbumColumn,
|
||||
ArtistColumn,
|
||||
BitDepthColumn,
|
||||
BitRateColumn,
|
||||
BpmColumn,
|
||||
ChannelsColumn,
|
||||
CodecColumn,
|
||||
CommentColumn,
|
||||
ComposerColumn,
|
||||
DateAddedColumn,
|
||||
DefaultColumn,
|
||||
DiscNumberColumn,
|
||||
DurationColumn,
|
||||
FavoriteColumn,
|
||||
GenreBadgeColumn,
|
||||
GenreColumn,
|
||||
ImageColumn,
|
||||
LastPlayedColumn,
|
||||
PathColumn,
|
||||
PlayCountColumn,
|
||||
RatingColumn,
|
||||
ReleaseDateColumn,
|
||||
RowIndexColumn,
|
||||
SampleRateColumn,
|
||||
SizeColumn,
|
||||
TitleArtistColumn,
|
||||
TitleColumn,
|
||||
TitleCombinedColumn,
|
||||
TrackNumberColumn,
|
||||
YearColumn,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { formatDateRelative } from '/@/renderer/utils/format';
|
||||
|
||||
export const LastPlayedColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.lastPlayedAt ? formatDateRelative(song.lastPlayedAt) : <> </>;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const PathColumn = ({ song }: ItemDetailListCellProps) => song.path ?? <> </>;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const PlayCountColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.playCount ? String(song.playCount) : <> </>;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
|
||||
|
||||
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <> </>;
|
||||
@@ -0,0 +1,5 @@
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import styles from './row-index-column.module.css';
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||
import { usePlayerStatus } from '/@/renderer/store';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export const RowIndexColumn = ({ rowIndex, song }: ItemDetailListCellProps) => {
|
||||
const status = usePlayerStatus();
|
||||
const { isActive } = useIsCurrentSong(song);
|
||||
const isPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
<div className={styles.iconWrapper}>
|
||||
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{String((rowIndex ?? 0) + 1)}</>;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const SampleRateColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.sampleRate ? `${song.sampleRate} Hz` : <> </>;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { formatSizeString } from '/@/renderer/utils/format';
|
||||
|
||||
export const SizeColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.size ? formatSizeString(song.size) : <> </>;
|
||||
@@ -0,0 +1,18 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './title-column.module.css';
|
||||
|
||||
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
|
||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
|
||||
|
||||
export const TitleArtistColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const { isActive } = useIsCurrentSong(song);
|
||||
|
||||
return (
|
||||
<span className={clsx({ [styles.active]: isActive })}>
|
||||
<ExplicitIndicator explicitStatus={song.explicitStatus} />
|
||||
{[song.name, song.artistName].filter(Boolean).join(' — ') ?? <> </>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.active {
|
||||
color: var(--theme-colors-primary);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './title-column.module.css';
|
||||
|
||||
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
|
||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
|
||||
|
||||
export const TitleColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const { isActive } = useIsCurrentSong(song);
|
||||
|
||||
return (
|
||||
<span className={clsx({ [styles.active]: isActive })}>
|
||||
<ExplicitIndicator explicitStatus={song.explicitStatus} />
|
||||
{song.name ?? <> </>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './title-column.module.css';
|
||||
|
||||
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
|
||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
|
||||
|
||||
export const TitleCombinedColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const { isActive } = useIsCurrentSong(song);
|
||||
|
||||
return (
|
||||
<span className={clsx({ [styles.active]: isActive })}>
|
||||
<ExplicitIndicator explicitStatus={song.explicitStatus} />
|
||||
{[song.name, song.artistName].filter(Boolean).join(' — ') ?? <> </>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const TrackNumberColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
const disc = song.discNumber ?? 1;
|
||||
const track = song.trackNumber.toString().padStart(2, '0');
|
||||
return `${disc}-${track}`;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
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';
|
||||
|
||||
export interface ItemDetailListCellProps {
|
||||
controls?: ItemControls;
|
||||
internalState?: ItemListStateActions;
|
||||
isMutatingFavorite?: boolean;
|
||||
isRowHovered?: boolean;
|
||||
onFavoriteClick?: (song: Song) => void;
|
||||
rowIndex?: number;
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
song: Song;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
export const YearColumn = ({ song }: ItemDetailListCellProps) =>
|
||||
song.releaseYear ? String(song.releaseYear) : <> </>;
|
||||
@@ -0,0 +1,556 @@
|
||||
.container {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.detail-list-header {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
grid-template-columns: 240px 1fr;
|
||||
gap: var(--theme-spacing-md);
|
||||
padding: 0 var(--theme-spacing-md);
|
||||
font-size: var(--theme-font-size-sm);
|
||||
user-select: none;
|
||||
background-color: var(--theme-colors-background);
|
||||
border-bottom: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-left-album-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--theme-font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--theme-colors-foreground);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tracks-table-header {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tracks-table-header-size-compact {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.tracks-table-header-size-default {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.tracks-table-header-size-large {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.track-header-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 60%;
|
||||
padding-right: var(--theme-spacing-sm);
|
||||
padding-left: var(--theme-spacing-sm);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.track-header-cell-no-h-padding {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.track-header-cell-with-vertical-border {
|
||||
border-right: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.track-header-cell-dragging {
|
||||
cursor: grabbing;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.track-header-cell-dragged-over-left::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 3px;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary);
|
||||
}
|
||||
|
||||
.track-header-cell-dragged-over-right::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
width: 3px;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary);
|
||||
}
|
||||
|
||||
.track-header-cell:hover .resize-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.track-header-cell:hover .resize-handle::before {
|
||||
background-color: var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
width: 2px;
|
||||
margin-right: -4px;
|
||||
cursor: col-resize;
|
||||
background: var(--theme-colors-border);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* .resize-handle::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
content: '';
|
||||
background-color: transparent;
|
||||
transition: background-color 0.15s ease;
|
||||
} */
|
||||
|
||||
.resize-handle-left {
|
||||
left: 0;
|
||||
margin-right: 0;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.resize-handle-left::before {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.resize-handle-right {
|
||||
right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.resize-handle-dragging {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
gap: var(--theme-spacing-md);
|
||||
padding: var(--theme-spacing-md);
|
||||
border-bottom: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.skeleton-column-wrapper {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border-radius: var(--theme-radius-md);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background-color: rgb(0 0 0);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@mixin dark {
|
||||
&::before {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .favorite-badge,
|
||||
&:hover .rating-badge {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite-badge {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: -50px;
|
||||
z-index: 1;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
pointer-events: none;
|
||||
background-color: var(--theme-colors-primary);
|
||||
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rating-badge {
|
||||
position: absolute;
|
||||
top: var(--theme-spacing-sm);
|
||||
right: var(--theme-spacing-sm);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||
font-size: var(--theme-font-size-md);
|
||||
font-weight: 600;
|
||||
color: var(--theme-colors-foreground);
|
||||
text-shadow: 0 1px 2px rgb(0 0 0 / 80%);
|
||||
pointer-events: none;
|
||||
background-color: var(--theme-colors-primary);
|
||||
border-radius: var(--theme-radius-md);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 50%);
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.row .image {
|
||||
object-fit: var(--theme-image-fit);
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
|
||||
.row .metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-xs);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--theme-font-size-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.row .title {
|
||||
font-weight: 500;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row .title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.row .artist {
|
||||
font-size: var(--theme-font-size-sm);
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row .artist-plain-text:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.row .metadata-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row .metadata-link:hover {
|
||||
color: var(--theme-colors-foreground);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.row .metadata-extra {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-xs);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: var(--theme-font-size-sm);
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.row .metadata-line {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap-style: balance;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row .metadata-line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.row .right {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row .tracks-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
font-size: var(--theme-font-size-sm);
|
||||
}
|
||||
|
||||
.row .track-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row .track-header-cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row .track-cell {
|
||||
min-width: 0;
|
||||
padding-right: var(--theme-spacing-sm);
|
||||
padding-left: var(--theme-spacing-sm);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row .track-row-size-compact {
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
.row .track-row-size-default {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.row .track-row-size-large {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
max-height: 48px;
|
||||
}
|
||||
|
||||
.row .track-cell-muted {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.row .track-cell-with-vertical-border {
|
||||
border-right: 1px solid transparent;
|
||||
}
|
||||
|
||||
.row .track-cell-vertical-border-visible {
|
||||
border-right-color: var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.row .track-cell-no-h-padding {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.track-row-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.track-row.track-row-alternate-even {
|
||||
background-color: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
.track-row.track-row-alternate-odd {
|
||||
@mixin dark {
|
||||
background-color: darken(var(--theme-colors-background), 30%);
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
background-color: darken(var(--theme-colors-background), 2%);
|
||||
}
|
||||
}
|
||||
|
||||
.track-row.track-row-selected {
|
||||
@mixin dark {
|
||||
background-color: lighten(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
background-color: darken(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.track-row.track-row-with-horizontal-border {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
|
||||
.track-row.track-row-horizontal-border-visible {
|
||||
border-top-color: var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.track-row.track-row-hover-highlight-enabled {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.track-row.track-row-hover-highlight-enabled .track-cell {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.track-row.track-row-hover-highlight-enabled:hover::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-surface);
|
||||
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;
|
||||
}
|
||||
|
||||
.skeleton-tracks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.skeleton-track-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 8rem;
|
||||
gap: var(--theme-spacing-sm);
|
||||
align-items: center;
|
||||
padding-right: var(--theme-spacing-sm);
|
||||
padding-left: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.skeleton-tracks-size-compact .skeleton-track-row {
|
||||
height: 32px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.skeleton-tracks-size-default .skeleton-track-row {
|
||||
height: 40px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.skeleton-tracks-size-large .skeleton-track-row {
|
||||
height: 48px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.skeleton-track-cell {
|
||||
width: 100%;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.skeleton-track-cell-title {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 1rem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
|
||||
[TableColumn.ACTIONS]: 32,
|
||||
[TableColumn.BIT_DEPTH]: 80,
|
||||
[TableColumn.BIT_RATE]: 80,
|
||||
[TableColumn.BPM]: 56,
|
||||
[TableColumn.CHANNELS]: 80,
|
||||
[TableColumn.CODEC]: 80,
|
||||
[TableColumn.DATE_ADDED]: 128,
|
||||
[TableColumn.DISC_NUMBER]: 36,
|
||||
[TableColumn.DURATION]: 72,
|
||||
[TableColumn.RELEASE_DATE]: 128,
|
||||
[TableColumn.SAMPLE_RATE]: 90,
|
||||
[TableColumn.TRACK_NUMBER]: 56,
|
||||
[TableColumn.USER_FAVORITE]: 32,
|
||||
[TableColumn.USER_RATING]: 64,
|
||||
[TableColumn.YEAR]: 56,
|
||||
};
|
||||
|
||||
const HOVER_ONLY_COLUMNS: TableColumn[] = [
|
||||
TableColumn.ACTIONS,
|
||||
TableColumn.USER_FAVORITE,
|
||||
TableColumn.USER_RATING,
|
||||
];
|
||||
|
||||
const NO_HORIZONTAL_PADDING_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 isNoHorizontalPaddingColumn(columnId: TableColumn): boolean {
|
||||
return NO_HORIZONTAL_PADDING_COLUMNS.includes(columnId);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
@@ -54,6 +54,7 @@ const ImageColumnBase = (props: ItemTableListInnerColumn) => {
|
||||
itemType: props.itemType,
|
||||
meta: {
|
||||
playType,
|
||||
singleSongOnly: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -58,6 +58,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
|
||||
itemType: props.itemType,
|
||||
meta: {
|
||||
playType,
|
||||
singleSongOnly: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -200,6 +201,7 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
|
||||
itemType: props.itemType,
|
||||
meta: {
|
||||
playType,
|
||||
singleSongOnly: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
+5
-5
@@ -7,8 +7,8 @@ import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||
import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
|
||||
|
||||
interface DragDropState {
|
||||
dragRef: null | React.Ref<HTMLDivElement>;
|
||||
interface DragDropState<TElement extends HTMLElement = HTMLDivElement> {
|
||||
dragRef: null | React.Ref<TElement>;
|
||||
isDraggedOver: 'bottom' | 'top' | null;
|
||||
isDragging: boolean;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ interface UseItemDragDropStateProps {
|
||||
playlistId?: string;
|
||||
}
|
||||
|
||||
export const useItemDragDropState = ({
|
||||
export const useItemDragDropState = <TElement extends HTMLElement = HTMLDivElement>({
|
||||
enableDrag,
|
||||
internalState,
|
||||
isDataRow,
|
||||
@@ -31,14 +31,14 @@ export const useItemDragDropState = ({
|
||||
itemType,
|
||||
playerContext,
|
||||
playlistId,
|
||||
}: UseItemDragDropStateProps): DragDropState => {
|
||||
}: UseItemDragDropStateProps): DragDropState<TElement> => {
|
||||
const shouldEnableDrag = enableDrag && isDataRow && !!item;
|
||||
|
||||
const {
|
||||
isDraggedOver,
|
||||
isDragging: isDraggingLocal,
|
||||
ref: dragRef,
|
||||
} = useDragDrop<HTMLDivElement>({
|
||||
} = useDragDrop<TElement>({
|
||||
drag: {
|
||||
getId: () => {
|
||||
if (!item || !isDataRow) {
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-xl);
|
||||
}
|
||||
|
||||
.container.no-horizontal-padding {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.container.center {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
@@ -205,6 +210,11 @@
|
||||
padding: 0 var(--theme-spacing-xl);
|
||||
}
|
||||
|
||||
.header-container.no-horizontal-padding {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.header-dragging {
|
||||
cursor: grabbing;
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -26,6 +26,7 @@ import styles from './item-table-list-column.module.css';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useItemSelectionState } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { isNoHorizontalPaddingColumn } from '/@/renderer/components/item-list/item-detail-list/utils';
|
||||
import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column';
|
||||
import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column';
|
||||
import { AlbumColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-column';
|
||||
@@ -479,6 +480,7 @@ export const TableColumnTextContainer = (
|
||||
[styles.dragging]: isDataRow && isDragging,
|
||||
[styles.large]: props.size === 'large',
|
||||
[styles.left]: props.columns[props.columnIndex].align === 'start',
|
||||
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
|
||||
[styles.paddingLg]: props.cellPadding === 'lg',
|
||||
[styles.paddingMd]: props.cellPadding === 'md',
|
||||
[styles.paddingSm]: props.cellPadding === 'sm',
|
||||
@@ -632,6 +634,7 @@ export const TableColumnContainer = (
|
||||
[styles.dragging]: isDataRow && isDragging,
|
||||
[styles.large]: props.size === 'large',
|
||||
[styles.left]: props.columns[props.columnIndex].align === 'start',
|
||||
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
|
||||
[styles.paddingLg]: props.cellPadding === 'lg',
|
||||
[styles.paddingMd]: props.cellPadding === 'md',
|
||||
[styles.paddingSm]: props.cellPadding === 'sm',
|
||||
@@ -850,6 +853,7 @@ export const TableColumnHeaderContainer = (
|
||||
[styles.headerDraggedOverLeft]: isDraggedOver === 'left',
|
||||
[styles.headerDraggedOverRight]: isDraggedOver === 'right',
|
||||
[styles.headerDragging]: isDragging,
|
||||
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
|
||||
[styles.paddingLg]: props.cellPadding === 'lg',
|
||||
[styles.paddingMd]: props.cellPadding === 'md',
|
||||
[styles.paddingSm]: props.cellPadding === 'sm',
|
||||
@@ -881,7 +885,7 @@ export const TableColumnHeaderContainer = (
|
||||
);
|
||||
};
|
||||
|
||||
const columnLabelMap: Record<TableColumn, ReactNode | string> = {
|
||||
export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
|
||||
[TableColumn.ACTIONS]: (
|
||||
<Flex className={styles.headerIconWrapper}>
|
||||
<Icon fill="default" icon="ellipsisHorizontal" />
|
||||
|
||||
@@ -98,7 +98,7 @@ export interface ItemListTableComponentProps<TQuery> extends ItemListComponentPr
|
||||
enableRowHoverHighlight?: boolean;
|
||||
enableSelection?: boolean;
|
||||
enableVerticalBorders?: boolean;
|
||||
size?: 'compact' | 'default';
|
||||
size?: 'compact' | 'default' | 'large';
|
||||
}
|
||||
|
||||
export interface ItemTableListColumnConfig {
|
||||
|
||||
Reference in New Issue
Block a user