mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
189
Yattee/Views/Components/VideoRowView.swift
Normal file
189
Yattee/Views/Components/VideoRowView.swift
Normal file
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// VideoRowView.swift
|
||||
// Yattee
|
||||
//
|
||||
// A video row for list layouts.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A video row for list layouts.
|
||||
///
|
||||
/// Automatically handles DeArrow integration for titles and thumbnails.
|
||||
/// Download status is automatically shown from the download manager.
|
||||
/// On iOS/macOS, supports configurable tap zones for thumbnail and text area.
|
||||
struct VideoRowView: View {
|
||||
let video: Video
|
||||
var style: VideoRowStyle = .regular
|
||||
var watchProgress: Double? = nil
|
||||
var showSourceBadge: Bool = false
|
||||
/// Custom metadata text to show instead of views/date (e.g., remaining time).
|
||||
var customMetadata: String? = nil
|
||||
/// Optional index number to display before thumbnail (for playlists).
|
||||
var index: Int? = nil
|
||||
/// When true, disables internal tap handling so parent view can handle all taps.
|
||||
var disableInternalTapHandling: Bool = false
|
||||
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@Environment(\.videoQueueContext) private var videoQueueContext
|
||||
|
||||
// Platform-specific fonts
|
||||
#if os(tvOS)
|
||||
private var titleFont: Font {
|
||||
style == .compact ? .caption : .body
|
||||
}
|
||||
private var authorFont: Font {
|
||||
style == .compact ? .caption2 : .subheadline
|
||||
}
|
||||
private var metadataFont: Font {
|
||||
style == .compact ? .caption2 : .caption
|
||||
}
|
||||
#else
|
||||
private var titleFont: Font {
|
||||
.subheadline
|
||||
}
|
||||
private var authorFont: Font {
|
||||
style == .compact ? .caption2 : .caption
|
||||
}
|
||||
private var metadataFont: Font {
|
||||
style == .compact ? .caption2 : .caption2
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
private var thumbnailTapAction: VideoTapAction {
|
||||
appEnvironment?.settingsManager.thumbnailTapAction ?? .playVideo
|
||||
}
|
||||
|
||||
private var textAreaTapAction: VideoTapAction {
|
||||
appEnvironment?.settingsManager.textAreaTapAction ?? .openInfo
|
||||
}
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
// Index number (for playlists)
|
||||
if let index {
|
||||
Text("\(index)")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 32)
|
||||
}
|
||||
|
||||
// Thumbnail
|
||||
thumbnailView
|
||||
.zoomTransitionSource(id: video.id)
|
||||
|
||||
// Info
|
||||
infoView
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityIdentifier("video.row.\(video.id.videoID)")
|
||||
.accessibilityLabel("video.row.\(video.id.videoID)")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var thumbnailView: some View {
|
||||
DeArrowVideoThumbnail(
|
||||
video: video,
|
||||
cornerRadius: style == .compact ? 4 : style == .regular ? 6 : 8,
|
||||
watchProgress: watchProgress,
|
||||
duration: style == .compact ? nil : video.formattedDuration
|
||||
)
|
||||
.frame(width: style.thumbnailWidth, height: style.thumbnailHeight)
|
||||
#if !os(tvOS)
|
||||
.contentShape(Rectangle())
|
||||
.if(thumbnailTapAction != .playVideo && !disableInternalTapHandling) { view in
|
||||
view.highPriorityGesture(
|
||||
TapGesture().onEnded {
|
||||
handleTapAction(thumbnailTapAction)
|
||||
}
|
||||
)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var infoView: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(video.displayTitle(using: appEnvironment?.deArrowBrandingProvider))
|
||||
.font(titleFont)
|
||||
.lineLimit(style == .large ? 3 : style == .regular ? 2 : 1)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
if style == .compact {
|
||||
// In compact mode, show author and duration inline with dot separator
|
||||
let duration = video.formattedDuration
|
||||
if !duration.isEmpty {
|
||||
Text("\(video.author.name) · \(duration)")
|
||||
.font(authorFont)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text(video.author.name)
|
||||
.font(authorFont)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
Text(video.author.name)
|
||||
.font(authorFont)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if showSourceBadge {
|
||||
SourceBadge(source: video.id.source)
|
||||
}
|
||||
}
|
||||
|
||||
if style != .compact {
|
||||
if let custom = customMetadata {
|
||||
Text(custom)
|
||||
.font(metadataFont)
|
||||
.foregroundStyle(.tertiary)
|
||||
} else if video.viewCount != nil || video.formattedPublishedDate != nil {
|
||||
VideoMetadataLine(viewCount: video.formattedViewCount, publishedText: video.formattedPublishedDate)
|
||||
.font(metadataFont)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#if !os(tvOS)
|
||||
.contentShape(Rectangle())
|
||||
.if(textAreaTapAction != .playVideo && !disableInternalTapHandling) { view in
|
||||
view.highPriorityGesture(
|
||||
TapGesture().onEnded {
|
||||
handleTapAction(textAreaTapAction)
|
||||
}
|
||||
)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
private func handleTapAction(_ action: VideoTapAction) {
|
||||
guard let env = appEnvironment else { return }
|
||||
|
||||
switch action {
|
||||
case .playVideo:
|
||||
// This should not be called since we only add gesture for non-playVideo actions
|
||||
// The .tappableVideo() modifier will handle playVideo actions
|
||||
break
|
||||
case .openInfo:
|
||||
env.navigationCoordinator.navigate(to: .video(.loaded(video), queueContext: videoQueueContext))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Row") {
|
||||
VideoRowView(
|
||||
video: .preview,
|
||||
watchProgress: 0.6,
|
||||
showSourceBadge: true
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
Reference in New Issue
Block a user