Skip to content
Closed
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ FastAPI backend (`server/`) wrapping `ytmusicapi` to expose YouTube Music endpoi

## iOS app (`ios/`)
- Two XcodeGen targets from `ios/project.yml`: `Carmen` (app) + `CarmenLiveActivity` (widget extension for the Live Activities — lock-screen lyrics *and* download progress). `Shared/` sources compile into both. `Carmen.xcodeproj` and the `Info.plist`s are **generated — never hand-edit**; regenerate with `cd ios && xcodegen generate`.
- Min iOS 18; every iOS 26 / Liquid Glass API is gated with `#available(iOS 26.0, *)` — use the wrappers in `ios/Carmen/Views/Glass.swift`, never call `.glassEffect` directly (and never `.clipped()` on a glass surface).
- The UI is a **Material 3 clone of the Android app** (`android/`, ArchiveTune fork): design tokens in `Views/Theme/Theme.swift` (`@Environment(\.m3)` roles derived live from the artwork accent / settings accent / `ED5564` fallback; Poppins type scale `.m3*`, bundled from `android/app/src/main/res/font/poppins.ttf` via `UIAppFonts`; `M3Shape`/`M3Dim`), and the component kit in `Views/Components/` (`TopAppBar`, `ListRow`, `GridTile`, chips, cards, shimmers, `M3*State` panes, sheet menus, buttons, `ArtworkView`). Screens consume the kit — never raw hexes or `.system(size:)` fonts. The root applies `.m3Theme()` (outermost modifier in `RootView`); NowPlayingView re-derives dark roles for its subtree.
- Shell (`RootView`): **3 tabs** (Home / Search / Library) with the native tab bar hidden behind a floating pill nav (`FloatingNavBlob` — Android's CONNECTED navigation bar: mini player stacked on the pill as one `surfaceContainer` blob, radius lerp 32↔28). The blob is a bottom `safeAreaInset` so scrolls end above it. Root screens own their TopAppBar (pinned via `safeAreaInset(edge: .top)`) and present Settings themselves; pushed detail/results screens keep the native inline bar. `Route`/`.carmenDestinations()` (`Views/Theme/Navigation.swift`) are the single push contract (album/artist/playlist/searchResults).
- Min iOS 18; every iOS 26 / Liquid Glass API is gated with `#available(iOS 26.0, *)` — use the wrappers in `ios/Carmen/Views/Glass.swift`, never call `.glassEffect` directly (and never `.clipped()` on a glass surface). The M3 clone does not use Liquid Glass anywhere.
- `Player` / `SettingsStore.shared` / `LibraryStore.shared` / `DownloadsStore.shared` are the single sources of truth (`@MainActor @Observable`); views read them from the environment (the App injects all four). The API base URL defaults to `https://api-carmen-v1.syr.qzz.io` and is overridable in-app.
- Playback source chain in `Player.startCurrent`: downloaded file (`Documents/songs/<id>/audio.*`) → `carmen-cache://` URL whose `StreamResourceLoader` (`State/StreamCache.swift`) serves/fills byte ranges from `Library/Caches/streams/` (1 GB LRU) → failure. Library data (favorites/recents/local playlists/downloads/suggestions) is on-device only — the API is read-only.
- The full player is an **overlay driven by `Player.expansion`** (0 = mini bar, 1 = full; RootView slides it by 1000 pt), not a `.fullScreenCover`. Always go through `openPlayer()` / `closePlayer()`; `showNowPlaying` is `private(set)` (set only there). Drag gestures write `expansion` directly.
Expand All @@ -31,7 +33,7 @@ FastAPI backend (`server/`) wrapping `ytmusicapi` to expose YouTube Music endpoi
- Adaptive accent: `Color.carmen` follows `Player.adaptiveColorHex` (the cover's own colour, read by `State/ArtworkPalette.swift` — port of the web `pickAccentColor`) when Settings → Appearance → Adaptive Accent is on (default), else the chosen palette accent. `NowPlayingBackground` washes the player with it; panels scrim over it instead of using a material.
- Downloads run through a **background URLSession** (`com.z44d.carmen.downloads` in `State/Downloads.swift`) so they survive suspension; iOS relaunches the app to deliver events (`CarmenAppDelegate.handleEventsForBackgroundURLSession` in `CarmenApp.swift`). Byte progress drives a download Live Activity (`Shared/DownloadActivityAttributes.swift` + `State/DownloadActivity.swift`; second `ActivityConfiguration` in the widget bundle). Delegate helpers the store exposes to the session are `nonisolated` and hop to `@MainActor` internally.
- Every fallible layer (API requests, stream loader, player transitions, downloads, motion artwork) writes to `State/DebugLog.swift` (`Caches/logs/carmen.log`, 2 MB rotation); users export it from Settings → Diagnostics. Log failures where they happen — that file is the bug report.
- Settings is the **4th tab** (`RootView`); `SettingsView(embeddedInTab: true)` hides Done. `NetworkMonitor` (`State/NetworkMonitor.swift`) bumps a `generation` that `ArtworkView` `.id()`s on (covers reload on reconnect) and triggers `Player.retryIfBroken()` from `RootView`.
- Settings is **not a tab** — each root screen's TopAppBar overflow opens it as `.sheet { SettingsView() }` (self-wrapped in a NavigationStack with Done; `embeddedInTab` still hides Done for embedded use). `NetworkMonitor` (`State/NetworkMonitor.swift`) bumps a `generation` that `ArtworkView` `.id()`s on (covers reload on reconnect) and triggers `Player.retryIfBroken()` from `RootView`. The Queue tab is gone — the queue lives in the player as a `QueuePanel` (`PlayerPanel.queue`).
- Use `Color.carmen` for the accent (Settings → Appearance, hex in `Settings.swift`), never `Color.accentColor` — it no longer matches the theme.
- No signing in `project.yml`: CI builds unsigned (`CODE_SIGNING_ALLOWED=NO`); a sideload tool re-signs the IPA. Release version = `MARKETING_VERSION` in `ios/project.yml`.
- There is **no Xcode on this machine** (CommandLineTools only): you cannot compile Swift locally. Verify with `swift format lint` (full parse), then rely on the `ios-check` job in `.github/workflows/build.yml` — it requires the iOS 26 SDK (Xcode 26+), enforced by an SDK guard.
Expand Down
Binary file added ios/Carmen/Fonts/poppins.ttf
Binary file not shown.
Loading
Loading