Commit Graph
199 Commits
Author SHA1 Message Date
yonlu 8cd4914842 fix(17-02): refresh cover art URLs after save
After a successful save, re-fetch albums alongside tracks and re-resolve
cover art URLs from the updated album data. Previously the dialog only
refreshed this.track but kept stale this.coverArt URLs pointing to the
old content-hash files, causing the image to revert until reopen.
2026-03-18 10:54:17 -04:00
yonlu d7c2965752 fix(17-02): fix cover art replace and remove
Three issues fixed:

1. asBytes() helper for []interface{} → []byte conversion — same
   float64 deserialization issue as numeric fields. Cover art data
   from the frontend arrives as []interface{} of float64, not []byte.

2. DB sync for cover art — was a placeholder no-op. Now saves image
   to covers cache dir (content-hash dedup + thumbnail generation),
   upserts cover_art row, and updates release_groups.cover_art_id.
   Clear sets cover_art_id to NULL on linked release groups.

3. Frontend ReadFile returns base64 string (Go []byte JSON encoding),
   not number[]. Decode with atob() before creating Uint8Array for
   preview blob URL.
2026-03-18 10:36:17 -04:00
yonlu ffcdc41b0d fix(17-02): refresh track-details dialog data after successful save
After saveEdit() succeeds, re-fetch tracks from library store and update
this.track with the fresh data so the dialog shows updated values instead
of the stale snapshot passed via show().
2026-03-18 01:23:34 -04:00
yonlu 265a9ea8ce feat(17-02): implement save flow, cover art editing, and error handling
- Wire saveEdit() to WriteTrackTagsByPath with diff-only TagChanges map
- Add cover art selection via ImageFilePicker with instant blob preview
- Add cover art removal (× button) with clearCoverArt state
- Show saving indicator and disable buttons during save
- Display errors inline in action bar; edit mode stays active on error
- Add ReadFile Go method on FrontendUtil to read cover art bytes
- Add Wails binding for ReadFile
- Clean up all edit state (editValues, pendingCoverArt, errorMessage) on close/cancel
2026-03-17 21:20:01 -04:00
yonlu fc5cf70e4c feat(17-01): add TrackMetadataChanged handler and remove selection gate on Track Details
- LibraryStore now listens for TrackMetadataChanged and invalidates all caches
- Track Details context menu item visible for any right-clicked track in all 4 views
- Removed selectionCount === 1 gate from track-list, queue-panel, cover-grid, playlist-details
2026-03-17 21:04:57 -04:00
yonlu 4235b4a4d5 feat(17-01): add WriteTrackTagsByPath and ImageFilePicker backend methods
- WriteTrackTagsByPath resolves filePath to trackID via GetAudioFileByPath
- ImageFilePicker opens native file dialog filtered to JPEG/PNG
- Added Wails TypeScript bindings for both new methods
2026-03-17 21:03:17 -04:00
yonlu 64322f9353 feat(16-03): WriteTrackTags pipeline with player safety, scan mutex, events, and app wiring
- TagWriter struct with WriteTrackTags entry point orchestrating full pipeline
- PlayerStopper interface + playerAdapter for import-cycle-free player safety
- PipelineLocker interface + pipelineMu on Library for scan/write mutual exclusion
- TrackMetadataChanged event constant + auto-generated TypeScript events
- TagWriter wired into app.go as Wails binding with SetContext in OnStartup
- 5 pipeline integration tests: player safety, scan mutex, orphan cleanup, genre relink, full DB sync
- Lint fixes: static error, else-if, wsl cuddle, golines
2026-03-17 10:55:10 -04:00
yonlu 9f595b7ac1 fix(13-02): resolve phantom playlist tracks using M3U8 paths after scan
- Add ScanHooks callback struct to library package (follows RemovalHooks pattern)
- Move phantom resolution from library to playlist service via hook
- New ResolvePhantomTracksAfterScan reads M3U8 files and resolves paths
  against current audio_files using multi-root resolution
- Handles pre-existing phantoms (match by M3U8 position) and new ones
  (match by phantom_file_path)
