mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55:03 +00:00
Show storyboard preview during tvOS scrubber arrow-seek
Arrow-seek on the focused scrubber previously moved the handle but showed no storyboard/chapter context. Reuse the existing SELECT-scrub overlay for arrow-seek too, with a ~2s lingering fade after the last press so the preview doesn't vanish the instant the seek commits.
This commit is contained in:
@@ -59,6 +59,11 @@ struct TVPlayerProgressBar: View {
|
|||||||
@State private var lastDPadTime: Date?
|
@State private var lastDPadTime: Date?
|
||||||
@State private var lastDPadDirection: MoveCommandDirection?
|
@State private var lastDPadDirection: MoveCommandDirection?
|
||||||
|
|
||||||
|
/// Whether the arrow-seek storyboard/chapter preview is currently shown.
|
||||||
|
@State private var showArrowSeekPreview = false
|
||||||
|
/// Delayed-hide task for the arrow-seek preview after the user stops pressing arrows.
|
||||||
|
@State private var arrowSeekPreviewHideTask: Task<Void, Never>?
|
||||||
|
|
||||||
/// The time to display. SELECT-based scrub takes priority, then the
|
/// The time to display. SELECT-based scrub takes priority, then the
|
||||||
/// parent's pending remote-seek target, then the actual playback time.
|
/// parent's pending remote-seek target, then the actual playback time.
|
||||||
private var displayTime: TimeInterval {
|
private var displayTime: TimeInterval {
|
||||||
@@ -124,6 +129,30 @@ struct TVPlayerProgressBar: View {
|
|||||||
guard newValue != nil, isScrubbing else { return }
|
guard newValue != nil, isScrubbing else { return }
|
||||||
cancelScrub()
|
cancelScrub()
|
||||||
}
|
}
|
||||||
|
.onChange(of: remoteSeekTime) { _, newValue in
|
||||||
|
if newValue != nil {
|
||||||
|
arrowSeekPreviewHideTask?.cancel()
|
||||||
|
arrowSeekPreviewHideTask = nil
|
||||||
|
if !showArrowSeekPreview {
|
||||||
|
withAnimation(.easeOut(duration: 0.15)) {
|
||||||
|
showArrowSeekPreview = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scheduleArrowSeekPreviewHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: isScrubbing) { _, scrubbing in
|
||||||
|
if scrubbing {
|
||||||
|
arrowSeekPreviewHideTask?.cancel()
|
||||||
|
arrowSeekPreviewHideTask = nil
|
||||||
|
if showArrowSeekPreview {
|
||||||
|
withAnimation(.easeOut(duration: 0.15)) {
|
||||||
|
showArrowSeekPreview = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.animation(.easeInOut(duration: 0.2), value: isFocused)
|
.animation(.easeInOut(duration: 0.2), value: isFocused)
|
||||||
.animation(.easeInOut(duration: 0.1), value: isScrubbing)
|
.animation(.easeInOut(duration: 0.1), value: isScrubbing)
|
||||||
}
|
}
|
||||||
@@ -212,8 +241,8 @@ struct TVPlayerProgressBar: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func scrubPreviewOverlay(geometry: GeometryProxy) -> some View {
|
private func scrubPreviewOverlay(geometry: GeometryProxy) -> some View {
|
||||||
if isScrubbing {
|
if isScrubbing || showArrowSeekPreview {
|
||||||
let seekTime = scrubTime ?? currentTime
|
let seekTime = displayTime
|
||||||
let currentChapter = showChapters ? chapters.last(where: { $0.startTime <= seekTime }) : nil
|
let currentChapter = showChapters ? chapters.last(where: { $0.startTime <= seekTime }) : nil
|
||||||
// Storyboard panel is 320 thumbnail + 4pt horizontal padding * 2 = 328, plus shadow.
|
// Storyboard panel is 320 thumbnail + 4pt horizontal padding * 2 = 328, plus shadow.
|
||||||
// Use a slightly larger clamp width so the shadow stays on screen.
|
// Use a slightly larger clamp width so the shadow stays on screen.
|
||||||
@@ -395,11 +424,14 @@ struct TVPlayerProgressBar: View {
|
|||||||
withAnimation(.easeOut(duration: 0.15)) {
|
withAnimation(.easeOut(duration: 0.15)) {
|
||||||
scrubTime = nil
|
scrubTime = nil
|
||||||
isScrubbing = false
|
isScrubbing = false
|
||||||
|
showArrowSeekPreview = false
|
||||||
}
|
}
|
||||||
panAccumulator = 0
|
panAccumulator = 0
|
||||||
dpadStreakCount = 0
|
dpadStreakCount = 0
|
||||||
lastDPadTime = nil
|
lastDPadTime = nil
|
||||||
lastDPadDirection = nil
|
lastDPadDirection = nil
|
||||||
|
arrowSeekPreviewHideTask?.cancel()
|
||||||
|
arrowSeekPreviewHideTask = nil
|
||||||
|
|
||||||
if wasScrubbing {
|
if wasScrubbing {
|
||||||
onScrubbingChanged?(false)
|
onScrubbingChanged?(false)
|
||||||
@@ -415,17 +447,31 @@ struct TVPlayerProgressBar: View {
|
|||||||
withAnimation(.easeOut(duration: 0.15)) {
|
withAnimation(.easeOut(duration: 0.15)) {
|
||||||
scrubTime = nil
|
scrubTime = nil
|
||||||
isScrubbing = false
|
isScrubbing = false
|
||||||
|
showArrowSeekPreview = false
|
||||||
}
|
}
|
||||||
panAccumulator = 0
|
panAccumulator = 0
|
||||||
dpadStreakCount = 0
|
dpadStreakCount = 0
|
||||||
lastDPadTime = nil
|
lastDPadTime = nil
|
||||||
lastDPadDirection = nil
|
lastDPadDirection = nil
|
||||||
|
arrowSeekPreviewHideTask?.cancel()
|
||||||
|
arrowSeekPreviewHideTask = nil
|
||||||
|
|
||||||
if wasScrubbing {
|
if wasScrubbing {
|
||||||
onScrubbingChanged?(false)
|
onScrubbingChanged?(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func scheduleArrowSeekPreviewHide() {
|
||||||
|
arrowSeekPreviewHideTask?.cancel()
|
||||||
|
arrowSeekPreviewHideTask = Task { @MainActor in
|
||||||
|
try? await Task.sleep(for: .milliseconds(2000))
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
withAnimation(.easeOut(duration: 0.2)) {
|
||||||
|
showArrowSeekPreview = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Pan Gesture View
|
// MARK: - Pan Gesture View
|
||||||
|
|||||||
Reference in New Issue
Block a user