From 5d96de329f494a28bb03c2339ca346463ca4cc80 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 20:15:57 +0900 Subject: [PATCH] Temporarily process deep link from HomeView --- JellyfinPlayer.xcodeproj/project.pbxproj | 4 + JellyfinPlayer/ConnectToServerView.swift | 3 + JellyfinPlayer/HomeView.swift | 14 +- JellyfinPlayer/Info.plist | 11 ++ JellyfinPlayer/JellyfinPlayerApp.swift | 3 + Shared/Singleton/AppURLHandler.swift | 102 +++++++++++ .../ViewModels/ConnectToServerViewModel.swift | 4 + Shared/ViewModels/HomeViewModel.swift | 1 - WidgetExtension/NextUpWidget.swift | 161 +++++++++--------- 9 files changed, 222 insertions(+), 81 deletions(-) create mode 100644 Shared/Singleton/AppURLHandler.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 85fc1d2e..1e32bbae 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -189,6 +189,7 @@ 6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 6220D0C826D63F3700B8E046 /* Stinsen */; }; 6220D0CA26D63F4D00B8E046 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; }; + 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; @@ -430,6 +431,7 @@ 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = ""; }; 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = ""; }; + 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; 625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; @@ -997,6 +999,7 @@ 62EC352E267666A5000E9F2D /* SessionManager.swift */, 536D3D73267BA8170004248C /* BackgroundManager.swift */, 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, + 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */, ); path = Singleton; sourceTree = ""; @@ -1513,6 +1516,7 @@ 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, + 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 5de1acd2..552a5779 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -177,5 +177,8 @@ struct ConnectToServerView: View { dismissButton: .cancel()) } .navigationTitle(NSLocalizedString("Connect to Server", comment: "")) + .onAppear { + AppURLHandler.shared.appURLState = .allowedInLogin + } } } diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index b6c7b897..cd580b91 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -8,8 +8,8 @@ */ import Foundation -import SwiftUI import Stinsen +import SwiftUI struct HomeView: View { @EnvironmentObject var homeRouter: NavigationRouter @@ -37,7 +37,9 @@ struct HomeView: View { .fontWeight(.bold) Spacer() Button { - homeRouter.route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")) + homeRouter + .route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), + title: library?.name ?? "")) } label: { HStack { Text("See All").font(.subheadline).fontWeight(.bold) @@ -45,7 +47,7 @@ struct HomeView: View { } } }.padding(.leading, 16) - .padding(.trailing, 16) + .padding(.trailing, 16) LatestMediaView(viewModel: .init(libraryID: libraryID)) } } @@ -67,5 +69,11 @@ struct HomeView: View { } } } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + AppURLHandler.shared.appURLState = .allowed + AppURLHandler.shared.processLaunchedURLIfNeeded() + } + } } } diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index 5121b455..b8c7ecaf 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -18,6 +18,17 @@ $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + jellyfin + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index bad2fc34..cb7d08c9 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -234,6 +234,9 @@ struct JellyfinPlayerApp: App { .onShake { EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) } + .onOpenURL { url in + AppURLHandler.shared.processDeepLink(url: url) + } } } diff --git a/Shared/Singleton/AppURLHandler.swift b/Shared/Singleton/AppURLHandler.swift new file mode 100644 index 00000000..33805a37 --- /dev/null +++ b/Shared/Singleton/AppURLHandler.swift @@ -0,0 +1,102 @@ +// +/* + * 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 Stinsen +import URLNavigator + +final class AppURLHandler { + static let deepLinkScheme = "jellyfin" + + @RouterObject + var router: NavigationRouter? + + enum AppURLState { + case launched + case allowedInLogin + case allowed + + func allowedScheme(with url: URL) -> Bool { + switch self { + case .launched: + return false + case .allowed: + return true + case .allowedInLogin: + return false + } + } + } + + static let shared = AppURLHandler() + + var appURLState: AppURLState = .launched + var launchURL: URL? +} + +extension AppURLHandler { + @discardableResult + func processDeepLink(url: URL) -> Bool { + guard url.scheme == Self.deepLinkScheme || url.scheme == "widget-extension" else { + return false + } + print(AppURLHandler.shared.appURLState.allowedScheme(with: url)) + if AppURLHandler.shared.appURLState.allowedScheme(with: url) { + if launchURL == nil { + return processURL(url) + } + } else { + launchURL = url + } + return true + } + + func processLaunchedURLIfNeeded() { + print("!@#!@#!@#!@#!@#!@") + print(launchURL) + guard let launchURL = launchURL else { return } + if processDeepLink(url: launchURL) { + self.launchURL = nil + } + } + + private func processURL(_ url: URL) -> Bool { + print("processURL(_ url: URL) -> Bool") + if processURLForUser(url: url) { + return true + } + + return false + } + + private func processURLForUser(url: URL) -> Bool { + print("processURLForUser(_ url: URL) -> Bool") + print(url) + print(url.host) + print(url.path) + print(url.pathComponents) + print(url.pathComponents[safe: 0]) + print(url.pathComponents[safe: 1]) + print(url.pathComponents[safe: 2]) + print(url.pathComponents[safe: 3]) + guard url.host?.lowercased() == "users", + url.pathComponents[safe: 1]?.isEmpty == false else { return false } + + // /Users/{UserID}/Items/{ItemID} + if url.pathComponents[safe: 2]?.lowercased() == "items", + let itemID = url.pathComponents[safe: 3] + { + print("Passed!@#") + router?.route(to: .item(viewModel: .init(id: itemID))) + return true + } + + return false + } +} diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index d3cf7794..06d8ad54 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -29,6 +29,10 @@ final class ConnectToServerViewModel: ViewModel { private let discovery = ServerDiscovery() @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] @Published var searching = false + + override init() { + super.init() + } func getPublicUsers() { if ServerEnvironment.current.server != nil { diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 44571bbf..0f98e7a4 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -24,7 +24,6 @@ final class HomeViewModel: ViewModel { override init() { super.init() - refresh() } diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index 26f91c61..fbe1930a 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -142,14 +142,14 @@ struct NextUpEntryView: View { .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) } else { switch family { - case .systemSmall: - small(item: entry.items.first) - case .systemMedium: - medium(items: entry.items) - case .systemLarge: - large(items: entry.items) - default: - EmptyView() + case .systemSmall: + small(item: entry.items.first) + case .systemMedium: + medium(items: entry.items) + case .systemLarge: + large(items: entry.items) + default: + EmptyView() } } } @@ -198,51 +198,55 @@ extension NextUpEntryView { } func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View { - VStack(alignment: .leading) { - if let image = item.1 { - Image(uiImage: image) - .resizable() - .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) - .clipped() - .cornerRadius(8) - .shadow(radius: 8) - } - Text(item.0.seriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - } - } - - func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { - HStack(spacing: 20) { - if let image = item.1 { - Image(uiImage: image) - .resizable() - .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) - .clipped() - .cornerRadius(8) - .shadow(radius: 8) - } - VStack(alignment: .leading, spacing: 8) { + Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: { + VStack(alignment: .leading) { + if let image = item.1 { + Image(uiImage: image) + .resizable() + .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) + .clipped() + .cornerRadius(8) + .shadow(radius: 8) + } Text(item.0.seriesName ?? "") .font(.caption) .fontWeight(.semibold) .foregroundColor(.primary) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .lineLimit(1) Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)") .font(.caption) .fontWeight(.semibold) .foregroundColor(.secondary) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .lineLimit(1) } - } + }) + } + + func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { + Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: { + HStack(spacing: 20) { + if let image = item.1 { + Image(uiImage: image) + .resizable() + .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) + .clipped() + .cornerRadius(8) + .shadow(radius: 8) + } + VStack(alignment: .leading, spacing: 8) { + Text(item.0.seriesName ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } + }) } } @@ -281,33 +285,36 @@ extension NextUpEntryView { func large(items: [(BaseItemDto, UIImage?)]) -> some View { VStack(spacing: 0) { if let firstItem = items[safe: 0] { - ZStack(alignment: .topTrailing) { - ZStack(alignment: .bottomLeading) { - if let image = firstItem.1 { - Image(uiImage: image) - .centerCropped() - .innerShadow(color: Color.black.opacity(0.5), radius: 0.5) - } - VStack(alignment: .leading, spacing: 8) { - Text(firstItem.0.seriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - Text("\(firstItem.0.name ?? "") · S\(firstItem.0.parentIndexNumber ?? 0):E\(firstItem.0.indexNumber ?? 0)") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.gray) - .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - } - .shadow(radius: 8) - .padding(12) - } - headerSymbol - .padding(12) - } - .clipped() - .shadow(radius: 8) + Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(firstItem.0.id!)")!, + label: { + ZStack(alignment: .topTrailing) { + ZStack(alignment: .bottomLeading) { + if let image = firstItem.1 { + Image(uiImage: image) + .centerCropped() + .innerShadow(color: Color.black.opacity(0.5), radius: 0.5) + } + VStack(alignment: .leading, spacing: 8) { + Text(firstItem.0.seriesName ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + Text("\(firstItem.0.name ?? "") · S\(firstItem.0.parentIndexNumber ?? 0):E\(firstItem.0.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.gray) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + .shadow(radius: 8) + .padding(12) + } + headerSymbol + .padding(12) + } + .clipped() + .shadow(radius: 8) + }) } VStack(spacing: 8) { if let secondItem = items[safe: 1] { @@ -354,7 +361,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol")) + UIImage(named: "WidgetHeaderSymbol")), ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemMedium)) @@ -365,7 +372,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), - UIImage(named: "WidgetHeaderSymbol")) + UIImage(named: "WidgetHeaderSymbol")), ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemLarge)) @@ -380,7 +387,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol")) + UIImage(named: "WidgetHeaderSymbol")), ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemMedium)) @@ -392,7 +399,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), - UIImage(named: "WidgetHeaderSymbol")) + UIImage(named: "WidgetHeaderSymbol")), ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemLarge)) @@ -405,7 +412,7 @@ struct NextUpWidget_Previews: PreviewProvider { NextUpEntryView(entry: .init(date: Date(), items: [ (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), - UIImage(named: "WidgetHeaderSymbol")) + UIImage(named: "WidgetHeaderSymbol")), ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemMedium)) @@ -415,7 +422,7 @@ struct NextUpWidget_Previews: PreviewProvider { (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), UIImage(named: "WidgetHeaderSymbol")), (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), - UIImage(named: "WidgetHeaderSymbol")) + UIImage(named: "WidgetHeaderSymbol")), ], error: nil)) .previewContext(WidgetPreviewContext(family: .systemLarge))