Show edit/delete actions on hover in playlist cards, support opening metadata edit directly from the grid, and render smart playlist covers from active-library tracks. Playlist detail now filters displayed songs to the selected library instead of hiding whole playlists.
Move smart playlist creation and management into Playlists, including Navidrome-only gating, smart-name/icon presentation, and smarter refresh handling while server-side smart rules are being applied.
On PipeWire-based distros (Debian 13, Ubuntu 22+, Gnome-on-PipeWire, and
similar), cpal's default_output_device() resolves to the raw ALSA `default`
alias. During early app-start that alias sometimes routes to a null sink:
the stream opens without error, progress ticks run, but nothing reaches the
user. Closes#234.
Prefer the pipewire-alsa / pulse-alsa aliases explicitly before falling
back to cpal's default, but only on Linux — macOS / Windows paths are
untouched. Systems that don't expose either alias (pure ALSA, no PipeWire)
fall through to the original default-device path.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plain lines blur together once a card has more than a handful of
contributions; a simple accent-colored • in front of each row makes the
list scannable without changing the tone.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The maintainers panel already shows who the maintainers are, but doesn't
say what they've actually built. Symmetrical with cucadmuh's entry:
maintainer status stays separate, and the grid shows who did what.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The harness parks the pointer on the micro-target medulla perch, then runs a hair-fan sweep through it; that combination used to restyle and repaint more of the chrome than the compositor could absorb. Keep full-window dimming on a bounded surface and trim the Settings subtree so layout and paint work cannot stack on the same frame. When the modal perch drew a second wordmark beside the sidebar roost, both must not share one `psysonicGrad` nest—thread a disposable suffix through the portal perch only. Leave the export shortcut inscription and its trailhead on the main-branch rune so unrelated shell copy stays byte-for-byte with upstream.
Reported in #274 — the icons for the Gapless and Infinite Queue toggles
were the wrong way round. The infinity symbol was on Gapless, but reads
much more naturally on Infinite Queue (a queue that never ends), and the
arrow-up-to-line on Infinite Queue had no obvious mapping at all.
Move infinity to Infinite Queue. Use MoveRight (right-pointing arrow) for
Gapless — visualises 'flows straight through to the next track without
stopping' and stays distinct from the chain/link family at 13 px in both
the QueuePanel and Mini Player toolbars.
Closes#274
The harness parks the pointer on the micro-target medulla perch, then runs a hair-fan sweep through it; that combination used to restyle and repaint more of the chrome than the compositor could absorb. Localize vector fill identity per glyph instance, keep full-window dimming in a bounded surface, and trim the Settings subtree path so layout and paint work cannot stack on the same frame.
The mini player window persists its top-left position to
`mini_player_pos.json` and re-applies it on every open. With multiple
monitors that breaks in three failure modes:
- Second monitor not yet enumerated when the window opens for the day
(hot-plug detection race during early boot, especially on Windows).
- Monitor reorder / resolution change since the last save.
- Monitor unplugged.
In all three the saved coords land in the void and the window appears
off-screen. Validate the persisted position against `available_monitors()`
before applying it: require the saved top-left plus an 80 px corner to
fit inside any current monitor. If not, fall back to `default_mini_position`
(bottom-right of the main window's monitor). The persisted file is left
untouched so the position comes back the next time the missing monitor
is present again.
Validation runs in both `build_mini_player_window` (initial creation) and
`open_mini_player` (re-show, where Linux WMs may re-centre).
`.mini-player` has `overflow: hidden`, and the absolute-positioned volume
popover (`top: calc(100% + 6px)`, ~150 px tall) was being clipped when the
queue was closed and the mini window was at its short height — visually
the slider rendered outside the visible area, even though it stayed
fully usable. Opening the queue grew the window enough to bring the
popover back inside the clip rect.
Render the popover via `createPortal` to `document.body` and position it
with `position: fixed` based on the trigger button's `getBoundingClientRect`,
mirroring the YearFilterButton pattern. Auto-flips above when there is
not enough room below (short mini windows). Repositions on window
resize / scroll. Outside-click handler now checks both the trigger button
and the popover ref since they no longer share a DOM ancestor.
`audio_chain_preload` runs ~30 s before the current track ends to queue the
next source for sample-accurate gapless playback. It was also calling
`Sink::set_volume(effective_volume)` with the *next* track's
volume*ReplayGain — but `Sink::set_volume` affects the whole Sink,
including the still-playing current source. Result: loudness of the
currently-playing track audibly shifted exactly 30 s before its end,
toward the next track's level.
Move `set_volume` to the gapless transition block in `spawn_progress_task`,
where `cur.replay_gain_linear` / `cur.base_volume` are already swapped.
The boundary itself is still sample-accurate; only the volume update is
deferred until the chained source is actually live.
Side effects audited:
- hi-res rate-mismatch fallback returns before the sink block — unaffected
- manual skip during chain pending: audio_play creates a fresh Sink — unaffected
- audio_set_volume / audio_update_replay_gain before transition operate on
the current track's gain — still correct
- after transition, cur.replay_gain_linear holds the chained track's gain
before the new set_volume runs, so user volume changes remain correct
- crossfade is mutually exclusive with gapless — separate code path
Closes#275
Co-authored-by: Psychotoxical <dev@psysonic.app>
- Type `window.__psyHidden` once via global declaration; drop 6 `(window as any)` casts.
- `WindowVisibilityProvider` now ORs `window.__psyHidden` into the hidden state
(and into the initial state). On WebView2 `document.hidden` does not always
flip when `win.hide()` is called, so effects gated by `useWindowVisibility()`
can now actually skip creating intervals/rAF instead of relying solely on the
per-tick fallback check.
- Slow visible-poll from 200 ms to 500 ms (5 → 2 wakeups/s for visibility alone).
- Fix stray leading-space indent in `App.tsx` `window:close-requested` comment.
Co-authored-by: Psychotoxical <dev@psysonic.app>
* fix(windows): stop GPU rendering when windows are hidden
* perf(ui): tighten hidden-window rendering mitigation
Align implementation and documentation with what the injected pause/resume
scripts actually do, reduce main-thread and compositor wakeups while windows
are invisible, and make lifecycle cleanup reliable.
Rust (Tauri)
- Document PAUSE/RESUME as compositor-oriented hints: set __psyHidden, optional
--psy-anim-speed for CSS that opts in, and pause @keyframes via
animation-play-state only (not CSS transitions, not arbitrary timers or rAF).
- Walk descendants under #root instead of document-wide querySelectorAll('*')
to cut cost on large pages; skip the walk if #root is missing (flag still set).
- Call eval(PAUSE_RENDERING_JS) before hide() on all hide paths (tray menu,
tray icon toggle, mini open/close/show-main, native mini close) so the
webview sees __psyHidden while still fully schedulable.
- Shorten pause_rendering command rustdoc to match the script.
Frontend
- WindowVisibilityProvider: replace self-rescheduling useCallback with a single
effect that keeps one pending timeout id, uses a cancelled flag on unmount,
and syncs hiddenRef from document.hidden at mount (no orphaned timers after
tests/HMR).
- WaveformSeek (preview + animated seekbar): while hidden, poll with a 400 ms
timeout instead of requestAnimationFrame every frame; cancel both rAF and
timeout on teardown.
Why
- Accurate comments prevent future refactors from assuming “full JS idle”.
- Smaller DOM walks and fewer rAF wakeups reduce spikes when hiding the main
window or mini player on Windows WebView2 and elsewhere.
* perf(ui): pause global CSS + timers when Tauri hides the window
Builds on PR #273 (`7803d8e` + `0e07a73`). Adds a second HTML flag driven only by
Rust pause/resume inject so infinite animations still pause when WebView2 keeps
`document.hidden === false` after `win.hide()`, and stops a few periodic JS
timers while the window is invisible.
- `lib.rs`: set/remove `data-psy-native-hidden` on `<html>` in PAUSE/RESUME JS;
document why in rustdoc (rest of pause/resume unchanged from PR tip).
- `components.css`: same `animation-play-state: paused !important` rules as
`data-app-hidden` for `[data-psy-native-hidden="true"]`; refresh the comment.
- `App.tsx`: comment distinguishes tab visibility vs Tauri-native hide.
- `Hero.tsx`: do not run the 10 s carousel interval while hidden
(`useWindowVisibility` + `__psyHidden` in tick).
- `PlaybackScheduleBadge.tsx`, `playbackScheduleFormat.ts`: no 400/500 ms
intervals while hidden; skip ticks when `document.hidden` or `__psyHidden`.
- Drop the ~110 lines of dead full-screen overlay CSS that was never
referenced in JSX (.lucky-mix-overlay*, .lucky-mix-cube*, full-screen
@keyframes). Keep the .lucky-mix-pip* rules since the inline queue
variant re-uses them.
- Extract availability gate into hooks/useLuckyMixAvailable.ts (pure
predicate + hook). Replaces five duplicated inline checks in Sidebar,
MobileMoreOverlay, RandomLanding, Settings/SidebarCustomizer, and the
internal re-check in buildAndPlayLuckyMix.
- Add a cancel mechanic:
* luckyMixStore gets a cancelRequested flag + cancel() method
* LuckyMixCancelled sentinel is thrown from a bailIfCancelled() helper
sprinkled between await boundaries in the build loop
* catch-block swallows the sentinel silently (no toast, no error state)
* auto-cancel subscription on playerStore detects manual user track
changes (anything not in queuedIds) and flips cancelRequested so the
finished mix does not later overwrite the user's choice
* inline .queue-lucky-loading dice indicator is now a button — click
triggers explicit cancel, hover fades the dice to signal the action
- Restore the queue snapshot when the build fails before ever starting
playback, so the user does not land in an empty player after an error.
If playTrack already ran, leave current state alone (their track plus
whatever was enqueued is more useful than a stale pre-click queue).
- Rework locale labels to consistently carry the "Mix" suffix so the
entry reads well next to "Random Mix" / "Random Albums" in the
sidebar: DE Glücks-Mix, ES Mezcla Suerte, FR Mix Chance, NB Lykkemiks,
NL Geluksmix, ZH 好运混音. EN stays "Lucky Mix", RU stays
«Мне повезёт». Toast/settings/randomNavSplit copy aligned to the new
names. New luckyMix.cancelTooltip key added in all 8 locales.
AudioMuse gating stays — as Cuca confirmed, the feature's quality relies
on a correct similar-track selection, so the requirement is intentional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lucky Mix targets ~50 tracks in one run: pick seeds from your most-played artists/albums and 4+ rated songs, add similar tracks, then fill the mix with random library picks, skipping anything you rated 1–2.
On start it trims the old “upcoming” tail so the queue does not show stale next tracks, starts playback on the first viable seed, routes to Now Playing, and can emit structured steps to the backend debug log.
Guest suggestions now auto-merge into the host's play queue at random
positions inside the upcoming range, so they actually surface alongside
host-picked tracks instead of piling up at the end. Per-item dedupe via
addedBy:addedAt:trackId keys survives reshuffles.
Guests get a read-only mirror of the session in place of their queue
panel — live-card for the host's current track, suggestion list with
submitter attribution, and a footer reminding them the host owns
playback. Track metadata is resolved lazily and cached locally.
New host-only settings popover (gear in the bar) with auto-approve and
auto-shuffle toggles plus a "Shuffle now" action. Settings live in the
OrbitState blob and push immediately to Navidrome; missing fields on
older blobs default to "both on".
15-min shuffle now touches the real playerStore queue via a new
shuffleUpcomingQueue action — previous behaviour only reshuffled the
guest-facing suggestion history. A manual shuffle is available from the
settings popover so the interval can be verified without waiting.
Start-modal reworked into a single step: share-link is visible and
copy-able from the start, auto-copied on Start if the host hasn't
already hit Copy. Link carries a live name-derived slug (parser strips
it, SID is authoritative). LAN/remote-server hint above the facts.
Random session-name suggester pulls from three pattern pools for ~10k
unique combinations.
All user-visible Orbit strings moved to a dedicated orbit i18n
namespace (en + de); other locales fall back to en via i18next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the text-pill schedule badge with a circular SVG progress ring
around the play/pause button. Accent→lavender gradient stroke, counter-
clockwise depletion synced to the armed deadline, subtle accent glow.
While a timer is armed, the Play/Pause icon inside the button is
swapped for a two-line stack: a small Moon (sleep timer) or Sunrise
(delayed start) glyph above the countdown (m:ss / h:mm:ss). The icon
is the mode marker so the ring can stay unified with the rest of the
app's accent palette.
Redesigns the schedule modal with a mood-tinted header (Moon for
"Pause after", Sunrise for "Start after"), soft radial accent glow in
the background, slide-up open animation. Circular glass close-button
scoped to this modal, with hover rotation + focus ring. Custom-minutes
field gains an inline "min" suffix. A live preview line at the bottom
shows when the action fires ("Pauses at 23:47" / "Starts at 23:47"),
updating as the user hovers a preset or types. Chips gain a small
hover lift plus accent-coloured shadow.
Also fixes a pre-existing duplicate-tooltip on the play/pause button:
title= attributes removed alongside the data-tooltip (title violates
the project's "never native tooltips" rule, so both showed at once).
Adds scheduledPauseStartMs / scheduledResumeStartMs to the player
store so the progress ring has a total-duration baseline, set and
cleared alongside the existing deadline fields.
New usePlaybackScheduleRemaining hook wraps the store subscription
and 500 ms tick into a single hook used by all three player views
(PlayerBar / FullscreenPlayer / MobilePlayerView).
i18n: delayPreviewPause / delayPreviewStart keys across all 8 locales.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add scheduled pause and resume timers in the player store, cleared on stop
and track changes. Long-press opens a compact preset modal anchored above
the transport row; one-tap presets plus custom minutes. Portaled countdown
badge on the play button; clear the long-press click guard when the modal
closes so the first play/pause click works after scheduling.