- Delete old resolvePhantomTracks method that required phantom_file_path
- Wire ScanHooks in app.go OnStartup
2026-03-16 12:47:42 -04:00
yonlu f05d2bb603 fix(13-02): defer virtualizer event delegation until element exists
- Event delegation handlers were attached in firstUpdated(), but the
  virtualizer is conditionally rendered (hidden when tracks empty/loading)
- On first render, tracks are [] so virtualizer doesn't exist, and
  firstUpdated() never fires again — delegation was never attached
- Move delegation to a guarded helper called from both firstUpdated()
  and updated(), so it attaches as soon as the virtualizer appears
- Fixes click, multi-select, context menu, double-click, and drag in
  both track-list and queue-panel components
2026-03-16 11:23:35 -04:00
yonlu 42b8cf9f52 feat(13-02): add library filter dropdown and wire all views to respect active filter
- Add selectedLibraryId state + ByLibrary conditional calls to library-store
- Add library filter pass-through methods to library-controller
- Create library-filter dropdown component in top bar
- Wire genre-details, cover-grid, artists-view, genres-view to use ByLibrary queries
- Update album-selection helper to respect library filter
- Regenerate Wails bindings for new ByLibrary methods
- Search remains client-side (automatically filtered by loaded data)
- Playlists remain unfiltered (use separate playlist store)
2026-03-16 09:41:27 -04:00
yonlu a4eac394ce perf: inline SVGs, memoize grid slices, batch store notifications
- Replace wa-icon shadow DOM with inline SVG in queue-panel (xmark)
  and album-dropdown (fav icons) to eliminate per-item shadow roots
- Memoize getBeforeEntries/getAfterEntries in cover-grid to prevent
  .slice() creating new array refs that trigger virtualizer relayout
- Remove transition: scale and border-radius from album cards to
  avoid per-frame repaints and anti-aliased path clipping
- Add queueMicrotask batching to player-store and favorites-store
  notify() to coalesce rapid-fire updates into single renders
2026-03-15 09:29:40 -04:00
yonlu 54df917ffd fix: preserve scroll position in cached grid views
display:none discards scrollTop in WebKitGTK, so navigating away
from album/artist/genre grids and back reset scroll to top.

Replace inline style.display toggling with a CSS class that uses
visibility:hidden + height:0 + overflow:hidden. This collapses
the element visually while keeping the DOM alive with its scroll
state intact. contain:strict on hidden views ensures zero layout
cost while collapsed.
2026-03-15 09:11:36 -04:00
yonlu 199c91013f perf: reduce software rendering overhead for NVIDIA+Wayland
Targeted optimizations for the DMABuf-disabled rendering path where
every frame is software-composited:

- Replace infinite CSS scroll-text animation with transition-based
  cycle that only repaints during active scroll, not during pauses
- Remove CSS mask-image on scrolling text (mask + animation was the
  single most expensive continuous repaint)
- Replace wa-icon in track rows with inline SVG — eliminates 30-50
  shadow DOM trees (each with SVG fetch/parse) during scroll
- Remove hover transitions on album cards, artist cards, genre cards,
  fav icons, queue remove buttons — each transition was causing
  per-frame software repaints
- Use visibility:hidden instead of opacity:0 for queue remove button
  (binary switch vs per-frame alpha blend)
- Add decoding=async to now-playing cover art images (prevents
  main-thread blocking during image decode on track change)
- Add contain:strict to fixed-height track rows (33px) and queue
  items (49px) — browser skips size contribution calculations
2026-03-15 09:00:59 -04:00
yonlu 3b2e189e7d fix(14-perf): fix scroll jumping and input latency
Root causes addressed:
- track-list had no _itemSize hint for flow layout — virtualizer
  defaulted to 100px, measured actual ~33px rows, then called
  _correctScrollError/scrollTo on every scroll causing visible jumps
- will-change:transform on virtualizer elements caused nested GPU
  layers (virtualizer positions children with transforms internally)
  adding compositor overhead instead of helping
- content-visibility:auto on album cards conflicted with virtualizer's
  own DOM recycling, causing redundant layout recalculation
- track-list visibilityChanged handler wrote to store synchronously
  on every event (per-item during scroll) without any throttling
- IIFE closure in renderTrackRow created a new function per row per render

