mirror of
https://github.com/yattee/yattee.git
synced 2026-11-05 19:37:19 +00:00
Load a single link from Open Link into its details screen
Open Link gets a Load action that resolves one link and shows its video details page, playlist or channel screen instead of only playing or downloading it. Play and Download stay as shortcuts for video links. Timed links start at their timestamp from the details page, the Show Details link action opens the details page, and a short extraction cache avoids re-extracting the same link for details and playback.
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
// OpenLinkSheet.swift
|
||||
// Yattee
|
||||
//
|
||||
// Sheet for entering URLs to play or download via Yattee Server's yt-dlp extraction.
|
||||
// Sheet for entering links: Load shows a single link's video, playlist or
|
||||
// channel screen; Play and Download act on video links directly (YouTube,
|
||||
// PeerTube, direct media, or anything Yattee Server's yt-dlp extracts).
|
||||
// Supports multiple URLs (one per line) with batch processing.
|
||||
//
|
||||
|
||||
@@ -264,33 +266,45 @@ struct OpenLinkFormView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await downloadAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.downloadAll")
|
||||
: String(localized: "openLink.download"),
|
||||
systemImage: "arrow.down.circle"
|
||||
)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
if allLinksAreVideos {
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await downloadAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.downloadAll")
|
||||
: String(localized: "openLink.download"),
|
||||
systemImage: "arrow.down.circle"
|
||||
)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await openAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.openAll")
|
||||
: String(localized: "openLink.open"),
|
||||
systemImage: "play.fill"
|
||||
)
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await openAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.openAll")
|
||||
: String(localized: "openLink.open"),
|
||||
systemImage: "play.fill"
|
||||
)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
}
|
||||
|
||||
if !isMultipleURLs {
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await loadURL() }
|
||||
} label: {
|
||||
Label(String(localized: "openLink.load"), systemImage: "arrow.right.circle")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.disabled(!isValidInput)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.disabled(!isValidInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,35 +544,49 @@ struct OpenLinkFormView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
// Open/Play button
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await openAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.openAll")
|
||||
: String(localized: "openLink.open"),
|
||||
systemImage: "play.fill"
|
||||
)
|
||||
// Load: show the link's video, playlist or channel screen
|
||||
if !isMultipleURLs {
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await loadURL() }
|
||||
} label: {
|
||||
Label(String(localized: "openLink.load"), systemImage: "arrow.right.circle")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
|
||||
#if !os(tvOS)
|
||||
// Download button
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await downloadAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.downloadAll")
|
||||
: String(localized: "openLink.download"),
|
||||
systemImage: "arrow.down.circle"
|
||||
)
|
||||
if allLinksAreVideos {
|
||||
// Play shortcut
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await openAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.openAll")
|
||||
: String(localized: "openLink.open"),
|
||||
systemImage: "play.fill"
|
||||
)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
|
||||
#if !os(tvOS)
|
||||
// Download shortcut
|
||||
Button {
|
||||
isTextEditorFocused = false
|
||||
Task { await downloadAllURLs() }
|
||||
} label: {
|
||||
Label(
|
||||
isMultipleURLs
|
||||
? String(localized: "openLink.downloadAll")
|
||||
: String(localized: "openLink.download"),
|
||||
systemImage: "arrow.down.circle"
|
||||
)
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
#endif
|
||||
}
|
||||
.disabled(!isValidInput)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,6 +654,18 @@ struct OpenLinkFormView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every parsed link is a video, so Play and Download apply.
|
||||
/// Playlists and channels can only be loaded.
|
||||
private var allLinksAreVideos: Bool {
|
||||
let router = URLRouter()
|
||||
return parsedURLs.allSatisfy { url in
|
||||
switch router.route(url) {
|
||||
case .video, .externalVideo, .directMedia, nil: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Clipboard
|
||||
|
||||
private func checkClipboard() {
|
||||
@@ -656,6 +696,33 @@ struct OpenLinkFormView: View {
|
||||
.map { $0 }
|
||||
}
|
||||
|
||||
// MARK: - Load Action
|
||||
|
||||
/// Resolves the single link and shows its screen: a video's details page,
|
||||
/// or the playlist / channel screen. Errors stay in the form.
|
||||
private func loadURL() async {
|
||||
guard let url = parsedURLs.first, !isMultipleURLs, let appEnvironment else { return }
|
||||
|
||||
isExtracting = true
|
||||
hasErrors = false
|
||||
extractedItems = [ExtractedItem(url: url, status: .extracting)]
|
||||
|
||||
do {
|
||||
let destination = try await LinkVideoResolver(appEnvironment: appEnvironment).resolveDestination(url)
|
||||
if case .video(.loaded(let video), _) = destination {
|
||||
extractedItems[0].video = video
|
||||
}
|
||||
extractedItems[0].status = .success
|
||||
isExtracting = false
|
||||
dismissIfRequested()
|
||||
appEnvironment.navigationCoordinator.navigateToOpenedLink(destination)
|
||||
} catch {
|
||||
extractedItems[0].status = .failed(error.localizedDescription)
|
||||
hasErrors = true
|
||||
isExtracting = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Open (Play) Action
|
||||
|
||||
private func openAllURLs() async {
|
||||
|
||||
@@ -2400,7 +2400,8 @@ struct VideoInfoView: View {
|
||||
// Skip API fetch for local media sources (SMB, WebDAV, local files)
|
||||
if extractor == MediaFile.smbProvider
|
||||
|| extractor == MediaFile.webdavProvider
|
||||
|| extractor == MediaFile.localFolderProvider {
|
||||
|| extractor == MediaFile.localFolderProvider
|
||||
|| extractor == DirectMediaHelper.provider {
|
||||
isLoadingVideoDetails = false
|
||||
return
|
||||
}
|
||||
@@ -2535,6 +2536,14 @@ struct VideoInfoView: View {
|
||||
return
|
||||
}
|
||||
|
||||
// An opened timed link asks for an exact position - honor it over any
|
||||
// saved progress, without the resume prompt.
|
||||
if let linkStartTime = videoQueueContext?.linkStartTime,
|
||||
videoQueueContext?.video.id == video.id {
|
||||
playVideoWithStartTime(linkStartTime, forceStartTime: true)
|
||||
return
|
||||
}
|
||||
|
||||
// Get saved watch progress from database
|
||||
let savedProgress = env.dataManager.watchProgress(for: video.id.videoID)
|
||||
let videoDuration = video.duration
|
||||
@@ -2570,7 +2579,7 @@ struct VideoInfoView: View {
|
||||
}
|
||||
|
||||
/// Plays the video with the specified start time.
|
||||
private func playVideoWithStartTime(_ time: TimeInterval) {
|
||||
private func playVideoWithStartTime(_ time: TimeInterval, forceStartTime: Bool = false) {
|
||||
guard let video = displayedVideo else { return }
|
||||
|
||||
// Media-browser playback must go through `playFromMediaBrowser` so the
|
||||
@@ -2599,7 +2608,7 @@ struct VideoInfoView: View {
|
||||
let list = context.videoList else {
|
||||
// No queue context - play single video
|
||||
if time > 0 {
|
||||
playerService?.openVideo(video, startTime: time)
|
||||
playerService?.openVideo(video, startTime: time, forceStartTime: forceStartTime)
|
||||
} else {
|
||||
playerService?.openVideo(video)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user