feat: confirm before discarding the current queue (#2300)

* feat: confirm before discarding the current queue
This commit is contained in:
York
2026-08-05 12:33:49 +08:00
committed by GitHub
parent 2572726730
commit 0a9d1fbbf1
5 changed files with 134 additions and 35 deletions
+6
View File
@@ -364,6 +364,10 @@
"title": "Add items to the queue", "title": "Add items to the queue",
"description": "This action will add all items in the current filtered view" "description": "This action will add all items in the current filtered view"
}, },
"queueChangeConfirmation": {
"title": "Discard the current queue?",
"description": "This will remove all items from the current queue."
},
"addToPlaylist": { "addToPlaylist": {
"create": "Create $t(entity.playlist, {\"count\": 1}) {{playlist}}", "create": "Create $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"input_playlists": "$t(entity.playlist, {\"count\": 2})", "input_playlists": "$t(entity.playlist, {\"count\": 2})",
@@ -1075,6 +1079,8 @@
"passwordStore": "Passwords/secret store", "passwordStore": "Passwords/secret store",
"playerFilters": "Filter songs from the queue", "playerFilters": "Filter songs from the queue",
"playerFilters_description": "Omit songs from being added to the queue based on the following criteria", "playerFilters_description": "Omit songs from being added to the queue based on the following criteria",
"confirmQueueChanges": "Confirm queue changes",
"confirmQueueChanges_description": "Ask for confirmation before discarding the current queue",
"playbackStyle_description": "Select the playback style to use for the audio player", "playbackStyle_description": "Select the playback style to use for the audio player",
"playbackStyle_optionCrossFade": "Crossfade", "playbackStyle_optionCrossFade": "Crossfade",
"playbackStyle_optionNormal": "Normal", "playbackStyle_optionNormal": "Normal",
@@ -138,7 +138,7 @@ const QueuePlaybackIcons = ({ tableRef }: { tableRef: RefObject<ItemListHandle |
variant="subtle" variant="subtle"
/> />
<ActionIcon <ActionIcon
icon="x" icon="delete"
iconProps={{ size: 'lg' }} iconProps={{ size: 'lg' }}
onClick={handleClearQueue} onClick={handleClearQueue}
tooltip={{ label: t('action.clearQueue') }} tooltip={{ label: t('action.clearQueue') }}
@@ -17,7 +17,12 @@ import {
} from '/@/renderer/features/player/utils'; } from '/@/renderer/features/player/utils';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { AddToQueueType, usePlayerActions, useSettingsStore } from '/@/renderer/store'; import {
AddToQueueType,
usePlayerActions,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
import { logger } from '/@/renderer/utils/logger'; import { logger } from '/@/renderer/utils/logger';
import { shuffle as shuffleArray } from '/@/renderer/utils/shuffle'; import { shuffle as shuffleArray } from '/@/renderer/utils/shuffle';
import { sortSongsByFetchedOrder } from '/@/shared/api/utils'; import { sortSongsByFetchedOrder } from '/@/shared/api/utils';
@@ -164,6 +169,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const storeActions = usePlayerActions(); const storeActions = usePlayerActions();
const settingsActions = useSettingsStoreActions();
const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({}); const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
const [doNotShowAgain, setDoNotShowAgain] = useLocalStorage({ const [doNotShowAgain, setDoNotShowAgain] = useLocalStorage({
@@ -171,6 +177,48 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
key: 'large_fetch_confirmation', key: 'large_fetch_confirmation',
}); });
const confirmQueueChange = useCallback(
(onConfirm: () => void) => {
const shouldConfirm = useSettingsStore.getState().general.confirmQueueChanges;
if (!shouldConfirm || storeActions.getQueue().items.length === 0) {
onConfirm();
return;
}
openModal({
children: (
<ConfirmModal
labels={{
cancel: t('common.cancel'),
confirm: t('common.confirm'),
}}
onConfirm={() => {
closeAllModals();
onConfirm();
}}
>
<Stack>
<Text>{t('form.queueChangeConfirmation.description')}</Text>
<Checkbox
label={t('common.doNotShowAgain')}
onChange={(event) => {
settingsActions.setSettings({
general: {
confirmQueueChanges: !event.currentTarget.checked,
},
});
}}
/>
</Stack>
</ConfirmModal>
),
title: t('form.queueChangeConfirmation.title'),
});
},
[settingsActions, storeActions, t],
);
const confirmLargeFetch = useCallback((): Promise<boolean> => { const confirmLargeFetch = useCallback((): Promise<boolean> => {
if (doNotShowAgain) { if (doNotShowAgain) {
return Promise.resolve(true); return Promise.resolve(true);
@@ -225,29 +273,42 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
filteredData = tagPlaylistContext(filteredData, resolvedContextId); filteredData = tagPlaylistContext(filteredData, resolvedContextId);
} }
if (typeof type === 'object' && 'edge' in type && type.edge !== null) { const addToQueue = () => {
const edge = type.edge === 'top' ? 'top' : 'bottom'; if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
logger.debug('Added to queue by data', { logger.debug('Added to queue by data', {
data: data.length, data: data.length,
edge, edge,
filtered: filteredData.length, filtered: filteredData.length,
type, type,
uniqueId: type.uniqueId, uniqueId: type.uniqueId,
}); });
storeActions.addToQueueByUniqueId(filteredData, type.uniqueId, edge, playSongId); storeActions.addToQueueByUniqueId(
filteredData,
type.uniqueId,
edge,
playSongId,
);
} else {
logger.debug('Added to queue by type', {
data: data.length,
filtered: filteredData.length,
type,
});
storeActions.addToQueueByType(filteredData, type as Play, playSongId);
}
};
if (isReplaceQueueType(type)) {
confirmQueueChange(addToQueue);
} else { } else {
logger.debug('Added to queue by type', { addToQueue();
data: data.length,
filtered: filteredData.length,
type,
});
storeActions.addToQueueByType(filteredData, type as Play, playSongId);
} }
}, },
[storeActions], [confirmQueueChange, storeActions],
); );
const addToQueueByFetch = useCallback( const addToQueueByFetch = useCallback(
@@ -324,11 +385,19 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
filteredSongs = tagPlaylistContext(filteredSongs, resolvedContextId); filteredSongs = tagPlaylistContext(filteredSongs, resolvedContextId);
} }
if (typeof type === 'object' && 'edge' in type && type.edge !== null) { const addToQueue = () => {
const edge = type.edge === 'top' ? 'top' : 'bottom'; if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
storeActions.addToQueueByUniqueId(filteredSongs, type.uniqueId, edge); const edge = type.edge === 'top' ? 'top' : 'bottom';
storeActions.addToQueueByUniqueId(filteredSongs, type.uniqueId, edge);
} else {
storeActions.addToQueueByType(filteredSongs, type as Play);
}
};
if (isReplaceQueueType(type)) {
confirmQueueChange(addToQueue);
} else { } else {
storeActions.addToQueueByType(filteredSongs, type as Play); addToQueue();
} }
} catch (err: any) { } catch (err: any) {
if (instanceOfCancellationError(err)) { if (instanceOfCancellationError(err)) {
@@ -347,7 +416,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
}); });
} }
}, },
[queryClient, storeActions, t], [confirmQueueChange, queryClient, storeActions, t],
); );
const addToQueueByListQuery = useCallback( const addToQueueByListQuery = useCallback(
@@ -526,10 +595,12 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
); );
const clearQueue = useCallback(() => { const clearQueue = useCallback(() => {
logger.debug('Cleared queue'); confirmQueueChange(() => {
logger.debug('Cleared queue');
storeActions.clearQueue(); storeActions.clearQueue();
}, [storeActions]); });
}, [confirmQueueChange, storeActions]);
const clearSelected = useCallback( const clearSelected = useCallback(
(items: QueueSong[]) => { (items: QueueSong[]) => {
@@ -637,15 +708,17 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setQueue = useCallback( const setQueue = useCallback(
(data: Song[], index?: number, position?: number) => { (data: Song[], index?: number, position?: number) => {
logger.debug('Set queue', { confirmQueueChange(() => {
data: data.length, logger.debug('Set queue', {
index, data: data.length,
position, index,
}); position,
});
storeActions.setQueue(data, index, position); storeActions.setQueue(data, index, position);
});
}, },
[storeActions], [confirmQueueChange, storeActions],
); );
const setSpeed = useCallback( const setSpeed = useCallback(
@@ -363,6 +363,24 @@ export const ApplicationSettings = memo(() => {
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.savePlayQueue'), title: t('setting.savePlayQueue'),
}, },
{
control: (
<Switch
aria-label={t('setting.confirmQueueChanges')}
checked={settings.confirmQueueChanges}
onChange={(event) => {
setSettings({
general: {
...settings,
confirmQueueChanges: event.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.confirmQueueChanges', { context: 'description' }),
title: t('setting.confirmQueueChanges'),
},
{ {
control: ( control: (
<Switch <Switch
+2
View File
@@ -519,6 +519,7 @@ export const GeneralSettingsSchema = z.object({
buttonSize: z.number(), buttonSize: z.number(),
collections: z.array(CollectionSchema), collections: z.array(CollectionSchema),
combinedLyricsAndVisualizer: z.boolean(), combinedLyricsAndVisualizer: z.boolean(),
confirmQueueChanges: z.boolean(),
disabledContextMenu: z.record(z.string(), z.boolean()), disabledContextMenu: z.record(z.string(), z.boolean()),
enableGridMultiSelect: z.boolean(), enableGridMultiSelect: z.boolean(),
externalLinks: z.boolean(), externalLinks: z.boolean(),
@@ -1302,6 +1303,7 @@ const initialState: SettingsState = {
buttonSize: 15, buttonSize: 15,
collections: [], collections: [],
combinedLyricsAndVisualizer: false, combinedLyricsAndVisualizer: false,
confirmQueueChanges: true,
disabledContextMenu: {}, disabledContextMenu: {},
enableGridMultiSelect: false, enableGridMultiSelect: false,
externalLinks: true, externalLinks: true,