From d22abf60882d894d1aedf86f108cd484545120fa Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 18 Sep 2026 23:57:34 +0200 Subject: [PATCH] Add an option to share videos via privacy frontend URLs When enabled (the default), share links to YouTube videos point to the active source's web watch page instead of youtube.com. Invidious serves its frontend at the API URL; Piped uses its configured frontend URL. Videos the active source can't serve (PeerTube, extracted sources) and sources without a web frontend keep their original links. The toggle lives in Settings > Layout & Navigation > Sharing on iOS and macOS, and is shown only when an enabled source has a web watch page. --- Yattee/Core/AppEnvironment.swift | 22 ++++++++++++++ .../Settings/SettingsManager+General.swift | 21 +++++++++++++ Yattee/Core/SettingsManager.swift | 4 +++ Yattee/Localizable.xcstrings | 30 +++++++++++++++++++ Yattee/Models/Instance.swift | 12 ++++++++ .../Components/PlayerPillButtonView.swift | 4 ++- .../Views/Components/VideoContextMenu.swift | 2 +- .../VideoSwipeActionsModifier.swift | 4 +-- .../Player/ControlsSectionRenderer.swift | 4 ++- Yattee/Views/Player/MiniPlayerView.swift | 2 +- .../macOS/MacOSControlsSectionRenderer.swift | 2 +- .../LayoutNavigationSettingsView.swift | 27 +++++++++++++++++ Yattee/Views/Video/VideoInfoView.swift | 2 +- 13 files changed, 128 insertions(+), 8 deletions(-) diff --git a/Yattee/Core/AppEnvironment.swift b/Yattee/Core/AppEnvironment.swift index b0c2e2ca..51799aab 100644 --- a/Yattee/Core/AppEnvironment.swift +++ b/Yattee/Core/AppEnvironment.swift @@ -756,6 +756,28 @@ final class AppEnvironment { } } + // MARK: - Sharing + + /// URL for sharing a video, honoring the privacy-frontend sharing setting. + /// + /// When enabled, YouTube videos are shared as links to the active + /// instance's web watch page. Videos the active instance doesn't serve + /// (PeerTube, extracted sources) and instances without a web frontend + /// keep the video's original share URL. + func shareURL(for video: Video) -> URL { + guard settingsManager.privacyFrontendSharingEnabled, + case .global(let provider) = video.id.source, + provider == ContentSource.youtubeProvider, + let base = instancesManager.activeInstance?.watchPageBaseURL, + var components = URLComponents(url: base, resolvingAgainstBaseURL: false) + else { + return video.shareURL + } + components.path += "/watch" + components.queryItems = [URLQueryItem(name: "v", value: video.id.videoID)] + return components.url ?? video.shareURL + } + // MARK: - Notifications /// Ensures the notification infrastructure is enabled (system permission + master toggle + background refresh). diff --git a/Yattee/Core/Settings/SettingsManager+General.swift b/Yattee/Core/Settings/SettingsManager+General.swift index 6d979225..e59fd90d 100644 --- a/Yattee/Core/Settings/SettingsManager+General.swift +++ b/Yattee/Core/Settings/SettingsManager+General.swift @@ -580,6 +580,27 @@ extension SettingsManager { } } + // MARK: - Sharing Settings + + /// Whether share links to YouTube videos point at the active instance's + /// web watch page instead of youtube.com. Default is true. + /// Videos the active instance can't serve keep their original URL. + /// This is a local-only setting (not synced to iCloud). + var privacyFrontendSharingEnabled: Bool { + get { + if let cached = _privacyFrontendSharingEnabled { return cached } + // Default to true if not set + if localDefaults.object(forKey: "privacyFrontendSharingEnabled") == nil { + return true + } + return localDefaults.bool(forKey: "privacyFrontendSharingEnabled") + } + set { + _privacyFrontendSharingEnabled = newValue + localDefaults.set(newValue, forKey: "privacyFrontendSharingEnabled") + } + } + // MARK: - Link Action Settings /// Default action when opening links from share extension or URL schemes. diff --git a/Yattee/Core/SettingsManager.swift b/Yattee/Core/SettingsManager.swift index d9bb13d2..bafa8308 100644 --- a/Yattee/Core/SettingsManager.swift +++ b/Yattee/Core/SettingsManager.swift @@ -203,6 +203,9 @@ final class SettingsManager { // Handoff settings var _handoffEnabled: Bool? + // Sharing settings + var _privacyFrontendSharingEnabled: Bool? + // Remote Control settings var _remoteControlCustomDeviceName: String? var _remoteControlHideWhenBackgrounded: Bool? @@ -541,6 +544,7 @@ final class SettingsManager { _queueAutoPlayNext = nil _queueAutoPlayCountdown = nil _handoffEnabled = nil + _privacyFrontendSharingEnabled = nil _defaultLinkAction = nil _remoteControlCustomDeviceName = nil _remoteControlHideWhenBackgrounded = nil diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 418adb0c..d2d5e001 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -11598,6 +11598,26 @@ } } }, + "settings.behavior.privacyFrontendSharing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Use Privacy Frontend URLs When Sharing" + } + } + } + }, + "settings.behavior.privacyFrontendSharing.footer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Share links to YouTube videos point to the current source's watch page instead of youtube.com. Videos the current source can't open keep their original link." + } + } + } + }, "settings.behavior.searchHistoryLimit" : { "comment" : "Label for search history limit picker", "localizations" : { @@ -11642,6 +11662,16 @@ } } }, + "settings.behavior.sharing.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sharing" + } + } + } + }, "settings.behavior.textAreaTapAction" : { "localizations" : { "en" : { diff --git a/Yattee/Models/Instance.swift b/Yattee/Models/Instance.swift index ace1a84c..892b45c0 100644 --- a/Yattee/Models/Instance.swift +++ b/Yattee/Models/Instance.swift @@ -174,6 +174,18 @@ struct Instance: Identifiable, Codable, Hashable, Sendable { frontendURL ?? url } + /// Base URL of the web watch page for building shareable YouTube links, + /// when this instance has one: Invidious serves its frontend at the API + /// URL, Piped needs the separately configured frontend URL (its API host + /// has no watch pages), and Yattee Server has no web frontend at all. + var watchPageBaseURL: URL? { + switch type { + case .invidious: return frontendURL ?? url + case .piped: return frontendURL + case .peertube, .yatteeServer: return nil + } + } + var contentSource: ContentSource { type.contentSource(for: url) } diff --git a/Yattee/Views/Components/PlayerPillButtonView.swift b/Yattee/Views/Components/PlayerPillButtonView.swift index 8f26e77e..b5227297 100644 --- a/Yattee/Views/Components/PlayerPillButtonView.swift +++ b/Yattee/Views/Components/PlayerPillButtonView.swift @@ -10,6 +10,8 @@ import SwiftUI struct PlayerPillButtonView: View { + @Environment(\.appEnvironment) private var appEnvironment + let configuration: ControlButtonConfiguration let isPlaying: Bool let hasNext: Bool @@ -199,7 +201,7 @@ struct PlayerPillButtonView: View { private var shareButton: some View { Group { if let video { - ShareLink(item: video.shareURL) { + ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) { genericButtonLabel } .buttonStyle(.plain) diff --git a/Yattee/Views/Components/VideoContextMenu.swift b/Yattee/Views/Components/VideoContextMenu.swift index f9931685..54984a6f 100644 --- a/Yattee/Views/Components/VideoContextMenu.swift +++ b/Yattee/Views/Components/VideoContextMenu.swift @@ -280,7 +280,7 @@ struct VideoContextMenuContent: View { // Share #if !os(tvOS) - ShareLink(item: video.shareURL) { + ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) { Label(String(localized: "video.context.share"), systemImage: "square.and.arrow.up") } #endif diff --git a/Yattee/Views/Components/VideoSwipeActionsModifier.swift b/Yattee/Views/Components/VideoSwipeActionsModifier.swift index 28bed229..f47a6fdc 100644 --- a/Yattee/Views/Components/VideoSwipeActionsModifier.swift +++ b/Yattee/Views/Components/VideoSwipeActionsModifier.swift @@ -191,7 +191,7 @@ struct VideoSwipeActionsModifier: ViewModifier { private func shareVideo() { #if os(iOS) let activityVC = UIActivityViewController( - activityItems: [video.shareURL], + activityItems: [appEnvironment?.shareURL(for: video) ?? video.shareURL], applicationActivities: nil ) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, @@ -217,7 +217,7 @@ struct VideoSwipeActionsModifier: ViewModifier { #elseif os(macOS) let pasteboard = NSPasteboard.general pasteboard.clearContents() - pasteboard.setString(video.shareURL.absoluteString, forType: .string) + pasteboard.setString((appEnvironment?.shareURL(for: video) ?? video.shareURL).absoluteString, forType: .string) #endif } diff --git a/Yattee/Views/Player/ControlsSectionRenderer.swift b/Yattee/Views/Player/ControlsSectionRenderer.swift index a29b825d..b68b74c8 100644 --- a/Yattee/Views/Player/ControlsSectionRenderer.swift +++ b/Yattee/Views/Player/ControlsSectionRenderer.swift @@ -11,6 +11,8 @@ import SwiftUI /// Renders a horizontal row of control buttons based on layout configuration. struct ControlsSectionRenderer: View { + @Environment(\.appEnvironment) private var appEnvironment + let section: LayoutSection let actions: PlayerControlsActions let globalSettings: GlobalLayoutSettings @@ -925,7 +927,7 @@ struct ControlsSectionRenderer: View { @ViewBuilder private var shareButton: some View { if let video = actions.currentVideo { - ShareLink(item: video.shareURL) { + ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) { buttonContent(systemImage: "square.and.arrow.up", tint: .white) } } diff --git a/Yattee/Views/Player/MiniPlayerView.swift b/Yattee/Views/Player/MiniPlayerView.swift index ceef2223..50aa06e1 100644 --- a/Yattee/Views/Player/MiniPlayerView.swift +++ b/Yattee/Views/Player/MiniPlayerView.swift @@ -572,7 +572,7 @@ struct MiniPlayerView: View { @ViewBuilder private func shareButton(config: ControlButtonConfiguration) -> some View { if let video = currentVideo { - ShareLink(item: video.shareURL) { + ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) { Image(systemName: "square.and.arrow.up") .font(isTabAccessory ? .title3 : .title2) .frame(width: 32, height: 32) diff --git a/Yattee/Views/Player/macOS/MacOSControlsSectionRenderer.swift b/Yattee/Views/Player/macOS/MacOSControlsSectionRenderer.swift index 995d7e9e..0517d440 100644 --- a/Yattee/Views/Player/macOS/MacOSControlsSectionRenderer.swift +++ b/Yattee/Views/Player/macOS/MacOSControlsSectionRenderer.swift @@ -670,7 +670,7 @@ struct MacOSControlsSectionRenderer: View { @ViewBuilder private var shareButton: some View { if let video = actions.currentVideo { - ShareLink(item: video.shareURL) { + ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) { buttonLabel(systemImage: "square.and.arrow.up") } .buttonStyle(buttonStyleForContext) diff --git a/Yattee/Views/Settings/LayoutNavigationSettingsView.swift b/Yattee/Views/Settings/LayoutNavigationSettingsView.swift index 61e70670..42273e7d 100644 --- a/Yattee/Views/Settings/LayoutNavigationSettingsView.swift +++ b/Yattee/Views/Settings/LayoutNavigationSettingsView.swift @@ -22,6 +22,7 @@ struct LayoutNavigationSettingsView: View { #else VideoActionsSection(settings: settings) LinkActionSection(settings: settings) + SharingSection(settings: settings) ClipboardSection(settings: settings) #endif #if os(iOS) @@ -143,6 +144,32 @@ private struct LinkActionSection: View { } } +// MARK: - Sharing Section + +#if !os(tvOS) +private struct SharingSection: View { + @Environment(\.appEnvironment) private var appEnvironment + @Bindable var settings: SettingsManager + + /// Shown only when a source with a web watch page exists — without one + /// the toggle can't change anything. + private var hasPrivacyFrontendSource: Bool { + appEnvironment?.instancesManager.enabledInstances.contains { $0.watchPageBaseURL != nil } ?? false + } + + var body: some View { + if hasPrivacyFrontendSource { + SettingsFormSection("settings.behavior.sharing.header", footer: "settings.behavior.privacyFrontendSharing.footer") { + Toggle( + String(localized: "settings.behavior.privacyFrontendSharing"), + isOn: $settings.privacyFrontendSharingEnabled + ) + } + } + } +} +#endif + // MARK: - Clipboard Section private struct ClipboardSection: View { diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index 4761f066..fe986d72 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -1173,7 +1173,7 @@ struct VideoInfoView: View { // Share button if let video = displayedVideo { - ShareLink(item: video.shareURL) { + ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) { #if os(macOS) Label(String(localized: "video.share"), systemImage: "square.and.arrow.up") .font(.body)