mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 18:35:05 +00:00
Match tvOS seek preview to iOS glass design
This commit is contained in:
@@ -213,21 +213,31 @@ struct TVPlayerProgressBar: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func scrubPreviewOverlay(geometry: GeometryProxy) -> some View {
|
private func scrubPreviewOverlay(geometry: GeometryProxy) -> some View {
|
||||||
if isScrubbing {
|
if isScrubbing {
|
||||||
let previewWidth: CGFloat = 352
|
let seekTime = scrubTime ?? currentTime
|
||||||
let previewHeight: CGFloat = 260
|
let currentChapter = showChapters ? chapters.last(where: { $0.startTime <= seekTime }) : nil
|
||||||
|
// Panel is 320 thumbnail + 4pt horizontal padding * 2 = 328, plus shadow.
|
||||||
|
// Use a slightly larger value for x-clamping to keep shadow inside screen.
|
||||||
|
let previewWidth: CGFloat = 344
|
||||||
|
// Panel height (thumbnail 180 + 6pt vertical padding * 2 = 192) plus
|
||||||
|
// optional chapter capsule (~36pt) and 8pt VStack spacing.
|
||||||
|
let previewHeight: CGFloat = currentChapter != nil ? 244 : 200
|
||||||
let halfWidth = previewWidth / 2
|
let halfWidth = previewWidth / 2
|
||||||
let clampedX = max(halfWidth, min(geometry.size.width - halfWidth, geometry.size.width * progress))
|
let clampedX = max(halfWidth, min(geometry.size.width - halfWidth, geometry.size.width * progress))
|
||||||
let yPosition = -previewHeight / 2 - 16
|
let yPosition = -previewHeight / 2 - 16
|
||||||
|
|
||||||
Group {
|
VStack(spacing: 8) {
|
||||||
|
if let currentChapter {
|
||||||
|
TVChapterCapsuleView(title: currentChapter.title)
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
}
|
||||||
|
|
||||||
if let storyboard {
|
if let storyboard {
|
||||||
TVSeekPreviewView(
|
TVSeekPreviewView(
|
||||||
storyboard: storyboard,
|
storyboard: storyboard,
|
||||||
seekTime: scrubTime ?? currentTime,
|
seekTime: seekTime
|
||||||
chapters: showChapters ? chapters : []
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text((scrubTime ?? currentTime).formattedAsTimestamp)
|
Text(seekTime.formattedAsTimestamp)
|
||||||
.font(.system(size: 48, weight: .medium))
|
.font(.system(size: 48, weight: .medium))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@@ -239,6 +249,7 @@ struct TVPlayerProgressBar: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.fixedSize()
|
||||||
.position(x: clampedX, y: yPosition)
|
.position(x: clampedX, y: yPosition)
|
||||||
.transition(.scale.combined(with: .opacity))
|
.transition(.scale.combined(with: .opacity))
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
|
|||||||
@@ -8,52 +8,55 @@
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Glass capsule showing the current chapter title above the tvOS seek preview.
|
||||||
|
struct TVChapterCapsuleView: View {
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 24, weight: .medium))
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.glassBackground(
|
||||||
|
.regular,
|
||||||
|
in: .capsule,
|
||||||
|
fallback: .ultraThinMaterial,
|
||||||
|
colorScheme: .dark
|
||||||
|
)
|
||||||
|
.shadow(radius: 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Preview thumbnail displayed during scrubbing on tvOS.
|
/// Preview thumbnail displayed during scrubbing on tvOS.
|
||||||
/// Scaled up for TV viewing distance.
|
/// Scaled up for TV viewing distance.
|
||||||
struct TVSeekPreviewView: View {
|
struct TVSeekPreviewView: View {
|
||||||
let storyboard: Storyboard
|
let storyboard: Storyboard
|
||||||
let seekTime: TimeInterval
|
let seekTime: TimeInterval
|
||||||
let chapters: [VideoChapter]
|
|
||||||
|
|
||||||
@State private var thumbnail: UIImage?
|
@State private var thumbnail: UIImage?
|
||||||
@State private var loadTask: Task<Void, Never>?
|
@State private var loadTask: Task<Void, Never>?
|
||||||
|
|
||||||
/// The current chapter based on seek time.
|
|
||||||
private var currentChapter: VideoChapter? {
|
|
||||||
chapters.last { $0.startTime <= seekTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
private let thumbnailWidth: CGFloat = 320
|
private let thumbnailWidth: CGFloat = 320
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
|
||||||
// Chapter name (only shown if chapters exist, larger for TV)
|
|
||||||
// Constrained to thumbnail width to prevent expanding the preview
|
|
||||||
if let chapter = currentChapter {
|
|
||||||
Text(chapter.title)
|
|
||||||
.font(.system(size: 28, weight: .medium))
|
|
||||||
.lineLimit(2)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.shadow(color: .black.opacity(0.8), radius: 3, x: 0, y: 1)
|
|
||||||
.frame(maxWidth: thumbnailWidth)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumbnail with timestamp overlay (scaled up for TV)
|
// Thumbnail with timestamp overlay (scaled up for TV)
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
Group {
|
Group {
|
||||||
if let thumbnail {
|
if let thumbnail {
|
||||||
Image(uiImage: thumbnail)
|
Image(uiImage: thumbnail)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fill)
|
||||||
} else {
|
} else {
|
||||||
// Placeholder while loading
|
// Placeholder while loading
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.gray.opacity(0.3))
|
.fill(Color.gray.opacity(0.3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(width: thumbnailWidth, height: 180)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
// Timestamp overlaid at bottom center (larger for TV)
|
// Timestamp overlaid at bottom center (larger for TV)
|
||||||
Text(seekTime.formattedAsTimestamp)
|
Text(seekTime.formattedAsTimestamp)
|
||||||
@@ -66,12 +69,15 @@ struct TVSeekPreviewView: View {
|
|||||||
.clipShape(.rect(cornerRadius: 6))
|
.clipShape(.rect(cornerRadius: 6))
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
}
|
}
|
||||||
.frame(width: thumbnailWidth, height: 180)
|
.clipShape(.rect(cornerRadius: 4))
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.padding(4)
|
||||||
}
|
.glassBackground(
|
||||||
.padding(16)
|
.regular,
|
||||||
.background(.ultraThinMaterial)
|
in: .rect(cornerRadius: 8),
|
||||||
.clipShape(.rect(cornerRadius: 16))
|
fallback: .ultraThinMaterial,
|
||||||
|
colorScheme: .dark
|
||||||
|
)
|
||||||
|
.shadow(radius: 4)
|
||||||
.onChange(of: seekTime) { _, newTime in
|
.onChange(of: seekTime) { _, newTime in
|
||||||
loadThumbnail(for: newTime)
|
loadThumbnail(for: newTime)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user