feat: redesign Dynamic Island with phase-driven UI and animations
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
import ActivityKit
|
||||
import Foundation
|
||||
import AppIntents
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
#endif
|
||||
|
||||
// MARK: — Shared notification names (used by LiveActivityIntent + PlayerView)
|
||||
|
||||
extension Notification.Name {
|
||||
static let togglePauseFromActivity = Notification.Name("togglePauseFromActivity")
|
||||
}
|
||||
|
||||
// MARK: — Phase enum
|
||||
|
||||
enum WorkoutPhase: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
case prep
|
||||
case warmup
|
||||
@@ -32,9 +41,49 @@ enum WorkoutPhase: String, Codable, Hashable, Sendable, CaseIterable {
|
||||
case .complete: return Color(red: 0.19, green: 0.82, blue: 0.35)
|
||||
}
|
||||
}
|
||||
|
||||
var dimColor: Color {
|
||||
color.opacity(0.3)
|
||||
}
|
||||
|
||||
var glowColor: Color {
|
||||
color.opacity(0.5)
|
||||
}
|
||||
|
||||
var gradientStops: [Gradient.Stop] {
|
||||
switch self {
|
||||
case .work:
|
||||
return [
|
||||
.init(color: Color(red: 1.0, green: 0.42, blue: 0.21).opacity(0.15), location: 0),
|
||||
.init(color: .clear, location: 1),
|
||||
]
|
||||
case .rest, .interBlockRest:
|
||||
return [
|
||||
.init(color: Color(red: 0.35, green: 0.78, blue: 0.98).opacity(0.12), location: 0),
|
||||
.init(color: .clear, location: 1),
|
||||
]
|
||||
case .prep, .warmup:
|
||||
return [
|
||||
.init(color: Color(red: 1.0, green: 0.58, blue: 0.0).opacity(0.12), location: 0),
|
||||
.init(color: .clear, location: 1),
|
||||
]
|
||||
case .cooldown:
|
||||
return [
|
||||
.init(color: Color(red: 0.35, green: 0.78, blue: 0.98).opacity(0.08), location: 0),
|
||||
.init(color: .clear, location: 1),
|
||||
]
|
||||
case .complete:
|
||||
return [
|
||||
.init(color: Color(red: 0.19, green: 0.82, blue: 0.35).opacity(0.15), location: 0),
|
||||
.init(color: .clear, location: 1),
|
||||
]
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: — Activity Attributes
|
||||
|
||||
struct WorkoutActivityAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable, Sendable {
|
||||
var exerciseName: String
|
||||
@@ -48,5 +97,47 @@ struct WorkoutActivityAttributes: ActivityAttributes {
|
||||
var trackArtist: String
|
||||
var isPlaying: Bool
|
||||
var isPaused: Bool
|
||||
var blockIndex: Int = 0
|
||||
var blockCount: Int = 0
|
||||
var exerciseShortName: String = ""
|
||||
var phaseElapsedSeconds: TimeInterval = 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Concurrency-safe throttle
|
||||
|
||||
private actor TogglePauseThrottle {
|
||||
private var lastFireTime: Date = .distantPast
|
||||
|
||||
func shouldFire() -> Bool {
|
||||
let now = Date()
|
||||
guard now.timeIntervalSince(lastFireTime) > 3.0 else { return false }
|
||||
lastFireTime = now
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Live Activity Intents (run in the main app process)
|
||||
|
||||
struct TogglePauseIntent: LiveActivityIntent {
|
||||
|
||||
static let title: LocalizedStringResource = "Toggle Pause"
|
||||
private static let throttle = TogglePauseThrottle()
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await Self.throttle.shouldFire() else {
|
||||
print("[LiveActivityIntent] TogglePauseIntent throttled")
|
||||
return .result()
|
||||
}
|
||||
|
||||
print("[LiveActivityIntent] TogglePauseIntent.perform() fired")
|
||||
|
||||
NotificationCenter.default.post(name: .togglePauseFromActivity, object: nil)
|
||||
|
||||
// Small sleep keeps widget button in a "processing" visual state
|
||||
try await Task.sleep(for: .milliseconds(400))
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user