Commit Graph

279 Commits

Author SHA1 Message Date
cucadmuh 2dd993257a feat(player): build infinite queue using instant-mix strategy (#163)
Prefer artist-driven candidates (Top + Similar) when extending infinite queue, with random songs as a fallback when mix sources are empty.
2026-04-12 12:34:27 +02:00
Psychotoxical c2f7d6d495 chore(about): add missing contributor entries for v1.34.4 PRs
kilyabin: PR #148 (ru locale), #155 (Build a Mix hub), #156 (FS player perf + settings)
cucadmuh: PR #158 (Folder Browser keyboard nav + context menus)
kveld9: new contributor — PR #159 (Spanish translation), #160 (column sorting)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 12:11:17 +02:00
Psychotoxical 6a8422ad1f fix(artists): add hover effect to alphabet filter buttons
Buttons were built with inline styles — no :hover possible. Replaced with
.artists-alpha-btn CSS class: accent-colored hover with subtle glow ring,
active state unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 11:52:31 +02:00
Psychotoxical 8f45f7230e fix(statistics): accurate counts for large libraries + album-artist tooltip
Remove the 10-page cap in fetchStatisticsLibraryAggregates — the loop now runs until
the server returns a partial page, so albums/songs/playtime reflect the full library
regardless of size (previously capped at 5,000 albums). Switched sort type from
'newest' to 'alphabeticalByName' for a stable pagination order.

Add a tooltip on the Artists stat card explaining it shows album artists only (Subsonic
API limitation — track-level featured/guest artists without their own album are not
included). Tooltip added in all 8 locales. Labels with a tooltip get a dotted underline
and cursor:help as visual hint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 11:47:41 +02:00
cucadmuh ae2e1bcb97 feat(folder-browser): keyboard nav, context menus, now-playing path UX
Adds full keyboard navigation (arrow keys, Enter, Ctrl+Enter for context menu) across Folder Browser columns. Context menus for all row types with keyboard-operable submenus and star-rating control via arrow keys + Enter. Now-playing path is visually emphasized and stays stable; rebuilds on hotkey re-invoke or active-path follow-along. Adaptive column layout for deep trees with right-side visibility priority. New configurable 'Open Folder Browser' keybinding. StarRating animation sync for keyboard-driven changes. All 7 locales updated.
2026-04-12 11:27:26 +02:00
kilyabin 3d07a877f2 feat(fullscreen): performance fixes + appearance settings
Adds no_compositing_mode Tauri command; frontend adds html.no-compositing on Linux to replace GPU-only CSS effects (backdrop-filter, filter, mask-image) with software-friendly equivalents.

Settings → Appearance → Fullscreen Player: toggle for artist portrait visibility + 0–80% dimming slider.

Fix: long words in lyric lines now wrap correctly.
2026-04-12 11:26:44 +02:00
kilyabin bf38a286cd feat(nav): merge Random Mix & Albums into Build a Mix hub
Replaces two sidebar entries (Random Mix, Random Albums) with a single 'Build a Mix' item (Wand2 icon) at /random. Landing page shows two cards — Mix by Tracks and Mix by Albums — with glow-on-hover. Old routes updated throughout. All 7 locales updated.
2026-04-12 11:26:27 +02:00
Kveld. 8f18f73b33 feat(i18n): add Spanish (es) translation
Adds complete Spanish locale with 964 translated strings. Registered in i18n.ts, added to language dropdown in Settings. 8 languages total.
2026-04-12 11:25:54 +02:00
Kveld. 1a2352009b feat(tracklist): column-header sorting for albums & playlists
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.
2026-04-12 11:25:40 +02:00
Psychotoxical b9b8f3fc15 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>
2026-04-11 11:42:47 +02:00
Maxim Isaev b4c3124168 fix(player): minor hot-cache eviction, prefetch budget, and settings live size
Small fix for hot playback cache: eviction keeps current and next only;
prefetch up to five tracks when under cap, always fetch the immediate next;
grace for the previous current until debounce; run evict immediately on
MB or folder changes; re-read cap after download; optional Track.size;
live disk usage on Audio settings.
2026-04-11 03:29:59 +03:00
Psychotoxical 20dabbfd03 feat(lyrics): configurable sources with drag-to-reorder + feat(audio): ReplayGain pre-gain & fallback
Lyrics Sources:
- Replace lyricsServerFirst + enableNeteaselyrics toggles with a new
  Settings section — three sources (Server, LRCLIB, Netease) each
  individually toggled and drag-reorderable via psy-drop DnD
- useLyrics.ts iterates sources in user-defined order, skipping disabled
  ones; embedded SYLT from local files still wins unconditionally
- onRehydrateStorage migration from legacy fields on first load

ReplayGain Pre-Gain:
- New authStore fields: replayGainPreGainDb (0…+6 dB) and
  replayGainFallbackDb (-6…0 dB, untagged / radio)
- audio.rs compute_gain applies pre_gain_db and fallback_db
- Settings sliders under ReplayGain mode selector
- Radio HTML5 volume scaled by fallbackDb factor on playRadio

i18n(ru): incorporate PR #148 translation improvements (kilyabin)
All 7 locales updated for new keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 23:57:36 +02:00
Psychotoxical 003e7a3203 Merge pull request #148 from kilyabin/lang-ru-fix
i18n(ru): improvement of some translation strings
2026-04-10 23:56:10 +02:00
Psychotoxical c142e9e983 feat(radio): PLS/M3U playlist resolution + ICY metadata for playlist streams
Resolves PLS and M3U/M3U8 playlist URLs to their first direct stream URL
before playback and ICY metadata fetching. Stations configured with a
.pls or .m3u URL (e.g. SomaFM, schizoid.in) now play correctly and
report track metadata via ICY headers.

- Rust: parse_pls_stream_url / parse_m3u_stream_url helpers
- Rust: resolve_playlist_url (shared) + resolve_stream_url Tauri command
- fetch_icy_metadata: auto-resolves playlist URLs before connecting
- playerStore: playRadio() awaits resolve_stream_url before setting audio src

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 23:18:53 +02:00
Psychotoxical 549677ffd4 feat(random-mix): context-aware remix button + genre reset chip
Remix button now targets the active context: relabels to "{{genre}} neu
mixen" when a genre is selected and re-fetches that genre instead of the
global random pool. "Alle Songs" chip added as first genre option to
reset back to the full library mix without leaving the page.

Also fixes album-grid cards stretching to full page height on macOS
WebKit (align-items: start on .album-grid-wrap).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:19:22 +02:00
Psychotoxical dcc3e52ad1 fix(store): reset conflicting hot cache + preload state on rehydration
Users who had both hotCacheEnabled and preloadMode !== 'off' before
mutual exclusion was enforced will have both reset to off on next start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:50:52 +02:00
kilyabin 9986149e2c i18n(ru): improvement of some translation strings 2026-04-10 19:25:52 +04:00
Psychotoxical 1cc674e4ee fix(ui): icon updates, remove mic button, theme fixes
- LiveSearch: replace SlidersVertical with TextSearch for advanced search
- AlbumHeader: add Highlighter icon for artist bio button
- PlayerBar: remove unused lyrics/mic button
- components.css: album-detail-badge always opaque (accent bg, white text)
- theme: Middle Earth — remove sidebar stripes, fix queue/artist/bio contrast
- theme: Toy Tale — fix muted text, queue tabs, sidebar labels, divider
- theme: Tetrastack — brighten purple/blue palette, raise text-muted contrast
- theme: Horde & Alliance — remove repeating sidebar line pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:05:20 +02:00
Psychotoxical 41e98d5783 fix(lyrics): strip Netease metadata lines from LRC output
Filter lines matching 作词/作曲/编曲/制作人/etc. that Netease
embeds as timestamped LRC entries at the start of the song.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 15:13:35 +02:00
Psychotoxical 6ffcd6f6fa feat(lyrics): add Netease Cloud Music as opt-in fallback source
Netease is queried only when server and LRCLIB both return nothing,
preserving the existing lyrics chain completely. Off by default.

- Rust command `fetch_netease_lyrics` proxies Netease API (CORS bypass)
- `src/api/netease.ts` TypeScript wrapper via invoke
- `authStore.enableNeteaselyrics` toggle (default: false)
- `useLyrics`: Netease fires last in fallback chain when enabled
- Settings toggle in the Lyrics section
- `lyricsSourceNetease` label in LyricsPane
- i18n: all 7 languages (en, de, fr, nl, nb, ru, zh)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 15:11:06 +02:00
Psychotoxical 33a15fd17a chore(settings): add AudioMuse-AI PR #147 to cucadmuh's contributions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:51:14 +02:00
Psychotoxical 084543e59b fix(tracklist): split multi-artist tracks and fix reset button style
- 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>
2026-04-10 14:37:29 +02:00
Psychotoxical ccc9f2cae5 Merge pull request #147: feat(discovery): Navidrome AudioMuse-AI integration
Resolves conflict in ContextMenu.tsx: keep useShallow import from main
alongside getSimilarSongs added by this PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:09:13 +02:00
Psychotoxical f34bd7c0f1 fix(audio): separate gapless chain from preload gate and reduce black flash
- Gapless now always chains at 30s regardless of preloadMode ('off' no
  longer breaks gapless playback)
- Byte pre-download (audio_preload) skips when Hot Cache is active to
  avoid duplicate downloads
- When both Gapless and Preload are active, bytes are pre-fetched early
  and the chain reuses the cached data (separate bytePreloadingId guard)
- player-album-art-wrap gets background: var(--bg-card) so the brief
  opacity-0 loading state shows a themed colour instead of black

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:04:30 +02:00
Psychotoxical 8cb5eb9384 chore(settings): remove alpha badges from Hot Cache and Hi-Res sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:45:56 +02:00
Psychotoxical c49f6af38b perf: reduce CPU usage and unify next-track buffering settings
- Throttle audio:progress from 100ms to 500ms; WaveformSeek and
  FsSeekbar use imperative DOM updates instead of React re-renders
- Fix all usePlayerStore() calls without selectors across pages and
  components (useShallow / individual selectors throughout)
- Remove filter:blur and transform:scale from Hero, AlbumDetail and
  FullscreenPlayer backgrounds — eliminated expensive software
  compositing layers on WebKitGTK
- Replace translate3d with 2D translate in FS mesh and portrait
  animations; remove will-change:transform from mesh blobs
- Move Hot Cache section from Storage tab to Audio tab; group Preload
  and Hot Cache under a shared 'Next Track Buffering' section with a
  mutual-exclusivity note and auto-disable logic
- Add 'off' toggle to Preload (replaces Off button with toggle switch);
  enabling either method now automatically disables the other

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:38:33 +02:00
Maxim Isaev 69c0c5a907 merge: integrate origin/main into feat/audiomuse-navidrome 2026-04-10 13:04:34 +03:00
Maxim Isaev 4de577eede feat(navidrome): Instant Mix probe, ping identity, and AudioMuse UX
Parse type and serverVersion from Subsonic ping; persist per-server identity and
run a background Instant Mix probe (random songs + getSimilarSongs) for
Navidrome 0.60+. Hide the AudioMuse server toggle when the probe returns no
similar tracks; clear prefs when the server is no longer eligible.

Artist pages fall back to Last.fm similar artists when AudioMuse is enabled but
the server returns none. Instant Mix failures set a per-server issue flag with a
settings warning and toast. Settings description links the official plugin repo
via i18n Trans.
2026-04-10 13:02:46 +03:00
Psychotoxical c9dfbcc19f chore(settings): reset uiScale to 1.0 on startup while scaling is disabled
Users who had previously set a custom scale are silently reset to 100%
on next launch so they are not stuck at a broken zoom level. The stored
value is cleared in fontStore so the slider starts at 100% once the
feature is re-enabled.

Also credits nisrael for the ICY / AzuraCast PR #146.

TODO: remove the reset effect and re-enable the slider in Settings.tsx
when UI scaling is properly reworked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:17:43 +02:00
Maxim Isaev 36f3d42dbe feat(discovery): Navidrome AudioMuse-AI client integration and library scoping
Add per-server toggle for AudioMuse-style discovery: Instant Mix via
getSimilarSongs, server similar artists instead of Last.fm on artist pages,
and higher similar-artist count in Now Playing when enabled.

When browsing a single music folder, filter similar/top song results by
album ids from that scope (Navidrome does not apply musicFolderId to those
endpoints). Request larger similar batches under a narrow scope.

Keep radio-from-track as two blocks (shuffled top songs, then shuffled
similar-by-artist) so it differs from Instant Mix. Show Alpha badge beside
the setting. Add research/ to .gitignore.
2026-04-10 12:07:32 +03:00
Psychotoxical 49f7fe5f6e feat: add ICY metadata and AzuraCast radio streaming support (#146)
feat: add ICY metadata and AzuraCast radio streaming support
2026-04-10 11:05:59 +02:00
Psychotoxical 28943f1ecb chore(settings): disable UI scale slider pending rework
Interface scaling is temporarily disabled in the Settings UI
(opacity + pointer-events: none) with a note that it will return
in a future update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:04:21 +02:00
Nils Israel 46cefb5712 feat: add ICY metadata and AzuraCast radio streaming support
Agent-Logs-Url: https://github.com/nisrael/psysonic/sessions/88faada5-28bb-446f-b53b-46a0efef387e

Co-authored-by: GitHub Copilot <198982749+copilot@users.noreply.github.com>
Signed-off-by: Nils Israel <nils@sxda.io>
2026-04-10 10:38:59 +02:00
Psychotoxical 74985fe331 fix(audio): fall back to track gain when album gain is missing
In album gain mode, tracks without albumGain tags received no
ReplayGain adjustment at all. Now falls back to trackGain before
defaulting to 0 dB — fixes inconsistent loudness across albums
where only some tracks have albumGain metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 01:12:06 +02:00
Maxim Isaev d2592839b0 i18n(ru): use «Популярное» for most-played nav, home, and page title
«Чаще всего» read awkwardly; «Популярное» matches the play-count sense
without sounding like a grammar exercise.
2026-04-10 02:06:24 +03:00
Maxim Isaev bbeb2f661e merge: integrate origin/main into fix/statistics-i18n-small-fixes 2026-04-10 01:42:14 +03:00
Psychotoxical 5f0fb5dcbd feat(audio): auto-switch to new audio output device at runtime
Adds a background device-watcher that polls the OS default output device
every 3 s via CPAL. When the device changes (Bluetooth headphones connecting,
USB DAC plugging in, etc.) the stream is reopened on the new device, the old
Sink is dropped (it was bound to the now-closed OutputStream), and
audio:device-changed is emitted to the frontend.

Frontend handler (TauriEventBridge): if playing, restarts the current track
from the saved position; if paused, clears the warm-pause flag so the next
resume uses the cold path (audio_play + seek) which creates a new Sink on
the new device.

Fixes #143 (audio through speakers after connecting Bluetooth headphones).
Also covers the intermittently reported single-channel output after long idle,
which can be caused by the OS reconfiguring the audio device in the background.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:40:12 +02:00
Maxim Isaev 6f6cb0fd6b fix(statistics): cache Subsonic stat fetches and localize duration units
Add 7-minute TTL caches (same idea as rating prefetch) for overview
album strips, random-song format sample, and paginated library
aggregates; key by server and music folder.

Introduce formatHumanHoursMinutes with common.duration* strings in all
locales for Statistics and playlist duration labels.

Refresh RU/ZH statistics and nav wording; fix zh playtime label.
2026-04-10 01:39:08 +03:00
Psychotoxical 15dc970f53 fix(audio): restore msg binding in DecodeError arm, use SlidersVertical for EQ icon
- Revert _msg → msg in DecodeError match arm; the variable is used in the
  debug_assertions eprintln! block so the underscore prefix caused a compile error
- Replace SlidersHorizontal with SlidersVertical (lucide-react) in PlayerBar,
  LiveSearch, and AdvancedSearch so the EQ/filter icon shows vertical bars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:28:12 +02:00
Maxim Isaev fc40d235d0 fix(statistics): scope genre insights to selected music library
getGenres() ignores musicFolderId. Derive genre counts from the same
paginated getAlbumList('newest') scan used for playtime and totals,
using each album's genre and songCount.
2026-04-10 01:18:44 +03:00
Psychotoxical 1ba8619a37 chore: remove preview-update debug button, add GDI features, add contributor
- Remove debug "Preview Update Modal" button from Settings/About
- Add Win32_Graphics/Win32_Graphics_Gdi features for taskbar GDI icons
- Add sorensiimSalling contributor credit for Russian translation (PR #140)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 22:30:23 +02:00
kilyabin 973b472c1f fix: not translated lyricsServer* string to RU locale (#140) 2026-04-09 22:24:03 +02:00
Psychotoxical 20bf93c344 feat(windows): taskbar thumbnail toolbar with GDI media icons
Implements ITaskbarList3::ThumbBarAddButtons for Prev/Play-Pause/Next
buttons in the Windows taskbar thumbnail preview. Icons are drawn at
runtime via GDI (no binary assets). WndProc subclass intercepts
THBN_CLICKED and emits the same media:* events as souvlaki/tray.
update_taskbar_icon command swaps Play↔Pause icon on state change.
Frontend syncs via playerStore subscribe alongside mpris_set_playback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 22:12:35 +02:00
Psychotoxical a78c0fe9ac fix: font switching, layout scaling, embedded lyrics, folder opening
- fix(fonts): update CSS var declarations to @fontsource-variable naming
  ('Inter Variable', 'Outfit Variable', etc.) so dynamic font switching works
- fix(layout): 100vh → 100% on .app-shell to fix Windows WebView2 playerbar drift;
  1fr → minmax(0,1fr) in all grid-template-rows + remove min-height: 720px to fix
  Linux playerbar disappearing at high zoom or small window sizes
- fix(updater): replace shell open() with Rust open_folder command to bypass
  shell:allow-open capability scope blocking local paths on Windows
- fix(lyrics): add get_embedded_lyrics Tauri command (id3 crate for MP3 SYLT/USLT,
  lofty for FLAC SYNCEDLYRICS/LYRICS); fix parseLrc regex for LRC without fractional
  seconds; fix SubsonicStructuredLyrics to accept both synced and issynced fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:52:27 +02:00
Psychotoxical 9c57d4f887 feat(updater): professional update modal with skip, changelog, and OS-aware downloader
Replaces the small corner toast with a centered modal that appears on
startup when a newer GitHub release is detected (4 s delay).

Features:
- Skip this Version: stores skipped tag in localStorage, reappears only
  for newer releases
- Collapsible changelog: renders GitHub release body as markdown accordion
- OS-aware download: Windows → .exe installer, macOS → .dmg (aarch64
  preferred), Linux Arch → AUR hint (yay/pacman), Linux other → .AppImage/.deb
- In-app downloader: Rust download_update command streams to ~/Downloads/,
  emits update:download:progress every 250 ms for a real-time progress bar
- Post-download: Show in Folder button opens Downloads dir via shell.open
- Buttons: Download Now / Skip this Version / Remind me Later

Rust: check_arch_linux() reads /etc/arch-release + /etc/os-release
platform.ts: adds IS_MACOS, IS_WINDOWS alongside existing IS_LINUX
Settings About: Preview Update Modal button for testing
Fixes: renderInline regex had nested capture group causing undefined entries
in split() result → TypeError → React crash → WebKit white-screen freeze

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:23:48 +02:00
Psychotoxical 5d067b1f8b fix(app): hide resize grips in native fullscreen on all platforms
Previously onResized only tracked fullscreen on Linux. Now all platforms
set isWindowFullscreen, and an initial check on mount catches windows
that start maximized/fullscreened. CSS: .app-shell[data-fullscreen] .resizer
{ display: none } added in layout.css (shipped with updater commit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:23:48 +02:00
Psychotoxical 48d0145dc8 fix(albums): align year filter inputs to button height and font size
Input padding and font-size now match adjacent .btn elements so the
year filter row no longer has mixed heights.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:23:48 +02:00
Psychotoxical 0da92c2fa1 fix(radio): prevent macOS WKWebView crash + shuffle Artist Radio queue
- Remove currentTime from Zustand persist partialize: it caused 100ms
  localStorage writes that grew with the queue, crashing WKWebView SQLite
  after ~10 min of Artist Radio on macOS
- Trim old played radio tracks from queue (keep last 5) to cap localStorage
  payload size during proactive radio top-up in next()
- Add reconnect logic for internet radio stall events (max 5 retries)
- Shuffle initial Artist Radio queue via Fisher-Yates so positions 2+
  are drawn from similar-artist tracks, not predictable top-5 tracks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:23:48 +02:00
Psychotoxical 4e3efa3814 feat: self-host fonts via @fontsource-variable, remove Google Fonts CDN
Replaces all Google Fonts CDN references (index.html preconnect/stylesheet
and theme.css @import) with local @fontsource-variable npm packages.
All 10 UI fonts now bundle as WOFF2 into dist/assets/ — app renders
correctly without any internet connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:23:48 +02:00
Psychotoxical 77085a544e chore(credits): add missing contributor PRs to Settings page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:23:48 +02:00