Files
yattee/Yattee/Views/Components/VideoSwipeActionsModifier.swift
Arkadiusz Fal 4770bbe3b9 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.
2026-10-08 23:57:56 +02:00

293 lines
10 KiB
Swift

//
// VideoSwipeActionsModifier.swift
// Yattee
//
// View modifier that applies configurable swipe actions to video rows.
//
import SwiftUI
#if !os(tvOS)
/// View modifier that applies user-configurable swipe actions plus fixed context-specific actions.
struct VideoSwipeActionsModifier: ViewModifier {
@Environment(\.appEnvironment) private var appEnvironment
@Environment(\.presentAppSheet) private var presentAppSheet
let video: Video
var fixedActions: [SwipeAction] = []
func body(content: Content) -> some View {
content
.swipeActions(actionsArray: allActions())
}
private var visibleActions: [VideoSwipeAction] {
appEnvironment?.settingsManager.visibleVideoSwipeActions() ?? []
}
private func allActions() -> [SwipeAction] {
var actions = visibleActions.map { swipeAction(for: $0) }
actions.append(contentsOf: fixedActions)
return actions
}
private func swipeAction(for action: VideoSwipeAction) -> SwipeAction {
switch action {
case .playNext:
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
appEnvironment?.queueManager.playNext(video)
reset()
}
case .addToQueue:
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
appEnvironment?.queueManager.addToQueue(video)
reset()
}
case .download:
return downloadSwipeAction(action: action)
case .share:
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
shareVideo()
reset()
}
case .videoInfo:
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
appEnvironment?.navigationCoordinator.navigate(to: .video(.loaded(video)))
reset()
}
case .goToChannel:
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
appEnvironment?.navigationCoordinator.navigateToChannel(for: video)
reset()
}
case .addToBookmarks:
return bookmarkSwipeAction(action: action)
case .addToPlaylist:
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
presentAppSheet(.addToPlaylist(video))
reset()
}
case .markWatched:
return watchedSwipeAction(action: action)
}
}
private func downloadSwipeAction(action: VideoSwipeAction) -> SwipeAction {
let isDownloading = appEnvironment?.downloadManager.isDownloading(video.id) ?? false
let isDownloaded = appEnvironment?.downloadManager.isDownloaded(video.id) ?? false
if isDownloading {
// Cancel download
return SwipeAction(
symbolImage: "xmark.circle",
tint: action.tint,
background: .red
) { reset in
if let download = appEnvironment?.downloadManager.download(for: video.id) {
Task {
await appEnvironment?.downloadManager.cancel(download)
}
}
reset()
}
} else if isDownloaded {
// Already downloaded - show checkmark
return SwipeAction(
symbolImage: "checkmark.circle.fill",
tint: action.tint,
background: action.backgroundColor
) { reset in
// No action - already downloaded
reset()
}
} else {
// Start download
return SwipeAction(
symbolImage: action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
startDownload()
reset()
}
}
}
private func bookmarkSwipeAction(action: VideoSwipeAction) -> SwipeAction {
let isBookmarked = appEnvironment?.dataManager.isBookmarked(videoID: video.id.videoID) ?? false
return SwipeAction(
symbolImage: isBookmarked ? "bookmark.slash" : action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
if isBookmarked {
appEnvironment?.dataManager.removeBookmark(for: video.id.videoID)
} else {
appEnvironment?.dataManager.addBookmark(for: video)
}
reset()
}
}
private func watchedSwipeAction(action: VideoSwipeAction) -> SwipeAction {
let isWatched = appEnvironment?.dataManager.watchEntry(for: video.id.videoID)?.isFinished ?? false
return SwipeAction(
symbolImage: isWatched ? "eye.slash" : action.symbolImage,
tint: action.tint,
background: action.backgroundColor
) { reset in
if isWatched {
appEnvironment?.dataManager.markAsUnwatched(videoID: video.id.videoID)
} else {
appEnvironment?.dataManager.markAsWatched(video: video)
}
reset()
}
}
private func shareVideo() {
#if os(iOS)
let activityVC = UIActivityViewController(
activityItems: [appEnvironment?.shareURL(for: video) ?? video.shareURL],
applicationActivities: nil
)
// The key window is the expanded player's while it is up, so the
// share sheet opens above it rather than behind it.
if let rootVC = UIApplication.shared.foregroundWindow?.rootViewController {
// Find the top-most view controller
var topVC = rootVC
while let presented = topVC.presentedViewController {
topVC = presented
}
// On iPad UIActivityViewController is presented as a popover and
// requires an anchor; without sourceView/barButtonItem UIKit aborts
// in -[UIPopoverPresentationController presentationTransitionWillBegin].
if let popover = activityVC.popoverPresentationController {
popover.sourceView = topVC.view
popover.sourceRect = CGRect(x: topVC.view.bounds.midX,
y: topVC.view.bounds.midY,
width: 0, height: 0)
popover.permittedArrowDirections = []
}
topVC.present(activityVC, animated: true)
}
#elseif os(macOS)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString((appEnvironment?.shareURL(for: video) ?? video.shareURL).absoluteString, forType: .string)
#endif
}
private func startDownload() {
guard let appEnvironment else {
presentAppSheet(.downloadOptions(video))
return
}
// Media source videos use direct file URLs
if video.isFromMediaSource {
Task {
do {
try await appEnvironment.downloadManager.autoEnqueueMediaSource(
video,
mediaSourcesManager: appEnvironment.mediaSourcesManager,
webDAVClient: appEnvironment.webDAVClient,
smbClient: appEnvironment.smbClient
)
} catch {
appEnvironment.toastManager.show(
category: .error,
title: String(localized: "download.error.title"),
subtitle: error.localizedDescription,
icon: "exclamationmark.triangle",
iconColor: .red
)
}
}
return
}
let downloadSettings = appEnvironment.downloadSettings
// Check if auto-download mode
if downloadSettings.preferredDownloadQuality != .ask,
let instance = appEnvironment.instancesManager.instance(for: video) {
Task {
do {
try await appEnvironment.downloadManager.autoEnqueue(
video,
preferredQuality: downloadSettings.preferredDownloadQuality,
preferredAudioLanguage: appEnvironment.settingsManager.preferredAudioLanguage,
preferredSubtitlesLanguage: appEnvironment.settingsManager.preferredSubtitlesLanguage,
includeSubtitles: downloadSettings.includeSubtitlesInAutoDownload,
contentService: appEnvironment.contentService,
instance: instance
)
} catch {
appEnvironment.toastManager.show(
category: .error,
title: String(localized: "download.error.title"),
subtitle: error.localizedDescription,
icon: "exclamationmark.triangle",
iconColor: .red
)
}
}
} else {
presentAppSheet(.downloadOptions(video))
}
}
}
// MARK: - View Extension
extension View {
/// Applies user-configurable video swipe actions with optional fixed context-specific actions.
///
/// - Parameters:
/// - video: The video to apply swipe actions for.
/// - fixedActions: Context-specific fixed actions (e.g., delete for history).
/// These appear to the right of configurable actions.
func videoSwipeActions(
video: Video,
fixedActions: [SwipeAction] = []
) -> some View {
modifier(VideoSwipeActionsModifier(video: video, fixedActions: fixedActions))
}
}
#endif