From 94323e91fa02b75ee70792448f0caa7fbf993025 Mon Sep 17 00:00:00 2001 From: kveld9 Date: Sun, 12 Apr 2026 17:37:36 -0300 Subject: [PATCH] fixed bugs --- src/components/ContextMenu.tsx | 67 ++++++++++++++++++---------------- src/components/QueuePanel.tsx | 10 ++--- src/components/Sidebar.tsx | 24 ++++++------ src/locales/de.ts | 32 ++++++++++++++++ src/locales/en.ts | 32 ++++++++++++++++ src/locales/es.ts | 20 ++++++++-- src/locales/fr.ts | 57 +++++++++++++++++++++++------ src/locales/nb.ts | 57 +++++++++++++++++++++++------ src/locales/nl.ts | 57 +++++++++++++++++++++++------ src/locales/ru.ts | 59 ++++++++++++++++++++++++------ src/locales/zh.ts | 59 ++++++++++++++++++++++++------ src/pages/Playlists.tsx | 36 ++++++++++-------- src/store/playlistStore.ts | 36 +++++++++++++++++- src/styles/components.css | 1 + 14 files changed, 424 insertions(+), 123 deletions(-) diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index dc7099f6..3c84b138 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -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 LastfmIcon from './LastfmIcon'; import StarRating from './StarRating'; import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm'; import { usePlayerStore, Track, songToTrack } from '../store/playerStore'; 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 { useAuthStore } from '../store/authStore'; import { useDownloadModalStore } from '../store/downloadModalStore'; @@ -40,28 +40,26 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: { const { t } = useTranslation(); const subRef = useRef(null); const newNameRef = useRef(null); - const [playlists, setPlaylists] = useState([]); const [adding, setAdding] = useState(null); const [creating, setCreating] = useState(false); const [newName, setNewName] = useState(''); const [flipLeft, setFlipLeft] = useState(false); - const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist); + const storePlaylists = usePlaylistStore((s) => s.playlists); const recentIds = usePlaylistStore((s) => s.recentIds); + const createPlaylist = usePlaylistStore((s) => s.createPlaylist); + const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist); - useEffect(() => { - getPlaylists().then((all) => { - const sorted = [...all].sort((a, b) => { - const ai = recentIds.indexOf(a.id); - const bi = recentIds.indexOf(b.id); - if (ai === -1 && bi === -1) return a.name.localeCompare(b.name); - if (ai === -1) return 1; - if (bi === -1) return -1; - return ai - bi; - }); - setPlaylists(sorted); - }).catch(() => {}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Sort playlists by recent usage + const playlists = useMemo(() => { + return [...storePlaylists].sort((a, b) => { + const ai = recentIds.indexOf(a.id); + const bi = recentIds.indexOf(b.id); + if (ai === -1 && bi === -1) return a.name.localeCompare(b.name); + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }); + }, [storePlaylists, recentIds]); // Flip submenu left if it would overflow the right edge of the viewport useLayoutEffect(() => { @@ -92,10 +90,7 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: { const handleCreate = async () => { const name = newName.trim() || t('playlists.unnamed'); - try { - const pl = await createPlaylist(name, songIds); - if (pl?.id) touchPlaylist(pl.id); - } catch {} + await createPlaylist(name, songIds); onDone(); }; @@ -139,7 +134,7 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: { {playlists.length === 0 && (
{t('playlists.empty')}
)} - {playlists.map((pl) => ( + {playlists.map((pl: SubsonicPlaylist) => (
('.context-menu-keyboard-active') + .forEach(el => el.classList.remove('context-menu-keyboard-active')); const prev = previousFocusRef.current; previousFocusRef.current = null; if (prev?.isConnected) { @@ -1572,12 +1571,15 @@ export default function ContextMenu() { const { showToast } = await import('../utils/toast'); const { deletePlaylist } = await import('../api/subsonic'); const { usePlaylistStore } = await import('../store/playlistStore'); - const removeId = usePlaylistStore.getState().removeId; + const { removeId } = usePlaylistStore.getState(); try { await deletePlaylist(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'); - window.location.reload(); } catch { showToast(t('playlists.deleteFailed', { name: playlist.name }), 3000, 'error'); } @@ -1713,21 +1715,24 @@ export default function ContextMenu() { const { showToast } = await import('../utils/toast'); const { usePlaylistStore } = await import('../store/playlistStore'); const { deletePlaylist } = await import('../api/subsonic'); - const removeId = usePlaylistStore.getState().removeId; - let deleted = 0; + const { removeId } = usePlaylistStore.getState(); + const deletedIds: string[] = []; for (const pl of selectedPlaylists) { try { await deletePlaylist(pl.id); removeId(pl.id); - deleted++; + deletedIds.push(pl.id); } catch { showToast(t('playlists.deleteFailed', { name: pl.name }), 3000, 'error'); } } - if (deleted > 0) { - showToast(t('playlists.deleteSuccess', { count: deleted }), 3000, 'info'); + if (deletedIds.length > 0) { + // 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(); })}> {t('playlists.deleteSelected')}
diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 0709755b..79de75a9 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -1,7 +1,8 @@ import React, { useState, useRef, useMemo } from 'react'; 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 { 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 { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -655,10 +656,9 @@ export default function QueuePanel() { onClose={() => setSaveModalOpen(false)} onSave={async (name) => { try { - await createPlaylist(name, queue.map(t => t.id)); - const playlists = await getPlaylists(); - const created = playlists.find(p => p.name === name); - if (created) setActivePlaylist({ id: created.id, name: created.name }); + const createPlaylist = usePlaylistStore.getState().createPlaylist; + const pl = await createPlaylist(name, queue.map(t => t.id)); + if (pl) setActivePlaylist({ id: pl.id, name: pl.name }); setSaveModalOpen(false); } catch (e) { console.error('Failed to save playlist', e); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 1c92a85a..fdf3a318 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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 { usePlayerStore } from '../store/playerStore'; import { useOfflineStore } from '../store/offlineStore'; @@ -14,7 +14,8 @@ import { } from 'lucide-react'; import PsysonicLogo from './PsysonicLogo'; 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. // Exported so Settings can render the same item metadata. @@ -58,8 +59,13 @@ export default function Sidebar({ const sidebarItems = useSidebarStore(s => s.items); const [libraryDropdownOpen, setLibraryDropdownOpen] = useState(false); const [playlistsExpanded, setPlaylistsExpanded] = useState(false); - const [playlists, setPlaylists] = useState([]); - const [playlistsLoading, setPlaylistsLoading] = useState(false); + const playlistsRaw = usePlaylistStore(s => s.playlists); + 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 libraryTriggerRef = useRef(null); const showLibraryPicker = !isCollapsed && isLoggedIn && musicFolders.length > 1; @@ -120,12 +126,8 @@ export default function Sidebar({ // Fetch playlists when expanded useEffect(() => { if (!playlistsExpanded || !isLoggedIn) return; - setPlaylistsLoading(true); - getPlaylists() - .then(setPlaylists) - .catch(() => {}) - .finally(() => setPlaylistsLoading(false)); - }, [playlistsExpanded, isLoggedIn]); + fetchPlaylists(); + }, [playlistsExpanded, isLoggedIn, fetchPlaylists]); // Resolve ordered, visible items per section from store config const visibleLibrary = sidebarItems @@ -264,7 +266,7 @@ export default function Sidebar({ ) : playlists.length === 0 ? (
{t('playlists.empty')}
) : ( - playlists.map(pl => ( + playlists.map((pl: { id: string; name: string }) => ( s.openContextMenu); const touchPlaylist = usePlaylistStore((s) => s.touchPlaylist); 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([]); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [newName, setNewName] = useState(''); @@ -54,23 +56,20 @@ export default function Playlists() { const selectedPlaylists = playlists.filter(p => selectedIds.has(p.id)); useEffect(() => { - getPlaylists() - .then(setPlaylists) - .catch(() => {}) - .finally(() => setLoading(false)); - }, []); + fetchPlaylists().finally(() => setLoading(false)); + }, [fetchPlaylists]); useEffect(() => { if (creating) nameInputRef.current?.focus(); }, [creating]); + const createPlaylist = usePlaylistStore(s => s.createPlaylist); + const handleCreate = async () => { const name = newName.trim() || t('playlists.unnamed'); - try { - await createPlaylist(name); - const updated = await getPlaylists(); - setPlaylists(updated); - } catch {} + await createPlaylist(name); + // Refresh playlists from API to get the new one + await fetchPlaylists(); setCreating(false); setNewName(''); }; @@ -99,8 +98,13 @@ export default function Playlists() { try { await deletePlaylist(pl.id); removeId(pl.id); - setPlaylists((prev) => prev.filter((p) => p.id !== pl.id)); - } catch {} + usePlaylistStore.setState((s) => ({ + 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); }; @@ -116,7 +120,9 @@ export default function Playlists() { 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(); if (deleted > 0) { showToast(t('playlists.deleteSuccess', { count: deleted }), 3000, 'info'); diff --git a/src/store/playlistStore.ts b/src/store/playlistStore.ts index ffe4171f..f772bf5c 100644 --- a/src/store/playlistStore.ts +++ b/src/store/playlistStore.ts @@ -1,22 +1,56 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { getPlaylists, createPlaylist as apiCreatePlaylist, SubsonicPlaylist } from '../api/subsonic'; interface PlaylistStore { recentIds: string[]; + playlists: SubsonicPlaylist[]; + playlistsLoading: boolean; touchPlaylist: (id: string) => void; removeId: (id: string) => void; + fetchPlaylists: () => Promise; + createPlaylist: (name: string, songIds?: string[]) => Promise; + addPlaylist: (playlist: SubsonicPlaylist) => void; } export const usePlaylistStore = create()( persist( - (set) => ({ + (set, get) => ({ recentIds: [], + playlists: [], + playlistsLoading: false, touchPlaylist: (id) => set((s) => ({ recentIds: [id, ...s.recentIds.filter((x) => x !== id)].slice(0, 50), })), removeId: (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' } ) diff --git a/src/styles/components.css b/src/styles/components.css index 8cd7ae1b..35952446 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -3713,6 +3713,7 @@ html.no-compositing .fs-lyrics-rail { backdrop-filter: blur(16px); color: var(--text-primary); font-size: 13px; + outline: none; } .context-menu-item {