From 544dc70c5df3802502ce8cbd3b9f3aea0be29b32 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 27 Oct 2021 00:59:59 +0200 Subject: [PATCH] Share button --- Model/Accounts/Instance.swift | 9 ++++++ Model/Applications/VideosAPI.swift | 19 +++++++++++++ Model/Player/PlayerQueue.swift | 4 +-- Pearvidious.xcodeproj/project.pbxproj | 24 +++++++++++----- Shared/PearvidiousApp.swift | 1 + Shared/Player/VideoDetails.swift | 34 +++++++++++++++++----- Shared/Views/ChannelPlaylistView.swift | 28 ++++++++++++++++++ Shared/Views/ChannelVideosView.swift | 34 ++++++++++++++++++++-- Shared/Views/ShareButton.swift | 39 ++++++++++++++++++++++++++ iOS/ShareSheet.swift | 28 ++++++++++++++++++ macOS/AppDelegate.swift | 4 +++ 11 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 Shared/Views/ShareButton.swift create mode 100644 iOS/ShareSheet.swift diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index ac93e529..91de308a 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -74,6 +74,15 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { Account(instanceID: id, name: "Anonymous", url: url, anonymous: true) } + var urlComponents: URLComponents { + URLComponents(string: url)! + } + + var frontendHost: String { + // TODO: piped frontend link + urlComponents.host!.replacingOccurrences(of: "api", with: "") + } + func hash(into hasher: inout Hasher) { hasher.combine(url) } diff --git a/Model/Applications/VideosAPI.swift b/Model/Applications/VideosAPI.swift index ee0d08f6..25bed1d0 100644 --- a/Model/Applications/VideosAPI.swift +++ b/Model/Applications/VideosAPI.swift @@ -2,6 +2,7 @@ import Foundation import Siesta protocol VideosAPI { + var account: Account! { get } var signedIn: Bool { get } func channel(_ id: String) -> Resource @@ -25,6 +26,7 @@ protocol VideosAPI { func channelPlaylist(_ id: String) -> Resource? func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) + func shareURL(_ item: ContentItem) -> URL } extension VideosAPI { @@ -45,4 +47,21 @@ extension VideosAPI { completionHandler(newItem) } } + + func shareURL(_ item: ContentItem) -> URL { + var urlComponents = account.instance.urlComponents + urlComponents.host = account.instance.frontendHost + switch item.contentType { + case .video: + urlComponents.path = "/watch" + urlComponents.query = "v=\(item.video.videoID)" + case .channel: + urlComponents.path = "/channel/\(item.channel.id)" + case .playlist: + urlComponents.path = "/playlist" + urlComponents.query = "list=\(item.playlist.id)" + } + + return urlComponents.url! + } } diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index 05df0aec..f51032bf 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -84,7 +84,7 @@ extension PlayerModel { } @discardableResult func remove(_ item: PlayerQueueItem) -> PlayerQueueItem? { - if let index = queue.firstIndex(where: { $0 == item }) { + if let index = queue.firstIndex(where: { $0.videoID == item.videoID }) { return queue.remove(at: index) } @@ -138,7 +138,7 @@ extension PlayerModel { } func addItemToHistory(_ item: PlayerQueueItem) { - if let index = history.firstIndex(where: { $0.video.videoID == item.video?.videoID }) { + if let index = history.firstIndex(where: { $0.video?.videoID == item.video?.videoID }) { history.remove(at: index) } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index a1d81fa5..d1aca3f3 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -191,6 +191,9 @@ 377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; 377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7EC267A0A0800A6BBAF /* SwiftyJSON */; }; 377FC7F3267A0A0800A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7F2267A0A0800A6BBAF /* Logging */; }; + 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23A272894DA00B09468 /* ShareSheet.swift */; }; + 3784B23D2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; }; + 3784B23E2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; }; 3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; }; 3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; }; 3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; }; @@ -540,6 +543,8 @@ 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = ""; }; 37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = ""; }; + 3784B23A272894DA00B09468 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; + 3784B23C2728B85300B09468 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = ""; }; 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = ""; }; 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = ""; }; 378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; @@ -805,6 +810,7 @@ 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */, 37AAF27D26737323007FC770 /* PopularView.swift */, 37AAF27F26737550007FC770 /* SearchView.swift */, + 3784B23C2728B85300B09468 /* ShareButton.swift */, 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */, 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */, @@ -917,6 +923,7 @@ 37992DC826CC50CD003D4C27 /* iOS */ = { isa = PBXGroup; children = ( + 3784B23A272894DA00B09468 /* ShareSheet.swift */, 37992DC726CC50BC003D4C27 /* Info.plist */, ); path = iOS; @@ -1173,10 +1180,10 @@ isa = PBXNativeTarget; buildConfigurationList = 37D4B0EC2671614900C925CA /* Build configuration list for PBXNativeTarget "Pearvidious (iOS)" */; buildPhases = ( + 37CC3F48270CE89B00608308 /* ShellScript */, 37D4B0C52671614900C925CA /* Sources */, 37D4B0C62671614900C925CA /* Frameworks */, 37D4B0C72671614900C925CA /* Resources */, - 37CC3F48270CE89B00608308 /* ShellScript */, 37A3B1932725735F000FB5EE /* Embed App Extensions */, ); buildRules = ( @@ -1205,10 +1212,10 @@ isa = PBXNativeTarget; buildConfigurationList = 37D4B0EF2671614900C925CA /* Build configuration list for PBXNativeTarget "Pearvidious (macOS)" */; buildPhases = ( + 37CC3F4A270CE8D000608308 /* ShellScript */, 37D4B0CB2671614900C925CA /* Sources */, 37D4B0CC2671614900C925CA /* Frameworks */, 37D4B0CD2671614900C925CA /* Resources */, - 37CC3F4A270CE8D000608308 /* ShellScript */, 37A3B17127255E7F000FB5EE /* Embed App Extensions */, ); buildRules = ( @@ -1272,10 +1279,10 @@ isa = PBXNativeTarget; buildConfigurationList = 37D4B177267164B000C925CA /* Build configuration list for PBXNativeTarget "Pearvidious (tvOS)" */; buildPhases = ( + 37CC3F49270CE8CA00608308 /* ShellScript */, 37D4B154267164AE00C925CA /* Sources */, 37D4B155267164AE00C925CA /* Frameworks */, 37D4B156267164AE00C925CA /* Resources */, - 37CC3F49270CE8CA00608308 /* ShellScript */, ); buildRules = ( ); @@ -1498,7 +1505,7 @@ }; 37CC3F48270CE89B00608308 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 12; files = ( ); inputFileListPaths = ( @@ -1511,7 +1518,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "if test -d \"/opt/homebrew/bin/\"; then\n PATH=\"/opt/homebrew/bin/:${PATH}\"\nfi\n\nexport PATH\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; 37CC3F49270CE8CA00608308 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1528,7 +1535,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "if test -d \"/opt/homebrew/bin/\"; then\n PATH=\"/opt/homebrew/bin/:${PATH}\"\nfi\n\nexport PATH\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; 37CC3F4A270CE8D000608308 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1545,7 +1552,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "if test -d \"/opt/homebrew/bin/\"; then\n PATH=\"/opt/homebrew/bin/:${PATH}\"\nfi\n\nexport PATH\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; 37FD43EA2704A2350073EE42 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1608,6 +1615,7 @@ 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */, + 3784B23D2728B85300B09468 /* ShareButton.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */, 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, @@ -1677,6 +1685,7 @@ 3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */, 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, + 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */, 379775932689365600DD52A8 /* Array+Next.swift in Sources */, 37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, @@ -1799,6 +1808,7 @@ 37732FF52703D32400F04329 /* Sidebar.swift in Sources */, 379775942689365600DD52A8 /* Array+Next.swift in Sources */, 3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */, + 3784B23E2728B85300B09468 /* ShareButton.swift in Sources */, 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */, 37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */, 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, diff --git a/Shared/PearvidiousApp.swift b/Shared/PearvidiousApp.swift index a43edcc8..9fef158f 100644 --- a/Shared/PearvidiousApp.swift +++ b/Shared/PearvidiousApp.swift @@ -15,6 +15,7 @@ struct PearvidiousApp: App { .handlesExternalEvents(matching: Set(["*"])) .commands { SidebarCommands() + CommandGroup(replacing: .newItem, addition: {}) } #endif diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 7b6e5c86..cf1603c6 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -12,6 +12,7 @@ struct VideoDetails: View { @State private var subscribed = false @State private var confirmationShown = false @State private var presentingAddToPlaylist = false + @State private var presentingShareSheet = false @State private var currentPage = Page.details @@ -249,6 +250,11 @@ struct VideoDetails: View { Group { if let video = player.currentVideo { HStack { + ShareButton( + contentItem: ContentItem(video: video), + presentingShareSheet: $presentingShareSheet + ) + Spacer() if let views = video.viewsCount { @@ -269,14 +275,17 @@ struct VideoDetails: View { Spacer() - Button { - presentingAddToPlaylist = true - } label: { - Label("Add to Playlist", systemImage: "text.badge.plus") - .labelStyle(.iconOnly) - .help("Add to Playlist...") + if accounts.app.supportsUserPlaylists { + Button { + presentingAddToPlaylist = true + } label: { + Label("Add to Playlist", systemImage: "text.badge.plus") + .labelStyle(.iconOnly) + .help("Add to Playlist...") + } + .buttonStyle(.plain) + .foregroundColor(.blue) } - .buttonStyle(.plain) } .frame(maxHeight: 35) .foregroundColor(.secondary) @@ -287,6 +296,17 @@ struct VideoDetails: View { AddToPlaylistView(video: video) } } + #if os(iOS) + .sheet(isPresented: $presentingShareSheet) { + ShareSheet(activityItems: [ + accounts.api.shareURL(contentItem) + ]) + } + #endif + } + + private var contentItem: ContentItem { + ContentItem(video: player.currentVideo!) } var detailsPage: some View { diff --git a/Shared/Views/ChannelPlaylistView.swift b/Shared/Views/ChannelPlaylistView.swift index ad8521aa..6401f975 100644 --- a/Shared/Views/ChannelPlaylistView.swift +++ b/Shared/Views/ChannelPlaylistView.swift @@ -4,6 +4,8 @@ import SwiftUI struct ChannelPlaylistView: View { var playlist: ChannelPlaylist + @State private var presentingShareSheet = false + @StateObject private var store = Store() @Environment(\.dismiss) private var dismiss @@ -44,12 +46,26 @@ struct ChannelPlaylistView: View { #endif VerticalCells(items: items) } + #if os(iOS) + .sheet(isPresented: $presentingShareSheet) { + ShareSheet(activityItems: [ + accounts.api.shareURL(contentItem) + ]) + } + #endif .onAppear { resource?.addObserver(store) resource?.loadIfNeeded() } #if !os(tvOS) .toolbar { + ToolbarItem(placement: shareButtonPlacement) { + ShareButton( + contentItem: contentItem, + presentingShareSheet: $presentingShareSheet + ) + } + ToolbarItem(placement: .cancellationAction) { if inNavigationView { Button("Done") { @@ -64,6 +80,18 @@ struct ChannelPlaylistView: View { .background(.thickMaterial) #endif } + + private var shareButtonPlacement: ToolbarItemPlacement { + #if os(iOS) + .navigation + #else + .automatic + #endif + } + + private var contentItem: ContentItem { + ContentItem(playlist: playlist) + } } struct ChannelPlaylistView_Previews: PreviewProvider { diff --git a/Shared/Views/ChannelVideosView.swift b/Shared/Views/ChannelVideosView.swift index 55356923..c3d39479 100644 --- a/Shared/Views/ChannelVideosView.swift +++ b/Shared/Views/ChannelVideosView.swift @@ -4,6 +4,8 @@ import SwiftUI struct ChannelVideosView: View { let channel: Channel + @State private var presentingShareSheet = false + @StateObject private var store = Store() @Environment(\.dismiss) private var dismiss @@ -70,6 +72,13 @@ struct ChannelVideosView: View { #endif #if !os(tvOS) .toolbar { + ToolbarItem(placement: shareButtonPlacement) { + ShareButton( + contentItem: contentItem, + presentingShareSheet: $presentingShareSheet + ) + } + ToolbarItem { HStack { Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers") @@ -91,6 +100,13 @@ struct ChannelVideosView: View { #else .background(.thickMaterial) #endif + #if os(iOS) + .sheet(isPresented: $presentingShareSheet) { + ShareSheet(activityItems: [ + accounts.api.shareURL(contentItem) + ]) + } + #endif .modifier(UnsubscribeAlertModifier()) .onAppear { if store.item.isNil { @@ -101,14 +117,14 @@ struct ChannelVideosView: View { .navigationTitle(navigationTitle) } - var resource: Resource { + private var resource: Resource { let resource = accounts.api.channel(channel.id) resource.addObserver(store) return resource } - var subscriptionToggleButton: some View { + private var subscriptionToggleButton: some View { Group { if accounts.app.supportsSubscriptions && accounts.signedIn { if subscriptions.isSubscribing(channel.id) { @@ -126,7 +142,19 @@ struct ChannelVideosView: View { } } - var navigationTitle: String { + private var shareButtonPlacement: ToolbarItemPlacement { + #if os(iOS) + .navigation + #else + .automatic + #endif + } + + private var contentItem: ContentItem { + ContentItem(channel: channel) + } + + private var navigationTitle: String { store.item?.name ?? channel.name } } diff --git a/Shared/Views/ShareButton.swift b/Shared/Views/ShareButton.swift new file mode 100644 index 00000000..cec62a27 --- /dev/null +++ b/Shared/Views/ShareButton.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct ShareButton: View { + let contentItem: ContentItem + @Binding var presentingShareSheet: Bool + + @EnvironmentObject private var accounts + + var body: some View { + Button { + #if os(iOS) + presentingShareSheet = true + #else + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(shareURL, forType: .string) + #endif + } label: { + #if os(iOS) + Label("Share", systemImage: "square.and.arrow.up") + #else + EmptyView() + #endif + } + .keyboardShortcut("c") + .foregroundColor(.blue) + .buttonStyle(.plain) + .labelStyle(.iconOnly) + } + + private var shareURL: String { + accounts.api.shareURL(contentItem).absoluteString + } +} + +struct ShareButton_Previews: PreviewProvider { + static var previews: some View { + ShareButton(contentItem: ContentItem(video: Video.fixture), presentingShareSheet: .constant(false)) + } +} diff --git a/iOS/ShareSheet.swift b/iOS/ShareSheet.swift new file mode 100644 index 00000000..59cc6737 --- /dev/null +++ b/iOS/ShareSheet.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftUI + +struct ShareSheet: UIViewControllerRepresentable { + typealias Callback = (_ activityType: UIActivity.ActivityType?, + _ completed: Bool, + _ returnedItems: [Any]?, + _ error: Error?) -> Void + + let activityItems: [Any] + let applicationActivities = [UIActivity]() + let excludedActivityTypes = [UIActivity.ActivityType]() + let callback: Callback? = nil + + func makeUIViewController(context _: Context) -> UIActivityViewController { + let controller = UIActivityViewController( + activityItems: activityItems, + applicationActivities: applicationActivities + ) + + controller.excludedActivityTypes = excludedActivityTypes + controller.completionWithItemsHandler = callback + + return controller + } + + func updateUIViewController(_: UIActivityViewController, context _: Context) {} +} diff --git a/macOS/AppDelegate.swift b/macOS/AppDelegate.swift index b89b9573..2bd3b2ef 100644 --- a/macOS/AppDelegate.swift +++ b/macOS/AppDelegate.swift @@ -6,6 +6,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { true } + func applicationWillFinishLaunching(_: Notification) { + NSWindow.allowsAutomaticWindowTabbing = false + } + func applicationWillTerminate(_: Notification) { ScreenSaverManager.shared.enable() }