Add iOS app with CarPlay and Siri

SwiftUI port of the Android app under ios/, with CarPlay browse and
Now Playing, Siri play-media intents, and a build-ipa.sh script that
makes an unsigned IPA for sideloading. README gets an iOS section and
the vibecoded disclaimer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-10-20 07:15:47 -07:00
1 parent 45bc0eefe0
commit fdf9e970b2
45 files changed
+9822

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
import SwiftUI
enum Tab: String, CaseIterable, Hashable {
case Library, Playlists, Search, Downloads, Settings
var icon: String {
switch self {
case .Library: "square.stack.fill"
case .Playlists: "music.note.list"
case .Search: "magnifyingglass"
case .Downloads: "arrow.down.circle.fill"
case .Settings: "gearshape.fill"
}
}
}
enum Route: Hashable {
case Artists, Albums, Songs, Playlists
case Eq, EqFilters(profileId: String), EqFilterPage(profileId: String, filterId: String)
case PlexAccount, StemServer, Quality, Storage, Appearance
case ArtistPage(Artist), AlbumPage(Album), PlaylistPage(Playlist)
}
/// Tabs, their stacks and the Now Playing sheet: the things any screen can ask of the shell.
@MainActor
@Observable
final class Ui {
var tab: Tab = .Library
var paths: [Tab: [Route]] = [:]
var nowPlayingOpen = false {
didSet { overlays.level = nowPlayingOpen ? 1 : 0 }
}
let overlays: Overlays
init(overlays: Overlays) { self.overlays = overlays }
func path(_ t: Tab) -> Binding<[Route]> {
Binding(get: { self.paths[t] ?? [] }, set: { self.paths[t] = $0 })
}
func push(_ r: Route) { paths[tab, default: []].append(r) }
func pop() { if paths[tab]?.isEmpty == false { paths[tab]?.removeLast() } }
/// Tapping the selected tab again pops it to its root.
func selectTab(_ t: Tab) {
if t == tab { paths[t] = [] } else { tab = t }
}
func openNowPlaying() { nowPlayingOpen = true }
/// Switches to the Library tab and pushes [r] there.
func showInLibrary(_ r: Route) {
nowPlayingOpen = false
tab = .Library
paths[.Library, default: []].append(r)
}
/// Switches to Settings and opens [r] (the Equalizer) there.
func showInSettings(_ r: Route) {
nowPlayingOpen = false
tab = .Settings
if paths[.Settings]?.last != r { paths[.Settings, default: []].append(r) }
}
}