Compare commits
6 Commits
feature/ci
...
807361449b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
807361449b | ||
|
|
ee01380be8 | ||
|
|
210c254298 | ||
|
|
1bb6a544e4 | ||
|
|
a17367081c | ||
|
|
40ffb04f31 |
@@ -28,7 +28,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>2</string>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>TabataGo reads your health data to show fitness stats and personalize your workouts.</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<array>
|
||||
<string>health-records</string>
|
||||
</array>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
|
||||
@@ -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 ─────────────────────────────────────────
|
||||
|
||||
@@ -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,14 @@ final class PlayerViewModel: ObservableObject {
|
||||
attributes: attrs,
|
||||
content: ActivityContent(state: state, staleDate: staleDate)
|
||||
)
|
||||
liveActivityError = nil
|
||||
observeActivityState()
|
||||
} catch let authError as ActivityAuthorizationError {
|
||||
liveActivityError = "Live Activities: \(authError.localizedDescription)"
|
||||
} catch {
|
||||
let msg = error.localizedDescription
|
||||
let truncated = msg.count > 200 ? String(msg.prefix(200)) + "…" : msg
|
||||
liveActivityError = "Live Activity error: \(truncated)"
|
||||
print("❌ Failed to start Live Activity: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// ══════════════════════════════════════════════════
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>2</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>2</string>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>TabataGo reads your heart rate and calories during workouts.</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>2</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -553,7 +553,7 @@ struct WorkoutLiveActivity: Widget {
|
||||
.contentMargins(.leading, 8, for: .expanded)
|
||||
.contentMargins(.trailing, 8, for: .expanded)
|
||||
.contentMargins(.bottom, 6, for: .expanded)
|
||||
.widgetURL(URL(string: "tabatago://workout")!)
|
||||
.widgetURL(URL(string: "tabatago://workout"))
|
||||
}
|
||||
.supplementalActivityFamilies([.small, .medium])
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ targets:
|
||||
|
||||
- target: TabataGoWatch
|
||||
embed: true
|
||||
- target: TabataGoWidget
|
||||
embed: true
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago
|
||||
@@ -156,6 +158,25 @@ targets:
|
||||
TARGETED_DEVICE_FAMILY: "4"
|
||||
WATCHOS_DEPLOYMENT_TARGET: "11.0"
|
||||
|
||||
TabataGoWidget:
|
||||
type: app-extension
|
||||
platform: iOS
|
||||
deploymentTarget: "26.0"
|
||||
sources:
|
||||
- path: TabataGoWidget
|
||||
excludes:
|
||||
- "**/.DS_Store"
|
||||
- path: TabataGo/Models/WorkoutActivityAttributes.swift
|
||||
- path: TabataGo/Models/MusicActivityAttributes.swift
|
||||
info:
|
||||
path: TabataGoWidget/Info.plist
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.widget
|
||||
IPHONEOS_DEPLOYMENT_TARGET: "26.0"
|
||||
TARGETED_DEVICE_FAMILY: "1"
|
||||
ENABLE_PREVIEWS: YES
|
||||
|
||||
TabataGoTests:
|
||||
type: bundle.unit-test
|
||||
platform: iOS
|
||||
@@ -185,6 +206,7 @@ schemes:
|
||||
build:
|
||||
targets:
|
||||
TabataGo: all
|
||||
TabataGoWidget: all
|
||||
run:
|
||||
config: Debug
|
||||
archive:
|
||||
|
||||
Reference in New Issue
Block a user