Commit graph

160 commits

Author SHA1 Message Date
842cb6353b Widget v2 glassmorphism, lyrics, backup, crossfade fixes
Widget v2:
- Glassmorphism glass panel, 40-bar waveform Canvas with SeekToIntent
- CIAreaAverage color extraction + secondary color for adaptive theming
- Small: inset glass. Medium/Large: flush edge-to-edge (contentMarginsDisabled)
- Large: 3-item queue with real cover art thumbnails, crossfade countdown
- Waveform sampled from offline vis buffer, seeded PRNG fallback

Live Lyrics:
- LyricsService: direct LRCLIB from iOS, no companion dependency
- Embedded lyrics read via AVAsset.load(.metadata) from downloads
- Karaoke word-by-word gradient fill, auto-scroll, fade edges
- Tap-to-sync timing editor with +/-0.1s offset adjust
- Companion API fallback only for server-side embedded tags

Backup System:
- .nvdbackup export/import via ZIPFoundation
- UTI registered for AirDrop, passwords stripped on export
- PendingOperationsQueue with retry + disk persistence

Crossfade fixes:
- Seek bar: reports from incoming player immediately, not at 50%
- songHandoff at midpoint: art/colors/text/lyrics transition mid-fade
- Scrobble fires before metadata swap (correct outgoing song)

Visualizer fixes:
- stopAll() zeros _audioLevels + clears offlineVisBuffer
- All 5 simulation start sites gated behind realAudioAnalysis check
- Bars stay flat between skips, only rise when real vis data loads

Smart DJ bulk prefetch, appendingPathComponent slash fix
2026-04-14 00:44:38 -07:00
cea4e3868e Seek bar glitch — The time observer was reporting from the outgoing player (near 100%) for the first half, then jumping to the incoming player (near 0%) at 50%. Now it reports from the incoming player as soon as crossfade begins. The user hears the new song fading in — the seek bar matches.
Metadata/art/colors delayed — currentSong, artwork, widget colors, and lyrics only updated after finalizeCrossfade. Now a songHandoff callback fires at the crossfade midpoint (50%) — updates everything mid-fade so the UI transitions with the audio. Scrobble of the outgoing song also fires here (before currentSong changes).
Visualizer stale data — Old song’s vis frames stayed in the buffer during the first half of crossfade. Now visualizerHandoff zeros offlineVisBuffer + _audioLevels before loading the new song’s data. Clean slate → simulation fills the gap → real vis takes over.
The needsNextTrack callback (post-finalize) now only handles player swap, observer re-registration, and queue persistence — no redundant metadata/scrobble work.
2026-04-14 00:27:10 -07:00
2a0b3d8c47 Widget v2 glassmorphism + lyrics + backup system
- Widget data layer: waveform sampling, CIAreaAverage color extraction,
  secondary color computation, crossfade countdown from DJ profiles
- Widget views full rewrite: glass panel, 40-bar waveform Canvas with
  SeekToIntent tap zones, color-adaptive theming, transport controls
- Small: inset glass. Medium/Large: flush edge-to-edge (no red border)
- Medium: art + info + waveform + controls + Up Next footer
- Large: art + info + waveform + controls + 3-item queue + crossfade footer
- Live lyrics: karaoke word-by-word gradient fill, LRCLIB search/fetch,
  timing editor with tap-to-sync, embed via Companion API
- Backup system: .nvdbackup export/import via ZIPFoundation, UTI for AirDrop
- Smart DJ bulk prefetch: single gzipped request on app launch
- clearAll() now covers all 17 widget keys including seekToTime
2026-04-13 23:36:22 -07:00
7413163d57 Live lyrics system — full implementation
Companion API (4 new endpoints):
- GET /lyrics/search — proxy to LRCLIB, returns matches with sync status
- GET /lyrics/fetch — exact match by artist/title/duration, caches in SQLite
- GET /lyrics/get — checks embedded tags > .lrc sidecar > DB cache
- POST /lyrics/embed — writes LRC into audio file tags via mutagen + .lrc sidecar
- New lyrics table in companion database

iOS data layer (LyricsModels.swift):
- LyricLine/LyricWord structs with Codable conformance
- Full LRC parser (line-level + enhanced word-level timestamps)
- LRC serializer (toLRC) for saving edited timings
- LyricsCache with memory + disk tiers keyed by artist-title

