mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55:03 +00:00
Resolve URL shorteners and prompt for ambiguous description links
Tapping bit.ly/tinyurl/t.co/etc. in a description or comment previously opened Safari even when the destination was a playable YouTube URL. Added an opt-in "Resolve Short Links" toggle under YouTube Enhancements (off by default) that follows the redirect on tap: if the target is a YouTube/PeerTube/direct-media URL, open it in-app; otherwise prompt the user before falling back to yt-dlp extraction or the browser. Also added a confirmation dialog for non-shortener links that only matched the loose .externalVideo yt-dlp fallback, so arbitrary web pages in descriptions no longer silently kick off extraction. Prompts live on NavigationCoordinator and are dual-hosted by YatteeApp and ExpandedPlayerSheet so they remain visible whether or not the expanded player is covering the main view.
This commit is contained in:
@@ -7,6 +7,12 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
// MARK: - Description Text Utilities
|
||||
|
||||
@@ -87,12 +93,40 @@ enum DescriptionText {
|
||||
|
||||
// MARK: - OpenURL Action for Seeking
|
||||
|
||||
/// Opens `url` in the user's default system browser (Safari on iOS/tvOS,
|
||||
/// default browser on macOS). Used as the fallback when short-link resolution
|
||||
/// fails or the destination isn't a URL the app can handle.
|
||||
@MainActor
|
||||
private func openInSystemBrowser(_ url: URL) {
|
||||
#if canImport(UIKit) && !os(watchOS)
|
||||
UIApplication.shared.open(url)
|
||||
#elseif canImport(AppKit)
|
||||
NSWorkspace.shared.open(url)
|
||||
#endif
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Adds a URL handler that intercepts timestamp links (seeks the player) and
|
||||
/// known content URLs — YouTube/PeerTube video/channel/playlist links and external
|
||||
/// video URLs — so they open in-app instead of the browser.
|
||||
///
|
||||
/// When the user has enabled "Resolve Short Links" in YouTube Enhancements,
|
||||
/// taps on known URL shorteners (bit.ly, tinyurl, t.co, …) whose hosts aren't
|
||||
/// themselves routable are resolved asynchronously: if the redirect target is
|
||||
/// a supported URL, it's opened in-app; otherwise we fall back to the system
|
||||
/// browser with the original URL.
|
||||
func handleTimestampLinks(using playerService: PlayerService?) -> some View {
|
||||
self.environment(\.openURL, OpenURLAction { url in
|
||||
self.modifier(HandleTimestampLinksModifier(playerService: playerService))
|
||||
}
|
||||
}
|
||||
|
||||
private struct HandleTimestampLinksModifier: ViewModifier {
|
||||
let playerService: PlayerService?
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let resolveShortLinks = appEnvironment?.settingsManager.resolveShortLinksEnabled ?? false
|
||||
return content.environment(\.openURL, OpenURLAction { url in
|
||||
if let seconds = DescriptionText.seekSeconds(from: url) {
|
||||
Task {
|
||||
await playerService?.seek(to: TimeInterval(seconds))
|
||||
@@ -100,11 +134,47 @@ extension View {
|
||||
return .handled
|
||||
}
|
||||
|
||||
if URLRouter().route(url) != nil {
|
||||
let router = URLRouter()
|
||||
|
||||
// 1. Definitely-playable URLs (YouTube / PeerTube / direct media /
|
||||
// custom scheme) open in-app without any prompt.
|
||||
if router.routeConfidently(url) != nil {
|
||||
NotificationCenter.default.post(name: .openDescriptionLink, object: url)
|
||||
return .handled
|
||||
}
|
||||
|
||||
// 2. URL shorteners — resolve first, *then* decide. This has to run
|
||||
// before the loose `route()` check below, because `route()` would
|
||||
// otherwise match bit.ly/t.co/etc. as `.externalVideo` and send
|
||||
// the *shortener* URL itself to yt-dlp.
|
||||
if resolveShortLinks && URLShortenerResolver.isShortener(url) {
|
||||
Task { @MainActor in
|
||||
guard let resolved = await URLShortenerResolver.resolve(url) else {
|
||||
openInSystemBrowser(url)
|
||||
return
|
||||
}
|
||||
|
||||
if router.routeConfidently(resolved) != nil {
|
||||
NotificationCenter.default.post(name: .openDescriptionLink, object: resolved)
|
||||
} else {
|
||||
// Ambiguous destination (e.g. a news article). Let the user
|
||||
// decide whether to try opening in Yattee (falls back to
|
||||
// yt-dlp extraction) or in the system browser.
|
||||
NotificationCenter.default.post(name: .promptResolvedShortLink, object: resolved)
|
||||
}
|
||||
}
|
||||
return .handled
|
||||
}
|
||||
|
||||
// 3. Non-shortener URLs that only match via the loose `.externalVideo`
|
||||
// fallback (e.g. vimeo.com/…, news articles, any http(s)): we
|
||||
// can't tell whether yt-dlp will successfully extract, so ask
|
||||
// the user whether to try extracting or open in the browser.
|
||||
if router.routeConfidently(url) == nil, router.route(url) != nil {
|
||||
NotificationCenter.default.post(name: .promptAmbiguousExternalLink, object: url)
|
||||
return .handled
|
||||
}
|
||||
|
||||
return .systemAction
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user