mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +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 { showToast } from '../utils/toast';
|
||||||
import type { EntityShareKind } from '../utils/shareLink';
|
import type { EntityShareKind } from '../utils/shareLink';
|
||||||
import { copyEntityShareLink } from '../utils/copyEntityShareLink';
|
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 {
|
function sanitizeFilename(name: string): string {
|
||||||
return name
|
return name
|
||||||
@@ -110,10 +127,17 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
|
|||||||
if (newIds.length > 0) {
|
if (newIds.length > 0) {
|
||||||
await updatePlaylist(pl.id, [...songs.map((s) => s.id), ...newIds]);
|
await updatePlaylist(pl.id, [...songs.map((s) => s.id), ...newIds]);
|
||||||
showToast(t('playlists.addSuccess', { count: newIds.length, playlist: pl.name }));
|
showToast(t('playlists.addSuccess', { count: newIds.length, playlist: pl.name }));
|
||||||
|
touchPlaylist(pl.id);
|
||||||
} else {
|
} 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 {
|
} catch {
|
||||||
showToast(t('playlists.addError'), 3000, 'error');
|
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 addedCount = newIds.length;
|
||||||
const duplicateCount = duplicateIds.length;
|
const duplicateCount = duplicateIds.length;
|
||||||
|
|
||||||
if (addedCount === 0 && duplicateCount > 0) {
|
if (addedCount > 0) {
|
||||||
showToast(
|
await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...newIds]);
|
||||||
t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }),
|
touchPlaylist(pl.id);
|
||||||
4000,
|
if (duplicateCount > 0) {
|
||||||
'info'
|
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) {
|
} else if (duplicateCount > 0) {
|
||||||
showToast(
|
const accepted = await confirmAddAllDuplicates(pl.name, duplicateCount, t);
|
||||||
t('playlists.addPartial', { added: addedCount, skipped: duplicateCount, playlist: pl.name }),
|
if (accepted) {
|
||||||
4000,
|
await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...songIds]);
|
||||||
'info'
|
touchPlaylist(pl.id);
|
||||||
);
|
showToast(
|
||||||
} else {
|
t('playlists.addedAsDuplicates', { count: duplicateCount, playlist: pl.name }),
|
||||||
showToast(
|
3000,
|
||||||
t('playlists.addSuccess', { count: addedCount, playlist: pl.name }),
|
'info'
|
||||||
3000,
|
);
|
||||||
'info'
|
} else {
|
||||||
);
|
showToast(
|
||||||
|
t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }),
|
||||||
|
4000,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(t('playlists.addError'), 4000, 'error');
|
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 addedCount = newIds.length;
|
||||||
const duplicateCount = duplicateIds.length;
|
const duplicateCount = duplicateIds.length;
|
||||||
|
|
||||||
if (addedCount === 0 && duplicateCount > 0) {
|
if (addedCount > 0) {
|
||||||
showToast(
|
await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...newIds]);
|
||||||
t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }),
|
touchPlaylist(pl.id);
|
||||||
4000,
|
if (duplicateCount > 0) {
|
||||||
'info'
|
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) {
|
} else if (duplicateCount > 0) {
|
||||||
showToast(
|
const accepted = await confirmAddAllDuplicates(pl.name, duplicateCount, t);
|
||||||
t('playlists.addPartial', { added: addedCount, skipped: duplicateCount, playlist: pl.name }),
|
if (accepted) {
|
||||||
4000,
|
await updatePlaylist(pl.id, [...existingSongs.map((s) => s.id), ...songIds]);
|
||||||
'info'
|
touchPlaylist(pl.id);
|
||||||
);
|
showToast(
|
||||||
} else {
|
t('playlists.addedAsDuplicates', { count: duplicateCount, playlist: pl.name }),
|
||||||
showToast(
|
3000,
|
||||||
t('playlists.addSuccess', { count: addedCount, playlist: pl.name }),
|
'info'
|
||||||
3000,
|
);
|
||||||
'info'
|
} else {
|
||||||
);
|
showToast(
|
||||||
|
t('playlists.addAllSkipped', { count: duplicateCount, playlist: pl.name }),
|
||||||
|
4000,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(t('playlists.addError'), 4000, 'error');
|
showToast(t('playlists.addError'), 4000, 'error');
|
||||||
|
|||||||
@@ -1309,6 +1309,10 @@ export const deTranslation = {
|
|||||||
addSuccess: '{{count}} Songs zu {{playlist}} hinzugefügt',
|
addSuccess: '{{count}} Songs zu {{playlist}} hinzugefügt',
|
||||||
addPartial: '{{added}} hinzugefügt, {{skipped}} übersprungen (Duplikate)',
|
addPartial: '{{added}} hinzugefügt, {{skipped}} übersprungen (Duplikate)',
|
||||||
addAllSkipped: 'Alle übersprungen ({{count}} 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',
|
addError: 'Fehler beim Hinzufügen von Songs',
|
||||||
createAndAddSuccess: 'Playlist "{{playlist}}" mit {{count}} Songs erstellt',
|
createAndAddSuccess: 'Playlist "{{playlist}}" mit {{count}} Songs erstellt',
|
||||||
createError: 'Fehler beim Erstellen der Playlist',
|
createError: 'Fehler beim Erstellen der Playlist',
|
||||||
|
|||||||
@@ -1315,6 +1315,10 @@ export const enTranslation = {
|
|||||||
addSuccess: '{{count}} songs added to {{playlist}}',
|
addSuccess: '{{count}} songs added to {{playlist}}',
|
||||||
addPartial: '{{added}} added, {{skipped}} skipped (duplicates)',
|
addPartial: '{{added}} added, {{skipped}} skipped (duplicates)',
|
||||||
addAllSkipped: 'All skipped ({{count}} 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',
|
addError: 'Error adding songs',
|
||||||
createAndAddSuccess: 'Playlist "{{playlist}}" created with {{count}} songs',
|
createAndAddSuccess: 'Playlist "{{playlist}}" created with {{count}} songs',
|
||||||
createError: 'Error creating playlist',
|
createError: 'Error creating playlist',
|
||||||
|
|||||||
@@ -1302,6 +1302,10 @@ export const esTranslation = {
|
|||||||
addSuccess: '{{count}} canciones agregadas a {{playlist}}',
|
addSuccess: '{{count}} canciones agregadas a {{playlist}}',
|
||||||
addPartial: '{{added}} agregadas, {{skipped}} skippeadas por duplicadas',
|
addPartial: '{{added}} agregadas, {{skipped}} skippeadas por duplicadas',
|
||||||
addAllSkipped: 'Todas skippeadas ({{count}} 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',
|
addError: 'Error al agregar canciones',
|
||||||
createAndAddSuccess: 'Lista "{{playlist}}" creada con {{count}} canciones',
|
createAndAddSuccess: 'Lista "{{playlist}}" creada con {{count}} canciones',
|
||||||
createError: 'Error al crear lista',
|
createError: 'Error al crear lista',
|
||||||
|
|||||||
@@ -1297,6 +1297,10 @@ export const frTranslation = {
|
|||||||
addSuccess: '{{count}} morceaux ajoutés à {{playlist}}',
|
addSuccess: '{{count}} morceaux ajoutés à {{playlist}}',
|
||||||
addPartial: '{{added}} ajoutés, {{skipped}} ignorés (doublons)',
|
addPartial: '{{added}} ajoutés, {{skipped}} ignorés (doublons)',
|
||||||
addAllSkipped: 'Tous ignorés ({{count}} 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',
|
addError: 'Erreur lors de l\'ajout des morceaux',
|
||||||
createAndAddSuccess: 'Playlist "{{playlist}}" créée avec {{count}} morceaux',
|
createAndAddSuccess: 'Playlist "{{playlist}}" créée avec {{count}} morceaux',
|
||||||
createError: 'Erreur lors de la création de la playlist',
|
createError: 'Erreur lors de la création de la playlist',
|
||||||
|
|||||||
@@ -1296,6 +1296,10 @@ export const nbTranslation = {
|
|||||||
addSuccess: '{{count}} sanger lagt til i {{playlist}}',
|
addSuccess: '{{count}} sanger lagt til i {{playlist}}',
|
||||||
addPartial: '{{added}} lagt til, {{skipped}} hoppet over (duplikater)',
|
addPartial: '{{added}} lagt til, {{skipped}} hoppet over (duplikater)',
|
||||||
addAllSkipped: 'Alle hoppet over ({{count}} 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',
|
addError: 'Feil ved å legge til sanger',
|
||||||
createAndAddSuccess: 'Spilleliste "{{playlist}}" opprettet med {{count}} sanger',
|
createAndAddSuccess: 'Spilleliste "{{playlist}}" opprettet med {{count}} sanger',
|
||||||
createError: 'Feil ved oppretting av spilleliste',
|
createError: 'Feil ved oppretting av spilleliste',
|
||||||
|
|||||||
@@ -1296,6 +1296,10 @@ export const nlTranslation = {
|
|||||||
addSuccess: '{{count}} nummers toegevoegd aan {{playlist}}',
|
addSuccess: '{{count}} nummers toegevoegd aan {{playlist}}',
|
||||||
addPartial: '{{added}} toegevoegd, {{skipped}} overgeslagen (duplicaten)',
|
addPartial: '{{added}} toegevoegd, {{skipped}} overgeslagen (duplicaten)',
|
||||||
addAllSkipped: 'Alles overgeslagen ({{count}} 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',
|
addError: 'Fout bij toevoegen van nummers',
|
||||||
createAndAddSuccess: 'Playlist "{{playlist}}" aangemaakt met {{count}} nummers',
|
createAndAddSuccess: 'Playlist "{{playlist}}" aangemaakt met {{count}} nummers',
|
||||||
createError: 'Fout bij aanmaken van playlist',
|
createError: 'Fout bij aanmaken van playlist',
|
||||||
|
|||||||
@@ -1381,6 +1381,10 @@ export const ruTranslation = {
|
|||||||
addSuccess: '{{count}} треков добавлено в {{playlist}}',
|
addSuccess: '{{count}} треков добавлено в {{playlist}}',
|
||||||
addPartial: '{{added}} добавлено, {{skipped}} пропущено (дубликаты)',
|
addPartial: '{{added}} добавлено, {{skipped}} пропущено (дубликаты)',
|
||||||
addAllSkipped: 'Все пропущены ({{count}} дубликатов)',
|
addAllSkipped: 'Все пропущены ({{count}} дубликатов)',
|
||||||
|
addedAsDuplicates: '{{count}} треков добавлено в {{playlist}} (как дубликаты)',
|
||||||
|
duplicateConfirmTitle: 'Уже в плейлисте',
|
||||||
|
duplicateConfirmMessage: 'Все {{count}} треков уже есть в «{{playlist}}». Всё равно добавить их как дубликаты?',
|
||||||
|
duplicateConfirmAction: 'Всё равно добавить',
|
||||||
addError: 'Ошибка при добавлении треков',
|
addError: 'Ошибка при добавлении треков',
|
||||||
createAndAddSuccess: 'Плейлист "{{playlist}}" создан с {{count}} треками',
|
createAndAddSuccess: 'Плейлист "{{playlist}}" создан с {{count}} треками',
|
||||||
createError: 'Ошибка при создании плейлиста',
|
createError: 'Ошибка при создании плейлиста',
|
||||||
|
|||||||
@@ -1291,6 +1291,10 @@ export const zhTranslation = {
|
|||||||
addSuccess: '{{count}} 首歌曲已添加到 {{playlist}}',
|
addSuccess: '{{count}} 首歌曲已添加到 {{playlist}}',
|
||||||
addPartial: '{{added}} 首已添加,{{skipped}} 首跳过(重复)',
|
addPartial: '{{added}} 首已添加,{{skipped}} 首跳过(重复)',
|
||||||
addAllSkipped: '全部跳过({{count}} 首重复)',
|
addAllSkipped: '全部跳过({{count}} 首重复)',
|
||||||
|
addedAsDuplicates: '{{count}} 首歌曲已添加到 {{playlist}}(作为重复项)',
|
||||||
|
duplicateConfirmTitle: '已在播放列表中',
|
||||||
|
duplicateConfirmMessage: '所有 {{count}} 首歌曲已在 "{{playlist}}" 中。仍要作为重复项添加吗?',
|
||||||
|
duplicateConfirmAction: '仍要添加',
|
||||||
addError: '添加歌曲时出错',
|
addError: '添加歌曲时出错',
|
||||||
createAndAddSuccess: '播放列表 "{{playlist}}" 已创建,包含 {{count}} 首歌曲',
|
createAndAddSuccess: '播放列表 "{{playlist}}" 已创建,包含 {{count}} 首歌曲',
|
||||||
createError: '创建播放列表时出错',
|
createError: '创建播放列表时出错',
|
||||||
|
|||||||
Reference in New Issue
Block a user