mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fixed bugs
This commit is contained in:
@@ -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,17 +40,18 @@ 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);
|
||||||
@@ -58,10 +59,7 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: {
|
|||||||
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>
|
||||||
|
|||||||
@@ -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
@@ -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}`}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: '网络电台',
|
||||||
|
|||||||
+20
-14
@@ -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);
|
||||||
const updated = await getPlaylists();
|
// Refresh playlists from API to get the new one
|
||||||
setPlaylists(updated);
|
await fetchPlaylists();
|
||||||
} 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');
|
||||||
|
|||||||
@@ -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' }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user