iOS manager (LyricsManager.swift):
- Source pipeline: embedded > .lrc sidecar > local cache > LRCLIB auto-fetch
- Binary search line/word tracking (O(log n))
- Word-level progress for karaoke gradient fill
- Hooked into AudioPlayer time observer (0.5s sync) and pushWidgetState (song change)

iOS views:
- LyricsOverlayView: karaoke word-by-word highlighting via FlowLayout + KaraokeWordView
  gradient fill sweep, ScrollViewReader auto-scroll, tap any line to seek
- LyricsSearchSheet: LRCLIB search with debounced input, duration mismatch warnings,
  preview sheet, import to cache
- LyricsEditorView: tap-to-sync mode, per-line ±0.1s adjust buttons, global offset
  sheet, save to device or embed in file via Companion API

NowPlayingView integration:
- Lyrics toggle button (quote.bubble) in bottom controls bar
- Portrait layout swaps album art for lyrics overlay with animation
- Blur panel: .ultraThinMaterial with gradient mask so visualizer fades through
- Lyrics search/editor sheets wired with notification observer

Mini player lyric ticker:
- Both standard and compact mini player bars show current lyric line
- Accent pink with ♪ prefix, falls back to artist name when no lyrics
- Push transition animation on line changes

Bug fixes included:
- NavidromePlayerApp: removed unnecessary try on CompanionAPIService.shared
- Info.plist: added LSSupportsOpeningDocumentsInPlace for document type support
2026-04-13 09:01:30 -07:00
c0a073bf3f Live lyrics system — foundation + search + editor
Companion API (4 new endpoints):
- GET /lyrics/search — proxy to LRCLIB, returns matches with sync status
- GET /lyrics/fetch — exact match by artist/title/duration, auto-caches in SQLite
- GET /lyrics/get — checks embedded tags > .lrc sidecar > DB cache
- POST /lyrics/embed — writes LRC into audio file tags via mutagen + .lrc sidecar

iOS data layer:
- LyricsModels.swift: LyricLine, LyricWord, LyricsData, LRCParser (full LRC parser + serializer)
- LyricsCache: local disk cache keyed by artist-title, memory tier
- CompanionAPIService: 4 new methods (fetchForPath, fetchFromLRCLIB, search, embed)

iOS manager:
- LyricsManager: source pipeline (embedded > lrc > cache > LRCLIB), binary search line/word tracking
- Hooked into AudioPlayer periodic time observer (0.5s sync) and pushWidgetState (song change)
- Word-level progress tracking for karaoke gradient fill

iOS views:
- LyricsOverlayView: karaoke word-by-word highlighting, FlowLayout, ScrollViewReader auto-scroll
- LyricsSearchSheet: LRCLIB search with results, duration mismatch warnings, preview + import
- LyricsEditorView: tap-to-sync timing, per-line ±0.1s adjust, global offset, save to device or embed

Still needs wiring: lyrics toggle button in NowPlayingView, blur panel overlay, mini player ticker
2026-04-12 20:56:48 -07:00
3824f42ab5 quick fix 2026-04-12 20:18:29 -07:00
0a1a46df47 Widget glassmorphism redesign 2026-04-12 19:57:20 -07:00
Dallas Groot
b41a0062b8 Captured remaining into let finalRemaining
•	BackupManager — replaced barCount/gain/colorScheme with the 18 actual
VisualizerSettings properties on both export and import sides
	•	PendingOperationsQueue — captured remaining into let finalRemaining
before MainActor.run closure (Swift 6 concurrency), changed try?
CompanionAPIService() to CompanionAPIService.shared
	•	Widget — removed unused barWidth
	•	Version — both plists now use $(MARKETING_VERSION)
The localization warning about String Catalog Symbol Generation is an
Xcode recommendation, not an error — you can dismiss it or enable it
in project settings.
2026-04-12 19:49:05 -07:00
Dallas Groot
b8f3544395 quick fix 2026-04-12 19:34:58 -07:00
8607bce56a Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-12 19:25:15 -07:00
Dallas Groot
b9844b23cd Performance audit, Now Playing widget, crossfade stability, cover art embedding, DJ profile bulk cache
PERFORMANCE AUDIT
- Removed 16 dead SubsonicClient methods (~117 lines)
- Added NSCache memory tier to LibraryCache, AlbumCoverStore,
ArtistCoverStore, RadioCoverStore
- Replaced weak polynomial hash with FNV-1a 64-bit in ImageCache
- Split PlaybackStateStore into save() (full queue) and savePosition()
(time only)
- Reused single SubsonicClient in OfflineManager instead of
per-download allocation
- Added periodic ImageCache disk trim every 50 writes
- Changed AudioPreFetcher to fuzzy offline match
(isSongAvailableOffline)
- Removed dead code: hasCompanionLibrary, downloadedSongIds, isActive,
CachedImageLoader.task
- Fixed thread safety: inline JSONEncoder/JSONDecoder in LibraryCache
(no shared instances)

