Commit Graph
106 Commits
Author SHA1 Message Date
vicrodh e0e1bdfbee feat(qt): multi-select removes tracks from a playlist
The playlist view's multi-select toolbar gains "Remove from playlist" for
the owner of the open playlist, behind a confirm modal that names the
count. One dispatcher (`playlist_remove_tracks`) routes the picked row ids
by the open document's kind: a local playlist removes by stored position
(descending, one blocking pass, then reload); a mixed playlist drops its
sidecar rows and batches the remaining catalog ids into a single
`remove_tracks_from_playlist` call with an optimistic document patch.

Row positions are resolved before any delete so the compaction the local
repo performs after each removal cannot shift a later target.

cc
2026-09-14 02:29:16 -06:00
vicrodh 47b5c14d32 feat(qt): two Wallpaper background modes, with the desktop's picture or your own
"Dynamic background" gains "Wallpaper" and "Wallpaper, blurred". The
picture is the desktop's wallpaper — Plasma's appletsrc (the containment
on screen 0, image plugin only; a wallpaper package resolves to its
largest bitmap, a slideshow folder to its first image), GNOME through
gsettings (the dark variant under a dark scheme), Windows' current
TranscodedWallpaper, macOS through System Events — or an image the user
picks (Settings > Appearance > Background image), which outranks it. The
resolved path and its mtime are memoised, so the frequent refreshes
(session entry, a mode switch, the window coming to the front) republish
nothing while the file holds.

"Wallpaper, blurred" sends the picture through the same atmosphere pass
Blurred art gives the cover, held still. "Wallpaper" shows the part of
the wallpaper that lies behind the window, lightly blurred under the
half-alpha chrome, at most a screen-sized texture: the window's place on
the screen comes from Qt where the platform tells it (X11, macOS,
Windows); Wayland hides window positions, so there the crop is centred
until the Plasma window-management protocol is wired. Neither mode waits
for a playing track. Six new strings in eight catalogs.

cc
2026-09-14 01:54:57 -06:00
vicrodh 119fab64ee i18n: strings for View label, Copy submenu, Buy on Qobuz and the unavailable-release page
Twelve new msgids in all eight catalogs.

cc
2026-09-13 20:26:11 -06:00
vicrodh 3f3c4ad548 feat(qt): Track info and Album info are selectable and copy basic or full text
Every field of both modals is a QbzSelectableText — artist, label and
musician names stay links. A copy button beside the close cross opens a
flyout: "Copy basic data" puts "{Track} - {Album} - {Artist}" (or
"{Album} - {Artist}") on the clipboard; "Copy full" formats the whole sheet
as plain text — the track's metadata, credits and copyright, or the album's
header plus the ACTIVE tab: credits per track with performers, or the
review. Album credit row heads stay plain because their click expands the
row. The two new strings ship in all eight catalogs.

