diff --git a/crates/Cargo.lock b/crates/Cargo.lock index e5656d34e..c1cac05e8 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -3209,9 +3209,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -3269,7 +3269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" dependencies = [ "num-traits", - "rand 0.10.0", + "rand 0.10.1", ] [[package]] @@ -3417,7 +3417,7 @@ dependencies = [ "cpal", "dasp_sample", "num-rational", - "rand 0.10.0", + "rand 0.10.1", "rand_distr", "rtrb", "symphonia", @@ -4187,7 +4187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", diff --git a/docs/release-1.2.7/TAG_DETAILS.md b/docs/release-1.2.7/TAG_DETAILS.md new file mode 100644 index 000000000..9dd0ff009 --- /dev/null +++ b/docs/release-1.2.7/TAG_DETAILS.md @@ -0,0 +1,147 @@ +# 1.2.7 — Accolade Watch (diagnostics + carousel fix) + +Two things drove this release, both of them corrections: + +A defensive fix for the Discover carousel click-through regression introduced in 1.2.6: the mouse-drag scroll used implicit pointer capture on `pointerdown`, which on some WebKitGTK + compositor combinations retargeted the subsequent click to the scroll container instead of the card — so opening albums, artists, or playlists from Home / Editor's Picks / For You quietly stopped working. Capture is now deferred until a real drag is in progress; normal clicks fall through cleanly. + +A big expansion of **System Diagnostics**: the one-shot snapshot now reports OS / distro / install method / key library versions (WebKitGTK, GTK, glibc, ALSA, PipeWire, PulseAudio), live playback state, the Qobuz Connect role (controller / local renderer / observer), and — opt-out toggle in the View Logs modal — an optional 10-second Chromecast + DLNA scan, all bundled into the same paste upload. Bug reports can ship one link instead of three, and no sensitive data (no track/user/device IDs) ever reaches the paste. + +My apologies for stacking three releases where one would have been enough — I misjudged the scope and timing of each batch. + +Everything from 1.2.6 and 1.2.5 below is included as well. + +--- + + +A quick follow-up on the heels of 1.2.5. Two things drove this release: + +- A long-overdue **accessibility pass**: four new WCAG-compliant themes (including a true high-contrast mode for severely vision-impaired users) and mouse-drag scrolling on every horizontal album row. +- A **regression rescue**: Infinite Play (the ∞ toggle in the queue) had silently lost its backend wiring during the V2 migration — the toggle persisted in localStorage but the queue never refilled and the mode was never sent to the backend. Reimplemented from scratch in V2 with no legacy wrappers. + +Everything from 1.2.5 below is included as well; if you're upgrading from 1.2.4 or earlier, this is what you're getting. + +--- + +## What's new in 1.2.6 + +### Accessibility (discussion #313) + + - **Four new WCAG-compliant themes** in Settings > Appearance > Theme: + - **WCAG Light** — AAA contrast (7:1+) on every text level, blue accent + - **WCAG Dark** — AAA contrast on dark backgrounds + - **High Contrast** — pure white on pure black with yellow accent (21:1 ratio). Built explicitly for severely vision-impaired users + - **Colorblind** — deuteranopia-safe palette (Wong colors), no red/green adjacency + - **Mouse-drag scrolling on horizontal album rows** — click and drag sideways on Home, For You, Award and Label views, just like on iPad or in a browser. Arrow buttons still work as before + - **Qobuz Connect badge always visible** (#318) — even with an empty queue (e.g. on app start) you can connect to a remote renderer instead of having to start playing something first; the badge keeps its size across responsive breakpoints + +### Regression fixes + + - **Infinite Play (∞) restored** (#319) — the toggle now persists across restarts and refills the queue with related tracks (Qobuz `/radio/artist`, seeded from your last played tracks) when the queue ends, instead of stopping. V2 reimplementation + - **Gentoo `qbz-bin` ebuild** — now correctly inherits `xdg-utils`, so the hicolor icon cache and the desktop database are refreshed on install/uninstall. This was the cause of stale icons after upgrading to 1.2.5 + +### Branding polish + + - **App icon resized to fill the canvas** (matches Steam/Chrome/Papirus footprint, ~97%) with a subtle blue rim and inner highlight so the matte-vinyl disc doesn't disappear on dark backgrounds; new 512x512 PNG export added + - **Tray icons regenerated** to match the new vinyl Q (the v1.2.5 refresh missed the monochromatic tray glyphs and the Snap launcher) + - **Login screen** — drop-shadow under the logo removed (the new icon's edge rim and inner light make it redundant) + +--- + +## What 1.2.5 brought (still applies) + +After a rough week of real-world work, this release brought a dedicated Award view, labels you can follow, and one of the most anticipated sections from the mobile apps landing on the desktop at last: **Release Watch**. + +On the functionality side, you can drag and drop any track — or a multi-selection — from any listing onto the sidebar playlists. The rest of the changes are mostly architectural and not user-facing, but they show up as overall stability. + +Regarding Qobuz Connect: I know there's still a lot of room for improvement. Most of it is WIP and will start landing in the coming releases. + +I want to mention again that the project has been receiving donations. I had linked the accounts to an email I don't check very often and only noticed last week — I'm overwhelmed (in the good way) and genuinely flattered. I haven't fully decided how to use the funds yet, but part will definitely go toward infrastructure. When I started the project, the free tiers seemed more than enough; now we have hundreds of installations on Snapcraft, thousands on Flathub, and a steady stream of direct downloads from GitHub releases. Since qbz has zero telemetry, I can only keep a partial count — it's not a number I ever imagined reaching. These donations will let me make sure the Cloudflare workers that power the MusicBrainz and Last.fm integrations stay up even if we start to brush against the free-tier limits. I couldn't be more grateful to every sponsor, donor, and everyone who supports the project through code, feedback, or simply by using the app. + +Saludos a todos y muchas gracias. + +### Accolades (Awards + Press) + + - **Dedicated Award page** — new `AwardView` with header matching the Label/Album conventions, gold press ribbon, and an "Other awards" carousel; wired to V2 commands for `/award/page` and `/award/getAlbums` + - **AwardAlbumsView** — See-all grid for every award, follow-award as a first-class entity + - **Award resolution by name** — when `/album/get` omits the award id, the backend now resolves it via `/award/explore`, normalises ids end-to-end as strings, and tolerates inconsistent Qobuz shapes on `AwardPageData` / `AwardMagazine` + - **Editorial seed** — `/award/explore` seeds across all locales with diacritic-insensitive lookup; hard-coded seed removed + - **Album-view right sidebar** — shows the full awards stack plus the album's label + - **Album ribbons extended to press accolades** — last award wins on the card; the dedicated sidebar still shows the full stack + - **Home cleanup** — editorial ribbons added for Qobuzissime and Album of the Week on the home rails + +### Release Watch + + - **Dedicated tabbed view** — mirrors the Qobuz mobile layout with its own entry in Discover; moved below Your Mixes in the home grid + - **Backend** — `/favorite/getNewReleases?type=artists` as REST (not signed RPC), projected onto `SearchResultsPage`, artist field backfilled from `artists[0]` + - **Persistence** — Release Watch now survives home-cache invalidations so it renders immediately on revisit + +### Labels + + - **Follow / unfollow labels** — mirrors Follow Artist; heart overlay on `LabelView`'s more-labels cards + - **Favorites → Labels tab** — new tab in the Favorites view + +### Visual identity + + - **New matte-vinyl logo** applied to every icon asset: Tauri standard sizes, Windows Store squares, Android icons and mipmaps, `static/`, README, tray + - **Monochrome symbolic variants** — `tray-light.png` (black glyph) and `tray-dark.png` (white glyph) shipped alongside `tray.png`; the Linux tray picks the matching variant at runtime by probing GNOME `color-scheme`, GTK `prefer-dark-theme`, and KDE `kdeglobals` + - **Login screen refresh** — logo resized to 175px, plus QBZ and "Qobuz™ Player" branding under the mark; `app.tagline` rewritten in en/es + - **About dialog** — fixed a duplicated "License" label (the second row was meant to show the platform); codename updated to "Accolade Watch" + - **DAC Setup Wizard icon contrast** — button icon drops `invert(1)` so it reads on light accent themes (Catppuccin Lavender, Dracula…); the tooltip copy of the same icon now has `invert(1)` so it reads on the dark surface bg + +### Desktop integration (Linux) + + - **Window controls auto-detect** — new `desktop_theme` Rust module reads `kwinrc`, `kdeglobals`, and `klassyrc`; a runtime-derived "Klassy (auto-detect)" or "Plasma (auto-detect)" preset mirrors the system decoration colors and button shape when Plasma is detected, and stays hidden otherwise + - **Klassy button shapes** — `mapKlassyShapeToQbz()` maps Klassy's `IntegratedRoundedRectangle`, `FullHeightRectangle`, `FullHeightRoundedRectangle`, `Tab`, `Circle`, `Square` onto QBZ's `ButtonShape` set; adds a new `full-height-rounded` variant to cover Klassy's most common preset + - **Opt-in rounded window corners** — "Match system window chrome" toggle persists to `window_settings.db` and gates the Tauri window transparency decision at startup; on next launch the main window is rebuilt transparent and the detected corner radius (10 Klassy, 6 Breeze, 12 Adwaita, 8 fallback) is applied via `clip-path` + `border-radius` with GPU compositing for clean anti-aliasing on WebKitGTK + - **ksni tray on Linux** — replaces the libayatana-appindicator path so left-click actually toggles the window (issue #310) + - **Window size clamp** — persisted sizes that exceed the largest available monitor now clamp to fit instead of opening off-screen + +### Streaming, audio, and player + + - **Request signing expanded** — all remaining Qobuz API endpoints and every search endpoint now sign their requests; stronger compliance and fewer 403 edge cases + - **BitPerfectMode surfaced in QualityBadge** (#288) — mode now flows through `PlaybackEvent` and the badge updates on playback start + - **DAC rate unsupported fallback** (#288) — track quality is downgraded and the backend falls back to `plughw` instead of CPAL when the DAC can't handle a requested rate; clearer error when CPAL cannot open an enumerated device + - **Output device missing notification** (#307) — user gets a toast when the selected output device disappears + - **Audio output badges refresh** on playback start + - **ALSA Direct `hw` immersive volume lock** — volume slider is disabled in the immersive player when ALSA Direct hw is active (bit-perfect lock) + - **ALSA default plugin** — `alsa_plugin` defaults to `Hw` when switching to the ALSA backend + - **Gapless loop-one fix** — logic moved from backend to frontend; loop-one now works with gapless playback + - **Gapless local-library fallback** — tries local library if not in any cache; fails silently when track isn't found + - **ALSA engine shutdown** — engine stops before dropping the ALSA stream on format changes + - **Real offline mode** (#279) — snapshot streaming, network blocked, diagnostic logs + - **Player fixes** — `durationSecs` passed on session-restore first play so the seekbar advances (previously stuck); qconnect always persists local session to preserve track-level restore (#304) + +### Updater (opt-in, non-store builds) + + - **tauri-plugin-updater integrated** — backend + frontend; `UpdateProgressModal` added with i18n; wired into app bootstrap and Settings + - **Gated behind Cargo feature `updater`** — sandboxed builds (Flathub, Snap) pass `--no-default-features` so the updater stays disabled for store-managed installs + - **Signed manifest** — minisign public key baked in; CI signs artifacts and consolidates the update manifest; updater manifest path no longer triggers an infinite `tauri dev` rebuild loop + +### UI fixes and polish + + - **Smart positioning for sidebar menus** — the per-playlist right-click menu now flips above the cursor when it would fall below the window; the Sort submenu inside the general playlists menu uses `use:portal` to escape clipping by `.dropdown-menu` + - **Genre filter popup** — collision check reads the CSS `max-height` (530/630 width, 500/700 height) instead of the measured rect so the popup flips above the trigger or clamps to the viewport when content grows asynchronously + - **Right-section collapse** (#303) — hamburger at narrow widths instead of overflow + - **QualityBadge / QconnectBadge compact variants** — keeps them at full bar height without overflow + - **Silk animations removed** from ForYouTab mix cards and Your Mixes covers — performance cost outweighed the visual benefit and they caused minor rendering glitches + - **Multi-select drag** enabled in all views; track drag & drop extended to artist and search views and onto sidebar playlists; compact drag ghost with artist + album + - **Lyrics active line** uses theme accent color + - **For-You load order** — waits for `topArtists`/`recentAlbums` before loading dependent rails + - **Banner layout** uses a CSS variable; i18n key fixed; responsive height + - **Favorites Select All** — new checkbox in multi-select mode across track views + +### Internal architecture + + - **`commands_v2` split into a module directory** — the former single-file module is now nineteen focused files, easier to navigate and review + - **Custom device name** persists across restarts for QConnect + - **Prefetch perf** — cache depth increased to 5 tracks with 2 concurrent CMAF segment downloads in parallel + - **Dependency bumps** — tauri-plugin-dialog 2.7.0, tauri-plugin-deep-link, rodio 0.22.2, vite 8.0.8, svelte 5.55.2, sveltejs/kit 2.57.1, rand, jsdom 29.0.2, notify-rust 4.14.0 + +### Packaging + + - **Snap** — MPRIS slot restored after snapcraft approval; exclusive-audio note added to the description + - **CI ARM64** — native ARM runner replaces QEMU for the aarch64 build; targets glibc 2.35; dropped OpenSSL in favour of rustls across all crates + +--- + +Full changelog: https://github.com/vicrodh/qbz/compare/v1.2.6...v1.2.7 diff --git a/flake.nix b/flake.nix index 184c817d7..dedece6b9 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ # VERSION BUMP: update version, rev, and hashes # when tagging a new release. # ────────────────────────────────────────────── - qbzVersion = "1.2.6"; + qbzVersion = "1.2.7"; qbzRev = "v${qbzVersion}"; srcHash = ""; # nix build will report the correct hash on first run npmHash = ""; # nix build will report the correct hash on first run diff --git a/package-lock.json b/package-lock.json index 10a7985ae..17085db57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "qbz-nix", - "version": "1.2.6", + "version": "1.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "qbz-nix", - "version": "1.2.6", + "version": "1.2.7", "license": "MIT", "dependencies": { "@icons-pack/svelte-simple-icons": "^7.2.0", @@ -27,11 +27,11 @@ "@tauri-apps/cli": "^2", "@testing-library/svelte": "^5.3.1", "jsdom": "^29.0.2", - "svelte": "^5.55.2", + "svelte": "^5.55.4", "svelte-check": "^4.4.6", "typescript": "~5.9.3", "vite": "^8.0.8", - "vitest": "^4.1.2" + "vitest": "^4.1.4" } }, "node_modules/@asamuzakjp/css-color": { @@ -1610,16 +1610,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1628,13 +1628,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.2", + "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1665,9 +1665,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", "dev": true, "license": "MIT", "dependencies": { @@ -1678,13 +1678,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.4", "pathe": "^2.0.3" }, "funding": { @@ -1692,14 +1692,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1708,9 +1708,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", "dev": true, "license": "MIT", "funding": { @@ -1718,13 +1718,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", + "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2898,9 +2898,9 @@ "license": "MIT" }, "node_modules/svelte": { - "version": "5.55.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.2.tgz", - "integrity": "sha512-z41M/hi0ZPTzrwVKLvB/R1/Oo08gL1uIib8HZ+FncqxxtY9MLb01emg2fqk+WLZ/lNrrtNDFh7BZLDxAHvMgLw==", + "version": "5.55.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz", + "integrity": "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -3242,19 +3242,19 @@ } }, "node_modules/vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -3282,10 +3282,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.2", - "@vitest/browser-preview": "4.1.2", - "@vitest/browser-webdriverio": "4.1.2", - "@vitest/ui": "4.1.2", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -3309,6 +3311,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, diff --git a/package.json b/package.json index 59d805613..18571d27e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qbz-nix", - "version": "1.2.6", + "version": "1.2.7", "description": "Native high-fidelity client for Qobuz™ subscribers on Linux with Hi-Fi audio support (uses the Qobuz API; not certified by Qobuz)", "type": "module", "scripts": { @@ -44,11 +44,11 @@ "@tauri-apps/cli": "^2", "@testing-library/svelte": "^5.3.1", "jsdom": "^29.0.2", - "svelte": "^5.55.2", + "svelte": "^5.55.4", "svelte-check": "^4.4.6", "typescript": "~5.9.3", "vite": "^8.0.8", - "vitest": "^4.1.2" + "vitest": "^4.1.4" }, "overrides": { "cookie": "^0.7.0", diff --git a/packaging/flatpak/com.blitzfc.qbz.metainfo.xml b/packaging/flatpak/com.blitzfc.qbz.metainfo.xml index 37c1fbe7f..fbde19459 100644 --- a/packaging/flatpak/com.blitzfc.qbz.metainfo.xml +++ b/packaging/flatpak/com.blitzfc.qbz.metainfo.xml @@ -69,6 +69,17 @@ + + +

