mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add support for full playlist re-order (#1327)
This commit is contained in:
@@ -24,8 +24,6 @@ export const PlaylistSongContextMenu = ({ items, type }: PlaylistSongContextMenu
|
||||
return { ids };
|
||||
}, [items]);
|
||||
|
||||
console.log('items', items, ids);
|
||||
|
||||
return (
|
||||
<ContextMenu.Content
|
||||
bottomStickyContent={<ContextMenuPreview items={items} itemType={type} />}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||
import { PlaylistDetailSongListEditTable } from '/@/renderer/features/playlists/components/playlist-detail-song-list-table';
|
||||
import { useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||
import { ItemListKey, ListDisplayType, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const PlaylistDetailSongListTable = lazy(() =>
|
||||
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
||||
@@ -19,9 +21,6 @@ const PlaylistDetailSongListTable = lazy(() =>
|
||||
);
|
||||
|
||||
export const PlaylistDetailSongListContent = () => {
|
||||
const { display, grid, itemsPerPage, pagination, table } = useListSettings(
|
||||
ItemListKey.PLAYLIST_SONG,
|
||||
);
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const server = useCurrentServer();
|
||||
const { setItemCount } = useListContext();
|
||||
@@ -71,28 +70,16 @@ export const PlaylistDetailSongListContent = () => {
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<PlaylistDetailSongListView
|
||||
data={playlistSongsQuery.data}
|
||||
display={display}
|
||||
grid={grid}
|
||||
itemsPerPage={itemsPerPage}
|
||||
pagination={pagination}
|
||||
table={table}
|
||||
/>
|
||||
<PlaylistDetailSongList data={playlistSongsQuery.data} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>, 'id'>;
|
||||
|
||||
export const PlaylistDetailSongListView = ({
|
||||
data,
|
||||
display,
|
||||
table,
|
||||
}: ItemListSettings & {
|
||||
data: PlaylistSongListResponse;
|
||||
}) => {
|
||||
export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||
const server = useCurrentServer();
|
||||
const { display, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
|
||||
|
||||
switch (display) {
|
||||
case ListDisplayType.TABLE: {
|
||||
@@ -114,3 +101,149 @@ export const PlaylistDetailSongListView = ({
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const server = useCurrentServer();
|
||||
const { display, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
|
||||
|
||||
const [localData, setLocalData] = useState<PlaylistSongListResponse>(data);
|
||||
|
||||
const tableRef = useRef<ItemListHandle | null>(null);
|
||||
|
||||
// Listen for playlist reorder events
|
||||
useEffect(() => {
|
||||
const handleReorder = (payload: {
|
||||
edge: 'bottom' | 'top' | null;
|
||||
playlistId: string;
|
||||
sourceIds: string[];
|
||||
targetId: string;
|
||||
}) => {
|
||||
// Only handle events for this playlist
|
||||
if (payload.playlistId !== playlistId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalData((prev) => {
|
||||
if (!prev?.items || !payload.edge) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Create a list of IDs in current order
|
||||
const currentIds = prev.items.map((item) => item.id);
|
||||
|
||||
// Find the target index
|
||||
const targetIndex = currentIds.indexOf(payload.targetId);
|
||||
if (targetIndex === -1) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Remove all source IDs from their current positions
|
||||
const idsWithoutSources = currentIds.filter(
|
||||
(id) => !payload.sourceIds.includes(id),
|
||||
);
|
||||
|
||||
// Calculate the insertion index based on the original target position
|
||||
const sourcesBeforeTarget = payload.sourceIds.filter((id) => {
|
||||
const sourceIndex = currentIds.indexOf(id);
|
||||
return sourceIndex !== -1 && sourceIndex < targetIndex;
|
||||
}).length;
|
||||
|
||||
// Calculate the insert index in the filtered list
|
||||
const insertIndexInFiltered =
|
||||
payload.edge === 'top'
|
||||
? targetIndex - sourcesBeforeTarget
|
||||
: targetIndex - sourcesBeforeTarget + 1;
|
||||
|
||||
// Ensure insertIndex is within bounds
|
||||
const insertIndex = Math.max(
|
||||
0,
|
||||
Math.min(insertIndexInFiltered, idsWithoutSources.length),
|
||||
);
|
||||
|
||||
// Insert source IDs at the calculated position
|
||||
const reorderedIds = [
|
||||
...idsWithoutSources.slice(0, insertIndex),
|
||||
...payload.sourceIds,
|
||||
...idsWithoutSources.slice(insertIndex),
|
||||
];
|
||||
|
||||
// Create a map for quick lookup
|
||||
const itemMap = new Map(prev.items.map((item) => [item.id, item]));
|
||||
|
||||
// Reorder items based on new ID order
|
||||
const reorderedItems = reorderedIds
|
||||
.map((id) => itemMap.get(id))
|
||||
.filter((item): item is NonNullable<typeof item> => item !== undefined);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
items: reorderedItems,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
eventEmitter.on('PLAYLIST_REORDER', handleReorder);
|
||||
|
||||
return () => {
|
||||
eventEmitter.off('PLAYLIST_REORDER', handleReorder);
|
||||
};
|
||||
}, [playlistId]);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
align: 'center' as 'center' | 'end' | 'start',
|
||||
id: TableColumn.PLAYLIST_REORDER,
|
||||
isEnabled: true,
|
||||
pinned: 'left' as 'left' | 'right' | null,
|
||||
width: 100,
|
||||
},
|
||||
...table.columns,
|
||||
];
|
||||
}, [table.columns]);
|
||||
|
||||
const { setListData } = useListContext();
|
||||
|
||||
useEffect(() => {
|
||||
setListData?.(localData.items);
|
||||
}, [localData, setListData]);
|
||||
|
||||
switch (display) {
|
||||
case ListDisplayType.TABLE: {
|
||||
return (
|
||||
<PlaylistDetailSongListEditTable
|
||||
autoFitColumns={table.autoFitColumns}
|
||||
columns={columns}
|
||||
data={localData}
|
||||
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||
enableVerticalBorders={table.enableVerticalBorders}
|
||||
ref={tableRef}
|
||||
serverId={server.id}
|
||||
size={table.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||
const { isSmartPlaylist, mode } = useListContext();
|
||||
|
||||
if (isSmartPlaylist) {
|
||||
return <PlaylistDetailSongListView data={data} />;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case 'edit':
|
||||
return <PlaylistDetailSongListEdit data={data} />;
|
||||
case 'view':
|
||||
return <PlaylistDetailSongListView data={data} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
+72
-3
@@ -1,7 +1,12 @@
|
||||
import { openContextModal } from '@mantine/modals';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { PLAYLIST_SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
@@ -9,14 +14,25 @@ import { ListRefreshButton } from '/@/renderer/features/shared/components/list-r
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
export const PlaylistDetailSongListHeaderFilters = () => {
|
||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||
isSmartPlaylist?: boolean;
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListHeaderFilters = ({
|
||||
isSmartPlaylist,
|
||||
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { mode, setMode } = useListContext();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
@@ -34,23 +50,42 @@ export const PlaylistDetailSongListHeaderFilters = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const { ref: containerRef, ...breakpoints } = useContainerQuery();
|
||||
|
||||
const isViewEditMode = !isSmartPlaylist && breakpoints.isSm;
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Flex justify="space-between" ref={containerRef}>
|
||||
<Group gap="sm" w="100%">
|
||||
<ListSortByDropdown
|
||||
defaultSortByValue={SongListSort.ID}
|
||||
disabled={isEditMode}
|
||||
itemType={LibraryItem.PLAYLIST_SONG}
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
/>
|
||||
<Divider orientation="vertical" />
|
||||
<ListSortOrderToggleButton
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
disabled={isEditMode}
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
/>
|
||||
<ListRefreshButton listKey={ItemListKey.PLAYLIST_SONG} />
|
||||
<ListRefreshButton disabled={isEditMode} listKey={ItemListKey.PLAYLIST_SONG} />
|
||||
<MoreButton onClick={handleMore} />
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
{isViewEditMode && <SaveAndReplaceButton mode={mode} />}
|
||||
{isViewEditMode && (
|
||||
<Button
|
||||
onClick={() => setMode?.(mode === 'edit' ? 'view' : 'edit')}
|
||||
uppercase
|
||||
variant="subtle"
|
||||
>
|
||||
{mode === 'edit'
|
||||
? t('common.view', { postProcess: 'titleCase' })
|
||||
: t('common.edit', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
)}
|
||||
<ListConfigMenu
|
||||
displayTypes={[
|
||||
{
|
||||
@@ -66,6 +101,40 @@ export const PlaylistDetailSongListHeaderFilters = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const openSaveAndReplaceModal = (playlistId: string, listData: unknown[]) => {
|
||||
openContextModal({
|
||||
innerProps: { listData, playlistId },
|
||||
modalKey: 'saveAndReplace',
|
||||
size: 'sm',
|
||||
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
||||
});
|
||||
};
|
||||
|
||||
const SaveAndReplaceButton = ({ mode }: { mode: 'edit' | 'view' | undefined }) => {
|
||||
const { t } = useTranslation();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const { listData } = useListContext();
|
||||
|
||||
const handleOpenModal = useCallback(() => {
|
||||
if (!playlistId || !listData) return;
|
||||
openSaveAndReplaceModal(playlistId, listData);
|
||||
}, [playlistId, listData]);
|
||||
|
||||
if (mode === 'view') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
leftSection={<Icon color="error" icon="save" />}
|
||||
onClick={handleOpenModal}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
>
|
||||
{t('common.saveAndReplace', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
// const GenreFilterSelection = () => {
|
||||
// const { t } = useTranslation();
|
||||
// const { playlistId } = useParams() as { playlistId: string };
|
||||
|
||||
@@ -22,7 +22,7 @@ interface PlaylistDetailSongListHeaderProps {
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListHeader = ({
|
||||
isSmartPlaylist: isSmartPlaylistProp,
|
||||
isSmartPlaylist,
|
||||
}: PlaylistDetailSongListHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
@@ -35,7 +35,6 @@ export const PlaylistDetailSongListHeader = ({
|
||||
initialData: location.state?.item,
|
||||
});
|
||||
|
||||
const isSmartPlaylist = isSmartPlaylistProp ?? detailQuery?.data?.rules;
|
||||
const playlistDuration = detailQuery?.data?.duration;
|
||||
|
||||
return (
|
||||
@@ -64,7 +63,7 @@ export const PlaylistDetailSongListHeader = ({
|
||||
<ListSearchInput />
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<PlaylistDetailSongListHeaderFilters />
|
||||
<PlaylistDetailSongListHeaderFilters isSmartPlaylist={isSmartPlaylist} />
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -96,6 +96,16 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
const getRowId = useMemo(() => {
|
||||
return (item: unknown) => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return 'id';
|
||||
}
|
||||
const song = item as Song;
|
||||
return song.playlistItemId || song.id;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ItemTableList
|
||||
activeRowId={currentSong?.id}
|
||||
@@ -109,7 +119,97 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||
enableSelection={enableSelection}
|
||||
enableVerticalBorders={enableVerticalBorders}
|
||||
getRowId="playlistItemId"
|
||||
getRowId={getRowId}
|
||||
initialTop={{
|
||||
to: scrollOffset ?? 0,
|
||||
type: 'offset',
|
||||
}}
|
||||
itemType={LibraryItem.PLAYLIST_SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
overrideControls={overrideControls}
|
||||
ref={ref}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const PlaylistDetailSongListEditTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
||||
(
|
||||
{
|
||||
autoFitColumns = false,
|
||||
columns,
|
||||
data,
|
||||
enableAlternateRowColors = false,
|
||||
enableHorizontalBorders = false,
|
||||
enableRowHoverHighlight = true,
|
||||
enableSelection = true,
|
||||
enableVerticalBorders = false,
|
||||
saveScrollOffset = true,
|
||||
size = 'default',
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||
enabled: saveScrollOffset,
|
||||
});
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.PLAYLIST_SONG,
|
||||
});
|
||||
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.PLAYLIST_SONG,
|
||||
});
|
||||
|
||||
const player = usePlayer();
|
||||
|
||||
const currentSong = usePlayerSong();
|
||||
|
||||
const overrideControls: Partial<ItemControls> = useMemo(() => {
|
||||
return {
|
||||
onDoubleClick: ({ index, internalState, item, meta }) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
const getRowId = useMemo(() => {
|
||||
return (item: unknown) => {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return 'id';
|
||||
}
|
||||
const song = item as Song;
|
||||
return song.playlistItemId || song.id;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ItemTableList
|
||||
activeRowId={currentSong?.id}
|
||||
autoFitColumns={autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={data.items}
|
||||
enableAlternateRowColors={enableAlternateRowColors}
|
||||
enableDrag
|
||||
enableExpansion={false}
|
||||
enableHorizontalBorders={enableHorizontalBorders}
|
||||
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||
enableSelection={enableSelection}
|
||||
enableVerticalBorders={enableVerticalBorders}
|
||||
getRowId={getRowId}
|
||||
initialTop={{
|
||||
to: scrollOffset ?? 0,
|
||||
type: 'offset',
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useReplacePlaylist } from '/@/renderer/features/playlists/mutations/replace-playlist-mutation';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export const SaveAndReplaceContextModal = ({
|
||||
innerProps,
|
||||
}: ContextModalProps<{ listData: unknown[]; playlistId: string }>) => {
|
||||
const { t } = useTranslation();
|
||||
const { listData, playlistId } = innerProps;
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const replacePlaylistMutation = useReplacePlaylist({});
|
||||
|
||||
// Get current songs from list data
|
||||
const currentSongIds = useMemo(() => {
|
||||
if (!listData || !Array.isArray(listData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return listData
|
||||
.filter((item): item is Song => {
|
||||
return (
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
'id' in item &&
|
||||
typeof (item as any).id === 'string'
|
||||
);
|
||||
})
|
||||
.map((song) => song.id);
|
||||
}, [listData]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (!serverId || !playlistId) {
|
||||
console.error('serverId or playlistId is not defined');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSongIds.length === 0) {
|
||||
console.error('currentSongIds is empty');
|
||||
toast.error({
|
||||
message: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
replacePlaylistMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId },
|
||||
body: {
|
||||
songId: currentSongIds,
|
||||
},
|
||||
query: {
|
||||
id: playlistId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
toast.error({
|
||||
message: err.message,
|
||||
title: t('error.genericError', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
closeAllModals();
|
||||
toast.success({
|
||||
message: t('form.editPlaylist.success', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [t, currentSongIds, serverId, playlistId, replacePlaylistMutation]);
|
||||
|
||||
return (
|
||||
<ConfirmModal loading={replacePlaylistMutation.isPending} onConfirm={handleConfirm}>
|
||||
<Text>{t('form.editPlaylist.editNote', { postProcess: 'sentenceCase' })}</Text>
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { useRecentPlaylists } from '/@/renderer/features/playlists/hooks/use-recent-playlists';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import { ReplacePlaylistArgs, ReplacePlaylistResponse } from '/@/shared/types/domain-types';
|
||||
|
||||
export const useReplacePlaylist = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
const queryClient = useQueryClient();
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const { addRecentPlaylist } = useRecentPlaylists(serverId);
|
||||
|
||||
return useMutation<ReplacePlaylistResponse, AxiosError, ReplacePlaylistArgs, null>({
|
||||
mutationFn: (args) => {
|
||||
return api.controller.replacePlaylist({
|
||||
...args,
|
||||
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, variables, context) => {
|
||||
const { apiClientProps } = variables;
|
||||
const serverId = apiClientProps.serverId;
|
||||
|
||||
if (!serverId) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
exact: false,
|
||||
queryKey: queryKeys.playlists.list(serverId),
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.detail(serverId, variables.query.id),
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.songList(serverId, variables.query.id),
|
||||
});
|
||||
|
||||
addRecentPlaylist(variables.query.id);
|
||||
|
||||
options?.onSuccess?.(_data, variables, context);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -386,10 +386,11 @@ const PlaylistDetailSongListRoute = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const isSmartPlaylist =
|
||||
const isSmartPlaylist = Boolean(
|
||||
!detailQuery?.isLoading &&
|
||||
detailQuery?.data?.rules &&
|
||||
server?.type === ServerType.NAVIDROME;
|
||||
detailQuery?.data?.rules &&
|
||||
server?.type === ServerType.NAVIDROME,
|
||||
);
|
||||
|
||||
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
||||
const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false);
|
||||
@@ -406,18 +407,22 @@ const PlaylistDetailSongListRoute = () => {
|
||||
|
||||
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||
const [listData, setListData] = useState<unknown[]>([]);
|
||||
const [mode, setMode] = useState<'edit' | 'view'>('view');
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return {
|
||||
customFilters: undefined,
|
||||
id: playlistId,
|
||||
isSmartPlaylist,
|
||||
itemCount,
|
||||
listData,
|
||||
mode,
|
||||
pageKey: ItemListKey.PLAYLIST_SONG,
|
||||
setItemCount,
|
||||
setListData,
|
||||
setMode,
|
||||
};
|
||||
}, [playlistId, itemCount, listData]);
|
||||
}, [playlistId, isSmartPlaylist, itemCount, listData, mode]);
|
||||
|
||||
return (
|
||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||
|
||||
@@ -5,13 +5,14 @@ import { RefreshButton } from '/@/renderer/features/shared/components/refresh-bu
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface ListRefreshButtonProps {
|
||||
disabled?: boolean;
|
||||
listKey: ItemListKey;
|
||||
}
|
||||
|
||||
export const ListRefreshButton = ({ listKey }: ListRefreshButtonProps) => {
|
||||
export const ListRefreshButton = ({ disabled, listKey }: ListRefreshButtonProps) => {
|
||||
const handleRefresh = useCallback(() => {
|
||||
eventEmitter.emit('ITEM_LIST_REFRESH', { key: listKey });
|
||||
}, [listKey]);
|
||||
|
||||
return <RefreshButton onClick={handleRefresh} />;
|
||||
return <RefreshButton disabled={disabled} onClick={handleRefresh} />;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface ListSortByDropdownProps {
|
||||
defaultSortByValue: string;
|
||||
disabled?: boolean;
|
||||
itemType: LibraryItem;
|
||||
listKey: ItemListKey;
|
||||
onChange?: (value: string) => void;
|
||||
@@ -26,6 +27,7 @@ interface ListSortByDropdownProps {
|
||||
|
||||
export const ListSortByDropdown = ({
|
||||
defaultSortByValue,
|
||||
disabled,
|
||||
itemType,
|
||||
listKey,
|
||||
onChange,
|
||||
@@ -44,9 +46,15 @@ export const ListSortByDropdown = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu disabled={disabled} position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
{target ? target : <Button variant="subtle">{sortByLabel}</Button>}
|
||||
{target ? (
|
||||
target
|
||||
) : (
|
||||
<Button disabled={disabled} variant="subtle">
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[itemType][server.type].map((f) => (
|
||||
|
||||
@@ -5,11 +5,13 @@ import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface ListSortOrderToggleButtonProps {
|
||||
defaultSortOrder: SortOrder;
|
||||
disabled?: boolean;
|
||||
listKey: ItemListKey;
|
||||
}
|
||||
|
||||
export const ListSortOrderToggleButton = ({
|
||||
defaultSortOrder,
|
||||
disabled,
|
||||
listKey,
|
||||
}: ListSortOrderToggleButtonProps) => {
|
||||
const { setSortOrder, sortOrder } = useSortOrderFilter(defaultSortOrder, listKey);
|
||||
@@ -20,6 +22,10 @@ export const ListSortOrderToggleButton = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={sortOrder as SortOrder} />
|
||||
<OrderToggleButton
|
||||
disabled={disabled}
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={sortOrder as SortOrder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,15 +5,22 @@ import { SortOrder } from '/@/shared/types/domain-types';
|
||||
|
||||
interface OrderToggleButtonProps {
|
||||
buttonProps?: Partial<ActionIconProps>;
|
||||
disabled?: boolean;
|
||||
onToggle: () => void;
|
||||
sortOrder: SortOrder;
|
||||
}
|
||||
|
||||
export const OrderToggleButton = ({ buttonProps, onToggle, sortOrder }: OrderToggleButtonProps) => {
|
||||
export const OrderToggleButton = ({
|
||||
buttonProps,
|
||||
disabled,
|
||||
onToggle,
|
||||
sortOrder,
|
||||
}: OrderToggleButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
disabled={disabled}
|
||||
icon={sortOrder === SortOrder.ASC ? 'sortAsc' : 'sortDesc'}
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
|
||||
Reference in New Issue
Block a user