mirror of
https://github.com/yattee/yattee.git
synced 2026-04-09 17:16:57 +00:00
Show seek time preview when no storyboards available
Display a floating time pill above the seek bar during dragging (iOS) and dragging/hovering (macOS) when video has no storyboard thumbnails. Includes chapter name when available.
This commit is contained in:
@@ -883,6 +883,8 @@ struct PlayerControlsView: View {
|
|||||||
.overlay {
|
.overlay {
|
||||||
if !playerState.isLive, let storyboard = playerState.preferredStoryboard {
|
if !playerState.isLive, let storyboard = playerState.preferredStoryboard {
|
||||||
seekPreviewOverlay(storyboard: storyboard, geometry: geometry)
|
seekPreviewOverlay(storyboard: storyboard, geometry: geometry)
|
||||||
|
} else if !playerState.isLive, isDragging {
|
||||||
|
seekTimePreviewOverlay(geometry: geometry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.gesture(
|
.gesture(
|
||||||
@@ -953,6 +955,25 @@ struct PlayerControlsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func seekTimePreviewOverlay(geometry: GeometryProxy) -> some View {
|
||||||
|
let seekTime = dragProgress * playerState.duration
|
||||||
|
let previewWidth: CGFloat = 80
|
||||||
|
let halfWidth = previewWidth / 2
|
||||||
|
let xPosition = max(halfWidth, min(geometry.size.width - halfWidth, geometry.size.width * dragProgress))
|
||||||
|
let yPosition: CGFloat = -20
|
||||||
|
|
||||||
|
SeekTimePreviewView(
|
||||||
|
seekTime: seekTime,
|
||||||
|
buttonBackground: activeLayout.globalSettings.buttonBackground,
|
||||||
|
theme: activeLayout.globalSettings.theme,
|
||||||
|
chapters: playerState.chapters
|
||||||
|
)
|
||||||
|
.position(x: xPosition, y: yPosition)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.9)))
|
||||||
|
.animation(.easeInOut(duration: 0.15), value: isDragging)
|
||||||
|
}
|
||||||
|
|
||||||
private var displayProgress: Double {
|
private var displayProgress: Double {
|
||||||
(isDragging || isPendingSeek) ? dragProgress : playerState.progress
|
(isDragging || isPendingSeek) ? dragProgress : playerState.progress
|
||||||
}
|
}
|
||||||
|
|||||||
61
Yattee/Views/Player/SeekTimePreviewView.swift
Normal file
61
Yattee/Views/Player/SeekTimePreviewView.swift
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// SeekTimePreviewView.swift
|
||||||
|
// Yattee
|
||||||
|
//
|
||||||
|
// Seek time preview shown when no storyboard thumbnails are available.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Lightweight seek time pill displayed above the seek bar when no storyboard is available.
|
||||||
|
struct SeekTimePreviewView: View {
|
||||||
|
let seekTime: TimeInterval
|
||||||
|
let buttonBackground: ButtonBackgroundStyle
|
||||||
|
let theme: ControlsTheme
|
||||||
|
let chapters: [VideoChapter]
|
||||||
|
|
||||||
|
private var currentChapter: VideoChapter? {
|
||||||
|
chapters.last { $0.startTime <= seekTime }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var formattedTime: String {
|
||||||
|
let totalSeconds = Int(seekTime)
|
||||||
|
let hours = totalSeconds / 3600
|
||||||
|
let minutes = (totalSeconds % 3600) / 60
|
||||||
|
let seconds = totalSeconds % 60
|
||||||
|
|
||||||
|
if hours > 0 {
|
||||||
|
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
return String(format: "%d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
if let chapter = currentChapter {
|
||||||
|
Text(chapter.title)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.shadow(color: .black.opacity(0.8), radius: 2, x: 0, y: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(formattedTime)
|
||||||
|
.font(.system(size: 16, weight: .medium))
|
||||||
|
.monospacedDigit()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.glassBackground(
|
||||||
|
buttonBackground.glassStyle ?? .regular,
|
||||||
|
in: .rect(cornerRadius: 8),
|
||||||
|
fallback: .ultraThinMaterial,
|
||||||
|
colorScheme: .dark
|
||||||
|
)
|
||||||
|
.shadow(radius: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,29 @@ struct MacOSControlBar: View {
|
|||||||
seekPreviewView(storyboard: storyboard)
|
seekPreviewView(storyboard: storyboard)
|
||||||
.offset(x: clampedX, y: -150)
|
.offset(x: clampedX, y: -150)
|
||||||
}
|
}
|
||||||
|
} else if (isDragging || isHoveringProgress),
|
||||||
|
!playerState.isLive {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
let previewProgress = isDragging ? dragProgress : hoverProgress
|
||||||
|
let horizontalPadding: CGFloat = 16
|
||||||
|
let timeLabelWidth: CGFloat = 50
|
||||||
|
let spacing: CGFloat = 8
|
||||||
|
let progressBarOffset: CGFloat = horizontalPadding + timeLabelWidth + spacing
|
||||||
|
let progressBarWidth: CGFloat = geometry.size.width - (2 * horizontalPadding) - (2 * timeLabelWidth) - (2 * spacing)
|
||||||
|
let previewWidth: CGFloat = 80
|
||||||
|
let xOffset = progressBarOffset + (progressBarWidth * previewProgress) - (previewWidth / 2)
|
||||||
|
let clampedX = max(0, min(geometry.size.width - previewWidth, xOffset))
|
||||||
|
|
||||||
|
SeekTimePreviewView(
|
||||||
|
seekTime: previewProgress * playerState.duration,
|
||||||
|
buttonBackground: .none,
|
||||||
|
theme: .dark,
|
||||||
|
chapters: showChapters ? playerState.chapters : []
|
||||||
|
)
|
||||||
|
.offset(x: clampedX, y: -60)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.9)))
|
||||||
|
.animation(.easeInOut(duration: 0.15), value: isDragging || isHoveringProgress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user