WIDGET EXTENSION (new target: NavidromeWidget)
- v2 glassmorphism design: blurred album art background + frosted
glass panel
- Waveform scrubber: 40-bar Canvas with tap-to-seek (20 segments via
SeekToIntent)
- Color-adaptive theming: CIAreaAverage dominant color extraction with
HSB contrast adjustment
- Transport controls: previous/play-pause/next with interactive
AppIntents
- Up Next footer with crossfade countdown from Smart DJ profiles
- Large widget: 3-item queue list with numbered rows
- Small/Medium/Large sizes matching design mockups
- App Group communication via WidgetSharedState (UserDefaults)
- Darwin notification observer for widget→app commands
- Foreground command pickup for suspended app recovery
- Idempotency guards on all widget commands

CROSSFADE & PLAYBACK FIXES
- Fixed dual audio on single-song queue: guard nextSong.id ==
currentSong?.id in prepareNextForCrossfade
- Fixed crossfade play path never calling pushWidgetState (returned
before reaching it)
- Fixed crossfade needsNextTrack callback missing queue persistence +
widget push
- Fixed toggleShuffle queue not persisted after PlaybackStateStore
split
- Added nowPlayingSyncTimer restart on foreground (Lock Screen seek
bar drift)
- Added AVPlayer currentTime/duration sync in resumeVisTimers before
vis timer restart
(fixes waveform distortion after background — confirmed by Apple
Forums + SoundCloud engineering)

COVER ART PIPELINE
- Fixed pushWidgetState cover art size mismatch (300→600 to match
fetchAndSetArtwork)
- Added custom cover art key differentiation ("custom_" prefix forces
re-blur)
- Changed server art lookup from memoryOnlyImage to cachedImage
(memory+disk fallback)
- Added POST /library/cover-art-by-path endpoint (was missing — iOS
fallback hit 404)
- Added navidrome_id fallback on existing cover art endpoint
- Added embed_cover_art_in_file/embed_cover_art_in_directory: mutagen
writes cover art
directly into FLAC/MP3/M4A/OGG metadata tags so Navidrome serves
updated art
- All three upload paths (by-id, by-path, upload-tracks) now embed +
trigger_scan

COMPANION API FIXES
- Fixed _create_task recursion (was calling itself instead of
asyncio.create_task)
- Fixed navidrome_db NameError on /library/conflicts endpoint
- Reduced WebSocket connect/disconnect logging (only first-client and
all-disconnected)

SMART DJ PROFILE PREFETCH
- New endpoint: GET /smart-dj/profiles/export (bulk JSON, gzip
automatic)
- SmartDJCache.loadBulkCache() reads single file on launch (instant)
- SmartDJCache.bulkImport() writes all profiles in one atomic file
- CompanionAPIService.fetchAllProfiles() fetches entire profile set in
one request
- Wired into NavidromePlayerApp.task after server connect
- SmartCrossfadeManager unchanged — already reads from SmartDJCache
first

WEBSOCKET NOISE REDUCTION
- iOS: silent reconnect retries, only log milestones (#1, #5, every
20th)
- iOS: log "reconnected after N attempts" on success, silent initial
connect
- Python: only log first client connect and all-clients-disconnected

Files: 13 modified, 8 new (including companion-api/main.py)
2026-04-12 19:24:22 -07:00
070777626f Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-12 18:08:19 -07:00
Dallas Groot
aa97be4caa batch albumart fix 2026-04-12 18:07:12 -07:00
60031d01cd Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-12 17:12:49 -07:00
Dallas Groot
551e59a148 playback improvements to visualizer
1.	Reactivate audio session
	2.	Process stale widget commands
	3.	Sync currentTime/duration from live AVPlayer ← new
	4.	Reinstall periodic time observer
	5.	Resume crossfade manager
	6.	Restart nowPlayingSyncTimer
	7.	Restart vis timers (offline vis or level simulation)
