diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index 9b99c84e..9c7b5c94 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -883,6 +883,8 @@ struct PlayerControlsView: View { .overlay { if !playerState.isLive, let storyboard = playerState.preferredStoryboard { seekPreviewOverlay(storyboard: storyboard, geometry: geometry) + } else if !playerState.isLive, isDragging { + seekTimePreviewOverlay(geometry: geometry) } } .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 { (isDragging || isPendingSeek) ? dragProgress : playerState.progress } diff --git a/Yattee/Views/Player/SeekTimePreviewView.swift b/Yattee/Views/Player/SeekTimePreviewView.swift new file mode 100644 index 00000000..41274ce4 --- /dev/null +++ b/Yattee/Views/Player/SeekTimePreviewView.swift @@ -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) + } +} diff --git a/Yattee/Views/Player/macOS/MacOSControlBar.swift b/Yattee/Views/Player/macOS/MacOSControlBar.swift index 69e56ea9..ccd83bf7 100644 --- a/Yattee/Views/Player/macOS/MacOSControlBar.swift +++ b/Yattee/Views/Player/macOS/MacOSControlBar.swift @@ -114,6 +114,29 @@ struct MacOSControlBar: View { seekPreviewView(storyboard: storyboard) .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) + } } } }