mirror of
https://github.com/yattee/yattee.git
synced 2026-04-09 17:16:57 +00:00
Extract shared TimeInterval.formattedAsTimestamp replacing 8 identical formatTime/formattedTime implementations across player views. Remove unused currentTime parameter from GestureSeekPreviewView. Consolidate duplicated geometry math in MacOSControlBar into seekPreviewPosition().
145 lines
4.7 KiB
Swift
145 lines
4.7 KiB
Swift
//
|
|
// SeekPreviewView.swift
|
|
// Yattee
|
|
//
|
|
// Preview thumbnail shown above the seek bar during scrubbing.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// Glass capsule showing the current chapter title during seeking.
|
|
struct ChapterCapsuleView: View {
|
|
let title: String
|
|
let buttonBackground: ButtonBackgroundStyle
|
|
|
|
var body: some View {
|
|
Text(title)
|
|
.font(.caption)
|
|
.fontWeight(.medium)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 4)
|
|
.glassBackground(
|
|
buttonBackground.glassStyle ?? .regular,
|
|
in: .capsule,
|
|
fallback: .ultraThinMaterial,
|
|
colorScheme: .dark
|
|
)
|
|
.shadow(radius: 4)
|
|
}
|
|
|
|
/// Returns this capsule positioned so its center follows `xTarget`,
|
|
/// clamped to stay within `margin` of each edge of `availableWidth`.
|
|
func positioned(xTarget: CGFloat, availableWidth: CGFloat, margin: CGFloat = 8) -> some View {
|
|
self
|
|
.alignmentGuide(.leading) { d in
|
|
let targetLeading = xTarget - d.width / 2
|
|
let clampedLeading = max(margin, min(availableWidth - d.width - margin, targetLeading))
|
|
return -clampedLeading
|
|
}
|
|
.frame(width: availableWidth, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
/// Preview thumbnail displayed above the seek bar during scrubbing/hovering.
|
|
|
|
struct SeekPreviewView: View {
|
|
let storyboard: Storyboard
|
|
let seekTime: TimeInterval
|
|
let storyboardService: StoryboardService
|
|
let buttonBackground: ButtonBackgroundStyle
|
|
let theme: ControlsTheme
|
|
|
|
@State private var thumbnail: PlatformImage?
|
|
@State private var loadTask: Task<Void, Never>?
|
|
|
|
private let thumbnailWidth: CGFloat = 160
|
|
|
|
var body: some View {
|
|
// Thumbnail with timestamp overlay
|
|
VStack(spacing: 4) {
|
|
ZStack(alignment: .bottom) {
|
|
Group {
|
|
if let thumbnail {
|
|
#if os(macOS)
|
|
Image(nsImage: thumbnail)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
#else
|
|
Image(uiImage: thumbnail)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
#endif
|
|
} else {
|
|
// Placeholder while loading
|
|
Rectangle()
|
|
.fill(Color.gray.opacity(0.3))
|
|
}
|
|
}
|
|
.frame(width: thumbnailWidth, height: 90)
|
|
.clipped()
|
|
|
|
// Timestamp overlaid at bottom center
|
|
Text(seekTime.formattedAsTimestamp)
|
|
.font(.caption)
|
|
.fontWeight(.medium)
|
|
.monospacedDigit()
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(.black.opacity(0.7))
|
|
.clipShape(.rect(cornerRadius: 4))
|
|
.padding(.bottom, 4)
|
|
}
|
|
.clipShape(.rect(cornerRadius: 4))
|
|
}
|
|
.padding(.vertical, 6)
|
|
.padding(.horizontal, 4)
|
|
.glassBackground(
|
|
buttonBackground.glassStyle ?? .regular,
|
|
in: .rect(cornerRadius: 8),
|
|
fallback: .ultraThinMaterial,
|
|
colorScheme: .dark
|
|
)
|
|
.shadow(radius: 4)
|
|
.onChange(of: seekTime) { _, newTime in
|
|
loadThumbnail(for: newTime)
|
|
}
|
|
.onAppear {
|
|
loadThumbnail(for: seekTime)
|
|
}
|
|
.onDisappear {
|
|
loadTask?.cancel()
|
|
}
|
|
}
|
|
|
|
private func loadThumbnail(for time: TimeInterval) {
|
|
loadTask?.cancel()
|
|
|
|
loadTask = Task {
|
|
// First try to get cached thumbnail
|
|
if let cached = await storyboardService.thumbnail(for: time, from: storyboard) {
|
|
await MainActor.run {
|
|
self.thumbnail = cached
|
|
}
|
|
return
|
|
}
|
|
|
|
// Load the sheet and nearby sheets
|
|
await storyboardService.preloadNearbySheets(around: time, from: storyboard)
|
|
|
|
// Check for cancellation
|
|
guard !Task.isCancelled else { return }
|
|
|
|
// Try again after loading
|
|
if let loaded = await storyboardService.thumbnail(for: time, from: storyboard) {
|
|
await MainActor.run {
|
|
self.thumbnail = loaded
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|