mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
Yattee v2 rewrite
This commit is contained in:
193
Yattee/Views/Player/PlayerHelperViews.swift
Normal file
193
Yattee/Views/Player/PlayerHelperViews.swift
Normal file
@@ -0,0 +1,193 @@
|
||||
//
|
||||
// PlayerHelperViews.swift
|
||||
// Yattee
|
||||
//
|
||||
// Helper views and types for the player sheet.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS) || os(macOS) || os(tvOS)
|
||||
|
||||
// MARK: - Playback Info
|
||||
|
||||
/// Holds computed playback state flags to avoid duplicating these checks
|
||||
struct PlaybackInfo {
|
||||
let state: PlaybackState
|
||||
let isLoading: Bool
|
||||
let isIdle: Bool
|
||||
let isEnded: Bool
|
||||
let isFailed: Bool
|
||||
let hasBackend: Bool
|
||||
}
|
||||
|
||||
// MARK: - Compact Label
|
||||
|
||||
/// A compact label with an icon and text.
|
||||
struct CompactLabel: View {
|
||||
let text: String
|
||||
let systemImage: String
|
||||
var spacing: CGFloat = 4
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: spacing) {
|
||||
Image(systemName: systemImage)
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading Overlay View
|
||||
|
||||
/// An overlay view shown while video is loading.
|
||||
struct LoadingOverlayView: View {
|
||||
/// Buffer progress percentage (0-100), nil shows indeterminate spinner.
|
||||
var bufferProgress: Int?
|
||||
|
||||
var body: some View {
|
||||
Color.black.opacity(0.4)
|
||||
VStack(spacing: 12) {
|
||||
if let progress = bufferProgress, progress < 100 {
|
||||
// Circular progress indicator showing buffer percentage
|
||||
CircularBufferProgress(progress: progress)
|
||||
} else {
|
||||
// Indeterminate spinner
|
||||
ProgressView()
|
||||
.tint(.white)
|
||||
.controlSize(.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Circular progress view showing buffer percentage.
|
||||
struct CircularBufferProgress: View {
|
||||
let progress: Int
|
||||
|
||||
private var progressValue: Double {
|
||||
Double(progress) / 100.0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background circle
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.3), lineWidth: 4)
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
// Progress arc
|
||||
Circle()
|
||||
.trim(from: 0, to: progressValue)
|
||||
.stroke(Color.white, style: StrokeStyle(lineWidth: 4, lineCap: .round))
|
||||
.frame(width: 44, height: 44)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeInOut(duration: 0.2), value: progressValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Details Sheet
|
||||
|
||||
/// Sheet displaying error details with copy/share options.
|
||||
struct ErrorDetailsSheet: View {
|
||||
let errorMessage: String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Error message
|
||||
Text(errorMessage)
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(String(localized: "player.error.details.title"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(role: .cancel) {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
// Copy button
|
||||
Button {
|
||||
copyToClipboard()
|
||||
} label: {
|
||||
Label(String(localized: "player.error.copy"), systemImage: "doc.on.doc")
|
||||
}
|
||||
.accessibilityLabel(String(localized: "player.error.copy.accessibilityLabel"))
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
// Share button (not available on tvOS)
|
||||
ShareLink(item: errorMessage) {
|
||||
Label(String(localized: "player.error.share"), systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.accessibilityLabel(String(localized: "player.error.share.accessibilityLabel"))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.presentationDetents([.medium])
|
||||
#endif
|
||||
}
|
||||
|
||||
private func copyToClipboard() {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.string = errorMessage
|
||||
#elseif os(macOS)
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(errorMessage, forType: .string)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Player Overlay Button
|
||||
|
||||
/// A circular glass button used for player overlays (play, replay, retry, etc.)
|
||||
struct PlayerOverlayButton: View {
|
||||
let icon: String
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, *) {
|
||||
Button(action: action) {
|
||||
Image(systemName: icon)
|
||||
.font(.title)
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 70, height: 70)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(.regular.interactive(), in: .circle)
|
||||
.environment(\.colorScheme, .dark)
|
||||
} else {
|
||||
Button(action: action) {
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.frame(width: 70, height: 70)
|
||||
.overlay {
|
||||
Image(systemName: icon)
|
||||
.font(.title)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user