cc
2026-09-13 20:03:36 -06:00
vicrodh 41ca92961a feat(qt): header nav goes compact only when the text tabs no longer fit, and compact items honour the click rule
The compact icon form is now a fit decision, agnostic of the sidebar state:
text tabs while every label at the 11 px floor plus the tab chrome and
whatever trails the tabs (the playlists button a closed sidebar adds) fits
left of the search box, icons only otherwise — or always with "Compact
header navigation" on. A closed sidebar used to force the compact form by
itself; it now only adds one button to the measure, so a window with room
keeps its text tabs. The room is measured from the third sacred button,
not from the hidden row (a Row leaves a hidden child's x where it was).

The downloads ring and the separator + playlists flyout button move out of
the compact row into a trailing group shared by both forms, so a closed
sidebar with room keeps its playlists entry; the group hides outright when
it has nothing to show.

Compact section buttons now call NavFlyout.sectionClicked like the text
tabs and the sidebar rows do, so under the click-navigates opt-in a click
on an icon lands on the section's first entry instead of only opening the
flyout — Purchases was the one compact item that navigated.

Settings > Navigation describes the new rule in all eight catalogs (en
never carried the old entry). The Purchases navigation gate models
headerNavOn and the fit bit: 128 x 3 combinations, still exactly one
reachable Purchases entry.

cc
2026-09-13 16:27:53 -06:00
vicrodh 1ba578a1d8 feat(qt): move a playlist to a folder from its editor, delete one from any context menu
The editor gains a Folder row — the same folders the sidebar and the
manager file playlists into, "No folder" for the root — seeded from the
playlist's current folder and applied on Save through the manager's own
move verb, so an existing playlist no longer needs the sidebar's row menu
to be filed (#785). "Delete playlist" joins the sidebar row menu (own and
local playlists; sidebar entries now carry isOwner) and the playlist card
menu (own playlists), behind a shell-level "Delete playlist?" confirmation
that runs the same delete the editor's button runs and leaves the detail
page only when it was the deleted one (#776). Eight catalogs updated.

cc
2026-09-13 14:47:12 -06:00
vicrodh a106ef790c feat(qt): Qobuz Connect lists renderers on this network and pairs them
A LAN-only receiver — a BluOS player, another QBZ — was invisible until an
official app paired it into the session; that is the "it showed up once,
for a day" report. The Qt LAN runtime now browses _qobuz-connect._tcp
alongside its own receiver (same lifetime: Qobuz Connect enabled and
ready), probes every announcement that is not us, and the picker shows the
answers under "On this network" with a Pair action. Pairing mints the
delegated pair with /qws/delegateAuth for the renderer's app id and hands
it over with this controller's session uuid and become_active, exactly as
the official controllers do; the cloud then lists the device and its LAN
row drops out by uuid. scripts/qconnect-lan-fake-renderer.sh is a receiver
that never joins the cloud, for exercising this without a Node. Pending:
qbzd parity, the 5-minute retry backoff, become_active=false.

cc
2026-09-13 13:46:13 -06:00
vicrodh 228a6f12f3 chore(i18n): drop the retired Purchases placement strings from the seven translated catalogs
cc
2026-09-13 11:31:29 -06:00
vicrodh db7ddc5edd feat(qt): Purchases follows the section navigation, sidebar or header
The separate "Purchases in title bar" placement toggle is gone: with Show
Purchases on, the entry sits wherever the other sections are — in the
sidebar while the navigation lives there, in the header beside the full
tabs or the compact buttons otherwise — so it is never the one entry left
behind. The nav_tb_purchases pref and its bridge key are retired; the
Show Purchases description says where the entry goes.

cc
2026-09-13 11:28:44 -06:00
vicrodh d22907b0e0 feat(qt): Settings > Audio > Loudness — normalization toggle, target loudness presets, clipping guard
The bars' audio flyout keeps its switch; Settings > Audio gains the same
key with its two companions: a Target loudness select (-11 / -14 default /
-16 / -18 / -23 LUFS, a value set elsewhere shows as a display-only
"Custom (N LUFS)" entry instead of being re-mapped) and the Prevent
clipping toggle. Eight catalogs updated.

cc
2026-09-13 11:25:22 -06:00
vicrodh e8351e57ba feat(qt): Settings sub-nav regrouped under an Advanced fold, with new glyphs
Audio, Playback, Appearance, Navigation, Local Library, Offline,
Integrations and Updates stay in the column; Blacklist, Storage,
Import / Export, Developer (and Orbit while enabled) move under an ADVANCED
disclosure row with a chevron. The fold is not persisted: it opens itself
whenever the current section lives inside it, and reads as active while
closed over the selected page. Appearance takes lucide's
paintbrush-vertical, Integrations unplug and Storage database — baked into
every tint and registered as masters. The kiosk selector follows the same
order, flattened. "Share logs" stays pinned at the bottom.

cc
2026-09-13 10:26:34 -06:00
vicrodh 198fbfc6fe feat(qt): the offline cache is purged from the manager only, with a tooltip and an explicit modal
Settings > Offline loses its "Clear cache" row: it ran the same
QbzOffline.clearAll() as the trash glyph in the Offline Cache Manager's
stats bar, so the whole cache had two entry points and one misleading text
("your downloaded albums are kept" — what is kept are the purchased
downloads in the music folder). The glyph now carries a hover tooltip (the
manager mounts the shared QbzTooltip overlay; the limit and folder glyphs
get one too) and its confirmation says exactly what goes — the live track
count and size — and what stays. The Tauri-era string leaves the eight
catalogs; the new ones enter them.

cc
2026-09-13 10:26:33 -06:00
vicrodh 9b93e58367 refactor(qt): Offline cache and Playback cache return to their own sections
Settings > Storage keeps only the caches that have no section of their own:
the artwork cache, the lyrics cache and the Plex mirror. The offline
downloads group goes back to Offline and the playback cache (memory profile,
usage, storage folder) back to Playback, since both configure the feature
they sit next to rather than disk in general. Rows and bridge verbs are
unchanged; the playback cache QML test instantiates PlaybackSettings again
(16/16) and the .po location comments follow the artwork rows to Storage.

cc
2026-09-13 10:09:59 -06:00
vicrodh 71eee01e4c feat(qt): featured artists on track rows are on by default
The preference stays (opt-out); an absent key reads as on so rows never
flash without the names before the first settings snapshot.

cc
2026-09-13 09:20:21 -06:00
vicrodh 29710342a2 feat(qt): Settings > Storage — every on-disk cache in one section
Offline downloads, the artwork cache, the lyrics cache, the Plex mirror and
the playback cache (memory profile + storage folder) move to a new Storage
section, each row keeping the bridge verb it had. Offline keeps the mode
rows, Playback keeps the behaviour rows, Plex keeps Disconnect.

cc
2026-09-13 09:09:30 -06:00
vicrodh 903952cacb fix(artwork): bound the image cache during use, make the budget a setting, sweep orphans
The boot trim alone let a long session grow the folder past the budget
again (an early release reached 1 GB). A trim now also runs on the
blocking pool every 16 MB of new originals, the budget comes from
ui_prefs image_cache_max_mb (100/200/500/1000 MB, 200 default), .img files
with no row older than an hour are swept at boot, and the settings
document carries the stats, the size options and a clear action.

cc
2026-09-13 09:09:29 -06:00
vicrodh 04e5c50045 feat(qt): featured performers as a comma-separated list after the main artist
cc
2026-09-13 08:20:44 -06:00
vicrodh 9174b33b1b feat(qt): A-B loop from the player bar's + menu
Mark A, mark B, loop between them until cleared; local playback only,
never persisted, cleared on track change or a seek outside the section.
A 100 ms watcher reads the millisecond position and reissues the local
seek; the seekbar shows the section and its two marks.

cc
2026-09-13 03:50:29 -06:00
vicrodh 4824c672fa feat(qt): opt-in ±10 s skip buttons beside Previous and Next
Settings > Appearance > Player & Visuals, mirrored on QbzPlayer.showSkipTen;
hidden on hosts too narrow for the cluster.

cc
2026-09-13 03:50:28 -06:00
vicrodh c2c8d7de57 feat(qt): opt-in "feat." featured artists on the track-row artist line
Settings > Appearance > Player & Visuals. Qobuz rows only; the artist line
stays a single text so the artist link keeps its hit area.

cc
2026-09-13 03:50:10 -06:00
vicrodh e291eb3af4 fix: stabilize playback memory, local library sync and Qt settings
Add shared playback memory profiles, bounded disk-backed playback and storage
selection for desktop and daemon. Preserve gapless during streaming-only playback
and provide explicit cache eviction with playback confirmation.

Stop watcher read events and unchanged scans from rebuilding the local catalog.
Restore playlist/sidebar updates, folder availability feedback and player layout.
Add appearance presets and a radio-based cache settings panel with translated
trade-offs, plus consistent clipboard menus across visible Qt text editors.

Validate with Rust regressions, QML interaction tests, isolated release startup
and idle-library checks; extend the shared local and CI gates.

cx
2026-09-13 02:57:21 -06:00
vicrodh 7a22fd6867 feat(updater): add native update channels and one-line installation
Support Flatpak portal updates and signed Windows MSI upgrades. Add the Linux/macOS installer for 2.1.2, release signing, translations and regression coverage.

cx
2026-09-12 16:25:08 -06:00
vicrodh 64ec3e2bc8 fix(updater): restore Qt release checks and signed installation
cx
2026-09-12 15:20:23 -06:00
vicrodh 74e1610433 feat(orbit): add shared library foundations and gated host tools
Extract the HTTP host and catalog layer into qbz-control, preserve the
existing daemon API, and keep private session credentials out of SSE.

Scope library stores and scan workers to their host. Share bounded source
search and show Local results alongside Qobuz in the Qt search page.

Add the launch-gated Qt Orbit laboratory and persistent remote-context
banner. Let qbzd run --orbit own and administer its file library through
instance-scoped API and CLI operations. Full remote UI control remains
pending.

Validate library/control/daemon suites, Qt search and QML regressions,
two isolated daemon processes, Qt release build and final cargo check.

cx
2026-09-12 13:02:29 -06:00
vicrodh e402fa250a feat(myqbz): collapsible My QBZ sidebar with per-element hide and toolbar
Opt-in collapsible My QBZ tree in the sidebar (Settings > Navigation): a
branding-titled parent over Mixtapes and Collections sections, each header a
link to its grid with a chevron that only collapses. Header text and icon on
the left, a Playlists-parity toolbar floating right (search, new, sort A-Z/Z-A,
refresh, collapse) with the chevron aligned to the playlist carets. The tree
feed rides QbzShell (QbzMyQbz is not reliably registered in the shell context)
and is published through publish_sidebar, so it lands reliably at startup.

Per-element visibility reuses the mixtape hidden flag (repo::set_hidden): a
row's right-click hides it from the sidebar, and the detail hero menu toggles
"Hide from sidebar" / "Show in sidebar". Collapse state for the playlist tree
and all three My QBZ sections is persisted. The collapsible tree and the flat
My QBZ entry are mutually exclusive — the classic row is suppressed whenever the
tree is on. Settings gains a Navigation section (Navigation + Search moved out
of Appearance). Detail view also gains a discreet, column-aligned sub-header
over the inline track list. Eight locales updated.

cc
2026-09-10 20:19:48 -06:00
vicrodh cc50b01dca fix(myqbz): mixtape/collection detail row corrections (#766)
- A song row opens the album it is on (its resolved, source-aware albumId)
  instead of a blank page; the album id is plumbed from the first resolved track
  through the resolve pass, and an unresolved click is ignored like the artist
  link. (#766 problem 1)
- The row menu says "Remove from Mixtape" on a mixtape, "Remove from collection"
  elsewhere. (#766 problem 2)
- A playlist inside a mixtape hides its quality cell (per-track quality is
  varied); collections keep it.
- The source column shows one glyph per DISTINCT source across the item's
  tracks, so a mixed-source playlist shows them all.
- The tracks column is empty only for a track; an album or playlist shows its
  resolved track count instead of a dash.

cc
2026-09-10 16:11:44 -06:00
vicrodh 89da9bbd13 feat(scene): loading heads-up and a 5-minute retry timeout
The ArtistScene discovery fans out many MusicBrainz requests, so a throttled MB
can keep it loading a while. After ~10s the loading screen shows a "this can
take a few minutes" note; after 5 minutes it swaps the spinner for a Retry (a
late success still swaps in the grid on its own). Both derive from the existing
pulse-driven clock, so no new Timer and the repaint law holds. Two new strings
across all eight locales.

cc
2026-09-10 13:23:53 -06:00
vicrodh 04f6ef44e4 feat(qt): 2.1.x UI polish round — header selects, seekbar, shortcuts, per-tab filters
- Homologate the three Library header selects to the ambient Group style; Sort
  gains an arrow-down-up glyph, a "Sort" placeholder and an explicit Default,
  and every select's border clears when it returns to its default value.
- Seekbar fluidity: a drag scrubs locally and commits a single seek on release;
  the time readout is a floating opaque tooltip reparented to the overlay so the
  content-frame shadow no longer bleeds noise over it.
- Route the NowPlayingBar info button and title to the LOCAL track-info modal
  for a local row, so a local track no longer opens an empty Qobuz modal.
- App menu shows each entry's shortcut (muted, smaller, right-aligned, resolved
  per platform — Cmd on macOS, Ctrl elsewhere) and gains a Quit entry; the
  keyboard-shortcuts modal adds Close Window and Quit under Navigation, moves
  Immersive below Interface, and tightens row heights. i18n for Quit/Close
  Window across all eight locales.
- Persist each Library tab's filter toggles to disk, independently per tab.

cc
2026-09-10 11:21:50 -06:00
vicrodh 2fc03d325d fix(qt): restore window lifecycle and improve grid artwork
Restore tray-aware close and quit confirmation, circular label covers, and missing label artist portraits. Publish artwork progressively, preload two grid rows, and skip redundant motion reports while preserving data refreshes.

Include regression tests and isolated artwork benchmarks. Verified Linux build, QML checks, and scrolling smoke tests.

cx
2026-09-09 21:58:25 -06:00
vicrodh 75b8eee1c6 feat(media): add Jellyfin Quick Connect and harden remote connections
Add cancellable Quick Connect pairing and shared connection guards for Jellyfin and Subsonic. Bound connection attempts, validate library access before saving credentials, and report per-provider connection and synchronization status.

cx
2026-09-09 17:12:45 -06:00
vicrodh 17e08e93de fix(library): restore offline visibility and unify filters and sorting
Recover missing offline library entries, recognize cached-only libraries, and refresh availability after cache changes. Unify per-tab filters, compact the toolbar, and add reversible sort criteria with complete available purchase metadata.

cx
2026-09-09 17:08:33 -06:00
vicrodh 0b6b111c74 fix(qt): correct playback commands and library browsing
Handle explicit MPRIS Play/Pause without toggling paused playback, remove the 10,000-favorite cutoff, and sort albums by original release dates. Isolate Library filters by tab, include purchased release dates, and support server-ranked artist popularity with safe pagination.

Validated with 766 Qt tests, QML checks, release-sort regressions, and a release build. Refs #750, #751.

cx
2026-09-08 18:27:51 -06:00
vicrodh 0ebcab6d3e fix(library): support SACD artwork fallback and image sidecars
Refresh missing artwork for unchanged SACD images when a folder cover is added after scanning. Enable image-specific metadata and cover sidecars in the Qt editor while preserving physical track numbers and never writing the ISO.

Validated with SACD regressions, the real Heathen image, 13 Qt editor tests, QML audits and the combined release build.

cx
2026-09-08 15:26:03 -06:00
vicrodh 31da9c57bb fix(qt): kiosk feedback round — chrome, search, album, artists, lyrics
Address the owner's kiosk feedback on top of the release hardening:

- Chrome: the back bar is a drag surface and shrank to 44px with one shared
  button form for Back/Forward/Connect/Cast; Qobuz Connect and Cast are a
  permanent cluster in the bar; the dead Share-logs button left the kiosk
  Settings header.
- Transport bar: the hidden overflow menu is gone; Volume is always visible
  with a vertical sheet, and Desktop mode has its own button.
- Now Playing: one centred transport row (New-bar order); lyrics are centred
  (shared LyricsLinesView gains an opt-in centered, desktop default false).
- Local Library: the media funnel and an Open (ephemeral) entry point reach
  the kiosk; Artists is its own multi-column round-card grid (kiosk-only, the
  desktop LocalArtistsTab is untouched); the Artists drill-down query no
  longer hangs off a stale derived guard.
- Search: the All-tab dashboard shelves render again — they were mounted in a
  Loader Component that read the delegate id under reuse and bound an empty
  model; they are direct children now.
- Album detail: icon actions the height of the quality badge plus play next /
  later / queue, an inline floating quality badge, and zebra rows; both album
  headers show a quality badge.
- Immersive: a quiet auto-hiding kiosk exit moved to the bottom, clear of the
  window controls.

Regression coverage in scripts/test_qt_kiosk_feedback.mjs (wired into the
runtime gate). Verified on a private authenticated Xvfb; QML audits 6/6,
Node gate 5/5, release build 0 warnings.

cc
2026-09-07 12:33:28 -06:00
vicrodh b1dba198c9 feat(qt): kiosk profile release hardening
Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus
22 shared opt-in hosts, shared navigation history with Back/Forward, Albums
as the default library tab, native paged Local Library (artists/tracks),
lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and
artwork sized to the physical frame, static skeletons, 64px transport bar,
Settings touch reflow and a persistent Immersive exit.

Desktop keeps its prior behavior: every shared component defaults to the
desktop path and only opts into Kiosk behavior through explicit properties
(origin-relative ScrollMemory, viewport-bounded artwork requests).

Regression coverage: kiosk navigation and artwork Node tests added to the
runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n
strings for all eight locales.

Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and
1280x800 and a Desktop A/B with seven pixel-identical captures.

cc
2026-09-07 08:23:11 -06:00
vicrodh 39ba3282f5 fix(ci): eliminate test-crates build warnings
Correct dead platform declarations, stale rustdoc and test warnings; validate complete gettext headers. Install LLD in the Qt job and explicitly validate committed shader packs without rebaking them. Keep the independent shader bake gate and add three build-policy tests.

The full local gate passed 2398 workspace tests and 715 Qt tests, QML audits, shader baking and the isolated offscreen smoke without compiler, rustdoc or gettext warnings.

cx
2026-09-05 20:31:05 -06:00
vicrodh f063646d4a merge: integrate QConnect controller hardening
Preserve unavailable library replacement changes from pre-release. Combine additive translation entries from both branches in all eight locales.

cx
2026-09-05 18:38:00 -06:00
vicrodh 9b3b52cf9c fix(qconnect): preserve controller state across renderer handoffs
Keep queue reads from starving controls without weakening real queue-write serialization. Restore official proto3 zero defaults and next/previous loop, shuffle and autoplay semantics.

Project controls from the active peer state and target exact upcoming queue occurrences without optimistic cursor changes or local playback fallback. Cover takeover, seek, mute, wire decoding and manual skip regressions in the CI gate.

cx
2026-09-05 18:37:07 -06:00
vicrodh 316a0c8f46 fix(qt): replace unavailable library favorites
cx
2026-09-05 17:31:17 -06:00
vicrodh 93ba7584b0 fix(qt): polish purchases queue and search loading
cx
2026-09-05 14:39:07 -06:00
vicrodh f01950571d fix(purchases): restore local copy and download semantics
Restore downward stream-format downloads while keeping purchased masters on the download grant. Make Purchases playback prefer the highest complete local copy, repair duplicate copy inventory, and move copy management into a bounded modal with safe library and record actions.

cx
2026-09-05 12:10:55 -06:00
vicrodh 4bb4982312 feat(sacd): support raw and DST-compressed ISO playback
Parse resilient SACD images, decode DST, scan ISO releases into the local library, and keep explicit opens ephemeral.

Ship decoder notices across distribution formats and align DSD album filtering and version selection.

cx
2026-09-05 10:00:24 -06:00
vicrodh 2fdf8878dc i18n: add the artist-page and Qobuz-links strings to all eight locales
"Play random", "Play selected", "Cancel selection", "QOBUZ LINKS", "Open
Qobuz links in QBZ" and the toggle's description. msgfmt --check clean.

cc
2026-09-04 13:10:42 -06:00
vicrodh 30912962d3 feat(library): refine track source controls
Preserve the remaining queue when shuffle is disabled, expose clear transport state tooltips, retain Library source filters for the session, and add independent track-artwork preferences for Queue and Library.

cx
2026-09-03 14:53:22 -06:00
vicrodh c0a37e6b87 feat(purchases): manage independent download copies
cx
2026-09-03 05:13:05 -06:00
vicrodh 561b3c25c7 feat(purchases): gate album actions by entitlement
cx
2026-09-03 04:32:23 -06:00
vicrodh 247c0403b7 feat(library): type folder registration and queue scans
cx
2026-09-03 03:58:44 -06:00
vicrodh 8aab112afe feat(dsd): support seeking in direct playback
Add random access for DSF, DFF, and SACD demuxers, replace live DoP/native sources without releasing ALSA, and expose seek through Qt, MPRIS, and the daemon API.

cx
2026-09-03 02:32:52 -06:00
vicrodh 35ff1818f6 feat(qt): say "not yet available" on pre-release tracks
/album/get sends streamable_at and release_date_stream per track: a future
value marks a pre-release, null a pulled track. Track carries both fields
and Track::is_upcoming applies the one rule (future streamable_at wins,
then a future release date); the album, playlist, search and label rows
publish qobuzUpcoming and the unavailable glyph reads "This track is not
yet available" instead of "no longer". Artist page rows carry no dates and
keep the generic copy. New msgid in the seven translated catalogs.

cc
2026-09-02 20:34:58 -06:00
vicrodh 472706f84e fix(settings): harden migration and accessibility
Apply interface scaling through Qt at startup, complete keyboard navigation, move account migration into setup and progress modals, and defer Subsonic sync while the server scans.

cx
2026-09-02 14:33:08 -06:00