The vis timer’s very first frame now reads the correct position
instead of the stale one from backgrounding.
2026-04-12 17:12:07 -07:00
8d1d30c5e5 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-12 16:17:40 -07:00
Dallas Groot
3c28413af8 bug fixes and improvements
Gap 1: Lock Screen seek bar drift — nowPlayingSyncTimer (5s timer that
pushes elapsed time to MPNowPlayingInfoCenter) was created in
playWithAVPlayer but never restarted after a background/foreground
cycle. Now invalidated in suspendVisTimers() and recreated in
resumeVisTimers(). The crossfade path benefits too — it never went
through playWithAVPlayer so the timer was never created at all for
crossfade sessions.
Gap 2: Prefetcher re-downloads after restructure — AudioPreFetcher
used isSongDownloaded(song.id) (exact ID match). After a Companion
restructure changes IDs, songs already downloaded were re-fetched.
Changed to isSongAvailableOffline(song) which falls back to
title/artist/duration matching.
Gap 3: Unbounded image disk cache — trimDiskCache() only ran on app
launch. A long browsing session could push well past the 200MB limit.
Now storeToDisk increments a write counter and triggers trim every 50
writes. The counter lives on ioQueue (serial) so no lock needed.
Gap 4: Custom cover art in widget — WidgetBridge cached blur keyed by
coverArtId. Custom covers don’t change the ID, so the bridge skipped
the update. Now pushWidgetState() passes "custom_\(id)" as the key
when AlbumCoverStore has a custom image. Same album’s songs still
share the key → blur is reused, not redone. When custom is removed,
key reverts to the bare ID → re-blurs with server art.
2026-04-12 16:16:32 -07:00
4dea87ae64 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-12 13:52:25 -07:00
Dallas Groot
8ee74aed91 hotfix 2026-04-12 13:52:10 -07:00
9a595931f7 quick fix 2026-04-12 13:49:03 -07:00
Dallas Groot
53539e5a67 quick fix 2026-04-12 13:39:13 -07:00
Dallas Groot
8aa47319f2 quick fix 2026-04-12 13:21:20 -07:00
Dallas Groot
098e9b9363 added widget support 2026-04-12 12:57:42 -07:00
Dallas Groot
346b3ef378 quick fix 2026-04-11 19:28:18 -07:00
Dallas Groot
282eb5d80c Fixed idle visualizer bug
AudioPlayer.swift — startRadioSimulation() replaces
startLevelSimulation() for radio streams. It generates two overlapping
sine waves with a slow breathing amplitude envelope. The key property:
it’s driven by an internal radioPhase counter that increments at a
fixed rate, completely independent of currentTime. Buffer seeks, HLS
restarts, and stream reconnects have zero visual effect.
MitsuhaVisualizerView.swift — Two fixes:
	1.	TimelineView(.periodic(from: .distantPast, by: tickInterval)) —
using .distantPast as the stable schedule origin instead of .now.
Previously, any recalculation of tickInterval (triggered by
isRenderingActive flipping during a radio buffer hiccup) would reset
the origin to the current moment, deferring the next tick by a full
interval — the visible stall.
	2.	songId parameter with .onChange(of: songId) that resets
peakFollower, levelHistoryBuf, and lastTickTime on song change. This
prevents the outgoing song’s simulation spike from “raising” the wave
at the start of the next song.
NowPlayingView.swift and MainTabView.swift — Pass songId:
audioPlayer.currentSong?.id through to MitsuhaVisualizerView and
CompactVisualizerView at all call sites.
2026-04-11 19:01:10 -07:00
Dallas Groot
7d78b22ce2 quick fix 2026-04-11 18:25:09 -07:00
Dallas Groot
3f3010a8fd fixed dynamicminiplayer placement 2026-04-11 18:15:10 -07:00
Dallas Groot
8eaab0bc93 Three changes to DynamicIslandView:
•	Top spacer 54 → 12 — pulls the pill up to sit directly under the
Dynamic Island cutout
	•	Horizontal padding 32 → 56 — narrows the pill from both sides
	•	Height 48 → 44 — slightly more compact to match the island’s
