mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Each video row carried its own sheets: tappableVideo added the queue action and resume sheets, the context menu and swipe actions added add-to-playlist and download options, so a row could declare up to six sheets, owned by a row that a list may recycle. Rows now present those through the nearest sheet host, as does the video info view's resume prompt. The queue action sheet for a tapped description link becomes a global request instead of being declared at both the app root and the expanded player. Sheets take the color scheme where their host sits, so the floating details panel's dark override no longer reaches them and the presentationColorScheme workaround is gone.
294 lines
10 KiB
Swift
294 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
|
|
@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
|
|
)
|
|
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
|
|
}
|
|
// 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
|