fixed bugs

This commit is contained in:
kveld9
2026-04-12 17:37:36 -03:00
parent bef6941a2b
commit 94323e91fa
14 changed files with 424 additions and 123 deletions
+36 -31
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2 } from 'lucide-react'; import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2 } from 'lucide-react';
import LastfmIcon from './LastfmIcon'; import LastfmIcon from './LastfmIcon';
import StarRating from './StarRating'; import StarRating from './StarRating';
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm'; import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
import { usePlayerStore, Track, songToTrack } from '../store/playerStore'; import { usePlayerStore, Track, songToTrack } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getSimilarSongs, getTopSongs, buildDownloadUrl, getAlbum, getArtist, getPlaylists, getPlaylist, createPlaylist, updatePlaylist, SubsonicPlaylist, setRating } from '../api/subsonic'; import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getSimilarSongs, getTopSongs, buildDownloadUrl, getAlbum, getArtist, getPlaylists, getPlaylist, updatePlaylist, SubsonicPlaylist, setRating } from '../api/subsonic';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useDownloadModalStore } from '../store/downloadModalStore'; import { useDownloadModalStore } from '../store/downloadModalStore';
@@ -40,28 +40,26 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
const { t } = useTranslation(); const { t } = useTranslation();
const subRef = useRef<HTMLDivElement>(null); const subRef = useRef<HTMLDivElement>(null);
const newNameRef = useRef<HTMLInputElement>(null); const newNameRef = useRef<HTMLInputElement>(null);
const [playlists, setPlaylists] = useState<SubsonicPlaylist[]>([]);
const [adding, setAdding] = useState<string | null>(null); const [adding, setAdding] = useState<string | null>(null);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState('');
const [flipLeft, setFlipLeft] = useState(false); const [flipLeft, setFlipLeft] = useState(false);
const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist); const storePlaylists = usePlaylistStore((s) => s.playlists);
const recentIds = usePlaylistStore((s) => s.recentIds); const recentIds = usePlaylistStore((s) => s.recentIds);
const createPlaylist = usePlaylistStore((s) => s.createPlaylist);
const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist);
useEffect(() => { // Sort playlists by recent usage
getPlaylists().then((all) => { const playlists = useMemo(() => {
const sorted = [...all].sort((a, b) => { return [...storePlaylists].sort((a, b) => {
const ai = recentIds.indexOf(a.id); const ai = recentIds.indexOf(a.id);
const bi = recentIds.indexOf(b.id); const bi = recentIds.indexOf(b.id);
if (ai === -1 && bi === -1) return a.name.localeCompare(b.name); if (ai === -1 && bi === -1) return a.name.localeCompare(b.name);
if (ai === -1) return 1; if (ai === -1) return 1;
if (bi === -1) return -1; if (bi === -1) return -1;
return ai - bi; return ai - bi;
}); });
setPlaylists(sorted); }, [storePlaylists, recentIds]);
}).catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Flip submenu left if it would overflow the right edge of the viewport // Flip submenu left if it would overflow the right edge of the viewport
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -92,10 +90,7 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
const handleCreate = async () => { const handleCreate = async () => {
const name = newName.trim() || t('playlists.unnamed'); const name = newName.trim() || t('playlists.unnamed');
try { await createPlaylist(name, songIds);
const pl = await createPlaylist(name, songIds);
if (pl?.id) touchPlaylist(pl.id);
} catch {}
onDone(); onDone();
}; };
@@ -139,7 +134,7 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
{playlists.length === 0 && ( {playlists.length === 0 && (
<div className="context-submenu-empty">{t('playlists.empty')}</div> <div className="context-submenu-empty">{t('playlists.empty')}</div>
)} )}
{playlists.map((pl) => ( {playlists.map((pl: SubsonicPlaylist) => (
<div <div
key={pl.id} key={pl.id}
className="context-menu-item" className="context-menu-item"
@@ -990,6 +985,10 @@ export default function ContextMenu() {
previousFocusRef.current = document.activeElement as HTMLElement | null; previousFocusRef.current = document.activeElement as HTMLElement | null;
return; return;
} }
// Clean up any keyboard focus styling when menu closes
menuRef.current
?.querySelectorAll<HTMLElement>('.context-menu-keyboard-active')
.forEach(el => el.classList.remove('context-menu-keyboard-active'));
const prev = previousFocusRef.current; const prev = previousFocusRef.current;
previousFocusRef.current = null; previousFocusRef.current = null;
if (prev?.isConnected) { if (prev?.isConnected) {
@@ -1572,12 +1571,15 @@ export default function ContextMenu() {
const { showToast } = await import('../utils/toast'); const { showToast } = await import('../utils/toast');
const { deletePlaylist } = await import('../api/subsonic'); const { deletePlaylist } = await import('../api/subsonic');
const { usePlaylistStore } = await import('../store/playlistStore'); const { usePlaylistStore } = await import('../store/playlistStore');
const removeId = usePlaylistStore.getState().removeId; const { removeId } = usePlaylistStore.getState();
try { try {
await deletePlaylist(playlist.id); await deletePlaylist(playlist.id);
removeId(playlist.id); removeId(playlist.id);
// Update local playlist state without page reload to preserve audio playback state
usePlaylistStore.setState((s) => ({
playlists: s.playlists.filter((p) => p.id !== playlist.id),
}));
showToast(t('playlists.deleteSuccess', { count: 1 }), 3000, 'info'); showToast(t('playlists.deleteSuccess', { count: 1 }), 3000, 'info');
window.location.reload();
} catch { } catch {
showToast(t('playlists.deleteFailed', { name: playlist.name }), 3000, 'error'); showToast(t('playlists.deleteFailed', { name: playlist.name }), 3000, 'error');
} }
@@ -1713,21 +1715,24 @@ export default function ContextMenu() {
const { showToast } = await import('../utils/toast'); const { showToast } = await import('../utils/toast');
const { usePlaylistStore } = await import('../store/playlistStore'); const { usePlaylistStore } = await import('../store/playlistStore');
const { deletePlaylist } = await import('../api/subsonic'); const { deletePlaylist } = await import('../api/subsonic');
const removeId = usePlaylistStore.getState().removeId; const { removeId } = usePlaylistStore.getState();
let deleted = 0; const deletedIds: string[] = [];
for (const pl of selectedPlaylists) { for (const pl of selectedPlaylists) {
try { try {
await deletePlaylist(pl.id); await deletePlaylist(pl.id);
removeId(pl.id); removeId(pl.id);
deleted++; deletedIds.push(pl.id);
} catch { } catch {
showToast(t('playlists.deleteFailed', { name: pl.name }), 3000, 'error'); showToast(t('playlists.deleteFailed', { name: pl.name }), 3000, 'error');
} }
} }
if (deleted > 0) { if (deletedIds.length > 0) {
showToast(t('playlists.deleteSuccess', { count: deleted }), 3000, 'info'); // Update local playlist state without page reload to preserve audio playback state
usePlaylistStore.setState((s) => ({
playlists: s.playlists.filter((p) => !deletedIds.includes(p.id)),
}));
showToast(t('playlists.deleteSuccess', { count: deletedIds.length }), 3000, 'info');
} }
window.location.reload();
})}> })}>
<Trash2 size={14} /> {t('playlists.deleteSelected')} <Trash2 size={14} /> {t('playlists.deleteSelected')}
</div> </div>
+5 -5
View File
@@ -1,7 +1,8 @@
import React, { useState, useRef, useMemo } from 'react'; import React, { useState, useRef, useMemo } from 'react';
import { Track, usePlayerStore, songToTrack } from '../store/playerStore'; import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio } from 'lucide-react'; import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, ArrowUpToLine, Radio } from 'lucide-react';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, createPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic'; import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
import { usePlaylistStore } from '../store/playlistStore';
import { useCachedUrl } from './CachedImage'; import { useCachedUrl } from './CachedImage';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -655,10 +656,9 @@ export default function QueuePanel() {
onClose={() => setSaveModalOpen(false)} onClose={() => setSaveModalOpen(false)}
onSave={async (name) => { onSave={async (name) => {
try { try {
await createPlaylist(name, queue.map(t => t.id)); const createPlaylist = usePlaylistStore.getState().createPlaylist;
const playlists = await getPlaylists(); const pl = await createPlaylist(name, queue.map(t => t.id));
const created = playlists.find(p => p.name === name); if (pl) setActivePlaylist({ id: pl.id, name: pl.name });
if (created) setActivePlaylist({ id: created.id, name: created.name });
setSaveModalOpen(false); setSaveModalOpen(false);
} catch (e) { } catch (e) {
console.error('Failed to save playlist', e); console.error('Failed to save playlist', e);
+13 -11
View File
@@ -1,4 +1,4 @@
import React, { useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react'; import React, { useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import { useOfflineStore } from '../store/offlineStore'; import { useOfflineStore } from '../store/offlineStore';
@@ -14,7 +14,8 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import PsysonicLogo from './PsysonicLogo'; import PsysonicLogo from './PsysonicLogo';
import PSmallLogo from './PSmallLogo'; import PSmallLogo from './PSmallLogo';
import { getPlaylists, SubsonicPlaylist } from '../api/subsonic'; import { getPlaylists } from '../api/subsonic';
import { usePlaylistStore } from '../store/playlistStore';
// All configurable nav items — order and visibility controlled by sidebarStore. // All configurable nav items — order and visibility controlled by sidebarStore.
// Exported so Settings can render the same item metadata. // Exported so Settings can render the same item metadata.
@@ -58,8 +59,13 @@ export default function Sidebar({
const sidebarItems = useSidebarStore(s => s.items); const sidebarItems = useSidebarStore(s => s.items);
const [libraryDropdownOpen, setLibraryDropdownOpen] = useState(false); const [libraryDropdownOpen, setLibraryDropdownOpen] = useState(false);
const [playlistsExpanded, setPlaylistsExpanded] = useState(false); const [playlistsExpanded, setPlaylistsExpanded] = useState(false);
const [playlists, setPlaylists] = useState<SubsonicPlaylist[]>([]); const playlistsRaw = usePlaylistStore(s => s.playlists);
const [playlistsLoading, setPlaylistsLoading] = useState(false); const playlistsLoading = usePlaylistStore(s => s.playlistsLoading);
const fetchPlaylists = usePlaylistStore(s => s.fetchPlaylists);
// Sort playlists alphabetically by name
const playlists = useMemo(() => {
return [...playlistsRaw].sort((a, b) => a.name.localeCompare(b.name));
}, [playlistsRaw]);
const [dropdownRect, setDropdownRect] = useState({ top: 0, left: 0, width: 0 }); const [dropdownRect, setDropdownRect] = useState({ top: 0, left: 0, width: 0 });
const libraryTriggerRef = useRef<HTMLButtonElement>(null); const libraryTriggerRef = useRef<HTMLButtonElement>(null);
const showLibraryPicker = !isCollapsed && isLoggedIn && musicFolders.length > 1; const showLibraryPicker = !isCollapsed && isLoggedIn && musicFolders.length > 1;
@@ -120,12 +126,8 @@ export default function Sidebar({
// Fetch playlists when expanded // Fetch playlists when expanded
useEffect(() => { useEffect(() => {
if (!playlistsExpanded || !isLoggedIn) return; if (!playlistsExpanded || !isLoggedIn) return;
setPlaylistsLoading(true); fetchPlaylists();
getPlaylists() }, [playlistsExpanded, isLoggedIn, fetchPlaylists]);
.then(setPlaylists)
.catch(() => {})
.finally(() => setPlaylistsLoading(false));
}, [playlistsExpanded, isLoggedIn]);
// Resolve ordered, visible items per section from store config // Resolve ordered, visible items per section from store config
const visibleLibrary = sidebarItems const visibleLibrary = sidebarItems
@@ -264,7 +266,7 @@ export default function Sidebar({
) : playlists.length === 0 ? ( ) : playlists.length === 0 ? (
<div className="sidebar-playlists-empty">{t('playlists.empty')}</div> <div className="sidebar-playlists-empty">{t('playlists.empty')}</div>
) : ( ) : (
playlists.map(pl => ( playlists.map((pl: { id: string; name: string }) => (
<NavLink <NavLink
key={pl.id} key={pl.id}
to={`/playlists/${pl.id}`} to={`/playlists/${pl.id}`}
+32
View File
@@ -110,6 +110,9 @@ export const deTranslation = {
goToArtist: 'Zum Künstler', goToArtist: 'Zum Künstler',
download: 'Herunterladen (ZIP)', download: 'Herunterladen (ZIP)',
addToPlaylist: 'Zur Playlist hinzufügen', addToPlaylist: 'Zur Playlist hinzufügen',
selectedPlaylists: '{{count}} Playlists ausgewählt',
selectedAlbums: '{{count}} Alben ausgewählt',
selectedArtists: '{{count}} Künstler ausgewählt',
songInfo: 'Song-Infos', songInfo: 'Song-Infos',
}, },
albumDetail: { albumDetail: {
@@ -296,6 +299,11 @@ export const deTranslation = {
notFound: 'Keine Künstler gefunden.', notFound: 'Keine Künstler gefunden.',
albumCount_one: '{{count}} Album', albumCount_one: '{{count}} Album',
albumCount_other: '{{count}} Alben', albumCount_other: '{{count}} Alben',
selectionCount: '{{count}} ausgewählt',
select: 'Mehrfachauswahl',
startSelect: 'Mehrfachauswahl aktivieren',
cancelSelect: 'Abbrechen',
addToPlaylist: 'Zur Playlist hinzufügen',
}, },
login: { login: {
subtitle: 'Dein Navidrome Desktop Player', subtitle: 'Dein Navidrome Desktop Player',
@@ -921,6 +929,30 @@ export const deTranslation = {
coverUpdated: 'Cover aktualisiert', coverUpdated: 'Cover aktualisiert',
metaSaved: 'Playlist aktualisiert', metaSaved: 'Playlist aktualisiert',
downloadZip: 'Herunterladen (ZIP)', downloadZip: 'Herunterladen (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} Songs zu {{playlist}} hinzugefügt',
addPartial: '{{added}} hinzugefügt, {{skipped}} übersprungen (Duplikate)',
addAllSkipped: 'Alle übersprungen ({{count}} Duplikate)',
addError: 'Fehler beim Hinzufügen von Songs',
createAndAddSuccess: 'Playlist "{{playlist}}" mit {{count}} Songs erstellt',
createError: 'Fehler beim Erstellen der Playlist',
deleteSuccess: '{{count}} Playlists gelöscht',
deleteFailed: 'Fehler beim Löschen von {{name}}',
deleteSelected: 'Ausgewählte löschen',
mergeSuccess: '{{count}} Songs in {{playlist}} zusammengeführt',
mergeNoNewSongs: 'Keine neuen Songs zum Hinzufügen',
mergeError: 'Fehler beim Zusammenführen von Playlists',
mergeInto: 'Zusammenführen in',
selectionCount: '{{count}} ausgewählt',
select: 'Auswählen',
startSelect: 'Auswahl aktivieren',
cancelSelect: 'Abbrechen',
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',
}, },
mostPlayed: { mostPlayed: {
title: 'Meistgehört', title: 'Meistgehört',
+32
View File
@@ -111,6 +111,9 @@ export const enTranslation = {
goToArtist: 'Go to Artist', goToArtist: 'Go to Artist',
download: 'Download (ZIP)', download: 'Download (ZIP)',
addToPlaylist: 'Add to Playlist', addToPlaylist: 'Add to Playlist',
selectedPlaylists: '{{count}} playlists selected',
selectedAlbums: '{{count}} albums selected',
selectedArtists: '{{count}} artists selected',
songInfo: 'Song Info', songInfo: 'Song Info',
}, },
albumDetail: { albumDetail: {
@@ -297,6 +300,11 @@ export const enTranslation = {
notFound: 'No artists found.', notFound: 'No artists found.',
albumCount_one: '{{count}} Album', albumCount_one: '{{count}} Album',
albumCount_other: '{{count}} Albums', albumCount_other: '{{count}} Albums',
selectionCount: '{{count}} selected',
select: 'Multi-select',
startSelect: 'Enable multi-select',
cancelSelect: 'Cancel',
addToPlaylist: 'Add to Playlist',
}, },
login: { login: {
subtitle: 'Your Navidrome Desktop Player', subtitle: 'Your Navidrome Desktop Player',
@@ -923,6 +931,30 @@ export const enTranslation = {
coverUpdated: 'Cover updated', coverUpdated: 'Cover updated',
metaSaved: 'Playlist updated', metaSaved: 'Playlist updated',
downloadZip: 'Download (ZIP)', downloadZip: 'Download (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} songs added to {{playlist}}',
addPartial: '{{added}} added, {{skipped}} skipped (duplicates)',
addAllSkipped: 'All skipped ({{count}} duplicates)',
addError: 'Error adding songs',
createAndAddSuccess: 'Playlist "{{playlist}}" created with {{count}} songs',
createError: 'Error creating playlist',
deleteSuccess: '{{count}} playlists deleted',
deleteFailed: 'Error deleting {{name}}',
deleteSelected: 'Delete selected',
mergeSuccess: '{{count}} songs merged into {{playlist}}',
mergeNoNewSongs: 'No new songs to add',
mergeError: 'Error merging playlists',
mergeInto: 'Merge into',
selectionCount: '{{count}} selected',
select: 'Select',
startSelect: 'Enable selection',
cancelSelect: 'Cancel',
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',
}, },
mostPlayed: { mostPlayed: {
title: 'Most Played', title: 'Most Played',
+17 -3
View File
@@ -7,6 +7,7 @@ export const esTranslation = {
randomAlbums: 'Álbumes Aleatorios', randomAlbums: 'Álbumes Aleatorios',
artists: 'Artistas', artists: 'Artistas',
randomMix: 'Mezcla Aleatoria', randomMix: 'Mezcla Aleatoria',
randomPicker: 'Crear Mezcla',
favorites: 'Favoritos', favorites: 'Favoritos',
nowPlaying: 'Reproduciendo Ahora', nowPlaying: 'Reproduciendo Ahora',
system: 'Sistema', system: 'Sistema',
@@ -207,6 +208,13 @@ export const esTranslation = {
removeSong: 'Quitar de favoritos', removeSong: 'Quitar de favoritos',
stations: 'Estaciones de Radio', stations: 'Estaciones de Radio',
}, },
randomLanding: {
title: 'Crear Mezcla',
mixByTracks: 'Mezcla por Canciones',
mixByTracksDesc: 'Selección aleatoria de canciones de toda tu biblioteca',
mixByAlbums: 'Mezcla por Álbumes',
mixByAlbumsDesc: 'Álbumes aleatorios para tu próximo descubrimiento',
},
randomAlbums: { randomAlbums: {
title: 'Álbumes Aleatorios', title: 'Álbumes Aleatorios',
refresh: 'Refrescar', refresh: 'Refrescar',
@@ -279,7 +287,7 @@ export const esTranslation = {
downloadZipFailed: 'Error al descargar {{name}}', downloadZipFailed: 'Error al descargar {{name}}',
offlineQueuing: 'Encolando {{count}} álbum(es) para offline…', offlineQueuing: 'Encolando {{count}} álbum(es) para offline…',
offlineFailed: 'Error al agregar {{name}} offline', offlineFailed: 'Error al agregar {{name}} offline',
addToPlaylist: 'Agregar a Lista', addToPlaylist: 'Agregar a Lista de Reproducción',
}, },
artists: { artists: {
title: 'Artistas', title: 'Artistas',
@@ -294,8 +302,8 @@ export const esTranslation = {
albumCount_one: '{{count}} Álbum', albumCount_one: '{{count}} Álbum',
albumCount_other: '{{count}} Álbumes', albumCount_other: '{{count}} Álbumes',
selectionCount: '{{count}} seleccionados', selectionCount: '{{count}} seleccionados',
select: 'Seleccionar', select: 'Selección múltiple',
startSelect: 'Activar selección', startSelect: 'Activar selección múltiple',
cancelSelect: 'Cancelar', cancelSelect: 'Cancelar',
addToPlaylist: 'Agregar a Lista', addToPlaylist: 'Agregar a Lista',
}, },
@@ -589,6 +597,7 @@ export const esTranslation = {
shortcutSeekForward: 'Avanzar 10s', shortcutSeekForward: 'Avanzar 10s',
shortcutSeekBackward: 'Retroceder 10s', shortcutSeekBackward: 'Retroceder 10s',
shortcutToggleQueue: 'Alternar cola', shortcutToggleQueue: 'Alternar cola',
shortcutOpenFolderBrowser: 'Abrir {{folderBrowser}}',
shortcutFullscreenPlayer: 'Reproductor pantalla completa', shortcutFullscreenPlayer: 'Reproductor pantalla completa',
shortcutNativeFullscreen: 'Pantalla completa nativa', shortcutNativeFullscreen: 'Pantalla completa nativa',
playbackTitle: 'Reproducción', playbackTitle: 'Reproducción',
@@ -618,6 +627,10 @@ export const esTranslation = {
infiniteQueue: 'Cola Infinita', infiniteQueue: 'Cola Infinita',
infiniteQueueDesc: 'Agregar automáticamente pistas aleatorias cuando la cola termina', infiniteQueueDesc: 'Agregar automáticamente pistas aleatorias cuando la cola termina',
experimental: 'Experimental', experimental: 'Experimental',
fsPlayerSection: 'Reproductor Pantalla Completa',
fsShowArtistPortrait: 'Mostrar foto del artista',
fsShowArtistPortraitDesc: 'Muestra la foto del artista (o portada del álbum) en el lado derecho del reproductor pantalla completa.',
fsPortraitDim: 'Oscurecimiento de foto',
seekbarStyle: 'Estilo de Barra de Progreso', seekbarStyle: 'Estilo de Barra de Progreso',
seekbarStyleDesc: 'Elige la apariencia de la barra de progreso del reproductor', seekbarStyleDesc: 'Elige la apariencia de la barra de progreso del reproductor',
seekbarWaveform: 'Forma de Onda', seekbarWaveform: 'Forma de Onda',
@@ -937,6 +950,7 @@ export const esTranslation = {
select: 'Seleccionar', select: 'Seleccionar',
startSelect: 'Activar selección', startSelect: 'Activar selección',
cancelSelect: 'Cancelar', cancelSelect: 'Cancelar',
loadingArtists: 'Resolviendo {{count}} artistas…',
myPlaylists: 'Mis Listas', myPlaylists: 'Mis Listas',
noOtherPlaylists: 'No hay otras listas disponibles', noOtherPlaylists: 'No hay otras listas disponibles',
addToPlaylistSuccess: '{{count}} canciones agregadas a {{playlist}}', addToPlaylistSuccess: '{{count}} canciones agregadas a {{playlist}}',
+46 -11
View File
@@ -110,6 +110,9 @@ export const frTranslation = {
goToArtist: 'Aller à l\'artiste', goToArtist: 'Aller à l\'artiste',
download: 'Télécharger (ZIP)', download: 'Télécharger (ZIP)',
addToPlaylist: 'Ajouter à la playlist', addToPlaylist: 'Ajouter à la playlist',
selectedPlaylists: '{{count}} playlists sélectionnées',
selectedAlbums: '{{count}} albums sélectionnés',
selectedArtists: '{{count}} artistes sélectionnés',
songInfo: 'Infos du morceau', songInfo: 'Infos du morceau',
}, },
albumDetail: { albumDetail: {
@@ -272,17 +275,17 @@ export const frTranslation = {
yearTo: 'À', yearTo: 'À',
yearFilterClear: 'Effacer le filtre année', yearFilterClear: 'Effacer le filtre année',
yearFilterLabel: 'Année', yearFilterLabel: 'Année',
select: 'Multi-select', select: 'Sélection multiple',
startSelect: 'Enable multi-select', startSelect: 'Activer la sélection multiple',
cancelSelect: 'Cancel', cancelSelect: 'Annuler',
selectionCount: '{{count}} selected', selectionCount: '{{count}} sélectionnés',
downloadZips: 'Download ZIPs', downloadZips: 'Télécharger les ZIPs',
addOffline: 'Add Offline', addOffline: 'Ajouter hors ligne',
downloadingZip: 'Downloading {{current}}/{{total}}: {{name}}', downloadingZip: 'Téléchargement {{current}}/{{total}} : {{name}}',
downloadZipDone: '{{count}} ZIP(s) downloaded', downloadZipDone: '{{count}} ZIP(s) téléchargé(s)',
downloadZipFailed: 'Failed to download {{name}}', downloadZipFailed: 'Échec du téléchargement de {{name}}',
offlineQueuing: 'Queuing {{count}} album(s) for offline…', offlineQueuing: 'Mise en file d\'attente de {{count}} album(s) hors ligne…',
offlineFailed: 'Failed to add {{name}} offline', offlineFailed: 'Échec de l\'ajout de {{name}} hors ligne',
}, },
artists: { artists: {
title: 'Artistes', title: 'Artistes',
@@ -296,6 +299,11 @@ export const frTranslation = {
notFound: 'Aucun artiste trouvé.', notFound: 'Aucun artiste trouvé.',
albumCount_one: '{{count}} album', albumCount_one: '{{count}} album',
albumCount_other: '{{count}} albums', albumCount_other: '{{count}} albums',
selectionCount: '{{count}} sélectionnés',
select: 'Sélection multiple',
startSelect: 'Activer la sélection multiple',
cancelSelect: 'Annuler',
addToPlaylist: 'Ajouter à la playlist',
}, },
login: { login: {
subtitle: 'Votre lecteur de bureau Navidrome', subtitle: 'Votre lecteur de bureau Navidrome',
@@ -919,6 +927,30 @@ export const frTranslation = {
coverUpdated: 'Pochette mise à jour', coverUpdated: 'Pochette mise à jour',
metaSaved: 'Playlist mise à jour', metaSaved: 'Playlist mise à jour',
downloadZip: 'Télécharger (ZIP)', downloadZip: 'Télécharger (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} morceaux ajoutés à {{playlist}}',
addPartial: '{{added}} ajoutés, {{skipped}} ignorés (doublons)',
addAllSkipped: 'Tous ignorés ({{count}} doublons)',
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',
deleteSuccess: '{{count}} playlists supprimées',
deleteFailed: 'Erreur lors de la suppression de {{name}}',
deleteSelected: 'Supprimer la sélection',
mergeSuccess: '{{count}} morceaux fusionnés dans {{playlist}}',
mergeNoNewSongs: 'Aucun nouveau morceau à ajouter',
mergeError: 'Erreur lors de la fusion des playlists',
mergeInto: 'Fusionner dans',
selectionCount: '{{count}} sélectionnés',
select: 'Sélectionner',
startSelect: 'Activer la sélection',
cancelSelect: 'Annuler',
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',
}, },
mostPlayed: { mostPlayed: {
title: 'Les plus joués', title: 'Les plus joués',
@@ -929,6 +961,9 @@ export const frTranslation = {
sortLeast: 'Moins joués en premier', sortLeast: 'Moins joués en premier',
loadMore: 'Charger plus d\'albums', loadMore: 'Charger plus d\'albums',
noData: 'Aucune donnée d\'écoute. Commencez à écouter !', noData: 'Aucune donnée d\'écoute. Commencez à écouter !',
noArtists: 'Tous les artistes filtrés.',
filterCompilations: 'Masquer les artistes de compilations (Various Artists, Soundtracks, etc.)',
filterCompilationsShort: 'Masquer les compilations',
}, },
radio: { radio: {
title: 'Radio Internet', title: 'Radio Internet',
+46 -11
View File
@@ -110,6 +110,9 @@ export const nbTranslation = {
goToArtist: 'Gå til artist', goToArtist: 'Gå til artist',
download: 'Last ned (ZIP)', download: 'Last ned (ZIP)',
addToPlaylist: 'Legg til i spilleliste', addToPlaylist: 'Legg til i spilleliste',
selectedPlaylists: '{{count}} spillelister valgt',
selectedAlbums: '{{count}} album valgt',
selectedArtists: '{{count}} artister valgt',
songInfo: 'Sanginfo', songInfo: 'Sanginfo',
}, },
albumDetail: { albumDetail: {
@@ -272,17 +275,17 @@ export const nbTranslation = {
yearTo: 'Til', yearTo: 'Til',
yearFilterClear: 'Tøm år filteret', yearFilterClear: 'Tøm år filteret',
yearFilterLabel: 'År', yearFilterLabel: 'År',
select: 'Multi-select', select: 'Multivalg',
startSelect: 'Enable multi-select', startSelect: 'Aktiver multivalg',
cancelSelect: 'Cancel', cancelSelect: 'Avbryt',
selectionCount: '{{count}} selected', selectionCount: '{{count}} valgt',
downloadZips: 'Download ZIPs', downloadZips: 'Last ned ZIPs',
addOffline: 'Add Offline', addOffline: 'Legg til offline',
downloadingZip: 'Downloading {{current}}/{{total}}: {{name}}', downloadingZip: 'Laster ned {{current}}/{{total}}: {{name}}',
downloadZipDone: '{{count}} ZIP(s) downloaded', downloadZipDone: '{{count}} ZIP(s) lastet ned',
downloadZipFailed: 'Failed to download {{name}}', downloadZipFailed: 'Kunne ikke laste ned {{name}}',
offlineQueuing: 'Queuing {{count}} album(s) for offline…', offlineQueuing: 'Legger {{count}} album i kø for offline…',
offlineFailed: 'Failed to add {{name}} offline', offlineFailed: 'Kunne ikke legge til {{name}} offline',
}, },
artists: { artists: {
title: 'Artister', title: 'Artister',
@@ -296,6 +299,11 @@ export const nbTranslation = {
notFound: 'Ingen artister funnet.', notFound: 'Ingen artister funnet.',
albumCount_one: '{{count}} album', albumCount_one: '{{count}} album',
albumCount_other: '{{count}} album', albumCount_other: '{{count}} album',
selectionCount: '{{count}} valgt',
select: 'Multivalg',
startSelect: 'Aktiver multivalg',
cancelSelect: 'Avbryt',
addToPlaylist: 'Legg til i spilleliste',
}, },
login: { login: {
subtitle: 'Din Navidrome-mediaspiller', subtitle: 'Din Navidrome-mediaspiller',
@@ -918,6 +926,30 @@ export const nbTranslation = {
coverUpdated: 'Omslag oppdatert', coverUpdated: 'Omslag oppdatert',
metaSaved: 'Spillelisten er oppdatert', metaSaved: 'Spillelisten er oppdatert',
downloadZip: 'Last ned (ZIP)', downloadZip: 'Last ned (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} sanger lagt til i {{playlist}}',
addPartial: '{{added}} lagt til, {{skipped}} hoppet over (duplikater)',
addAllSkipped: 'Alle hoppet over ({{count}} duplikater)',
addError: 'Feil ved å legge til sanger',
createAndAddSuccess: 'Spilleliste "{{playlist}}" opprettet med {{count}} sanger',
createError: 'Feil ved oppretting av spilleliste',
deleteSuccess: '{{count}} spillelister slettet',
deleteFailed: 'Feil ved sletting av {{name}}',
deleteSelected: 'Slett valgte',
mergeSuccess: '{{count}} sanger slått sammen i {{playlist}}',
mergeNoNewSongs: 'Ingen nye sanger å legge til',
mergeError: 'Feil ved sammenslåing av spillelister',
mergeInto: 'Slå sammen i',
selectionCount: '{{count}} valgt',
select: 'Velg',
startSelect: 'Aktiver valg',
cancelSelect: 'Avbryt',
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',
}, },
mostPlayed: { mostPlayed: {
title: 'Mest spilt', title: 'Mest spilt',
@@ -928,6 +960,9 @@ export const nbTranslation = {
sortLeast: 'Minst spilt først', sortLeast: 'Minst spilt først',
loadMore: 'Last inn flere album', loadMore: 'Last inn flere album',
noData: 'Ingen avspillingsdata ennå. Begynn å høre!', noData: 'Ingen avspillingsdata ennå. Begynn å høre!',
noArtists: 'Alle artister filtrert bort.',
filterCompilations: 'Skjul kompilasjonsartister (Various Artists, Soundtracks, etc.)',
filterCompilationsShort: 'Skjul kompilasjoner',
}, },
radio: { radio: {
title: 'Internettradio', title: 'Internettradio',
+46 -11
View File
@@ -110,6 +110,9 @@ export const nlTranslation = {
goToArtist: 'Naar artiest', goToArtist: 'Naar artiest',
download: 'Downloaden (ZIP)', download: 'Downloaden (ZIP)',
addToPlaylist: 'Toevoegen aan playlist', addToPlaylist: 'Toevoegen aan playlist',
selectedPlaylists: '{{count}} playlists geselecteerd',
selectedAlbums: '{{count}} albums geselecteerd',
selectedArtists: '{{count}} artiesten geselecteerd',
songInfo: 'Nummerinfo', songInfo: 'Nummerinfo',
}, },
albumDetail: { albumDetail: {
@@ -272,17 +275,17 @@ export const nlTranslation = {
yearTo: 'Tot', yearTo: 'Tot',
yearFilterClear: 'Jaarfilter wissen', yearFilterClear: 'Jaarfilter wissen',
yearFilterLabel: 'Jaar', yearFilterLabel: 'Jaar',
select: 'Multi-select', select: 'Meervoudige selectie',
startSelect: 'Enable multi-select', startSelect: 'Meervoudige selectie inschakelen',
cancelSelect: 'Cancel', cancelSelect: 'Annuleren',
selectionCount: '{{count}} selected', selectionCount: '{{count}} geselecteerd',
downloadZips: 'Download ZIPs', downloadZips: 'ZIPs downloaden',
addOffline: 'Add Offline', addOffline: 'Offline toevoegen',
downloadingZip: 'Downloading {{current}}/{{total}}: {{name}}', downloadingZip: 'Downloaden {{current}}/{{total}}: {{name}}',
downloadZipDone: '{{count}} ZIP(s) downloaded', downloadZipDone: '{{count}} ZIP(s) gedownload',
downloadZipFailed: 'Failed to download {{name}}', downloadZipFailed: 'Downloaden van {{name}} mislukt',
offlineQueuing: 'Queuing {{count}} album(s) for offline…', offlineQueuing: '{{count}} album(s) in wachtrij voor offline…',
offlineFailed: 'Failed to add {{name}} offline', offlineFailed: 'Toevoegen van {{name}} offline mislukt',
}, },
artists: { artists: {
title: 'Artiesten', title: 'Artiesten',
@@ -296,6 +299,11 @@ export const nlTranslation = {
notFound: 'Geen artiesten gevonden.', notFound: 'Geen artiesten gevonden.',
albumCount_one: '{{count}} album', albumCount_one: '{{count}} album',
albumCount_other: '{{count}} albums', albumCount_other: '{{count}} albums',
selectionCount: '{{count}} geselecteerd',
select: 'Meervoudige selectie',
startSelect: 'Meervoudige selectie inschakelen',
cancelSelect: 'Annuleren',
addToPlaylist: 'Toevoegen aan playlist',
}, },
login: { login: {
subtitle: 'Jouw Navidrome-desktopspeler', subtitle: 'Jouw Navidrome-desktopspeler',
@@ -919,6 +927,30 @@ export const nlTranslation = {
coverUpdated: 'Omslag bijgewerkt', coverUpdated: 'Omslag bijgewerkt',
metaSaved: 'Playlist bijgewerkt', metaSaved: 'Playlist bijgewerkt',
downloadZip: 'Downloaden (ZIP)', downloadZip: 'Downloaden (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} nummers toegevoegd aan {{playlist}}',
addPartial: '{{added}} toegevoegd, {{skipped}} overgeslagen (duplicaten)',
addAllSkipped: 'Alles overgeslagen ({{count}} duplicaten)',
addError: 'Fout bij toevoegen van nummers',
createAndAddSuccess: 'Playlist "{{playlist}}" aangemaakt met {{count}} nummers',
createError: 'Fout bij aanmaken van playlist',
deleteSuccess: '{{count}} playlists verwijderd',
deleteFailed: 'Fout bij verwijderen van {{name}}',
deleteSelected: 'Geselecteerde verwijderen',
mergeSuccess: '{{count}} nummers samengevoegd in {{playlist}}',
mergeNoNewSongs: 'Geen nieuwe nummers om toe te voegen',
mergeError: 'Fout bij samenvoegen van playlists',
mergeInto: 'Samenvoegen in',
selectionCount: '{{count}} geselecteerd',
select: 'Selecteren',
startSelect: 'Selectie inschakelen',
cancelSelect: 'Annuleren',
loadingArtists: '{{count}} artiesten oplossen…',
myPlaylists: 'Mijn playlists',
noOtherPlaylists: 'Geen andere playlists beschikbaar',
addToPlaylistSuccess: '{{count}} nummers toegevoegd aan {{playlist}}',
addToPlaylistNoNew: 'Geen nieuwe nummers voor {{playlist}}',
addToPlaylistError: 'Fout bij toevoegen aan playlist',
}, },
mostPlayed: { mostPlayed: {
title: 'Meest gespeeld', title: 'Meest gespeeld',
@@ -929,6 +961,9 @@ export const nlTranslation = {
sortLeast: 'Minst gespeeld eerst', sortLeast: 'Minst gespeeld eerst',
loadMore: 'Meer albums laden', loadMore: 'Meer albums laden',
noData: 'Nog geen afspeelgegevens. Begin met luisteren!', noData: 'Nog geen afspeelgegevens. Begin met luisteren!',
noArtists: 'Alle artiesten gefilterd.',
filterCompilations: 'Compilatie-artiesten verbergen (Various Artists, Soundtracks, etc.)',
filterCompilationsShort: 'Compilaties verbergen',
}, },
radio: { radio: {
title: 'Internetradio', title: 'Internetradio',
+47 -12
View File
@@ -111,6 +111,9 @@ export const ruTranslation = {
goToArtist: 'К исполнителю', goToArtist: 'К исполнителю',
download: 'Скачать (ZIP)', download: 'Скачать (ZIP)',
addToPlaylist: 'В плейлист', addToPlaylist: 'В плейлист',
selectedPlaylists: '{{count}} плейлистов выбрано',
selectedAlbums: '{{count}} альбомов выбрано',
selectedArtists: '{{count}} исполнителей выбрано',
songInfo: 'Сведения о треке', songInfo: 'Сведения о треке',
}, },
albumDetail: { albumDetail: {
@@ -281,17 +284,17 @@ export const ruTranslation = {
yearTo: 'По', yearTo: 'По',
yearFilterClear: 'Сбросить год', yearFilterClear: 'Сбросить год',
yearFilterLabel: 'Год', yearFilterLabel: 'Год',
select: 'Multi-select', select: 'Множественный выбор',
startSelect: 'Enable multi-select', startSelect: 'Включить множественный выбор',
cancelSelect: 'Cancel', cancelSelect: 'Отмена',
selectionCount: '{{count}} selected', selectionCount: '{{count}} выбрано',
downloadZips: 'Download ZIPs', downloadZips: 'Скачать ZIP-архивы',
addOffline: 'Add Offline', addOffline: 'Добавить офлайн',
downloadingZip: 'Downloading {{current}}/{{total}}: {{name}}', downloadingZip: 'Загрузка {{current}}/{{total}}: {{name}}',
downloadZipDone: '{{count}} ZIP(s) downloaded', downloadZipDone: '{{count}} ZIP-архив(ов) загружено',
downloadZipFailed: 'Failed to download {{name}}', downloadZipFailed: 'Не удалось скачать {{name}}',
offlineQueuing: 'Queuing {{count}} album(s) for offline…', offlineQueuing: 'Добавление {{count}} альбом(ов) в офлайн…',
offlineFailed: 'Failed to add {{name}} offline', offlineFailed: 'Не удалось добавить {{name}} офлайн',
}, },
artists: { artists: {
title: 'Исполнители', title: 'Исполнители',
@@ -307,6 +310,11 @@ export const ruTranslation = {
albumCount_few: '{{count}} альбома', albumCount_few: '{{count}} альбома',
albumCount_many: '{{count}} альбомов', albumCount_many: '{{count}} альбомов',
albumCount_other: '{{count}} альбомов', albumCount_other: '{{count}} альбомов',
selectionCount: '{{count}} выбрано',
select: 'Множественный выбор',
startSelect: 'Включить множественный выбор',
cancelSelect: 'Отмена',
addToPlaylist: 'В плейлист',
}, },
login: { login: {
subtitle: 'Десктопный клиент для Navidrome', subtitle: 'Десктопный клиент для Navidrome',
@@ -978,6 +986,30 @@ export const ruTranslation = {
coverUpdated: 'Обложка обновлена', coverUpdated: 'Обложка обновлена',
metaSaved: 'Плейлист сохранён', metaSaved: 'Плейлист сохранён',
downloadZip: 'Скачать (ZIP)', downloadZip: 'Скачать (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} треков добавлено в {{playlist}}',
addPartial: '{{added}} добавлено, {{skipped}} пропущено (дубликаты)',
addAllSkipped: 'Все пропущены ({{count}} дубликатов)',
addError: 'Ошибка при добавлении треков',
createAndAddSuccess: 'Плейлист "{{playlist}}" создан с {{count}} треками',
createError: 'Ошибка при создании плейлиста',
deleteSuccess: '{{count}} плейлистов удалено',
deleteFailed: 'Ошибка при удалении {{name}}',
deleteSelected: 'Удалить выбранные',
mergeSuccess: '{{count}} треков объединено в {{playlist}}',
mergeNoNewSongs: 'Нет новых треков для добавления',
mergeError: 'Ошибка при объединении плейлистов',
mergeInto: 'Объединить в',
selectionCount: '{{count}} выбрано',
select: 'Выбрать',
startSelect: 'Включить выбор',
cancelSelect: 'Отмена',
loadingArtists: 'Загрузка {{count}} исполнителей…',
myPlaylists: 'Мои плейлисты',
noOtherPlaylists: 'Нет других доступных плейлистов',
addToPlaylistSuccess: '{{count}} треков добавлено в {{playlist}}',
addToPlaylistNoNew: 'Нет новых треков для {{playlist}}',
addToPlaylistError: 'Ошибка при добавлении в плейлист',
}, },
mostPlayed: { mostPlayed: {
title: 'Популярное', title: 'Популярное',
@@ -987,7 +1019,10 @@ export const ruTranslation = {
sortMost: 'Сначала популярные', sortMost: 'Сначала популярные',
sortLeast: 'Сначала малоизвестные', sortLeast: 'Сначала малоизвестные',
loadMore: 'Загрузить больше альбомов', loadMore: 'Загрузить больше альбомов',
noData: 'Нет данных о прослушиваниях. Начните слушать!', noData: 'Пока нет данных о прослушивании. Начните слушать!',
noArtists: 'Все исполнители отфильтрованы.',
filterCompilations: 'Скрыть исполнителей сборников (Various Artists, Soundtracks и др.)',
filterCompilationsShort: 'Скрыть сборники',
}, },
radio: { radio: {
title: 'Онлайн-радио', title: 'Онлайн-радио',
+47 -12
View File
@@ -110,6 +110,9 @@ export const zhTranslation = {
goToArtist: '前往艺术家', goToArtist: '前往艺术家',
download: '下载 (ZIP)', download: '下载 (ZIP)',
addToPlaylist: '添加到播放列表', addToPlaylist: '添加到播放列表',
selectedPlaylists: '已选择 {{count}} 个播放列表',
selectedAlbums: '已选择 {{count}} 个专辑',
selectedArtists: '已选择 {{count}} 个艺术家',
songInfo: '歌曲信息', songInfo: '歌曲信息',
}, },
albumDetail: { albumDetail: {
@@ -272,17 +275,17 @@ export const zhTranslation = {
yearTo: '到', yearTo: '到',
yearFilterClear: '清除年份筛选', yearFilterClear: '清除年份筛选',
yearFilterLabel: '年份', yearFilterLabel: '年份',
select: 'Multi-select', select: '多选',
startSelect: 'Enable multi-select', startSelect: '启用多选',
cancelSelect: 'Cancel', cancelSelect: '取消',
selectionCount: '{{count}} selected', selectionCount: '{{count}} 已选择',
downloadZips: 'Download ZIPs', downloadZips: '下载 ZIP',
addOffline: 'Add Offline', addOffline: '添加离线',
downloadingZip: 'Downloading {{current}}/{{total}}: {{name}}', downloadingZip: '正在下载 {{current}}/{{total}}: {{name}}',
downloadZipDone: '{{count}} ZIP(s) downloaded', downloadZipDone: '已下载 {{count}} ZIP',
downloadZipFailed: 'Failed to download {{name}}', downloadZipFailed: '下载 {{name}} 失败',
offlineQueuing: 'Queuing {{count}} album(s) for offline…', offlineQueuing: '正在将 {{count}} 张专辑加入离线队列…',
offlineFailed: 'Failed to add {{name}} offline', offlineFailed: '添加 {{name}} 离线失败',
}, },
artists: { artists: {
title: '艺术家', title: '艺术家',
@@ -296,6 +299,11 @@ export const zhTranslation = {
notFound: '未找到艺术家。', notFound: '未找到艺术家。',
albumCount_one: '{{count}} 张专辑', albumCount_one: '{{count}} 张专辑',
albumCount_other: '{{count}} 张专辑', albumCount_other: '{{count}} 张专辑',
selectionCount: '{{count}} 已选择',
select: '多选',
startSelect: '启用多选',
cancelSelect: '取消',
addToPlaylist: '添加到播放列表',
}, },
login: { login: {
subtitle: '您的 Navidrome 桌面播放器', subtitle: '您的 Navidrome 桌面播放器',
@@ -915,6 +923,30 @@ export const zhTranslation = {
coverUpdated: '封面已更新', coverUpdated: '封面已更新',
metaSaved: '播放列表已更新', metaSaved: '播放列表已更新',
downloadZip: '下载 (ZIP)', downloadZip: '下载 (ZIP)',
// Toast notifications for multi-select
addSuccess: '{{count}} 首歌曲已添加到 {{playlist}}',
addPartial: '{{added}} 首已添加,{{skipped}} 首跳过(重复)',
addAllSkipped: '全部跳过({{count}} 首重复)',
addError: '添加歌曲时出错',
createAndAddSuccess: '播放列表 "{{playlist}}" 已创建,包含 {{count}} 首歌曲',
createError: '创建播放列表时出错',
deleteSuccess: '{{count}} 个播放列表已删除',
deleteFailed: '删除 {{name}} 时出错',
deleteSelected: '删除所选',
mergeSuccess: '{{count}} 首歌曲已合并到 {{playlist}}',
mergeNoNewSongs: '没有新歌曲可添加',
mergeError: '合并播放列表时出错',
mergeInto: '合并到',
selectionCount: '{{count}} 已选择',
select: '选择',
startSelect: '启用选择',
cancelSelect: '取消',
loadingArtists: '正在解析 {{count}} 位艺术家…',
myPlaylists: '我的播放列表',
noOtherPlaylists: '没有其他可用播放列表',
addToPlaylistSuccess: '{{count}} 首歌曲已添加到 {{playlist}}',
addToPlaylistNoNew: '{{playlist}} 没有新歌曲',
addToPlaylistError: '添加到播放列表时出错',
}, },
mostPlayed: { mostPlayed: {
title: '最常播放', title: '最常播放',
@@ -924,7 +956,10 @@ export const zhTranslation = {
sortMost: '最多播放在前', sortMost: '最多播放在前',
sortLeast: '最少播放在前', sortLeast: '最少播放在前',
loadMore: '加载更多专辑', loadMore: '加载更多专辑',
noData: '暂无播放数据开始听吧!', noData: '暂无播放数据开始听吧!',
noArtists: '所有艺术家已过滤。',
filterCompilations: '隐藏合辑艺术家(Various Artists、原声带等)',
filterCompilationsShort: '隐藏合辑',
}, },
radio: { radio: {
title: '网络电台', title: '网络电台',
+21 -15
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef, useCallback } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ListMusic, Play, Plus, Trash2, X, CheckSquare2, Check } from 'lucide-react'; import { ListMusic, Play, Plus, Trash2, X, CheckSquare2, Check } from 'lucide-react';
import { getPlaylists, createPlaylist, deletePlaylist, SubsonicPlaylist, getPlaylist, buildCoverArtUrl, coverArtCacheKey, updatePlaylist } from '../api/subsonic'; import { getPlaylists, deletePlaylist, SubsonicPlaylist, getPlaylist, buildCoverArtUrl, coverArtCacheKey, updatePlaylist } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore'; import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlaylistStore } from '../store/playlistStore'; import { usePlaylistStore } from '../store/playlistStore';
import CachedImage from '../components/CachedImage'; import CachedImage from '../components/CachedImage';
@@ -20,8 +20,10 @@ export default function Playlists() {
const openContextMenu = usePlayerStore(s => s.openContextMenu); const openContextMenu = usePlayerStore(s => s.openContextMenu);
const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist); const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist);
const removeId = usePlaylistStore((s) => s.removeId); const removeId = usePlaylistStore((s) => s.removeId);
const playlists = usePlaylistStore((s) => s.playlists);
const fetchPlaylists = usePlaylistStore((s) => s.fetchPlaylists);
const playlistsLoading = usePlaylistStore((s) => s.playlistsLoading);
const [playlists, setPlaylists] = useState<SubsonicPlaylist[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState('');
@@ -54,23 +56,20 @@ export default function Playlists() {
const selectedPlaylists = playlists.filter(p => selectedIds.has(p.id)); const selectedPlaylists = playlists.filter(p => selectedIds.has(p.id));
useEffect(() => { useEffect(() => {
getPlaylists() fetchPlaylists().finally(() => setLoading(false));
.then(setPlaylists) }, [fetchPlaylists]);
.catch(() => {})
.finally(() => setLoading(false));
}, []);
useEffect(() => { useEffect(() => {
if (creating) nameInputRef.current?.focus(); if (creating) nameInputRef.current?.focus();
}, [creating]); }, [creating]);
const createPlaylist = usePlaylistStore(s => s.createPlaylist);
const handleCreate = async () => { const handleCreate = async () => {
const name = newName.trim() || t('playlists.unnamed'); const name = newName.trim() || t('playlists.unnamed');
try { await createPlaylist(name);
await createPlaylist(name); // Refresh playlists from API to get the new one
const updated = await getPlaylists(); await fetchPlaylists();
setPlaylists(updated);
} catch {}
setCreating(false); setCreating(false);
setNewName(''); setNewName('');
}; };
@@ -99,8 +98,13 @@ export default function Playlists() {
try { try {
await deletePlaylist(pl.id); await deletePlaylist(pl.id);
removeId(pl.id); removeId(pl.id);
setPlaylists((prev) => prev.filter((p) => p.id !== pl.id)); usePlaylistStore.setState((s) => ({
} catch {} playlists: s.playlists.filter((p) => p.id !== pl.id),
}));
showToast(t('playlists.deleteSuccess', { count: 1 }), 3000, 'info');
} catch {
showToast(t('playlists.deleteFailed', { name: pl.name }), 3000, 'error');
}
setDeleteConfirmId(null); setDeleteConfirmId(null);
}; };
@@ -116,7 +120,9 @@ export default function Playlists() {
showToast(t('playlists.deleteFailed', { name: pl.name }), 3000, 'error'); showToast(t('playlists.deleteFailed', { name: pl.name }), 3000, 'error');
} }
} }
setPlaylists((prev) => prev.filter((p) => !selectedIds.has(p.id))); usePlaylistStore.setState((s) => ({
playlists: s.playlists.filter((p) => !selectedIds.has(p.id)),
}));
clearSelection(); clearSelection();
if (deleted > 0) { if (deleted > 0) {
showToast(t('playlists.deleteSuccess', { count: deleted }), 3000, 'info'); showToast(t('playlists.deleteSuccess', { count: deleted }), 3000, 'info');
+35 -1
View File
@@ -1,22 +1,56 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { getPlaylists, createPlaylist as apiCreatePlaylist, SubsonicPlaylist } from '../api/subsonic';
interface PlaylistStore { interface PlaylistStore {
recentIds: string[]; recentIds: string[];
playlists: SubsonicPlaylist[];
playlistsLoading: boolean;
touchPlaylist: (id: string) => void; touchPlaylist: (id: string) => void;
removeId: (id: string) => void; removeId: (id: string) => void;
fetchPlaylists: () => Promise<void>;
createPlaylist: (name: string, songIds?: string[]) => Promise<SubsonicPlaylist | null>;
addPlaylist: (playlist: SubsonicPlaylist) => void;
} }
export const usePlaylistStore = create<PlaylistStore>()( export const usePlaylistStore = create<PlaylistStore>()(
persist( persist(
(set) => ({ (set, get) => ({
recentIds: [], recentIds: [],
playlists: [],
playlistsLoading: false,
touchPlaylist: (id) => touchPlaylist: (id) =>
set((s) => ({ set((s) => ({
recentIds: [id, ...s.recentIds.filter((x) => x !== id)].slice(0, 50), recentIds: [id, ...s.recentIds.filter((x) => x !== id)].slice(0, 50),
})), })),
removeId: (id) => removeId: (id) =>
set((s) => ({ recentIds: s.recentIds.filter((x) => x !== id) })), set((s) => ({ recentIds: s.recentIds.filter((x) => x !== id) })),
fetchPlaylists: async () => {
set({ playlistsLoading: true });
try {
const playlists = await getPlaylists();
set({ playlists, playlistsLoading: false });
} catch {
set({ playlistsLoading: false });
}
},
createPlaylist: async (name: string, songIds?: string[]) => {
try {
const playlist = await apiCreatePlaylist(name, songIds);
set((s) => ({
playlists: [...s.playlists, playlist],
recentIds: [playlist.id, ...s.recentIds.filter((x) => x !== playlist.id)].slice(0, 50),
}));
return playlist;
} catch {
return null;
}
},
addPlaylist: (playlist) => {
set((s) => ({
playlists: [...s.playlists, playlist],
}));
},
}), }),
{ name: 'psysonic_playlists_recent' } { name: 'psysonic_playlists_recent' }
) )
+1
View File
@@ -3713,6 +3713,7 @@ html.no-compositing .fs-lyrics-rail {
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
color: var(--text-primary); color: var(--text-primary);
font-size: 13px; font-size: 13px;
outline: none;
} }
.context-menu-item { .context-menu-item {