From 0a9d1fbbf1fd3c92c58de71b42e20513a3a4c019 Mon Sep 17 00:00:00 2001 From: York Date: Wed, 5 Aug 2026 12:33:49 +0800 Subject: [PATCH] feat: confirm before discarding the current queue (#2300) * feat: confirm before discarding the current queue --- src/i18n/locales/en.json | 6 + .../components/play-queue-list-controls.tsx | 2 +- .../player/context/player-context.tsx | 141 +++++++++++++----- .../general/application-settings.tsx | 18 +++ src/renderer/store/settings.store.ts | 2 + 5 files changed, 134 insertions(+), 35 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 9374d3680..82cfcbb35 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -364,6 +364,10 @@ "title": "Add items to the queue", "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": { "create": "Create $t(entity.playlist, {\"count\": 1}) {{playlist}}", "input_playlists": "$t(entity.playlist, {\"count\": 2})", @@ -1075,6 +1079,8 @@ "passwordStore": "Passwords/secret store", "playerFilters": "Filter songs from the queue", "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_optionCrossFade": "Crossfade", "playbackStyle_optionNormal": "Normal", diff --git a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx index 719b2ce31..1a274614e 100644 --- a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx +++ b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx @@ -138,7 +138,7 @@ const QueuePlaybackIcons = ({ tableRef }: { tableRef: RefObject { const { t } = useTranslation(); const queryClient = useQueryClient(); const storeActions = usePlayerActions(); + const settingsActions = useSettingsStoreActions(); const timeoutIds = useRef>>({}); const [doNotShowAgain, setDoNotShowAgain] = useLocalStorage({ @@ -171,6 +177,48 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { 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: ( + { + closeAllModals(); + onConfirm(); + }} + > + + {t('form.queueChangeConfirmation.description')} + { + settingsActions.setSettings({ + general: { + confirmQueueChanges: !event.currentTarget.checked, + }, + }); + }} + /> + + + ), + title: t('form.queueChangeConfirmation.title'), + }); + }, + [settingsActions, storeActions, t], + ); + const confirmLargeFetch = useCallback((): Promise => { if (doNotShowAgain) { return Promise.resolve(true); @@ -225,29 +273,42 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { filteredData = tagPlaylistContext(filteredData, resolvedContextId); } - if (typeof type === 'object' && 'edge' in type && type.edge !== null) { - const edge = type.edge === 'top' ? 'top' : 'bottom'; + const addToQueue = () => { + if (typeof type === 'object' && 'edge' in type && type.edge !== null) { + const edge = type.edge === 'top' ? 'top' : 'bottom'; - logger.debug('Added to queue by data', { - data: data.length, - edge, - filtered: filteredData.length, - type, - uniqueId: type.uniqueId, - }); + logger.debug('Added to queue by data', { + data: data.length, + edge, + filtered: filteredData.length, + type, + 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 { - logger.debug('Added to queue by type', { - data: data.length, - filtered: filteredData.length, - type, - }); - - storeActions.addToQueueByType(filteredData, type as Play, playSongId); + addToQueue(); } }, - [storeActions], + [confirmQueueChange, storeActions], ); const addToQueueByFetch = useCallback( @@ -324,11 +385,19 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { filteredSongs = tagPlaylistContext(filteredSongs, resolvedContextId); } - if (typeof type === 'object' && 'edge' in type && type.edge !== null) { - const edge = type.edge === 'top' ? 'top' : 'bottom'; - storeActions.addToQueueByUniqueId(filteredSongs, type.uniqueId, edge); + const addToQueue = () => { + if (typeof type === 'object' && 'edge' in type && type.edge !== null) { + 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 { - storeActions.addToQueueByType(filteredSongs, type as Play); + addToQueue(); } } catch (err: any) { if (instanceOfCancellationError(err)) { @@ -347,7 +416,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { }); } }, - [queryClient, storeActions, t], + [confirmQueueChange, queryClient, storeActions, t], ); const addToQueueByListQuery = useCallback( @@ -526,10 +595,12 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { ); const clearQueue = useCallback(() => { - logger.debug('Cleared queue'); + confirmQueueChange(() => { + logger.debug('Cleared queue'); - storeActions.clearQueue(); - }, [storeActions]); + storeActions.clearQueue(); + }); + }, [confirmQueueChange, storeActions]); const clearSelected = useCallback( (items: QueueSong[]) => { @@ -637,15 +708,17 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => { const setQueue = useCallback( (data: Song[], index?: number, position?: number) => { - logger.debug('Set queue', { - data: data.length, - index, - position, - }); + confirmQueueChange(() => { + logger.debug('Set queue', { + data: data.length, + index, + position, + }); - storeActions.setQueue(data, index, position); + storeActions.setQueue(data, index, position); + }); }, - [storeActions], + [confirmQueueChange, storeActions], ); const setSpeed = useCallback( diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index 2404aa915..e5038e038 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -363,6 +363,24 @@ export const ApplicationSettings = memo(() => { isHidden: !isElectron(), title: t('setting.savePlayQueue'), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + confirmQueueChanges: event.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.confirmQueueChanges', { context: 'description' }), + title: t('setting.confirmQueueChanges'), + }, { control: (