@@ -1064,7 +1083,7 @@ export default function ContextMenu() {
};
}, [pendingSubmenuKeyboardFocus, playlistSubmenuOpen, getMenuNavItems, focusMenuItemAt]);
- const { type, item, queueIndex } = contextMenu;
+ const { type, item, queueIndex, playlistId, playlistSongIndex } = contextMenu;
const isStarred = (id: string, itemStarred?: string) =>
id in starredOverrides ? starredOverrides[id] : !!itemStarred;
@@ -1485,6 +1504,26 @@ export default function ContextMenu() {
handleAction(() => openSongInfo(song.id))}>
{t('contextMenu.songInfo')}
+ {playlistId && playlistSongIndex !== undefined && (
+
handleAction(async () => {
+ const { getPlaylist, updatePlaylist } = await import('../api/subsonic');
+ const { usePlaylistStore } = await import('../store/playlistStore');
+ const { showToast } = await import('../utils/toast');
+ const touchPlaylist = usePlaylistStore.getState().touchPlaylist;
+ try {
+ const { songs } = await getPlaylist(playlistId);
+ const prevCount = songs.length;
+ const updatedIds = songs.filter((_, i) => i !== playlistSongIndex).map(s => s.id);
+ await updatePlaylist(playlistId, updatedIds, prevCount);
+ touchPlaylist(playlistId);
+ showToast(t('playlists.removeSuccess'), 3000, 'info');
+ } catch {
+ showToast(t('playlists.removeError'), 4000, 'error');
+ }
+ })}>
+ {t('contextMenu.removeFromPlaylist')}
+
+ )}
>
);
})()}
diff --git a/src/locales/de.ts b/src/locales/de.ts
index 3c539f4b..6ecf447a 100644
--- a/src/locales/de.ts
+++ b/src/locales/de.ts
@@ -106,6 +106,7 @@ export const deTranslation = {
unfavoriteArtist: 'Künstler aus Favoriten entfernen',
unfavoriteAlbum: 'Album aus Favoriten entfernen',
removeFromQueue: 'Diesen Song entfernen',
+ removeFromPlaylist: 'Aus Playlist entfernen',
openAlbum: 'Album öffnen',
goToArtist: 'Zum Künstler',
download: 'Herunterladen (ZIP)',
@@ -947,12 +948,15 @@ export const deTranslation = {
select: 'Auswählen',
startSelect: 'Auswahl aktivieren',
cancelSelect: 'Abbrechen',
+ loadingAlbums: '{{count}} Alben werden aufgelöst…',
loadingArtists: '{{count}} Künstler werden aufgelöst…',
myPlaylists: 'Meine Playlists',
noOtherPlaylists: 'Keine anderen Playlists verfügbar',
addToPlaylistSuccess: '{{count}} Songs zu {{playlist}} hinzugefügt',
addToPlaylistNoNew: 'Keine neuen Songs für {{playlist}}',
addToPlaylistError: 'Fehler beim Hinzufügen zur Playlist',
+ removeSuccess: 'Song aus Playlist entfernt',
+ removeError: 'Fehler beim Entfernen aus der Playlist',
},
mostPlayed: {
title: 'Meistgehört',
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 3153ddc0..e0d1d68d 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -107,6 +107,7 @@ export const enTranslation = {
unfavoriteArtist: 'Remove Artist from Favorites',
unfavoriteAlbum: 'Remove Album from Favorites',
removeFromQueue: 'Remove from Queue',
+ removeFromPlaylist: 'Remove from Playlist',
openAlbum: 'Open Album',
goToArtist: 'Go to Artist',
download: 'Download (ZIP)',
@@ -949,12 +950,15 @@ export const enTranslation = {
select: 'Select',
startSelect: 'Enable selection',
cancelSelect: 'Cancel',
+ loadingAlbums: 'Resolving {{count}} albums…',
loadingArtists: 'Resolving {{count}} artists…',
myPlaylists: 'My Playlists',
noOtherPlaylists: 'No other playlists available',
addToPlaylistSuccess: '{{count}} songs added to {{playlist}}',
addToPlaylistNoNew: 'No new songs to add to {{playlist}}',
addToPlaylistError: 'Error adding to playlist',
+ removeSuccess: 'Song removed from playlist',
+ removeError: 'Error removing song from playlist',
},
mostPlayed: {
title: 'Most Played',
diff --git a/src/locales/es.ts b/src/locales/es.ts
index cdba265f..e719e5b0 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -107,6 +107,7 @@ export const esTranslation = {
unfavoriteArtist: 'Quitar Artista de Favoritos',
unfavoriteAlbum: 'Quitar Álbum de Favoritos',
removeFromQueue: 'Quitar de la Cola',
+ removeFromPlaylist: 'Quitar de la Playlist',
openAlbum: 'Abrir Álbum',
goToArtist: 'Ir al Artista',
download: 'Descargar (ZIP)',
@@ -950,12 +951,15 @@ export const esTranslation = {
select: 'Seleccionar',
startSelect: 'Activar selección',
cancelSelect: 'Cancelar',
+ loadingAlbums: 'Resolviendo {{count}} álbumes…',
loadingArtists: 'Resolviendo {{count}} artistas…',
myPlaylists: 'Mis Listas',
noOtherPlaylists: 'No hay otras listas disponibles',
addToPlaylistSuccess: '{{count}} canciones agregadas a {{playlist}}',
addToPlaylistNoNew: 'No hay canciones nuevas para agregar a {{playlist}}',
addToPlaylistError: 'Error al agregar a la lista',
+ removeSuccess: 'Canción quitada de la playlist',
+ removeError: 'Error al quitar la canción de la playlist',
},
mostPlayed: {
title: 'Más Reproducidos',
diff --git a/src/locales/fr.ts b/src/locales/fr.ts
index b8e15c50..dcfac669 100644
--- a/src/locales/fr.ts
+++ b/src/locales/fr.ts
@@ -106,6 +106,7 @@ export const frTranslation = {
unfavoriteArtist: 'Retirer l\'artiste des favoris',
unfavoriteAlbum: 'Retirer l\'album des favoris',
removeFromQueue: 'Retirer de la file',
+ removeFromPlaylist: 'Retirer de la playlist',
openAlbum: 'Ouvrir l\'album',
goToArtist: 'Aller à l\'artiste',
download: 'Télécharger (ZIP)',
@@ -945,12 +946,15 @@ export const frTranslation = {
select: 'Sélectionner',
startSelect: 'Activer la sélection',
cancelSelect: 'Annuler',
+ loadingAlbums: 'Résolution de {{count}} albums…',
loadingArtists: 'Résolution de {{count}} artistes…',
myPlaylists: 'Mes Playlists',
noOtherPlaylists: 'Aucune autre playlist disponible',
addToPlaylistSuccess: '{{count}} morceaux ajoutés à {{playlist}}',
addToPlaylistNoNew: 'Aucun nouveau morceau pour {{playlist}}',
addToPlaylistError: 'Erreur lors de l\'ajout à la playlist',
+ removeSuccess: 'Morceau retiré de la playlist',
+ removeError: 'Erreur lors du retrait de la playlist',
},
mostPlayed: {
title: 'Les plus joués',
diff --git a/src/locales/nb.ts b/src/locales/nb.ts
index 2798fc3d..a76fad14 100644
--- a/src/locales/nb.ts
+++ b/src/locales/nb.ts
@@ -106,6 +106,7 @@ export const nbTranslation = {
unfavoriteArtist: 'Fjern artist fra favoritter',
unfavoriteAlbum: 'Fjern album fra favoritter',
removeFromQueue: 'Fjern fra kø',
+ removeFromPlaylist: 'Fjern fra spilleliste',
openAlbum: 'Åpne album',
goToArtist: 'Gå til artist',
download: 'Last ned (ZIP)',
@@ -944,12 +945,15 @@ export const nbTranslation = {
select: 'Velg',
startSelect: 'Aktiver valg',
cancelSelect: 'Avbryt',
+ loadingAlbums: 'Løser {{count}} album…',
loadingArtists: 'Løser {{count}} artister…',
myPlaylists: 'Mine spillelister',
noOtherPlaylists: 'Ingen andre spillelister tilgjengelig',
addToPlaylistSuccess: '{{count}} sanger lagt til i {{playlist}}',
addToPlaylistNoNew: 'Ingen nye sanger for {{playlist}}',
addToPlaylistError: 'Feil ved å legge til i spilleliste',
+ removeSuccess: 'Sang fjernet fra spilleliste',
+ removeError: 'Feil ved fjerning fra spilleliste',
},
mostPlayed: {
title: 'Mest spilt',
diff --git a/src/locales/nl.ts b/src/locales/nl.ts
index 16eada86..9c90db27 100644
--- a/src/locales/nl.ts
+++ b/src/locales/nl.ts
@@ -945,6 +945,7 @@ export const nlTranslation = {
select: 'Selecteren',
startSelect: 'Selectie inschakelen',
cancelSelect: 'Annuleren',
+ loadingAlbums: '{{count}} albums oplossen…',
loadingArtists: '{{count}} artiesten oplossen…',
myPlaylists: 'Mijn playlists',
noOtherPlaylists: 'Geen andere playlists beschikbaar',
diff --git a/src/locales/ru.ts b/src/locales/ru.ts
index ccb4482f..e1beca09 100644
--- a/src/locales/ru.ts
+++ b/src/locales/ru.ts
@@ -107,6 +107,7 @@ export const ruTranslation = {
unfavoriteArtist: 'Убрать исполнителя из избранного',
unfavoriteAlbum: 'Убрать альбом из избранного',
removeFromQueue: 'Убрать из очереди',
+ removeFromPlaylist: 'Убрать из плейлиста',
openAlbum: 'Открыть альбом',
goToArtist: 'К исполнителю',
download: 'Скачать (ZIP)',
@@ -1004,12 +1005,15 @@ export const ruTranslation = {
select: 'Выбрать',
startSelect: 'Включить выбор',
cancelSelect: 'Отмена',
+ loadingAlbums: 'Загрузка {{count}} альбомов…',
loadingArtists: 'Загрузка {{count}} исполнителей…',
myPlaylists: 'Мои плейлисты',
noOtherPlaylists: 'Нет других доступных плейлистов',
addToPlaylistSuccess: '{{count}} треков добавлено в {{playlist}}',
addToPlaylistNoNew: 'Нет новых треков для {{playlist}}',
addToPlaylistError: 'Ошибка при добавлении в плейлист',
+ removeSuccess: 'Трек убран из плейлиста',
+ removeError: 'Ошибка при удалении из плейлиста',
},
mostPlayed: {
title: 'Популярное',
diff --git a/src/locales/zh.ts b/src/locales/zh.ts
index 0aa80781..2c92d681 100644
--- a/src/locales/zh.ts
+++ b/src/locales/zh.ts
@@ -941,6 +941,7 @@ export const zhTranslation = {
select: '选择',
startSelect: '启用选择',
cancelSelect: '取消',
+ loadingAlbums: '正在解析 {{count}} 张专辑…',
loadingArtists: '正在解析 {{count}} 位艺术家…',
myPlaylists: '我的播放列表',
noOtherPlaylists: '没有其他可用播放列表',
diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx
index 827fb659..6d49e0e9 100644
--- a/src/pages/PlaylistDetail.tsx
+++ b/src/pages/PlaylistDetail.tsx
@@ -234,6 +234,8 @@ export default function PlaylistDetail() {
}, [contextMenuOpen]);
// ── Load ─────────────────────────────────────────────────────
+ const lastModified = usePlaylistStore(s => (id ? s.lastModified[id] : undefined));
+
useEffect(() => {
if (!id) return;
setLoading(true);
@@ -253,7 +255,7 @@ export default function PlaylistDetail() {
})
.catch(() => {})
.finally(() => setLoading(false));
- }, [id]);
+ }, [id, lastModified]);
// ── Suggestions ───────────────────────────────────────────────
const loadSuggestions = useCallback(async (currentSongs: SubsonicSong[]) => {
@@ -1019,7 +1021,7 @@ export default function PlaylistDetail() {
onContextMenu={e => {
e.preventDefault();
setContextMenuSongId(song.id);
- openContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song');
+ openContextMenu(e.clientX, e.clientY, songToTrack(song), 'album-song', undefined, id, realIdx);
}}
>
{visibleCols.map(colDef => {
diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts
index 0d41e3d3..fb1899a9 100644
--- a/src/store/playerStore.ts
+++ b/src/store/playerStore.ts
@@ -176,8 +176,10 @@ interface PlayerState {
item: any;
type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist' | null;
queueIndex?: number;
+ playlistId?: string;
+ playlistSongIndex?: number;
};
- openContextMenu: (x: number, y: number, item: any, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist', queueIndex?: number) => void;
+ openContextMenu: (x: number, y: number, item: any, type: 'song' | 'album' | 'artist' | 'queue-item' | 'album-song' | 'playlist' | 'multi-album' | 'multi-artist' | 'multi-playlist', queueIndex?: number, playlistId?: string, playlistSongIndex?: number) => void;
closeContextMenu: () => void;
songInfoModal: { isOpen: boolean; songId: string | null };
@@ -729,8 +731,8 @@ export const usePlayerStore = create
()(
repeatMode: 'off',
contextMenu: { isOpen: false, x: 0, y: 0, item: null, type: null },
- openContextMenu: (x, y, item, type, queueIndex) => set({
- contextMenu: { isOpen: true, x, y, item, type, queueIndex },
+ openContextMenu: (x, y, item, type, queueIndex, playlistId, playlistSongIndex) => set({
+ contextMenu: { isOpen: true, x, y, item, type, queueIndex, playlistId, playlistSongIndex },
}),
closeContextMenu: () => set(state => ({
contextMenu: { ...state.contextMenu, isOpen: false },
diff --git a/src/store/playlistStore.ts b/src/store/playlistStore.ts
index f772bf5c..cc6e36bf 100644
--- a/src/store/playlistStore.ts
+++ b/src/store/playlistStore.ts
@@ -6,6 +6,7 @@ interface PlaylistStore {
recentIds: string[];
playlists: SubsonicPlaylist[];
playlistsLoading: boolean;
+ lastModified: Record;
touchPlaylist: (id: string) => void;
removeId: (id: string) => void;
fetchPlaylists: () => Promise;
@@ -19,9 +20,11 @@ export const usePlaylistStore = create()(
recentIds: [],
playlists: [],
playlistsLoading: false,
+ lastModified: {},
touchPlaylist: (id) =>
set((s) => ({
recentIds: [id, ...s.recentIds.filter((x) => x !== id)].slice(0, 50),
+ lastModified: { ...s.lastModified, [id]: Date.now() },
})),
removeId: (id) =>
set((s) => ({ recentIds: s.recentIds.filter((x) => x !== id) })),