mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-27 04:46:32 +02:00
Add album detail list view (#1681)
This commit is contained in:
@@ -233,8 +233,8 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
{metadataItems.map((item, index) => (
|
||||
<Fragment key={item.id}>
|
||||
{index > 0 && (
|
||||
<Text fw={400} isMuted isNoSelect>
|
||||
•
|
||||
<Text isMuted isNoSelect>
|
||||
<Separator />
|
||||
</Text>
|
||||
)}
|
||||
<Text fw={400}>{item.value}</Text>
|
||||
|
||||
@@ -36,6 +36,18 @@ const AlbumListPaginatedTable = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const AlbumListInfiniteDetail = lazy(() =>
|
||||
import('/@/renderer/features/albums/components/album-list-infinite-detail').then((module) => ({
|
||||
default: module.AlbumListInfiniteDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const AlbumListPaginatedDetail = lazy(() =>
|
||||
import('/@/renderer/features/albums/components/album-list-paginated-detail').then((module) => ({
|
||||
default: module.AlbumListPaginatedDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const AlbumListFilters = () => {
|
||||
return (
|
||||
<ListWithSidebarContainer.SidebarPortal>
|
||||
@@ -62,13 +74,16 @@ export const AlbumListContent = () => {
|
||||
};
|
||||
|
||||
const AlbumListSuspenseContainer = () => {
|
||||
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ALBUM);
|
||||
const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings(
|
||||
ItemListKey.ALBUM,
|
||||
);
|
||||
|
||||
const { customFilters } = useListContext();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<AlbumListView
|
||||
detail={detail}
|
||||
display={display}
|
||||
grid={grid}
|
||||
itemsPerPage={itemsPerPage}
|
||||
@@ -83,13 +98,17 @@ const AlbumListSuspenseContainer = () => {
|
||||
export type OverrideAlbumListQuery = Omit<Partial<AlbumListQuery>, 'limit' | 'startIndex'>;
|
||||
|
||||
export const AlbumListView = ({
|
||||
detail,
|
||||
display,
|
||||
grid,
|
||||
itemsPerPage,
|
||||
overrideQuery,
|
||||
pagination,
|
||||
table,
|
||||
}: ItemListSettings & { overrideQuery?: OverrideAlbumListQuery }) => {
|
||||
}: ItemListSettings & {
|
||||
detail?: ItemListSettings['detail'];
|
||||
overrideQuery?: OverrideAlbumListQuery;
|
||||
}) => {
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useListContext();
|
||||
|
||||
@@ -179,6 +198,32 @@ export const AlbumListView = ({
|
||||
return null;
|
||||
}
|
||||
}
|
||||
case ListDisplayType.DETAIL: {
|
||||
switch (pagination) {
|
||||
case ListPaginationType.INFINITE: {
|
||||
return (
|
||||
<AlbumListInfiniteDetail
|
||||
enableHeader={detail?.enableHeader}
|
||||
itemsPerPage={itemsPerPage}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case ListPaginationType.PAGINATED: {
|
||||
return (
|
||||
<AlbumListPaginatedDetail
|
||||
enableHeader={detail?.enableHeader}
|
||||
itemsPerPage={itemsPerPage}
|
||||
query={mergedQuery}
|
||||
serverId={server.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import {
|
||||
ALBUM_TABLE_COLUMNS,
|
||||
SONG_TABLE_COLUMNS,
|
||||
} from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
@@ -92,8 +95,15 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
|
||||
<ListRefreshButton listKey={pageKey as ItemListKey} />
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ListDisplayTypeToggleButton listKey={ItemListKey.ALBUM} />
|
||||
<ListDisplayTypeToggleButton enableDetail listKey={ItemListKey.ALBUM} />
|
||||
<ListConfigMenu
|
||||
detailConfig={{
|
||||
optionsConfig: {
|
||||
autoFitColumns: { hidden: true },
|
||||
},
|
||||
tableColumnsData: SONG_TABLE_COLUMNS,
|
||||
tableKey: 'detail',
|
||||
}}
|
||||
listKey={ItemListKey.ALBUM}
|
||||
tableColumnsData={ALBUM_TABLE_COLUMNS}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list';
|
||||
import { ItemListComponentProps } from '/@/renderer/components/item-list/types';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface AlbumListInfiniteDetailProps extends ItemListComponentProps<AlbumListQuery> {
|
||||
enableHeader?: boolean;
|
||||
}
|
||||
|
||||
export const AlbumListInfiniteDetail = ({
|
||||
enableHeader = true,
|
||||
itemsPerPage = 100,
|
||||
query = {
|
||||
sortBy: AlbumListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
},
|
||||
serverId,
|
||||
}: AlbumListInfiniteDetailProps) => {
|
||||
const listCountQuery = albumQueries.listCount({
|
||||
query: { ...query },
|
||||
serverId: serverId,
|
||||
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||
|
||||
const listQueryFn = api.controller.getAlbumList;
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.ALBUM,
|
||||
tableKey: 'detail',
|
||||
});
|
||||
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.ALBUM,
|
||||
tableKey: 'detail',
|
||||
});
|
||||
|
||||
const { getItem, itemCount, loadedItems, onRangeChanged } = useItemListInfiniteLoader({
|
||||
eventKey: ItemListKey.ALBUM,
|
||||
itemsPerPage,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
listCountQuery,
|
||||
listQueryFn,
|
||||
query,
|
||||
serverId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ItemDetailList
|
||||
data={loadedItems}
|
||||
enableHeader={enableHeader}
|
||||
getItem={getItem}
|
||||
itemCount={itemCount}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
onRangeChanged={onRangeChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list';
|
||||
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||
import { ItemListComponentProps } from '/@/renderer/components/item-list/types';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface AlbumListPaginatedDetailProps extends ItemListComponentProps<AlbumListQuery> {
|
||||
enableHeader?: boolean;
|
||||
}
|
||||
|
||||
export const AlbumListPaginatedDetail = ({
|
||||
enableHeader = true,
|
||||
itemsPerPage = 100,
|
||||
query = {
|
||||
sortBy: AlbumListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
},
|
||||
serverId,
|
||||
}: AlbumListPaginatedDetailProps) => {
|
||||
const listCountQuery = albumQueries.listCount({
|
||||
query: { ...query },
|
||||
serverId: serverId,
|
||||
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||
|
||||
const listQueryFn = api.controller.getAlbumList;
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.ALBUM,
|
||||
tableKey: 'detail',
|
||||
});
|
||||
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.ALBUM,
|
||||
tableKey: 'detail',
|
||||
});
|
||||
|
||||
const { currentPage, onChange } = useItemListPagination();
|
||||
|
||||
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
|
||||
currentPage,
|
||||
eventKey: ItemListKey.ALBUM,
|
||||
itemsPerPage,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
listCountQuery,
|
||||
listQueryFn,
|
||||
query,
|
||||
serverId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ItemListWithPagination
|
||||
currentPage={currentPage}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onChange={onChange}
|
||||
pageCount={pageCount}
|
||||
totalItemCount={totalItemCount}
|
||||
>
|
||||
<ItemDetailList
|
||||
currentPage={currentPage}
|
||||
enableHeader={enableHeader}
|
||||
items={data || []}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
/>
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,28 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Fragment, memo } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Text, TextProps } from '/@/shared/components/text/text';
|
||||
import { AlbumArtist, RelatedAlbumArtist, RelatedArtist } from '/@/shared/types/domain-types';
|
||||
|
||||
export const JOINED_ARTISTS_MUTED_PROPS = {
|
||||
linkProps: { fw: 400, isMuted: true },
|
||||
rootTextProps: { fw: 400, isMuted: true, size: 'sm' as const },
|
||||
} as const;
|
||||
|
||||
interface JoinedArtistsProps {
|
||||
artistName: string;
|
||||
artists: AlbumArtist[] | RelatedAlbumArtist[] | RelatedArtist[];
|
||||
linkProps?: Partial<Omit<TextProps, 'children' | 'component' | 'to'>>;
|
||||
readOnly?: boolean;
|
||||
rootTextProps?: Partial<Omit<TextProps, 'children' | 'component'>>;
|
||||
}
|
||||
|
||||
export const JoinedArtists = ({
|
||||
const JoinedArtistsComponent = ({
|
||||
artistName,
|
||||
artists,
|
||||
linkProps,
|
||||
readOnly = false,
|
||||
rootTextProps,
|
||||
}: JoinedArtistsProps) => {
|
||||
const parts: (
|
||||
@@ -111,7 +118,7 @@ export const JoinedArtists = ({
|
||||
{artists.map((artist, index) => (
|
||||
<Fragment key={artist.id || `artist-${index}`}>
|
||||
{index > 0 && ', '}
|
||||
{artist.id ? (
|
||||
{artist.id && !readOnly ? (
|
||||
<Text
|
||||
component={Link}
|
||||
fw={500}
|
||||
@@ -124,7 +131,7 @@ export const JoinedArtists = ({
|
||||
{artist.name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text fw={500} {...linkProps}>
|
||||
<Text component="span" fw={500} {...linkProps}>
|
||||
{artist.name}
|
||||
</Text>
|
||||
)}
|
||||
@@ -152,7 +159,7 @@ export const JoinedArtists = ({
|
||||
|
||||
const { artist, text } = part;
|
||||
|
||||
if (artist.id) {
|
||||
if (artist.id && !readOnly) {
|
||||
return (
|
||||
<Text
|
||||
component={Link}
|
||||
@@ -169,7 +176,7 @@ export const JoinedArtists = ({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text fw={500} key={`${artist.name}-${index}`} {...linkProps}>
|
||||
<Text component="span" fw={500} key={`${artist.name}-${index}`} {...linkProps}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
@@ -180,7 +187,7 @@ export const JoinedArtists = ({
|
||||
{unmatchedArtists.map((artist, index) => (
|
||||
<Fragment key={artist.id}>
|
||||
{index > 0 && ', '}
|
||||
{artist.id ? (
|
||||
{artist.id && !readOnly ? (
|
||||
<Text
|
||||
component={Link}
|
||||
fw={500}
|
||||
@@ -192,6 +199,10 @@ export const JoinedArtists = ({
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
) : artist.id ? (
|
||||
<Text component="span" fw={500} {...linkProps}>
|
||||
{artist.name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text component="span" isMuted>
|
||||
{artist.name}
|
||||
@@ -205,6 +216,8 @@ export const JoinedArtists = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const JoinedArtists = memo(JoinedArtistsComponent);
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, useShowRatings } from '/@/renderer/store';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
@@ -160,7 +161,11 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
|
||||
.filter((i) => i.enabled)
|
||||
.map((item, index) => (
|
||||
<Fragment key={`item-${item.id}-${index}`}>
|
||||
{index > 0 && <Text isNoSelect>•</Text>}
|
||||
{index > 0 && (
|
||||
<Text isMuted isNoSelect>
|
||||
{SEPARATOR_STRING}
|
||||
</Text>
|
||||
)}
|
||||
<Text isMuted={item.secondary}>{item.value}</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export const useIsCurrentSong = (song: QueueSong) => {
|
||||
export const useIsCurrentSong = (song: QueueSong | Song) => {
|
||||
const currentSong = usePlayerSong();
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
return song._uniqueId === currentSong?._uniqueId;
|
||||
}, [song._uniqueId, currentSong?._uniqueId]);
|
||||
const queueSong = song as QueueSong;
|
||||
|
||||
if (queueSong._uniqueId != null && queueSong._uniqueId !== '') {
|
||||
return queueSong._uniqueId === currentSong?._uniqueId;
|
||||
}
|
||||
|
||||
return song.id === currentSong?.id;
|
||||
}, [song, currentSong?.id, currentSong?._uniqueId]);
|
||||
|
||||
return { isActive };
|
||||
};
|
||||
|
||||
@@ -16,10 +16,11 @@ export const DisplayTypeToggleButton = ({
|
||||
}: DisplayTypeToggleButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isGrid = displayType === ListDisplayType.GRID;
|
||||
const isDetail = displayType === ListDisplayType.DETAIL;
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
icon={isGrid ? 'layoutGrid' : 'layoutTable'}
|
||||
icon={isGrid ? 'layoutGrid' : isDetail ? 'layoutDetail' : 'layoutTable'}
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
}}
|
||||
@@ -27,7 +28,9 @@ export const DisplayTypeToggleButton = ({
|
||||
tooltip={{
|
||||
label: isGrid
|
||||
? t('table.config.view.grid', { postProcess: 'sentenceCase' })
|
||||
: t('table.config.view.table', { postProcess: 'sentenceCase' }),
|
||||
: isDetail
|
||||
? t('table.config.view.detail', { postProcess: 'sentenceCase' })
|
||||
: t('table.config.view.table', { postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
variant="subtle"
|
||||
{...buttonProps}
|
||||
|
||||
@@ -37,6 +37,15 @@ const DISPLAY_TYPES = [
|
||||
),
|
||||
value: ListDisplayType.GRID,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Group align="center" justify="center" p="sm">
|
||||
<Icon icon="layoutDetail" size="lg" />
|
||||
{i18n.t('table.config.view.detail', { postProcess: 'sentenceCase' }) as string}
|
||||
</Group>
|
||||
),
|
||||
value: ListDisplayType.DETAIL,
|
||||
},
|
||||
// {
|
||||
// disabled: true,
|
||||
// label: (
|
||||
@@ -63,6 +72,12 @@ export const ListConfigBooleanControl = ({
|
||||
);
|
||||
};
|
||||
|
||||
export interface ListConfigMenuDetailConfig {
|
||||
optionsConfig?: ListConfigMenuOptionsConfig['detail'];
|
||||
tableColumnsData: { label: string; value: string }[];
|
||||
tableKey: 'detail';
|
||||
}
|
||||
|
||||
export interface ListConfigMenuDisplayTypeConfig {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
@@ -75,6 +90,9 @@ export interface ListConfigMenuOptionConfig {
|
||||
}
|
||||
|
||||
export interface ListConfigMenuOptionsConfig {
|
||||
detail?: {
|
||||
[key: string]: ListConfigMenuOptionConfig;
|
||||
};
|
||||
grid?: {
|
||||
[key: string]: ListConfigMenuOptionConfig;
|
||||
};
|
||||
@@ -85,6 +103,7 @@ export interface ListConfigMenuOptionsConfig {
|
||||
|
||||
interface ListConfigMenuProps {
|
||||
buttonProps?: ActionIconProps;
|
||||
detailConfig?: ListConfigMenuDetailConfig;
|
||||
displayTypes?: ListConfigMenuDisplayTypeConfig[];
|
||||
listKey: ItemListKey;
|
||||
optionsConfig?: ListConfigMenuOptionsConfig;
|
||||
@@ -172,6 +191,20 @@ const Config = ({
|
||||
...props
|
||||
}: ListConfigMenuProps & { displayType: ListDisplayType }) => {
|
||||
switch (displayType) {
|
||||
case ListDisplayType.DETAIL:
|
||||
if (props.detailConfig) {
|
||||
return (
|
||||
<TableConfig
|
||||
enablePinColumnButtons={false}
|
||||
listKey={props.listKey}
|
||||
optionsConfig={props.detailConfig.optionsConfig}
|
||||
tableColumnsData={props.detailConfig.tableColumnsData}
|
||||
tableKey="detail"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
case ListDisplayType.GRID:
|
||||
return (
|
||||
<GridConfig
|
||||
|
||||
@@ -3,21 +3,47 @@ import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
interface ListDisplayTypeToggleButtonProps {
|
||||
enableDetail?: boolean;
|
||||
listKey: ItemListKey;
|
||||
}
|
||||
|
||||
export const ListDisplayTypeToggleButton = ({ listKey }: ListDisplayTypeToggleButtonProps) => {
|
||||
export const ListDisplayTypeToggleButton = ({
|
||||
enableDetail = false,
|
||||
listKey,
|
||||
}: ListDisplayTypeToggleButtonProps) => {
|
||||
const displayType = useSettingsStore(
|
||||
(state) => state.lists[listKey]?.display,
|
||||
) as ListDisplayType;
|
||||
const { setList } = useSettingsStoreActions();
|
||||
|
||||
const handleToggleDisplayType = () => {
|
||||
const newDisplayType =
|
||||
displayType === ListDisplayType.GRID ? ListDisplayType.TABLE : ListDisplayType.GRID;
|
||||
let newDisplayType: ListDisplayType;
|
||||
|
||||
if (enableDetail) {
|
||||
if (displayType === ListDisplayType.DETAIL) {
|
||||
newDisplayType = ListDisplayType.TABLE;
|
||||
} else if (displayType === ListDisplayType.TABLE) {
|
||||
newDisplayType = ListDisplayType.GRID;
|
||||
} else if (displayType === ListDisplayType.GRID) {
|
||||
newDisplayType = ListDisplayType.DETAIL;
|
||||
} else {
|
||||
newDisplayType = ListDisplayType.GRID;
|
||||
}
|
||||
} else {
|
||||
if (displayType === ListDisplayType.GRID) {
|
||||
newDisplayType = ListDisplayType.TABLE;
|
||||
} else if (displayType === ListDisplayType.TABLE) {
|
||||
newDisplayType = ListDisplayType.GRID;
|
||||
} else {
|
||||
newDisplayType = ListDisplayType.GRID;
|
||||
}
|
||||
}
|
||||
|
||||
setList(listKey, {
|
||||
display: newDisplayType,
|
||||
});
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
return <DisplayTypeToggleButton displayType={displayType} onToggle={handleToggleDisplayType} />;
|
||||
|
||||
@@ -21,7 +21,12 @@ import {
|
||||
ListConfigBooleanControl,
|
||||
ListConfigTable,
|
||||
} from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ItemListSettings, useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import {
|
||||
type DataTableProps,
|
||||
ItemListSettings,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
@@ -39,6 +44,7 @@ import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/d
|
||||
import { ItemListKey, ListPaginationType } from '/@/shared/types/types';
|
||||
|
||||
interface TableConfigProps {
|
||||
enablePinColumnButtons?: boolean;
|
||||
extraOptions?: {
|
||||
component: React.ReactNode;
|
||||
id: string;
|
||||
@@ -52,19 +58,37 @@ interface TableConfigProps {
|
||||
};
|
||||
};
|
||||
tableColumnsData: { label: string; value: string }[];
|
||||
tableKey?: 'detail' | 'main';
|
||||
}
|
||||
|
||||
export const TableConfig = ({
|
||||
enablePinColumnButtons = true,
|
||||
extraOptions,
|
||||
listKey,
|
||||
optionsConfig,
|
||||
tableColumnsData,
|
||||
tableKey = 'main',
|
||||
}: TableConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
|
||||
const { setList } = useSettingsStoreActions();
|
||||
|
||||
const table = tableKey === 'detail' ? (list?.detail ?? list?.table) : list?.table;
|
||||
|
||||
const setTableUpdate = useCallback(
|
||||
(patch: Partial<DataTableProps>) => {
|
||||
if (tableKey === 'detail') {
|
||||
setList(listKey, { detail: patch } as Parameters<
|
||||
ReturnType<typeof useSettingsStoreActions>['setList']
|
||||
>[1]);
|
||||
} else {
|
||||
setList(listKey, { table: patch });
|
||||
}
|
||||
},
|
||||
[listKey, setList, tableKey],
|
||||
);
|
||||
|
||||
const advancedSettings = useMemo(() => {
|
||||
const allOptions = [
|
||||
{
|
||||
@@ -152,12 +176,12 @@ export const TableConfig = ({
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setList(listKey, {
|
||||
table: { size: value as 'compact' | 'default' },
|
||||
setTableUpdate({
|
||||
size: value as 'compact' | 'default' | 'large',
|
||||
})
|
||||
}
|
||||
size="sm"
|
||||
value={list.table.size}
|
||||
value={table?.size ?? 'default'}
|
||||
w="100%"
|
||||
/>
|
||||
),
|
||||
@@ -169,8 +193,8 @@ export const TableConfig = ({
|
||||
{
|
||||
component: (
|
||||
<ListConfigBooleanControl
|
||||
onChange={(e) => setList(listKey, { table: { enableHeader: e } })}
|
||||
value={list.table.enableHeader}
|
||||
onChange={(e) => setTableUpdate({ enableHeader: e })}
|
||||
value={table.enableHeader}
|
||||
/>
|
||||
),
|
||||
id: 'enableHeader',
|
||||
@@ -181,10 +205,8 @@ export const TableConfig = ({
|
||||
{
|
||||
component: (
|
||||
<ListConfigBooleanControl
|
||||
onChange={(e) =>
|
||||
setList(listKey, { table: { enableRowHoverHighlight: e } })
|
||||
}
|
||||
value={list.table.enableRowHoverHighlight}
|
||||
onChange={(e) => setTableUpdate({ enableRowHoverHighlight: e })}
|
||||
value={table.enableRowHoverHighlight}
|
||||
/>
|
||||
),
|
||||
id: 'enableRowHoverHighlight',
|
||||
@@ -195,10 +217,8 @@ export const TableConfig = ({
|
||||
{
|
||||
component: (
|
||||
<ListConfigBooleanControl
|
||||
onChange={(e) =>
|
||||
setList(listKey, { table: { enableAlternateRowColors: e } })
|
||||
}
|
||||
value={list.table.enableAlternateRowColors}
|
||||
onChange={(e) => setTableUpdate({ enableAlternateRowColors: e })}
|
||||
value={table.enableAlternateRowColors}
|
||||
/>
|
||||
),
|
||||
id: 'enableAlternateRowColors',
|
||||
@@ -209,10 +229,8 @@ export const TableConfig = ({
|
||||
{
|
||||
component: (
|
||||
<ListConfigBooleanControl
|
||||
onChange={(e) =>
|
||||
setList(listKey, { table: { enableHorizontalBorders: e } })
|
||||
}
|
||||
value={list.table.enableHorizontalBorders}
|
||||
onChange={(e) => setTableUpdate({ enableHorizontalBorders: e })}
|
||||
value={table.enableHorizontalBorders}
|
||||
/>
|
||||
),
|
||||
id: 'enableHorizontalBorders',
|
||||
@@ -223,8 +241,8 @@ export const TableConfig = ({
|
||||
{
|
||||
component: (
|
||||
<ListConfigBooleanControl
|
||||
onChange={(e) => setList(listKey, { table: { enableVerticalBorders: e } })}
|
||||
value={list.table.enableVerticalBorders}
|
||||
onChange={(e) => setTableUpdate({ enableVerticalBorders: e })}
|
||||
value={table.enableVerticalBorders}
|
||||
/>
|
||||
),
|
||||
id: 'enableVerticalBorders',
|
||||
@@ -235,8 +253,10 @@ export const TableConfig = ({
|
||||
{
|
||||
component: (
|
||||
<ListConfigBooleanControl
|
||||
onChange={(e) => setList(listKey, { table: { autoFitColumns: e } })}
|
||||
value={list.table.autoFitColumns}
|
||||
onChange={(e) => setTableUpdate({ autoFitColumns: e })}
|
||||
value={
|
||||
tableKey === 'main' ? (table as DataTableProps).autoFitColumns : false
|
||||
}
|
||||
/>
|
||||
),
|
||||
id: 'autoFitColumns',
|
||||
@@ -256,7 +276,18 @@ export const TableConfig = ({
|
||||
return option;
|
||||
})
|
||||
.filter((option): option is NonNullable<typeof option> => option !== null);
|
||||
}, [extraOptions, listKey, optionsConfig, setList, t, list]);
|
||||
}, [
|
||||
t,
|
||||
list.pagination,
|
||||
list.itemsPerPage,
|
||||
table,
|
||||
tableKey,
|
||||
extraOptions,
|
||||
setList,
|
||||
listKey,
|
||||
setTableUpdate,
|
||||
optionsConfig,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -264,8 +295,9 @@ export const TableConfig = ({
|
||||
<Divider />
|
||||
<TableColumnConfig
|
||||
data={tableColumnsData}
|
||||
onChange={(columns) => setList(listKey, { table: { columns } })}
|
||||
value={list.table.columns}
|
||||
enablePinColumnButtons={enablePinColumnButtons}
|
||||
onChange={(columns) => setTableUpdate({ columns })}
|
||||
value={table.columns}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -273,10 +305,12 @@ export const TableConfig = ({
|
||||
|
||||
const TableColumnConfig = ({
|
||||
data,
|
||||
enablePinColumnButtons,
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
data: { label: string; value: string }[];
|
||||
enablePinColumnButtons: boolean;
|
||||
onChange: (value: ItemTableListColumnConfig[]) => void;
|
||||
value: ItemTableListColumnConfig[];
|
||||
}) => {
|
||||
@@ -473,6 +507,7 @@ const TableColumnConfig = ({
|
||||
<div style={{ userSelect: 'none' }}>
|
||||
{filteredColumns.map(({ item, matches }) => (
|
||||
<TableColumnItem
|
||||
enablePinColumnButtons={enablePinColumnButtons}
|
||||
handleAlignCenter={handleAlignCenter}
|
||||
handleAlignLeft={handleAlignLeft}
|
||||
handleAlignRight={handleAlignRight}
|
||||
@@ -516,6 +551,7 @@ const DragHandle = ({
|
||||
|
||||
const TableColumnItem = memo(
|
||||
({
|
||||
enablePinColumnButtons,
|
||||
handleAlignCenter,
|
||||
handleAlignLeft,
|
||||
handleAlignRight,
|
||||
@@ -531,6 +567,7 @@ const TableColumnItem = memo(
|
||||
label,
|
||||
matches,
|
||||
}: {
|
||||
enablePinColumnButtons: boolean;
|
||||
handleAlignCenter: (item: ItemTableListColumnConfig) => void;
|
||||
handleAlignLeft: (item: ItemTableListColumnConfig) => void;
|
||||
handleAlignRight: (item: ItemTableListColumnConfig) => void;
|
||||
@@ -667,32 +704,34 @@ const TableColumnItem = memo(
|
||||
variant="subtle"
|
||||
/>
|
||||
</ActionIconGroup>
|
||||
<ActionIconGroup className={styles.group}>
|
||||
<ActionIcon
|
||||
icon="arrowLeftToLine"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={() => handlePinToLeft(item)}
|
||||
size="xs"
|
||||
tooltip={{
|
||||
label: t('table.config.general.pinToLeft', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
}}
|
||||
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon="arrowRightToLine"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={() => handlePinToRight(item)}
|
||||
size="xs"
|
||||
tooltip={{
|
||||
label: t('table.config.general.pinToRight', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
}}
|
||||
variant={item.pinned === 'right' ? 'filled' : 'subtle'}
|
||||
/>
|
||||
</ActionIconGroup>
|
||||
{enablePinColumnButtons && (
|
||||
<ActionIconGroup className={styles.group}>
|
||||
<ActionIcon
|
||||
icon="arrowLeftToLine"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={() => handlePinToLeft(item)}
|
||||
size="xs"
|
||||
tooltip={{
|
||||
label: t('table.config.general.pinToLeft', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
}}
|
||||
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon="arrowRightToLine"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={() => handlePinToRight(item)}
|
||||
size="xs"
|
||||
tooltip={{
|
||||
label: t('table.config.general.pinToRight', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
}}
|
||||
variant={item.pinned === 'right' ? 'filled' : 'subtle'}
|
||||
/>
|
||||
</ActionIconGroup>
|
||||
)}
|
||||
<ActionIconGroup className={styles.group}>
|
||||
<ActionIcon
|
||||
icon="alignLeft"
|
||||
@@ -772,6 +811,7 @@ const TableColumnItem = memo(
|
||||
(prevProps, nextProps) => {
|
||||
// Custom comparison function for better memoization
|
||||
return (
|
||||
prevProps.enablePinColumnButtons === nextProps.enablePinColumnButtons &&
|
||||
prevProps.item.id === nextProps.item.id &&
|
||||
prevProps.item.isEnabled === nextProps.item.isEnabled &&
|
||||
prevProps.item.autoSize === nextProps.item.autoSize &&
|
||||
|
||||
@@ -520,6 +520,28 @@ export const applyFavoriteOptimisticUpdates = (
|
||||
}
|
||||
});
|
||||
|
||||
const songListQueryKey = queryKeys.songs.list(variables.apiClientProps.serverId);
|
||||
const songListQueries = queryClient.getQueriesData({
|
||||
exact: false,
|
||||
queryKey: songListQueryKey,
|
||||
});
|
||||
|
||||
songListQueries.forEach(([queryKey, data]) => {
|
||||
if (data) {
|
||||
pendingUpdates.push({
|
||||
previousData: data,
|
||||
queryKey,
|
||||
updater: (prev: undefined | { items: Song[] }) => {
|
||||
if (!prev) return prev;
|
||||
const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) =>
|
||||
createFavoriteUpdater<Song>(item),
|
||||
);
|
||||
return updatedItems ? { ...prev, items: updatedItems } : prev;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const topSongsQueryKey = queryKeys.albumArtists.topSongs(
|
||||
variables.apiClientProps.serverId,
|
||||
);
|
||||
@@ -679,6 +701,7 @@ export const applyFavoriteOptimisticUpdatesDeferred = (
|
||||
queryKeys.playlists.songList(variables.apiClientProps.serverId),
|
||||
'playlist-song-list',
|
||||
);
|
||||
collectQueries(queryKeys.songs.list(variables.apiClientProps.serverId), 'song-list');
|
||||
collectQueries(
|
||||
queryKeys.albumArtists.topSongs(variables.apiClientProps.serverId),
|
||||
'top-songs',
|
||||
@@ -742,6 +765,7 @@ export const applyFavoriteOptimisticUpdatesDeferred = (
|
||||
case 'album-list':
|
||||
case 'artist-list':
|
||||
case 'playlist-song-list':
|
||||
case 'song-list':
|
||||
case 'top-songs': {
|
||||
const updatedItems = updateItemInArray(
|
||||
prev.items || [],
|
||||
|
||||
@@ -519,6 +519,28 @@ export const applyRatingOptimisticUpdates = (
|
||||
}
|
||||
});
|
||||
|
||||
const songListQueryKey = queryKeys.songs.list(variables.apiClientProps.serverId);
|
||||
const songListQueries = queryClient.getQueriesData({
|
||||
exact: false,
|
||||
queryKey: songListQueryKey,
|
||||
});
|
||||
|
||||
songListQueries.forEach(([queryKey, data]) => {
|
||||
if (data) {
|
||||
pendingUpdates.push({
|
||||
previousData: data,
|
||||
queryKey,
|
||||
updater: (prev: undefined | { items: Song[] }) => {
|
||||
if (!prev) return prev;
|
||||
const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) =>
|
||||
createRatingUpdater<Song>(item),
|
||||
);
|
||||
return updatedItems ? { ...prev, items: updatedItems } : prev;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const topSongsQueryKey = queryKeys.albumArtists.topSongs(
|
||||
variables.apiClientProps.serverId,
|
||||
);
|
||||
@@ -652,6 +674,7 @@ export const applyRatingOptimisticUpdatesDeferred = (
|
||||
queryKeys.songs.detail(variables.apiClientProps.serverId),
|
||||
'song-detail',
|
||||
);
|
||||
collectQueries(queryKeys.songs.list(variables.apiClientProps.serverId), 'song-list');
|
||||
collectQueries(
|
||||
queryKeys.albumArtists.topSongs(variables.apiClientProps.serverId),
|
||||
'top-songs',
|
||||
@@ -712,6 +735,7 @@ export const applyRatingOptimisticUpdatesDeferred = (
|
||||
case 'album-artist-list':
|
||||
case 'album-list':
|
||||
case 'artist-list':
|
||||
case 'song-list':
|
||||
case 'top-songs': {
|
||||
const updatedItems = updateItemInArray(
|
||||
prev.items || [],
|
||||
|
||||
Reference in New Issue
Block a user