mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +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 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<HTMLDivElement>(null);
|
||||
const newNameRef = useRef<HTMLInputElement>(null);
|
||||
const [playlists, setPlaylists] = useState<SubsonicPlaylist[]>([]);
|
||||
const [adding, setAdding] = useState<string | null>(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 && (
|
||||
<div className="context-submenu-empty">{t('playlists.empty')}</div>
|
||||
)}
|
||||
{playlists.map((pl) => (
|
||||
{playlists.map((pl: SubsonicPlaylist) => (
|
||||
<div
|
||||
key={pl.id}
|
||||
className="context-menu-item"
|
||||
@@ -990,6 +985,10 @@ export default function ContextMenu() {
|
||||
previousFocusRef.current = document.activeElement as HTMLElement | null;
|
||||
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;
|
||||
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();
|
||||
})}>
|
||||
<Trash2 size={14} /> {t('playlists.deleteSelected')}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+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 { 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<SubsonicPlaylist[]>([]);
|
||||
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<HTMLButtonElement>(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 ? (
|
||||
<div className="sidebar-playlists-empty">{t('playlists.empty')}</div>
|
||||
) : (
|
||||
playlists.map(pl => (
|
||||
playlists.map((pl: { id: string; name: string }) => (
|
||||
<NavLink
|
||||
key={pl.id}
|
||||
to={`/playlists/${pl.id}`}
|
||||
|
||||
Reference in New Issue
Block a user