ios: reorderable tab bar with more tabs, debug log export, AirPlay picker, more room above seeker

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Sonnet 5.5 committed 2026-10-20 14:11:58 -07:00
1 parent a0fe35a8d8
commit 926ad6ee99
10 files changed
+350 -10

No files matched your search

@@ -181,6 +181,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
/// Surfaces a message wherever messages show (toasts on the phone, an alert in the car).
func report(_ text: String) {
Log.shared.error("report", text)
message = AppMessage(text: text)
messageListeners.values.forEach { $0(text) }
}
@@ -797,6 +798,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
guard let self, let info = n.userInfo,
let raw = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: raw) else { return }
Log.shared.info("session", "interruption \(type == .began ? "began" : "ended")")
switch type {
case .began:
// A stale "began" arrives after the app was suspended; the session wasn't really taken.
@@ -813,12 +815,14 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
nc.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { [weak self] n in
guard let self, let raw = n.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: raw) else { return }
Log.shared.info("route", "change reason=\(reason.rawValue) now \(AirRoute.describe())")
// Headphones unplugged or Bluetooth gone: don't carry on out of the speaker.
if reason == .oldDeviceUnavailable && self.playWhenReady { self.pause() }
self.eq.refreshRoute()
}
nc.addObserver(forName: AVAudioSession.mediaServicesWereResetNotification, object: nil, queue: .main) { [weak self] _ in
guard let self else { return }
Log.shared.error("session", "media services reset")
let was = self.playWhenReady
let pos = self.positionMs()
self.currentToken = -1