mirror of
https://github.com/yattee/yattee.git
synced 2026-11-05 19:37:19 +00:00
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.
This commit is contained in:
@@ -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
|
// MARK: - Notifications
|
||||||
|
|
||||||
/// Ensures the notification infrastructure is enabled (system permission + master toggle + background refresh).
|
/// Ensures the notification infrastructure is enabled (system permission + master toggle + background refresh).
|
||||||
|
|||||||
@@ -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
|
// MARK: - Link Action Settings
|
||||||
|
|
||||||
/// Default action when opening links from share extension or URL schemes.
|
/// Default action when opening links from share extension or URL schemes.
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ final class SettingsManager {
|
|||||||
// Handoff settings
|
// Handoff settings
|
||||||
var _handoffEnabled: Bool?
|
var _handoffEnabled: Bool?
|
||||||
|
|
||||||
|
// Sharing settings
|
||||||
|
var _privacyFrontendSharingEnabled: Bool?
|
||||||
|
|
||||||
// Remote Control settings
|
// Remote Control settings
|
||||||
var _remoteControlCustomDeviceName: String?
|
var _remoteControlCustomDeviceName: String?
|
||||||
var _remoteControlHideWhenBackgrounded: Bool?
|
var _remoteControlHideWhenBackgrounded: Bool?
|
||||||
@@ -541,6 +544,7 @@ final class SettingsManager {
|
|||||||
_queueAutoPlayNext = nil
|
_queueAutoPlayNext = nil
|
||||||
_queueAutoPlayCountdown = nil
|
_queueAutoPlayCountdown = nil
|
||||||
_handoffEnabled = nil
|
_handoffEnabled = nil
|
||||||
|
_privacyFrontendSharingEnabled = nil
|
||||||
_defaultLinkAction = nil
|
_defaultLinkAction = nil
|
||||||
_remoteControlCustomDeviceName = nil
|
_remoteControlCustomDeviceName = nil
|
||||||
_remoteControlHideWhenBackgrounded = nil
|
_remoteControlHideWhenBackgrounded = nil
|
||||||
|
|||||||
@@ -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" : {
|
"settings.behavior.searchHistoryLimit" : {
|
||||||
"comment" : "Label for search history limit picker",
|
"comment" : "Label for search history limit picker",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -11642,6 +11662,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings.behavior.sharing.header" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sharing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings.behavior.textAreaTapAction" : {
|
"settings.behavior.textAreaTapAction" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -174,6 +174,18 @@ struct Instance: Identifiable, Codable, Hashable, Sendable {
|
|||||||
frontendURL ?? url
|
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 {
|
var contentSource: ContentSource {
|
||||||
type.contentSource(for: url)
|
type.contentSource(for: url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlayerPillButtonView: View {
|
struct PlayerPillButtonView: View {
|
||||||
|
@Environment(\.appEnvironment) private var appEnvironment
|
||||||
|
|
||||||
let configuration: ControlButtonConfiguration
|
let configuration: ControlButtonConfiguration
|
||||||
let isPlaying: Bool
|
let isPlaying: Bool
|
||||||
let hasNext: Bool
|
let hasNext: Bool
|
||||||
@@ -199,7 +201,7 @@ struct PlayerPillButtonView: View {
|
|||||||
private var shareButton: some View {
|
private var shareButton: some View {
|
||||||
Group {
|
Group {
|
||||||
if let video {
|
if let video {
|
||||||
ShareLink(item: video.shareURL) {
|
ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) {
|
||||||
genericButtonLabel
|
genericButtonLabel
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ struct VideoContextMenuContent: View {
|
|||||||
|
|
||||||
// Share
|
// Share
|
||||||
#if !os(tvOS)
|
#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")
|
Label(String(localized: "video.context.share"), systemImage: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ struct VideoSwipeActionsModifier: ViewModifier {
|
|||||||
private func shareVideo() {
|
private func shareVideo() {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let activityVC = UIActivityViewController(
|
let activityVC = UIActivityViewController(
|
||||||
activityItems: [video.shareURL],
|
activityItems: [appEnvironment?.shareURL(for: video) ?? video.shareURL],
|
||||||
applicationActivities: nil
|
applicationActivities: nil
|
||||||
)
|
)
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
@@ -217,7 +217,7 @@ struct VideoSwipeActionsModifier: ViewModifier {
|
|||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
let pasteboard = NSPasteboard.general
|
let pasteboard = NSPasteboard.general
|
||||||
pasteboard.clearContents()
|
pasteboard.clearContents()
|
||||||
pasteboard.setString(video.shareURL.absoluteString, forType: .string)
|
pasteboard.setString((appEnvironment?.shareURL(for: video) ?? video.shareURL).absoluteString, forType: .string)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import SwiftUI
|
|||||||
|
|
||||||
/// Renders a horizontal row of control buttons based on layout configuration.
|
/// Renders a horizontal row of control buttons based on layout configuration.
|
||||||
struct ControlsSectionRenderer: View {
|
struct ControlsSectionRenderer: View {
|
||||||
|
@Environment(\.appEnvironment) private var appEnvironment
|
||||||
|
|
||||||
let section: LayoutSection
|
let section: LayoutSection
|
||||||
let actions: PlayerControlsActions
|
let actions: PlayerControlsActions
|
||||||
let globalSettings: GlobalLayoutSettings
|
let globalSettings: GlobalLayoutSettings
|
||||||
@@ -925,7 +927,7 @@ struct ControlsSectionRenderer: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var shareButton: some View {
|
private var shareButton: some View {
|
||||||
if let video = actions.currentVideo {
|
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)
|
buttonContent(systemImage: "square.and.arrow.up", tint: .white)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ struct MiniPlayerView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func shareButton(config: ControlButtonConfiguration) -> some View {
|
private func shareButton(config: ControlButtonConfiguration) -> some View {
|
||||||
if let video = currentVideo {
|
if let video = currentVideo {
|
||||||
ShareLink(item: video.shareURL) {
|
ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) {
|
||||||
Image(systemName: "square.and.arrow.up")
|
Image(systemName: "square.and.arrow.up")
|
||||||
.font(isTabAccessory ? .title3 : .title2)
|
.font(isTabAccessory ? .title3 : .title2)
|
||||||
.frame(width: 32, height: 32)
|
.frame(width: 32, height: 32)
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ struct MacOSControlsSectionRenderer: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var shareButton: some View {
|
private var shareButton: some View {
|
||||||
if let video = actions.currentVideo {
|
if let video = actions.currentVideo {
|
||||||
ShareLink(item: video.shareURL) {
|
ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) {
|
||||||
buttonLabel(systemImage: "square.and.arrow.up")
|
buttonLabel(systemImage: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
.buttonStyle(buttonStyleForContext)
|
.buttonStyle(buttonStyleForContext)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct LayoutNavigationSettingsView: View {
|
|||||||
#else
|
#else
|
||||||
VideoActionsSection(settings: settings)
|
VideoActionsSection(settings: settings)
|
||||||
LinkActionSection(settings: settings)
|
LinkActionSection(settings: settings)
|
||||||
|
SharingSection(settings: settings)
|
||||||
ClipboardSection(settings: settings)
|
ClipboardSection(settings: settings)
|
||||||
#endif
|
#endif
|
||||||
#if os(iOS)
|
#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
|
// MARK: - Clipboard Section
|
||||||
|
|
||||||
private struct ClipboardSection: View {
|
private struct ClipboardSection: View {
|
||||||
|
|||||||
@@ -1173,7 +1173,7 @@ struct VideoInfoView: View {
|
|||||||
|
|
||||||
// Share button
|
// Share button
|
||||||
if let video = displayedVideo {
|
if let video = displayedVideo {
|
||||||
ShareLink(item: video.shareURL) {
|
ShareLink(item: appEnvironment?.shareURL(for: video) ?? video.shareURL) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Label(String(localized: "video.share"), systemImage: "square.and.arrow.up")
|
Label(String(localized: "video.share"), systemImage: "square.and.arrow.up")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
|
|||||||
Reference in New Issue
Block a user