mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(playlists): confirm before adding songs that are already in the playlist (#329)
Closes #327. When every selected song is already in the target playlist, the "Add to Playlist" flow used to silently show an "all skipped" toast. Users could not tell whether they had hit the wrong target or whether the action was deliberately ignored, and they had no way to add duplicates intentionally (e.g. for a song they actually want twice). New behavior: if every id is a duplicate, ask via the existing GlobalConfirmModal — "Add anyway" appends them as duplicates, "Cancel" keeps the previous silent-skip toast. Mixed cases (some new, some duplicate) and the all-new path are unchanged. Applied at all three add-to-playlist call sites in the context menu (single/multi-track, album selection, artist selection). Strings added to all 8 locales (en, de, es, fr, nl, nb, ru, zh). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
9fe81ee6f6
commit
e0c53da94d
@@ -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, unknown>) => string,
|
||||
): Promise<boolean> {
|
||||
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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Ошибка при создании плейлиста',
|
||||
|
||||
@@ -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: '创建播放列表时出错',
|
||||
|
||||
Reference in New Issue
Block a user