Files
yattee/Yattee/Views/Components/VideoSwipeActionsModifier.swift
Arkadiusz Fal 612dce6b9f Refactor views
2026-02-09 01:13:02 +01:00

292 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(\.videoQueueContext) private var queueContext
let video: Video
var fixedActions: [SwipeAction] = []
@State private var showingPlaylistSheet = false
@State private var showingDownloadSheet = false
func body(content: Content) -> some View {
content
.swipeActions(actionsArray: allActions())
.sheet(isPresented: $showingPlaylistSheet) {
PlaylistSelectorSheet(video: video)
}
.sheet(isPresented: $showingDownloadSheet) {
DownloadQualitySheet(video: video)
}
}
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
showingPlaylistSheet = true
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: [video.shareURL],
applicationActivities: nil
)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first,
let rootVC = window.rootViewController {
// Find the top-most view controller
var topVC = rootVC
while let presented = topVC.presentedViewController {
topVC = presented
}
topVC.present(activityVC, animated: true)
}
#elseif os(macOS)
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(video.shareURL.absoluteString, forType: .string)
#endif
}
private func startDownload() {
guard let appEnvironment else {
showingDownloadSheet = true
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 {
showingDownloadSheet = true
}
}
}
// 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