proportions
If it’s sitting too high or too low after testing, the spacer value is
the one knob to turn — increase it if it overlaps the island, decrease
if there’s too much gap.
2026-04-11 17:53:59 -07:00
Dallas Groot
f19d21e4cc Fix: Album streaming, seek bar gesture, and offline playback
Resolve several playback bugs introduced during the CPU optimization
work. Stop SyncEngine from overwriting the Subsonic album cache with
synthetic Companion IDs, which was silently breaking every album in
the Albums tab by routing through the Companion song path instead of
Navidrome streaming. Fix MiniProgressBar seek gesture dropping touches
by moving the DragGesture outside the TimelineView closure — the 10Hz
timer was reconstructing the gesture recognizer every tick,
interrupting in-progress drags. Add AVPlayerItem status observation to
AudioPlayer so stream failures surface in the debug console instead of
silently stalling. Add fuzzy title/artist/duration fallback to
OfflineManager.localURL so songs remain playable offline after a
Companion API restructure changes their Navidrome ID.
2026-04-11 17:41:20 -07:00
7657b5841e The Albums tab was being populated with Companion API IDs (companion:Album Name|Artist Name) instead of real Navidrome IDs. Every time the Companion sync ran, it overwrote the valid Subsonic album cache with these synthetic IDs. AlbumDetailView would detect the companion: prefix, load songs from the Companion API instead of Navidrome, and those songs have Companion song IDs that Navidrome can’t stream.
The Artist → Album path bypassed this entirely because it navigates via artistId which fetches albums fresh from Navidrome each time.
After installing this and doing a pull-to-refresh, the Albums tab will use real Navidrome IDs again. You may need to clear the app’s cache once if the stale Companion IDs are already persisted — Settings → clear library cache if that option exists, or just force-quit and relaunch after refreshing.​​​​​​​​​​​​​​​​
2026-04-11 17:33:13 -07:00
Dallas Groot
fc69d8a3cf CPU: Remove @Published from AudioPlayer time properties
Replace @Published var currentTime/duration with plain vars and drive
progress bars via TimelineView(.periodic) instead of SwiftUI
observation. This stops objectWillChange from firing 20x/second on
AudioPlayer, eliminating continuous body re-evaluation on
NowPlayingView, MiniPlayerBar, and MyMusicView regardless of
visualizer state.
2026-04-11 16:44:56 -07:00
Dallas Groot
758d7a5ebd quick fix
NowPlayingView.swift — The _radioProgressBarImpl stub I left as a
“reference” comment block was still compiled by Swift and referenced
isDraggingSlider, dragPosition, and playbackTime which no longer exist
on NowPlayingView. Removed the entire stub. Also transportControls had
one remaining playbackTime reference — replaced with
audioPlayer.currentTime directly since transport controls genuinely
need the current position for the timeshift enable/disable logic and
don’t cause a body re-eval issue there.
NavidromeWatchApp.swift — syncLibrary() is declared async not async
throws, so try? was redundant. Removed it.
2026-04-11 16:31:24 -07:00
Dallas Groot
9add1e014a quick fix 2026-04-11 16:23:41 -07:00
Dallas Groot
3b56626d6d debugging high cpu usages 2026-04-11 16:15:27 -07:00
Dallas Groot
85c85c2090 memory improvements 2026-04-11 15:37:14 -07:00
Dallas Groot
0730fa11f8 bug fixes 2026-04-11 15:09:06 -07:00
Dallas Groot
f3b9483b23 overhaul
AUDIT-036 — Slider/button fixes (direct Liquid Glass cause)
scheduleFlush() now runs Task { @MainActor } instead of bare Task. The
pendingSaves dictionary is now only ever read/written on the main
thread. Before this fix, a UserDefaults write could race with a slider
didSet, causing values to snap back or write the wrong value — which
is exactly why buttons were switching state unexpectedly.
AUDIT-034 — 60fps idle Canvas (direct Liquid Glass cause)
TimelineView now uses isRenderingActive ? settings.effectiveFPS : 2.0.
When paused or not visible, the Canvas drops from 60fps to 2fps. This
stops the continuous GPU wakeups that were fighting Liquid Glass
gesture tracking, which is why sliders needed multiple attempts.
AUDIT-001 — FFT real-time heap allocation
processFFT no longer allocates any heap memory. The Hann window is
computed once in init(). All four scratch buffers (fftWindow,
fftWindowed, fftRealp/fftImagp, fftMagnitudes) are pre-allocated and
reused every render callback — zero allocations on the real-time audio
thread.
AUDIT-002 — WatchOfflineStore data race
taskToSongId and pendingSongs now protected by a dedicated serial
storeQueue. URLSession delegate reads and main thread writes are
serialised.
AUDIT-019 — URLSession per AsyncCoverArt render
CompanionAPIService() no longer instantiated per render. Companion
cover art URLs now built directly from
CompanionSettings.shared.baseURL — no URLSession created.
AUDIT-020 — Synchronous disk read on main thread
CachedImageLoader now uses memoryOnlyImage (sync, no I/O) for the
first check, then cachedImageAsync (disk read on ioQueue) for the
second. Main thread never blocks on disk I/O.
AUDIT-033 — Lost star/unstar actions offline
Star/unstar now routes through OptimisticActionQueue — actions survive
Tailscale reconnection and are retried automatically.
AUDIT-035 — OptimisticActionQueue flush race
flush() Task is now @MainActor — pendingActions only ever touched on
main thread, no more race between rapid taps and in-flight flushes.
AUDIT-038 — O(n²) deduplication
deduplicateAlbums now O(n) using a frequency dictionary. For 843
albums: ~7.1M string comparisons/second during playback → ~1,700.
AUDIT-026, AUDIT-015 — Duplicate setResourceValue removed, cacheSize
now uses totalSize directly
2026-04-11 11:17:40 -07:00
Dallas Groot
2f65da3ccc quick fix
Companion (main.py):
	•	NAVIDROME_TAGS whitelist — the single source of truth for what tags