Fixes applied:
- Add _itemSize:{height:33} + fixed height:33px on .track-row (matches
  queue-panel pattern that already worked smoothly)
- Add overflow-anchor:none on track-list virtualizer
- Remove will-change:transform from all 6 scroll containers
- Remove content-visibility:auto from album cards
- RAF-throttle visibilityChanged scroll position saves
- Replace IIFE with direct cols.map() in template
2026-03-14 14:46:38 -04:00
yonlu 4b7d35d7ec fix(14-01): downgrade main-panel from contain:strict to layout+style+paint
contain:strict includes size containment which caused a timing issue
where the flex-based main-panel height wasn't resolved before the
virtualizer measured its container, resulting in track-list rendering
at ~20% height on first load until something triggered a relayout.
2026-03-14 14:29:19 -04:00
yonlu d0c05dc1d4 perf(14-03): add notification batching to queue store and granular change tracking to library store
- Queue store now uses queueMicrotask batching (matching library store pattern)
- Library store adds changeGeneration counter incremented only on actual data changes
- LibraryController checks changeGeneration before requestUpdate, skipping loading-only transitions
- Reduces unnecessary component re-renders during data loading cycles
2026-03-14 13:53:59 -04:00
yonlu 2f7ed70304 perf(14-03): eliminate per-item closure allocation in scroll render paths
- Replace inline arrow closures in renderTrackRow with event delegation via data-index
- Replace inline arrow closures in renderTrackItem with event delegation via data-index
- Add delegated click/dblclick/contextmenu/dragstart handlers on virtualizer elements
- Remove button click in queue panel also delegated via closest('.remove-button')
- Zero new function objects created per renderItem call during scroll
2026-03-14 13:52:34 -04:00
yonlu 6ca0b3c5a8 perf(14-04): RAF-throttle scroll position saves and add overflow-anchor to queue panel
- Replace 100ms debounced scroll save in cover grid with requestAnimationFrame throttling
- Position now saves continuously during scrolling (~16ms) instead of only after stop
- Cancel pending RAF in teardown() to prevent leaks
- Add overflow-anchor: none CSS to queue panel lit-virtualizer
- Keep monkey-patch for lit-virtualizer _correctScrollError with expanded comment explaining why CSS alone is insufficient
2026-03-14 13:51:32 -04:00
yonlu ac8a52e110 perf(14-01): add GPU promotion and containment to all scroll containers
- contain: layout style on :host of all 6 scroll-heavy components
- contain: paint + will-change: transform on all scroll containers for GPU compositing
- content-visibility: auto + contain-intrinsic-size on .album-card for off-screen skip
- cover-grid, track-list, queue-panel, artists-view, genres-view, playlist-view
2026-03-14 13:46:08 -04:00
yonlu ad9104374a perf(14-02): replace innerHTML navigation with view caching system
- Primary views (tracks, albums, artists, genres, playlists, settings) created once and kept in DOM
- Navigation toggles display:none/display:'' instead of destroying/recreating components
- viewCache Map bounded to 6 entries — no memory leaks
- Detail views (artist-details, playlist-details, genre-details) remain ephemeral
- Scroll positions naturally preserved by keeping DOM alive
- No virtualizer reinit, no data refetch, no image reload on navigation
2026-03-14 13:44:37 -04:00
yonlu efa06f7edf perf(14-01): add CSS containment to app shell layout boundaries
- contain: layout style on .content-area to isolate main+queue from header/sidebar
- contain: strict on .main-panel for maximum layout isolation
- contain: layout style paint on .main-panel > * for paint containment per view
- contain: layout style paint on sidebar to isolate from main panel
- contain: layout style on .bottom-bar to isolate footer from content reflows
2026-03-14 13:44:22 -04:00
yonlu 12c678284c feat(12-02): make config sections collapsible with chevron dropdown
Sections start collapsed showing only heading, description, and a
chevron icon. Click to expand and reveal the fields. The open property
allows sections to start expanded if needed.
2026-03-14 12:57:35 -04:00
yonlu 649e516aa3 fix(12-02): keep Add Library button visible during scan
Add Library is now outside the scanning/not-scanning conditional so
it's always accessible, even while a scan is in progress.
2026-03-14 12:42:15 -04:00
yonlu df824c6989 feat(12-02): show scan progress bar inline in library list entry
Progress bar now renders below the library row being scanned, with
a small status label (phase + percent) after the library name. Removed
the separate status-bar progress display.
2026-03-14 12:30:00 -04:00
yonlu 771345dd9d fix(12-02): move Add Library button inline with scan buttons
Add Library now sits in the same row as Scan and Full Rescan, sharing
the same btn-primary style and size. Removed the standalone button
below the library list.
2026-03-14 12:13:11 -04:00
yonlu ba3f840a28 fix(12-02): move scan buttons above library list, default to none selected
Scan and Full Rescan buttons now sit above the library list for easier
access. Libraries start unchecked — both buttons are disabled until at
least one library is selected.
2026-03-14 12:11:12 -04:00
yonlu b093fbb10a fix(12-02): invalidate library store cache on LibraryRemoved event
Track list, albums, artists, and genres views now refresh immediately
after a library is removed instead of showing stale cached data.
2026-03-14 10:46:15 -04:00
yonlu 1f872aa005 fix(12-02): refresh library track counts after scan completes
loadLibraries() now runs on ScanComplete and ScanQueueDrained so newly
added libraries show their track count without navigating away.
2026-03-14 10:01:35 -04:00
yonlu 1d735c3a5f fix(12-02): reorder orphan cleanup to delete FK children before recordings
recording_genres and release_group_recordings reference recordings.id,
so they must be deleted BEFORE the recordings table is cleaned.
Also extends toast duration to 8s for readability.
2026-03-13 13:38:39 -04:00
yonlu cf004986c9 fix(12-02): wait for scan to stop before library removal, surface errors in UI
RemoveLibrary now polls until the cancelled scan goroutine finishes
before proceeding with the removal transaction. Also show removal
errors as toast messages instead of only logging to console, and
explicitly reload library list after successful removal.
2026-03-13 11:17:37 -04:00
yonlu 13a42aea22 feat(12-02): selectable library list with checkbox scan targeting
- Add checkboxes to library list with select-all header
- Soft Scan operates on selected libraries (queues each individually)
- Full Rescan stays global (nukes all data, rescans all libraries)
- Remove redundant 'Scan All Libraries' button
- Fix FullRescan Go backend to scan all libraries after wipe, not just first
2026-03-13 10:33:15 -04:00
yonlu 9272b060bf fix(12-02): dismiss inline rename on click outside 2026-03-13 09:24:32 -04:00
yonlu 05598224e4 fix(12-02): replace removed Scan() import with ScanAllLibraries()
Scan() wrapper was deleted in Phase 11 but config-page.ts and
library-manager.ts still imported it. Use ScanAllLibraries() for
soft scan since multi-library model scans all libraries.
2026-03-13 08:27:47 -04:00
yonlu e199712a56 feat(12-02): remove Libraries sidebar nav item and view routing
- Remove 'libraries' from View type union in app-sidebar.ts
- Remove Libraries nav item from sidebar navigation list
- Remove case 'libraries' view routing in index.ts
- Remove library-manager component import from index.ts
2026-03-12 19:51:41 -04:00
yonlu ffc5d9639c feat(12-02): replace config-page library section with full library management UI
- Add library list with name, path, track count per library
- Add Library button opens folder picker, auto-creates library
- Inline rename with Enter/Escape via overflow menu
- Removal confirmation dialog with real impact counts (tracks, playlists, queue)
- Toast notification with removal summary after library removal
- Add GetAllLibrariesWithTrackCounts + Info type to backend Library struct
- Add Wails binding stubs for AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact
- Add Info, RemovalImpact, RemovalSummary types to models.ts
- Remove old single-directory library config UI (GetLibraryDirectory/SetLibraryDirectory)
2026-03-12 19:50:59 -04:00
yonlu bd44f8306c feat(12-01): implement library CRUD methods and orphan cleanup pipeline
- Add AddLibrary, RenameLibrary, RemoveLibrary, GetRemovalImpact methods
- RemoveLibrary follows exact order: phantom populate → delete audio_files → orphan cleanup → commit → FTS5 rebuild → cover art file cleanup → queue compact → events
- Add cancelLibraryScan and currentTrackBelongsToLibrary helpers
- Add RemovalHooks type and removalHooks field on Library struct
- Add LibraryAdded, LibraryRenamed, LibraryRemoved event constants
- Regenerate frontend events.ts with new CRUD events
- All hand-crafted SQL has SAFETY comments per project convention
- Dual artist_credit FK check (recordings + release_groups) for orphan cleanup
2026-03-12 19:37:07 -04:00
yonlu d61f122b56 feat(11-02): update library-manager with per-library progress and Scan All button
- Add libraryId, libraryName, queuedCount to ScanProgress interface
- Show library name in progress label (Scanning: [Library Name])
- Show queue count below progress bar when libraries queued
- Add Scan All Libraries button (calls ScanAllLibraries binding)
- Subscribe to LibraryScanQueued/LibraryScanQueueDrained events
- Scan state properly tracks queue draining (doesn't reset early)
2026-03-09 16:11:23 -04:00
yonlu d01591d6cc feat(11-02): update config-page with per-library progress display and queue-aware cancel dialog
- Add libraryId, libraryName, queuedCount to ScanProgress interface
- Replace CancelScan import with CancelCurrentScan, CancelAllScans
- Add ScanAllLibraries import and Scan All Libraries button
- Show library name in progress label (Scanning: [Library Name])
- Show queue count below progress bar when libraries queued
- Cancel dialog shows scope choice (Cancel This Library / Cancel All) when queue > 0
- Subscribe to LibraryScanQueued and LibraryScanQueueDrained events
- Track scanQueuedCount state for queue-aware UI behavior
2026-03-09 16:09:22 -04:00
yonlu 943db1cf27 feat(11-01): per-library scan pipeline with queue coordinator
Task 1: Schema, events, and progress types
- Add library_id to CreateAudioFile SQL INSERT and regenerate sqlc code
- Add LibraryScanQueued and LibraryScanQueueDrained event constants
- Regenerate TypeScript events via genevents
- Add LibraryID, LibraryName, QueuedCount to ScanProgress
- Add LibraryID, LibraryName to ScanMetrics
- Add libraryID field to importResult for threading through pipeline

Task 2: Scan queue coordinator and per-library scanning
- Create scan_queue.go with ScanLibrary(id), ScanAllLibraries()
- Add CancelCurrentScan(), CancelAllScans() for queue-aware cancellation
- FIFO scan queue with silent dedup (same library already scanning or queued)
- Refactor Scan() -> scanInternal(libraryID, libraryName, libraryPath)
- Replace GetAllAudioFiles with GetAudioFilesByLibrary for per-library loading
- Thread libraryID through DB writer to set CreateAudioFileParams.LibraryID
- drainQueue auto-starts next queued library or emits LibraryScanQueueDrained
- Pause freezes current scan AND queue
- Add GetScanQueueLength() and QueuedLibraryNames() for UI
- Mark CancelScan() and Scan() as deprecated
2026-03-09 16:02:54 -04:00
yonlu ce23177228 feat(quick-18): replace track-info with multi-column grid layout in playlist-details
- Remove track-info component, add formatMilliseconds import
- Add 5-column grid (#, Title, Artist, Album, Duration) with header row
- Display 1-indexed playlist order numbers in # column
- Phantom tracks span full grid width with grid-column: 1 / -1
- Add column-specific CSS: tabular-nums, text truncation, alignment
2026-03-08 09:35:59 -04:00
yonlu 955cd68be2 refactor(quick-17): simplify playlist-view to navigate instead of expand
- Plain click now navigates to playlist-details subpage instead of toggling expand/collapse
- Removed all inline track expansion: renderPlaylistBody, track-item rendering, chevron icons
- Removed SelectionController, ContextMenuController, PlayerController (track-level interactions moved to playlist-details)
- Removed track-info, track-details, phantom-resolver imports (all live in playlist-details now)
- Simplified search to filter by playlist name only (no inline track search)
- Kept: playlist list, context menu (rename/delete/set-default), Ctrl/Shift multi-select, drag-drop target, create/import, sort toolbar
2026-03-07 22:10:24 -05:00
yonlu dc5c7d6ca6 feat(quick-17): create playlist-details subpage component
- New playlist-details component with header (back button, playlist icon, title, track count)
- Full track list with all interactions: select, play, context menu, drag, phantom handling
- Drop target support for adding tracks from other views
- Search filtering for tracks within the detail view
- Navigation routing in index.ts for playlist-details view
- Added playlist-details to SEARCHABLE_VIEWS in search-store
2026-03-07 22:06:53 -05:00
yonlu 0bd8cefa00 fix(queue-panel): suppress virtualizer scroll corrections during scrollbar drag
lit-virtualizer's flow layout has a scroll error correction mechanism that
calls scrollTo() to fix sub-pixel estimation errors. On large lists (20k+),
even with fixed-height items, floating-point differences from
getBoundingClientRect() (e.g. 49.000003px vs 49px) accumulate across items
and trigger corrections that fight the native scrollbar drag gesture,
causing the thumb to desync from the mouse.

Detect scrollbar drag by checking if mousedown occurs in the scrollbar
gutter (clientX > element clientWidth), then monkey-patch the virtualizer's
_correctScrollError method to discard accumulated errors during drag
instead of calling scrollTo(). Items continue to render/recycle normally
since layout updates are not suppressed -- only the scroll position
corrections are skipped.
2026-03-07 11:14:54 -05:00
yonlu 288d9deae2 fix(queue-panel): set flow layout _itemSize to match actual track item height
The previous fix (fixed CSS height on .track-item) was insufficient because
lit-virtualizer's flow layout defaults to estimating items at 100px tall.
With 20k items, the difference between 100px estimate and 49px actual creates
a ~1M px scroll height that collapses as items get measured, triggering
scroll error corrections (programmatic scrollTo calls) that fight the native
scrollbar during drag.

Setting _itemSize to { height: 49 } via the flow() config ensures the initial
scroll size estimate matches reality, eliminating the scroll error corrections
that caused the scrollbar thumb to desync from the mouse when dragging down.
2026-03-07 10:49:06 -05:00
yonlu 8576999ae3 now-playing component scrolls text when hovering 2026-03-07 10:20:51 -05:00
yonlu 906ea28751 feat(16-01): wire shortcut:select-all listener in track-list, queue-panel, and playlist-view
- Add handleSelectAll bound handler calling selection.selectAll() in all three components
- Register/unregister event listeners in connectedCallback/disconnectedCallback
2026-03-07 10:19:07 -05:00
yonlu f5677628ef feat(16-01): add selectAll() to SelectionController and dispatch shortcut:select-all event
- Add selectAll() method to SelectionController that selects all items via host interface
- Change app.selectAll dispatch from document.execCommand('selectAll') to CustomEvent('shortcut:select-all')
2026-03-07 10:18:08 -05:00
yonlu bb3fd204f0 fix(09-05): emit VolumeChanged event and persist state in ChangeVolume and MuteToggle 2026-03-07 02:06:08 -05:00
yonlu 0451fb3880 feat(09-04): add keyboard shortcuts section to config page with conflict detection
- Keyboard Shortcuts section with shortcuts grouped by Player, Navigation, App categories
- All 16 default shortcuts listed with human-readable labels
- shortcut-capture widget for each action enables record-style rebinding
- Conflict detection warns before overwriting existing bindings with Overwrite/Cancel
- Reset All to Defaults button calls store.resetAll()
- Panel-specific shortcuts display scope label (e.g. track-list)
- ShortcutsController provides reactive state updates
2026-03-06 21:58:13 -05:00
yonlu 391436927c feat(09-03): add scan control UI with pause/resume/cancel and confirmation dialog
- Pause/Resume buttons replace Soft Scan/Full Rescan during active scan
- Cancel button shows confirmation dialog with Keep/Discard/Continue options
- Register LibraryScanPaused/Resumed/Cancelled event handlers
- Status bar shows 'Scan paused.' with accent color when paused
- Add CancelScan/PauseScan/ResumeScan Wails binding stubs
- Cancel dialog overlay dismissible by clicking outside or Continue Scanning
- Add cancelled field to ScanMetrics TypeScript interface
2026-03-06 21:54:50 -05:00