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:
1 parent
45bc0eefe0
commit
fdf9e970b2
45 files changed
+9822
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// Whether Plex or stemserve answers right now. In the car the phone is usually on cellular
|
||||
/// and the LAN addresses just time out, so the car UI asks this first (a quick probe,
|
||||
/// remembered briefly) and goes straight to cached data when home is out of reach.
|
||||
@Observable
|
||||
final class HomeReach: @unchecked Sendable {
|
||||
private static let ttl: UInt64 = 20_000_000_000
|
||||
|
||||
@ObservationIgnored private let settings: Settings
|
||||
@ObservationIgnored private let session: URLSession
|
||||
@ObservationIgnored private let lock = AsyncLock()
|
||||
@ObservationIgnored private let stamp = UnfairLock()
|
||||
@ObservationIgnored private var checkedAt: UInt64 = 0
|
||||
@ObservationIgnored private var _last = true
|
||||
@ObservationIgnored private var listeners: [() -> Void] = []
|
||||
|
||||
/// The last answer; true until a probe says otherwise. Changes as the phone joins or leaves home Wi-Fi.
|
||||
private(set) var home = true
|
||||
|
||||
/// Readable from any thread (the car's Plex client asks it on every request).
|
||||
var lastKnown: Bool { stamp.with { _last } }
|
||||
|
||||
init(settings: Settings, net: Connectivity) {
|
||||
self.settings = settings
|
||||
let c = URLSessionConfiguration.ephemeral
|
||||
c.timeoutIntervalForRequest = 2
|
||||
c.timeoutIntervalForResource = 2.5
|
||||
c.waitsForConnectivity = false
|
||||
session = URLSession(configuration: c)
|
||||
// A new network (home Wi-Fi in range, or lost) invalidates the answer; ask again once it
|
||||
// settles, so the car's lists can switch over without anyone browsing first.
|
||||
net.onChange { [weak self] in
|
||||
guard let self else { return }
|
||||
self.stamp.with { self.checkedAt = 0 }
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_500_000_000)
|
||||
_ = await self.reachable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main-thread callback when [home] changes.
|
||||
func onChange(_ f: @escaping () -> Void) { listeners.append(f) }
|
||||
|
||||
func reachable() async -> Bool {
|
||||
await lock.run {
|
||||
let now = DispatchTime.now().uptimeNanoseconds
|
||||
let (at, last) = stamp.with { (checkedAt, _last) }
|
||||
if at != 0 && now - at < Self.ttl { return last }
|
||||
let plex = settings.plexUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/identity"
|
||||
let stems = settings.stemUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + "/api/info"
|
||||
async let a = answers(plex)
|
||||
async let b = answers(stems)
|
||||
let (ra, rb) = await (a, b)
|
||||
let ok = ra || rb
|
||||
stamp.with { _last = ok; checkedAt = DispatchTime.now().uptimeNanoseconds }
|
||||
await MainActor.run {
|
||||
if home != ok {
|
||||
home = ok
|
||||
listeners.forEach { $0() }
|
||||
}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
private func answers(_ s: String) async -> Bool {
|
||||
guard let url = URL(string: s), let (_, r) = try? await session.data(from: url) else { return false }
|
||||
return ((r as? HTTPURLResponse)?.statusCode ?? 500) < 500
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user