Commit Graph
4067 Commits
Author SHA1 Message Date
dependabot[bot] 7dcfb363fc chore(deps): bump tauri from 2.10.3 to 2.11.1 in /src-tauri
Bumps [tauri](https://github.com/tauri-apps/tauri) from 2.10.3 to 2.11.1.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.10.3...tauri-v2.11.1)

---
updated-dependencies:
- dependency-name: tauri
  dependency-version: 2.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-07 23:20:08 +00:00
vicrodh 79e40d1ec5 Merge pre-release into main: flake bump to 1.2.10 2026-04-29 00:43:31 -06:00
vicrodh fd7b589866 chore(flake): bump to v1.2.10
Update qbzVersion, srcHash (via nix-prefetch-url of the v1.2.10
tarball) and npmHash (via prefetch-npm-deps on package-lock.json).

cc
2026-04-29 00:43:30 -06:00
vicrodh ffa7287baa Merge pre-release into main for 1.2.10 2026-04-29 00:19:40 -06:00
vicrodh e0fd0584fa chore(release): bump version to 1.2.10 + tag notes
Hotfix on top of 1.2.9 to address the gapless-play regression
introduced by dee987c1a's tightened isGaplessTransition gate combined
with a long-standing race in the player's transition-swap atomics.

Bumped:
- src-tauri/Cargo.toml + Cargo.lock
- src-tauri/tauri.conf.json
- package.json + package-lock.json
- snapcraft.yaml (version + source-tag) — packaging/snap/snapcraft.yaml
  follows automatically via symlink
- packaging/flatpak/com.blitzfc.qbz.metainfo.xml (new release entry)

cc
2026-04-29 00:19:32 -06:00
vicrodh 4fb7f92c67 Merge bugfix/internal/gapless-transition-swap-race into pre-release 2026-04-28 23:47:49 -06:00
vicrodh 43ac3e18fa fix(player): order gapless slot reset before track_id swap to close race
The audio-thread iteration that swaps in the next gapless track
mutates several atomics in qbz-player::player::SharedState. The
polling loop in lib.rs reads those atomics independently (one load
per field) at ~250 ms cadence and emits a PlaybackEvent that the
frontend handlePlaybackEvent then classifies. When the polling load
falls inside the swap window, it can observe an inconsistent mix:
current_track_id already advanced to the new track, but
gapless_next_track_id still pointing at that same track because the
reset hasn't run yet.

The frontend's isGaplessTransition predicate
(playerStore.ts) requires both `event.track_id !== currentTrack.id`
AND `event.gapless_next_track_id === 0`. The mid-swap event
satisfies the first and fails the second, so the event is mis-
classified as an external track change. The "Track changed
externally" branch then asks the queue manager for the current
queue track (still the previous track because the gapless flow
hasn't called v2_next_track yet) and prints the now-familiar
"Queue track mismatch or null: 212171 vs event: 216597" warn.

In 1.2.8 this race was self-recovering: subsequent ticks observed
the consistent post-swap state and isGaplessTransition fired one
tick later. dee987c1a tightened the gate with
`gaplessAttemptedForCurrent`, which gets reset to null by the
mid-swap event's external-track path; subsequent ticks then fail
the new gate and the misclassification becomes permanent for the
remainder of the track. UI stays on the previous title while the
audio plays the new one, and the queue cursor never advances —
visible to the user as "the player says it's still on Viento but
Afuera is playing".

Fix the race at the producer side: in both transition swap paths
(mainline pos>=dur detection and ALSA Direct take_source_transition),
clear gapless_next_track_id and gapless_ready BEFORE storing the
new current_track_id. With SeqCst ordering, any reader that
observes the new track_id is guaranteed to also observe the
cleared gapless slot — eliminating the inconsistent intermediate
state by construction. Cheaper than coordinating reads on the
consumer side and keeps the polling loop's lock-free design.

cc
2026-04-28 23:47:39 -06:00
vicrodh 9f02efb8f8 Merge bugfix/internal/gapless-request-armed-sticky into pre-release 2026-04-28 23:26:39 -06:00
vicrodh 9e7cd2973b fix(player): clear gapless flags on transition swap so next prefetch can fire
Two regressions ride together when gapless plays a chain of more than
two tracks:

  - The pos/dur snapshot at the top of the audio-thread iteration is
    captured BEFORE the gapless transition swap. After the swap
    updates current_track_id and duration, the local pos/dur still
    point at the outgoing track, and the "approaching end" check a
    few lines below fires immediately for the new track with stale
    299/299-style values.
  - That spurious firing arms gapless_request_armed=true and sets
    gapless_ready=true. gapless_request_armed only gets cleared again
    on a future transition (which will never happen because the new
    track has no prefetch queued) or on engine empty (i.e. when
    playback has already stopped). gapless_ready also stays sticky-
    true because nothing else clears it once the false-positive fires.
  - End result: the new track plays through with the gate stuck
    closed, the real "approaching end" check at ~T-10s never fires,
    the frontend never gets prompted to call play_next_gapless for
    track #N+1, and the audio thread reaches engine empty and stops.

Fix:
  - Track whether a transition swap consumed pending in this
    iteration via a local transition_consumed_pending bool. Skip the
    "approaching end" check while that flag is set — the next
    iteration will run with fresh pos/dur for the new track and
    fire the trigger correctly.
  - Reset gapless_ready=false in both transition swap paths
    (mainline and ALSA Direct), mirroring the existing
    gapless_request_armed=false reset, so neither flag survives a
    transition.

cc
2026-04-28 23:26:33 -06:00
vicrodh 64c7b674d3 Merge pre-release into main: flake bump to 1.2.9 2026-04-28 22:31:25 -06:00
vicrodh b1904ad70d chore(flake): bump to v1.2.9
Update qbzVersion, srcHash (via nix-prefetch-url of the v1.2.9
tarball) and npmHash (via prefetch-npm-deps on package-lock.json).

cc
2026-04-28 22:31:23 -06:00
vicrodh 2acdb23fae Merge pre-release into main: drop build-qbzd workflow 2026-04-28 21:40:15 -06:00
vicrodh ec3fbf1d3f chore(ci): remove build-qbzd workflow
The qbzd daemon is still in early development and the build pipeline
isn't ready to ship artifacts. Drop the workflow until the daemon
work lands so it doesn't fire on tag push and produce broken jobs.

cc
2026-04-28 21:40:13 -06:00
vicrodh f66c8627f6 Merge pre-release into main: 1.2.9 codename in About modal 2026-04-28 21:18:24 -06:00
vicrodh ca8084f1af chore(about): codename Quiet Polish for 1.2.9
Match the in-app About modal to the v1.2.9 tag notes.

cc
2026-04-28 21:18:23 -06:00
vicrodh 17f4095f79 Merge pre-release into main for 1.2.9 2026-04-28 21:13:17 -06:00
vicrodh 93a07acd9f docs(release-1.2.9): personalise tag notes
Reorganise the release notes around the user's voice: lead with the
"polish + bugfix + perf" framing, group user-visible polish first,
QConnect cleanup and the Look-and-Feel block (sidebar collage cache,
tray live tooltip, middle-click + scroll, icon variant picker, ksni
worker thread) next, and close with a thanks-and-roadmap paragraph
about the daemon work pushing the next release out a bit.

Credits @afonsojramos for the macOS contributions.

cc
2026-04-28 21:12:43 -06:00
vicrodh f17a1733f7 chore(release): bump version to 1.2.9 + tag notes
Quiet Polish: QConnect cleanup (#316) and a long round of Linux
tray polish. Sticky-header gap fix (no more padding-top on scroll
containers), Edit folder shortcut from the playlist sidebar (#364),
shift-range + Ctrl/Cmd+A multi-select across the remaining views,
opt-in resume of seek position (#317), and Qobuz subtitle/edition
propagated through queue / MPRIS / scrobbling (#360).

Bumped:
- src-tauri/Cargo.toml + Cargo.lock
- src-tauri/tauri.conf.json
- package.json + package-lock.json
- snapcraft.yaml (version + source-tag)
- packaging/flatpak/com.blitzfc.qbz.metainfo.xml (new release entry)

packaging/snap/snapcraft.yaml is a symlink to the root snapcraft.yaml
so it tracks automatically.

cc
2026-04-28 20:29:59 -06:00
vicrodh fc402506bf chore(icons): regenerate icon.icns from current logo
The bundled icon.icns was a 1024x1024 PNG masquerading as an .icns
container (placeholder, since no proper libicns tooling is installed
on the build machine). Regenerated from the current icon.svg so the
artwork at least matches the rest of the branding. A real .icns
container with multiple Apple-spec sizes still needs to be produced
by a macOS contributor.

cc
2026-04-28 20:29:52 -06:00
vicrodh 17730e0233 fix(tray): icon variant picker with Color option and mono labels
Three issues with the 1.2.9-pre tray icon override:

  - The mono PNGs filled the entire canvas, so the tray icon read as
    larger than its panel neighbours (Plasma + GNOME).
  - The dropdown labels were inverted: picking "Light icon" produced
    the dark glyph and vice-versa, because the constants were named
    after the panel they suit ("light panel" -> dark glyph), not after
    the glyph colour the user expects to see.
  - There was no way to pick the colour vinyl logo even on desktops
    where a monochromatic glyph clashes with the rest of the tray.

Rebuild the mono pixmaps (22/32/44/64) from icon-symbolic.svg with a
2px content inset; render a new colour set from icon.svg at the same
sizes; rename the Rust constants to glyph-colour names and introduce
an IconVariant enum so the UI label, the persisted value, and the
file selection all line up.

Dropdown options now read Auto / Mono light / Mono dark / Color in
all five locales. normalize_tray_icon_theme on the Rust side and
normaliseTrayTheme in SettingsView migrate the inverted "light" /
"dark" values stored by 1.2.9-pre to the user-intent equivalents.

cc
2026-04-28 20:29:49 -06:00
vicrodh 8cdcdd45a6 Merge feature/internal/tray-icon-theme-toggle into pre-release 2026-04-28 19:12:29 -06:00
vicrodh 388a150893 feat(settings): tray icon theme dropdown for Linux
Adds an Auto / Light icon / Dark icon dropdown next to the existing
tray toggles, hidden on macOS. Wires v2_set_tray_icon_theme so the
choice persists and the live SNI tray reflects it instantly.

i18n keys settings.appearance.tray.iconTheme.* added to en/es/de/
fr/pt.

cc
2026-04-28 19:02:50 -06:00
vicrodh ee713e3f0c feat(tray): persisted icon theme override with live update on Linux
Auto-detected dark vs light tray glyph fails on GNOME, whose top bar
is permanently dark even when the system color scheme is light, so
the dark glyph disappears against it. Add a persisted user override
("auto" / "light" / "dark") that the SNI tray respects at startup
and on the fly.

Backend pieces:
- TraySettings gains tray_icon_theme: String with "auto" default and
  a normalize_tray_icon_theme() helper that clamps unknown values
  back to "auto". SQLite migration ALTERs the table to add the
  column when an older DB is opened.
- tray_linux_ksni::decode_tray_icons takes an optional override and
  resolve_dark_tray() consults it before falling back to the
  existing GNOME/GTK/KDE auto-detection. init() forwards it.
- LinuxTrayHandle gains set_icon_theme(theme); a new
  TrayUpdate::SetIconTheme variant flows through the dedicated
  worker thread so re-decoding plus handle.update() happen outside
  the tokio runtime (same panic-avoidance pattern used for
  tooltip updates).
- v2_set_tray_icon_theme persists the value, mirrors it to the
  global startup store, and pushes it to the live tray handle so
  the icon flips without restart.

cc
2026-04-28 19:02:46 -06:00
vicrodh bf81814a65 Merge bugfix/internal/sticky-header-phantom-block into pre-release 2026-04-28 18:50:51 -06:00
vicrodh a9fccc6fe8 fix(views): pin sticky headers flush by removing scroll-container padding-top
Commit e05ed48d covered a real WebKitGTK gap by painting bg-primary in
two places: as a 32px upward box-shadow above .jump-nav /
.tracks-sticky-header, and as a fill on the .artist-detail /
.playlist-detail scroll containers. The upward box-shadow rendered as
a phantom dark block above the bar even when not pinned, sitting on
top of the social-action buttons in ArtistDetailView.

The actual cause is that CSS sticky pins to the scrollport's
padding-box top, not the border-box top. With padding-top: 8px on the
scroll container the pinned bar landed 8px below the visible top
edge, leaving the gap the original commit was trying to hide.

Move the 8px out of the scroll container's padding-top and onto the
first child (.back-btn margin-top in ArtistDetailView, .nav-row
margin-top in PlaylistDetailView). The sticky bar now pins flush. Drop
the upward box-shadow and the bg-primary fills on the scroll
containers — neither is needed once the gap is gone. Add 10px
padding-top inside .tracks-sticky-header so its taller toolbar doesn't
press against the titlebar when pinned.

cc
2026-04-28 18:50:02 -06:00
vicrodh e1922d20dc Merge feature/internal/playlist-folder-edit-shortcuts into pre-release 2026-04-28 16:38:34 -06:00
vicrodh 1a7ecccaf3 feat(playlist-manager): edit pencil in folder breadcrumb
Inside a folder the breadcrumb showed the folder name with no way to
reach the edit dialog from there. Adds a small Pencil button beside
the current folder name that reuses the existing
openEditFolderModal() helper, mirroring the per-card edit affordance
on the folders grid.

Closes part 2 of #364.

cc
2026-04-28 16:34:45 -06:00
vicrodh dd512ee5ac feat(sidebar): edit folder from context menu
Adds an "Edit folder" entry at the top of the playlist-folder context
menu in the sidebar, opening the existing FolderEditModal. The modal is
mounted globally in +page.svelte (separate instance from the one in
PlaylistManagerView) so it works from any view, and uses Tauri's ask()
for delete confirmation per the destructive-dialog convention.

Closes part 1 of #364.

cc
2026-04-28 16:34:42 -06:00
vicrodh d3ccf40ddd Merge feature/internal/tray-rich-tooltip into pre-release 2026-04-28 16:22:31 -06:00
vicrodh 7714d83212 fix(tray): apply tooltip updates from a dedicated thread
ksni 0.3.4 with the "blocking" feature wraps every Handle::update in
Runtime::block_on, which panics when called from inside an existing
tokio runtime — exactly where Tauri commands and our async polling
loop run. The panic was being swallowed by the runtime, so set_track
and set_playing looked like no-ops and the SNI tooltip kept rendering
the static "QBZ / Music Player / Nothing playing" fallback.

Move all updates onto a dedicated std::thread that owns the ksni
blocking handle and processes mutations from a std::sync::mpsc
channel, fully outside any tokio context.

cc
2026-04-28 15:59:25 -06:00
vicrodh 789b0262b6 feat(tray): live tooltip and scroll/middle-click controls on Linux
Replicate the Plasma media plasmoid tray behavior on Linux SNI panels:

- Tooltip now shows live track title, "by Artist", album line, and
  "Middle-click to play/pause" + "Scroll to adjust volume" hints.
- Middle-click (SecondaryActivate) emits tray:play_pause instead of
  toggling the window — the window toggle stays on left-click.
- Mouse wheel (Scroll) emits tray:volume_delta with normalised ticks
  (~120 SNI units per notch -> +/-5% volume per tick), wired to the
  existing handleVolumeChange path on the frontend so QConnect remote
  renderers and the local player both respond.

Architecture: ksni 0.3.4's blocking Handle is now stored as
LinuxTrayHandle in Tauri state. v2_set_media_metadata pushes track
fields into the tooltip, and the playback poll loop mirrors the
play/pause flag so the hint flips while the panel popup is open.

cc
2026-04-28 15:29:04 -06:00
vicrodh a47d00cd71 Merge feature/queue-stop-after into pre-release
Adds two queue actions per #356:
- Stop after this track (single transient marker, pauses on natural
  end, queue intact). Works with both standard and gapless playback
  paths.
- Remove all tracks after this (drops queue tail, toast feedback).

Backend lives in qbz-player::queue (V2 cleanroom) so headless and
TUI clients also get the feature without UI-specific code paths.
44 unit tests in qbz-player::queue, 4 Tauri commands, 5-locale
i18n with ICU plural for the toast.

Refs #356.

cc
2026-04-28 15:10:33 -06:00
vicrodh fbbbf72220 fix(playback): suppress gapless prefetch when current track has stop-after marker
Manual verification (#356) caught that the audio engine's gapless
prefetch was bypassing the marker. Sequence: ~10s before track end
the engine asks the frontend for the next track via the gapless
callback; once a successor is queued, the engine transitions
internally without firing setOnTrackEnded — so consumeStopAfterIf
never ran and the marked track's end seamlessly continued playing.

Fix: in the setGaplessGetNextTrackId callback in +page.svelte,
return null when the currently-playing track matches the marker.
With no successor pre-queued, the track ends naturally, fires
setOnTrackEnded, and the existing consume path pauses correctly.

Known edge: if the marker is set during the last ~10s of a track,
the gapless prep already happened and the transition still fires.
Acceptable for v1 — full coverage would require backend cancel of
in-flight gapless prep.

Refs #356.

cc
2026-04-28 15:10:17 -06:00
vicrodh 0d62007a1c feat(playback): wire stop-after marker into natural track-end handler
setOnTrackEnded in +page.svelte calls consumeStopAfterIf(finishedId)
before advancing. If it returns true: stop playback, set isPlaying
to false, return without advancing. Manual skip paths (player bar
Next, click on queue track) don't go through this callback, so the
marker correctly only fires on natural reproduction. Cast/DLNA
natural-end is covered by the same callback per playerStore.ts:631-633.

Refs #356.

cc
2026-04-28 14:37:32 -06:00
vicrodh 32c7cbd2ff fix(queue): use ICU plural for tracksRemoved toast
Per code review on commit cdf30a7b: the tracksRemoved key was
hardcoded plural ("1 tracks removed" reads wrong). Convert to ICU
plural form across all 5 locales using svelte-i18n's MessageFormat
support. Also align the new handlers with the existing closeMenu()
pattern for consistency.

Refs #356.

cc
2026-04-28 14:31:35 -06:00
vicrodh cdf30a7b74 feat(queue-panel): add stop-after and remove-after menu entries
Two new context-menu entries on queue tracks:
- Stop after this track (dynamic label: shows "Cancel" if already set)
- Remove all tracks after this (hidden on the last track)

Visual indicator: marked track replaces its index display with a
small CircleStop icon in the accent color.

Refs #356.

cc
2026-04-28 14:26:58 -06:00
vicrodh 813e4dd76e fix(i18n): restore player.queue string and put new keys in top-level queue namespace
Commit e40587e1 converted player.queue from a string to an object to
accommodate the new stop-after / remove-after keys. That broke five
call sites that read $t('player.queue') expecting the localized
"Queue" string (QueuePanel, ImmersiveHeader, ExpandedPlayer,
NowPlayingBar, immersive QueuePanel).

Restore player.queue to its original string form in all 5 locales,
and add a new top-level queue namespace for the four action keys
(stopAfterThis, cancelStopAfter, removeAllAfter, tracksRemoved).
This matches the plan's intended key paths: queue.stopAfterThis etc.

Refs #356.

cc
2026-04-28 14:23:01 -06:00
vicrodh e40587e1cb i18n(queue): add stop-after and remove-after keys to all 5 locales
en.json + es.json + de.json + fr.json + pt.json.

Refs #356.

cc
2026-04-28 14:16:18 -06:00
vicrodh 2f5a2b1b89 fix(queue-store): clear stopAfterTrackId in reset()
Per code review on commit ec6c99dd: the reset() function (called on
logout / user switch) was clearing all other queue state but missed
the new stopAfterTrackId writable, leaking marker state into a fresh
user session. Add the explicit clear.

Refs #356.

cc
2026-04-28 13:58:09 -06:00
vicrodh ec6c99dd81 feat(queue-store): add stop-after store and remove-after action
Derived store stopAfterTrackId reads from queueState (single source
of truth). Actions: setStopAfter / clearStopAfter / toggleStopAfter
mutate via v2_queue_* commands. consumeStopAfterIf is for the
natural-end handler. removeAfter wraps v2_queue_remove_after and
returns count for toast display.

Refs #356.

cc
2026-04-28 13:54:32 -06:00
vicrodh 27295dea98 fix(commands_v2): write-lock and debug-log v2_queue_consume_stop_after_if
Per code review on commit 97b5bd79:
- consume_stop_after_if mutates inner state on match (clears the
  marker), so use write().await for semantic consistency with the
  other mutating commands. The inner Mutex made read() work today
  but the intent is mutation.
- Demote the per-call log to debug — this fires on every natural
  track-end and would spam info-level logs during normal listening.

Refs #356.

cc
2026-04-28 13:52:23 -06:00
vicrodh 97b5bd79c7 feat(commands_v2): add four queue commands for stop-after and remove-after
v2_queue_set_stop_after / v2_queue_clear_stop_after / v2_queue_consume_stop_after_if
/ v2_queue_remove_after — thin wrappers over QueueManager via CoreBridge. The
consume command is what the frontend's natural-end handler calls before
advancing; manual skip paths do not call it.

Refs #356.

cc
2026-04-28 13:47:56 -06:00
vicrodh 3fe06195c6 feat(queue): expose stop-after marker through QueueState
The marker becomes part of the queue state snapshot the frontend
already consumes via syncQueueState(). Single source of truth — no
parallel event channel, no separate store on the frontend.

Refs #356.

cc
2026-04-28 13:23:38 -06:00
vicrodh 098176e0e3 feat(queue): add remove_after to drop tail of queue from a pivot
Removes all tracks at indices > pivot, preserving the pivot itself.
Returns the count removed. Updates shuffle_order and history to
drop dangling indices. Auto-invalidates the stop-after marker if it
referenced any removed track.

Refs #356.

cc
2026-04-28 13:20:26 -06:00
vicrodh 17dd52aa35 feat(queue): invalidate stop-after marker on queue mutations
set_queue, set_queue_with_order, and clear unconditionally drop the
marker (queue replaced/emptied → marked track may not exist anymore).
remove_track and remove_upcoming_track conditionally clear the marker
when the removed track matches it. move_track and add_* are unchanged
— a moved track is still the same track ID, and adding doesn't touch
existing tracks.

Refs #356.

cc
2026-04-28 13:16:40 -06:00
vicrodh 23ebe9bf1f feat(queue): add consume_stop_after_if for one-shot marker firing
Auto-advance drivers (frontend natural-end handlers) call this with
the just-finished track ID. If it matches the marker, returns true
and clears the marker; the caller pauses instead of advancing.
Otherwise returns false — caller proceeds with normal advance.
Manual skip paths must NOT call this method.

Refs #356.

cc
2026-04-28 13:13:28 -06:00
vicrodh d36adcbb4a feat(queue): add stop-after marker field and basic API
Add `stop_after_track_id: Option<u64>` to InternalState plus three
methods on QueueManager: `set_stop_after(id)`, `clear_stop_after()`,
`get_stop_after()`. Setting on an ID not present in the queue is a
silent no-op so the marker can never reference a phantom track.

Refs #356.

cc
2026-04-28 13:10:40 -06:00
vicrodh af41d66f18 fix(scrobble): enrich track title with version for Last.fm and ListenBrainz
The metadata fix from #360 wired formatTrackTitle() into the UI and
MPRIS so remix tracks display as "Title (Version)", but the Last.fm
and ListenBrainz scrobble paths in playbackService.ts were still
sending track.title raw. Result: a remix played in qbz scrobbled as
the original song to both services.

Apply formatTrackTitle() at the caller of updateLastfmNowPlaying,
updateListenBrainzNowPlaying, and showTrackNotification. Both the
immediate now_playing and the deferred scrobble (plus the offline
queue) capture the title from the same closure variable, so a single
change at the caller covers all paths going forward. Refs #360.

cc
2026-04-28 09:21:51 -06:00
vicrodh 7eb8fc1533 fix(snap): bump source-tag to v1.2.8 to match version field
The snapcraft.yaml `version:` field was bumped to 1.2.8 but
`source-tag:` under parts.qbz still pointed to v1.2.0. The Snap
Store build farm clones the repo and runs `git checkout v1.2.0`,
so every revision since 1.2.1 was published with metadata "1.2.X"
but a binary compiled from v1.2.0 source — silent regression
visible only when the user opens the app and sees the wrong
version reported.

Affects 8 releases (1.2.1 through 1.2.8). Fixes #361.

cc
2026-04-28 09:21:43 -06:00
vicrodh 2ebb874f43 fix(icons): improve Linux tray display and refresh assets (#362)
Returning an empty `icon_name` in `tray_linux_ksni.rs` forces SNI
panels (e.g., KDE Plasma) to render the explicit `IconPixmap` data.
This prevents panels from incorrectly resolving the full-color app icon
from the system theme instead of the intended monochrome tray glyph.

Also updates `src-tauri/icons/icon-symbolic.svg` and regenerates
all `tray-dark-*.png` and `tray-light-*.png` assets.
Adds `static/logo.svg` as the main application logo.
2026-04-28 07:37:48 -06:00