Follow-up to 1.2.5 and 1.2.6: a defensive fix and a diagnostics expansion.

+
    +
  • Defensive fix for the Discover carousel click-through regression in 1.2.6: mouse-drag scrolling no longer captures the pointer until a real drag is in progress, so clicks on album/artist/playlist cards reach the card's onClick across every WebKitGTK + compositor combo
  • +
  • System Diagnostics expansion: one-shot snapshot now reports OS / distro / install method / key library versions (WebKitGTK, GTK, glibc, ALSA, PipeWire, PulseAudio), live playback state, Qobuz Connect role (controller / local renderer / observer), and — opt-out toggle in View Logs — an optional 10s Chromecast + DLNA scan, all bundled into the same paste upload
  • +
  • Favorites sidebar backfill: users whose persisted tab order predated the Labels tab were missing the Labels entry; order is now diffed against the canonical default and repaired on load
  • +
  • Misc: dependency bumps (svelte 5.55.4, vitest 4.1.4, rand 0.10.1 for GHSA-cq8v-f236-94qc)
  • +
+
+

Accessibility, polish, and a couple of regression fixes.

diff --git a/snapcraft.yaml b/snapcraft.yaml index 8eaf7035a..9e7b37f17 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -3,7 +3,7 @@ name: qbz-player title: "QBZ — Native Qobuz Client for Linux" -version: '1.2.6' +version: '1.2.7' summary: Native Qobuz player for Linux with bit-perfect audio and DAC control description: | QBZ is a native Linux desktop client for Qobuz™, designed for high-fidelity playback. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 35c9f9ab7..0af3132ec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4910,7 +4910,7 @@ dependencies = [ [[package]] name = "qbz-nix" -version = "1.2.6" +version = "1.2.7" dependencies = [ "aes-gcm", "anyhow", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 93093ae2e..d94eaa897 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qbz-nix" -version = "1.2.6" +version = "1.2.7" description = "Native high-fidelity client for Qobuz™ subscribers on Linux with Hi-Fi audio support (uses the Qobuz API; not certified by Qobuz)" authors = ["blitzkriegfc"] edition = "2021" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 30f7e27b4..49c74a902 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -28,6 +28,7 @@ "core:window:allow-inner-size", "core:window:allow-inner-position", "core:window:allow-set-position", + "core:window:allow-set-title", "core:webview:default", "core:webview:allow-create-webview-window", "core:webview:allow-webview-close", diff --git a/src-tauri/src/commands_v2/diagnostics.rs b/src-tauri/src/commands_v2/diagnostics.rs index 93a21e589..429a953e3 100644 --- a/src-tauri/src/commands_v2/diagnostics.rs +++ b/src-tauri/src/commands_v2/diagnostics.rs @@ -166,3 +166,166 @@ pub fn v2_get_runtime_diagnostics( app_version: env!("CARGO_PKG_VERSION").to_string(), }) } + +// ==================== System Info ==================== + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemInfo { + pub os: String, + pub arch: String, + pub kernel_version: Option, + pub distro_id: Option, + pub distro_version_id: Option, + pub distro_pretty_name: Option, + pub install_method: String, + pub flatpak_runtime: Option, + pub flatpak_runtime_version: Option, + pub webkit2gtk_version: Option, + pub gtk_version: Option, + pub glibc_version: Option, + pub alsa_version: Option, + pub pipewire_version: Option, + pub pulseaudio_version: Option, +} + +/// Parse /etc/os-release (or the Flatpak host equivalent) into key/value pairs. +fn read_os_release() -> std::collections::HashMap { + let mut map = std::collections::HashMap::new(); + // Try Flatpak-exposed host file first, then the normal path. + let candidates = ["/run/host/os-release", "/etc/os-release"]; + for path in candidates { + if let Ok(text) = std::fs::read_to_string(path) { + for line in text.lines() { + if let Some(idx) = line.find('=') { + let key = line[..idx].trim().to_string(); + let mut value = line[idx + 1..].trim().to_string(); + if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 { + value = value[1..value.len() - 1].to_string(); + } + map.insert(key, value); + } + } + if !map.is_empty() { + return map; + } + } + } + map +} + +fn detect_kernel_version() -> Option { + std::fs::read_to_string("/proc/sys/kernel/osrelease") + .ok() + .map(|s| s.trim().to_string()) +} + +fn detect_install_method() -> (String, Option, Option) { + // (method, flatpak_runtime, flatpak_runtime_version) + if std::env::var("FLATPAK_ID").is_ok() || std::path::Path::new("/.flatpak-info").exists() { + let mut runtime = None; + let mut runtime_version = None; + if let Ok(text) = std::fs::read_to_string("/.flatpak-info") { + for line in text.lines() { + if let Some(value) = line.strip_prefix("runtime=") { + let v = value.trim(); + if let Some(slash) = v.rfind('/') { + runtime = Some(v[..slash].to_string()); + runtime_version = Some(v[slash + 1..].to_string()); + } else { + runtime = Some(v.to_string()); + } + } + } + } + return ("flatpak".to_string(), runtime, runtime_version); + } + if std::env::var("SNAP").is_ok() { + return ("snap".to_string(), None, None); + } + if std::env::var("APPIMAGE").is_ok() { + return ("appimage".to_string(), None, None); + } + if cfg!(debug_assertions) { + return ("dev".to_string(), None, None); + } + ("native".to_string(), None, None) +} + +/// Extract the best-available version string from the filename of a shared +/// library loaded by the current process. Looks for patterns like +/// `libfoo.so.0.15.7` → `0.15.7`, or `libfoo.so.2` → `2`. +/// Returns `None` if the library isn't mapped. +fn detect_loaded_lib_version(lib_name_stem: &str) -> Option { + let maps = std::fs::read_to_string("/proc/self/maps").ok()?; + let mut best: Option = None; + for line in maps.lines() { + // Last column is the path (may contain spaces, very rare). + let path = line.splitn(6, ' ').nth(5).unwrap_or("").trim(); + if path.is_empty() { + continue; + } + let filename = std::path::Path::new(path) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or(""); + if !filename.starts_with(lib_name_stem) { + continue; + } + // filename example: "libwebkit2gtk-4.1.so.0.15.7" + // Strip the "lib_name_stem.so" prefix and leading dot. + let tail = match filename.split_once(".so") { + Some((_, rest)) => rest.trim_start_matches('.'), + None => continue, + }; + if tail.is_empty() { + continue; + } + // Resolve symlink target if possible — often the real file carries + // a fuller version than the SONAME alias. + if let Ok(real) = std::fs::canonicalize(path) { + if let Some(real_name) = real.file_name().and_then(|s| s.to_str()) { + if let Some((_, rest)) = real_name.split_once(".so") { + let real_tail = rest.trim_start_matches('.'); + if !real_tail.is_empty() { + best = Some(real_tail.to_string()); + continue; + } + } + } + } + best.get_or_insert_with(|| tail.to_string()); + } + best +} + +#[tauri::command] +pub fn v2_get_system_info() -> Result { + let os = std::env::consts::OS.to_string(); + let arch = std::env::consts::ARCH.to_string(); + let (install_method, flatpak_runtime, flatpak_runtime_version) = detect_install_method(); + let osr = read_os_release(); + + Ok(SystemInfo { + os, + arch, + kernel_version: detect_kernel_version(), + distro_id: osr.get("ID").cloned(), + distro_version_id: osr.get("VERSION_ID").cloned(), + distro_pretty_name: osr + .get("PRETTY_NAME") + .cloned() + .or_else(|| osr.get("NAME").cloned()), + install_method, + flatpak_runtime, + flatpak_runtime_version, + // Runtime shared-library versions, parsed from /proc/self/maps. + webkit2gtk_version: detect_loaded_lib_version("libwebkit2gtk-4.1"), + gtk_version: detect_loaded_lib_version("libgtk-3") + .or_else(|| detect_loaded_lib_version("libgtk-4")), + glibc_version: detect_loaded_lib_version("libc"), + alsa_version: detect_loaded_lib_version("libasound"), + pipewire_version: detect_loaded_lib_version("libpipewire-0.3"), + pulseaudio_version: detect_loaded_lib_version("libpulse"), + }) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7abe365f8..3bb26bf42 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1361,6 +1361,7 @@ pub fn run() { commands_v2::v2_set_visualizer_enabled, commands_v2::v2_get_developer_settings, commands_v2::v2_get_runtime_diagnostics, + commands_v2::v2_get_system_info, commands_v2::v2_set_developer_force_dmabuf, commands_v2::v2_get_graphics_settings, commands_v2::v2_get_graphics_startup_status, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 06664e87d..f391c6c75 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "QBZ", - "version": "1.2.6", + "version": "1.2.7", "identifier": "com.blitzfc.qbz", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/app.css b/src/app.css index 4e66d5b2e..97ab53b0c 100644 --- a/src/app.css +++ b/src/app.css @@ -1586,16 +1586,20 @@ select { .action-btn-circle.primary { width: 44px; height: 44px; - background: var(--accent-primary); border: none; + /* Resting = the previous hover look: --accent-hover fill, white glyph, + white 1px ring. Hover flips to the --accent-primary fill with the + glyph now in --accent-hover (the previous resting style). */ + background: var(--accent-hover); color: var(--text-primary); - box-shadow: inset 0 0 0 2px var(--border-subtle); - transition: background 150ms ease, box-shadow 200ms ease; + box-shadow: inset 0 0 0 1px var(--text-primary); + transition: background 150ms ease, color 150ms ease; } .action-btn-circle.primary:hover { - background: var(--accent-hover); - box-shadow: inset 0 0 0 2px var(--text-primary); + background: var(--accent-primary); + color: var(--accent-hover); + box-shadow: inset 0 0 0 1px var(--text-primary); } .action-btn-circle.is-active { diff --git a/src/lib/components/CastPicker.svelte b/src/lib/components/CastPicker.svelte index a8967fcb3..943f01890 100644 --- a/src/lib/components/CastPicker.svelte +++ b/src/lib/components/CastPicker.svelte @@ -78,6 +78,13 @@ let error = $state(null); let discoveryStarted = $state(false); let connecting = $state(false); + // Multicast discovery (DLNA / Chromecast) commonly needs 5-15s before + // devices announce themselves. `loading` only covers the first poll, so we + // keep a separate `scanning` flag that stays true long enough to show a + // spinner instead of a premature "No devices found" message. + let scanning = $state(false); + let scanTimeout: ReturnType | null = null; + const SCAN_DURATION_MS = 15000; // Cast state from store let castState = $state(getCastState()); @@ -122,6 +129,9 @@ loading = true; error = null; discoveryStarted = true; + scanning = true; + if (scanTimeout) clearTimeout(scanTimeout); + scanTimeout = setTimeout(() => { scanning = false; }, SCAN_DURATION_MS); try { // Start discovery protocols in parallel @@ -136,11 +146,15 @@ } catch (err) { error = String(err); loading = false; + scanning = false; + if (scanTimeout) { clearTimeout(scanTimeout); scanTimeout = null; } } } async function stopDiscovery() { discoveryStarted = false; + scanning = false; + if (scanTimeout) { clearTimeout(scanTimeout); scanTimeout = null; } try { await Promise.allSettled([ invoke('v2_cast_stop_discovery'), @@ -468,7 +482,7 @@ {/if} - {:else if loading && devices().length === 0} + {:else if devices().length === 0 && (loading || scanning)}

{$t('toast.loadingDevices')}

diff --git a/src/lib/components/DiagnosticsPanel.svelte b/src/lib/components/DiagnosticsPanel.svelte index 07d13ec6d..46bbec1b0 100644 --- a/src/lib/components/DiagnosticsPanel.svelte +++ b/src/lib/components/DiagnosticsPanel.svelte @@ -1,9 +1,17 @@
@@ -199,6 +493,132 @@ {#if diagnostics}
QBZ v{diagnostics.appVersion}
+ + {#if systemInfo} + + {#if systemOpen} + + + + + + + + + + {#each systemRows(systemInfo) as row (row.label)} + + + + + + {/each} + +
{$t('settings.developer.diagnostics.colSetting')}{$t('settings.developer.diagnostics.colRuntime')}
{row.label}{row.runtime}·
+ {/if} + {/if} + + + {#if playback} + + {#if playbackOpen} + + + + + + + + + + {#each playbackRows(playback) as row (row.label)} + + + + + + {/each} + +
{$t('settings.developer.diagnostics.colSetting')}{$t('settings.developer.diagnostics.colRuntime')}
{row.label}{row.runtime}·
+ {/if} + {/if} + + + {#if qconnect} + + {#if qconnectOpen} + + + + + + + + + + {#each qconnectRows(qconnect) as row (row.label)} + + + + + + {/each} + +
{$t('settings.developer.diagnostics.colSetting')}{$t('settings.developer.diagnostics.colRuntime')}
{row.label}{row.runtime}·
+ {/if} + {/if} + + + + {#if castOpen} +
+ +
+ {#if castScanning && !castScan} +
+ + {$t('settings.developer.diagnostics.castScanning')} +
+ {/if} + {#if castScan} + + + + + + + + + + {#each castRows(castScan) as row (row.label + row.runtime)} + + + + + + {/each} + +
{$t('settings.developer.diagnostics.colSetting')}{$t('settings.developer.diagnostics.colRuntime')}
{row.label}{row.runtime}{row.status === 'mismatch' ? '✗' : '·'}
+ {/if} + {/if} + + +
+
+ {/snippet} + + + diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index b1b7672a3..44bab1eca 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -973,7 +973,21 @@ async function loadFavoritesPreferences() { try { const prefs = await invoke('v2_get_favorites_preferences'); - favoritesTabOrder = prefs.tab_order || ['tracks', 'albums', 'artists', 'labels', 'playlists']; + const defaultOrder = ['tracks', 'albums', 'artists', 'labels', 'playlists']; + let order = prefs.tab_order || defaultOrder; + // Backfill any tab the user's persisted order is missing (e.g. sessions + // created before `labels` was added as a favorites tab). Preserve the + // user's existing order; append missing tabs in their default position. + const missing = defaultOrder.filter((tab) => !order.includes(tab)); + if (missing.length > 0) { + const merged = [...order]; + for (const tab of missing) { + const defaultIdx = defaultOrder.indexOf(tab); + merged.splice(defaultIdx, 0, tab); + } + order = merged; + } + favoritesTabOrder = order; } catch (err) { console.debug('[Sidebar] Failed to load favorites preferences:', err); } diff --git a/src/lib/components/UserCard.svelte b/src/lib/components/UserCard.svelte index 48168f7ca..def5cec08 100644 --- a/src/lib/components/UserCard.svelte +++ b/src/lib/components/UserCard.svelte @@ -2,6 +2,8 @@ import { Settings, SlidersHorizontal, Keyboard, Bug, BookOpen, CircleHelp } from 'lucide-svelte'; import { openUrl } from '@tauri-apps/plugin-opener'; import { t } from '$lib/i18n'; + import ReportIssueModal from './ReportIssueModal.svelte'; + import { setSettingsIntent } from '$lib/stores/settingsIntentStore'; interface Props { username: string; @@ -16,6 +18,25 @@ let isMenuOpen = $state(false); let menuRef: HTMLDivElement | null = $state(null); + let showReportModal = $state(false); + + const GITHUB_ISSUES_URL = 'https://github.com/vicrodh/qbz/issues/new?template=bug_report.yml'; + const HIDE_REPORT_MODAL_KEY = 'qbz-hide-report-issue-modal'; + + function openGithubIssue() { + openUrl(GITHUB_ISSUES_URL).catch((err) => console.error('Failed to open URL:', err)); + } + + function handleReportGoToLogs() { + showReportModal = false; + setSettingsIntent({ section: 'developer', openLogs: true }); + onSettingsClick(); + } + + function handleReportCreateIssue() { + showReportModal = false; + openGithubIssue(); + } function toggleMenu(e: MouseEvent) { e.stopPropagation(); @@ -43,8 +64,15 @@ function handleReportBug() { closeMenu(); - openUrl('https://github.com/vicrodh/qbz/issues/new?template=bug_report.yml') - .catch(err => console.error('Failed to open URL:', err)); + let skipModal = false; + try { + skipModal = localStorage.getItem(HIDE_REPORT_MODAL_KEY) === 'true'; + } catch { /* ignore */ } + if (skipModal) { + openGithubIssue(); + } else { + showReportModal = true; + } } function handleDocumentation() { @@ -132,6 +160,13 @@ + showReportModal = false} + onGoToLogs={handleReportGoToLogs} + onCreateIssue={handleReportCreateIssue} +/> +