diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 2e28fed1..94061497 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -25,6 +25,23 @@ import { useTranslation } from 'react-i18next'; import { showToast } from '../utils/toast'; import type { EntityShareKind } from '../utils/shareLink'; import { copyEntityShareLink } from '../utils/copyEntityShareLink'; +import { useConfirmModalStore } from '../store/confirmModalStore'; + +/** Ask user before re-adding songs to a playlist when *all* selected ids are + * already present. Returns true → caller should append them as duplicates, + * false → caller should keep today's silent-skip toast behavior. */ +async function confirmAddAllDuplicates( + playlistName: string, + count: number, + t: (key: string, opts?: Record) => string, +): Promise { + return useConfirmModalStore.getState().request({ + title: t('playlists.duplicateConfirmTitle'), + message: t('playlists.duplicateConfirmMessage', { count, playlist: playlistName }), + confirmLabel: t('playlists.duplicateConfirmAction'), + cancelLabel: t('common.cancel'), + }); +} function sanitizeFilename(name: string): string { return name @@ -110,10 +127,17 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: { if (newIds.length > 0) { await updatePlaylist(pl.id, [...songs.map((s) => s.id), ...newIds]); showToast(t('playlists.addSuccess', { count: newIds.length, playlist: pl.name })); + touchPlaylist(pl.id); } else { - showToast(t('playlists.addAllSkipped', { count: songIds.length, playlist: pl.name }), 3000, 'info'); + const accepted = await confirmAddAllDuplicates(pl.name, songIds.length, t); + if (accepted) { + await updatePlaylist(pl.id, [...songs.map((s) => s.id), ...songIds]); + showToast(t('playlists.addedAsDuplicates', { count: songIds.length, playlist: pl.name }), 3000, 'info'); + touchPlaylist(pl.id); + } else { + showToast(t('playlists.addAllSkipped', { count: songIds.length, playlist: pl.name }), 3000, 'info'); + } } - touchPlaylist(pl.id); } catch { showToast(t('playlists.addError'), 3000, 'error'); } @@ -273,34 +297,42 @@ function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId }: { albumIds } } - if (newIds.length > 0) { - await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...newIds]); - touchPlaylist(pl.id); - } - - // Show detailed toast notification - const totalSongs = songIds.length; const addedCount = newIds.length; const duplicateCount = duplicateIds.length; - if (addedCount === 0 && duplicateCount > 0) { - showToast( - t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }), - 4000, - 'info' - ); + if (addedCount > 0) { + await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...newIds]); + touchPlaylist(pl.id); + if (duplicateCount > 0) { + showToast( + t('playlists.addPartial', { added: addedCount, skipped: duplicateCount, playlist: pl.name }), + 4000, + 'info' + ); + } else { + showToast( + t('playlists.addSuccess', { count: addedCount, playlist: pl.name }), + 3000, + 'info' + ); + } } else if (duplicateCount > 0) { - showToast( - t('playlists.addPartial', { added: addedCount, skipped: duplicateCount, playlist: pl.name }), - 4000, - 'info' - ); - } else { - showToast( - t('playlists.addSuccess', { count: addedCount, playlist: pl.name }), - 3000, - 'info' - ); + const accepted = await confirmAddAllDuplicates(pl.name, duplicateCount, t); + if (accepted) { + await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...songIds]); + touchPlaylist(pl.id); + showToast( + t('playlists.addedAsDuplicates', { count: duplicateCount, playlist: pl.name }), + 3000, + 'info' + ); + } else { + showToast( + t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }), + 4000, + 'info' + ); + } } } catch (err) { showToast(t('playlists.addError'), 4000, 'error'); @@ -509,33 +541,42 @@ function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId }: { artist } } - if (newIds.length > 0) { - await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...newIds]); - touchPlaylist(pl.id); - } - - // Show detailed toast notification const addedCount = newIds.length; const duplicateCount = duplicateIds.length; - if (addedCount === 0 && duplicateCount > 0) { - showToast( - t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }), - 4000, - 'info' - ); + if (addedCount > 0) { + await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...newIds]); + touchPlaylist(pl.id); + if (duplicateCount > 0) { + showToast( + t('playlists.addPartial', { added: addedCount, skipped: duplicateCount, playlist: pl.name }), + 4000, + 'info' + ); + } else { + showToast( + t('playlists.addSuccess', { count: addedCount, playlist: pl.name }), + 3000, + 'info' + ); + } } else if (duplicateCount > 0) { - showToast( - t('playlists.addPartial', { added: addedCount, skipped: duplicateCount, playlist: pl.name }), - 4000, - 'info' - ); - } else { - showToast( - t('playlists.addSuccess', { count: addedCount, playlist: pl.name }), - 3000, - 'info' - ); + const accepted = await confirmAddAllDuplicates(pl.name, duplicateCount, t); + if (accepted) { + await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...songIds]); + touchPlaylist(pl.id); + showToast( + t('playlists.addedAsDuplicates', { count: duplicateCount, playlist: pl.name }), + 3000, + 'info' + ); + } else { + showToast( + t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }), + 4000, + 'info' + ); + } } } catch (err) { showToast(t('playlists.addError'), 4000, 'error'); diff --git a/src/locales/de.ts b/src/locales/de.ts index 2a8c8f71..2a1a8d2c 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1309,6 +1309,10 @@ export const deTranslation = { addSuccess: '{{count}} Songs zu {{playlist}} hinzugefügt', addPartial: '{{added}} hinzugefügt, {{skipped}} übersprungen (Duplikate)', addAllSkipped: 'Alle übersprungen ({{count}} Duplikate)', + addedAsDuplicates: '{{count}} Songs zu {{playlist}} hinzugefügt (als Duplikate)', + duplicateConfirmTitle: 'Bereits in Playlist', + duplicateConfirmMessage: 'Alle {{count}} Songs sind bereits in "{{playlist}}". Trotzdem als Duplikate hinzufügen?', + duplicateConfirmAction: 'Trotzdem hinzufügen', addError: 'Fehler beim Hinzufügen von Songs', createAndAddSuccess: 'Playlist "{{playlist}}" mit {{count}} Songs erstellt', createError: 'Fehler beim Erstellen der Playlist', diff --git a/src/locales/en.ts b/src/locales/en.ts index a3d8cd22..81e046af 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1315,6 +1315,10 @@ export const enTranslation = { addSuccess: '{{count}} songs added to {{playlist}}', addPartial: '{{added}} added, {{skipped}} skipped (duplicates)', addAllSkipped: 'All skipped ({{count}} duplicates)', + addedAsDuplicates: '{{count}} songs added to {{playlist}} (as duplicates)', + duplicateConfirmTitle: 'Already in playlist', + duplicateConfirmMessage: 'All {{count}} songs are already in "{{playlist}}". Add them again as duplicates?', + duplicateConfirmAction: 'Add anyway', addError: 'Error adding songs', createAndAddSuccess: 'Playlist "{{playlist}}" created with {{count}} songs', createError: 'Error creating playlist', diff --git a/src/locales/es.ts b/src/locales/es.ts index 5de501a7..8e3524ad 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1302,6 +1302,10 @@ export const esTranslation = { addSuccess: '{{count}} canciones agregadas a {{playlist}}', addPartial: '{{added}} agregadas, {{skipped}} skippeadas por duplicadas', addAllSkipped: 'Todas skippeadas ({{count}} duplicadas)', + addedAsDuplicates: '{{count}} canciones añadidas a {{playlist}} (como duplicados)', + duplicateConfirmTitle: 'Ya en la lista', + duplicateConfirmMessage: 'Las {{count}} canciones ya están en "{{playlist}}". ¿Añadirlas igualmente como duplicados?', + duplicateConfirmAction: 'Añadir igualmente', addError: 'Error al agregar canciones', createAndAddSuccess: 'Lista "{{playlist}}" creada con {{count}} canciones', createError: 'Error al crear lista', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 1730b4d4..33e0926a 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -1297,6 +1297,10 @@ export const frTranslation = { addSuccess: '{{count}} morceaux ajoutés à {{playlist}}', addPartial: '{{added}} ajoutés, {{skipped}} ignorés (doublons)', addAllSkipped: 'Tous ignorés ({{count}} doublons)', + addedAsDuplicates: '{{count}} morceaux ajoutés à {{playlist}} (en doublons)', + duplicateConfirmTitle: 'Déjà dans la playlist', + duplicateConfirmMessage: 'Les {{count}} morceaux sont déjà dans « {{playlist}} ». Les ajouter quand même en doublons ?', + duplicateConfirmAction: 'Ajouter quand même', addError: 'Erreur lors de l\'ajout des morceaux', createAndAddSuccess: 'Playlist "{{playlist}}" créée avec {{count}} morceaux', createError: 'Erreur lors de la création de la playlist', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 053bb9a2..c16f41de 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -1296,6 +1296,10 @@ export const nbTranslation = { addSuccess: '{{count}} sanger lagt til i {{playlist}}', addPartial: '{{added}} lagt til, {{skipped}} hoppet over (duplikater)', addAllSkipped: 'Alle hoppet over ({{count}} duplikater)', + addedAsDuplicates: '{{count}} sanger lagt til i {{playlist}} (som duplikater)', + duplicateConfirmTitle: 'Allerede i spillelisten', + duplicateConfirmMessage: 'Alle {{count}} sanger finnes allerede i "{{playlist}}". Legg dem til likevel som duplikater?', + duplicateConfirmAction: 'Legg til likevel', addError: 'Feil ved å legge til sanger', createAndAddSuccess: 'Spilleliste "{{playlist}}" opprettet med {{count}} sanger', createError: 'Feil ved oppretting av spilleliste', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 6d814e9c..c2038da1 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -1296,6 +1296,10 @@ export const nlTranslation = { addSuccess: '{{count}} nummers toegevoegd aan {{playlist}}', addPartial: '{{added}} toegevoegd, {{skipped}} overgeslagen (duplicaten)', addAllSkipped: 'Alles overgeslagen ({{count}} duplicaten)', + addedAsDuplicates: '{{count}} nummers toegevoegd aan {{playlist}} (als duplicaten)', + duplicateConfirmTitle: 'Al in afspeellijst', + duplicateConfirmMessage: 'Alle {{count}} nummers staan al in "{{playlist}}". Toch als duplicaten toevoegen?', + duplicateConfirmAction: 'Toch toevoegen', addError: 'Fout bij toevoegen van nummers', createAndAddSuccess: 'Playlist "{{playlist}}" aangemaakt met {{count}} nummers', createError: 'Fout bij aanmaken van playlist', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index dfbd274f..08bce342 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1381,6 +1381,10 @@ export const ruTranslation = { addSuccess: '{{count}} треков добавлено в {{playlist}}', addPartial: '{{added}} добавлено, {{skipped}} пропущено (дубликаты)', addAllSkipped: 'Все пропущены ({{count}} дубликатов)', + addedAsDuplicates: '{{count}} треков добавлено в {{playlist}} (как дубликаты)', + duplicateConfirmTitle: 'Уже в плейлисте', + duplicateConfirmMessage: 'Все {{count}} треков уже есть в «{{playlist}}». Всё равно добавить их как дубликаты?', + duplicateConfirmAction: 'Всё равно добавить', addError: 'Ошибка при добавлении треков', createAndAddSuccess: 'Плейлист "{{playlist}}" создан с {{count}} треками', createError: 'Ошибка при создании плейлиста', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index c1b551d8..4bcfd670 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1291,6 +1291,10 @@ export const zhTranslation = { addSuccess: '{{count}} 首歌曲已添加到 {{playlist}}', addPartial: '{{added}} 首已添加,{{skipped}} 首跳过(重复)', addAllSkipped: '全部跳过({{count}} 首重复)', + addedAsDuplicates: '{{count}} 首歌曲已添加到 {{playlist}}(作为重复项)', + duplicateConfirmTitle: '已在播放列表中', + duplicateConfirmMessage: '所有 {{count}} 首歌曲已在 "{{playlist}}" 中。仍要作为重复项添加吗?', + duplicateConfirmAction: '仍要添加', addError: '添加歌曲时出错', createAndAddSuccess: '播放列表 "{{playlist}}" 已创建,包含 {{count}} 首歌曲', createError: '创建播放列表时出错',