From b92d66e26eeb7dff0b8fd245ed3eeca1a7222147 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 22 Sep 2021 05:28:14 +0900 Subject: [PATCH] Support DeepLink jellyfin://Users/{UserID}/Items/{ItemID} --- JellyfinPlayer.xcodeproj/project.pbxproj | 12 ++++++ .../Coordinators/MainCoordinator.swift | 14 +++++++ .../Coordinators/MainTabCoordinator.swift | 9 +++++ JellyfinPlayer/DeepLink.swift | 26 +++++++++++++ .../Singleton/AppURLHandler.swift | 39 +++++++++++++++---- 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 JellyfinPlayer/DeepLink.swift rename {Shared => JellyfinPlayer}/Singleton/AppURLHandler.swift (62%) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 84a6349b..fcb469da 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -249,6 +249,7 @@ 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; + 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; @@ -483,6 +484,7 @@ 62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = ""; }; 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; + 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = ""; }; D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = ""; }; @@ -771,6 +773,7 @@ children = ( 62C29E9D26D0FE5900C1D2E7 /* Coordinators */, 53F866422687A45400DCD1D7 /* Components */, + 62ECA01926FA6D6900E8EBB7 /* Singleton */, 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, @@ -796,6 +799,7 @@ 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */, + 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */, ); path = JellyfinPlayer; sourceTree = ""; @@ -1019,6 +1023,13 @@ 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, 62EC352E267666A5000E9F2D /* SessionManager.swift */, + ); + path = Singleton; + sourceTree = ""; + }; + 62ECA01926FA6D6900E8EBB7 /* Singleton */ = { + isa = PBXGroup; + children = ( 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */, ); path = Singleton; @@ -1587,6 +1598,7 @@ E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */, + 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */, diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift index 7b76e2fd..aa00abd9 100644 --- a/JellyfinPlayer/Coordinators/MainCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainCoordinator.swift @@ -39,6 +39,7 @@ import SwiftUI let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil) nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil) + nc.addObserver(self, selector: #selector(processDeepLink), name: Notification.Name("processDeepLink"), object: nil) } @objc func didLogIn() { @@ -51,6 +52,19 @@ import SwiftUI root(\.connectToServer) } + @objc func processDeepLink(_ notification: Notification) { + guard let deepLink = notification.object as? DeepLink else { return } + if let coordinator = hasRoot(\.mainTab) { + switch deepLink { + case let .item(item): + coordinator.focusFirst(\.home) + .child + .popToRoot() + .route(to: \.item, item) + } + } + } + func makeMainTab() -> MainTabCoordinator { MainTabCoordinator() } diff --git a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift index ab91d28d..d5f430ab 100644 --- a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift @@ -38,4 +38,13 @@ final class MainTabCoordinator: TabCoordinatable { Image(systemName: "folder") Text("All Media") } + + @ViewBuilder func customize(_ view: AnyView) -> some View { + view.onAppear { + AppURLHandler.shared.appURLState = .allowed + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + AppURLHandler.shared.processLaunchedURLIfNeeded() + } + } + } } diff --git a/JellyfinPlayer/DeepLink.swift b/JellyfinPlayer/DeepLink.swift new file mode 100644 index 00000000..94f15232 --- /dev/null +++ b/JellyfinPlayer/DeepLink.swift @@ -0,0 +1,26 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +enum DeepLinkError: LocalizedError { + case general + + var errorDescription: String? { + switch self { + case .general: + return "Couldn't create deep link" + } + } +} + +enum DeepLink { + case item(BaseItemDto) +} diff --git a/Shared/Singleton/AppURLHandler.swift b/JellyfinPlayer/Singleton/AppURLHandler.swift similarity index 62% rename from Shared/Singleton/AppURLHandler.swift rename to JellyfinPlayer/Singleton/AppURLHandler.swift index fb95b08e..9dcb5eaf 100644 --- a/Shared/Singleton/AppURLHandler.swift +++ b/JellyfinPlayer/Singleton/AppURLHandler.swift @@ -7,15 +7,14 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Combine import Foundation +import JellyfinAPI import Stinsen final class AppURLHandler { static let deepLinkScheme = "jellyfin" - @RouterObject - var router: HomeCoordinator.Router? - enum AppURLState { case launched case allowedInLogin @@ -35,6 +34,8 @@ final class AppURLHandler { static let shared = AppURLHandler() + var cancellables = Set() + var appURLState: AppURLState = .launched var launchURL: URL? } @@ -46,9 +47,7 @@ extension AppURLHandler { return false } if AppURLHandler.shared.appURLState.allowedScheme(with: url) { - if launchURL == nil { - return processURL(url) - } + return processURL(url) } else { launchURL = url } @@ -56,7 +55,8 @@ extension AppURLHandler { } func processLaunchedURLIfNeeded() { - guard let launchURL = launchURL else { return } + guard let launchURL = launchURL, + !launchURL.absoluteString.isEmpty else { return } if processDeepLink(url: launchURL) { self.launchURL = nil } @@ -76,12 +76,35 @@ extension AppURLHandler { // /Users/{UserID}/Items/{ItemID} if url.pathComponents[safe: 2]?.lowercased() == "items", + let userID = url.pathComponents[safe: 1], let itemID = url.pathComponents[safe: 3] { -// router?.route(to: \.item(item: item)) + // It would be nice if the ItemViewModel could be initialized to id later. + getItem(userID: userID, itemID: itemID) { item in + guard let item = item else { return } + NotificationCenter.default.post(name: Notification.Name("processDeepLink"), object: DeepLink.item(item)) + } + return true } return false } } + +extension AppURLHandler { + func getItem(userID: String, itemID: String, completion: @escaping (BaseItemDto?) -> Void) { + UserLibraryAPI.getItem(userId: userID, itemId: itemID) + .sink(receiveCompletion: { innerCompletion in + switch innerCompletion { + case .failure: + completion(nil) + default: + break + } + }, receiveValue: { item in + completion(item) + }) + .store(in: &cancellables) + } +}