feat(tracklist): multi-select + psyDnD, filter/sort, settings & UI polish

- AlbumTrackList: extract TrackRow as React.memo, selection state moved to
  selectionStore (Zustand) for O(1) re-renders per toggle; Ctrl/Cmd+Click
  enters select mode; drag selected tracks as {type:'songs'} payload;
  selection clears on outside click or song-list change
- QueuePanel: handle 'songs' multi-track drop type; whitelist drag types to
  suppress drop feedback for non-queue drags (lyrics grip etc.)
- AlbumDetail + PlaylistDetail: filter/sort toolbar (title/artist, natural
  order); disc grouping bypassed when sorted; playlist reorder DnD disabled
  while filter active
- useTracklistColumns: 'known' field auto-shows newly added columns for
  existing users
- PlayerBar: mute/unmute restores previous volume via premuteVolumeRef
  instead of hardcoded 0.7
- Settings/Input: reset buttons restyled as RotateCcw icon above card,
  matching HomeCustomizer layout
- i18n: filterSongs, sortNatural, sortByTitle, sortByArtist keys across
  all 7 locales
- components.css: album-card-title/artist nowrap to keep playlist grid
  cards uniform height

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-11 11:42:31 +02:00
parent b46acc88e3
commit b9b8f3fc15
16 changed files with 573 additions and 243 deletions
+9 -1
View File
@@ -46,6 +46,7 @@ export default function PlayerBar() {
const navigate = useNavigate();
const [eqOpen, setEqOpen] = useState(false);
const [showVolPct, setShowVolPct] = useState(false);
const premuteVolumeRef = useRef(1);
const showLyrics = useLyricsStore(s => s.showLyrics);
const activeTab = useLyricsStore(s => s.activeTab);
// currentTime is intentionally excluded — PlaybackTime handles it via direct DOM update.
@@ -313,7 +314,14 @@ export default function PlayerBar() {
<div className="player-volume-section">
<button
className="player-btn player-btn-sm"
onClick={() => setVolume(volume === 0 ? 0.7 : 0)}
onClick={() => {
if (volume === 0) {
setVolume(premuteVolumeRef.current);
} else {
premuteVolumeRef.current = volume;
setVolume(0);
}
}}
aria-label={t('player.volume')}
style={{ color: 'var(--text-muted)', flexShrink: 0 }}
>