survive
	•	enforce_tag_whitelist() — whitelist enforcer, replaces blacklist
approach
	•	All 5 write points updated: apply_tags, apply_tags_dict,
upload-track, upload-tracks, restructure_all
	•	preserve_composer and preserve_lyrics flags on both upload
endpoints (default False)
	•	/library/clean-tags now uses whitelist enforcer
iOS:
	•	UploadMetadata — preserveComposer and preserveLyrics fields
	•	buildMultipartBody — sends both flags as form fields
	•	BatchUploadView — two toggles, both off by default, wired end-to-end
	•	MultiAlbumEditorSheet — full rewrite matching
BatchAlbumEditorSheet: MusicBrainz search, swipe to exclude/include
tracks, Reset button, cover art widget with red glow
2026-04-11 09:40:51 -07:00
Dallas Groot
7a9c837650 batch upload quick fix
Companion (main.py):
	•	NAVIDROME_TAGS whitelist — the single source of truth for what tags
survive
	•	enforce_tag_whitelist() — whitelist enforcer, replaces blacklist
approach
	•	All 5 write points updated: apply_tags, apply_tags_dict,
upload-track, upload-tracks, restructure_all
	•	preserve_composer and preserve_lyrics flags on both upload
endpoints (default False)
	•	/library/clean-tags now uses whitelist enforcer
iOS:
	•	UploadMetadata — preserveComposer and preserveLyrics fields
	•	buildMultipartBody — sends both flags as form fields
	•	BatchUploadView — two toggles, both off by default, wired end-to-end
	•	MultiAlbumEditorSheet — full rewrite matching
BatchAlbumEditorSheet: MusicBrainz search, swipe to exclude/include
tracks, Reset button, cover art widget with red glow
2026-04-11 09:37:22 -07:00
Dallas Groot
92a5a54b8d quick fix 2026-04-11 08:36:32 -07:00
Dallas Groot
d32d63a749 batch updated with cover art support 2026-04-11 08:07:55 -07:00
Dallas Groot
3ea57fa99b quick fix 2026-04-11 02:23:03 -07:00
Dallas Groot
b4d2a5ce92 Merge ; commit '07d125da3c29035a8bca6a992229655d2ff446a0' 2026-04-11 02:22:29 -07:00
Dallas Groot
a9acd65001 quick fix 2026-04-11 02:20:54 -07:00
Dallas Groot
07d125da3c Ignored IDs persisted in UserDefaults
Active tab — all unignored issues, swipe left → “Ignore” (grey)
	•	Ignored tab — all ignored issues shown dimmed, swipe left →
“Restore” (pink) to bring them back
	•	Fix buttons hidden on ignored issues
	•	Ignored IDs persisted in @AppStorage so they survive app restarts
	•	Tab labels show live counts: Active (1) | Ignored (31)
2026-04-11 02:13:06 -07:00
Dallas Groot
d3434f1911 quick fix 2026-04-11 01:59:36 -07:00
Dallas Groot
12dab4cc37 quick fix 2026-04-11 01:52:16 -07:00
Dallas Groot
01baf8792f quick fix 2026-04-11 01:47:30 -07:00
Dallas Groot
9bbe7bafe4 quick fix 2026-04-11 01:44:36 -07:00
Dallas Groot
ef7116d0bf quick fix 2026-04-11 01:41:58 -07:00