uodared readme
This commit is contained in:
parent
8667f623ef
commit
e42764b042
1 changed files with 155 additions and 52 deletions
207
README.md
207
README.md
|
|
@ -5,34 +5,44 @@ A native iOS + watchOS music player for [Navidrome](https://www.navidrome.org/)
|
|||
## Features
|
||||
|
||||
### iOS App
|
||||
|
||||
- **Full Navidrome/Subsonic library** — browse by albums, artists, songs, genres, playlists
|
||||
- **Offline playback** — download songs for offline listening with per-track and per-album downloads
|
||||
- **Mitsuha visualizer** — real-time FFT waveform visualization with Catmull-Rom spline rendering, multiple styles (wave, bar, line), color modes (dynamic, album art, custom), and configurable presets
|
||||
- **Offline playback** — download songs for offline listening with per-track and per-album downloads; all song rows grey out and disable when network or server is unavailable
|
||||
- **Mitsuha visualizer** — real-time FFT waveform visualization with Catmull-Rom spline rendering, multiple styles (wave, bar, line), color modes (dynamic, album art, Siri, custom), and configurable presets; compact wave morphs between mini player and Dynamic Island using `matchedGeometryEffect`
|
||||
- **Now Playing** — full-screen player with album art color extraction, drag-to-dismiss, AirPlay, seek bar, queue management
|
||||
- **Mini player** — persistent bottom bar with scrubbable progress, visualizer overlay, transport controls
|
||||
- **Radio streaming** — live radio with HLS/PLS/M3U playlist resolution, timeshift buffering, recording, and Shazam identification
|
||||
- **Smart DJ** — crossfade engine with silence skipping, loudness normalization (LUFS), and predictive transitions (requires Companion API)
|
||||
- **Dynamic Island** — pill morphs from mini player with spring animation; album art and visualizer fly between contexts using matched geometry
|
||||
- **Radio streaming** — live radio with HLS/PLS/M3U playlist resolution, timeshift buffering, recording, and Shazam identification; stations grey out and show offline banner when server is unreachable
|
||||
- **Smart DJ** — crossfade engine with silence skipping, loudness normalization (LUFS), predictive transitions, and full queue reactivity (requires Companion API)
|
||||
- **Playlist reorder** — drag-and-drop song reorder within playlist detail, synced to server via `updatePlaylist`
|
||||
- **Batch metadata editing** — edit album/artist tags across multiple albums simultaneously (requires Companion API)
|
||||
- **Zip import** — import audio files via ZIPFoundation; extraction and batch upload with background `URLSession`
|
||||
- **Custom album covers** — long-press album art to replace with photos from your library
|
||||
- **Multi-server support** — automatic failover between servers with the same credentials
|
||||
- **Background audio** — Lock Screen / Control Center controls, Dynamic Island support
|
||||
- **Keyboard dismiss** — tap anywhere outside text fields or scroll to dismiss
|
||||
- **Dynamic status bar** — status bar icon colour (light/dark) adapts to album art brightness via UIKit `preferredStatusBarStyle` override; decoupled from SwiftUI `preferredColorScheme` so the rest of the UI always stays dark
|
||||
- **Download progress** — circular arc + thin progress bar on song cover art during downloads across all song-list views (Albums, Songs, Playlists, Search)
|
||||
|
||||
### watchOS App
|
||||
|
||||
- **Offline playback** — transfer songs from iPhone to Apple Watch for standalone listening
|
||||
- **Bluetooth + Speaker mode** — Apple Watch Ultra speaker support via HKWorkoutSession
|
||||
- **Crown control** — Digital Crown volume control with haptic feedback
|
||||
- **Compact visualizer** — waveform visualization on watch face
|
||||
|
||||
### Companion API (Optional)
|
||||
|
||||
A Python FastAPI server that runs alongside Navidrome on your server (e.g., Raspberry Pi) providing:
|
||||
|
||||
- **Smart DJ analysis** — BPM detection, silence boundary mapping, LUFS loudness measurement
|
||||
- **Mitsuha visualizer pre-computation** — generate FFT frames on the server instead of on-device
|
||||
- **Remote ID3 tag editing** — edit metadata on server files via mutagen
|
||||
- **File uploads** — upload and auto-tag new music via the app
|
||||
- **WebSocket push** — real-time notifications to the iOS app when metadata changes or uploads complete
|
||||
- **File uploads** — upload and auto-tag new music via the app; ZIP archives extracted via ZIPFoundation
|
||||
- **WebSocket push** — real-time notifications to the iOS app; metadata updates and track uploads trigger an immediate library delta-sync in the app
|
||||
- **Navidrome scan trigger** — automatically rescan after tag edits
|
||||
|
||||
-----
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
|
|
@ -42,15 +52,17 @@ NavidromePlayer/
|
|||
│ ├── Audio/AudioPlayer.swift # AVPlayer + AVAudioEngine, FFT, visualizer levels
|
||||
│ ├── Models/Models.swift # Codable models for all API responses
|
||||
│ └── Storage/
|
||||
│ ├── LibraryCache.swift # Disk cache for instant offline browsing
|
||||
│ ├── LibraryCache.swift # Disk cache + NWPathMonitor + isServerAvailable
|
||||
│ ├── OfflineManager.swift # Download manager with progress tracking
|
||||
│ ├── ServerManager.swift # Multi-server with auto-failover
|
||||
│ └── WatchConnectivityManager.swift # WCSession file transfers
|
||||
│
|
||||
├── iOS/
|
||||
│ ├── App/NavidromePlayerApp.swift # Entry point, background upload delegate
|
||||
│ ├── App/
|
||||
│ │ ├── NavidromePlayerApp.swift # Entry point, BGTask registration
|
||||
│ │ └── NavidromeSceneDelegate.swift # UIWindowSceneDelegate, NavidromeHostingController
|
||||
│ └── Views/
|
||||
│ ├── Common/ # MainTabView, MiniPlayer, AsyncCoverArt, DebugConsole
|
||||
│ ├── Common/ # MainTabView, MiniPlayer, DynamicIslandView, AsyncCoverArt, DebugConsole
|
||||
│ ├── Companion/ # Companion API integration
|
||||
│ │ ├── CompanionAPIService.swift # API client + WebSocket push client
|
||||
│ │ ├── CompanionSettingsView.swift # Config, Smart DJ toggles, analysis triggers
|
||||
|
|
@ -59,25 +71,27 @@ NavidromePlayer/
|
|||
│ │ ├── BatchAlbumEditorSheet.swift # Edit tags for one album
|
||||
│ │ ├── MultiAlbumEditorSheet.swift # Batch edit across multiple albums
|
||||
│ │ ├── BatchUploadView.swift # Zip import + batch upload
|
||||
│ │ └── ZipImportManager.swift # Background URLSession uploads
|
||||
│ │ └── ZipImportManager.swift # ZIPFoundation extraction + background URLSession uploads
|
||||
│ ├── Library/ # MyMusic, Albums, Artists, Playlists, Search, Radio, Downloads
|
||||
│ ├── Login/ # Server configuration
|
||||
│ ├── NowPlaying/ # Full player, SiriSeekBar, RadioStreamBuffer, Shazam
|
||||
│ └── Visualizer/ # MitsuhaVisualizerView, OfflineAudioAnalyzer, storage
|
||||
│ └── Visualizer/ # MitsuhaVisualizerView, OfflineAudioAnalyzer, storage
|
||||
│
|
||||
├── watchOS/
|
||||
│ ├── App/ # Watch app entry, offline store, session manager
|
||||
│ ├── Audio/WatchAudioPlayer.swift # Dual mode: Bluetooth + Ultra speaker
|
||||
│ └── Views/ # Library, NowPlaying, Setup, Visualizer
|
||||
│ └── Views/ # Library, NowPlaying, Setup
|
||||
│
|
||||
├── project.yml # XcodeGen project definition
|
||||
├── project.yml # XcodeGen project definition (includes ZIPFoundation SPM)
|
||||
└── generate.sh # Regenerate .xcodeproj
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
## Requirements
|
||||
|
||||
- **iOS 17.0+** / **watchOS 10.0+**
|
||||
- **Xcode 15+**
|
||||
- **iOS 26.0+** / **watchOS 26.0+**
|
||||
- **Xcode 26+**
|
||||
- **XcodeGen** — `brew install xcodegen`
|
||||
- **Navidrome server** — any version with Subsonic API support
|
||||
|
||||
|
|
@ -90,7 +104,50 @@ NavidromePlayer/
|
|||
|
||||
Set your Apple Developer Team ID in `project.yml` under `DEVELOPMENT_TEAM`.
|
||||
|
||||
---
|
||||
> **ZIPFoundation** is declared as an SPM dependency in `project.yml`. Xcode resolves it automatically after running `./generate.sh`.
|
||||
|
||||
-----
|
||||
|
||||
## Key Architecture Notes
|
||||
|
||||
### Offline Availability (`LibraryCache.isServerAvailable`)
|
||||
|
||||
All song-list views use `LibraryCache.shared.isServerAvailable` to decide whether to grey out and disable songs. This property combines two signals:
|
||||
|
||||
1. **NWPathMonitor** — detects network loss (`isOffline`)
|
||||
1. **ServerManager.connectionState** — detects server unreachability even when the network is up
|
||||
|
||||
`LibraryCache` subscribes to `ServerManager.$connectionState` via Combine and fires `objectWillChange`, so all observing views update reactively when the server connects or drops. A `hasEverConnected` guard prevents songs flashing grey on first launch while the initial ping is in-flight.
|
||||
|
||||
### Status Bar Dynamic Coloring
|
||||
|
||||
`preferredStatusBarStyle` is decoupled from SwiftUI’s `preferredColorScheme`. Architecture:
|
||||
|
||||
- `NavidromeSceneDelegate` (in `iOS/App/NavidromeSceneDelegate.swift`) creates the window manually with `NavidromeHostingController` as root
|
||||
- `NavidromeHostingController: UIHostingController<AnyView>` overrides `preferredStatusBarStyle` to read from `AlbumColorExtractor.shared.preferredStatusBarStyle`
|
||||
- `AlbumColorExtractor` derives UIKit style from album art HSB brightness (threshold: 0.88) and publishes it — the scene delegate subscribes and calls `setNeedsStatusBarAppearanceUpdate()` on change
|
||||
- `NowPlayingView` keeps `.preferredColorScheme(.dark)` so all SwiftUI views stay dark; only the status bar icons flip
|
||||
|
||||
### Smart DJ / Crossfade Queue Reactivity
|
||||
|
||||
`SmartCrossfadeManager` (dual-AVPlayer A/B engine) stays in sync with the queue through:
|
||||
|
||||
- All queue mutations (`playNext`, `playLater`, `moveQueueItem`, `removeFromQueue`) call `notifyQueueChanged()`
|
||||
- If not crossfading: immediately calls `prepareNextForCrossfade()` to load the new `queue[queueIndex+1]` into standby
|
||||
- If mid-crossfade: sets `queueChangedDuringFade = true` to avoid interrupting audio; `finalizeCrossfade()` calls `needsNextTrack()` → `prepareNextForCrossfade()` which reads current queue state
|
||||
- `needsNextTrack` syncs `queueIndex` by searching for `pendingNextSong.id` in the current queue (handles reorder and removal); falls back to sequential advancement if the song was removed
|
||||
- Scrobble fires in `needsNextTrack` (not `playerDidFinish`) to avoid double-counting on the crossfade path
|
||||
- Safety-net `AVPlayerItemDidPlayToEndTime` observer is re-registered on every crossfade slot swap
|
||||
|
||||
### Companion Push → Library Sync
|
||||
|
||||
`SyncEngine` subscribes to `.companionMetadataUpdated` and `.companionTrackUploaded` notifications (posted by `CompanionPushClient`) via Combine. Events are debounced 2 seconds and trigger a delta sync, so uploads and tag edits appear in the library immediately rather than waiting for the 15-minute background refresh.
|
||||
|
||||
### WaveStateCache (Visualizer Morph Continuity)
|
||||
|
||||
`WaveStateCache.shared` stores the most recent compact visualizer `displayLevels` array. When the wave flies between `MiniPlayerBar` and `DynamicIslandView` via `matchedGeometryEffect`, the incoming view seeds from the cache so the wave shape is continuous rather than resetting to flat idle.
|
||||
|
||||
-----
|
||||
|
||||
## Companion API
|
||||
|
||||
|
|
@ -101,16 +158,16 @@ The Companion API is optional. Without it, the app works as a standard Navidrome
|
|||
```
|
||||
/home/pi/docker/navidrome/
|
||||
├── docker-compose.yml # Both Navidrome + Companion
|
||||
├── navidrome_data/ # Navidrome database (auto-created)
|
||||
├── companion_data/ # Persistent data (auto-created)
|
||||
│ ├── smart_dj.db # BPM, silence, loudness profiles
|
||||
│ └── vis_cache/ # Pre-computed Mitsuha FFT frames
|
||||
└── companion_api/ # Companion API source code
|
||||
├── navidrome_data/ # Navidrome database (auto-created)
|
||||
├── companion_data/ # Persistent data (auto-created)
|
||||
│ ├── smart_dj.db # BPM, silence, loudness profiles
|
||||
│ └── vis_cache/ # Pre-computed Mitsuha FFT frames
|
||||
└── companion_api/ # Companion API source code
|
||||
├── Dockerfile
|
||||
├── main.py
|
||||
└── pre_analyze.py
|
||||
|
||||
/home/pi/navidrome/music/ # Your music library
|
||||
/home/pi/navidrome/music/ # Your music library
|
||||
├── Artist/Album/song.flac
|
||||
└── ...
|
||||
```
|
||||
|
|
@ -175,7 +232,7 @@ docker compose exec music-companion python pre_analyze.py --dj # DJ profile
|
|||
docker compose exec music-companion python pre_analyze.py --vis # Visualizer frames only
|
||||
docker compose exec music-companion python pre_analyze.py --force # Re-analyze everything
|
||||
|
||||
# ── Reset Analysis Data ─────────────────────────────────
|
||||
# ── Reset Analysis Data ──────────────────────────────────
|
||||
rm companion_data/smart_dj.db # Delete DJ profiles
|
||||
rm -rf companion_data/vis_cache/* # Delete vis frame cache
|
||||
docker compose exec music-companion python pre_analyze.py # Regenerate from scratch
|
||||
|
|
@ -188,47 +245,61 @@ docker compose down -v # Stop + remove volu
|
|||
|
||||
### API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/health` | Server status, profile count, vis cache count, connected clients |
|
||||
| `PATCH` | `/edit-metadata` | Edit ID3 tags (JSON body: `relative_path`, `title`, `artist`, `album`, etc.) |
|
||||
| `POST` | `/upload-track` | Upload audio file with metadata (multipart: `file`, `title`, `artist`, `album`) |
|
||||
| `GET` | `/smart-dj/profile?relative_path=...` | BPM, silence start/end, LUFS for a track |
|
||||
| `GET` | `/smart-dj/bulk-profiles?paths=...` | Batch fetch profiles (comma-separated paths) |
|
||||
| `GET` | `/visualizer/frames?relative_path=...` | Pre-computed Mitsuha FFT frames (JSON array) |
|
||||
| `POST` | `/visualizer/precompute` | Trigger background vis frame generation for all tracks |
|
||||
| `POST` | `/bulk-fix` | Trigger Navidrome library rescan |
|
||||
| `WS` | `/ws/push` | Real-time push events to iOS app |
|
||||
|Method |Endpoint |Description |
|
||||
|-------|--------------------------------------|-------------------------------------------------------------------------------|
|
||||
|`GET` |`/health` |Server status, profile count, vis cache count, connected clients |
|
||||
|`PATCH`|`/edit-metadata` |Edit ID3 tags (JSON body: `relative_path`, `title`, `artist`, `album`, etc.) |
|
||||
|`POST` |`/upload-track` |Upload audio file with metadata (multipart: `file`, `title`, `artist`, `album`)|
|
||||
|`GET` |`/smart-dj/profile?relative_path=...` |BPM, silence start/end, LUFS for a track |
|
||||
|`GET` |`/smart-dj/bulk-profiles?paths=...` |Batch fetch profiles (comma-separated paths) |
|
||||
|`GET` |`/visualizer/frames?relative_path=...`|Pre-computed Mitsuha FFT frames (JSON array) |
|
||||
|`POST` |`/visualizer/precompute` |Trigger background vis frame generation for all tracks |
|
||||
|`POST` |`/bulk-fix` |Trigger Navidrome library rescan |
|
||||
|`WS` |`/ws/push` |Real-time push events to iOS app |
|
||||
|
||||
### Path Resolution
|
||||
|
||||
Navidrome's `song.path` field can differ from the actual filesystem path. The Companion API uses a three-strategy resolver:
|
||||
Navidrome’s `song.path` field can differ from the actual filesystem path. The Companion API uses a four-strategy resolver:
|
||||
|
||||
1. **Direct join** — `MUSIC_DIR + relative_path`
|
||||
2. **Strip prefix** — removes leading components one at a time (handles Navidrome's library folder prefix)
|
||||
3. **Filename search** — walks the music directory looking for an exact filename match
|
||||
1. **Strip prefix** — removes leading components one at a time (handles Navidrome’s library folder prefix)
|
||||
1. **Filename search** — walks the music directory for an exact filename match
|
||||
1. **Fuzzy DB match** — searches `file_index` SQLite table with 50% word-match threshold
|
||||
|
||||
If a 404 still occurs, the error response includes the exact paths that were tried for debugging.
|
||||
|
||||
### iOS App Configuration
|
||||
|
||||
1. **Settings** → **Companion API**
|
||||
2. Enter your server's IP address and port (default: 8000)
|
||||
3. Toggle **Enable Companion API**
|
||||
4. Tap **Test Connection** to verify
|
||||
5. Toggle **Smart DJ** for crossfade and analysis features
|
||||
1. Enter your server’s IP address and port (default: 8000)
|
||||
1. Toggle **Enable Companion API**
|
||||
1. Tap **Test Connection** to verify
|
||||
1. Toggle **Smart DJ** for crossfade and analysis features
|
||||
|
||||
---
|
||||
-----
|
||||
|
||||
## Feature Details
|
||||
|
||||
### Mitsuha Visualizer
|
||||
|
||||
The visualizer renders smooth liquid waveforms using Catmull-Rom splines with configurable tension. FFT data comes from one of three sources:
|
||||
|
||||
- **Real-time engine FFT** — AVAudioEngine tap on local files
|
||||
- **Offline pre-analyzed frames** — cached FFT data synced to playback position
|
||||
- **Server-computed frames** — downloaded from Companion API (saves device battery)
|
||||
|
||||
Settings include per-view configuration for Now Playing and Mini Player independently, with four built-in presets. The mini player visualizer automatically pauses when the full-screen Now Playing view is open (battery optimization).
|
||||
The compact visualizer (mini player, Dynamic Island) uses `WaveStateCache` to preserve wave shape across the `matchedGeometryEffect` flight animation, and `matchedGeometryEffect(id: "visWave")` to physically squish/morph the wave between positions rather than cutting. Settings include per-view configuration for Now Playing and Mini Player independently. The mini player visualizer pauses when the full-screen Now Playing view is open (battery optimisation).
|
||||
|
||||
> **Do not refactor `WavePhysics` or the `Canvas` rendering.** It is heavily optimised with Catmull-Rom splines, touch ripples, temporal smoothing via viscosity, and a phase-shifted depth shadow layer for liquid parallax. Breaking any part of this will degrade visual quality significantly.
|
||||
|
||||
### Smart DJ / Crossfade
|
||||
|
||||
- **Silence detection** — `silence_start` is where *trailing* silence begins (crossfade trigger); `silence_end` is where *leading* silence ends (seek-to point). These fields are intentionally backwards from what you’d expect.
|
||||
- **LUFS normalization** — volume scaled as `pow(10, gainDB/20)` clamped to 0.05–1.0
|
||||
- **Dual-AVPlayer A/B** — standby player pre-loaded and seeked past leading silence while active player fades out
|
||||
- **Queue reactivity** — see *Smart DJ / Crossfade Queue Reactivity* above
|
||||
- **Scrobble** — fires in `needsNextTrack` on the crossfade path; fires in `playerDidFinish` on the normal AVPlayer path (guarded against double-fire)
|
||||
- **Visualizer handoff** — at 50% crossfade midpoint, `visualizerHandoff` callback triggers loading the incoming song’s offline vis frames so they’re ready by the time the fade completes
|
||||
|
||||
### Radio
|
||||
|
||||
|
|
@ -238,41 +309,73 @@ Settings include per-view configuration for Now Playing and Mini Player independ
|
|||
- **Recording** — capture radio segments to local files
|
||||
- **Shazam** — identify currently playing content via SHSession + dedicated AVAudioEngine mic tap
|
||||
- **Recorded playback** — recorded radio files play with ±5s skip buttons instead of prev/next
|
||||
- **Live toggle** — start/stop buffering, snap to live edge
|
||||
- **Offline** — stations dim and show an orange banner when server is unreachable; taps are blocked
|
||||
|
||||
### Playlist Reorder
|
||||
|
||||
Playlist detail view has a **Reorder** toolbar button that toggles a `List` with `.onMove` + `.environment(\.editMode, .constant(.active))`. Drag to reorder locally (optimistic update), synced to server via `SubsonicClient.reorderPlaylist()` which removes all indices then re-adds in new order atomically. Reverts to server state on error.
|
||||
|
||||
### Batch Tag Editing
|
||||
|
||||
Long-press any album to:
|
||||
- **Edit Album** — edit tags for all tracks in that album
|
||||
- **Select Albums** — enter multi-select mode (nav bar transforms: Cancel / count / Edit button), tap albums to select, "Select All" toggle, then apply changes across all selected albums
|
||||
|
||||
The "Set same artist on all tracks" toggle fixes compilation albums that split into separate per-artist entries in Navidrome.
|
||||
- **Edit Album** — edit tags for all tracks in that album
|
||||
- **Select Albums** — enter multi-select mode (nav bar transforms: Cancel / count / Edit button), tap albums to select, “Select All” toggle, then apply changes across all selected albums
|
||||
|
||||
The “Set same artist on all tracks” toggle fixes compilation albums that split into separate per-artist entries in Navidrome.
|
||||
|
||||
### Zip Import
|
||||
|
||||
`ZipImportManager` uses [ZIPFoundation](https://github.com/weichsel/ZIPFoundation) (SPM, iOS target only) to extract archives. The previous `NSFileCoordinator(.forUploading)` approach was incorrect — it re-compressed the source URL rather than extracting it. After extraction, audio files are staged and uploaded via background `URLSession` with per-file progress tracking.
|
||||
|
||||
### Downloads
|
||||
|
||||
Split into two sub-tabs:
|
||||
|
||||
- **Offline** — storage usage, downloaded songs with playback, swipe to delete
|
||||
- **Watch** — Apple Watch connection status, pending transfers with progress, songs on watch, send all / delete all
|
||||
|
||||
Download progress shows as a circular arc overlay on cover art plus a thin capsule progress bar under the song title — consistent across Albums, Songs, Playlists, and Search views.
|
||||
|
||||
### Debug Console
|
||||
|
||||
Toggle in Settings → Developer. Two display modes:
|
||||
|
||||
- **Docked** — panel above the tab bar, drag handle to resize (120–500pt)
|
||||
- **PiP** — floating draggable/resizable window with collapse, dock-back, and close buttons
|
||||
|
||||
All iOS-path log calls route through `DebugLogger` via `alog()` / `wlog()` helpers. watchOS falls back to `print()` since `DebugLogger` is iOS-only.
|
||||
|
||||
### Caching
|
||||
|
||||
All views use cache-first loading to eliminate spinners on warm launches:
|
||||
- **LibraryCache** — albums, artists, playlists, genres, album/artist details as JSON
|
||||
- **ImageCache** — two-tier NSCache + disk JPEG with 200MB limit and LRU eviction
|
||||
|
||||
- **LibraryCache** — albums, artists, playlists, genres, album/artist details as JSON; also owns `isServerAvailable` combining NWPathMonitor + ServerManager state
|
||||
- **ImageCache** — two-tier NSCache + disk JPEG with 200 MB limit and LRU eviction
|
||||
- **SmartDJCache** — Smart DJ profiles cached locally per song path
|
||||
- **WaveStateCache** — compact visualizer levels for morph continuity across DI transitions
|
||||
- **AlbumCoverStore** — user-set custom album covers in Documents
|
||||
- **VisualizerStorageManager** — pre-analyzed FFT frames per song
|
||||
|
||||
Detail views load from cache instantly, refresh from server silently, and show a Retry button if both cache and server are unavailable.
|
||||
|
||||
---
|
||||
-----
|
||||
|
||||
## Critical Rules for Future Developers
|
||||
|
||||
1. **DO NOT** refactor `WavePhysics` or `Canvas` rendering in `MitsuhaVisualizerView`
|
||||
1. **DO NOT** make `AudioPlayer.currentTime` / `duration` `@Published` — causes 10 Hz full-hierarchy rerenders; leaf views poll via local `Timer.publish`
|
||||
1. **DO NOT** use `Menu` in `NowPlayingView` — causes `_UIReparentingView` errors; use `Button + .sheet`
|
||||
1. **DO NOT** use `.contextMenu` on watchOS — deprecated; use `.swipeActions`
|
||||
1. **DO NOT** use `UIScreen.main` — deprecated iOS 26; use `GeometryReader`
|
||||
1. **DO NOT** use `sheet(isPresented:)` with `if let` — causes timing bugs; use `sheet(item:)`
|
||||
1. **DO NOT** use `.preferredColorScheme` to control status bar style — it cascades to the entire view hierarchy and flips all system colors; use the `NavidromeHostingController` UIKit bridge instead
|
||||
1. `silence_start` from the Companion API is **trailing** silence; `silence_end` is **leading** silence — this is backwards from what you’d expect
|
||||
1. `song.path` from Navidrome is a virtual ID3-derived path — never treat it as a real filesystem path
|
||||
1. `album_artist` is the field Navidrome groups albums by — not `artist`
|
||||
|
||||
-----
|
||||
|
||||
## License
|
||||
|
||||
Personal project. Not affiliated with Navidrome.
|
||||
Personal project. Not affiliated with Navidrome.
|
||||
Loading…
Reference in a new issue