release: bump to v1.34.4

Song ratings in context menu + player bar, entity ratings (PR #130),
5 new seekbar styles, custom Linux title bar, album multi-select,
mix rating filter, top-rated stats, compilation filter, scroll reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-08 11:13:23 +02:00
parent 737b057d4a
commit dbb53bfa70
20 changed files with 258 additions and 16 deletions
+9 -1
View File
@@ -14,9 +14,10 @@ interface AlbumCardProps {
selected?: boolean;
selectionMode?: boolean;
onToggleSelect?: (id: string) => void;
showRating?: boolean;
}
function AlbumCard({ album, selected, selectionMode, onToggleSelect }: AlbumCardProps) {
function AlbumCard({ album, selected, selectionMode, onToggleSelect, showRating = false }: AlbumCardProps) {
const navigate = useNavigate();
const openContextMenu = usePlayerStore(s => s.openContextMenu);
const serverId = useAuthStore(s => s.activeServerId ?? '');
@@ -103,6 +104,13 @@ function AlbumCard({ album, selected, selectionMode, onToggleSelect }: AlbumCard
onClick={e => { if (album.artistId) { e.stopPropagation(); navigate(`/artist/${album.artistId}`); } }}
>{album.artist}</p>
{album.year && <p className="album-card-year">{album.year}</p>}
{showRating && (album.userRating ?? 0) > 0 && (
<div className="album-card-rating-row">
<span className="album-card-rating-stars">
{'★'.repeat(album.userRating!)}{'☆'.repeat(5 - album.userRating!)}
</span>
</div>
)}
</div>
</div>
);
+3 -2
View File
@@ -12,9 +12,10 @@ interface Props {
moreLink?: string;
moreText?: string;
onLoadMore?: () => Promise<void>;
showRating?: boolean;
}
export default function AlbumRow({ title, titleLink, albums, moreLink, moreText, onLoadMore }: Props) {
export default function AlbumRow({ title, titleLink, albums, moreLink, moreText, onLoadMore, showRating }: Props) {
const { t } = useTranslation();
const scrollRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
@@ -89,7 +90,7 @@ export default function AlbumRow({ title, titleLink, albums, moreLink, moreText,
<div className="album-grid-wrapper">
<div className="album-grid" ref={scrollRef} onScroll={handleScroll}>
{albums.map(a => <AlbumCard key={a.id} album={a} />)}
{albums.map(a => <AlbumCard key={a.id} album={a} showRating={showRating} />)}
{loadingMore && (
<div className="album-card-more" style={{ cursor: 'default' }}>
<div style={{ padding: '1rem', background: 'var(--bg-app)', borderRadius: '50%' }}>
+17 -2
View File
@@ -1,9 +1,10 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Play, ListPlus, Radio, Heart, Download, ChevronRight, User, Disc3, ListMusic, Plus, Info } 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 { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getTopSongs, buildDownloadUrl, getAlbum, getPlaylists, getPlaylist, createPlaylist, updatePlaylist, SubsonicPlaylist } from '../api/subsonic';
import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getTopSongs, buildDownloadUrl, getAlbum, getPlaylists, getPlaylist, createPlaylist, updatePlaylist, SubsonicPlaylist, setRating } from '../api/subsonic';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { useDownloadModalStore } from '../store/downloadModalStore';
@@ -163,7 +164,7 @@ function AlbumToPlaylistSubmenu({ albumId, onDone }: { albumId: string; onDone:
export default function ContextMenu() {
const { t } = useTranslation();
const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo } = usePlayerStore();
const { contextMenu, closeContextMenu, playTrack, enqueue, queue, currentTrack, removeTrack, lastfmLovedCache, setLastfmLovedForSong, starredOverrides, setStarredOverride, openSongInfo, userRatingOverrides, setUserRatingOverride } = usePlayerStore();
const auth = useAuthStore();
const requestDownloadFolder = useDownloadModalStore(s => s.requestFolder);
const navigate = useNavigate();
@@ -381,6 +382,13 @@ export default function ContextMenu() {
);
})()}
<div className="context-menu-divider" />
<div className="context-menu-rating-row" onClick={e => e.stopPropagation()}>
<StarRating
value={userRatingOverrides[song.id] ?? song.userRating ?? 0}
onChange={r => { setUserRatingOverride(song.id, r); setRating(song.id, r).catch(() => {}); }}
ariaLabel={t('albumDetail.ratingLabel')}
/>
</div>
<div className="context-menu-item" onClick={() => handleAction(() => openSongInfo(song.id))}>
<Info size={14} /> {t('contextMenu.songInfo')}
</div>
@@ -501,6 +509,13 @@ export default function ContextMenu() {
<Radio size={14} /> {t('contextMenu.startRadio')}
</div>
<div className="context-menu-divider" />
<div className="context-menu-rating-row" onClick={e => e.stopPropagation()}>
<StarRating
value={userRatingOverrides[song.id] ?? song.userRating ?? 0}
onChange={r => { setUserRatingOverride(song.id, r); setRating(song.id, r).catch(() => {}); }}
ariaLabel={t('albumDetail.ratingLabel')}
/>
</div>
<div className="context-menu-item" onClick={() => handleAction(() => openSongInfo(song.id))}>
<Info size={14} /> {t('contextMenu.songInfo')}
</div>
+11 -1
View File
@@ -6,10 +6,11 @@ import {
} from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar } from '../api/subsonic';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar, setRating } from '../api/subsonic';
import CachedImage from './CachedImage';
import WaveformSeek from './WaveformSeek';
import Equalizer from './Equalizer';
import StarRating from './StarRating';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useLyricsStore } from '../store/lyricsStore';
@@ -37,6 +38,7 @@ export default function PlayerBar() {
lastfmLoved, toggleLastfmLove,
isQueueVisible, toggleQueue,
starredOverrides, setStarredOverride,
userRatingOverrides, setUserRatingOverride,
} = usePlayerStore();
const { lastfmSessionKey } = useAuthStore();
@@ -138,6 +140,14 @@ export default function PlayerBar() {
style={{ cursor: !isRadio && currentTrack?.artistId ? 'pointer' : 'default' }}
onClick={() => !isRadio && currentTrack?.artistId && navigate(`/artist/${currentTrack.artistId}`)}
/>
{currentTrack && !isRadio && (
<StarRating
value={userRatingOverrides[currentTrack.id] ?? currentTrack.userRating ?? 0}
onChange={r => { setUserRatingOverride(currentTrack.id, r); setRating(currentTrack.id, r).catch(() => {}); }}
className="player-track-rating"
ariaLabel={t('albumDetail.ratingLabel')}
/>
)}
</div>
{currentTrack && !isRadio && (
<button