mirror of
https://github.com/yattee/yattee.git
synced 2026-11-05 03:17:19 +00:00
Remove dead code found by Periphery on all platforms
Delete declarations that Periphery reports unused on every platform that compiles them (iOS, macOS, tvOS), including nine views used only by their own previews. Platform-gated code still used on another platform, device- only MPV render paths, migration models, CloudKit record names, App Intents metadata and unused parameters are kept.
This commit is contained in:
@@ -13,8 +13,6 @@ import SwiftUI
|
||||
/// Automatically handles DeArrow integration.
|
||||
/// Supports optional queue context for auto-play functionality.
|
||||
struct BookmarkRowView: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
|
||||
let bookmark: Bookmark
|
||||
var style: VideoRowStyle = .regular
|
||||
var watchProgress: Double? = nil
|
||||
@@ -27,10 +25,6 @@ struct BookmarkRowView: View {
|
||||
var videoIndex: Int? = nil
|
||||
var loadMoreVideos: LoadMoreVideosCallback? = nil
|
||||
|
||||
private var accentColor: Color {
|
||||
appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor
|
||||
}
|
||||
|
||||
private var isBookmarkValid: Bool {
|
||||
bookmark.modelContext != nil && !bookmark.isDeleted
|
||||
}
|
||||
|
||||
@@ -27,14 +27,6 @@ struct ExpandedCommentsView: View {
|
||||
@State private var scrollBounceOffset: CGFloat = 0
|
||||
@State private var isDismissThresholdReached = false
|
||||
|
||||
private var playerState: PlayerState? { appEnvironment?.playerService.state }
|
||||
private var contentService: ContentService? { appEnvironment?.contentService }
|
||||
private var instancesManager: InstancesManager? { appEnvironment?.instancesManager }
|
||||
|
||||
private var comments: [Comment] { playerState?.comments ?? [] }
|
||||
private var commentsState: CommentsLoadState { playerState?.commentsState ?? .idle }
|
||||
private var videoSource: ContentSource? { playerState?.currentVideo?.id.source }
|
||||
|
||||
private var backgroundColor: Color {
|
||||
#if os(iOS)
|
||||
Color(.systemBackground)
|
||||
@@ -363,53 +355,6 @@ struct CommentsListContent: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scroll Fade Overlay
|
||||
|
||||
/// A view modifier that applies a gradient fade overlay to the top and bottom edges of a scroll view.
|
||||
private struct ScrollFadeOverlayModifier: ViewModifier {
|
||||
let fadeHeight: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay(alignment: .top) {
|
||||
// Top gradient fade
|
||||
LinearGradient(
|
||||
colors: [backgroundColor, .clear],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: fadeHeight)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
// Bottom gradient fade
|
||||
LinearGradient(
|
||||
colors: [.clear, backgroundColor],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(height: fadeHeight)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
#if os(iOS)
|
||||
Color(.systemBackground)
|
||||
#elseif os(macOS)
|
||||
Color(nsColor: .windowBackgroundColor)
|
||||
#else
|
||||
Color.black
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func scrollFadeMask(fadeHeight: CGFloat = 24) -> some View {
|
||||
modifier(ScrollFadeOverlayModifier(fadeHeight: fadeHeight))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scroll Detection
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
@@ -83,52 +83,6 @@ struct ErrorStateView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Load State View (Generic Container)
|
||||
|
||||
/// A generic container that handles loading, error, and content states.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```swift
|
||||
/// LoadStateView(
|
||||
/// isLoading: viewModel.isLoading,
|
||||
/// errorMessage: viewModel.error,
|
||||
/// data: viewModel.data,
|
||||
/// loadingMessage: "Loading...",
|
||||
/// errorTitle: "Error",
|
||||
/// onRetry: { await viewModel.load() }
|
||||
/// ) { data in
|
||||
/// ContentView(data: data)
|
||||
/// }
|
||||
/// ```
|
||||
struct LoadStateView<Content: View, Data>: View {
|
||||
let isLoading: Bool
|
||||
let errorMessage: String?
|
||||
let data: Data?
|
||||
var loadingMessage: String? = nil
|
||||
var loadingSubtext: String? = nil
|
||||
var errorTitle: String = "Error"
|
||||
var onRetry: (() async -> Void)? = nil
|
||||
var onDismiss: (() -> Void)? = nil
|
||||
@ViewBuilder let content: (Data) -> Content
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isLoading && data == nil {
|
||||
LoadingView(message: loadingMessage, subtext: loadingSubtext)
|
||||
} else if let error = errorMessage, data == nil {
|
||||
ErrorStateView(
|
||||
title: errorTitle,
|
||||
message: error,
|
||||
onRetry: onRetry,
|
||||
onDismiss: onDismiss
|
||||
)
|
||||
} else if let data {
|
||||
content(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Loading") {
|
||||
@@ -143,38 +97,3 @@ struct LoadStateView<Content: View, Data>: View {
|
||||
onDismiss: {}
|
||||
)
|
||||
}
|
||||
|
||||
#Preview("LoadStateView - Loading") {
|
||||
LoadStateView(
|
||||
isLoading: true,
|
||||
errorMessage: nil,
|
||||
data: nil as String?,
|
||||
loadingMessage: "Fetching data..."
|
||||
) { data in
|
||||
Text(data)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("LoadStateView - Error") {
|
||||
LoadStateView(
|
||||
isLoading: false,
|
||||
errorMessage: "Network error occurred",
|
||||
data: nil as String?,
|
||||
errorTitle: "Failed to Load",
|
||||
onRetry: {}
|
||||
) { data in
|
||||
Text(data)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("LoadStateView - Content") {
|
||||
LoadStateView(
|
||||
isLoading: false,
|
||||
errorMessage: nil,
|
||||
data: "Hello, World!",
|
||||
loadingMessage: "Loading..."
|
||||
) { data in
|
||||
Text(data)
|
||||
.font(.largeTitle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,34 +24,4 @@ struct ShareSheet: UIViewControllerRepresentable {
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
struct ShareSheet: View {
|
||||
let items: [Any]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
if let text = items.first as? String {
|
||||
ScrollView {
|
||||
Text(text)
|
||||
.font(.caption)
|
||||
.fontDesign(.monospaced)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
.frame(maxHeight: 400)
|
||||
}
|
||||
|
||||
Button(String(localized: "settings.advanced.logs.export.copy")) {
|
||||
if let text = items.first as? String {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(text, forType: .string)
|
||||
}
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
.padding()
|
||||
.frame(minWidth: 420, minHeight: 320)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
//
|
||||
// SourceListContainer.swift
|
||||
// Yattee
|
||||
//
|
||||
// Container for source list views with inset/plain styling.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A container that handles inset/plain list styling for source views.
|
||||
///
|
||||
/// Simplified version of VideoListContainer for simpler source rows.
|
||||
struct SourceListContainer<Header: View, Content: View>: View {
|
||||
let listStyle: VideoListStyle
|
||||
|
||||
@ViewBuilder let header: () -> Header
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
/// Corner radius for card container in inset style.
|
||||
private let cardCornerRadius: CGFloat = 10
|
||||
|
||||
/// Horizontal padding for card container.
|
||||
private let cardHorizontalPadding: CGFloat = 16
|
||||
|
||||
/// Bottom padding for card container.
|
||||
private let cardBottomPadding: CGFloat = 16
|
||||
|
||||
var body: some View {
|
||||
#if os(tvOS)
|
||||
// tvOS: Simple ScrollView without background overlay
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
header()
|
||||
content()
|
||||
}
|
||||
}
|
||||
#else
|
||||
// iOS/macOS: Background overlay pattern
|
||||
backgroundStyle.color
|
||||
.ignoresSafeArea()
|
||||
.overlay(
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
header()
|
||||
|
||||
if listStyle == .inset {
|
||||
insetCardContent
|
||||
} else {
|
||||
plainContent
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var backgroundStyle: ListBackgroundStyle {
|
||||
listStyle == .inset ? .grouped : .plain
|
||||
}
|
||||
|
||||
private var insetCardContent: some View {
|
||||
LazyVStack(spacing: 0) {
|
||||
content()
|
||||
}
|
||||
.background(ListBackgroundStyle.card.color)
|
||||
.clipShape(RoundedRectangle(cornerRadius: cardCornerRadius))
|
||||
.padding(.horizontal, cardHorizontalPadding)
|
||||
.padding(.bottom, cardBottomPadding)
|
||||
}
|
||||
|
||||
private var plainContent: some View {
|
||||
LazyVStack(spacing: 0) {
|
||||
content()
|
||||
}
|
||||
.padding(.bottom, cardBottomPadding)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Initializers
|
||||
|
||||
extension SourceListContainer where Header == EmptyView {
|
||||
/// Creates a source list container without a header.
|
||||
init(
|
||||
listStyle: VideoListStyle,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.listStyle = listStyle
|
||||
self.header = { EmptyView() }
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Inset Style") {
|
||||
SourceListContainer(listStyle: .inset) {
|
||||
Text("Header")
|
||||
.padding()
|
||||
} content: {
|
||||
ForEach(0..<5) { index in
|
||||
SourceListRow(isLast: index == 4, listStyle: .inset) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "server.rack")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.blue)
|
||||
.frame(width: 32)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Source \(index + 1)")
|
||||
.font(.headline)
|
||||
Text("example.com")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Plain Style") {
|
||||
SourceListContainer(listStyle: .plain) {
|
||||
ForEach(0..<5) { index in
|
||||
SourceListRow(isLast: index == 4, listStyle: .plain) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "server.rack")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.blue)
|
||||
.frame(width: 32)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Source \(index + 1)")
|
||||
.font(.headline)
|
||||
Text("example.com")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,6 @@ struct SwipeActionModifier: ViewModifier {
|
||||
@State private var progress: CGFloat = 0
|
||||
|
||||
// Scroll tracking for auto-close on scroll
|
||||
@State private var currentScrollOffset: CGFloat = 0
|
||||
@State private var storedScrollOffset: CGFloat?
|
||||
|
||||
// Shared state reference (computed to avoid inclusion in memberwise init)
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
//
|
||||
// TextTruncationEffect.swift
|
||||
// Yattee
|
||||
//
|
||||
// Animated text truncation effect with "...more" indicator.
|
||||
// Adapted from Balaji Venkatesh's TruncationEffect implementation.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension Text {
|
||||
/// Applies an animated truncation effect that shows "...more" when collapsed.
|
||||
/// - Parameters:
|
||||
/// - length: Number of lines to show when truncated
|
||||
/// - isEnabled: Whether truncation is enabled (false = expanded)
|
||||
/// - animation: Animation to use for expand/collapse transitions
|
||||
@ViewBuilder
|
||||
func truncationEffect(length: Int, isEnabled: Bool, animation: Animation) -> some View {
|
||||
self.modifier(
|
||||
TruncationEffectViewModifier(
|
||||
length: length,
|
||||
isEnabled: isEnabled,
|
||||
animation: animation
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Modifier
|
||||
|
||||
private struct TruncationEffectViewModifier: ViewModifier {
|
||||
var length: Int
|
||||
var isEnabled: Bool
|
||||
var animation: Animation
|
||||
|
||||
@State private var limitedSize: CGSize = .zero
|
||||
@State private var fullSize: CGSize = .zero
|
||||
@State private var animatedProgress: CGFloat = 0
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.lineLimit(length)
|
||||
.opacity(0)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onGeometryChange(for: CGSize.self) {
|
||||
$0.size
|
||||
} action: { newValue in
|
||||
limitedSize = newValue
|
||||
}
|
||||
.frame(height: isExpanded ? fullSize.height : nil)
|
||||
.overlay {
|
||||
// Full content with animation
|
||||
GeometryReader { proxy in
|
||||
let contentSize = proxy.size
|
||||
|
||||
content
|
||||
.textRenderer(
|
||||
TruncationTextRenderer(
|
||||
length: length,
|
||||
progress: animatedProgress
|
||||
)
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.onGeometryChange(for: CGSize.self) {
|
||||
$0.size
|
||||
} action: { newValue in
|
||||
fullSize = newValue
|
||||
}
|
||||
.frame(
|
||||
width: contentSize.width,
|
||||
height: contentSize.height,
|
||||
alignment: isExpanded ? .leading : .topLeading
|
||||
)
|
||||
}
|
||||
}
|
||||
.contentShape(.rect)
|
||||
.onChange(of: isEnabled) { _, newValue in
|
||||
withAnimation(animation) {
|
||||
animatedProgress = !newValue ? 1 : 0
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Set initial value without animation
|
||||
animatedProgress = !isEnabled ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
var isExpanded: Bool {
|
||||
animatedProgress == 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Text Renderer
|
||||
|
||||
@Animatable
|
||||
private struct TruncationTextRenderer: TextRenderer {
|
||||
@AnimatableIgnored var length: Int
|
||||
var progress: CGFloat
|
||||
|
||||
/// Minimum number of hidden characters to show "...more" indicator.
|
||||
/// If less than this, just show the full text without truncation indicator.
|
||||
private let minHiddenCharacters = 10
|
||||
|
||||
func draw(layout: Text.Layout, in ctx: inout GraphicsContext) {
|
||||
let totalLines = layout.count
|
||||
let hasExtraLines = totalLines > length
|
||||
|
||||
// Count characters in lines beyond the limit
|
||||
let hiddenCharacterCount: Int = {
|
||||
guard hasExtraLines else { return 0 }
|
||||
var count = 0
|
||||
for index in length..<totalLines {
|
||||
count += layout[index].flatMap { $0 }.count
|
||||
}
|
||||
return count
|
||||
}()
|
||||
|
||||
// If hidden content is too short, don't show "...more" - just draw normally
|
||||
let shouldShowMoreIndicator = hasExtraLines && hiddenCharacterCount >= minHiddenCharacters
|
||||
|
||||
for (index, line) in layout.enumerated() {
|
||||
var copyContext = ctx
|
||||
if index == length - 1 && shouldShowMoreIndicator {
|
||||
drawMoreTextAtEnd(line: line, context: ©Context)
|
||||
} else {
|
||||
if index < length {
|
||||
// Draw all visible lines
|
||||
copyContext.draw(line)
|
||||
} else if shouldShowMoreIndicator {
|
||||
drawLinesWithBlurEffect(index: index, layout: layout, in: ©Context)
|
||||
} else {
|
||||
// No "...more" indicator - just draw with fade based on progress
|
||||
copyContext.opacity = progress
|
||||
copyContext.draw(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func drawLinesWithBlurEffect(index: Int, layout: Text.Layout, in ctx: inout GraphicsContext) {
|
||||
let line = layout[index]
|
||||
|
||||
let lineIndex = Double(index - length)
|
||||
let totalExtraLines = Double(layout.count - length)
|
||||
|
||||
// Divide the animation progress among all extra lines
|
||||
let lineStartProgress = lineIndex / max(1, totalExtraLines)
|
||||
let lineEndProgress = (lineIndex + 1) / max(1, totalExtraLines)
|
||||
|
||||
// Calculate this specific line's progress
|
||||
let lineProgress = max(0, min(1, (progress - lineStartProgress) / (lineEndProgress - lineStartProgress)))
|
||||
|
||||
ctx.opacity = lineProgress
|
||||
ctx.addFilter(.blur(radius: blurRadius - (blurRadius * lineProgress)))
|
||||
ctx.draw(line)
|
||||
}
|
||||
|
||||
private func drawMoreTextAtEnd(line: Text.Layout.Element, context: inout GraphicsContext) {
|
||||
let runs = line.flatMap { $0 }
|
||||
let runsCount = runs.count
|
||||
let text = " ..." + String(localized: "common.more")
|
||||
let textCount = text.count
|
||||
|
||||
// Draw runs until the text count
|
||||
for index in 0..<max(runsCount - textCount, 0) {
|
||||
let run = runs[index]
|
||||
context.draw(run)
|
||||
}
|
||||
|
||||
// Draw remaining runs with opacity filter
|
||||
for index in max(runsCount - textCount, 0)..<runsCount {
|
||||
let run = runs[index]
|
||||
context.opacity = progress
|
||||
context.draw(run)
|
||||
}
|
||||
|
||||
// Draw "...more" text
|
||||
let textRunIndex = max(runsCount - textCount, 0)
|
||||
var typography: Text.Layout.TypographicBounds
|
||||
if !runs.isEmpty {
|
||||
typography = runs[textRunIndex].typographicBounds
|
||||
} else {
|
||||
typography = line.typographicBounds
|
||||
}
|
||||
|
||||
let fontSize: CGFloat = typography.ascent
|
||||
let font = platformFont(ofSize: fontSize)
|
||||
|
||||
let spacing: CGFloat = textWidth(text, font: font) / 2
|
||||
|
||||
let swiftUIText = Text(text)
|
||||
.font(Font(font))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
let origin = CGPoint(
|
||||
x: typography.rect.minX + spacing,
|
||||
y: typography.rect.midY
|
||||
)
|
||||
|
||||
context.opacity = 1 - progress
|
||||
context.draw(swiftUIText, at: origin)
|
||||
}
|
||||
|
||||
private var blurRadius: CGFloat {
|
||||
5
|
||||
}
|
||||
|
||||
// MARK: - Cross-Platform Font Helpers
|
||||
|
||||
#if os(macOS)
|
||||
private func platformFont(ofSize size: CGFloat) -> NSFont {
|
||||
NSFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
private func textWidth(_ text: String, font: NSFont) -> CGFloat {
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: font]
|
||||
return (text as NSString).size(withAttributes: attributes).width
|
||||
}
|
||||
#else
|
||||
private func platformFont(ofSize size: CGFloat) -> UIFont {
|
||||
UIFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
private func textWidth(_ text: String, font: UIFont) -> CGFloat {
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: font]
|
||||
return (text as NSString).size(withAttributes: attributes).width
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -138,22 +138,6 @@ extension VideoListContainer where Footer == EmptyView {
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoListContainer where Header == EmptyView {
|
||||
/// Creates a video list container with a footer but no header.
|
||||
init(
|
||||
listStyle: VideoListStyle,
|
||||
rowStyle: VideoRowStyle,
|
||||
@ViewBuilder content: @escaping () -> Content,
|
||||
@ViewBuilder footer: @escaping () -> Footer
|
||||
) {
|
||||
self.listStyle = listStyle
|
||||
self.rowStyle = rowStyle
|
||||
self.header = { EmptyView() }
|
||||
self.content = content
|
||||
self.footer = footer
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Inset Style") {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
//
|
||||
// VideoNavigationButton.swift
|
||||
// Yattee
|
||||
//
|
||||
// Navigation button for switching between videos in VideoInfoView.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A floating circular button for navigating between videos in a queue.
|
||||
/// Used in VideoInfoView to switch to previous/next video.
|
||||
struct VideoNavigationButton: View {
|
||||
let direction: Direction
|
||||
let action: () -> Void
|
||||
var isLoading: Bool = false
|
||||
var hasError: Bool = false
|
||||
|
||||
enum Direction {
|
||||
case previous
|
||||
case next
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .previous: return "chevron.left"
|
||||
case .next: return "chevron.right"
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityLabel: String {
|
||||
switch self {
|
||||
case .previous: return String(localized: "video.navigation.previous")
|
||||
case .next: return String(localized: "video.navigation.next")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
ZStack {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.tint(.primary)
|
||||
} else {
|
||||
Image(systemName: direction.icon)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary)
|
||||
.frame(width: 44, height: 44)
|
||||
.glassBackground(.regular, in: .circle)
|
||||
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
|
||||
.overlay {
|
||||
if hasError {
|
||||
Circle()
|
||||
.stroke(Color.red, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isLoading)
|
||||
.accessibilityLabel(direction.accessibilityLabel)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview("Previous") {
|
||||
VideoNavigationButton(direction: .previous) {}
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Next") {
|
||||
VideoNavigationButton(direction: .next) {}
|
||||
.padding()
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import SwiftUI
|
||||
/// View modifier that applies user-configurable swipe actions plus fixed context-specific actions.
|
||||
struct VideoSwipeActionsModifier: ViewModifier {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@Environment(\.videoQueueContext) private var queueContext
|
||||
@Environment(\.presentAppSheet) private var presentAppSheet
|
||||
|
||||
let video: Video
|
||||
|
||||
@@ -12,7 +12,6 @@ import SwiftUI
|
||||
/// Shows layout picker (list/grid), size options, and filter toggles.
|
||||
/// Reusable across different views with their own storage bindings.
|
||||
struct ViewOptionsSheet: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@Binding var layout: VideoListLayout
|
||||
@Binding var rowStyle: VideoRowStyle
|
||||
@Binding var gridColumns: Int
|
||||
|
||||
Reference in New Issue
Block a user