Add Gesture controls for media skip and forward

This commit is contained in:
Hariharan Mudaliar 2025-12-24 14:50:36 +05:30
commit d70569e32f
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
9 changed files with 267 additions and 21 deletions

View file

@ -8,9 +8,14 @@ struct MusicControlOverlay: View {
@ObservedObject private var musicManager = MusicManager.shared
@Default(.musicSkipBehavior) private var musicSkipBehavior
private let seekInterval: TimeInterval = 10
private let seekInterval: TimeInterval = MusicManager.skipGestureSeekInterval
private let skipPressMagnitude: CGFloat = 8
private var trackBackwardPressEffect: FloatingMediaButton.PressEffect { .nudge(-skipPressMagnitude) }
private var trackForwardPressEffect: FloatingMediaButton.PressEffect { .nudge(skipPressMagnitude) }
private var tenSecondBackwardPressEffect: FloatingMediaButton.PressEffect { .wiggle(.counterClockwise) }
private var tenSecondForwardPressEffect: FloatingMediaButton.PressEffect { .wiggle(.clockwise) }
private var controlsEnabled: Bool {
!musicManager.isPlayerIdle || musicManager.bundleIdentifier != nil
}
@ -39,14 +44,14 @@ struct MusicControlOverlay: View {
case .track:
return ButtonConfig(
icon: "backward.fill",
pressEffect: .nudge(-skipPressMagnitude),
pressEffect: trackBackwardPressEffect,
symbolEffect: .replace,
action: { MusicManager.shared.previousTrack() }
)
case .tenSecond:
return ButtonConfig(
icon: "gobackward.10",
pressEffect: .wiggle(.counterClockwise),
pressEffect: tenSecondBackwardPressEffect,
symbolEffect: .wiggle,
action: { MusicManager.shared.seek(by: -seekInterval) }
)
@ -58,14 +63,14 @@ struct MusicControlOverlay: View {
case .track:
return ButtonConfig(
icon: "forward.fill",
pressEffect: .nudge(skipPressMagnitude),
pressEffect: trackForwardPressEffect,
symbolEffect: .replace,
action: { MusicManager.shared.nextTrack() }
)
case .tenSecond:
return ButtonConfig(
icon: "goforward.10",
pressEffect: .wiggle(.clockwise),
pressEffect: tenSecondForwardPressEffect,
symbolEffect: .wiggle,
action: { MusicManager.shared.seek(by: seekInterval) }
)
@ -84,6 +89,9 @@ struct MusicControlOverlay: View {
var body: some View {
let verticalPadding = max(8, notchHeight * 0.12)
let backwardGestureTrigger = skipGestureTrigger(for: .backward)
let forwardGestureTrigger = skipGestureTrigger(for: .forward)
HStack(spacing: 18) {
FloatingMediaButton(
icon: backwardConfig.icon,
@ -92,6 +100,8 @@ struct MusicControlOverlay: View {
foregroundColor: .white.opacity(controlsEnabled ? 0.9 : 0.35),
pressEffect: backwardConfig.pressEffect,
symbolEffectStyle: backwardConfig.symbolEffect,
externalTriggerToken: backwardGestureTrigger?.token,
externalTriggerEffect: backwardGestureTrigger?.pressEffect,
isEnabled: controlsEnabled,
action: backwardConfig.action
)
@ -103,6 +113,8 @@ struct MusicControlOverlay: View {
foregroundColor: .white,
pressEffect: playPauseConfig.pressEffect,
symbolEffectStyle: playPauseConfig.symbolEffect,
externalTriggerToken: nil,
externalTriggerEffect: nil,
isEnabled: controlsEnabled,
action: playPauseConfig.action
)
@ -114,6 +126,8 @@ struct MusicControlOverlay: View {
foregroundColor: .white.opacity(controlsEnabled ? 0.9 : 0.35),
pressEffect: forwardConfig.pressEffect,
symbolEffectStyle: forwardConfig.symbolEffect,
externalTriggerToken: forwardGestureTrigger?.token,
externalTriggerEffect: forwardGestureTrigger?.pressEffect,
isEnabled: controlsEnabled,
action: forwardConfig.action
)
@ -147,6 +161,8 @@ private struct FloatingMediaButton: View {
let foregroundColor: Color
let pressEffect: PressEffect
let symbolEffectStyle: SymbolEffectStyle
let externalTriggerToken: Int?
let externalTriggerEffect: PressEffect?
let isEnabled: Bool
let action: () -> Void
@ -154,6 +170,7 @@ private struct FloatingMediaButton: View {
@State private var pressOffset: CGFloat = 0
@State private var rotationAngle: Double = 0
@State private var wiggleToken: Int = 0
@State private var lastExternalTriggerToken: Int?
var body: some View {
Button {
@ -178,6 +195,12 @@ private struct FloatingMediaButton: View {
isHovering = hovering
}
}
.onChange(of: externalTriggerToken) { _, newToken in
guard isEnabled, let newToken else { return }
guard newToken != lastExternalTriggerToken else { return }
lastExternalTriggerToken = newToken
triggerPressEffect(override: externalTriggerEffect)
}
}
private var iconView: some View {
@ -219,8 +242,10 @@ private struct FloatingMediaButton: View {
}
}
private func triggerPressEffect() {
switch pressEffect {
private func triggerPressEffect(override: PressEffect? = nil) {
let activeEffect = override ?? pressEffect
switch activeEffect {
case .none:
return
case .nudge(let amount):
@ -269,6 +294,28 @@ private struct FloatingMediaButton: View {
}
}
private extension MusicControlOverlay {
func skipGestureTrigger(for direction: MusicManager.SkipDirection) -> (token: Int, pressEffect: FloatingMediaButton.PressEffect)? {
guard let pulse = musicManager.skipGesturePulse, pulse.direction == direction else {
return nil
}
let effect: FloatingMediaButton.PressEffect
switch (pulse.behavior, direction) {
case (.track, .backward):
effect = trackBackwardPressEffect
case (.track, .forward):
effect = trackForwardPressEffect
case (.tenSecond, .backward):
effect = tenSecondBackwardPressEffect
case (.tenSecond, .forward):
effect = tenSecondForwardPressEffect
}
return (token: pulse.token, pressEffect: effect)
}
}
#Preview {
MusicControlOverlay(notchHeight: 34, cornerRadius: 14)
.padding()