mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat: statistics upgrade, playlists redesign, artist cards, and UX improvements (v1.4.0)
- Statistics page: library stat cards, recently played, most played, highest rated, genre chart - Playlists page: list layout with sort (Name/Tracks/Duration) and filter input - Favorites songs: full tracklist layout with artist column, context menu, enqueue-all button - AlbumDetail: extracted AlbumHeader and AlbumTrackList components - Artist cards: square cover, same sizing as album cards (clamp 140-180px) - Random Albums: removed renderKey remount, added loadingRef guard, fixed manual refresh race - Context menu: "Go to Album" option for song and queue-item types - Queue panel meta box: artist → artist page, album → album page, removed year - Random Mix: hover persistence via .context-active class while context menu is open - i18n: "Warteschlange" consistently used for queue in German Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+16
-2
@@ -42,6 +42,9 @@ export default function RandomMix() {
|
||||
const [songs, setSongs] = useState<SubsonicSong[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const playTrack = usePlayerStore(s => s.playTrack);
|
||||
const openContextMenu = usePlayerStore(s => s.openContextMenu);
|
||||
const contextMenuOpen = usePlayerStore(s => s.contextMenu.isOpen);
|
||||
const [contextMenuSongId, setContextMenuSongId] = useState<string | null>(null);
|
||||
const [starredSongs, setStarredSongs] = useState<Set<string>>(new Set());
|
||||
const { excludeAudiobooks, setExcludeAudiobooks, customGenreBlacklist, setCustomGenreBlacklist } = useAuthStore();
|
||||
const [addedGenre, setAddedGenre] = useState<string | null>(null);
|
||||
@@ -69,6 +72,10 @@ export default function RandomMix() {
|
||||
.catch(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenuOpen) setContextMenuSongId(null);
|
||||
}, [contextMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSongs();
|
||||
getGenres().then(setServerGenres).catch(() => {});
|
||||
@@ -314,8 +321,9 @@ export default function RandomMix() {
|
||||
<span style={{ textAlign: 'right' }}>{t('randomMix.trackDuration')}</span>
|
||||
</div>
|
||||
{genreMixSongs.map(song => (
|
||||
<div key={song.id} className="track-row" style={{ gridTemplateColumns: '36px 1fr 1fr 1fr 120px 80px' }}
|
||||
<div key={song.id} className={`track-row${contextMenuSongId === song.id ? ' context-active' : ''}`} style={{ gridTemplateColumns: '36px 1fr 1fr 1fr 120px 80px' }}
|
||||
onDoubleClick={() => playTrack(song, genreMixSongs)} role="row" draggable
|
||||
onContextMenu={e => { e.preventDefault(); setContextMenuSongId(song.id); openContextMenu(e.clientX, e.clientY, { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating }, 'song'); }}
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'song', track: { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating } }));
|
||||
@@ -357,11 +365,17 @@ export default function RandomMix() {
|
||||
{filteredSongs.map((song) => (
|
||||
<div
|
||||
key={song.id}
|
||||
className="track-row"
|
||||
className={`track-row${contextMenuSongId === song.id ? ' context-active' : ''}`}
|
||||
style={{ gridTemplateColumns: '36px 1fr 1fr 1fr 120px 60px 80px' }}
|
||||
onDoubleClick={() => playTrack(song, filteredSongs)}
|
||||
role="row"
|
||||
draggable
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
const track = { id: song.id, title: song.title, artist: song.artist, album: song.album, albumId: song.albumId, artistId: song.artistId, duration: song.duration, coverArt: song.coverArt, track: song.track, year: song.year, bitRate: song.bitRate, suffix: song.suffix, userRating: song.userRating };
|
||||
setContextMenuSongId(song.id);
|
||||
openContextMenu(e.clientX, e.clientY, track, 'song');
|
||||
}}
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
const track = {
|
||||
|
||||
Reference in New Issue
Block a user