mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
292 lines
10 KiB
Swift
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
|