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",
"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",
@@ -138,7 +138,7 @@ const QueuePlaybackIcons = ({ tableRef }: { tableRef: RefObject<ItemListHandle |
variant="subtle"
/>
<ActionIcon
icon="x"
icon="delete"
iconProps={{ size: 'lg' }}
onClick={handleClearQueue}
tooltip={{ label: t('action.clearQueue') }}
@@ -17,7 +17,12 @@ import {
} from '/@/renderer/features/player/utils';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-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 { shuffle as shuffleArray } from '/@/renderer/utils/shuffle';
import { sortSongsByFetchedOrder } from '/@/shared/api/utils';
@@ -164,6 +169,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const storeActions = usePlayerActions();
const settingsActions = useSettingsStoreActions();
const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
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: (
<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> => {
if (doNotShowAgain) {
return Promise.resolve(true);
@@ -225,6 +273,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
filteredData = tagPlaylistContext(filteredData, resolvedContextId);
}
const addToQueue = () => {
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
@@ -236,7 +285,12 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
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,
@@ -246,8 +300,15 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
storeActions.addToQueueByType(filteredData, type as Play, playSongId);
}
};
if (isReplaceQueueType(type)) {
confirmQueueChange(addToQueue);
} else {
addToQueue();
}
},
[storeActions],
[confirmQueueChange, storeActions],
);
const addToQueueByFetch = useCallback(
@@ -324,12 +385,20 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
filteredSongs = tagPlaylistContext(filteredSongs, resolvedContextId);
}
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 {
addToQueue();
}
} catch (err: any) {
if (instanceOfCancellationError(err)) {
return;
@@ -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(() => {
confirmQueueChange(() => {
logger.debug('Cleared queue');
storeActions.clearQueue();
}, [storeActions]);
});
}, [confirmQueueChange, storeActions]);
const clearSelected = useCallback(
(items: QueueSong[]) => {
@@ -637,6 +708,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const setQueue = useCallback(
(data: Song[], index?: number, position?: number) => {
confirmQueueChange(() => {
logger.debug('Set queue', {
data: data.length,
index,
@@ -644,8 +716,9 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
});
storeActions.setQueue(data, index, position);
});
},
[storeActions],
[confirmQueueChange, storeActions],
);
const setSpeed = useCallback(
@@ -363,6 +363,24 @@ export const ApplicationSettings = memo(() => {
isHidden: !isElectron(),
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: (
<Switch
+2
View File
@@ -519,6 +519,7 @@ export const GeneralSettingsSchema = z.object({
buttonSize: z.number(),
collections: z.array(CollectionSchema),
combinedLyricsAndVisualizer: z.boolean(),
confirmQueueChanges: z.boolean(),
disabledContextMenu: z.record(z.string(), z.boolean()),
enableGridMultiSelect: z.boolean(),
externalLinks: z.boolean(),
@@ -1302,6 +1303,7 @@ const initialState: SettingsState = {
buttonSize: 15,
collections: [],
combinedLyricsAndVisualizer: false,
confirmQueueChanges: true,
disabledContextMenu: {},
enableGridMultiSelect: false,
externalLinks: true,