Commit Graph

1473 Commits

Author SHA1 Message Date
Psychotoxical e945da693a refactor(settings): co-locate settings feature into features/settings
Theme infra (fixedThemes, theme utils), music-network presetIcon, audio-device
probe, library-index sync, and the auth*SettingsActions slices stay in the
core/global layer (consumed via @/ alias) to avoid inverting core->feature.
2026-06-29 23:48:19 +02:00
Psychotoxical fbc37db64e refactor(search): co-locate search feature into features/search 2026-06-29 23:42:22 +02:00
Psychotoxical 931c47e19e refactor(nowPlaying): co-locate now playing feature into features/nowPlaying 2026-06-29 23:38:09 +02:00
Psychotoxical e0e10e0034 refactor(sidebar): co-locate sidebar feature into features/sidebar 2026-06-29 23:26:00 +02:00
Psychotoxical e6269c7b85 refactor(miniPlayer): co-locate mini player feature into features/miniPlayer 2026-06-29 23:22:00 +02:00
Psychotoxical 6dd0d4a130 refactor(fullscreenPlayer): co-locate fullscreen player feature into features/fullscreenPlayer 2026-06-29 23:17:14 +02:00
Psychotoxical 896fe3f407 refactor(radio): co-locate internet radio feature into features/radio 2026-06-29 23:04:34 +02:00
Psychotoxical dbffe59e58 refactor(favorites): co-locate favorites feature into features/favorites 2026-06-29 22:59:21 +02:00
Psychotoxical ea3e3f1adf refactor(deviceSync): co-locate device sync feature into features/deviceSync 2026-06-29 22:55:35 +02:00
Psychotoxical 236acc33dd refactor(stats): co-locate statistics feature into features/stats 2026-06-29 22:51:54 +02:00
Psychotoxical 431d65b587 refactor(ui): co-locate CustomSelect as a shared primitive in src/ui 2026-06-29 22:36:43 +02:00
Psychotoxical 53b244de1f refactor(ui): co-locate CachedImage as a shared primitive in src/ui 2026-06-29 22:36:12 +02:00
Psychotoxical bd2d669b99 refactor(ui): co-locate OverlayScrollArea as a shared primitive in src/ui 2026-06-29 22:35:41 +02:00
Psychotoxical 146727e980 refactor(ui): co-locate TooltipPortal and tooltipAttrs as shared primitives in src/ui 2026-06-29 22:35:14 +02:00
Psychotoxical a18f8fc857 refactor(ui): co-locate BackToTopButton as a shared primitive in src/ui 2026-06-29 22:34:34 +02:00
Psychotoxical 6d7464fddf refactor(waveform): co-locate seekbar feature into features/waveform 2026-06-29 22:30:53 +02:00
Psychotoxical d9f8d01be9 build(restructure): wire @/ alias into vite dev/build resolver 2026-06-29 22:15:57 +02:00
Psychotoxical e66b674937 Revise README for feature enhancements and Polish language (#1221)
Updated README.md to reflect new features and language support.
2026-06-29 20:20:13 +02:00
Psychotoxical e1ff4385d6 feat(settings): add "Square Corners" appearance toggle (#1215)
* feat(settings): add "Square Corners" appearance toggle

A Display toggle in Settings → Appearance → Visual Options that overrides
the active theme to render cards and cover art with square (non-rounded)
corners, for a sharper, boxy look.

Covered surfaces: grid cards (album/playlist/artist/song/because) and
their covers, detail-header heros (album/playlist/tracks), the Now
Playing / Radio view, the fullscreen player, the cover lightbox, the
queue panel cover, and the mini player.

Persisted in themeStore and driven by an `html[data-square-corners]`
attribute set in App.tsx — applied in both the main and mini-player
webviews (the mini player rehydrates the theme store on the shared
`storage` event). Strings added for all 12 locales.

* docs(changelog): add Square Corners to 1.50.0 CHANGELOG and What's New
2026-06-29 06:48:26 +02:00
Psychotoxical 4c102d88a5 chore(aur): bump PKGBUILD to v1.49.0 (#1214) 2026-06-29 03:09:23 +02:00
github-actions[bot] c9fa69a0ad chore(release): bump main to 1.50.0-dev (#1212)
* chore(release): bump main to 1.50.0-dev

* chore(nix): sync npmDepsHash with package-lock.json

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 02:47:22 +02:00
cucadmuh 6b1a898151 docs(release): sync 1.49.0 CHANGELOG, What's New, and credits (#1211)
Set the 1.49.0 release date, fix Added PR order for Polish (#1185),
refresh WHATS_NEW highlights for missing user-facing fixes, and credit
ImAsra for WinGet release automation (#1077).
2026-06-29 02:33:40 +03:00
Psychotoxical 19860672eb Update TELEMETRY.md (#1210)
added fanart to external services
2026-06-28 23:20:22 +02:00
Psychotoxical 2b80a87bfc refactor(settings): shared id-based reorder for the customizer panels (#1207)
Extract the drag-to-reorder boilerplate the sidebar, artist-layout, lyrics,
queue-toolbar and servers customizers each hand-rolled into a single
`useListReorderDnd` hook plus an id-based `applyListReorderById`, and migrate
all five onto it. Reorders resolve by stable item id, never positional index,
so a render filter can never share an index space with the reorder and desync
it (the class behind #1164). No user-facing change.

- New: useListReorderDnd, applyListReorderById (+ tests), ReorderGripHandle.
- applySidebarReorderById now builds on the shared core; section/conserved
  guards stay sidebar-specific.
- Queue/mini-queue/playlist reorder stay backend-index based (out of scope).
2026-06-28 16:17:31 +02:00
Psychotoxical fcdb58e0d8 fix(sidebar): reorder sidebar nav items by id to stop drag drift (#1206)
* fix(sidebar): reorder nav items by id instead of positional index

The Settings sidebar customizer rendered library rows with one filter and
reordered them with another, so a hidden/gated item (luckyMix when AudioMuse
is unavailable) made the rendered list shorter than the reorder list and drag
indices landed one slot off — rows jumped back on drop. Replace the index-based
reorder with an id-based primitive shared by the customizer and the in-sidebar
long-press DnD, so a render filter can no longer desync the move. Unknown,
cross-section, conserved, or no-op drops are rejected.

* docs(changelog): note sidebar reorder fix (#1206)
2026-06-28 15:42:35 +02:00
Psychotoxical 184501744b fix(cover): restore full-resolution album and artist covers (#1205)
* fix(cover): build cover tiers from the full-resolution source

Derive the larger tiers from the decoded download instead of re-reading the just-written smaller tier (resize never upscales, so 512/800 were stored at the small resolution). Resolve the full-res 2000 tier exactly so it is actually downloaded and cached. Bump the cache layout stamp to drop already-poisoned tiers.

* fix(cover): open the cover lightbox at full resolution

Require the exact requested tier for the full-res helper so a smaller warmed tier no longer pins the lightbox to a downscaled image. Race the 2000 fetch against a 500ms opening window, show the 800 tier meanwhile, and persist 2000 for the next open. Adds an opening animation (reduced-motion aware) and tests.

* docs(changelog): note full-resolution cover fix (#1205)

* fix(cover): keep full-res peek exact in peek_batch and the grid seeder

Review follow-up: ensure-path peek alone left a hole — cover_cache_peek_batch still laddered a 2000 request down to a smaller tier, and the in-memory grid seeder wrote that smaller path under the 2000 key, so Hero/fullscreen/lightbox surfaces (which peek 2000 before ensure) still showed a downscaled cover. Share one exact-2000 rule (peek_plain_cover_tier) across ensure and peek_batch, and never seed the full-res key from a smaller tier file.
2026-06-28 04:23:12 +02:00
cucadmuh 281e86fd3b fix(queue): persist timeline play history across queue replace (#1096) (#1204)
* fix(queue): persist timeline play history across queue replace (#1096)

Add a session-scoped play-history buffer (with play_session cold bootstrap)
and timeline UI that shows history + current + upcoming without mutating
the canonical queue or Subsonic sync.

* fix(queue): pin timeline current to top and replay history in-place

Timeline scroll matches queue mode (current at top). History clicks insert
after the playing track instead of replacing the queue, and replayed tracks
stay visible in the history strip.

* docs: add CHANGELOG and credits for timeline play history (PR #1204)

* fix(queue): resolve cross-server cover art for timeline history

Include album/cover ids in play_session bootstrap rows, prefetch history
refs through the queue resolver per server, and resolve before replay so
Now Playing artwork works for inactive-server tracks.

* fix(now-playing): stop playbackReport on cross-server track switch

Send stopped to the previous server's playbackReport session when the
playback server changes (queue click, history replay, etc.) so Who is
listening clears on the server that was showing the prior track.

* fix(queue): close timeline history review gaps for PR #1204

Defer play_session bootstrap until the library index is ready with retry
while timeline mode is active; resolve history and queue rows by serverId
+ trackId for mixed-server queues; add tests for bootstrap defer and ref lookup.

* chore(queue): remove dead timeline scroll guard in QueueList

Timeline scroll is handled in the virtual-rows effect; the legacy branch
is queue/playlist only.

* fix(queue): address PR review nits for timeline play history

Use authoritative row.ref.serverId for history clicks before resolver fill;
simplify empty bootstrap seed; tighten completion types; unify recent_plays SQL.

* fix(queue): immutable session history append for useSyncExternalStore

Replace in-place push with a fresh array so getSnapshot returns a new
reference and React re-renders on live appends without a playerStore update.
2026-06-28 02:08:02 +00:00
Psychotoxical 979eb85ad1 docs(whats-new): refresh the 1.49.0 release highlights (#1203) 2026-06-27 22:36:16 +02:00
Psychotoxical 604cdd54d6 feat(updater): show the WinGet update command in the Windows update modal (#1202)
* feat(updater): show WinGet update command in the Windows update modal

* docs(changelog): WinGet update command in the Windows update dialog
2026-06-27 15:56:54 +02:00
Psychotoxical 6d4d82d6a3 fix(playlist): stop the detail page reloading on Play/Shuffle/Enqueue (#1201)
* fix(playlist): stop the detail page reloading on Play/Shuffle/Enqueue

* docs(changelog): playlist play no longer reloads the page
2026-06-27 15:24:28 +02:00
Psychotoxical 7e6a2100e5 fix(updater): hold Windows update notice during WinGet moderation window (#1200)
* fix(updater): hold Windows update notice during WinGet moderation window

* docs(changelog): Windows update notice WinGet moderation delay
2026-06-27 14:50:37 +02:00
Psychotoxical ed02ba4e1d feat(titlebar): theme-colored window bar on macOS (#1199)
* feat(titlebar): theme-colored window bar on macOS

macOS showed the grey native title bar that ignores the app theme
(#1198). Enable titleBarStyle: Overlay so the webview reaches the top
edge and the native traffic lights float over the existing in-page
title bar, which already paints with the active theme.

Reuses the Linux title-bar grid: the bar is shown on macOS too (hidden
in native fullscreen), without the custom window buttons since the
traffic lights stay native. The GTK resize grips are now Linux-only.
Renamed the stylesheet to drop the misleading linux-only name.

* docs(changelog): macOS themed title bar

* fix(titlebar): crisp macOS bar text — drop transform layer and shadow

* fix(titlebar): drop now-playing text from the macOS bar
2026-06-27 14:17:55 +02:00
ImAsra c609beddfa add GitHub Action to automate WinGet package updates (#1077)
* Add GitHub Actions workflow for WinGet publishing

* Delete .github/workflows/winget-publish.yml

* Add GitHub Actions workflow for WinGet publishing

* Update winget-publish workflow to derive version
2026-06-27 13:38:46 +03:00
Psychotoxical d70060923b refactor(settings): interface scale as a segmented picker (#1197)
Replace the Interface Scale zoom slider and its tick row with the shared
SettingsSegmented control — one button per preset (80–150%), matching the
Lyrics, Queue and Track-transitions sections. Drops the slider/tick
alignment issue entirely. Legacy off-preset values snap to the nearest
preset so one button is always active. Removes the now-dead slider CSS.
2026-06-27 02:11:09 +02:00
Psychotoxical d5ad275851 fix(hero): mainstage backdrop falls back to the album cover, reorder defaults (#1196)
* fix(hero): use the album cover as the mainstage backdrop's last fallback

The mainstage hero inherited the artist-detail fallback chain, ending on
the Navidrome artist image. The hero frames an album, so its last-resort
backdrop should be the album's own cover (the layer shown when the
feature is off), not the artist image. Resolve it scope-true from the
album cover ref already in hand. Artist-detail keeps the artist cover.

* fix(hero): default backdrop order to fanart, Navidrome, then banner

Lead both heroes with the artist fanart and fall back to the wide banner
last, instead of trying the banner first. The fullscreen player keeps
fanart then Navidrome (it has no banner). Affects defaults / reset only;
saved per-surface orders are untouched and remain user-reorderable.
2026-06-27 01:46:01 +02:00
Psychotoxical c59bf937e4 refactor(settings): unify exclusive pickers on a shared segmented control (#1195)
Extract a reusable SettingsSegmented helper for the settings-segmented
pill picker. Track transitions adopts it unchanged (mode picker + AutoDJ
overlap cap). Lyrics scroll style and Queue display mode move from
stacks of mutually-exclusive toggles to the segmented picker, with the
active option's description shown below.
2026-06-27 01:14:51 +02:00
Psychotoxical 86ae462ad6 feat(hero): album-artist backdrop, configurable per-surface sources, and prefetch (#1193)
* refactor(cover): extract shared pickArtistBackdrop priority helper

* feat(hero): show the album artist's fanart as the mainstage hero backdrop

* feat(settings): configurable per-surface artist backdrop sources

Each artist-backdrop surface (mainstage hero, artist-detail header, fullscreen player) gets its own enable toggle + an ordered, individually-toggleable source list, configured under External Artwork Scraper on the Integrations tab (shown when the scraper is on). Reorder via the shared useDragSource/psy-drop drag infra plus keyboard-accessible up/down buttons; each source has its own on/off.

The shared chooser pickArtistBackdrop is generalised to resolveBackdrop + backdropFromConfig (ordered candidate list with the same pending/miss/centred-framing semantics), so all three surfaces resolve identically. themeStore persist bumped to v2; defaults reproduce today's order, so there is no visible change without user action.

Gating decoupled: the three surfaces are gated solely by their own per-surface flag, not by enableCoverArtBackground (which stays scoped to album/playlist-header cover blur). Reorder maths extracted to a pure, unit-tested module. i18n en + de (other locales TODO before PR).

Tests: resolver (10) + reorder (7).

* feat(cover): make the ensure queue surface-aware for artist backdrops

coverEnsureQueued now threads optional CoverEnsureOpts through to the Rust ensure and weaves the external surface into the in-flight key, so the fanart and banner surfaces of one artist no longer collide on one download chain. External surfaces also bypass the disk-src memory short-circuit (their {tier}-{surface}.webp never seeds those caches, and the canonical cover must not read as a hit). New thin ensureArtistBackdropQueued wrapper. Backward-compatible: plain covers append nothing to the key; the 5 queue tests stay green.

* fix(cover): reset the artist external-image hook synchronously on artist change

The hook reset src in an effect (one render late), so for the render between an artist change and that effect a consumer read the *previous* artist's resolved image. The mainstage hero then froze (and cached into per-album memory) a neighbouring slide's banner. Reset synchronously via the React adjust-state-on-prop-change pattern. Also removes brief stale-image flashes on the artist-detail header and fullscreen player.

* feat(hero): prefetch artist backdrops and show-ready-now / upgrade-on-re-entry

warmHomeMainstageCovers now prefetches each hero slide's artist backdrop (banner/fanart) at static slide-index priorities (idx1=high, idx0=low, rest=middle; no reprioritise on navigation), then predecodes every slide already on disk. useHeroBackdrop shows the best source ready at entry (Navidrome on a cold first visit, the prefetched/cached external one on re-entry) and freezes that source choice for the visit so nothing swaps mid-dwell; the url is derived live from the frozen choice, with a per-album disk memory for re-entry. HeroBg now crossfades only after the image bytes load (onLoad/complete gate + onError + fallback). Inert when the scraper is off. Tests: per-album memory (5).

* docs(changelog): configurable artist backdrops + mainstage hero (PR #1193)

CHANGELOG Added entry, settingsCredits line, and the What's New Artist-artwork highlight extended to cover the mainstage hero backdrop and per-place source config.

* docs(changelog): fold mainstage hero + per-place backdrops into the fanart entry (PR #1193)

Merge the configurable-backdrops changes into the existing 'Artist artwork from fanart.tv' block (now PR #1137 and #1193) instead of a separate entry.

* fix(hero): revert HeroBg load-gate that blanked the app (Maximum update depth)

The byte-load gate I added drove the crossfade reveal from an inline img ref + onLoad that re-fires on every render and scheduled a setTimeout each call; frequent re-renders (playback, marquee) stacked nested updates until React threw 'Maximum update depth exceeded' — and with no ErrorBoundary the whole window blanked. Reverted HeroBg to the proven timer-based crossfade. The hero only ever receives ready/predecoded urls, so the gate was cosmetic.

* fix(hero): gate the HeroBg crossfade on image load to stop the slide flicker

The bare 20 ms reveal faded a layer in before its bytes were ready (notably the Navidrome raw url), so a slide change flickered. Now an Image() preloader in the [url] effect reveals the layer on load (or a cached complete check), with a fallback. Everything is scheduled once per url — no per-render <img> ref/onLoad — so unlike the reverted gate it can't stack nested updates.

* feat(i18n): translate the backdrop-source settings into the remaining 10 locales

Adds the per-surface backdrop config strings (es, fr, nl, zh, nb, ru, ro, ja, hu, pl) and extends externalArtworkDesc to mention the mainstage hero where the key exists (ja has none → falls back to en).
2026-06-27 00:42:32 +02:00
Psychotoxical 014d57c53c feat(app): top-level error boundary so a render error no longer blanks the app (#1194)
* feat(app): add a top-level error boundary so a render error no longer blanks the app

Until now any thrown error during render took the whole window down with no recovery (issue #382). A class ErrorBoundary around the authenticated shell catches it and shows a recoverable fallback (Try again / Reload app) while playback keeps going. Hook-free + English-only with literal CSS var() fallbacks so it renders even when i18n/theme/state is what broke.

* docs(changelog): error boundary prevents the app blanking on a render error (PR #1194)
2026-06-26 20:51:42 +02:00
kilyabin afe5b377e0 feat(i18n): Russian locale — missing strings (#1181) 2026-06-25 21:22:45 +00:00
Hive Mind eb42a32315 feat(i18n): add Polish (pl) translation (#1185)
* Translated settings. Added polish option to other languages and in the fronted UI itself. Translated settings to polish

* Corrections to the settings. Translated help page

* Translated orbit. Made corrections to help and settings

* playlists translated

* deviceSync translated

* search translated

* Statistics translated

* musicNetwork translated

* common translated

* player translated

* queue translated

* albumDetails translated

* connection and random mix translated

* artistDefails translated

* albums translated

* smartPlaylists translated

* nowPlaying translated. smartPlaylists corrections

* sidebar translated

* contextMenu translated

* translated radio

* translated sharePaste

* favorites translated

* nowPlayingInfo translated

* translated home and login

* translated artists, genres, licences, migration, mostPlayed, songInfo and tracks

* translated changelog, composerDetail, composers, entityRating, folderBrowser, hero, losslessAlbums, luckyMix, miniPlayer, randomAlbums, randomLanding, tray and whatsNew

* chore(release): CHANGELOG and credits for Polish locale (PR #1185)

* chore(release): mention Polish in 1.49.0 What's New highlights

* settings updated with new strings. Deleted old unecessary ones

* translated missing translations

* Fixed typos

* translated missing strings

* Added missing translations that appeared in #1186 and #1189
2026-06-25 21:32:52 +03:00
cucadmuh cea814e993 fix(connection): strengthen sidebar recovery after reconnect (#1160) (#1190) 2026-06-25 19:52:40 +03:00
Psychotoxical 00512df207 feat(playlist): sort playlist tracks by date added (#1191)
* feat(sort-dropdown): support a right-aligned popover via align prop

* feat(playlist): sort playlist tracks by date added

* docs(changelog): playlist sort by date added (#1191)

* docs(credits): playlist sort by date added (#1191)
2026-06-25 18:29:35 +02:00
Psychotoxical d49424e95b feat(settings): add a Compact buttons appearance toggle (#1189)
* feat(ui): prototype compact hero and toolbar buttons (Large/Small appearance setting)

* feat(settings): rename action-button size toggle to "Compact buttons"

Promote the hero-button prototype to a real, app-wide setting.

- rename heroButtonSize → buttonSize, data-hero-buttons → data-button-size,
  hero-action-bar/hero-btn-label → compact-action-bar/compact-btn-label
- relabel "Hero buttons" → "Compact buttons" and broaden the description
  (action + toolbar buttons across detail pages and browse views) in all 11 locales
- move the feature CSS out of cover-lightbox.css into its own compact-buttons.css
- add tests: themeStore buttonSize toggle, SelectionToggleButton

* docs(changelog): compact buttons appearance toggle (#1189)

* docs(credits): compact buttons appearance toggle (#1189)
2026-06-25 14:49:09 +02:00
Psychotoxical 8b89596fcf feat(album): show all genres in album details, linkable to genre pages (#1186)
* feat(album): show all genres in album details, linkable to genre pages

The album header listed only a single genre. It now renders every genre
from the album's OpenSubsonic genres[] (falling back to splitting the
legacy genre string), each linking to its genre page via the existing
genre route.

* docs(changelog): multiple genres in album details (#1186)

* refactor(genres): extract genreColor into a shared util

* feat(album): union album and track genres behind a +N cursor menu

* feat(album): read album detail from the library index when ready

* docs(changelog): expand the album genres entry

* feat(album): genre pill row with keyboard-navigable popover

* test(offline): cover index-first resolveAlbum path with mocked libraryIsReady

* refactor(styles): move album-detail genre styles out of cover-lightbox.css
2026-06-25 13:16:24 +02:00
cucadmuh 4aa6427727 feat(login): custom HTTP headers on initial Add Server dialog (#1187) (#1188) 2026-06-25 13:44:50 +03:00
cucadmuh a88d5f3181 fix(autodj): last-track tail and queue-end idle pull rewind (#1183) 2026-06-25 00:31:12 +03:00
Psychotoxical b6ac879b14 refactor(settings): SettingsSubCard primitives across all of Settings (#1182)
* feat(settings): add SettingsSubCard primitives for sub-section controls

Reusable wrappers (SettingsSubCard / SettingsField / SettingsRow /
SettingsValue / SettingsCallout) that encapsulate the settings-norm-* sub-card
styles. Per-mode and detail controls inside a settings section should use these
instead of hand-rolling the box with inline padding or one-off classes — the
lack of a shared primitive is what let recent additions drift off the pattern.

* refactor(settings): move the Audio tab onto the SettingsSubCard primitives

Normalization, Track transitions and Hi-Res now build their sub-cards from the
shared primitives instead of raw settings-norm-* markup. Track Previews, which
rendered its sub-options bare with inline styles and dividers, now uses the same
sub-card so the whole Audio tab is consistent. Behaviour and styling unchanged;
Track Previews' start/duration become labelled slider rows like Normalization.

* refactor(settings): move the remaining tabs onto SettingsSubCard

Apply the shared sub-card primitives across the rest of Settings so every tab
matches the Audio-tab reference look:

- Appearance: grid columns, window buttons, UI scale, font, seekbar
- Library: random-mix blacklist, rating-filter detail
- Input: in-app + global keybind lists
- Integrations: Discord templates, external-artwork BYOK
- System: language, Linux Wayland text render, clock format, logging
- Storage: media dir, hot-cache detail, downloads folder
- Backup: mode picker + per-mode export/import

Detail/per-mode controls now sit in the bordered sub-card; toggle groups stay on
SettingsToggle. Behaviour unchanged.

* refactor(settings): box the data-heavy and shared sections in SettingsSubCard

Bring the previously-skipped 'borderline' sections onto the sub-card too:

- Playback speed: wrap the settings-mode controls in a sub-card; the compact
  player-popup path is left untouched (shares the component via the compact flag)
- Theme scheduler: mode picker + theme/time selects move into the sub-card
- Cover-cache + analytics strategy: the per-server tables sit in a sub-card

Behaviour unchanged; the table layouts and the playback-rate-* internals stay as
they are, only the surrounding sub-card is added for visual consistency.
2026-06-24 23:13:10 +02:00
cucadmuh 4e8f16b8ae docs: refresh WHATS_NEW and CHANGELOG for 1.49.0 (#1180) 2026-06-25 00:04:16 +03:00
cucadmuh 7b9e676af7 chore(library): CJK artist name_sort regression tests (#1178) 2026-06-24 23:27:26 +03:00
cucadmuh ec98fcc4ff fix(genres): hide empty genres after library resync (#1176) 2026-06-24 23:07:22 +03:00