diff --git a/tabatago-swift/TabataGo/Resources/Info.plist b/tabatago-swift/TabataGo/Resources/Info.plist index 9af55fc..36ae005 100644 --- a/tabatago-swift/TabataGo/Resources/Info.plist +++ b/tabatago-swift/TabataGo/Resources/Info.plist @@ -28,7 +28,7 @@ CFBundleVersion - 1 + 2 NSHealthShareUsageDescription TabataGo reads your health data to show fitness stats and personalize your workouts. NSHealthUpdateUsageDescription diff --git a/tabatago-swift/TabataGo/Services/AudioService.swift b/tabatago-swift/TabataGo/Services/AudioService.swift index 0b053a3..ec8c6b4 100644 --- a/tabatago-swift/TabataGo/Services/AudioService.swift +++ b/tabatago-swift/TabataGo/Services/AudioService.swift @@ -28,12 +28,55 @@ final class AudioService { try audioSession.setCategory( .playback, mode: .default, - options: [.mixWithOthers, .allowAirPlay, .allowBluetooth] + options: [.allowAirPlay, .allowBluetooth] ) try audioSession.setActive(true) } catch { print("[Audio] Session configuration failed: \(error)") } + + // ─── Interruption Handler + NotificationCenter.default.addObserver( + forName: AVAudioSession.interruptionNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self, + let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } + + if type == .ended { + guard let optionsValue = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt, + AVAudioSession.InterruptionOptions(rawValue: optionsValue).contains(.shouldResume) else { return } + do { + try self.audioSession.setActive(true) + } catch { + print("[Audio] Re-activation failed after interruption: \(error)") + } + } + } + + // ─── Route Change Handler + NotificationCenter.default.addObserver( + forName: AVAudioSession.routeChangeNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self, + let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } + + switch reason { + case .newDeviceAvailable, .oldDeviceUnavailable: + do { + try self.audioSession.setActive(true) + } catch { + print("[Audio] Route re-activation failed: \(error)") + } + default: + break + } + } } // ─── Phase Audio Cues ───────────────────────────────────────── diff --git a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift index 087cd8c..ec53046 100644 --- a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift +++ b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift @@ -39,6 +39,7 @@ final class PlayerViewModel: ObservableObject { @Published var isComplete: Bool = false @Published var showExitConfirmation: Bool = false @Published private(set) var completedSession: WorkoutSession? = nil + @Published var liveActivityError: String? = nil // Track info for Live Activity — set by View when music changes var currentTrackTitle = "" @@ -382,7 +383,10 @@ final class PlayerViewModel: ObservableObject { // ─── Dynamic Island / Live Activity ───────────────────────────── func syncActivity(shouldAlert: Bool = false) { - guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } + guard ActivityAuthorizationInfo().areActivitiesEnabled else { + liveActivityError = "Live Activities are disabled in Settings" + return + } guard isRunning else { return } let isPlayingMusic = (phase == .work || phase == .rest) && isRunning && !isPaused @@ -449,8 +453,10 @@ final class PlayerViewModel: ObservableObject { attributes: attrs, content: ActivityContent(state: state, staleDate: staleDate) ) + liveActivityError = nil observeActivityState() } catch { + liveActivityError = error.localizedDescription print("❌ Failed to start Live Activity: \(error.localizedDescription)") } } diff --git a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift index c43cb71..ea6a7a8 100644 --- a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift +++ b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift @@ -66,6 +66,40 @@ struct PlayerView: View { } } + // ══════════════════════════════════════════════════ + // Live Activity error banner (non-blocking) + // ══════════════════════════════════════════════════ + if let error = vm.liveActivityError { + VStack { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.caption) + .foregroundStyle(.yellow) + Text(error) + .font(.caption2.weight(.medium)) + .foregroundStyle(.white.opacity(0.9)) + Spacer() + Button { + vm.liveActivityError = nil + } label: { + Image(systemName: "xmark") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.white.opacity(0.6)) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(.ultraThinMaterial.opacity(0.9)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.horizontal, 16) + .padding(.top, 60) + Spacer() + } + .transition(.move(edge: .top).combined(with: .opacity)) + .animation(.spring(duration: 0.3), value: vm.liveActivityError) + .zIndex(100) + } + // ══════════════════════════════════════════════════ // Layer 4 — Compact timer ring (top-right corner) // ══════════════════════════════════════════════════ diff --git a/tabatago-swift/TabataGoWatch/Complications/Info.plist b/tabatago-swift/TabataGoWatch/Complications/Info.plist index e556090..5bfd1a7 100644 --- a/tabatago-swift/TabataGoWatch/Complications/Info.plist +++ b/tabatago-swift/TabataGoWatch/Complications/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 1 + 2 NSExtension NSExtensionPointIdentifier diff --git a/tabatago-swift/TabataGoWatch/Resources/Info.plist b/tabatago-swift/TabataGoWatch/Resources/Info.plist index 0eed266..ea4cb6f 100644 --- a/tabatago-swift/TabataGoWatch/Resources/Info.plist +++ b/tabatago-swift/TabataGoWatch/Resources/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 1 + 2 NSHealthShareUsageDescription TabataGo reads your heart rate and calories during workouts. NSHealthUpdateUsageDescription diff --git a/tabatago-swift/TabataGoWidget/Info.plist b/tabatago-swift/TabataGoWidget/Info.plist index fb382bf..b64cae1 100644 --- a/tabatago-swift/TabataGoWidget/Info.plist +++ b/tabatago-swift/TabataGoWidget/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 1 + 2 NSExtension NSExtensionPointIdentifier