iOS: CarPlay (#420)

A CarPlay scene beside the phone's: In Progress, Unread and Shows as
CPListTemplates from ipx's API, read with the player's credentials, and
a pick played through the same Playback, so it resumes from the saved
place, saves as it goes and marks read at 90%. Now Playing is the
system's template over Playback's now-playing info.

When the car starts a different episode the page is sent
{t:"replaced"} and closes its player bar. Left open, the bar would have
gone on as the old episode while the host's clock belonged to the new
one, saving that time and marking the wrong item read.

The badge's fetch moves into API.get, which CarPlay shares. Build 13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ray Slakinski
2026-11-09 08:10:29 -05:00
parent 711b0395f1
commit 2bb7ed2109
12 changed files with 354 additions and 18 deletions

View File

@@ -29,6 +29,8 @@ final class Playback: NSObject {
var onError: ((String) -> Void)?
/// The sleep timer ran out and playback paused.
var onSlept: (() -> Void)?
/// Something the page did not choose is playing now: the car's pick (#420).
var onReplaced: (() -> Void)?
private let session: URLSession = {
let cfg = URLSessionConfiguration.default
@@ -53,6 +55,8 @@ final class Playback: NSObject {
private(set) var sleepAt: Date?
var isPlaying: Bool { player?.timeControlStatus == .playing }
/// What is loaded, for the car's lists to mark.
var current: Item? { item }
var currentTime: Double { player?.currentTime().seconds ?? 0 }
var duration: Double? {
guard let d = player?.currentItem?.duration.seconds, d.isFinite, d > 0 else { return nil }
@@ -163,6 +167,19 @@ final class Playback: NSObject {
return true
}
/// An episode chosen away from the page, in the car (#420): played from where it got to, and
/// the page told, whose player bar would otherwise go on as the old one, saving its place and
/// marking it read by this one's clock.
func playChosen(_ item: Item, startAt: Double?) {
if item == self.item {
play()
return
}
onReplaced?()
load(item, startAt: startAt)
play()
}
func pause() {
player?.pause()
onState?(false)