* feat(offline): local-bytes browse for artists and albums
Make Artists, All Albums, and artist/album detail pages work offline
from the library index limited to on-disk library and favorite-auto
tracks. Add a DEV header toggle to simulate offline browse for testing.
* feat(offline): reactive DEV offline toggle with full disconnect simulation
Subscribe nav and browse/detail hooks to useOfflineBrowseActive so UI
refreshes on toggle. DEV force-offline now blocks server probes, reports
disconnected status, and gates Subsonic like real offline for player parity.
* feat(offline): bytes-first favorites when offline browse is active
Load Favorites from local playback bytes, filter starred tracks client-side,
and restrict album-level star queries to local album ids. Drop interim perf
attempts (lean SQL, progressive load, connection singleton, prefetch UX).
* feat(offline): tracks, help, player stats; suspend library picker offline
- Offline browse for Tracks hub from local bytes; sidebar nav for tracks/help/statistics
- Statistics redirects to player-stats offline; server/Last.fm tabs skip network fetches
- Hide music-library picker offline; save filter and restore on reconnect (all libraries while disconnected)
- Unified isOfflineSidebarNavAllowed for library + system entries
* feat(offline): fork disconnect navigation by offline browse capability
When the server drops: stay on the page if nothing is browsable offline;
reload in place on offline-capable routes; otherwise redirect to All Albums
instead of the old /offline or /favorites bounce.
* feat(offline): browse cached playlists when the server is down
List and open manually pinned regular playlists from local library-tier
bytes offline, with sidebar/nav routing and read-only playlist UI.
* feat(offline): read-only artist detail and local play-all paths
Hide favorites and discography offline actions when browse is offline;
load Play All, Shuffle, and top-track continuation from local album bytes.
* feat(offline): read-only album detail and enqueue from local bytes
Hide favorites, download, and cache-offline actions on album pages when
offline browse is active. Favorites album cards enqueue via the same
resolveAlbumForServer path as play, including local playback bytes.
* chore: remove unused import in AlbumCard after enqueue refactor
* feat(offline): unify browse integration contract across the app
Add useOfflineBrowseContext, offlineMediaResolve, and offlineActionPolicy;
wire shell nav to a single capability source; migrate play/enqueue and
context-menu paths off raw getAlbum; replace readOnly with action policy
on detail surfaces. Tests updated for the media-resolve facade.
* feat(offline): close browse contract gaps and fix offline Home feed
Split offline browse modules, align favorites capability across servers,
wire action policy on context menus, migrate hooks to useOfflineBrowseContext,
and preserve stale Home feed cache when offline so the UI does not empty.
* fix(offline): block playbar stars, close audit gaps, trim dead exports
Hide star rating and favorite in PlayerBar when offline browse is active via
offlineActionPolicy playerBar surface. Wire stay-reload token into browse
hooks, migrate hooks to context.active, guard rating prefetch network calls,
and route playlist load through resolvePlaylist.
* docs: add CHANGELOG and credits for offline browse PR #1017
* fix(offline): stop DEV connection probe regression in tests
React to devForceOffline transitions only in useConnectionStatus so mount
does not double-fire check() or ignore disableBackgroundPolling. Add
pingWithCredentials to PlayerBar test mock and DEV-toggle unit tests.
* fix(playlist): columns dropdown no longer clipped on short lists (#839)
The column picker rendered inside `.tracklist` (overflow-x: auto, which
makes overflow-y compute to auto). On a 1-song playlist the downward popover
overflowed the short box → clipped behind suggestions, an extra scrollbar,
and the row vanishing when scrolling that inner bar (the virtualizer tracks
the main viewport, not the tracklist). Move the picker outside `.tracklist`
by reusing the shared TracklistColumnPicker (parametrized with allColumns);
fixes the same latent bug in the favorites tracklist and dedupes three
inline copies into one.
* docs(changelog): playlist/favorites column picker fix (#853)
* feat(album): show OpenSubsonic disc subtitles after the CD heading
Multi-disc albums in OpenSubsonic / Navidrome carry a per-disc
subtitle (`discTitles`) — e.g. "Sessions" on CD 3 of a deluxe
edition. AlbumTrackList only rendered "CD N" and dropped the
subtitle, so users couldn't tell two discs apart unless they read
the track names.
* `SubsonicAlbum.discTitles` typed; `getAlbum` forwards it as-is.
* `AlbumTrackList` and `AlbumTrackListMobile` take a discTitleByNum
map and render the subtitle in the disc separator after "CD N".
* Heading bumped slightly (13 → 15 px, icon 16 → 18 px) so the disc
separator stays legible next to the new subtitle.
* docs(changelog): note disc subtitles after CD heading (#753)
111 of 122 top-level src/utils/ files move into 16 topic folders (audio,
cache, cover, share, server, playback, playlist, deviceSync, waveform,
mix, format, export, changelog, ui, perf, componentHelpers). True
singletons with no cluster stay at the utils/ root.
Pure file-move: a path-aware codemod rewrote 539 relative-import
specifiers across 275 files; no logic touched. The hot-path coverage
gate list (.github/frontend-hot-path-files.txt) is updated to the new
paths for the 11 gated utils files — a mechanical consequence of the
move, not a CI change. tsc is green.
* refactor(album-track-list): extract helpers + types
Pull formatDuration / codecLabel, the COLUMNS / CENTERED_COLS / SORTABLE_COLS
tables, ColKey / SortKey types, and the isSortable type guard into
utils/albumTrackListHelpers.ts. SortKey is re-exported from
AlbumTrackList.tsx so existing imports stay valid.
AlbumTrackList.tsx: 662 → 633 LOC.
* refactor(album-track-list): extract TrackRow subcomponent
Move the memoised tracklist row (~220 LOC including renderCell switch and
mouse handlers) into components/albumTrackList/TrackRow.tsx. It still
subscribes to its own selection + preview state via primitive selectors,
so per-row re-render scope is unchanged.
AlbumTrackList.tsx: 633 → 404 LOC.
* refactor(album-track-list): extract AlbumTrackListMobile subcomponent
Move the narrow-viewport branch (compact tracklist with disc separators
and no column grid) into components/albumTrackList/AlbumTrackListMobile.tsx.
AlbumTrackList.tsx: 404 → 376 LOC.
* refactor(album-track-list): extract TracklistColumnPicker subcomponent
The column visibility dropdown lives outside .tracklist to avoid the
overflow box clipping its menu — pull the wrapper + button + popover into
components/albumTrackList/TracklistColumnPicker.tsx.
AlbumTrackList.tsx: 376 → 347 LOC.
* refactor(album-track-list): extract TracklistHeaderRow subcomponent
The fixed header (sortable + resizable per-column with the bulk-select
toggle on the num cell) moves into
components/albumTrackList/TracklistHeaderRow.tsx, taking 85+ LOC of cell
rendering with it.
AlbumTrackList.tsx: 347 → 254 LOC.
* refactor(album-track-list): extract useAlbumTrackListSelection hook
Pull bulk-selection state (selectedIds-size subscription, shift-range
toggle, click-outside-clear, song-list-change clear) and the drag-start
dispatcher (single vs multi-song drag) into
hooks/useAlbumTrackListSelection.ts.
AlbumTrackList.tsx: 254 → 187 LOC.
First Phase F slice. Splits the 1333-LOC `api/subsonic.ts` along its
two most obvious axes:
- `subsonicTypes.ts` — all ~24 exported interfaces + type aliases
(album/song/artist/playlist/directory/genre/now-playing/radio,
random-songs filters, three statistics shapes, search + starred
results, AlbumInfo, structured-lyrics types, etc.) plus the
`RADIO_PAGE_SIZE` constant.
- `subsonicClient.ts` — token-auth + `getClient` + `api<T>()` +
`libraryFilterParams` + `secureRandomSalt` / `getAuthParams` /
`SUBSONIC_CLIENT`. The credential-bearing API helpers
(`pingWithCredentials`, `apiWithCredentials`, `restBaseFromUrl`,
`probeInstantMixWithCredentials`) stay in `subsonic.ts` for now —
they could move into the client module in a follow-up.
66 external call sites migrated to direct imports from the new
modules (no re-export shims in `subsonic.ts`). Pure code-move;
contract test stays green.
subsonic.ts: 1333 → 1078 LOC (−255).
Migrates ~74 call sites away from the playerStore re-export shims that
were kept during M0–E.41 to avoid touching 30+ imports per PR. Now
that the bigger refactor work is done, each helper goes back to its
real home:
- `initAudioListeners`, `installQueueUndoHotkey`, `flushPlayQueuePosition`
→ from their own store modules
- `getPlaybackProgressSnapshot`, `subscribePlaybackProgress`,
`PlaybackProgressSnapshot` → from `playbackProgress`
- `resolveReplayGainDb`, `shuffleArray`, `songToTrack`
→ from `utils/*`
- `_resetQueueUndoStacksForTest`, `consumePendingQueueListScrollTop`,
`registerQueueListScrollTopReader` → from `queueUndo`
- `PlayerState`, `Track` types → from `playerStoreTypes`
Drops the corresponding 13 re-export stubs from `playerStore.ts` and
the now-unused imports. Also drops dead section banners + per-wrapper
comments above one-line action delegates. Trims one stale "(separate
PR)" note in `transportLightActions.ts` since that follow-up landed
in E.39.
`playerStore.ts`: 180 → 112 LOC (−68). Down from Phase E's starting
3732 LOC.
`bootstrap.test.ts` mock target updated from `../store/playerStore`
to `../store/queueUndoHotkey` to keep the spy reachable after the
import change.
* fix(orbit): guest short-circuits queue-exhaustion fallback paths
When a guest's local queue runs out (single-track queue from `syncToHost`
empties on `audio:ended`), the player walks the standard fallback chain
in `next()`: radio top-up → infinite-queue → stop. The infinite-queue
branch builds a 6-track queue and calls `playTrack`, which trips
`orbitBulkGuard` and pops a "Add 6 tracks to the Orbit queue?" modal.
Hitting Cancel leaves playback frozen; "Add them all" injects unrelated
tracks into the host's shared queue.
In an active Orbit guest session the host owns the queue. Skip the
fallback paths entirely and just stop — the next `useOrbitGuest` pull
tick will sync to whatever the host advanced to.
Bonus side-effect: kills the deferred-promise race where a
`buildInfiniteQueueCandidates().then(...)` from a guest's track end
could resolve *after* a Catch Up replaced the queue and pop the modal
a second time against the now-current 1-track queue.
* fix(orbit): treat natural track-end as not-diverged in guest sync
When a guest's track ended naturally before the host advanced, the
divergence-detection branch read `player.isPlaying === false` and
classified it as the user manually paused — so it refused to load the
host's next track. The guest sat silent until they clicked Catch Up.
`handleAudioEnded` keeps `currentTrack` pinned to the just-ended track
and resets `currentTime` to 0, while a real manual pause leaves
`currentTime` somewhere mid-track. Use the 0-position discriminator to
classify natural-end as not-diverged so the host's new track loads.
Confirmed via the captured guest log buffer:
18:43:08.598 [track-change] host: VJkV5… → 6i6RP… BUT guest diverged
(player.isPlaying=false ≠ last.isPlaying=true)
— guest stuck for ~33s until Catch Up was pressed.
* fix(orbit): Catch Up polls until engine is ready before seeking
The 400 ms blind setTimeout in `onCatchUp` was too short for an
HTTP-streamed cold-start on high-latency links. If the audio engine
wasn't ready by then, `seek(fraction)` silently no-oped and playback
started at 0:00, making Catch Up effectively useless on exactly the
slow links where it's needed. Captured log shows a Catch Up bringing
the guest to posSec=30, then 5 s later the guest was at posSec=6
(playback restarted from the head).
Replace with the same poll-until-ready pattern `syncToHost` already
uses: check every 100 ms, fire the seek as soon as the engine reports
playing, fall back to a blind apply at the 4 s deadline.
* docs(changelog): add orbit guest playback fixes entry
* fix(orbit): debounce Catch Up button + match bar item height
Two follow-on UX fixes after PR #525's three primary bugs landed:
1. **Debounce visibility.** Drift is computed from an asymmetric signal:
guest's `currentTime` updates in coarse ~5 s chunks, while host's
position is extrapolated linearly via `(nowMs - posAt)`. Even on a
perfectly-synced session the diff swings ±5 s every tick, so the
button flickered in and out continuously. Show only after drift has
stayed over the 3 s threshold for ≥ 3 s of wall clock — measurement
noise is filtered out, real sustained drift still surfaces in time.
2. **Match neighbour height.** The button was 32 px tall against 26 px
for the other action buttons (.orbit-bar__settings) so every flicker
shifted the entire bar height. Set `height: 26 px` and tighten the
padding/font so the layout is stable regardless of visibility.
* fix(orbit): tighten queue-extension lockout + reliable initial-sync seek
Two follow-on fixes after the 4-bug umbrella:
**1. Local queue-extension paths fully off during Orbit.**
Phase check broadened from `active` to cover `starting` / `joining` /
`active` so a fetch-then-join race can't pop the bulk-add modal *after*
the join. The proactive infinite-queue topper inside `next()` (which
fires when ≤ 2 auto-tracks remain ahead) is now also gated, plus each
async `.then()` callback in the radio + infinite-queue paths re-checks
at resolution time. A `playTrack(... 6-track queue ...)` after the user
joined Orbit was the path that re-triggered the "Add 5 tracks?" modal
on a freshly-joined guest.
**2. `syncToHost` only seeks once the engine reports playing.**
The previous 2 s deadline-fallback applied the seek even when the
engine hadn't started, where the seek silently no-ops and the track
plays from 0:00. Symptom: clicking Catch Up makes the song "jump 50 %
forward" — that's the seek finally landing because the engine is now
ready, the initial-sync seek had already failed silently. New deadline
is 5 s, and on timeout we return `false` so the outer pull tick keeps
`lastAppliedRef` null and the 500 ms fast-poll retries.
* fix(orbit): double-click play button + hide preview during session
Two cucadmuh-flagged gaps:
**1. Double-click on the inline play button now reaches the orbit-add
path.** The album-track row's onDoubleClick already routes to
`addTrackToOrbit` when in Orbit, but the inline play button stopped
propagation on click — so clicking it twice just fired the "double-
click to add" hint toast and never touched the orbit queue. Add an
onDoubleClick on the button itself that delegates to the parent's
`onDoubleClickSong`.
**2. Track preview is suppressed during an Orbit session.** Preview
shares the Rust audio engine with the shared playback, so starting
one as a guest yanks the host's track out from under everyone. A new
`[data-orbit-active]` attribute on `<html>` (set whenever role is
host/guest and phase is starting/joining/active) hides every
preview button via a single CSS rule, and `previewStore.startPreview`
short-circuits as a defensive guard for keyboard shortcuts and any
programmatic callers.
* perf(tracklist): replace animated EQ-bar + active-row pulse with static icon
The currently-playing track in any tracklist (AlbumDetail, ArtistDetail,
PlaylistDetail, Favorites, RandomMix) was rendered with two animations
on top of an already-busy DOM:
- `.track-row.active` ran `track-pulse` 3s opacity 1 → 0.6 → 1 infinite
on the entire row subtree (title button, several Lucide icons, star
rating SVGs, hover affordances). Opacity is a compositor property, but
on WebKitGTK without compositing — Linux + NVIDIA proprietary +
WEBKIT_DISABLE_COMPOSITING_MODE=1 — every animated row falls back to a
full software repaint of the subtree per frame.
- The "now playing" indicator in the track-number cell was three
`<span class="eq-bar">` siblings with `transform: scaleY()` keyframes
(`eq-bounce`), each on its own delay/duration. Same composite story:
three software-repainted layers per frame, every frame.
On AlbumDetail (long tracklist + cover-art header background + the
already-running WaveformSeek progress rAF in the player bar) the
combined cost held the WebProcess at ~80 % CPU with 1.2 GB RSS for the
entire duration of playback. CPU dropped immediately on pause/stop;
on Composers / Settings (no track rows) the symptom never appeared.
Profiler confirmed continuous Layout & Rendering work synchronised with
isPlaying, not the canvas itself.
Replace both animations with static visuals:
- `.track-row.active` keeps the `--accent-dim` background, drops the
pulse animation entirely.
- The "now playing" indicator becomes a single Lucide `AudioLines` icon
(four vertical bars of different heights — reads as an EQ icon, no
animation, one SVG per active row instead of three animated spans).
`.eq-bars` className now only sets the accent colour.
Cleanup: dead `@keyframes track-pulse`, `@keyframes eq-bounce`,
`.eq-bars.paused` rule, plus a duplicate `.eq-bar` block in theme.css
(with a `wave` keyframe that was being shadowed by components.css and
had no other consumers).
No behaviour change beyond removing the animation; the row is still
visibly the active one and the icon still marks the playing track.
* docs: CHANGELOG entry for tracklist animation perf fix (PR #488)
The squash-merge of PR #419 was performed against an outdated PR base
that predated several main-side refactors and features. The resulting
squash inadvertently re-introduced files that had already been removed
(`src-tauri/src/audio.rs` monolith, `app-icon.png`) and reverted main's
content for ~20 files (`src-tauri/src/lib.rs` decompose, `src/App.tsx`
animation-pause, `src/components/AlbumRow.tsx` headerExtra, etc).
This commit:
* Restores all collateral-damage files to their pre-#419 main state
(5662dafe), including the audio module split, lib decompose,
Cargo.toml `windows` dep, animation-pause-on-blur logic, and
`reducedAnimations` toggle plumbing in WaveformSeek/Settings.
* Keeps the genuine #419 work intact: tri-state duration toggle,
position counter, persistent Now Playing collapse, animated EQ
indicator (in QueuePanel.tsx, 8 locales, authStore, components.css).
* Merges authStore.ts so both `reducedAnimations` (from #426) and
`queueNowPlayingCollapsed` (from #419) coexist.
* Adds the `.eq-bars.paused` CSS rule manually since components.css
needed a 5662dafe base + the single #419 addition.
* Fixes a latent type error in OverlayScrollArea.tsx (`useRef<T>(null)`
is `RefObject<T>` / read-only under current `@types/react`; widened to
`useRef<T | null>(null)` so the existing `wrapRef.current = el`
assignment compiles).
Verified locally with `cargo check` and `npm run build` — both green.
* feat(queue): add ETA display, equalizer indicator and collapsible now playing
* deleted endsAt and showDuration strings, changed eta update to 30s
* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon
* feat(queue): fold ETA into existing total/remaining toggle as third mode
The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.
ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.
i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.
* docs(changelog): add #419 queue UX improvements entry
Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
* fix(preview): sync audio start, ring animation, and download timeout
Three coupled fixes for the track-preview engine:
1. Audio sync. `Sink::try_seek` was running on a worker thread after
`sink.append(source)`, so the sink began playing position 0 while
the seek was still iterating to the mid-track target. With the
30 s `take_duration` cap counting wall-clock from append, audio
could only become audible ~25% into the preview window. The seek
now runs on the bare source before append, then `take_duration`
wraps it — playback starts at the seek position with the cap
measured from there.
2. Ring animation gating. The CSS progress-ring animation was
bound to `is-previewing` (set on click), so the ring sprinted
ahead of any download/decode/seek warmup and didn't reset
cleanly when switching from one preview to another. Added an
`audioStarted` flag in `previewStore` that flips on
`audio:preview-start` from the engine; CSS animation is now
gated on `audio-started` instead. `is-previewing` still drives
tooltip/icon for instant click feedback. Same SVG is reused for
a 25%-arc rotating loading spinner while waiting for audio,
with a 150 ms delay so cached/short previews don't flash.
3. Download timeout. The shared `audio_http_client` caps at 30 s,
which aborts mid-download on multi-hundred-MB uncompressed
files (e.g. 18-min Hi-Res WAV ~600 MB). The preview engine now
builds a dedicated client with a 5 min timeout for the bytes
fetch. Watchdog still bounds the playback window at 30 s once
the audio actually starts.
Touches `audio/preview.rs`, `previewStore.ts`, `components.css`
plus the eight tracklist/player-bar components that render the
preview button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): add preview audio sync fix for PR #423
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ui): unify queue toggle handle behavior
Show the queue toggle in the header when the queue is collapsed and use a seam-aligned drag handle when it is open. Hide the seam handle while the main content is actively scrolling to reduce accidental interactions.
* feat(ui): add adaptive header search collapse behavior
Collapse header search to a magnifier when top controls get crowded and expand it as an overlay only while active. Use measured header space with hysteresis to avoid flicker and keep neighboring controls stable.
* chore(ui): remove leftover search prototype artifacts
Drop an unused icon import from live search and remove an unused header container-type rule left from an earlier layout experiment.
* feat(ui): persist sidebar and queue visibility state
Save left sidebar collapse and right queue open/closed visibility in local storage helpers so both panel modes are restored after app restart.
* feat(ui): unify player overflow menu behavior
Use a single overflow menu for click and wheel interactions, with a volume-only mode that keeps the same layout and volume controls as the full menu.
* feat(ui): add wheel seek controls to waveform
Apply 10-second wheel seek steps with trailing 1-second debounce and keep the waveform preview stable so the playhead moves smoothly during rapid scroll input.
* fix(now-playing): stabilize narrow dashboard layout
Switch now-playing responsiveness to container-based breakpoints and prevent stacked widgets from overlapping when width is constrained.
* fix(search): reduce collapse jitter and avoid header overlap
Add a short collapse-state cooldown to prevent threshold flicker and hide conflicting header controls while collapsed search expands as an overlay.
* fix(i18n): localize player overflow controls across locales
Replace hardcoded player overflow labels with translation keys and add the missing keys for all shipped locale files.
* fix(search): keep advanced control clickable in collapsed mode
Prevent focus loss on the advanced search button in collapsed overlay mode so its click handler consistently runs.
* fix(i18n): restore queue translation in offline library
Use the existing queue.appendToQueue key for the offline enqueue button tooltip and label instead of a missing key and hardcoded English text.
* fix(ui): apply overlay scrollbar to right-panel text tabs
Switch now-playing content, lyrics, and info panes to OverlayScrollArea and harden tour-item layout so long concert metadata stays within panel bounds.
* fix(ui): add unread indicator for new releases and guard sidebar drag clicks
Track unread new-release IDs per server/library scope and clear the badge when opening the New Releases page. Also prevent click-through navigation after sidebar drag release and keep related i18n/responsive sidebar-adjacent refinements in this snapshot.
* fix(ui): stabilize live dropdown layering and unread reset flow
Render the topbar Live dropdown via a portal so it consistently overlays sidebar layers. Rework new-releases unread tracking to handle library scope baselines, ignore stale refresh races, and mark items as seen after a 5-second stay on the New Releases page.
* feat(ui): add localized New badges for recently added albums
Show a theme-consistent New badge on album cards and album detail for albums created within the last 48 hours. Localize the badge label across all supported locales and centralize recency logic in a shared utility to avoid duplication.
* fix(album): prevent tracklist jump when entering multiselect
Move bulk selection actions from the tracklist body into the album toolbar next to the track filter. Keep selection controls stable in the header area so enabling multiselect no longer shifts the tracklist content downward.
* fix(tray): add playback-state badge and finalize queue handle tooltip
Show play/pause/stop icons in the Linux tray now-playing entry and persist state safely in Tauri managed state.
Also switch the queue-resize handle tooltip to the dedicated localized key across all locales.
* fix(header): prioritize search collapse before Live/Orbit labels
Make topbar compression deterministic by collapsing search first and compacting Live/Orbit labels only in sustained low-space mode.
Add sticky hysteresis-based header compact state to prevent oscillation while resizing.
* fix(ui): stabilize header compaction and show tray state icons
Prevent topbar flicker in the narrow-width range by tightening compact-mode thresholds, gating on real overflow, and removing width transitions from live search.
Also include playback state icons in tray tooltip text across platforms while preserving tooltip length limits.
* fix(tray): keep tooltip iconization Windows-only
Revert Linux tray tooltip/title fallback attempts and keep state icons only in Windows tray tooltips, while Linux continues to show playback state in the now-playing menu entry.
* fix(ui): restore queue resize response after overlay scroll interactions
Hide the queue handle while scrolling on both the main route viewport and the now-playing viewport, and clear stale thumb-drag state before starting queue resize.
Also ignore inactive/faded overlay thumbs in resizer suppression so horizontal pointer transitions no longer leave the queue seam unresponsive.
* docs(changelog): summarize ui-refinements branch features
Document the branch-level feature additions in 1.45.0 as separate changelog sections and group remaining branch-local fixes under a single polish entry.
* docs(changelog): add PR #397 references for ui-refinements
Attach PR metadata to the new 1.45.0 ui-refinement sections and the polish entry so release notes map directly to the merged branch discussion.
* feat(player): preview-active state on play button (ring + stop icon)
Checkpoint: play button mirrors the inline preview button from tracklists
during preview playback — hollow circle, accent ring depleting over the
preview duration, Square (stop) icon. Click still resumes main playback,
which the Rust audio engine cancels the preview for.
i18n key player.previewActive in all 8 locales for tooltip + aria-label.
* feat(player): show preview track in player bar + smart stop semantics
The player-bar info cell (cover, title, artist) now mirrors the previewing
track during preview playback, with a small accent "Preview" pill above
the title and an accent top-border on the bar. Rating, fullscreen hint
and album/artist link clicks are suppressed while previewing — they
target the queued track, not the preview.
Stop semantics for the two transport buttons during preview:
- Big play button (Square+ring visual): stops preview, main auto-resumes
if it was playing before. Matches the tracklist preview-button behaviour.
- Small Stop button: new audio_preview_stop_silent Rust command — stops
preview AND leaves main paused, so "Stop = silence" actually goes silent.
previewStore now stores the full PreviewingTrack (id, title, artist,
coverArt) — the seven startPreview call sites pass it through.
i18n key player.previewLabel in all 8 locales.
* feat(audio): rust preview engine with secondary sink
Adds a parallel rodio Sink on the existing OutputStream for 30s
mid-track previews. Two new Tauri commands (audio_preview_play,
audio_preview_stop) plus three events (audio:preview-start /
-progress / -end). The main sink is paused with Sink::pause() and
auto-resumed on preview end iff it was playing beforehand.
* feat(playlists): migrate suggestion preview to rust audio engine
Replaces the HTML5 <audio> path with the new rust preview engine.
previewStore mirrors the engine's start/progress/end events so any
tracklist row can render preview UI from a single source of truth.
Spacebar redirects to stopPreview while a preview plays, hardware
mediakeys are silently dropped (Q5), and tray clicks cancel the
preview before forwarding the original action.
* feat(albums): inline play + preview buttons in tracklist rows
Track number stays static on hover instead of swapping to a play
icon — the dedicated Play and Preview buttons in the title cell
take over click-to-play and click-to-preview. Active+playing rows
keep the eq-bars (also on hover), active+paused rows fall back to
the static accent-coloured number.
Pilot for the wider rollout to other tracklists.
* feat(tracklists): roll out inline play + preview buttons
Mirrors the AlbumTrackList pilot across the remaining track-row
based lists: PlaylistDetail main tracks, Favorites, ArtistDetail
top tracks, RandomMix (both genre-mix and filtered-songs lists).
Track number stays static, the dedicated Play + Preview buttons
in the title cell take over click-to-play and click-to-preview.
* feat(settings): track preview toggle + configurable position and duration
Adds an opt-out switch and two sliders to Settings → Audio:
start position (0-90 % of track length, default 33 %) and preview
duration (5-60 s, default 30 s). The progress-ring animation
follows the duration via a CSS variable so the visual matches the
engine's auto-stop. Disabling the feature hides every inline
preview button via a single root-level data attribute, no per-row
conditional rendering required.
i18n keys added in all 8 locales.
* fix(audio): cancel preview when main playback (re)starts
audio_play, audio_play_radio, audio_resume and audio_stop did not
know about the parallel preview sink, so clicking Play on a track
that was currently being previewed left the preview running on top
of the freshly started main playback. New helper clears the resume
flag, bumps the preview generation, drops the sink and emits an
'interrupted' end event before any of those commands touches the
main sink.
* feat(settings): per-location track preview toggles
Splits the single trackPreviewsEnabled toggle into a master + 6 per-location
sub-toggles (suggestions, albums, playlists, favorites, artist, randomMix).
Master remains the kill switch; sub-toggles are only honoured when master
is on. Each tracklist container is marked with `data-preview-loc="<id>"` and
hidden via scoped CSS when the matching root attribute is "off".
startPreview now takes a location argument so the store can guard logic too.
i18n added in all 8 locales.
* fix(contextmenu): use ChevronsRight for Play Next to distinguish from preview
- Add "Reset to defaults" button to column picker in AlbumTrackList, PlaylistDetail, Favorites (all 8 locales)
- Fix device sync cross-OS status detection: store filenameTemplate in manifest, use it for path computation on import
- Redesign filename template UI: preset buttons (Standard/Multi-Disc/Alt. Folder), clickable token chips ({artist}, {album}, etc.), clear button
- Fix suggestions section in PlaylistDetail missing album column rendering
- Add PRIVACY.md documenting all third-party integrations (Last.fm, LRCLIB, Apple Music, Discord); link from README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.content-body has overflow-x:hidden — setting overflow-x:visible on
.tracklist would clip wide tables instead of scrolling them. Keep auto.
Also remove Spanish code comments (contributors are international).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces dedicated sort buttons with clickable column headers in album and playlist detail views. Three-click cycle: asc → desc → natural order. Sortable by title, artist, album, favorite, rating, duration. Active column shown bold with ▲/▼ indicator.
- Use OpenSubsonic `artists[]` array to render each artist separately
with · separator; artists with an ID are clickable, others plain text
- Fall back to single artist (artistId/artist) on non-OpenSubsonic servers
- Settings reset-to-defaults buttons changed from btn-ghost to btn-danger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply the same per-axis star cutoff to random mix (multi-batch fetch until
full), random albums, Hero, and Home hero/discover rows. Prefetch artist and
album ratings via Subsonic when list payloads omit them. Exclude items when
0 < rating ≤ threshold; 0 or missing counts as unrated.
Settings: rating filter description interpolates sidebar menu labels; Russian
strings for custom title bar; short axis labels aligned across locales.
Probe OpenSubsonic after connect; persist album/artist ratings via setRating when supported. Add shared StarRating (repeat same star to clear, pulse and shrink animations, hover freeze after clear). Widen tracklist duration column and bump narrow saved widths in localStorage. Use StarRating in AlbumHeader, track lists, and playlist rows.
Internet Radio full release (HTML5 engine, card UI, RadioDirectoryModal,
cover upload), Backup/Restore, Albums year filter, Statistics Library
Insights (playtime/genres/formats), Playlist cover upload, resizable
tracklist columns for Playlists & Favorites, crossfade fine control,
Settings Storage tab redesign, various fixes and UI polish.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
### Added
- Favorite/Star button in player bar (requested by @halfkey)
- Bulk multi-select for album tracklist and playlist detail (add to playlist, remove from playlist)
- Song Info modal via right-click context menu (metadata: format, bitrate, sample rate, bit depth, channels, file size, path, replay gain)
- Recently Played section on Home page
- "Show activity in Now Playing" opt-in toggle in Settings → Behavior
### Fixed
- Queue cover art not updating on track change (useCachedUrl/CachedImage reset on cacheKey change)
- FullscreenPlayer background flickering (FsBg preloads image before layer transition)
- Playlist card delete confirmation visual (size expansion + pulse animation)
- Gruvbox Light Soft: back button and badge invisible against light background
### Changed
- buildStreamUrl: removed unused suffix parameter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merges songToTrack() helper and audio_update_replay_gain Tauri command
from @trbn1 without losing v1.22.0 DnD/queue-management work:
- All track construction sites use songToTrack() for correct replay gain
- psy-drag system (mouse-event DnD) preserved across all components
- enqueueAt() position-aware insertion preserved in QueuePanel
- updatePlaylist / smart-save / active-playlist tracking preserved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replay gain was not working because track objects were created manually without
including replay gain metadata from the Subsonic API response.
Changes:
- Add songToTrack() helper function to properly map SubsonicSong to Track with
replayGainTrackDb, replayGainAlbumDb, and replayGainPeak fields
- Add audio_update_replay_gain Tauri command for dynamic volume recalculation
when replay gain settings change mid-playback
- Add updateReplayGainForCurrentTrack() to recalculate volume when toggling
replay gain setting
- Fetch fresh track data on cold resume (app relaunch) to ensure replay gain
values are current from server
- Update all files that create track objects to use songToTrack()
Fixes issue where toggling replay gain ON/OFF or changing between track/album
mode had no effect on currently playing or newly played tracks.
- 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>