mirror of
https://github.com/yattee/yattee.git
synced 2026-10-24 21:52:51 +00:00
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.
124 lines
4.5 KiB
Swift
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
|