Files
yattee/Yattee/Views/Player/Gestures/PlayerGestureOverlay.swift
Arkadiusz Fal b1625afb4a Collapse the corner side column by overdrag and fix the fast reveal glitch
Overdragging the side column's resize grabber past its minimum width now
slides the column off the trailing edge (the video grows with it), and a
swipe in from the trailing screen edge brings it back following the
finger. The seek pan skips that edge strip while the column is
collapsed. Showing the panel again restores every region hidden by its
own drag handle.

Swiping the below-video panel back up right after hiding it showed the
video clipped to a short centered box that sprang back to full size.
The hide flipped to the hidden layout in a `.logicallyComplete`
completion while the slide spring was still registered on the SwiftUI
host of the mpv render view; a reveal drag interrupting it made the
release spring start from a bogus shrunken rect. All slide-off
completions now wait for `.removed` and bail if a new drag took over.
The visibility-keyed `.animation` modifiers key on a toggle generation
bumped only by the animated show/hide paths, so drag flips never start
the toggle easing. The partnerless `matchedGeometryEffect` on the
widescreen video frame is dropped.
2026-09-24 22:21:47 +02:00

124 lines
4.5 KiB
Swift

//
// PlayerGestureOverlay.swift
// Yattee
//
// SwiftUI overlay for handling player gestures.
//
#if os(iOS)
import SwiftUI
import UIKit
/// Overlay view that handles tap and seek gestures on the player.
struct PlayerGestureOverlay: View {
let settings: GesturesSettings
let isActive: Bool
let isSeekable: Bool
let onTapAction: (TapGestureAction, TapZonePosition) -> Void
let onSingleTap: () -> Void
let onSeekGestureStarted: () -> Void
let onSeekGestureChanged: (CGFloat) -> Void
let onSeekGestureEnded: (CGFloat) -> Void
/// Returns true if a pinch gesture is currently active (blocks seek gesture).
var isPinchGestureActive: (() -> Bool)? = nil
/// Returns true if panel drag is active (blocks seek gesture).
var isPanelDragging: (() -> Bool)? = nil
/// Width of the trailing-edge strip reserved for a panel reveal swipe.
var trailingEdgeRevealInset: (() -> CGFloat)? = nil
var body: some View {
GeometryReader { geometry in
GestureRecognizerView(
settings: settings,
bounds: CGRect(origin: .zero, size: geometry.size),
isActive: isActive,
isSeekable: isSeekable,
onDoubleTap: { position in
if let config = settings.tapGestures.configuration(for: position) {
onTapAction(config.action, position)
}
},
onSingleTap: onSingleTap,
onSeekGestureStarted: onSeekGestureStarted,
onSeekGestureChanged: onSeekGestureChanged,
onSeekGestureEnded: onSeekGestureEnded,
isPinchGestureActive: isPinchGestureActive,
isPanelDragging: isPanelDragging,
trailingEdgeRevealInset: trailingEdgeRevealInset
)
}
// Always allow hit testing - single tap to toggle controls should work
// regardless of whether controls are visible. The coordinator handles
// disabling double-tap gestures when isActive is false.
.allowsHitTesting(true)
}
}
// MARK: - UIViewRepresentable
private struct GestureRecognizerView: UIViewRepresentable {
let settings: GesturesSettings
let bounds: CGRect
let isActive: Bool
let isSeekable: Bool
let onDoubleTap: (TapZonePosition) -> Void
let onSingleTap: () -> Void
let onSeekGestureStarted: () -> Void
let onSeekGestureChanged: (CGFloat) -> Void
let onSeekGestureEnded: (CGFloat) -> Void
var isPinchGestureActive: (() -> Bool)? = nil
var isPanelDragging: (() -> Bool)? = nil
var trailingEdgeRevealInset: (() -> CGFloat)? = nil
func makeCoordinator() -> PlayerGestureCoordinator {
PlayerGestureCoordinator(
tapSettings: settings.tapGestures,
seekSettings: settings.seekGesture
)
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
let coordinator = context.coordinator
coordinator.isActive = isActive
coordinator.isSeekable = isSeekable
coordinator.onDoubleTap = onDoubleTap
coordinator.onSingleTap = onSingleTap
coordinator.onSeekGestureStarted = onSeekGestureStarted
coordinator.onSeekGestureChanged = onSeekGestureChanged
coordinator.onSeekGestureEnded = onSeekGestureEnded
coordinator.isPinchGestureActive = isPinchGestureActive
coordinator.isPanelDragging = isPanelDragging
coordinator.trailingEdgeRevealInset = trailingEdgeRevealInset
coordinator.attach(to: view)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
let coordinator = context.coordinator
coordinator.tapSettings = settings.tapGestures
coordinator.seekSettings = settings.seekGesture
coordinator.bounds = bounds
coordinator.isActive = isActive
coordinator.isSeekable = isSeekable
// Update callbacks
coordinator.onDoubleTap = onDoubleTap
coordinator.onSingleTap = onSingleTap
coordinator.onSeekGestureStarted = onSeekGestureStarted
coordinator.onSeekGestureChanged = onSeekGestureChanged
coordinator.onSeekGestureEnded = onSeekGestureEnded
coordinator.isPinchGestureActive = isPinchGestureActive
coordinator.isPanelDragging = isPanelDragging
coordinator.trailingEdgeRevealInset = trailingEdgeRevealInset
}
static func dismantleUIView(_ uiView: UIView, coordinator: PlayerGestureCoordinator) {
coordinator.detach()
}
}
#endif