From 3a090aaf4e5f0ec1b165e8587f76d0771aecee0b Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 21 Aug 2021 19:55:32 +0900 Subject: [PATCH 01/18] Add Stinsen Add ConnectToServerCoodinator Add HomeCoordinator Add LibraryListCoordinator Add MainCoordinator Add MainTabCoordinator --- JellyfinPlayer.xcodeproj/project.pbxproj | 49 ++++++++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 9 ++++ JellyfinPlayer/ConnectToServerView.swift | 3 ++ .../ConnectToServerCoodinator.swift | 25 ++++++++++ .../Coordinators/HomeCoordinator.swift | 25 ++++++++++ .../Coordinators/LibraryListCoordinator.swift | 25 ++++++++++ .../Coordinators/MainCoordinator.swift | 35 +++++++++++++ .../Coordinators/MainTabCoordinator.swift | 48 ++++++++++++++++++ JellyfinPlayer/HomeView.swift | 2 +- JellyfinPlayer/JellyfinPlayerApp.swift | 50 ++++++++++--------- JellyfinPlayer/MainTabView.swift | 46 ----------------- JellyfinPlayer/SettingsView.swift | 43 ++++++++-------- JellyfinPlayer/SplashView.swift | 16 +++--- JellyfinPlayer/VideoPlayer.swift | 5 ++ .../ViewModels/ConnectToServerViewModel.swift | 30 +++++++---- 15 files changed, 297 insertions(+), 114 deletions(-) create mode 100644 JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift create mode 100644 JellyfinPlayer/Coordinators/HomeCoordinator.swift create mode 100644 JellyfinPlayer/Coordinators/LibraryListCoordinator.swift create mode 100644 JellyfinPlayer/Coordinators/MainCoordinator.swift create mode 100644 JellyfinPlayer/Coordinators/MainTabCoordinator.swift delete mode 100644 JellyfinPlayer/MainTabView.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 1bb21ac6..f053e3ff 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -141,7 +141,6 @@ 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; }; 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; - 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; }; 625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; }; 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; @@ -166,6 +165,12 @@ 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; }; 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; + 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 62C29E9B26D0FE4200C1D2E7 /* Stinsen */; }; + 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; }; + 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */; }; + 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; + 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; + 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; }; 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; }; 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; }; 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; @@ -352,7 +357,6 @@ 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; 625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; - 625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; 625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; @@ -369,6 +373,11 @@ 628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = ""; }; 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; + 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; + 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabCoordinator.swift; sourceTree = ""; }; + 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = ""; }; + 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; + 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = ""; }; 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = ""; }; 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; @@ -419,6 +428,7 @@ buildActionMask = 2147483647; files = ( 53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */, + 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, @@ -632,6 +642,7 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( + 62C29E9D26D0FE5900C1D2E7 /* Coordinators */, 53F866422687A45400DCD1D7 /* Components */, 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, @@ -660,7 +671,6 @@ 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */, - 625CB56B2678C0FD00530A6E /* MainTabView.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */, ); path = JellyfinPlayer; @@ -746,6 +756,18 @@ path = WidgetExtension; sourceTree = ""; }; + 62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */, + 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */, + 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, + 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, + 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, + ); + path = Coordinators; + sourceTree = ""; + }; 62EC352A26766657000E9F2D /* Singleton */ = { isa = PBXGroup; children = ( @@ -850,6 +872,7 @@ 62CB3F452685BAF7003D0A6F /* Defaults */, 53649AAC269CFAEA00A2D8B7 /* Puppy */, 6260FFF826A09754003FA968 /* CombineExt */, + 62C29E9B26D0FE4200C1D2E7 /* Stinsen */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -925,6 +948,7 @@ 53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */, 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */, 6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */, + 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -1165,11 +1189,14 @@ 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, + 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */, 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, + 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, + 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, @@ -1180,6 +1207,7 @@ 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, + 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */, 0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, @@ -1204,6 +1232,7 @@ 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, + 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, @@ -1216,7 +1245,6 @@ 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, - 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, @@ -1718,6 +1746,14 @@ kind = branch; }; }; + 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/rundfunk47/stinsen"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; + }; + }; 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/acvigue/Defaults"; @@ -1844,6 +1880,11 @@ package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; + 62C29E9B26D0FE4200C1D2E7 /* Stinsen */ = { + isa = XCSwiftPackageProductDependency; + package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */; + productName = Stinsen; + }; 62CB3F452685BAF7003D0A6F /* Defaults */ = { isa = XCSwiftPackageProductDependency; package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 462ac202..1a18b422 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -109,6 +109,15 @@ "version": "0.3.0" } }, + { + "package": "Stinsen", + "repositoryURL": "https://github.com/rundfunk47/stinsen", + "state": { + "branch": null, + "revision": "e72c20b2c4bde0d6c3a911d4eda688fee7aa3bba", + "version": "1.1.0" + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 0a565880..71f0caeb 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -6,8 +6,10 @@ */ import SwiftUI +import Stinsen struct ConnectToServerView: View { + @EnvironmentObject var main: ViewRouter @StateObject var viewModel = ConnectToServerViewModel() @State var username = "" @State var password = "" @@ -59,6 +61,7 @@ struct ConnectToServerView: View { if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { let user = SessionManager.current.getSavedSession(userID: publicUser.id!) SessionManager.current.loginWithSavedSession(user: user) + main.route(to: .mainTab) } else { username = publicUser.name ?? "" viewModel.selectedPublicUser = publicUser diff --git a/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift b/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift new file mode 100644 index 00000000..ec5e712b --- /dev/null +++ b/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift @@ -0,0 +1,25 @@ +// +/* + * 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 SwiftUI + +final class ConnectToServerCoodinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + ConnectToServerView() + } +} diff --git a/JellyfinPlayer/Coordinators/HomeCoordinator.swift b/JellyfinPlayer/Coordinators/HomeCoordinator.swift new file mode 100644 index 00000000..4b0fbeb8 --- /dev/null +++ b/JellyfinPlayer/Coordinators/HomeCoordinator.swift @@ -0,0 +1,25 @@ +// +/* + * 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 SwiftUI + +final class HomeCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + HomeView() + } +} diff --git a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift b/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift new file mode 100644 index 00000000..3bbf0728 --- /dev/null +++ b/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift @@ -0,0 +1,25 @@ +// +/* + * 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 SwiftUI + +final class LibraryListCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + LibraryListView() + } +} diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift new file mode 100644 index 00000000..af41d6de --- /dev/null +++ b/JellyfinPlayer/Coordinators/MainCoordinator.swift @@ -0,0 +1,35 @@ +// +/* + * 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 SwiftUI + +final class MainCoordinator: ViewCoordinatable { + var children = ViewChild() + + enum Route: ViewRoute { + case mainTab + case connectToServer + } + + func resolveRoute(route: Route) -> AnyCoordinatable { + switch route { + case .mainTab: + return MainTabCoordinator().eraseToAnyCoordinatable() + case .connectToServer: + return NavigationViewCoordinator(ConnectToServerCoodinator()).eraseToAnyCoordinatable() + } + } + + @ViewBuilder + func start() -> some View { + SplashView() + } +} diff --git a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift new file mode 100644 index 00000000..e9a825de --- /dev/null +++ b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift @@ -0,0 +1,48 @@ +// +/* + * 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 SwiftUI + +import Stinsen + +final class MainTabCoordinator: TabCoordinatable { + lazy var children = TabChild(self, tabRoutes: [.home, .allMedia]) + + enum Route: TabRoute { + case home + case allMedia + } + + func tabItem(forTab tab: Int) -> some View { + switch tab { + case 0: + Group { + Text("Home") + Image(systemName: "house") + } + case 1: + Group { + Text("Projects") + Image(systemName: "folder") + } + default: + fatalError() + } + } + + func resolveRoute(route: Route) -> AnyCoordinatable { + switch route { + case .home: + return NavigationViewCoordinator(HomeCoordinator()).eraseToAnyCoordinatable() + case .allMedia: + return NavigationViewCoordinator(LibraryListCoordinator()).eraseToAnyCoordinatable() + } + } +} diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index 72e282ac..bc80d7a2 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -20,7 +20,7 @@ struct HomeView: View { ProgressView() } else { ScrollView { - VStack(alignment: .leading) { + LazyVStack(alignment: .leading) { if !viewModel.resumeItems.isEmpty { ContinueWatchingView(items: viewModel.resumeItems) } diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index fd5bcf3e..bad2fc34 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -5,9 +5,10 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI -import MessageUI import Defaults +import MessageUI +import Stinsen +import SwiftUI // The notification we'll send when a shake gesture happens. extension UIDevice { @@ -16,11 +17,11 @@ extension UIDevice { // Override the default behavior of shake gestures to send our notification instead. extension UIWindow { - open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { + override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { if motion == .motionShake { NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil) } - } + } } // A view modifier that detects shaking and calls a function of our choosing. @@ -39,20 +40,20 @@ struct DeviceShakeViewModifier: ViewModifier { // A View extension to make the modifier easier to use. extension View { func onShake(perform action: @escaping () -> Void) -> some View { - self.modifier(DeviceShakeViewModifier(action: action)) + modifier(DeviceShakeViewModifier(action: action)) } } extension UIDevice { var hasNotch: Bool { - let bottom = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.safeAreaInsets.bottom ?? 0 + let bottom = UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.safeAreaInsets.bottom ?? 0 return bottom > 0 } } extension View { func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View { - self.background(HostingWindowFinder(callback: callback)) + background(HostingWindowFinder(callback: callback)) } } @@ -67,8 +68,7 @@ struct HostingWindowFinder: UIViewRepresentable { return view } - func updateUIView(_ uiView: UIView, context: Context) { - } + func updateUIView(_ uiView: UIView, context: Context) {} } struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey { @@ -105,18 +105,17 @@ class PreferenceUIHostingController: UIHostingController { init(wrappedView: V) { let box = Box() super.init(rootView: AnyView(wrappedView - .onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { - box.value?._prefersHomeIndicatorAutoHidden = $0 - }.onPreferenceChange(SupportedOrientationsPreferenceKey.self) { - box.value?._orientations = $0 - }.onPreferenceChange(ViewPreferenceKey.self) { - box.value?._viewPreference = $0 - } - )) + .onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { + box.value?._prefersHomeIndicatorAutoHidden = $0 + }.onPreferenceChange(SupportedOrientationsPreferenceKey.self) { + box.value?._orientations = $0 + }.onPreferenceChange(ViewPreferenceKey.self) { + box.value?._viewPreference = $0 + })) box.value = self } - @objc required dynamic init?(coder aDecoder: NSCoder) { + @objc dynamic required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) super.modalPresentationStyle = .fullScreen } @@ -131,6 +130,7 @@ class PreferenceUIHostingController: UIHostingController { public var _prefersHomeIndicatorAutoHidden = false { didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } } + override var prefersHomeIndicatorAutoHidden: Bool { _prefersHomeIndicatorAutoHidden } @@ -146,6 +146,7 @@ class PreferenceUIHostingController: UIHostingController { } } } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { _orientations } @@ -176,7 +177,7 @@ extension View { class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { public static let shared = EmailHelper() - private override init() { + override private init() { // } @@ -192,7 +193,9 @@ class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { let data = fileManager.contents(atPath: logURL.path) picker.setSubject("[DEV-BUG] SwiftFin") - picker.setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n", isHTML: false) + picker + .setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n", + isHTML: false) picker.setToRecipients(["SwiftFin Bug Reports "]) picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent) picker.mailComposeDelegate = self @@ -218,13 +221,15 @@ struct JellyfinPlayerApp: App { var body: some Scene { WindowGroup { - SplashView() + EmptyView() .environment(\.managedObjectContext, persistenceController.container.viewContext) .onAppear(perform: { setupAppearance() }) .withHostingWindow { window in - window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext)) + window? + .rootViewController = PreferenceUIHostingController(wrappedView: CoordinatorView(MainCoordinator()) + .environment(\.managedObjectContext, persistenceController.container.viewContext)) } .onShake { EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL()) @@ -239,7 +244,6 @@ struct JellyfinPlayerApp: App { } class AppDelegate: NSObject, UIApplicationDelegate { - static var orientationLock = UIInterfaceOrientationMask.all func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { diff --git a/JellyfinPlayer/MainTabView.swift b/JellyfinPlayer/MainTabView.swift deleted file mode 100644 index fabf9b8c..00000000 --- a/JellyfinPlayer/MainTabView.swift +++ /dev/null @@ -1,46 +0,0 @@ -// - /* - * 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 SwiftUI - -struct MainTabView: View { - @State private var tabSelection: Tab = .home - - var body: some View { - TabView(selection: $tabSelection) { - NavigationView { - HomeView() - } - .navigationViewStyle(StackNavigationViewStyle()) - .tabItem { - Text("Home") - Image(systemName: "house") - } - .tag(Tab.home) - NavigationView { - LibraryListView() - } - .navigationViewStyle(StackNavigationViewStyle()) - .tabItem { - Text("All Media") - Image(systemName: "folder") - } - .tag(Tab.allMedia) - } - } -} - -extension MainTabView { - - enum Tab: String { - case home - case allMedia - } -} diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 5637d055..6abdb076 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -6,10 +6,12 @@ */ import CoreData -import SwiftUI import Defaults +import Stinsen +import SwiftUI struct SettingsView: View { + @EnvironmentObject var main: ViewRouter @Environment(\.managedObjectContext) private var viewContext @ObservedObject var viewModel: SettingsViewModel @@ -44,13 +46,13 @@ struct SettingsView: View { Text(bitrate.name).tag(bitrate.value) } } - + Picker("Jump Forward Length", selection: $jumpForwardLength) { ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in Text(length.label).tag(length.rawValue) } } - + Picker("Jump Backward Length", selection: $jumpBackwardLength) { ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in Text(length.label).tag(length.rawValue) @@ -63,19 +65,22 @@ struct SettingsView: View { SearchablePicker(label: "Preferred subtitle language", options: viewModel.langs, optionToString: { $0.name }, - selected: Binding( - get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, - set: {autoSelectSubtitlesLangcode = $0.isoCode} - ) - ) + selected: Binding(get: { + viewModel.langs + .first(where: { $0.isoCode == autoSelectSubtitlesLangcode + }) ?? + .auto + }, + set: { autoSelectSubtitlesLangcode = $0.isoCode })) SearchablePicker(label: "Preferred audio language", options: viewModel.langs, optionToString: { $0.name }, - selected: Binding( - get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, - set: { autoSelectAudioLangcode = $0.isoCode} - ) - ) + selected: Binding(get: { + viewModel.langs + .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? + .auto + }, + set: { autoSelectAudioLangcode = $0.isoCode })) Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { ForEach(self.viewModel.appearances, id: \.self) { appearance in Text(appearance.localizedName).tag(appearance.rawValue) @@ -92,22 +97,16 @@ struct SettingsView: View { Spacer() Button { print("logging out") + main.route(to: .connectToServer) close = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } } label: { Text("Switch user").font(.callout) } } Button { + SessionManager.current.logout() + main.route(to: .connectToServer) close = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - SessionManager.current.logout() - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } } label: { Text("Sign out").font(.callout) } diff --git a/JellyfinPlayer/SplashView.swift b/JellyfinPlayer/SplashView.swift index 1b264644..d69e6dd5 100644 --- a/JellyfinPlayer/SplashView.swift +++ b/JellyfinPlayer/SplashView.swift @@ -7,19 +7,21 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct SplashView: View { + @EnvironmentObject var main: ViewRouter @StateObject var viewModel = SplashViewModel() var body: some View { - if viewModel.isLoggedIn { - MainTabView() - } else { - NavigationView { - ConnectToServerView() + ProgressView() + .onReceive(viewModel.$isLoggedIn) { flag in + if flag { + main.route(to: .mainTab) + } else { + main.route(to: .connectToServer) + } } - .navigationViewStyle(StackNavigationViewStyle()) - } } } diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index cb76ed87..0fe168b6 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -13,6 +13,7 @@ import Combine import GoogleCast import SwiftyJSON import Defaults +import Stinsen enum PlayerDestination { case remote @@ -27,6 +28,9 @@ protocol PlayerViewControllerDelegate: AnyObject { class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { + @RouterObject + var main: ViewRouter? + weak var delegate: PlayerViewControllerDelegate? var cancellables = Set() @@ -508,6 +512,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe case .error(401, _, _, _): self.delegate?.exitPlayer(self) SessionManager.current.logout() + main?.route(to: .connectToServer) case .error: self.delegate?.exitPlayer(self) } diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 33a7e893..d3cf7794 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -10,8 +10,11 @@ import Combine import Foundation import JellyfinAPI +import Stinsen final class ConnectToServerViewModel: ViewModel { + @RouterObject + var main: ViewRouter? @Published var isConnectedServer = false @@ -23,13 +26,14 @@ final class ConnectToServerViewModel: ViewModel { @Published var publicUsers = [UserDto]() @Published var selectedPublicUser = UserDto() - private let discovery: ServerDiscovery = ServerDiscovery() + private let discovery = ServerDiscovery() @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] @Published var searching = false func getPublicUsers() { if ServerEnvironment.current.server != nil { - LogManager.shared.log.debug("Attempting to read public users from \(ServerEnvironment.current.server.baseURI!)", tag: "getPublicUsers") + LogManager.shared.log.debug("Attempting to read public users from \(ServerEnvironment.current.server.baseURI!)", + tag: "getPublicUsers") UserAPI.getPublicUsers() .trackActivity(loading) .sink(receiveCompletion: { completion in @@ -46,12 +50,12 @@ final class ConnectToServerViewModel: ViewModel { } func hidePublicUsers() { - self.lastPublicUsers = publicUsers + lastPublicUsers = publicUsers publicUsers = [] } func showPublicUsers() { - self.publicUsers = lastPublicUsers + publicUsers = lastPublicUsers lastPublicUsers = [] } @@ -60,7 +64,8 @@ final class ConnectToServerViewModel: ViewModel { ServerEnvironment.current.create(with: uriSubject.value) .trackActivity(loading) .sink(receiveCompletion: { completion in - self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion) + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", + completion: completion) }, receiveValue: { _ in LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer") self.getPublicUsers() @@ -70,7 +75,7 @@ final class ConnectToServerViewModel: ViewModel { func connectToServer(at url: URL) { uriSubject.send(url.absoluteString) - self.connectToServer() + connectToServer() } func discoverServers() { @@ -81,7 +86,7 @@ final class ConnectToServerViewModel: ViewModel { self.searching = false } - discovery.locateServer { [self] (server) in + discovery.locateServer { [self] server in if let server = server, !servers.contains(server) { servers.append(server) } @@ -91,13 +96,16 @@ final class ConnectToServerViewModel: ViewModel { func login() { LogManager.shared.log.debug("Attempting to login to server at \"\(uriSubject.value)\"", tag: "login") - LogManager.shared.log.debug("username == \"\": \(usernameSubject.value.isEmpty), password == \"\": \(passwordSubject.value.isEmpty)", tag: "login") + LogManager.shared.log + .debug("username == \"\": \(usernameSubject.value.isEmpty), password == \"\": \(passwordSubject.value.isEmpty)", + tag: "login") SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value) .trackActivity(loading) .sink(receiveCompletion: { completion in - self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion) - }, receiveValue: { _ in - + self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", + completion: completion) + }, receiveValue: { [weak self] _ in + self?.main?.route(to: .mainTab) }) .store(in: &cancellables) } From 0640e7051dcea5b2298444a1aa5c5f160138abad Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 14:26:19 +0900 Subject: [PATCH 02/18] Add FilterCoordinator Add LibraryCoordinator Add SearchCoordinator Add SettingsCoordinator Update HomeCoordinator Update LibraryListCoordinator --- JellyfinPlayer.xcodeproj/project.pbxproj | 26 +++++++++++ .../Coordinators/FilterCoordinator.swift | 34 ++++++++++++++ .../Coordinators/HomeCoordinator.swift | 11 ++++- .../Coordinators/LibraryCoordinator.swift | 45 +++++++++++++++++++ .../Coordinators/LibraryListCoordinator.swift | 14 +++++- .../Coordinators/SearchCoordinator.swift | 30 +++++++++++++ .../Coordinators/SettingsCoordinator.swift | 25 +++++++++++ JellyfinPlayer/HomeView.swift | 8 ++-- JellyfinPlayer/LibraryListView.swift | 28 ++++++------ JellyfinPlayer/LibrarySearchView.swift | 4 +- JellyfinPlayer/LibraryView.swift | 27 +++++------ JellyfinPlayer/SettingsView.swift | 8 ++-- Shared/Extensions/ViewExtensions.swift | 17 +++++++ 13 files changed, 233 insertions(+), 44 deletions(-) create mode 100644 JellyfinPlayer/Coordinators/FilterCoordinator.swift create mode 100644 JellyfinPlayer/Coordinators/LibraryCoordinator.swift create mode 100644 JellyfinPlayer/Coordinators/SearchCoordinator.swift create mode 100644 JellyfinPlayer/Coordinators/SettingsCoordinator.swift create mode 100644 Shared/Extensions/ViewExtensions.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index f053e3ff..b574d0ec 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -136,6 +136,14 @@ 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; + 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; + 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; + 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; }; + 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */; }; + 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; }; + 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; }; + 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; + 6220D0BB26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.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 */; }; @@ -353,6 +361,11 @@ 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; + 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; + 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; + 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = ""; }; + 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = ""; }; + 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.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 = ""; }; @@ -740,6 +753,7 @@ 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */, 624C21742685CF60007F1390 /* SearchablePickerView.swift */, + 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -764,6 +778,10 @@ 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, + 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, + 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, + 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, + 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -1135,6 +1153,7 @@ 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, + 6220D0BB26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */, @@ -1164,6 +1183,7 @@ 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, + 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, @@ -1185,6 +1205,7 @@ buildActionMask = 2147483647; files = ( 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, @@ -1199,6 +1220,7 @@ 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, + 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, @@ -1232,6 +1254,7 @@ 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, + 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, @@ -1241,6 +1264,7 @@ 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, + 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, @@ -1249,6 +1273,7 @@ 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, + 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, @@ -1271,6 +1296,7 @@ 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */, E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */, 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, + 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */, 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */, E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */, diff --git a/JellyfinPlayer/Coordinators/FilterCoordinator.swift b/JellyfinPlayer/Coordinators/FilterCoordinator.swift new file mode 100644 index 00000000..0e35e092 --- /dev/null +++ b/JellyfinPlayer/Coordinators/FilterCoordinator.swift @@ -0,0 +1,34 @@ +// +/* + * 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 SwiftUI + +final class FilterCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + @Binding var filters: LibraryFilters + var enabledFilterType: [FilterType] + var parentId: String = "" + + init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { + _filters = filters + self.enabledFilterType = enabledFilterType + self.parentId = parentId + } + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId) + } +} diff --git a/JellyfinPlayer/Coordinators/HomeCoordinator.swift b/JellyfinPlayer/Coordinators/HomeCoordinator.swift index 4b0fbeb8..fae2323e 100644 --- a/JellyfinPlayer/Coordinators/HomeCoordinator.swift +++ b/JellyfinPlayer/Coordinators/HomeCoordinator.swift @@ -14,9 +14,16 @@ import SwiftUI final class HomeCoordinator: NavigationCoordinatable { var navigationStack = NavigationStack() - enum Route: NavigationRoute {} + enum Route: NavigationRoute { + case settings + } - func resolveRoute(route: Route) -> Transition {} + func resolveRoute(route: Route) -> Transition { + switch route { + case .settings: + return .modal(SettingsCoordinator().eraseToAnyCoordinatable()) + } + } @ViewBuilder func start() -> some View { diff --git a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift b/JellyfinPlayer/Coordinators/LibraryCoordinator.swift new file mode 100644 index 00000000..3767779c --- /dev/null +++ b/JellyfinPlayer/Coordinators/LibraryCoordinator.swift @@ -0,0 +1,45 @@ +// +/* + * 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 SwiftUI + +final class LibraryCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + var viewModel: LibraryViewModel + var title: String + + init(viewModel: LibraryViewModel, title: String) { + self.viewModel = viewModel + self.title = title + } + + enum Route: NavigationRoute { + case search(viewModel: LibrarySearchViewModel) + case filter(filters: Binding, enabledFilterType: [FilterType], parentId: String) + } + + func resolveRoute(route: Route) -> Transition { + switch route { + case let .search(viewModel): + return .push(SearchCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) + case let .filter(filters, enabledFilterType, parentId): + return .modal(FilterCoordinator(filters: filters, + enabledFilterType: enabledFilterType, + parentId: parentId) + .eraseToAnyCoordinatable()) + } + } + + @ViewBuilder + func start() -> some View { + LibraryView(viewModel: self.viewModel, title: title) + } +} diff --git a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift b/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift index 3bbf0728..370ebe8a 100644 --- a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift +++ b/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift @@ -14,9 +14,19 @@ import SwiftUI final class LibraryListCoordinator: NavigationCoordinatable { var navigationStack = NavigationStack() - enum Route: NavigationRoute {} + enum Route: NavigationRoute { + case search(viewModel: LibrarySearchViewModel) + case library(viewModel: LibraryViewModel, title: String) + } - func resolveRoute(route: Route) -> Transition {} + func resolveRoute(route: Route) -> Transition { + switch route { + case let .search(viewModel): + return .push(SearchCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) + case let .library(viewModel, title): + return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) + } + } @ViewBuilder func start() -> some View { diff --git a/JellyfinPlayer/Coordinators/SearchCoordinator.swift b/JellyfinPlayer/Coordinators/SearchCoordinator.swift new file mode 100644 index 00000000..c69c817b --- /dev/null +++ b/JellyfinPlayer/Coordinators/SearchCoordinator.swift @@ -0,0 +1,30 @@ +// +/* + * 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 SwiftUI + +final class SearchCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + var viewModel: LibrarySearchViewModel + + init(viewModel: LibrarySearchViewModel) { + self.viewModel = viewModel + } + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + LibrarySearchView(viewModel: self.viewModel) + } +} diff --git a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift b/JellyfinPlayer/Coordinators/SettingsCoordinator.swift new file mode 100644 index 00000000..07c23532 --- /dev/null +++ b/JellyfinPlayer/Coordinators/SettingsCoordinator.swift @@ -0,0 +1,25 @@ +// +/* + * 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 SwiftUI + +final class SettingsCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + SettingsView(viewModel: .init()) + } +} diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index bc80d7a2..bc147b72 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -9,10 +9,11 @@ import Foundation import SwiftUI +import Stinsen struct HomeView: View { + @EnvironmentObject var home: NavigationRouter @StateObject var viewModel = HomeViewModel() - @State var showingSettings = false @ViewBuilder var innerBody: some View { @@ -60,14 +61,11 @@ struct HomeView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - showingSettings = true + home.route(to: .settings) } label: { Image(systemName: "gear") } } } - .fullScreenCover(isPresented: $showingSettings) { - SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) - } } } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 059cd1de..f87607d5 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -6,17 +6,19 @@ */ import Foundation +import Stinsen import SwiftUI struct LibraryListView: View { + @EnvironmentObject var libraryList: NavigationRouter @StateObject var viewModel = LibraryListViewModel() var body: some View { ScrollView { LazyVStack { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites") - }) { + Button { + libraryList.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")) + } label: { ZStack { HStack { Spacer() @@ -59,9 +61,9 @@ struct LibraryListView: View { if !viewModel.isLoading { ForEach(viewModel.libraries, id: \.id) { library in if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") - }) { + Button { + libraryList.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? "")) + } label: { ZStack { ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) .opacity(0.4) @@ -76,8 +78,8 @@ struct LibraryListView: View { Spacer() }.padding(32) }.background(Color.black) - .frame(minWidth: 100, maxWidth: .infinity) - .frame(height: 100) + .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 100) } .cornerRadius(10) .shadow(radius: 5) @@ -90,15 +92,15 @@ struct LibraryListView: View { ProgressView() } }.padding(.leading, 16) - .padding(.trailing, 16) - .padding(.top, 8) + .padding(.trailing, 16) + .padding(.top, 8) } .navigationTitle(NSLocalizedString("All Media", comment: "")) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { - NavigationLink(destination: LazyView { - LibrarySearchView(viewModel: .init(parentID: nil)) - }) { + Button { + libraryList.route(to: .search(viewModel: .init(parentID: nil))) + } label: { Image(systemName: "magnifyingglass") } } diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 65b63b34..dbe2b053 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -8,10 +8,12 @@ import Combine import JellyfinAPI import SwiftUI +import Stinsen struct LibrarySearchView: View { + @EnvironmentObject var search: NavigationRouter @StateObject var viewModel: LibrarySearchViewModel - @State var searchQuery = "" + @State private var searchQuery = "" @State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index ee6b6a48..e60f8b67 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -6,17 +6,17 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct LibraryView: View { + @EnvironmentObject var library: NavigationRouter @StateObject var viewModel: LibraryViewModel var title: String // MARK: tracks for grid - var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) - @State var isShowingSearchView = false - @State var isShowingFilterView = false + var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) @State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) @@ -89,26 +89,19 @@ struct LibraryView: View { }.disabled(viewModel.isLoading) } Label("Icon One", systemImage: "line.horizontal.3.decrease.circle") - .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) - .onTapGesture { - isShowingFilterView = true - } + .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) + .onTapGesture { + library + .route(to: .filter(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, + parentId: viewModel.parentID ?? "")) + } Button { - isShowingSearchView = true + library.route(to: .search(viewModel: .init(parentID: viewModel.parentID))) } label: { Image(systemName: "magnifyingglass") } } } - .sheet(isPresented: $isShowingFilterView) { - LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "") - } - .background( - NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)), - isActive: $isShowingSearchView) { - EmptyView() - } - ) } } diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 6abdb076..aaea3ef0 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -12,11 +12,11 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var main: ViewRouter + @EnvironmentObject var settings: NavigationRouter @Environment(\.managedObjectContext) private var viewContext @ObservedObject var viewModel: SettingsViewModel - @Binding var close: Bool @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles @@ -98,7 +98,7 @@ struct SettingsView: View { Button { print("logging out") main.route(to: .connectToServer) - close = false + settings.dismiss() } label: { Text("Switch user").font(.callout) } @@ -106,7 +106,7 @@ struct SettingsView: View { Button { SessionManager.current.logout() main.route(to: .connectToServer) - close = false + settings.dismiss() } label: { Text("Sign out").font(.callout) } @@ -116,7 +116,7 @@ struct SettingsView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { Button { - close = false + settings.dismiss() } label: { Image(systemName: "xmark") } diff --git a/Shared/Extensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions.swift new file mode 100644 index 00000000..f93a5ee0 --- /dev/null +++ b/Shared/Extensions/ViewExtensions.swift @@ -0,0 +1,17 @@ +// +/* + * 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 SwiftUI + +extension View { + func eraseToAnyView() -> AnyView { + return AnyView(self) + } +} From 74a930202101899c9b2c22602c7622daf5633ff5 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 16:46:59 +0900 Subject: [PATCH 03/18] Add ItemCoordinator --- JellyfinPlayer.xcodeproj/project.pbxproj | 12 +- .../Components/PortraitItemView.swift | 155 +++++++++--------- JellyfinPlayer/ContinueWatchingView.swift | 7 +- .../Coordinators/HomeCoordinator.swift | 6 + .../Coordinators/ItemCoordinator.swift | 40 +++++ .../Coordinators/LibraryCoordinator.swift | 3 + .../Coordinators/SearchCoordinator.swift | 13 +- JellyfinPlayer/EpisodeItemView.swift | 78 +++++---- JellyfinPlayer/HomeView.swift | 6 +- JellyfinPlayer/ItemView.swift | 53 +++--- JellyfinPlayer/LatestMediaView.swift | 8 +- JellyfinPlayer/LibrarySearchView.swift | 9 +- JellyfinPlayer/LibraryView.swift | 6 +- JellyfinPlayer/MovieItemView.swift | 61 ++++--- JellyfinPlayer/NextUpView.swift | 10 +- JellyfinPlayer/SeasonItemView.swift | 100 +++++------ JellyfinPlayer/SeriesItemView.swift | 72 ++++---- Shared/ViewModels/DetailItemViewModel.swift | 1 - Shared/ViewModels/ItemViewModel.swift | 35 ++++ 19 files changed, 423 insertions(+), 252 deletions(-) create mode 100644 JellyfinPlayer/Coordinators/ItemCoordinator.swift create mode 100644 Shared/ViewModels/ItemViewModel.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index b574d0ec..c2da1273 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -143,7 +143,9 @@ 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; }; 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; }; 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; - 6220D0BB26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; + 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; + 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; + 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.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 */; }; @@ -366,6 +368,8 @@ 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = ""; }; 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = ""; }; 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = ""; }; + 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; + 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.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 = ""; }; @@ -515,6 +519,7 @@ 625CB5692678B71200530A6E /* SplashViewModel.swift */, 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, 625CB57B2678CE1000530A6E /* ViewModel.swift */, + 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -782,6 +787,7 @@ 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, + 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -1153,7 +1159,6 @@ 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, - 6220D0BB26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */, @@ -1193,6 +1198,7 @@ 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, @@ -1206,6 +1212,7 @@ files = ( 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, + 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, @@ -1213,6 +1220,7 @@ 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */, 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, + 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, diff --git a/JellyfinPlayer/Components/PortraitItemView.swift b/JellyfinPlayer/Components/PortraitItemView.swift index 60aa77a1..a2441314 100644 --- a/JellyfinPlayer/Components/PortraitItemView.swift +++ b/JellyfinPlayer/Components/PortraitItemView.swift @@ -1,89 +1,86 @@ // - /* - * 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 - */ +/* + * 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 SwiftUI import JellyfinAPI +import Stinsen +import SwiftUI + struct PortraitItemView: View { var item: BaseItemDto var body: some View { - NavigationLink(destination: LazyView { ItemView(item: item) }) { - VStack(alignment: .leading) { - ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .shadow(radius: 4, y: 2) - .shadow(radius: 4, y: 2) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(ProgressBar()) - .frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7) - .padding(0), alignment: .bottomLeading - ) - .overlay( - ZStack { - if item.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } - } - .padding(.leading, 2) - .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) - .opacity(1), alignment: .bottomLeading) - .overlay( - ZStack { - if item.userData?.played ?? false { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - .background(Color(.white)) - .clipShape(Circle().scale(0.8)) - } else { - if item.userData?.unplayedItemCount != nil { - Capsule() - .fill(Color.accentColor) - .frame(minWidth: 20, minHeight: 20, maxHeight: 20) - Text(String(item.userData!.unplayedItemCount ?? 0)) - .foregroundColor(.white) - .font(.caption2) - .padding(2) - } - } - }.padding(2) - .fixedSize() - .opacity(1), alignment: .topTrailing).opacity(1) - Text(item.seriesName ?? item.name ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if item.type == "Movie" || item.type == "Series" { - Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else if item.type == "Season" { - Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) + VStack(alignment: .leading) { + ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), + bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) + .frame(width: 100, height: 150) + .cornerRadius(10) + .shadow(radius: 4, y: 2) + .shadow(radius: 4, y: 2) + .overlay(Rectangle() + .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .mask(ProgressBar()) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7) + .padding(0), alignment: .bottomLeading) + .overlay(ZStack { + if item.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) + } } - }.frame(width: 100) - } + .padding(.leading, 2) + .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) + .opacity(1), alignment: .bottomLeading) + .overlay(ZStack { + if item.userData?.played ?? false { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + .background(Color(.white)) + .clipShape(Circle().scale(0.8)) + } else { + if item.userData?.unplayedItemCount != nil { + Capsule() + .fill(Color.accentColor) + .frame(minWidth: 20, minHeight: 20, maxHeight: 20) + Text(String(item.userData!.unplayedItemCount ?? 0)) + .foregroundColor(.white) + .font(.caption2) + .padding(2) + } + } + }.padding(2) + .fixedSize() + .opacity(1), alignment: .topTrailing).opacity(1) + Text(item.seriesName ?? item.name ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + if item.type == "Movie" || item.type == "Series" { + Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else if item.type == "Season" { + Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else { + Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } + }.frame(width: 100) } } diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 390f7aa3..a7a42473 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -8,6 +8,7 @@ import SwiftUI import JellyfinAPI +import Stinsen struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { @@ -31,13 +32,17 @@ struct ProgressBar: Shape { } struct ContinueWatchingView: View { + @EnvironmentObject var home: NavigationRouter + var items: [BaseItemDto] var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemView(item: item) }) { + Button { + home.route(to: .item(viewModel: .init(id: item.id!))) + } label: { VStack(alignment: .leading) { ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) .frame(width: 320, height: 180) diff --git a/JellyfinPlayer/Coordinators/HomeCoordinator.swift b/JellyfinPlayer/Coordinators/HomeCoordinator.swift index fae2323e..ac152866 100644 --- a/JellyfinPlayer/Coordinators/HomeCoordinator.swift +++ b/JellyfinPlayer/Coordinators/HomeCoordinator.swift @@ -16,12 +16,18 @@ final class HomeCoordinator: NavigationCoordinatable { enum Route: NavigationRoute { case settings + case library(viewModel: LibraryViewModel, title: String) + case item(viewModel: ItemViewModel) } func resolveRoute(route: Route) -> Transition { switch route { case .settings: return .modal(SettingsCoordinator().eraseToAnyCoordinatable()) + case let .library(viewModel, title): + return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) + case let .item(viewModel): + return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) } } diff --git a/JellyfinPlayer/Coordinators/ItemCoordinator.swift b/JellyfinPlayer/Coordinators/ItemCoordinator.swift new file mode 100644 index 00000000..379a877d --- /dev/null +++ b/JellyfinPlayer/Coordinators/ItemCoordinator.swift @@ -0,0 +1,40 @@ +// +/* + * 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 SwiftUI + +final class ItemCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + var viewModel: ItemViewModel + + init(viewModel: ItemViewModel) { + self.viewModel = viewModel + } + + enum Route: NavigationRoute { + case item(viewModel: ItemViewModel) + case library(viewModel: LibraryViewModel, title: String) + } + + func resolveRoute(route: Route) -> Transition { + switch route { + case let .item(viewModel): + return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) + case let .library(viewModel, title): + return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) + } + } + + @ViewBuilder + func start() -> some View { + ItemView(viewModel: self.viewModel) + } +} diff --git a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift b/JellyfinPlayer/Coordinators/LibraryCoordinator.swift index 3767779c..63f71b24 100644 --- a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift +++ b/JellyfinPlayer/Coordinators/LibraryCoordinator.swift @@ -24,6 +24,7 @@ final class LibraryCoordinator: NavigationCoordinatable { enum Route: NavigationRoute { case search(viewModel: LibrarySearchViewModel) case filter(filters: Binding, enabledFilterType: [FilterType], parentId: String) + case item(viewModel: ItemViewModel) } func resolveRoute(route: Route) -> Transition { @@ -35,6 +36,8 @@ final class LibraryCoordinator: NavigationCoordinatable { enabledFilterType: enabledFilterType, parentId: parentId) .eraseToAnyCoordinatable()) + case let .item(viewModel): + return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) } } diff --git a/JellyfinPlayer/Coordinators/SearchCoordinator.swift b/JellyfinPlayer/Coordinators/SearchCoordinator.swift index c69c817b..8a04f3a0 100644 --- a/JellyfinPlayer/Coordinators/SearchCoordinator.swift +++ b/JellyfinPlayer/Coordinators/SearchCoordinator.swift @@ -14,14 +14,21 @@ import SwiftUI final class SearchCoordinator: NavigationCoordinatable { var navigationStack = NavigationStack() var viewModel: LibrarySearchViewModel - + init(viewModel: LibrarySearchViewModel) { self.viewModel = viewModel } - enum Route: NavigationRoute {} + enum Route: NavigationRoute { + case item(viewModel: ItemViewModel) + } - func resolveRoute(route: Route) -> Transition {} + func resolveRoute(route: Route) -> Transition { + switch route { + case let .item(viewModel): + return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) + } + } @ViewBuilder func start() -> some View { diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index b58cd9d0..b51f29bf 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -5,9 +5,11 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct EpisodeItemView: View { + @EnvironmentObject var item: NavigationRouter @StateObject var viewModel: EpisodeItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) var hSizeClass @@ -15,7 +17,9 @@ struct EpisodeItemView: View { @EnvironmentObject private var playbackInfo: VideoPlayerItem var portraitHeaderView: some View { - ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getBackdropImageBlurHash()) + ImageView(src: viewModel.item + .getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + bh: viewModel.item.getBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) } @@ -108,7 +112,10 @@ struct EpisodeItemView: View { var body: some View { VStack(alignment: .leading) { if hSizeClass == .compact && vSizeClass == .regular { - ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { + ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds + .width * 0.5625) { VStack(alignment: .leading) { Spacer() .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) @@ -126,9 +133,9 @@ struct EpisodeItemView: View { HStack { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + } label: { Text(genre.name ?? "").font(.footnote) } } @@ -143,11 +150,13 @@ struct EpisodeItemView: View { Spacer().frame(width: 16) ForEach(viewModel.item.people!, id: \.self) { person in if person.type! == "Actor" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(person: person), title: person.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) + } label: { VStack { - ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash()) + ImageView(src: person + .getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), + bh: person.getBlurHash()) .frame(width: 100, height: 100) .cornerRadius(10) Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) @@ -171,16 +180,16 @@ struct EpisodeItemView: View { HStack { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { Text(studio.name ?? "").font(.footnote) } } }.padding(.leading, 16).padding(.trailing, 16) } } - if !(viewModel.similarItems).isEmpty { + if !viewModel.similarItems.isEmpty { Text("More Like This") .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) ScrollView(.horizontal, showsIndicators: false) { @@ -189,7 +198,9 @@ struct EpisodeItemView: View { HStack { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in - NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + Button { + item.route(to: .item(viewModel: .init(id: similarItem.id!))) + } label: { PortraitItemView(item: similarItem) } Spacer().frame(width: 10) @@ -213,7 +224,8 @@ struct EpisodeItemView: View { .blur(radius: 4) HStack { VStack { - ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash()) + ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), + bh: viewModel.item.getSeriesPrimaryImageBlurHash()) .frame(width: 120, height: 180) .cornerRadius(10) Spacer().frame(height: 15) @@ -274,7 +286,7 @@ struct EpisodeItemView: View { Spacer() }.frame(maxWidth: .infinity, alignment: .leading) .offset(x: 14) - .padding(.top, 1) + .padding(.top, 1) }.frame(maxWidth: .infinity, alignment: .leading) Spacer() @@ -318,9 +330,9 @@ struct EpisodeItemView: View { HStack { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + } label: { Text(genre.name ?? "").font(.footnote) } } @@ -337,14 +349,20 @@ struct EpisodeItemView: View { Spacer().frame(width: 16) ForEach(viewModel.item.people!, id: \.self) { person in if person.type! == "Actor" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(person: person), title: person.name ?? "") - }) { + Button { + item + .route(to: .library(viewModel: .init(person: person), + title: person.name ?? "")) + } label: { VStack { - ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash()) + ImageView(src: person + .getImage(baseURL: ServerEnvironment.current.server.baseURI!, + maxWidth: 100), + bh: person.getBlurHash()) .frame(width: 100, height: 100) .cornerRadius(10) - Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) + Text(person.name ?? "").font(.footnote).fontWeight(.regular) + .lineLimit(1) .frame(width: 100).foregroundColor(Color.primary) if person.role != "" { Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1) @@ -365,9 +383,9 @@ struct EpisodeItemView: View { HStack { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { Text(studio.name ?? "").font(.footnote) } } @@ -376,7 +394,7 @@ struct EpisodeItemView: View { .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } - if !(viewModel.similarItems).isEmpty { + if !viewModel.similarItems.isEmpty { Text("More Like This") .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) ScrollView(.horizontal, showsIndicators: false) { @@ -385,7 +403,9 @@ struct EpisodeItemView: View { HStack { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in - NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + Button { + item.route(to: .item(viewModel: .init(id: similarItem.id!))) + } label: { PortraitItemView(item: similarItem) } Spacer().frame(width: 10) diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index bc147b72..bc3f16b9 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -36,9 +36,9 @@ struct HomeView: View { .font(.title2) .fontWeight(.bold) Spacer() - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") - }) { + Button { + home.route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")) + } label: { HStack { Text("See All").font(.subheadline).fontWeight(.bold) Image(systemName: "chevron.right").font(Font.subheadline.bold()) diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index a60937cc..3d5c40e5 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -5,30 +5,30 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI import Introspect import JellyfinAPI +import SwiftUI class VideoPlayerItem: ObservableObject { @Published var shouldShowPlayer: Bool = false - @Published var itemToPlay: BaseItemDto = BaseItemDto() + @Published var itemToPlay = BaseItemDto() } struct ItemView: View { - private var item: BaseItemDto - - @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() - @State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view. + @StateObject var viewModel: ItemViewModel + @StateObject private var videoPlayerItem = VideoPlayerItem() + @State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view. @State private var isLoading: Bool = false @State private var viewDidLoad: Bool = false - init(item: BaseItemDto) { - self.item = item - } - var body: some View { - VStack { - NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) + ZStack { + NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { + VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, + loadBinding: $videoIsLoading, + pBinding: _videoPlayerItem + .projectedValue + .shouldShowPlayer) .navigationBarHidden(true) .navigationBarBackButtonHidden(true) .statusBar(hidden: true) @@ -37,25 +37,30 @@ struct ItemView: View { }, isActive: $videoPlayerItem.shouldShowPlayer) { EmptyView() } - VStack { - if item.type == "Movie" { - MovieItemView(viewModel: .init(item: item)) - } else if item.type == "Season" { - SeasonItemView(viewModel: .init(item: item)) - } else if item.type == "Series" { - SeriesItemView(viewModel: .init(item: item)) - } else if item.type == "Episode" { - EpisodeItemView(viewModel: .init(item: item)) - } else { - Text("Type: \(item.type ?? "") not implemented yet :(") + Group { + if let item = viewModel.item { + if item.type == "Movie" { + MovieItemView(viewModel: .init(item: item)) + } else if item.type == "Season" { + SeasonItemView(viewModel: .init(item: item)) + } else if item.type == "Series" { + SeriesItemView(viewModel: .init(item: item)) + } else if item.type == "Episode" { + EpisodeItemView(viewModel: .init(item: item)) + } else { + Text("Type: \(item.type ?? "") not implemented yet :(") + } } } - .introspectTabBarController { (UITabBarController) in + .introspectTabBarController { UITabBarController in UITabBarController.tabBar.isHidden = false } .navigationBarHidden(false) .navigationBarBackButtonHidden(false) .environmentObject(videoPlayerItem) } + if viewModel.isLoading { + ProgressView() + } } } diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 902891de..eb6bdbdd 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -5,9 +5,11 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct LatestMediaView: View { + @EnvironmentObject var home: NavigationRouter @StateObject var viewModel: LatestMediaViewModel var body: some View { @@ -15,7 +17,11 @@ struct LatestMediaView: View { LazyHStack { ForEach(viewModel.items, id: \.id) { item in if item.type == "Series" || item.type == "Movie" { - PortraitItemView(item: item) + Button { + home.route(to: .item(viewModel: .init(id: item.id!))) + } label: { + PortraitItemView(item: item) + } } }.padding(.trailing, 16) }.padding(.leading, 20) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index dbe2b053..4f6887a8 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -7,8 +7,8 @@ import Combine import JellyfinAPI -import SwiftUI import Stinsen +import SwiftUI struct LibrarySearchView: View { @EnvironmentObject var search: NavigationRouter @@ -80,7 +80,11 @@ struct LibrarySearchView: View { if !items.isEmpty { LazyVGrid(columns: tracks) { ForEach(items, id: \.id) { item in - PortraitItemView(item: item) + Button { + search.route(to: .item(viewModel: .init(id: item.id!))) + } label: { + PortraitItemView(item: item) + } } } .padding(.bottom, 16) @@ -108,7 +112,6 @@ struct LibrarySearchView: View { } private extension ItemType { - var localized: String { switch self { case .episode: diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index e60f8b67..133164e7 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -35,7 +35,11 @@ struct LibraryView: View { LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.id) { item in if item.type != "Folder" { - PortraitItemView(item: item) + Button { + library.route(to: .item(viewModel: .init(id: item.id!))) + } label: { + PortraitItemView(item: item) + } } } }.onRotate { _ in diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index bcef0c77..6916830d 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -5,9 +5,11 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct MovieItemView: View { + @EnvironmentObject var item: NavigationRouter @StateObject var viewModel: MovieItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) @@ -18,7 +20,8 @@ struct MovieItemView: View { private var playbackInfo: VideoPlayerItem var portraitHeaderView: some View { - ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + ImageView(src: viewModel.item + .getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) @@ -135,9 +138,9 @@ struct MovieItemView: View { HStack { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + } label: { Text(genre.name ?? "").font(.footnote) } } @@ -152,9 +155,9 @@ struct MovieItemView: View { Spacer().frame(width: 16) ForEach(viewModel.item.people!, id: \.self) { person in if person.type ?? "" == "Actor" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(person: person), title: person.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) + } label: { VStack { ImageView(src: person .getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), @@ -182,17 +185,16 @@ struct MovieItemView: View { HStack { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in - NavigationLink(destination: LazyView { - - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { Text(studio.name ?? "").font(.footnote) } } }.padding(.leading, 16).padding(.trailing, 16) } } - if !(viewModel.similarItems).isEmpty { + if !viewModel.similarItems.isEmpty { Text("More Like This") .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) ScrollView(.horizontal, showsIndicators: false) { @@ -201,7 +203,9 @@ struct MovieItemView: View { HStack { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in - NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + Button { + item.route(to: .item(viewModel: .init(id: similarItem.id!))) + } label: { PortraitItemView(item: similarItem) } Spacer().frame(width: 10) @@ -236,7 +240,8 @@ struct MovieItemView: View { self.playbackInfo.shouldShowPlayer = true } label: { HStack { - Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left") + Text(viewModel.item + .getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left") .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } @@ -290,7 +295,7 @@ struct MovieItemView: View { Spacer() }.frame(maxWidth: .infinity, alignment: .leading) .offset(x: 14) - .padding(.top, 1) + .padding(.top, 1) }.frame(maxWidth: .infinity, alignment: .leading) Spacer() HStack { @@ -333,9 +338,9 @@ struct MovieItemView: View { HStack { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + } label: { Text(genre.name ?? "").font(.footnote) } } @@ -352,9 +357,11 @@ struct MovieItemView: View { Spacer().frame(width: 16) ForEach(viewModel.item.people!, id: \.self) { person in if person.type! == "Actor" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(person: person), title: person.name ?? "") - }) { + Button { + item + .route(to: .library(viewModel: .init(person: person), + title: person.name ?? "")) + } label: { VStack { ImageView(src: person .getImage(baseURL: ServerEnvironment.current.server.baseURI!, @@ -384,9 +391,9 @@ struct MovieItemView: View { HStack { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { Text(studio.name ?? "").font(.footnote) } } @@ -395,7 +402,7 @@ struct MovieItemView: View { .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } - if !(viewModel.similarItems).isEmpty { + if !viewModel.similarItems.isEmpty { Text("More Like This") .font(.callout).fontWeight(.semibold).padding(.top, 3).padding(.leading, 16) ScrollView(.horizontal, showsIndicators: false) { @@ -404,7 +411,9 @@ struct MovieItemView: View { HStack { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in - NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + Button { + item.route(to: .item(viewModel: .init(id: similarItem.id!))) + } label: { PortraitItemView(item: similarItem) } Spacer().frame(width: 10) diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index c0301df1..8464ded4 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -5,11 +5,13 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI import Combine import JellyfinAPI +import Stinsen +import SwiftUI struct NextUpView: View { + @EnvironmentObject var home: NavigationRouter var items: [BaseItemDto] @@ -22,7 +24,11 @@ struct NextUpView: View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach(items, id: \.id) { item in - PortraitItemView(item: item) + Button { + home.route(to: .item(viewModel: .init(id: item.id!))) + } label: { + PortraitItemView(item: item) + } }.padding(.trailing, 16) } .padding(.leading, 20) diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 6d3758be..541c4b26 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -5,9 +5,11 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct SeasonItemView: View { + @EnvironmentObject var item: NavigationRouter @StateObject var viewModel: SeasonItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) var hSizeClass @@ -18,7 +20,9 @@ struct SeasonItemView: View { if viewModel.isLoading { EmptyView() } else { - ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: viewModel.item.getSeriesBackdropImageBlurHash()) + ImageView(src: viewModel.item + .getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + bh: viewModel.item.getSeriesBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) } @@ -43,7 +47,7 @@ struct SeasonItemView: View { } }.offset(y: -32) }.padding(.horizontal, 16) - .offset(y: 22) + .offset(y: 22) } @ViewBuilder @@ -63,42 +67,40 @@ struct SeasonItemView: View { .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) .padding(.trailing, 16) ForEach(viewModel.episodes, id: \.id) { episode in - NavigationLink(destination: ItemView(item: episode)) { + Button { + item.route(to: .item(viewModel: .init(id: episode.id!))) + } label: { HStack { ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) .shadow(radius: 5) .frame(width: 150, height: 90) .cornerRadius(10) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(ProgressBar()) - .frame(width: CGFloat(episode.userData?.playedPercentage ?? 0 * 1.5), height: 7) - .padding(0), alignment: .bottomLeading - ) - .overlay( - ZStack { - if episode.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } + .overlay(Rectangle() + .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .mask(ProgressBar()) + .frame(width: CGFloat(episode.userData?.playedPercentage ?? 0 * 1.5), height: 7) + .padding(0), alignment: .bottomLeading) + .overlay(ZStack { + if episode.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) } - .padding(.leading, 2) - .padding(.bottom, episode.userData?.playedPercentage == nil ? 2 : 9) - .opacity(1), alignment: .bottomLeading) - .overlay( - ZStack { - if episode.userData?.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } - }.padding(2) + } + .padding(.leading, 2) + .padding(.bottom, episode.userData?.playedPercentage == nil ? 2 : 9) + .opacity(1), alignment: .bottomLeading) + .overlay(ZStack { + if episode.userData?.played ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } + }.padding(2) .opacity(1), alignment: .topTrailing).opacity(1) VStack(alignment: .leading) { HStack { @@ -131,9 +133,9 @@ struct SeasonItemView: View { HStack { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { Text(studio.name ?? "").font(.footnote) } } @@ -148,7 +150,8 @@ struct SeasonItemView: View { } else { GeometryReader { geometry in ZStack { - ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200), bh: viewModel.item.getSeriesBackdropImageBlurHash()) + ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200), + bh: viewModel.item.getSeriesBackdropImageBlurHash()) .opacity(0.4) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) @@ -180,22 +183,23 @@ struct SeasonItemView: View { .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) .padding(.trailing, 16) ForEach(viewModel.episodes, id: \.id) { episode in - NavigationLink(destination: ItemView(item: episode)) { + Button { + item.route(to: .item(viewModel: .init(id: episode.id!))) + } label: { HStack { ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) .shadow(radius: 5) .frame(width: 150, height: 90) .cornerRadius(10) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(ProgressBar()) - .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) - .padding(0), alignment: .bottomLeading - ) + .overlay(Rectangle() + .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .mask(ProgressBar()) + .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) + .padding(0), alignment: .bottomLeading) VStack(alignment: .leading) { HStack { - Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) + Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))") + .font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) @@ -224,9 +228,9 @@ struct SeasonItemView: View { HStack { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { Text(studio.name ?? "").font(.footnote) } } diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 325bd684..d03d4920 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -5,9 +5,11 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct SeriesItemView: View { + @EnvironmentObject var item: NavigationRouter @StateObject var viewModel: SeriesItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) var hSizeClass @@ -69,27 +71,46 @@ struct SeriesItemView: View { .padding(.horizontal, 16) } if let genreItems = viewModel.item.genreItems, - !genreItems.isEmpty { + !genreItems.isEmpty + { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 8) { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(genreItems, id: \.id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") - }) { + Button { + item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + } label: { Text(genre.name ?? "").font(.footnote) } } } .padding(.horizontal, 16) } - .padding(.bottom, 8) + .padding(.bottom, 16) } Text(viewModel.item.overview ?? "") .font(.footnote) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 16) .padding(.horizontal, 16) + if let studios = viewModel.item.studios, + !studios.isEmpty + { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + Text("Studios:").font(.callout).fontWeight(.semibold) + ForEach(studios, id: \.id) { studio in + Button { + item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + } label: { + Text(studio.name ?? "").font(.footnote) + } + } + } + .padding(.horizontal, 16) + } + .padding(.bottom, 16) + } Text("Seasons") .font(.callout).fontWeight(.semibold) .padding(.horizontal, 16) @@ -97,14 +118,19 @@ struct SeriesItemView: View { .padding(.top, 24) LazyVGrid(columns: tracks) { ForEach(viewModel.seasons, id: \.id) { season in - PortraitItemView(item: season) + Button { + item.route(to: .item(viewModel: .init(id: season.id!))) + } label: { + PortraitItemView(item: season) + } } } .padding(.bottom, 16) .padding(.horizontal, 8) LazyVStack(alignment: .leading, spacing: 0) { if let people = viewModel.item.people, - !people.isEmpty { + !people.isEmpty + { Text("CAST") .font(.callout).fontWeight(.semibold) .padding(.bottom, 8) @@ -113,9 +139,11 @@ struct SeriesItemView: View { LazyHStack(spacing: 16) { ForEach(people, id: \.self) { person in if person.type == "Actor" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(person: person), title: person.name ?? "") - }) { + Button { + item + .route(to: .library(viewModel: .init(person: person), + title: person.name ?? "")) + } label: { VStack { ImageView(src: person .getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), @@ -125,7 +153,8 @@ struct SeriesItemView: View { Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) .frame(width: 100).foregroundColor(Color.primary) if let role = person.role, - !role.isEmpty { + !role.isEmpty + { Text(role).font(.caption).fontWeight(.medium).lineLimit(1) .foregroundColor(Color.secondary).frame(width: 100) } @@ -138,23 +167,6 @@ struct SeriesItemView: View { } .padding(.bottom, 16) } - if let studios = viewModel.item.studios, - !studios.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 16) { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(studios, id: \.id) { studio in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { - Text(studio.name ?? "").font(.footnote) - } - } - } - .padding(.horizontal, 16) - } - .padding(.bottom, 16) - } if !viewModel.similarItems.isEmpty { Text("More Like This") .font(.callout).fontWeight(.semibold) @@ -163,7 +175,9 @@ struct SeriesItemView: View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 16) { ForEach(viewModel.similarItems, id: \.self) { similarItem in - NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + Button { + item.route(to: .item(viewModel: .init(id: similarItem.id!))) + } label: { PortraitItemView(item: similarItem) } } diff --git a/Shared/ViewModels/DetailItemViewModel.swift b/Shared/ViewModels/DetailItemViewModel.swift index 6ce0e099..847a9b64 100644 --- a/Shared/ViewModels/DetailItemViewModel.swift +++ b/Shared/ViewModels/DetailItemViewModel.swift @@ -7,7 +7,6 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import Foundation import Foundation import JellyfinAPI diff --git a/Shared/ViewModels/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel.swift new file mode 100644 index 00000000..f0cf5b97 --- /dev/null +++ b/Shared/ViewModels/ItemViewModel.swift @@ -0,0 +1,35 @@ +// +/* + * 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 + +class ItemViewModel: ViewModel { + var id: String + + @Published var item: BaseItemDto? + + init(id: String) { + self.id = id + super.init() + + getRelatedItems() + } + + func getRelatedItems() { + UserLibraryAPI.getItem(userId: SessionManager.current.user.user_id!, itemId: id) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + self?.item = response + }) + .store(in: &cancellables) + } +} From 2aab9df5dfd0130faa0481deaaf8bb02cd5f29e0 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 17:37:42 +0900 Subject: [PATCH 04/18] Add VideoPlayerCoordinator rename router --- JellyfinPlayer.xcodeproj/project.pbxproj | 6 + JellyfinPlayer/ConnectToServerView.swift | 4 +- JellyfinPlayer/ContinueWatchingView.swift | 4 +- .../Coordinators/ItemCoordinator.swift | 4 + .../Coordinators/VideoPlayerCoordinator.swift | 31 ++ JellyfinPlayer/EpisodeItemView.swift | 20 +- JellyfinPlayer/HomeView.swift | 6 +- JellyfinPlayer/ItemView.swift | 22 +- JellyfinPlayer/LatestMediaView.swift | 4 +- JellyfinPlayer/LibraryFilterView.swift | 8 +- JellyfinPlayer/LibraryListView.swift | 8 +- JellyfinPlayer/LibrarySearchView.swift | 4 +- JellyfinPlayer/LibraryView.swift | 8 +- JellyfinPlayer/MovieItemView.swift | 20 +- JellyfinPlayer/NextUpView.swift | 4 +- JellyfinPlayer/SeasonItemView.swift | 10 +- JellyfinPlayer/SeriesItemView.swift | 12 +- JellyfinPlayer/SettingsView.swift | 14 +- JellyfinPlayer/SplashView.swift | 6 +- JellyfinPlayer/VideoPlayer.swift | 334 +++++++++++------- 20 files changed, 314 insertions(+), 215 deletions(-) create mode 100644 JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index c2da1273..b235bcec 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -146,6 +146,8 @@ 6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; }; 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; }; + 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; + 6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.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 */; }; @@ -370,6 +372,7 @@ 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -788,6 +791,7 @@ 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, + 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -1185,6 +1189,7 @@ 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, 531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */, + 6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, @@ -1261,6 +1266,7 @@ 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, + 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 71f0caeb..5de1acd2 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -9,7 +9,7 @@ import SwiftUI import Stinsen struct ConnectToServerView: View { - @EnvironmentObject var main: ViewRouter + @EnvironmentObject var mainRouter: ViewRouter @StateObject var viewModel = ConnectToServerViewModel() @State var username = "" @State var password = "" @@ -61,7 +61,7 @@ struct ConnectToServerView: View { if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { let user = SessionManager.current.getSavedSession(userID: publicUser.id!) SessionManager.current.loginWithSavedSession(user: user) - main.route(to: .mainTab) + mainRouter.route(to: .mainTab) } else { username = publicUser.name ?? "" viewModel.selectedPublicUser = publicUser diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index a7a42473..bc5c8698 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -32,7 +32,7 @@ struct ProgressBar: Shape { } struct ContinueWatchingView: View { - @EnvironmentObject var home: NavigationRouter + @EnvironmentObject var homeRouter: NavigationRouter var items: [BaseItemDto] @@ -41,7 +41,7 @@ struct ContinueWatchingView: View { LazyHStack { ForEach(items, id: \.id) { item in Button { - home.route(to: .item(viewModel: .init(id: item.id!))) + homeRouter.route(to: .item(viewModel: .init(id: item.id!))) } label: { VStack(alignment: .leading) { ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) diff --git a/JellyfinPlayer/Coordinators/ItemCoordinator.swift b/JellyfinPlayer/Coordinators/ItemCoordinator.swift index 379a877d..559342f5 100644 --- a/JellyfinPlayer/Coordinators/ItemCoordinator.swift +++ b/JellyfinPlayer/Coordinators/ItemCoordinator.swift @@ -8,6 +8,7 @@ */ import Foundation +import JellyfinAPI import Stinsen import SwiftUI @@ -22,6 +23,7 @@ final class ItemCoordinator: NavigationCoordinatable { enum Route: NavigationRoute { case item(viewModel: ItemViewModel) case library(viewModel: LibraryViewModel, title: String) + case videoPlayer(item: BaseItemDto) } func resolveRoute(route: Route) -> Transition { @@ -30,6 +32,8 @@ final class ItemCoordinator: NavigationCoordinatable { return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) case let .library(viewModel, title): return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) + case let .videoPlayer(item): + return .fullScreen(NavigationViewCoordinator(VideoPlayerCoordinator(item: item)).eraseToAnyCoordinatable()) } } diff --git a/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift b/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift new file mode 100644 index 00000000..5525c364 --- /dev/null +++ b/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift @@ -0,0 +1,31 @@ +// +/* + * 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 +import Stinsen +import SwiftUI + +final class VideoPlayerCoordinator: NavigationCoordinatable { + var navigationStack = NavigationStack() + var item: BaseItemDto + + init(item: BaseItemDto) { + self.item = item + } + + enum Route: NavigationRoute {} + + func resolveRoute(route: Route) -> Transition {} + + @ViewBuilder + func start() -> some View { + VideoPlayerView(item: item) + } +} diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index b51f29bf..a65390a6 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -9,7 +9,7 @@ import Stinsen import SwiftUI struct EpisodeItemView: View { - @EnvironmentObject var item: NavigationRouter + @EnvironmentObject var itemRouter: NavigationRouter @StateObject var viewModel: EpisodeItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) var hSizeClass @@ -63,7 +63,6 @@ struct EpisodeItemView: View { HStack { // Play button Button { - self.playbackInfo.itemToPlay = viewModel.item self.playbackInfo.shouldShowPlayer = true } label: { HStack { @@ -134,7 +133,7 @@ struct EpisodeItemView: View { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in Button { - item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) } label: { Text(genre.name ?? "").font(.footnote) } @@ -151,7 +150,7 @@ struct EpisodeItemView: View { ForEach(viewModel.item.people!, id: \.self) { person in if person.type! == "Actor" { Button { - item.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) } label: { VStack { ImageView(src: person @@ -181,7 +180,7 @@ struct EpisodeItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } @@ -199,7 +198,7 @@ struct EpisodeItemView: View { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in Button { - item.route(to: .item(viewModel: .init(id: similarItem.id!))) + itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) } label: { PortraitItemView(item: similarItem) } @@ -230,7 +229,6 @@ struct EpisodeItemView: View { .cornerRadius(10) Spacer().frame(height: 15) Button { - self.playbackInfo.itemToPlay = viewModel.item self.playbackInfo.shouldShowPlayer = true } label: { HStack { @@ -331,7 +329,7 @@ struct EpisodeItemView: View { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in Button { - item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) } label: { Text(genre.name ?? "").font(.footnote) } @@ -350,7 +348,7 @@ struct EpisodeItemView: View { ForEach(viewModel.item.people!, id: \.self) { person in if person.type! == "Actor" { Button { - item + itemRouter .route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) } label: { @@ -384,7 +382,7 @@ struct EpisodeItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } @@ -404,7 +402,7 @@ struct EpisodeItemView: View { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in Button { - item.route(to: .item(viewModel: .init(id: similarItem.id!))) + itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) } label: { PortraitItemView(item: similarItem) } diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index bc3f16b9..b6c7b897 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -12,7 +12,7 @@ import SwiftUI import Stinsen struct HomeView: View { - @EnvironmentObject var home: NavigationRouter + @EnvironmentObject var homeRouter: NavigationRouter @StateObject var viewModel = HomeViewModel() @ViewBuilder @@ -37,7 +37,7 @@ struct HomeView: View { .fontWeight(.bold) Spacer() Button { - home.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) @@ -61,7 +61,7 @@ struct HomeView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - home.route(to: .settings) + homeRouter.route(to: .settings) } label: { Image(systemName: "gear") } diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 3d5c40e5..2b6e8719 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -7,14 +7,15 @@ import Introspect import JellyfinAPI +import Stinsen import SwiftUI class VideoPlayerItem: ObservableObject { @Published var shouldShowPlayer: Bool = false - @Published var itemToPlay = BaseItemDto() } struct ItemView: View { + @EnvironmentObject var itemRouter: NavigationRouter @StateObject var viewModel: ItemViewModel @StateObject private var videoPlayerItem = VideoPlayerItem() @State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view. @@ -23,20 +24,6 @@ struct ItemView: View { var body: some View { ZStack { - NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { - VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, - loadBinding: $videoIsLoading, - pBinding: _videoPlayerItem - .projectedValue - .shouldShowPlayer) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .edgesIgnoringSafeArea(.all) - .prefersHomeIndicatorAutoHidden(true) - }, isActive: $videoPlayerItem.shouldShowPlayer) { - EmptyView() - } Group { if let item = viewModel.item { if item.type == "Movie" { @@ -59,6 +46,11 @@ struct ItemView: View { .navigationBarBackButtonHidden(false) .environmentObject(videoPlayerItem) } + .onReceive(videoPlayerItem.$shouldShowPlayer) { flag in + guard flag, + let item = viewModel.item else { return } + self.itemRouter.route(to: .videoPlayer(item: item)) + } if viewModel.isLoading { ProgressView() } diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index eb6bdbdd..496c9e33 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -9,7 +9,7 @@ import Stinsen import SwiftUI struct LatestMediaView: View { - @EnvironmentObject var home: NavigationRouter + @EnvironmentObject var homeRouter: NavigationRouter @StateObject var viewModel: LatestMediaViewModel var body: some View { @@ -18,7 +18,7 @@ struct LatestMediaView: View { ForEach(viewModel.items, id: \.id) { item in if item.type == "Series" || item.type == "Movie" { Button { - home.route(to: .item(viewModel: .init(id: item.id!))) + homeRouter.route(to: .item(viewModel: .init(id: item.id!))) } label: { PortraitItemView(item: item) } diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index 8c01231f..b8a3a7bd 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -7,8 +7,10 @@ import JellyfinAPI import SwiftUI +import Stinsen struct LibraryFilterView: View { + @EnvironmentObject var filterRouter: NavigationRouter @Environment(\.presentationMode) var presentationMode @Binding var filters: LibraryFilters var parentId: String = "" @@ -64,7 +66,7 @@ struct LibraryFilterView: View { Button { viewModel.resetFilters() self.filters = viewModel.modifiedFilters - presentationMode.wrappedValue.dismiss() + filterRouter.dismiss() } label: { Text("Reset") } @@ -74,7 +76,7 @@ struct LibraryFilterView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { Button { - presentationMode.wrappedValue.dismiss() + filterRouter.dismiss() } label: { Image(systemName: "xmark") } @@ -83,7 +85,7 @@ struct LibraryFilterView: View { Button { viewModel.updateModifiedFilter() self.filters = viewModel.modifiedFilters - presentationMode.wrappedValue.dismiss() + filterRouter.dismiss() } label: { Text("Apply") } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index f87607d5..c5764830 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -10,14 +10,14 @@ import Stinsen import SwiftUI struct LibraryListView: View { - @EnvironmentObject var libraryList: NavigationRouter + @EnvironmentObject var libraryListRouter: NavigationRouter @StateObject var viewModel = LibraryListViewModel() var body: some View { ScrollView { LazyVStack { Button { - libraryList.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")) + libraryListRouter.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")) } label: { ZStack { HStack { @@ -62,7 +62,7 @@ struct LibraryListView: View { ForEach(viewModel.libraries, id: \.id) { library in if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { Button { - libraryList.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? "")) + libraryListRouter.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? "")) } label: { ZStack { ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) @@ -99,7 +99,7 @@ struct LibraryListView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - libraryList.route(to: .search(viewModel: .init(parentID: nil))) + libraryListRouter.route(to: .search(viewModel: .init(parentID: nil))) } label: { Image(systemName: "magnifyingglass") } diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 4f6887a8..8431ca4c 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -11,7 +11,7 @@ import Stinsen import SwiftUI struct LibrarySearchView: View { - @EnvironmentObject var search: NavigationRouter + @EnvironmentObject var searchRouter: NavigationRouter @StateObject var viewModel: LibrarySearchViewModel @State private var searchQuery = "" @@ -81,7 +81,7 @@ struct LibrarySearchView: View { LazyVGrid(columns: tracks) { ForEach(items, id: \.id) { item in Button { - search.route(to: .item(viewModel: .init(id: item.id!))) + searchRouter.route(to: .item(viewModel: .init(id: item.id!))) } label: { PortraitItemView(item: item) } diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 133164e7..13b09384 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -10,7 +10,7 @@ import Stinsen import SwiftUI struct LibraryView: View { - @EnvironmentObject var library: NavigationRouter + @EnvironmentObject var libraryRouter: NavigationRouter @StateObject var viewModel: LibraryViewModel var title: String @@ -36,7 +36,7 @@ struct LibraryView: View { ForEach(viewModel.items, id: \.id) { item in if item.type != "Folder" { Button { - library.route(to: .item(viewModel: .init(id: item.id!))) + libraryRouter.route(to: .item(viewModel: .init(id: item.id!))) } label: { PortraitItemView(item: item) } @@ -95,12 +95,12 @@ struct LibraryView: View { Label("Icon One", systemImage: "line.horizontal.3.decrease.circle") .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) .onTapGesture { - library + libraryRouter .route(to: .filter(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "")) } Button { - library.route(to: .search(viewModel: .init(parentID: viewModel.parentID))) + libraryRouter.route(to: .search(viewModel: .init(parentID: viewModel.parentID))) } label: { Image(systemName: "magnifyingglass") } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 6916830d..2e183b71 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -9,7 +9,7 @@ import Stinsen import SwiftUI struct MovieItemView: View { - @EnvironmentObject var item: NavigationRouter + @EnvironmentObject var itemRouter: NavigationRouter @StateObject var viewModel: MovieItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) @@ -68,7 +68,6 @@ struct MovieItemView: View { HStack { // Play button Button { - self.playbackInfo.itemToPlay = viewModel.item self.playbackInfo.shouldShowPlayer = true } label: { HStack { @@ -139,7 +138,7 @@ struct MovieItemView: View { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in Button { - item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) } label: { Text(genre.name ?? "").font(.footnote) } @@ -156,7 +155,7 @@ struct MovieItemView: View { ForEach(viewModel.item.people!, id: \.self) { person in if person.type ?? "" == "Actor" { Button { - item.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) } label: { VStack { ImageView(src: person @@ -186,7 +185,7 @@ struct MovieItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } @@ -204,7 +203,7 @@ struct MovieItemView: View { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in Button { - item.route(to: .item(viewModel: .init(id: similarItem.id!))) + itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) } label: { PortraitItemView(item: similarItem) } @@ -236,7 +235,6 @@ struct MovieItemView: View { .cornerRadius(10) Spacer().frame(height: 15) Button { - self.playbackInfo.itemToPlay = viewModel.item self.playbackInfo.shouldShowPlayer = true } label: { HStack { @@ -339,7 +337,7 @@ struct MovieItemView: View { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.genreItems!, id: \.id) { genre in Button { - item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) } label: { Text(genre.name ?? "").font(.footnote) } @@ -358,7 +356,7 @@ struct MovieItemView: View { ForEach(viewModel.item.people!, id: \.self) { person in if person.type! == "Actor" { Button { - item + itemRouter .route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) } label: { @@ -392,7 +390,7 @@ struct MovieItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } @@ -412,7 +410,7 @@ struct MovieItemView: View { Spacer().frame(width: 16) ForEach(viewModel.similarItems, id: \.self) { similarItem in Button { - item.route(to: .item(viewModel: .init(id: similarItem.id!))) + itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) } label: { PortraitItemView(item: similarItem) } diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 8464ded4..08b03552 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -11,7 +11,7 @@ import Stinsen import SwiftUI struct NextUpView: View { - @EnvironmentObject var home: NavigationRouter + @EnvironmentObject var homeRouter: NavigationRouter var items: [BaseItemDto] @@ -25,7 +25,7 @@ struct NextUpView: View { LazyHStack { ForEach(items, id: \.id) { item in Button { - home.route(to: .item(viewModel: .init(id: item.id!))) + homeRouter.route(to: .item(viewModel: .init(id: item.id!))) } label: { PortraitItemView(item: item) } diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 541c4b26..8c52b78c 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -9,7 +9,7 @@ import Stinsen import SwiftUI struct SeasonItemView: View { - @EnvironmentObject var item: NavigationRouter + @EnvironmentObject var itemRouter: NavigationRouter @StateObject var viewModel: SeasonItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) var hSizeClass @@ -68,7 +68,7 @@ struct SeasonItemView: View { .padding(.trailing, 16) ForEach(viewModel.episodes, id: \.id) { episode in Button { - item.route(to: .item(viewModel: .init(id: episode.id!))) + itemRouter.route(to: .item(viewModel: .init(id: episode.id!))) } label: { HStack { ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) @@ -134,7 +134,7 @@ struct SeasonItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } @@ -184,7 +184,7 @@ struct SeasonItemView: View { .padding(.trailing, 16) ForEach(viewModel.episodes, id: \.id) { episode in Button { - item.route(to: .item(viewModel: .init(id: episode.id!))) + itemRouter.route(to: .item(viewModel: .init(id: episode.id!))) } label: { HStack { ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) @@ -229,7 +229,7 @@ struct SeasonItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(viewModel.item.studios!, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index d03d4920..76db1495 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -9,7 +9,7 @@ import Stinsen import SwiftUI struct SeriesItemView: View { - @EnvironmentObject var item: NavigationRouter + @EnvironmentObject var itemRouter: NavigationRouter @StateObject var viewModel: SeriesItemViewModel @State private var orientation = UIDeviceOrientation.unknown @Environment(\.horizontalSizeClass) var hSizeClass @@ -78,7 +78,7 @@ struct SeriesItemView: View { Text("Genres:").font(.callout).fontWeight(.semibold) ForEach(genreItems, id: \.id) { genre in Button { - item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) } label: { Text(genre.name ?? "").font(.footnote) } @@ -101,7 +101,7 @@ struct SeriesItemView: View { Text("Studios:").font(.callout).fontWeight(.semibold) ForEach(studios, id: \.id) { studio in Button { - item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) + itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) } label: { Text(studio.name ?? "").font(.footnote) } @@ -119,7 +119,7 @@ struct SeriesItemView: View { LazyVGrid(columns: tracks) { ForEach(viewModel.seasons, id: \.id) { season in Button { - item.route(to: .item(viewModel: .init(id: season.id!))) + itemRouter.route(to: .item(viewModel: .init(id: season.id!))) } label: { PortraitItemView(item: season) } @@ -140,7 +140,7 @@ struct SeriesItemView: View { ForEach(people, id: \.self) { person in if person.type == "Actor" { Button { - item + itemRouter .route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) } label: { @@ -176,7 +176,7 @@ struct SeriesItemView: View { LazyHStack(spacing: 16) { ForEach(viewModel.similarItems, id: \.self) { similarItem in Button { - item.route(to: .item(viewModel: .init(id: similarItem.id!))) + itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) } label: { PortraitItemView(item: similarItem) } diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index aaea3ef0..56949687 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -11,8 +11,8 @@ import Stinsen import SwiftUI struct SettingsView: View { - @EnvironmentObject var main: ViewRouter - @EnvironmentObject var settings: NavigationRouter + @EnvironmentObject var mainRouter: ViewRouter + @EnvironmentObject var settingsRouter: NavigationRouter @Environment(\.managedObjectContext) private var viewContext @ObservedObject var viewModel: SettingsViewModel @@ -97,16 +97,16 @@ struct SettingsView: View { Spacer() Button { print("logging out") - main.route(to: .connectToServer) - settings.dismiss() + mainRouter.route(to: .connectToServer) + settingsRouter.dismiss() } label: { Text("Switch user").font(.callout) } } Button { SessionManager.current.logout() - main.route(to: .connectToServer) - settings.dismiss() + mainRouter.route(to: .connectToServer) + settingsRouter.dismiss() } label: { Text("Sign out").font(.callout) } @@ -116,7 +116,7 @@ struct SettingsView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { Button { - settings.dismiss() + settingsRouter.dismiss() } label: { Image(systemName: "xmark") } diff --git a/JellyfinPlayer/SplashView.swift b/JellyfinPlayer/SplashView.swift index d69e6dd5..7444b269 100644 --- a/JellyfinPlayer/SplashView.swift +++ b/JellyfinPlayer/SplashView.swift @@ -11,16 +11,16 @@ import Stinsen import SwiftUI struct SplashView: View { - @EnvironmentObject var main: ViewRouter + @EnvironmentObject var mainRouter: ViewRouter @StateObject var viewModel = SplashViewModel() var body: some View { ProgressView() .onReceive(viewModel.$isLoggedIn) { flag in if flag { - main.route(to: .mainTab) + mainRouter.route(to: .mainTab) } else { - main.route(to: .connectToServer) + mainRouter.route(to: .connectToServer) } } } diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index ac7b38ad..cef3bf52 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -5,15 +5,15 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI -import MobileVLCKit +import Combine +import Defaults +import GoogleCast import JellyfinAPI import MediaPlayer -import Combine -import GoogleCast -import SwiftyJSON -import Defaults +import MobileVLCKit import Stinsen +import SwiftUI +import SwiftyJSON enum PlayerDestination { case remote @@ -27,10 +27,9 @@ protocol PlayerViewControllerDelegate: AnyObject { } class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { - @RouterObject var main: ViewRouter? - + weak var delegate: PlayerViewControllerDelegate? var cancellables = Set() @@ -68,9 +67,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe private var castDiscoveryManager: GCKDiscoveryManager { return GCKCastContext.sharedInstance().discoveryManager } + private var castSessionManager: GCKSessionManager { return GCKCastContext.sharedInstance().sessionManager } + var hasSentRemoteSeek: Bool = false var selectedPlaybackSpeedIndex: Int = 3 @@ -84,17 +85,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var jumpForwardLength: VideoPlayerJumpLength { return Defaults[.videoPlayerJumpForward] } + var jumpBackwardLength: VideoPlayerJumpLength { return Defaults[.videoPlayerJumpBackward] } - - var manifest: BaseItemDto = BaseItemDto() + + var manifest = BaseItemDto() var playbackItem = PlaybackItem() var remoteTimeUpdateTimer: Timer? - var upNextViewModel: UpNextViewModel = UpNextViewModel() - var lastOri: UIInterfaceOrientation? = nil + var upNextViewModel = UpNextViewModel() + var lastOri: UIInterfaceOrientation? // MARK: IBActions + @IBAction func seekSliderStart(_ sender: Any) { if playerDestination == .local { sendProgressReport(eventName: "pause") @@ -105,33 +108,36 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } @IBAction func seekSliderValueChanged(_ sender: Any) { - let videoDuration: Double = Double(manifest.runTimeTicks! / Int64(10_000_000)) + let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000)) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let secondsScrubbedRemaining = videoDuration - secondsScrubbedTo - + timeText.text = calculateTimeText(from: secondsScrubbedTo) timeLeftText.text = calculateTimeText(from: secondsScrubbedRemaining) } - + private func calculateTimeText(from duration: Double) -> String { let hours = floor(duration / 3600) - let minutes = (duration.truncatingRemainder(dividingBy: 3600)) / 60 - let seconds = (duration.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60) - + let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60 + let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60) + let timeText: String - + if hours != 0 { - timeText = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" + timeText = + "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" } else { - timeText = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" + timeText = + "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" } - + return timeText } @IBAction func seekSliderEnd(_ sender: Any) { isSeeking = false - let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : Double(remotePositionTicks / Int(10_000_000)) + let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : + Double(remotePositionTicks / Int(10_000_000)) let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000)) // Scrub is value from 0..1 - find position in video and add / or remove. let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) @@ -147,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe sendProgressReport(eventName: "unpause") } else { sendJellyfinCommand(command: "Seek", options: [ - "position": Int(secondsScrubbedTo) + "position": Int(secondsScrubbedTo), ]) } } @@ -184,7 +190,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if playerDestination == .local { mediaPlayer.jumpBackward(jumpBackwardLength.rawValue) } else { - self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000) - Int(jumpBackwardLength.rawValue)]) + sendJellyfinCommand(command: "Seek", + options: ["position": (remotePositionTicks / 10_000_000) - Int(jumpBackwardLength.rawValue)]) } } } @@ -194,7 +201,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if playerDestination == .local { mediaPlayer.jumpForward(jumpForwardLength.rawValue) } else { - self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000) + Int(jumpForwardLength.rawValue)]) + sendJellyfinCommand(command: "Seek", + options: ["position": (remotePositionTicks / 10_000_000) + Int(jumpForwardLength.rawValue)]) } } } @@ -232,7 +240,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton // Present the view controller (in a popover). - self.present(optionsVC!, animated: true) { + present(optionsVC!, animated: true) { print("popover visible, pause playback") self.mediaPlayer.pause() self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) @@ -240,6 +248,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } // MARK: Cast methods + @IBAction func castButtonPressed(_ sender: Any) { if selectedCastDevice == nil { LogManager.shared.log.debug("Presenting Cast modal") @@ -250,7 +259,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castDeviceVC?.popoverPresentationController?.sourceView = castButton // Present the view controller (in a popover). - self.present(castDeviceVC!, animated: true) { + present(castDeviceVC!, animated: true) { self.mediaPlayer.pause() self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } @@ -258,8 +267,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe LogManager.shared.log.info("Stopping casting session: button was pressed.") castSessionManager.endSessionAndStopCasting(true) selectedCastDevice = nil - self.castButton.isEnabled = true - self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) + castButton.isEnabled = true + castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) playerDestination = .local } } @@ -268,9 +277,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe LogManager.shared.log.debug("Cast modal dismissed") castDeviceVC?.dismiss(animated: true, completion: nil) if playerDestination == .local { - self.mediaPlayer.play() + mediaPlayer.play() } - self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } func castDeviceChanged() { @@ -284,11 +293,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } // MARK: Cast End + func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) if playerDestination == .local { - self.mediaPlayer.play() - self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + mediaPlayer.play() + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } } @@ -330,7 +340,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mediaPlayer.jumpForward(30) self.sendProgressReport(eventName: "timeupdate") } else { - self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)+30]) + self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks / 10_000_000) + 30]) } return .success } @@ -341,14 +351,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mediaPlayer.jumpBackward(15) self.sendProgressReport(eventName: "timeupdate") } else { - self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)-15]) + self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks / 10_000_000) - 15]) } return .success } // Scrubber - commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in - guard let self = self else {return .commandFailed} + commandCenter.changePlaybackPositionCommand.addTarget { [weak self] (remoteEvent) -> MPRemoteCommandHandlerStatus in + guard let self = self else { return .commandFailed } if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { let targetSeconds = event.positionTime @@ -358,14 +368,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if self.playerDestination == .local { if offset > 0 { - self.mediaPlayer.jumpForward(Int32(offset)/1000) + self.mediaPlayer.jumpForward(Int32(offset) / 1000) } else { - self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + self.mediaPlayer.jumpBackward(Int32(abs(offset)) / 1000) } self.sendProgressReport(eventName: "unpause") - } else { - - } + } else {} return .success } else { @@ -387,17 +395,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video" - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { if let artworkImage = UIImage(data: imageData as Data) { - let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in - return artworkImage + let artwork = MPMediaItemArtwork(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in + artworkImage }) - nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork + nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } } @@ -407,6 +415,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } // MARK: viewDidLoad + override func viewDidLoad() { super.viewDidLoad() if manifest.type == "Movie" { @@ -421,8 +430,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe DispatchQueue.main.async { self.lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? nil AppDelegate.orientationLock = .landscape - - if(self.lastOri != nil) { + + if self.lastOri != nil { if !self.lastOri!.isLandscape { UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() @@ -430,7 +439,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } - NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), + name: UIDevice.orientationDidChangeNotification, object: nil) } @objc func didChangedOrientation() { @@ -451,7 +461,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let totalDevices = castDiscoveryManager.deviceCount discoveredCastDevices = [] if totalDevices > 0 { - for i in 0...totalDevices-1 { + for i in 0 ... totalDevices - 1 { let device = castDiscoveryManager.device(at: i) discoveredCastDevices.append(device) } @@ -470,11 +480,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - self.tabBarController?.tabBar.isHidden = false - self.navigationController?.isNavigationBarHidden = false + tabBarController?.tabBar.isHidden = false + navigationController?.isNavigationBarHidden = false overrideUserInterfaceStyle = .unspecified DispatchQueue.main.async { - if(self.lastOri != nil) { + if self.lastOri != nil { AppDelegate.orientationLock = .all UIDevice.current.setValue(self.lastOri!.rawValue, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() @@ -483,11 +493,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } // MARK: viewDidAppear + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) overrideUserInterfaceStyle = .dark - self.tabBarController?.tabBar.isHidden = true - self.navigationController?.isNavigationBarHidden = true + tabBarController?.tabBar.isHidden = true + navigationController?.isNavigationBarHidden = true mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) // mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") @@ -500,7 +511,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func setupMediaPlayer() { - // Fetch max bitrate from UserDefaults depending on current connection mode let maxBitrate = Defaults[.inNetworkBandwidth] print(maxBitrate) @@ -508,16 +518,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) + let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), + startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, + autoOpenLiveStream: true) DispatchQueue.global(qos: .userInitiated).async { [self] in delegate?.showLoadingView(self) - MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) + MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, + maxStreamingBitrate: Int(maxBitrate), + startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, + playbackInfoDto: playbackInfo) .sink(receiveCompletion: { completion in switch completion { case .finished: break - case .failure(let error): + case let .failure(error): if let err = error as? ErrorResponse { switch err { case .error(401, _, _, _): @@ -528,7 +543,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.delegate?.exitPlayer(self) } } - break } }, receiveValue: { [self] response in dump(response) @@ -541,7 +555,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoType = .transcode item.videoUrl = streamURL! - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", + languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -553,7 +568,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, + delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", + languageCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -561,7 +578,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", + id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -570,7 +588,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if selectedAudioTrack == -1 { - if audioTrackArray.count > 0 { + if !audioTrackArray.isEmpty { selectedAudioTrack = audioTrackArray[0].id } } @@ -579,13 +597,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe playbackItem = item } else { // Item will be directly played by the client. - let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! + let streamURL = + URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! let item = PlaybackItem() item.videoUrl = streamURL item.videoType = .directPlay - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", + languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -597,7 +617,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, + delivery: stream.deliveryMethod!, codec: stream.codec!, + languageCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -605,7 +627,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", + id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -614,7 +637,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if selectedAudioTrack == -1 { - if audioTrackArray.count > 0 { + if !audioTrackArray.isEmpty { selectedAudioTrack = audioTrackArray[0].id } } @@ -630,7 +653,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe .store(in: &cancellables) } } - + private func setupJumpLengthButtons() { let buttonFont = UIFont.systemFont(ofSize: 35, weight: .regular) jumpForwardButton.setImage(jumpForwardLength.generateForwardImage(with: buttonFont), for: .normal) @@ -641,7 +664,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { if Defaults[.autoSelectSubtitlesLangCode] == "Auto", - subtitle.languageCode.contains(Locale.current.languageCode ?? "") { + subtitle.languageCode.contains(Locale.current.languageCode ?? "") + { selectedCaptionTrack = subtitle.id mediaPlayer.currentVideoSubTitleIndex = subtitle.id } else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) { @@ -688,21 +712,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe subtitleTrackArray.forEach { sub in // stupid fxcking jeff decides to re-encode these when added. // only add playback streams when codec not supported by VLC. - if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { + if sub.id != -1, sub.delivery == .external, sub.codec != "subrip" { mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false) } } } - self.mediaHasStartedPlaying() + mediaHasStartedPlaying() delegate?.hideLoadingView(self) videoContentView.setNeedsLayout() videoContentView.setNeedsDisplay() - self.view.setNeedsLayout() - self.view.setNeedsDisplay() - self.videoControlsView.setNeedsLayout() - self.videoControlsView.setNeedsDisplay() + view.setNeedsLayout() + view.setNeedsDisplay() + videoControlsView.setNeedsLayout() + videoControlsView.setNeedsDisplay() mediaPlayer.pause() mediaPlayer.play() @@ -710,6 +734,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } // MARK: VideoPlayerSettings Delegate + func subtitleTrackChanged(newTrackID: Int32) { selectedCaptionTrack = newTrackID mediaPlayer.currentVideoSubTitleIndex = newTrackID @@ -736,7 +761,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Create the swiftUI view let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel)) - self.upNextView.addSubview(contentView.view) + upNextView.addSubview(contentView.view) contentView.view.backgroundColor = .clear contentView.view.translatesAutoresizingMaskIntoConstraints = false contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true @@ -746,7 +771,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func getNextEpisode() { - TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, limit: 2) + TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, + limit: 2) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { [self] response in @@ -795,21 +821,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe setupMediaPlayer() getNextEpisode() } - } // MARK: - GCKGenericChannelDelegate + extension PlayerViewController: GCKGenericChannelDelegate { @objc func updateRemoteTime() { castButton.setImage(UIImage(named: "CastConnected"), for: .normal) if !paused { - remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt. + remotePositionTicks = remotePositionTicks + 2_000_000 // add 0.2 secs every timer evt. } if isSeeking == false { - let positiveSeconds = Double(remotePositionTicks/10_000_000) - let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000) - + let positiveSeconds = Double(remotePositionTicks / 10_000_000) + let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks)) / 10_000_000) + timeText.text = calculateTimeText(from: positiveSeconds) timeLeftText.text = calculateTimeText(from: remainingSeconds) @@ -828,14 +854,15 @@ extension PlayerViewController: GCKGenericChannelDelegate { if hasSentRemoteSeek == false { hasSentRemoteSeek = true sendJellyfinCommand(command: "Seek", options: [ - "position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position) + "position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position), ]) } } paused = json["data"]["PlayState"]["IsPaused"].boolValue - self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0 + remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0 if remoteTimeUpdateTimer == nil { - remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true) + remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), + userInfo: nil, repeats: true) } } } @@ -853,7 +880,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { "serverId": ServerEnvironment.current.server.server_id!, "serverVersion": "10.8.0", "receiverName": castSessionManager.currentCastSession!.device.friendlyName!, - "subtitleBurnIn": false + "subtitleBurnIn": false, ] let jsonData = JSON(payload) @@ -862,7 +889,13 @@ extension PlayerViewController: GCKGenericChannelDelegate { if command == "Seek" { remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000) // Send playback report as Jellyfin Chromecast isn't smarter than a rock. - let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") + let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, + mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), + subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, + positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), + volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, + liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, + nowPlayingQueue: [], playlistItemId: "playlistItem0") PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in @@ -876,9 +909,10 @@ extension PlayerViewController: GCKGenericChannelDelegate { } // MARK: - GCKSessionManagerListener + extension PlayerViewController: GCKSessionManagerListener { func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { - self.sendStopReport() + sendStopReport() mediaPlayer.stop() playerDestination = .remote @@ -896,25 +930,25 @@ extension PlayerViewController: GCKSessionManagerListener { let playNowOptions: [String: Any] = [ "items": [[ - "Id": self.manifest.id!, + "Id": manifest.id!, "ServerId": ServerEnvironment.current.server.server_id!, - "Name": self.manifest.name!, - "Type": self.manifest.type!, - "MediaType": self.manifest.mediaType!, - "IsFolder": self.manifest.isFolder! - ]] + "Name": manifest.name!, + "Type": manifest.type!, + "MediaType": manifest.mediaType!, + "IsFolder": manifest.isFolder!, + ]], ] sendJellyfinCommand(command: "PlayNow", options: playNowOptions) } func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { - self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") - self.sessionDidStart(manager: sessionManager, didStart: session) + jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") + sessionDidStart(manager: sessionManager, didStart: session) } func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { - self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") - self.sessionDidStart(manager: sessionManager, didStart: session) + jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") + sessionDidStart(manager: sessionManager, didStart: session) } func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { @@ -943,30 +977,29 @@ extension PlayerViewController: GCKSessionManagerListener { } // MARK: - VLCMediaPlayer Delegates + extension PlayerViewController: VLCMediaPlayerDelegate { func mediaPlayerStateChanged(_ aNotification: Notification!) { let currentState: VLCMediaPlayerState = mediaPlayer.state switch currentState { - case .stopped : + case .stopped: LogManager.shared.log.debug("Player state changed: STOPPED") - break - case .ended : + case .ended: LogManager.shared.log.debug("Player state changed: ENDED") - break - case .playing : + case .playing: LogManager.shared.log.debug("Player state changed: PLAYING") sendProgressReport(eventName: "unpause") delegate?.hideLoadingView(self) paused = false - case .paused : + case .paused: LogManager.shared.log.debug("Player state changed: PAUSED") paused = true - case .opening : + case .opening: LogManager.shared.log.debug("Player state changed: OPENING") - case .buffering : + case .buffering: LogManager.shared.log.debug("Player state changed: BUFFERING") delegate?.showLoadingView(self) - case .error : + case .error: LogManager.shared.log.error("Video had error.") sendStopReport() case .esAdded: @@ -978,19 +1011,19 @@ extension PlayerViewController: VLCMediaPlayerDelegate { func mediaPlayerTimeChanged(_ aNotification: Notification!) { let time = mediaPlayer.position - if abs(time-lastTime) > 0.00005 { + if abs(time - lastTime) > 0.00005 { paused = false mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) seekSlider.setValue(mediaPlayer.position, animated: true) delegate?.hideLoadingView(self) - if manifest.type == "Episode" && upNextViewModel.item != nil { + if manifest.type == "Episode", upNextViewModel.item != nil { if time > 0.96 { upNextView.isHidden = false - self.jumpForwardButton.isHidden = true + jumpForwardButton.isHidden = true } else { upNextView.isHidden = true - self.jumpForwardButton.isHidden = false + jumpForwardButton.isHidden = false } } @@ -998,7 +1031,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { timeLeftText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst()) if CACurrentMediaTime() - controlsAppearTime > 5 { - self.smallNextUpView() + smallNextUpView() UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { self.videoControlsView.alpha = 0.0 }, completion: { (_: Bool) in @@ -1018,42 +1051,61 @@ extension PlayerViewController: VLCMediaPlayerDelegate { } } +struct VideoPlayerView: View { + var item: BaseItemDto + @State private var isLoading = false + + var body: some View { + // Loading UI needs to be moved into ViewController later + LoadingViewNoBlur(isShowing: $isLoading) { + VLCPlayerWithControls(item: item, loadBinding: $isLoading) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .statusBar(hidden: true) + .edgesIgnoringSafeArea(.all) + .prefersHomeIndicatorAutoHidden(true) + } + } +} + // MARK: End VideoPlayerVC + struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto - @Environment(\.presentationMode) var presentationMode + @RouterObject var playerRouter: NavigationRouter? - var loadBinding: Binding - var pBinding: Binding + let loadBinding: Binding class Coordinator: NSObject, PlayerViewControllerDelegate { + let parent: VLCPlayerWithControls let loadBinding: Binding - let pBinding: Binding - init(loadBinding: Binding, pBinding: Binding) { + init(parent: VLCPlayerWithControls, loadBinding: Binding) { + self.parent = parent self.loadBinding = loadBinding - self.pBinding = pBinding } func hideLoadingView(_ viewController: PlayerViewController) { - self.loadBinding.wrappedValue = false + loadBinding.wrappedValue = false } func showLoadingView(_ viewController: PlayerViewController) { - self.loadBinding.wrappedValue = true + loadBinding.wrappedValue = true } func exitPlayer(_ viewController: PlayerViewController) { - self.pBinding.wrappedValue = false + parent.playerRouter?.dismiss() } } func makeCoordinator() -> Coordinator { - Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding) + Coordinator(parent: self, loadBinding: loadBinding) } typealias UIViewControllerType = PlayerViewController - func makeUIViewController(context: UIViewControllerRepresentableContext) -> VLCPlayerWithControls.UIViewControllerType { + func makeUIViewController(context: UIViewControllerRepresentableContext) -> VLCPlayerWithControls + .UIViewControllerType + { let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController customViewController.manifest = item @@ -1061,20 +1113,27 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { return customViewController } - func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext) { - } + func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, + context: UIViewControllerRepresentableContext) {} } // MARK: - Play State Update Methods + extension PlayerViewController { func sendProgressReport(eventName: String) { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { - var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)) + var ticks = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)) if ticks == 0 { ticks = manifest.userData?.playbackPositionTicks ?? 0 } - let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") + let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, + mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), + subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: mediaPlayer.state == .paused, + isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime), + volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, + liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, + nowPlayingQueue: [], playlistItemId: "playlistItem0") PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in @@ -1087,7 +1146,10 @@ extension PlayerViewController { } func sendStopReport() { - let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) + let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, + positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, + playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", + nowPlayingQueue: []) PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) .sink(receiveCompletion: { result in @@ -1103,7 +1165,13 @@ extension PlayerViewController { print("sending play report!") - let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") + let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, + mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), + subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, + positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), + volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, + liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], + playlistItemId: "playlistItem0") PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) .sink(receiveCompletion: { result in @@ -1116,7 +1184,7 @@ extension PlayerViewController { } extension UINavigationController { - open override var childForHomeIndicatorAutoHidden: UIViewController? { + override open var childForHomeIndicatorAutoHidden: UIViewController? { return nil } } From a16d70ab11a99e0e1281fe6f66f6aaade5fc84e8 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 18:03:04 +0900 Subject: [PATCH 05/18] Respond to main branch --- .../Coordinators/HomeCoordinator.swift | 2 +- .../Coordinators/SettingsCoordinator.swift | 11 +- JellyfinPlayer/SettingsView.swift | 193 ++++++++---------- JellyfinPlayer/VideoPlayer.swift | 1 - 4 files changed, 94 insertions(+), 113 deletions(-) diff --git a/JellyfinPlayer/Coordinators/HomeCoordinator.swift b/JellyfinPlayer/Coordinators/HomeCoordinator.swift index ac152866..e4369b06 100644 --- a/JellyfinPlayer/Coordinators/HomeCoordinator.swift +++ b/JellyfinPlayer/Coordinators/HomeCoordinator.swift @@ -23,7 +23,7 @@ final class HomeCoordinator: NavigationCoordinatable { func resolveRoute(route: Route) -> Transition { switch route { case .settings: - return .modal(SettingsCoordinator().eraseToAnyCoordinatable()) + return .modal(NavigationViewCoordinator(SettingsCoordinator()).eraseToAnyCoordinatable()) case let .library(viewModel, title): return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) case let .item(viewModel): diff --git a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift b/JellyfinPlayer/Coordinators/SettingsCoordinator.swift index 07c23532..15e05678 100644 --- a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift +++ b/JellyfinPlayer/Coordinators/SettingsCoordinator.swift @@ -14,9 +14,16 @@ import SwiftUI final class SettingsCoordinator: NavigationCoordinatable { var navigationStack = NavigationStack() - enum Route: NavigationRoute {} + enum Route: NavigationRoute { + case serverDetail + } - func resolveRoute(route: Route) -> Transition {} + func resolveRoute(route: Route) -> Transition { + switch route { + case .serverDetail: + return .push(ServerDetailView().eraseToAnyView()) + } + } @ViewBuilder func start() -> some View { diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index c78cc886..769f2615 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -11,7 +11,7 @@ import Stinsen import SwiftUI struct SettingsView: View { - @EnvironmentObject var mainRouter: ViewRouter + @RouterObject var mainRouter: ViewRouter? @EnvironmentObject var settingsRouter: NavigationRouter @Environment(\.managedObjectContext) private var viewContext @@ -27,125 +27,100 @@ struct SettingsView: View { @Default(.videoPlayerJumpBackward) var jumpBackwardLength var body: some View { - NavigationView { - Form { - Section(header: EmptyView()) { + Form { + Section { + HStack { + Text("User") + Spacer() + Text(SessionManager.current.user.username ?? "") + .foregroundColor(.jellyfinPurple) + } + + Button { + settingsRouter.route(to: .serverDetail) + } label: { HStack { - Text("User") + Text("Server") Spacer() - Text(SessionManager.current.user.username ?? "") + Text(ServerEnvironment.current.server.name ?? "") .foregroundColor(.jellyfinPurple) } - - NavigationLink( - destination: ServerDetailView(), - label: { - HStack { - Text("Server") - Spacer() - Text(ServerEnvironment.current.server.name ?? "") - .foregroundColor(.jellyfinPurple) - } - }) - - Button { - close = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - SessionManager.current.logout() - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } - } label: { - Text("Sign out") - .font(.callout) - } - } - Section(header: Text("Playback")) { - Picker("Default local quality", selection: $inNetworkStreamBitrate) { - ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - Text(bitrate.name).tag(bitrate.value) - } - } - - Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { - ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - Text(bitrate.name).tag(bitrate.value) - } - } - - Picker("Jump Forward Length", selection: $jumpForwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } - - Picker("Jump Backward Length", selection: $jumpBackwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } } - Section(header: Text("Accessibility")) { - Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - SearchablePicker(label: "Preferred subtitle language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding(get: { - viewModel.langs - .first(where: { $0.isoCode == autoSelectSubtitlesLangcode - }) ?? - .auto - }, - set: { autoSelectSubtitlesLangcode = $0.isoCode })) - SearchablePicker(label: "Preferred audio language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding(get: { - viewModel.langs - .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? - .auto - }, - set: { autoSelectAudioLangcode = $0.isoCode })) - Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { - ForEach(self.viewModel.appearances, id: \.self) { appearance in - Text(appearance.localizedName).tag(appearance.rawValue) - } - }.onChange(of: appAppearance, perform: { value in - guard let appearance = AppAppearance(rawValue: value) else { return } - UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appearance.style - }) - } - - Section(header: Text(ServerEnvironment.current.server.name ?? "")) { - HStack { - Text("Signed in as \(username)").foregroundColor(.primary) - Spacer() - Button { - print("logging out") - mainRouter.route(to: .connectToServer) - settingsRouter.dismiss() - } label: { - Text("Switch user").font(.callout) - } - } - Button { + Button { + settingsRouter.dismiss { SessionManager.current.logout() - mainRouter.route(to: .connectToServer) - settingsRouter.dismiss() - } label: { - Text("Sign out").font(.callout) + mainRouter?.route(to: .connectToServer) + } + } label: { + Text("Sign out") + .font(.callout) + } + } + Section(header: Text("Playback")) { + Picker("Default local quality", selection: $inNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + } + } + + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + } + } + + Picker("Jump Forward Length", selection: $jumpForwardLength) { + ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } + + Picker("Jump Backward Length", selection: $jumpBackwardLength) { + ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + Text(length.label).tag(length.rawValue) } } } - .navigationBarTitle("Settings", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - settingsRouter.dismiss() - } label: { - Image(systemName: "xmark") + + Section(header: Text("Accessibility")) { + Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) + SearchablePicker(label: "Preferred subtitle language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding(get: { + viewModel.langs + .first(where: { $0.isoCode == autoSelectSubtitlesLangcode + }) ?? + .auto + }, + set: { autoSelectSubtitlesLangcode = $0.isoCode })) + SearchablePicker(label: "Preferred audio language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding(get: { + viewModel.langs + .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? + .auto + }, + set: { autoSelectAudioLangcode = $0.isoCode })) + Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { + ForEach(self.viewModel.appearances, id: \.self) { appearance in + Text(appearance.localizedName).tag(appearance.rawValue) } + }.onChange(of: appAppearance, perform: { value in + guard let appearance = AppAppearance(rawValue: value) else { return } + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appearance.style + }) + } + } + .navigationBarTitle("Settings", displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + settingsRouter.dismiss() + } label: { + Image(systemName: "xmark") } } } diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index cef3bf52..f4799009 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -28,7 +28,6 @@ protocol PlayerViewControllerDelegate: AnyObject { class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { @RouterObject - var main: ViewRouter? weak var delegate: PlayerViewControllerDelegate? From 252c7a62edfc7ed71674ed2194e00763c4b605b5 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 18:09:34 +0900 Subject: [PATCH 06/18] fix tvOS build error --- JellyfinPlayer.xcodeproj/project.pbxproj | 10 +++++++ .../Coordinators/MainCoordinator.swift | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index e4075a38..85fc1d2e 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -187,6 +187,8 @@ 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; }; 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; 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 */; }; 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 */; }; @@ -488,6 +490,7 @@ files = ( 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */, 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */, + 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, 6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */, @@ -1057,6 +1060,7 @@ 53272534268BF9710035FBF1 /* SwiftUIFocusGuide */, 53649AAE269CFAF600A2D8B7 /* Puppy */, 6261A0DF26A0AB710072EF1C /* CombineExt */, + 6220D0C826D63F3700B8E046 /* Stinsen */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -1390,6 +1394,7 @@ E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, + 6220D0CA26D63F4D00B8E046 /* MainCoordinator.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, @@ -2240,6 +2245,11 @@ package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; + 6220D0C826D63F3700B8E046 /* Stinsen */ = { + isa = XCSwiftPackageProductDependency; + package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */; + productName = Stinsen; + }; 625CB5792678C4A400530A6E /* ActivityIndicator */ = { isa = XCSwiftPackageProductDependency; package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift index af41d6de..38dd8edd 100644 --- a/JellyfinPlayer/Coordinators/MainCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainCoordinator.swift @@ -11,6 +11,7 @@ import Foundation import Stinsen import SwiftUI +#if os(iOS) final class MainCoordinator: ViewCoordinatable { var children = ViewChild() @@ -33,3 +34,29 @@ final class MainCoordinator: ViewCoordinatable { SplashView() } } +#elseif os(tvOS) +// temp for fixing build error +final class MainCoordinator: ViewCoordinatable { + var children = ViewChild() + + enum Route: ViewRoute { + case mainTab + case connectToServer + } + + func resolveRoute(route: Route) -> AnyCoordinatable { + switch route { + case .mainTab: + return MainCoordinator().eraseToAnyCoordinatable() + case .connectToServer: + return MainCoordinator().eraseToAnyCoordinatable() + } + } + + @ViewBuilder + func start() -> some View { + SplashView() + } +} + +#endif From b544bd66cc303c482894d09b211fcecd7d070933 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 20:02:59 +0900 Subject: [PATCH 07/18] fix build error --- JellyfinPlayer/VideoPlayer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index f4799009..cef3bf52 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -28,6 +28,7 @@ protocol PlayerViewControllerDelegate: AnyObject { class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { @RouterObject + var main: ViewRouter? weak var delegate: PlayerViewControllerDelegate? From 5d96de329f494a28bb03c2339ca346463ca4cc80 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 20:15:57 +0900 Subject: [PATCH 08/18] 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)) From 3235f8046df85c3c8b6ad0d6e4f9d98d5776c9ab Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Thu, 26 Aug 2021 02:22:28 +0900 Subject: [PATCH 09/18] fix suggested changes --- JellyfinPlayer.xcodeproj/project.pbxproj | 14 +++++++------- JellyfinPlayer/SettingsView.swift | 1 + Shared/Singleton/AppURLHandler.swift | 15 --------------- Shared/ViewModels/ConnectToServerViewModel.swift | 4 ---- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 1e32bbae..e0ad0104 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -977,16 +977,16 @@ 62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = { isa = PBXGroup; children = ( + 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, + 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, + 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, + 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, + 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, + 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */, 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */, - 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, - 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, - 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */, - 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, - 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, - 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, - 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, + 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */, ); path = Coordinators; diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 769f2615..2f3d491e 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -44,6 +44,7 @@ struct SettingsView: View { Spacer() Text(ServerEnvironment.current.server.name ?? "") .foregroundColor(.jellyfinPurple) + Image(systemName: "chevron.right") } } diff --git a/Shared/Singleton/AppURLHandler.swift b/Shared/Singleton/AppURLHandler.swift index 33805a37..3b83027f 100644 --- a/Shared/Singleton/AppURLHandler.swift +++ b/Shared/Singleton/AppURLHandler.swift @@ -9,7 +9,6 @@ import Foundation import Stinsen -import URLNavigator final class AppURLHandler { static let deepLinkScheme = "jellyfin" @@ -46,7 +45,6 @@ extension AppURLHandler { 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) @@ -58,8 +56,6 @@ extension AppURLHandler { } func processLaunchedURLIfNeeded() { - print("!@#!@#!@#!@#!@#!@") - print(launchURL) guard let launchURL = launchURL else { return } if processDeepLink(url: launchURL) { self.launchURL = nil @@ -67,7 +63,6 @@ extension AppURLHandler { } private func processURL(_ url: URL) -> Bool { - print("processURL(_ url: URL) -> Bool") if processURLForUser(url: url) { return true } @@ -76,15 +71,6 @@ extension AppURLHandler { } 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 } @@ -92,7 +78,6 @@ extension AppURLHandler { 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 } diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 06d8ad54..d3cf7794 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -29,10 +29,6 @@ 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 { From 16e3cd6ea5bf12f71abe33512da08403c429275c Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Mon, 20 Sep 2021 20:32:04 +0900 Subject: [PATCH 10/18] migrate stinsen v1 to v2 --- JellyfinPlayer.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/swiftpm/Package.resolved | 24 +-- .../Components/PortraitItemView.swift | 154 ++++++++------- JellyfinPlayer/ConnectToServerView.swift | 4 +- JellyfinPlayer/ContinueWatchingView.swift | 23 +-- .../ConnectToServerCoodinator.swift | 11 +- .../Coordinators/FilterCoordinator.swift | 13 +- .../Coordinators/HomeCoordinator.swift | 32 ++-- .../Coordinators/ItemCoordinator.swift | 40 ++-- .../Coordinators/LibraryCoordinator.swift | 48 ++--- .../Coordinators/LibraryListCoordinator.swift | 22 +-- .../Coordinators/MainCoordinator.swift | 87 +++++---- .../Coordinators/MainTabCoordinator.swift | 45 ++--- .../Coordinators/SearchCoordinator.swift | 21 +-- .../Coordinators/SettingsCoordinator.swift | 19 +- .../Coordinators/VideoPlayerCoordinator.swift | 11 +- JellyfinPlayer/HomeView.swift | 20 +- JellyfinPlayer/ItemView/ItemView.swift | 42 +++-- .../Landscape/ItemLandscapeMainView.swift | 57 +++--- .../Portrait/ItemPortraitMainView.swift | 13 -- JellyfinPlayer/JellyfinPlayerApp.swift | 4 +- JellyfinPlayer/LatestMediaView.swift | 4 +- JellyfinPlayer/LibraryFilterView.swift | 8 +- JellyfinPlayer/LibraryListView.swift | 11 +- JellyfinPlayer/LibrarySearchView.swift | 4 +- JellyfinPlayer/LibraryView.swift | 10 +- JellyfinPlayer/NextUpView.swift | 4 +- JellyfinPlayer/SettingsView.swift | 176 +++++++++--------- JellyfinPlayer/SplashView.swift | 6 +- JellyfinPlayer/VideoPlayer.swift | 10 +- Shared/Singleton/AppURLHandler.swift | 6 +- .../ViewModels/ConnectToServerViewModel.swift | 13 +- 32 files changed, 462 insertions(+), 491 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 03d3805e..6815d46b 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -977,7 +977,6 @@ 6267B3D92671138200A7371D /* ImageExtensions.swift */, E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, 621338922660107500A81A2A /* StringExtensions.swift */, - 624C21742685CF60007F1390 /* SearchablePickerView.swift */, 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, ); path = Extensions; @@ -1535,7 +1534,7 @@ 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, E188460526DEF04800B0C5B7 /* CardVStackView.swift in Sources */, - 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, @@ -1551,7 +1550,7 @@ files = ( 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, - 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, @@ -1566,7 +1565,6 @@ 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, - 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, @@ -1607,7 +1605,7 @@ 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, - 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, + 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, @@ -1636,7 +1634,6 @@ 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, - 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, @@ -2246,7 +2243,7 @@ repositoryURL = "https://github.com/rundfunk47/stinsen"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.1.0; + minimumVersion = 2.0.2; }; }; 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = { diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1a18b422..d601cf81 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/Flight-School/AnyCodable", "state": { "branch": null, - "revision": "69261f239f0fffaf51495dadc4f8483fbfe97025", - "version": "0.6.1" + "revision": "b1a7a8a6186f2fcb28f7bda67cfc545de48b3c80", + "version": "0.6.2" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", "state": { "branch": null, - "revision": "6dcc7c034d28fe7ac652453faeae07656f723909", - "version": "0.5.1" + "revision": "6bde3b0063ba8e7537b43744948535ca7e9e0dad", + "version": "0.5.2" } }, { @@ -78,8 +78,8 @@ "repositoryURL": "https://github.com/kean/Nuke.git", "state": { "branch": null, - "revision": "3bd3a1765bdf62d561d4c2e10e1c4fc7a010f44e", - "version": "10.3.2" + "revision": "0db18dd34998cca18e9a28bcee136f84518007a0", + "version": "10.4.1" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/sushichop/Puppy", "state": { "branch": null, - "revision": "d670c669ce2a6ab554a903b815f461d6efc565e4", - "version": "0.3.0" + "revision": "95ce04b0e778b8d7c351876bc98bbf68328dfc9b", + "version": "0.3.1" } }, { @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/rundfunk47/stinsen", "state": { "branch": null, - "revision": "e72c20b2c4bde0d6c3a911d4eda688fee7aa3bba", - "version": "1.1.0" + "revision": "3d06c7603c70f8af1bd49f8d49f17e98f25b2d6a", + "version": "2.0.2" } }, { @@ -159,8 +159,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "b9eeb1a7ea3fd6fea54ce57dee2f5794b667c8df", - "version": "0.2.0" + "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", + "version": "0.2.1" } } ] diff --git a/JellyfinPlayer/Components/PortraitItemView.swift b/JellyfinPlayer/Components/PortraitItemView.swift index 90dc7738..8cc9e941 100644 --- a/JellyfinPlayer/Components/PortraitItemView.swift +++ b/JellyfinPlayer/Components/PortraitItemView.swift @@ -1,91 +1,85 @@ // - /* - * 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 - */ +/* + * 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 SwiftUI import JellyfinAPI +import SwiftUI struct PortraitItemView: View { - var item: BaseItemDto var body: some View { - NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) { - VStack(alignment: .leading) { - ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .shadow(radius: 4, y: 2) - .shadow(radius: 4, y: 2) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(ProgressBar()) - .frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7) - .padding(0), alignment: .bottomLeading - ) - .overlay( - ZStack { - if item.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } - } - .padding(.leading, 2) - .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) - .opacity(1), alignment: .bottomLeading) - .overlay( - ZStack { - if item.userData?.played ?? false { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - .background(Color(.white)) - .clipShape(Circle().scale(0.8)) - } else { - if item.userData?.unplayedItemCount != nil { - Capsule() - .fill(Color.accentColor) - .frame(minWidth: 20, minHeight: 20, maxHeight: 20) - Text(String(item.userData!.unplayedItemCount ?? 0)) - .foregroundColor(.white) - .font(.caption2) - .padding(2) - } - } - }.padding(2) - .fixedSize() - .opacity(1), alignment: .topTrailing).opacity(1) - Text(item.seriesName ?? item.name ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if item.type == "Movie" || item.type == "Series" { - Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else if item.type == "Season" { - Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) + VStack(alignment: .leading) { + ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), + bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) + .frame(width: 100, height: 150) + .cornerRadius(10) + .shadow(radius: 4, y: 2) + .shadow(radius: 4, y: 2) + .overlay(Rectangle() + .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .mask(ProgressBar()) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7) + .padding(0), alignment: .bottomLeading) + .overlay(ZStack { + if item.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) + } } - }.frame(width: 100) - } + .padding(.leading, 2) + .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) + .opacity(1), alignment: .bottomLeading) + .overlay(ZStack { + if item.userData?.played ?? false { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + .background(Color(.white)) + .clipShape(Circle().scale(0.8)) + } else { + if item.userData?.unplayedItemCount != nil { + Capsule() + .fill(Color.accentColor) + .frame(minWidth: 20, minHeight: 20, maxHeight: 20) + Text(String(item.userData!.unplayedItemCount ?? 0)) + .foregroundColor(.white) + .font(.caption2) + .padding(2) + } + } + }.padding(2) + .fixedSize() + .opacity(1), alignment: .topTrailing).opacity(1) + Text(item.seriesName ?? item.name ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + if item.type == "Movie" || item.type == "Series" { + Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else if item.type == "Season" { + Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else { + Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } + }.frame(width: 100) } } diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 552a5779..15f6fe04 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -9,7 +9,7 @@ import SwiftUI import Stinsen struct ConnectToServerView: View { - @EnvironmentObject var mainRouter: ViewRouter + @EnvironmentObject var mainRouter: MainCoordinator.Router @StateObject var viewModel = ConnectToServerViewModel() @State var username = "" @State var password = "" @@ -61,7 +61,7 @@ struct ConnectToServerView: View { if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) { let user = SessionManager.current.getSavedSession(userID: publicUser.id!) SessionManager.current.loginWithSavedSession(user: user) - mainRouter.route(to: .mainTab) + mainRouter.root(\.mainTab) } else { username = publicUser.name ?? "" viewModel.selectedPublicUser = publicUser diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index b0ed52d9..2b2702c4 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -6,8 +6,8 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI import JellyfinAPI +import SwiftUI struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { @@ -31,26 +31,27 @@ struct ProgressBar: Shape { } struct ContinueWatchingView: View { + @EnvironmentObject var homeRouter: HomeCoordinator.Router var items: [BaseItemDto] var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) { + Button { + homeRouter.route(to: \.item, item) + } label: { VStack(alignment: .leading) { ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) .frame(width: 320, height: 180) .cornerRadius(10) .shadow(radius: 4, y: 2) .shadow(radius: 4, y: 2) - .overlay( - Rectangle() - .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) - .mask(ProgressBar()) - .frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7) - .padding(0), alignment: .bottomLeading - ) + .overlay(Rectangle() + .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .mask(ProgressBar()) + .frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7) + .padding(0), alignment: .bottomLeading) HStack { Text("\(item.seriesName ?? item.name ?? "")") .font(.callout) @@ -68,11 +69,11 @@ struct ContinueWatchingView: View { Spacer() }.frame(width: 320, alignment: .leading) }.padding(.top, 10) - .padding(.bottom, 5) + .padding(.bottom, 5) } }.padding(.trailing, 16) }.frame(height: 215) - .padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2)) + .padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2)) } } } diff --git a/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift b/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift index ec5e712b..5f81bd85 100644 --- a/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift +++ b/JellyfinPlayer/Coordinators/ConnectToServerCoodinator.swift @@ -12,14 +12,11 @@ import Stinsen import SwiftUI final class ConnectToServerCoodinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \ConnectToServerCoodinator.start) - enum Route: NavigationRoute {} - - func resolveRoute(route: Route) -> Transition {} - - @ViewBuilder - func start() -> some View { + @Root var start = makeStart + + @ViewBuilder func makeStart() -> some View { ConnectToServerView() } } diff --git a/JellyfinPlayer/Coordinators/FilterCoordinator.swift b/JellyfinPlayer/Coordinators/FilterCoordinator.swift index 0e35e092..48496d14 100644 --- a/JellyfinPlayer/Coordinators/FilterCoordinator.swift +++ b/JellyfinPlayer/Coordinators/FilterCoordinator.swift @@ -11,8 +11,12 @@ import Foundation import Stinsen import SwiftUI +typealias FilterCoordinatorParams = (filters: Binding, enabledFilterType: [FilterType], parentId: String) + final class FilterCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \FilterCoordinator.start) + @Root var start = makeStart + @Binding var filters: LibraryFilters var enabledFilterType: [FilterType] var parentId: String = "" @@ -23,12 +27,7 @@ final class FilterCoordinator: NavigationCoordinatable { self.parentId = parentId } - enum Route: NavigationRoute {} - - func resolveRoute(route: Route) -> Transition {} - - @ViewBuilder - func start() -> some View { + @ViewBuilder func makeStart() -> some View { LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId) } } diff --git a/JellyfinPlayer/Coordinators/HomeCoordinator.swift b/JellyfinPlayer/Coordinators/HomeCoordinator.swift index e4369b06..be38b278 100644 --- a/JellyfinPlayer/Coordinators/HomeCoordinator.swift +++ b/JellyfinPlayer/Coordinators/HomeCoordinator.swift @@ -8,31 +8,31 @@ */ import Foundation +import JellyfinAPI import Stinsen import SwiftUI final class HomeCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \HomeCoordinator.start) - enum Route: NavigationRoute { - case settings - case library(viewModel: LibraryViewModel, title: String) - case item(viewModel: ItemViewModel) + @Root var start = makeStart + @Route(.modal) var settings = makeSettings + @Route(.push) var library = makeLibrary + @Route(.push) var item = makeItem + + func makeSettings() -> NavigationViewCoordinator { + NavigationViewCoordinator(SettingsCoordinator()) } - func resolveRoute(route: Route) -> Transition { - switch route { - case .settings: - return .modal(NavigationViewCoordinator(SettingsCoordinator()).eraseToAnyCoordinatable()) - case let .library(viewModel, title): - return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) - case let .item(viewModel): - return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) - } + func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { + LibraryCoordinator(viewModel: params.viewModel, title: params.title) } - @ViewBuilder - func start() -> some View { + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) + } + + @ViewBuilder func makeStart() -> some View { HomeView() } } diff --git a/JellyfinPlayer/Coordinators/ItemCoordinator.swift b/JellyfinPlayer/Coordinators/ItemCoordinator.swift index 559342f5..d8f4588f 100644 --- a/JellyfinPlayer/Coordinators/ItemCoordinator.swift +++ b/JellyfinPlayer/Coordinators/ItemCoordinator.swift @@ -13,32 +13,32 @@ import Stinsen import SwiftUI final class ItemCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() - var viewModel: ItemViewModel + let stack = NavigationStack(initial: \ItemCoordinator.start) - init(viewModel: ItemViewModel) { - self.viewModel = viewModel + @Root var start = makeStart + @Route(.push) var item = makeItem + @Route(.push) var library = makeLibrary + @Route(.fullScreen) var videoPlayer = makeVideoPlayer + + let itemDto: BaseItemDto + + init(item: BaseItemDto) { + self.itemDto = item } - enum Route: NavigationRoute { - case item(viewModel: ItemViewModel) - case library(viewModel: LibraryViewModel, title: String) - case videoPlayer(item: BaseItemDto) + func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { + LibraryCoordinator(viewModel: params.viewModel, title: params.title) } - func resolveRoute(route: Route) -> Transition { - switch route { - case let .item(viewModel): - return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) - case let .library(viewModel, title): - return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) - case let .videoPlayer(item): - return .fullScreen(NavigationViewCoordinator(VideoPlayerCoordinator(item: item)).eraseToAnyCoordinatable()) - } + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) } - @ViewBuilder - func start() -> some View { - ItemView(viewModel: self.viewModel) + func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(VideoPlayerCoordinator(item: item)) + } + + @ViewBuilder func makeStart() -> some View { + ItemNavigationView(item: itemDto) } } diff --git a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift b/JellyfinPlayer/Coordinators/LibraryCoordinator.swift index 63f71b24..47f45978 100644 --- a/JellyfinPlayer/Coordinators/LibraryCoordinator.swift +++ b/JellyfinPlayer/Coordinators/LibraryCoordinator.swift @@ -8,11 +8,20 @@ */ import Foundation +import JellyfinAPI import Stinsen import SwiftUI +typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String) + final class LibraryCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \LibraryCoordinator.start) + + @Root var start = makeStart + @Route(.push) var search = makeSearch + @Route(.modal) var filter = makeFilter + @Route(.push) var item = makeItem + var viewModel: LibraryViewModel var title: String @@ -21,28 +30,21 @@ final class LibraryCoordinator: NavigationCoordinatable { self.title = title } - enum Route: NavigationRoute { - case search(viewModel: LibrarySearchViewModel) - case filter(filters: Binding, enabledFilterType: [FilterType], parentId: String) - case item(viewModel: ItemViewModel) - } - - func resolveRoute(route: Route) -> Transition { - switch route { - case let .search(viewModel): - return .push(SearchCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) - case let .filter(filters, enabledFilterType, parentId): - return .modal(FilterCoordinator(filters: filters, - enabledFilterType: enabledFilterType, - parentId: parentId) - .eraseToAnyCoordinatable()) - case let .item(viewModel): - return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) - } - } - - @ViewBuilder - func start() -> some View { + @ViewBuilder func makeStart() -> some View { LibraryView(viewModel: self.viewModel, title: title) } + + func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { + SearchCoordinator(viewModel: viewModel) + } + + func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator { + NavigationViewCoordinator(FilterCoordinator(filters: params.filters, + enabledFilterType: params.enabledFilterType, + parentId: params.parentId)) + } + + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) + } } diff --git a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift b/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift index 370ebe8a..2ff63ad5 100644 --- a/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift +++ b/JellyfinPlayer/Coordinators/LibraryListCoordinator.swift @@ -12,24 +12,22 @@ import Stinsen import SwiftUI final class LibraryListCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \LibraryListCoordinator.start) - enum Route: NavigationRoute { - case search(viewModel: LibrarySearchViewModel) - case library(viewModel: LibraryViewModel, title: String) + @Root var start = makeStart + @Route(.push) var search = makeSearch + @Route(.push) var library = makeLibrary + + func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { + LibraryCoordinator(viewModel: params.viewModel, title: params.title) } - func resolveRoute(route: Route) -> Transition { - switch route { - case let .search(viewModel): - return .push(SearchCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) - case let .library(viewModel, title): - return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable()) - } + func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { + SearchCoordinator(viewModel: viewModel) } @ViewBuilder - func start() -> some View { + func makeStart() -> some View { LibraryListView() } } diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift index 38dd8edd..7b76e2fd 100644 --- a/JellyfinPlayer/Coordinators/MainCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainCoordinator.swift @@ -8,55 +8,68 @@ */ import Foundation +import Nuke import Stinsen import SwiftUI +#if !os(tvOS) + import WidgetKit +#endif #if os(iOS) -final class MainCoordinator: ViewCoordinatable { - var children = ViewChild() + final class MainCoordinator: NavigationCoordinatable { + var stack: NavigationStack - enum Route: ViewRoute { - case mainTab - case connectToServer - } + @Root var mainTab = makeMainTab + @Root var connectToServer = makeConnectToServer - func resolveRoute(route: Route) -> AnyCoordinatable { - switch route { - case .mainTab: - return MainTabCoordinator().eraseToAnyCoordinatable() - case .connectToServer: - return NavigationViewCoordinator(ConnectToServerCoodinator()).eraseToAnyCoordinatable() + init() { + if ServerEnvironment.current.server != nil, SessionManager.current.user != nil { + self.stack = NavigationStack(initial: \MainCoordinator.mainTab) + } else { + self.stack = NavigationStack(initial: \MainCoordinator.connectToServer) + } + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory + DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk + + #if !os(tvOS) + WidgetCenter.shared.reloadAllTimelines() + UIScrollView.appearance().keyboardDismissMode = .onDrag + #endif + + 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) + } + + @objc func didLogIn() { + LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") + root(\.mainTab) + } + + @objc func didLogOut() { + LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") + root(\.connectToServer) + } + + func makeMainTab() -> MainTabCoordinator { + MainTabCoordinator() + } + + func makeConnectToServer() -> NavigationViewCoordinator { + NavigationViewCoordinator(ConnectToServerCoodinator()) } } - @ViewBuilder - func start() -> some View { - SplashView() - } -} #elseif os(tvOS) -// temp for fixing build error -final class MainCoordinator: ViewCoordinatable { - var children = ViewChild() + // temp for fixing build error + final class MainCoordinator: NavigationCoordinatable { + var stack: NavigationStack - enum Route: ViewRoute { - case mainTab - case connectToServer - } + @Root var mainTab = makeMainTab + @Root var connectToServer = makeMainTab - func resolveRoute(route: Route) -> AnyCoordinatable { - switch route { - case .mainTab: - return MainCoordinator().eraseToAnyCoordinatable() - case .connectToServer: - return MainCoordinator().eraseToAnyCoordinatable() + func makeMainTab() -> NavigationViewCoordinator { + return NavigationViewCoordinator(MainTabCoordinator()) } } - - @ViewBuilder - func start() -> some View { - SplashView() - } -} - #endif diff --git a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift index e9a825de..ab91d28d 100644 --- a/JellyfinPlayer/Coordinators/MainTabCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainTabCoordinator.swift @@ -13,36 +13,29 @@ import SwiftUI import Stinsen final class MainTabCoordinator: TabCoordinatable { - lazy var children = TabChild(self, tabRoutes: [.home, .allMedia]) + var child = TabChild(startingItems: [ + \MainTabCoordinator.home, + \MainTabCoordinator.allMedia, + ]) - enum Route: TabRoute { - case home - case allMedia + @Route(tabItem: makeHomeTab) var home = makeHome + @Route(tabItem: makeTodosTab) var allMedia = makeTodos + + func makeHome() -> NavigationViewCoordinator { + return NavigationViewCoordinator(HomeCoordinator()) } - func tabItem(forTab tab: Int) -> some View { - switch tab { - case 0: - Group { - Text("Home") - Image(systemName: "house") - } - case 1: - Group { - Text("Projects") - Image(systemName: "folder") - } - default: - fatalError() - } + @ViewBuilder func makeHomeTab(isActive: Bool) -> some View { + Image(systemName: "house") + Text("Home") } - func resolveRoute(route: Route) -> AnyCoordinatable { - switch route { - case .home: - return NavigationViewCoordinator(HomeCoordinator()).eraseToAnyCoordinatable() - case .allMedia: - return NavigationViewCoordinator(LibraryListCoordinator()).eraseToAnyCoordinatable() - } + func makeTodos() -> NavigationViewCoordinator { + return NavigationViewCoordinator(LibraryListCoordinator()) + } + + @ViewBuilder func makeTodosTab(isActive: Bool) -> some View { + Image(systemName: "folder") + Text("All Media") } } diff --git a/JellyfinPlayer/Coordinators/SearchCoordinator.swift b/JellyfinPlayer/Coordinators/SearchCoordinator.swift index 8a04f3a0..60c761d2 100644 --- a/JellyfinPlayer/Coordinators/SearchCoordinator.swift +++ b/JellyfinPlayer/Coordinators/SearchCoordinator.swift @@ -10,28 +10,25 @@ import Foundation import Stinsen import SwiftUI +import JellyfinAPI final class SearchCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \SearchCoordinator.start) + + @Root var start = makeStart + @Route(.push) var item = makeItem + var viewModel: LibrarySearchViewModel init(viewModel: LibrarySearchViewModel) { self.viewModel = viewModel } - enum Route: NavigationRoute { - case item(viewModel: ItemViewModel) + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) } - func resolveRoute(route: Route) -> Transition { - switch route { - case let .item(viewModel): - return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable()) - } - } - - @ViewBuilder - func start() -> some View { + @ViewBuilder func makeStart() -> some View { LibrarySearchView(viewModel: self.viewModel) } } diff --git a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift b/JellyfinPlayer/Coordinators/SettingsCoordinator.swift index 15e05678..cbf6b1e0 100644 --- a/JellyfinPlayer/Coordinators/SettingsCoordinator.swift +++ b/JellyfinPlayer/Coordinators/SettingsCoordinator.swift @@ -12,21 +12,16 @@ import Stinsen import SwiftUI final class SettingsCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \SettingsCoordinator.start) - enum Route: NavigationRoute { - case serverDetail + @Root var start = makeStart + @Route(.push) var serverDetail = makeServerDetail + + @ViewBuilder func makeServerDetail() -> some View { + ServerDetailView() } - func resolveRoute(route: Route) -> Transition { - switch route { - case .serverDetail: - return .push(ServerDetailView().eraseToAnyView()) - } - } - - @ViewBuilder - func start() -> some View { + @ViewBuilder func makeStart() -> some View { SettingsView(viewModel: .init()) } } diff --git a/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift b/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift index 5525c364..ebe38123 100644 --- a/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift +++ b/JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift @@ -13,19 +13,16 @@ import Stinsen import SwiftUI final class VideoPlayerCoordinator: NavigationCoordinatable { - var navigationStack = NavigationStack() + let stack = NavigationStack(initial: \VideoPlayerCoordinator.start) + + @Root var start = makeStart var item: BaseItemDto init(item: BaseItemDto) { self.item = item } - enum Route: NavigationRoute {} - - func resolveRoute(route: Route) -> Transition {} - - @ViewBuilder - func start() -> some View { + @ViewBuilder func makeStart() -> some View { VideoPlayerView(item: item) } } diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index 797f8e06..05687d0e 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -11,9 +11,9 @@ import Foundation import SwiftUI struct HomeView: View { + @EnvironmentObject var homeRouter: HomeCoordinator.Router @StateObject var viewModel = HomeViewModel() - @State var showingSettings = false - + init() { let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill") let barAppearance = UINavigationBar.appearance() @@ -43,16 +43,19 @@ struct HomeView: View { .font(.title2) .fontWeight(.bold) Spacer() - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") - }) { + Button { + homeRouter + .route(to: \.library, (viewModel: .init(parentID: libraryID, + filters: viewModel.recentFilterSet), + title: library?.name ?? "")) + } label: { HStack { Text("See All").font(.subheadline).fontWeight(.bold) Image(systemName: "chevron.right").font(Font.subheadline.bold()) } } }.padding(.leading, 16) - .padding(.trailing, 16) + .padding(.trailing, 16) LatestMediaView(viewModel: .init(libraryID: libraryID)) } } @@ -68,14 +71,11 @@ struct HomeView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - showingSettings = true + homeRouter.route(to: \.settings) } label: { Image(systemName: "gear") } } } - .fullScreenCover(isPresented: $showingSettings) { - SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) - } } } diff --git a/JellyfinPlayer/ItemView/ItemView.swift b/JellyfinPlayer/ItemView/ItemView.swift index e3669681..1fb4af4a 100644 --- a/JellyfinPlayer/ItemView/ItemView.swift +++ b/JellyfinPlayer/ItemView/ItemView.swift @@ -5,41 +5,41 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ -import SwiftUI import Introspect import JellyfinAPI +import SwiftUI class VideoPlayerItem: ObservableObject { @Published var shouldShowPlayer: Bool = false - @Published var itemToPlay: BaseItemDto = BaseItemDto() + @Published var itemToPlay = BaseItemDto() } // Intermediary view for ItemView to set navigation bar settings struct ItemNavigationView: View { - private let item: BaseItemDto - + init(item: BaseItemDto) { self.item = item } - + var body: some View { ItemView(item: item) .navigationBarTitle("", displayMode: .inline) } } -fileprivate struct ItemView: View { +private struct ItemView: View { + @EnvironmentObject var itemRouter: ItemCoordinator.Router - @State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view. + @State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view. @State private var viewDidLoad: Bool = false @State private var orientation: UIDeviceOrientation = .unknown - @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() + @StateObject private var videoPlayerItem = VideoPlayerItem() @Environment(\.horizontalSizeClass) private var hSizeClass @Environment(\.verticalSizeClass) private var vSizeClass - + private let viewModel: ItemViewModel - + init(item: BaseItemDto) { switch item.itemType { case .movie: @@ -56,14 +56,20 @@ fileprivate struct ItemView: View { } var body: some View { - if hSizeClass == .compact && vSizeClass == .regular { - ItemPortraitMainView(videoIsLoading: $videoIsLoading) - .environmentObject(videoPlayerItem) - .environmentObject(viewModel) - } else { - ItemLandscapeMainView(videoIsLoading: $videoIsLoading) - .environmentObject(videoPlayerItem) - .environmentObject(viewModel) + Group { + if hSizeClass == .compact && vSizeClass == .regular { + ItemPortraitMainView(videoIsLoading: $videoIsLoading) + .environmentObject(videoPlayerItem) + .environmentObject(viewModel) + } else { + ItemLandscapeMainView(videoIsLoading: $videoIsLoading) + .environmentObject(videoPlayerItem) + .environmentObject(viewModel) + } + } + .onReceive(videoPlayerItem.$shouldShowPlayer) { flag in + guard flag else { return } + self.itemRouter.route(to: \.videoPlayer, viewModel.item) } } } diff --git a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift index 4ebb9061..b42dccaa 100644 --- a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift +++ b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift @@ -1,45 +1,45 @@ // - /* - * 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 - */ +/* + * 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 SwiftUI struct ItemLandscapeMainView: View { - @Binding private var videoIsLoading: Bool @EnvironmentObject private var viewModel: ItemViewModel @EnvironmentObject private var videoPlayerItem: VideoPlayerItem - + init(videoIsLoading: Binding) { self._videoIsLoading = videoIsLoading } - + // MARK: innerBody + private var innerBody: some View { HStack { - // MARK: Sidebar Image + VStack { ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130), bh: viewModel.item.getPrimaryImageBlurHash()) .frame(width: 130, height: 195) .cornerRadius(10) - + Spacer().frame(height: 15) - + Button { if let playButtonItem = viewModel.playButtonItem { self.videoPlayerItem.itemToPlay = playButtonItem self.videoPlayerItem.shouldShowPlayer = true } } label: { - // MARK: Play + HStack { Image(systemName: "play.fill") .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) @@ -53,18 +53,19 @@ struct ItemLandscapeMainView: View { .background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) .cornerRadius(10) }.disabled(viewModel.playButtonItem == nil) - + Spacer() } - + ScrollView { VStack(alignment: .leading) { - // MARK: ItemLandscapeTopBarView + ItemLandscapeTopBarView() .environmentObject(viewModel) - + // MARK: ItemViewBody + if let episodeViewModel = viewModel as? SeasonItemViewModel { CardVStackView(items: episodeViewModel.episodes) } else { @@ -75,32 +76,20 @@ struct ItemLandscapeMainView: View { } } } - + // MARK: body + var body: some View { VStack { - NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { - VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, - loadBinding: $videoIsLoading, - pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .edgesIgnoringSafeArea(.all) - .prefersHomeIndicatorAutoHidden(true) - }, isActive: $videoPlayerItem.shouldShowPlayer) { - EmptyView() - } - ZStack { - // MARK: Backdrop + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), bh: viewModel.item.getBackdropImageBlurHash()) .opacity(0.3) .edgesIgnoringSafeArea(.all) .blur(radius: 4) - + // iPadOS is making the view go all the way to the edge. // We have to accomodate this here if UIDevice.current.userInterfaceIdiom == .pad { diff --git a/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift b/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift index 7ccac0ee..9c7b74a4 100644 --- a/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift +++ b/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift @@ -37,19 +37,6 @@ struct ItemPortraitMainView: View { // MARK: body var body: some View { VStack(alignment: .leading) { - NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { - VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, - loadBinding: $videoIsLoading, - pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .edgesIgnoringSafeArea(.all) - .prefersHomeIndicatorAutoHidden(true) - }, isActive: $videoPlayerItem.shouldShowPlayer) { - EmptyView() - } - // MARK: ParallaxScrollView ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitStaticOverlayView, diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index 58f50b03..74e2cedd 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -28,7 +28,7 @@ extension UIWindow { struct DeviceShakeViewModifier: ViewModifier { let action: () -> Void - func body(content: Content) -> some View { + func body(content: Self.Content) -> some View { content .onAppear() .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in @@ -228,7 +228,7 @@ struct JellyfinPlayerApp: App { }) .withHostingWindow { window in window? - .rootViewController = PreferenceUIHostingController(wrappedView: CoordinatorView(MainCoordinator()) + .rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view() .environment(\.managedObjectContext, persistenceController.container.viewContext)) } .onShake { diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 496c9e33..d1a268ab 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -9,7 +9,7 @@ import Stinsen import SwiftUI struct LatestMediaView: View { - @EnvironmentObject var homeRouter: NavigationRouter + @EnvironmentObject var homeRouter: HomeCoordinator.Router @StateObject var viewModel: LatestMediaViewModel var body: some View { @@ -18,7 +18,7 @@ struct LatestMediaView: View { ForEach(viewModel.items, id: \.id) { item in if item.type == "Series" || item.type == "Movie" { Button { - homeRouter.route(to: .item(viewModel: .init(id: item.id!))) + homeRouter.route(to: \.item, item) } label: { PortraitItemView(item: item) } diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index b8a3a7bd..cbbd6624 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -10,7 +10,7 @@ import SwiftUI import Stinsen struct LibraryFilterView: View { - @EnvironmentObject var filterRouter: NavigationRouter + @EnvironmentObject var filterRouter: FilterCoordinator.Router @Environment(\.presentationMode) var presentationMode @Binding var filters: LibraryFilters var parentId: String = "" @@ -66,7 +66,7 @@ struct LibraryFilterView: View { Button { viewModel.resetFilters() self.filters = viewModel.modifiedFilters - filterRouter.dismiss() + filterRouter.dismissCoordinator() } label: { Text("Reset") } @@ -76,7 +76,7 @@ struct LibraryFilterView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { Button { - filterRouter.dismiss() + filterRouter.dismissCoordinator() } label: { Image(systemName: "xmark") } @@ -85,7 +85,7 @@ struct LibraryFilterView: View { Button { viewModel.updateModifiedFilter() self.filters = viewModel.modifiedFilters - filterRouter.dismiss() + filterRouter.dismissCoordinator() } label: { Text("Apply") } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index c5764830..baac87b8 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -10,14 +10,15 @@ import Stinsen import SwiftUI struct LibraryListView: View { - @EnvironmentObject var libraryListRouter: NavigationRouter + @EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router @StateObject var viewModel = LibraryListViewModel() var body: some View { ScrollView { LazyVStack { Button { - libraryListRouter.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")) + libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: "Favorites")) } label: { ZStack { HStack { @@ -62,7 +63,9 @@ struct LibraryListView: View { ForEach(viewModel.libraries, id: \.id) { library in if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { Button { - libraryListRouter.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? "")) + libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(parentID: library.id), + title: library.name ?? "")) } label: { ZStack { ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) @@ -99,7 +102,7 @@ struct LibraryListView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - libraryListRouter.route(to: .search(viewModel: .init(parentID: nil))) + libraryListRouter.route(to: \.search, LibrarySearchViewModel(parentID: nil)) } label: { Image(systemName: "magnifyingglass") } diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 8431ca4c..dc27b5da 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -11,7 +11,7 @@ import Stinsen import SwiftUI struct LibrarySearchView: View { - @EnvironmentObject var searchRouter: NavigationRouter + @EnvironmentObject var searchRouter: SearchCoordinator.Router @StateObject var viewModel: LibrarySearchViewModel @State private var searchQuery = "" @@ -81,7 +81,7 @@ struct LibrarySearchView: View { LazyVGrid(columns: tracks) { ForEach(items, id: \.id) { item in Button { - searchRouter.route(to: .item(viewModel: .init(id: item.id!))) + searchRouter.route(to: \.item, item) } label: { PortraitItemView(item: item) } diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 13b09384..b0fb15b5 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -10,7 +10,7 @@ import Stinsen import SwiftUI struct LibraryView: View { - @EnvironmentObject var libraryRouter: NavigationRouter + @EnvironmentObject var libraryRouter: LibraryCoordinator.Router @StateObject var viewModel: LibraryViewModel var title: String @@ -36,7 +36,7 @@ struct LibraryView: View { ForEach(viewModel.items, id: \.id) { item in if item.type != "Folder" { Button { - libraryRouter.route(to: .item(viewModel: .init(id: item.id!))) + libraryRouter.route(to: \.item, item) } label: { PortraitItemView(item: item) } @@ -96,11 +96,11 @@ struct LibraryView: View { .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) .onTapGesture { libraryRouter - .route(to: .filter(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, - parentId: viewModel.parentID ?? "")) + .route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, + parentId: viewModel.parentID ?? "")) } Button { - libraryRouter.route(to: .search(viewModel: .init(parentID: viewModel.parentID))) + libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID)) } label: { Image(systemName: "magnifyingglass") } diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 08b03552..5cdd5467 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -11,7 +11,7 @@ import Stinsen import SwiftUI struct NextUpView: View { - @EnvironmentObject var homeRouter: NavigationRouter + @EnvironmentObject var homeRouter: HomeCoordinator.Router var items: [BaseItemDto] @@ -25,7 +25,7 @@ struct NextUpView: View { LazyHStack { ForEach(items, id: \.id) { item in Button { - homeRouter.route(to: .item(viewModel: .init(id: item.id!))) + homeRouter.route(to: \.item, item) } label: { PortraitItemView(item: item) } diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 47c98512..86f0a7c8 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -6,15 +6,16 @@ */ import CoreData -import SwiftUI import Defaults +import Stinsen +import SwiftUI struct SettingsView: View { + @EnvironmentObject var settingsRouter: SettingsCoordinator.Router @Environment(\.managedObjectContext) private var viewContext @ObservedObject var viewModel: SettingsViewModel - @Binding var close: Bool @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles @@ -25,101 +26,104 @@ struct SettingsView: View { @Default(.videoPlayerJumpBackward) var jumpBackwardLength var body: some View { - NavigationView { - Form { - Section(header: EmptyView()) { + Form { + Section(header: EmptyView()) { + HStack { + Text("User") + Spacer() + Text(SessionManager.current.user.username ?? "") + .foregroundColor(.jellyfinPurple) + } + + Button { + settingsRouter.route(to: \.serverDetail) + } label: { HStack { - Text("User") + Text("Server") Spacer() - Text(SessionManager.current.user.username ?? "") + Text(ServerEnvironment.current.server.name ?? "") .foregroundColor(.jellyfinPurple) - } - NavigationLink( - destination: ServerDetailView(), - label: { - HStack { - Text("Server") - Spacer() - Text(ServerEnvironment.current.server.name ?? "") - .foregroundColor(.jellyfinPurple) - } - }) - - Button { - close = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - SessionManager.current.logout() - let nc = NotificationCenter.default - nc.post(name: Notification.Name("didSignOut"), object: nil) - } - } label: { - Text("Sign out") - .font(.callout) - } - } - Section(header: Text("Playback")) { - Picker("Default local quality", selection: $inNetworkStreamBitrate) { - ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - Text(bitrate.name).tag(bitrate.value) - } - } - - Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { - ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - Text(bitrate.name).tag(bitrate.value) - } - } - - Picker("Jump Forward Length", selection: $jumpForwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } - - Picker("Jump Backward Length", selection: $jumpBackwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } + Image(systemName: "chevron.right") } } - Section(header: Text("Accessibility")) { - Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - SearchablePicker(label: "Preferred subtitle language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding( - get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, - set: {autoSelectSubtitlesLangcode = $0.isoCode} - ) - ) - SearchablePicker(label: "Preferred audio language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding( - get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, - set: { autoSelectAudioLangcode = $0.isoCode} - ) - ) - Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { - ForEach(self.viewModel.appearances, id: \.self) { appearance in - Text(appearance.localizedName).tag(appearance.rawValue) - } - }.onChange(of: appAppearance, perform: { value in - UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style - }) + Button { + settingsRouter.dismissCoordinator() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + SessionManager.current.logout() + let nc = NotificationCenter.default + nc.post(name: Notification.Name("didSignOut"), object: nil) + } + } label: { + Text("Sign out") + .font(.callout) } } - .navigationBarTitle("Settings", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - close = false - } label: { - Image(systemName: "xmark") + Section(header: Text("Playback")) { + Picker("Default local quality", selection: $inNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) } } + + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + } + } + + Picker("Jump Forward Length", selection: $jumpForwardLength) { + ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } + + Picker("Jump Backward Length", selection: $jumpBackwardLength) { + ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } + } + + Section(header: Text("Accessibility")) { + Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) + SearchablePicker(label: "Preferred subtitle language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding(get: { + viewModel.langs + .first(where: { $0.isoCode == autoSelectSubtitlesLangcode + }) ?? + .auto + }, + set: { autoSelectSubtitlesLangcode = $0.isoCode })) + SearchablePicker(label: "Preferred audio language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding(get: { + viewModel.langs + .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? + .auto + }, + set: { autoSelectAudioLangcode = $0.isoCode })) + Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { + ForEach(self.viewModel.appearances, id: \.self) { appearance in + Text(appearance.localizedName).tag(appearance.rawValue) + } + }.onChange(of: appAppearance, perform: { _ in + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style + }) + } + } + .navigationBarTitle("Settings", displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + settingsRouter.dismissCoordinator() + } label: { + Image(systemName: "xmark") + } } } } diff --git a/JellyfinPlayer/SplashView.swift b/JellyfinPlayer/SplashView.swift index 7444b269..1235572e 100644 --- a/JellyfinPlayer/SplashView.swift +++ b/JellyfinPlayer/SplashView.swift @@ -11,16 +11,16 @@ import Stinsen import SwiftUI struct SplashView: View { - @EnvironmentObject var mainRouter: ViewRouter + @EnvironmentObject var mainRouter: MainCoordinator.Router @StateObject var viewModel = SplashViewModel() var body: some View { ProgressView() .onReceive(viewModel.$isLoggedIn) { flag in if flag { - mainRouter.route(to: .mainTab) + mainRouter.root(\.mainTab) } else { - mainRouter.route(to: .connectToServer) + mainRouter.root(\.connectToServer) } } } diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index cef3bf52..cf29da12 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -28,7 +28,7 @@ protocol PlayerViewControllerDelegate: AnyObject { class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { @RouterObject - var main: ViewRouter? + var main: MainCoordinator.Router? weak var delegate: PlayerViewControllerDelegate? @@ -538,7 +538,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe case .error(401, _, _, _): self.delegate?.exitPlayer(self) SessionManager.current.logout() - main?.route(to: .connectToServer) + main?.root(\.connectToServer) case .error: self.delegate?.exitPlayer(self) } @@ -1072,12 +1072,12 @@ struct VideoPlayerView: View { struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto - @RouterObject var playerRouter: NavigationRouter? + @RouterObject var playerRouter: VideoPlayerCoordinator.Router? let loadBinding: Binding class Coordinator: NSObject, PlayerViewControllerDelegate { - let parent: VLCPlayerWithControls + var parent: VLCPlayerWithControls let loadBinding: Binding init(parent: VLCPlayerWithControls, loadBinding: Binding) { @@ -1094,7 +1094,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { } func exitPlayer(_ viewController: PlayerViewController) { - parent.playerRouter?.dismiss() + parent.playerRouter?.dismissCoordinator() } } diff --git a/Shared/Singleton/AppURLHandler.swift b/Shared/Singleton/AppURLHandler.swift index 3b83027f..fb95b08e 100644 --- a/Shared/Singleton/AppURLHandler.swift +++ b/Shared/Singleton/AppURLHandler.swift @@ -14,7 +14,7 @@ final class AppURLHandler { static let deepLinkScheme = "jellyfin" @RouterObject - var router: NavigationRouter? + var router: HomeCoordinator.Router? enum AppURLState { case launched @@ -54,7 +54,7 @@ extension AppURLHandler { } return true } - + func processLaunchedURLIfNeeded() { guard let launchURL = launchURL else { return } if processDeepLink(url: launchURL) { @@ -78,7 +78,7 @@ extension AppURLHandler { if url.pathComponents[safe: 2]?.lowercased() == "items", let itemID = url.pathComponents[safe: 3] { - router?.route(to: .item(viewModel: .init(id: itemID))) +// router?.route(to: \.item(item: item)) return true } diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index a8b2ee28..04929d81 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -14,7 +14,7 @@ import Stinsen final class ConnectToServerViewModel: ViewModel { @RouterObject - var main: ViewRouter? + var main: MainCoordinator.Router? @Published var isConnectedServer = false @@ -60,13 +60,12 @@ final class ConnectToServerViewModel: ViewModel { } func connectToServer() { - #if targetEnvironment(simulator) - if uriSubject.value == "localhost" { - uriSubject.value = "http://localhost:8096" - } + if uriSubject.value == "localhost" { + uriSubject.value = "http://localhost:8096" + } #endif - + LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer") ServerEnvironment.current.create(with: uriSubject.value) .trackActivity(loading) @@ -112,7 +111,7 @@ final class ConnectToServerViewModel: ViewModel { self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion) }, receiveValue: { [weak self] _ in - self?.main?.route(to: .mainTab) + self?.main?.root(\.mainTab) }) .store(in: &cancellables) } From d10cb3545121ec4d057ca1035ce1d5264d7ee9f7 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 21 Sep 2021 10:50:07 -0600 Subject: [PATCH 11/18] Refactor --- JellyfinPlayer.xcodeproj/project.pbxproj | 12 ++-- ...View.swift => EpisodeCardVStackView.swift} | 8 ++- .../Components/PillHStackView.swift | 13 ++-- .../Components/PortraitHStackView.swift | 62 +++++++++---------- JellyfinPlayer/ItemView/ItemViewBody.swift | 26 +++++--- .../Landscape/ItemLandscapeMainView.swift | 5 +- .../Portrait/ItemPortraitMainView.swift | 7 ++- 7 files changed, 73 insertions(+), 60 deletions(-) rename JellyfinPlayer/Components/{CardVStackView.swift => EpisodeCardVStackView.swift} (95%) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 6815d46b..84a6349b 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -263,8 +263,8 @@ E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; }; E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; }; - E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* CardVStackView.swift */; }; - E188460526DEF04800B0C5B7 /* CardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* CardVStackView.swift */; }; + E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; }; + E188460526DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; }; E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; @@ -497,7 +497,7 @@ E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = ""; }; E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = ""; }; E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = ""; }; - E188460326DEF04800B0C5B7 /* CardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVStackView.swift; sourceTree = ""; }; + E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCardVStackView.swift; sourceTree = ""; }; E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = ""; }; E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = ""; }; @@ -962,7 +962,7 @@ E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */, E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, - E188460326DEF04800B0C5B7 /* CardVStackView.swift */, + E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */, ); path = Components; sourceTree = ""; @@ -1533,7 +1533,7 @@ 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, - E188460526DEF04800B0C5B7 /* CardVStackView.swift in Sources */, + E188460526DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, @@ -1575,7 +1575,7 @@ E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, - E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */, + E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, diff --git a/JellyfinPlayer/Components/CardVStackView.swift b/JellyfinPlayer/Components/EpisodeCardVStackView.swift similarity index 95% rename from JellyfinPlayer/Components/CardVStackView.swift rename to JellyfinPlayer/Components/EpisodeCardVStackView.swift index 84583dd9..b7f2bac8 100644 --- a/JellyfinPlayer/Components/CardVStackView.swift +++ b/JellyfinPlayer/Components/EpisodeCardVStackView.swift @@ -10,9 +10,10 @@ import SwiftUI import JellyfinAPI -struct CardVStackView: View { +struct EpisodeCardVStackView: View { let items: [BaseItemDto] + let selectedAction: (BaseItemDto) -> Void private func buildCardOverlayView(item: BaseItemDto) -> some View { HStack { @@ -45,8 +46,9 @@ struct CardVStackView: View { var body: some View { VStack { ForEach(items, id: \.id) { item in - NavigationLink(destination: ItemNavigationView(item: item)) { - + Button { + selectedAction(item) + } label: { HStack { // MARK: Image diff --git a/JellyfinPlayer/Components/PillHStackView.swift b/JellyfinPlayer/Components/PillHStackView.swift index 3c6fcf06..e0b5d0fc 100644 --- a/JellyfinPlayer/Components/PillHStackView.swift +++ b/JellyfinPlayer/Components/PillHStackView.swift @@ -13,11 +13,12 @@ protocol PillStackable { var title: String { get } } -struct PillHStackView: View { +struct PillHStackView: View { let title: String let items: [ItemType] - let navigationView: (ItemType) -> NavigationView +// let navigationView: (ItemType) -> NavigationView + let selectedAction: (ItemType) -> Void var body: some View { VStack(alignment: .leading) { @@ -30,14 +31,14 @@ struct PillHStackView: View { ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(items, id: \.title) { item in - NavigationLink(destination: LazyView { - navigationView(item) - }) { + Button { + selectedAction(item) + } label: { ZStack { Color(UIColor.systemFill) .frame(maxWidth: .infinity, maxHeight: .infinity) .cornerRadius(10) - + Text(item.title) .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/Components/PortraitHStackView.swift b/JellyfinPlayer/Components/PortraitHStackView.swift index 96e32645..4d97d34a 100644 --- a/JellyfinPlayer/Components/PortraitHStackView.swift +++ b/JellyfinPlayer/Components/PortraitHStackView.swift @@ -17,20 +17,20 @@ public protocol PortraitImageStackable { var failureInitials: String { get } } -struct PortraitImageHStackView: View { +struct PortraitImageHStackView: View { let items: [ItemType] let maxWidth: Int let horizontalAlignment: HorizontalAlignment let topBarView: () -> TopBarView - let navigationView: (ItemType) -> NavigationView + let selectedAction: (ItemType) -> Void - init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, navigationView: @escaping (ItemType) -> NavigationView) { + init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) { self.items = items self.maxWidth = maxWidth self.horizontalAlignment = horizontalAlignment self.topBarView = topBarView - self.navigationView = navigationView + self.selectedAction = selectedAction } var body: some View { @@ -45,38 +45,36 @@ struct PortraitImageHStackView Date: Wed, 22 Sep 2021 04:02:51 +0900 Subject: [PATCH 12/18] Apply Coordinator pattern to shared view --- JellyfinPlayer/ItemView/ItemViewBody.swift | 54 +++++++++---------- .../Landscape/ItemLandscapeMainView.swift | 5 +- .../Portrait/ItemPortraitMainView.swift | 36 +++++++------ 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/JellyfinPlayer/ItemView/ItemViewBody.swift b/JellyfinPlayer/ItemView/ItemViewBody.swift index fe6cb5ad..3c819b25 100644 --- a/JellyfinPlayer/ItemView/ItemViewBody.swift +++ b/JellyfinPlayer/ItemView/ItemViewBody.swift @@ -1,29 +1,30 @@ // - /* - * 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 - */ +/* + * 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 SwiftUI import JellyfinAPI +import SwiftUI struct ItemViewBody: View { - + @EnvironmentObject var itemRouter: ItemCoordinator.Router @EnvironmentObject private var viewModel: ItemViewModel - + var body: some View { VStack(alignment: .leading) { - // MARK: Overview + Text(viewModel.item.overview ?? "") .font(.footnote) .padding(.horizontal, 16) .padding(.vertical, 3) - + // MARK: Seasons + if let seriesViewModel = viewModel as? SeriesItemViewModel { PortraitImageHStackView(items: seriesViewModel.seasons, maxWidth: 150, @@ -34,31 +35,31 @@ struct ItemViewBody: View { .padding(.top, 3) .padding(.leading, 16) }, selectedAction: { season in - // Router work here to present a: - // ItemNavigationView(item: Season) + itemRouter.route(to: \.item, season) }) } - + // MARK: Genres + PillHStackView(title: "Genres", items: viewModel.item.genreItems ?? [], selectedAction: { genre in - // Router work here to present a: - // LibraryView(viewModel: .init(genre: genre), title: genre.title) - }) - + itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) + }) + // MARK: Studios + if let studios = viewModel.item.studios { PillHStackView(title: "Studios", items: studios) { studio in - // Router work here to present a: - // LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") + itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) } } - + // MARK: Cast & Crew + if let castAndCrew = viewModel.item.people { - PortraitImageHStackView(items: castAndCrew.filter({ BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") }), + PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") }, maxWidth: 150, topBarView: { Text("Cast & Crew") @@ -68,12 +69,12 @@ struct ItemViewBody: View { .padding(.leading, 16) }, selectedAction: { person in - // Router work here to present a: - // LibraryView(viewModel: .init(person: person), title: person.title) + itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) }) } // MARK: More Like This + if !viewModel.similarItems.isEmpty { PortraitImageHStackView(items: viewModel.similarItems, maxWidth: 150, @@ -85,8 +86,7 @@ struct ItemViewBody: View { .padding(.leading, 16) }, selectedAction: { item in - // Router work here to present a: - // ItemNavigationView(item: item) + itemRouter.route(to: \.item, item) }) } } diff --git a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift index dab98a46..22390b34 100644 --- a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift +++ b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift @@ -7,9 +7,11 @@ * Copyright 2021 Aiden Vigue & Jellyfin Contributors */ +import Stinsen import SwiftUI struct ItemLandscapeMainView: View { + @EnvironmentObject var itemRouter: ItemCoordinator.Router @Binding private var videoIsLoading: Bool @EnvironmentObject private var viewModel: ItemViewModel @EnvironmentObject private var videoPlayerItem: VideoPlayerItem @@ -68,8 +70,7 @@ struct ItemLandscapeMainView: View { if let episodeViewModel = viewModel as? SeasonItemViewModel { EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in - // Router work here to present a: - // ItemNavigationView(item: episode) + itemRouter.route(to: \.item, episode) } } else { ItemViewBody() diff --git a/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift b/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift index 6c818fd2..259af534 100644 --- a/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift +++ b/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift @@ -1,57 +1,59 @@ // - /* - * 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 - */ +/* + * 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 SwiftUI import JellyfinAPI +import SwiftUI struct ItemPortraitMainView: View { - + @EnvironmentObject var itemRouter: ItemCoordinator.Router @Binding private var videoIsLoading: Bool @EnvironmentObject private var viewModel: ItemViewModel @EnvironmentObject private var videoPlayerItem: VideoPlayerItem - + init(videoIsLoading: Binding) { self._videoIsLoading = videoIsLoading } - + // MARK: portraitHeaderView + var portraitHeaderView: some View { ImageView(src: viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)), bh: viewModel.item.getBackdropImageBlurHash()) .opacity(0.4) .blur(radius: 2.0) } - + // MARK: portraitStaticOverlayView + var portraitStaticOverlayView: some View { PortraitHeaderOverlayView() .environmentObject(viewModel) } - + // MARK: body + var body: some View { VStack(alignment: .leading) { // MARK: ParallaxScrollView + ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitStaticOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIScreen.main.bounds.width * 0.5625) { - VStack { Spacer() .frame(height: 70) - + if let episodeViewModel = viewModel as? SeasonItemViewModel { Spacer() EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in - // Router work to present a: - // ItemNavigationView(item: episode) + itemRouter.route(to: \.item, episode) } .padding(.top, 5) } else { From b92d66e26eeb7dff0b8fd245ed3eeca1a7222147 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 22 Sep 2021 05:28:14 +0900 Subject: [PATCH 13/18] 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) + } +} From 8175395ade793a0b580a5a00aca37528de9c6a10 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 22 Sep 2021 05:35:00 +0900 Subject: [PATCH 14/18] remove DeepLinkError --- JellyfinPlayer/DeepLink.swift | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/JellyfinPlayer/DeepLink.swift b/JellyfinPlayer/DeepLink.swift index 94f15232..9270682f 100644 --- a/JellyfinPlayer/DeepLink.swift +++ b/JellyfinPlayer/DeepLink.swift @@ -1,26 +1,15 @@ // - /* - * 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 - */ +/* + * 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) } From cd7a58b6f237c1970ed9d75b6aa359d08db4f83b Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 22 Sep 2021 06:09:41 +0900 Subject: [PATCH 15/18] fix crash when sign out --- JellyfinPlayer/SettingsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 86f0a7c8..c309b8c2 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -31,7 +31,7 @@ struct SettingsView: View { HStack { Text("User") Spacer() - Text(SessionManager.current.user.username ?? "") + Text(SessionManager.current.user?.username ?? "") .foregroundColor(.jellyfinPurple) } @@ -41,7 +41,7 @@ struct SettingsView: View { HStack { Text("Server") Spacer() - Text(ServerEnvironment.current.server.name ?? "") + Text(ServerEnvironment.current.server?.name ?? "") .foregroundColor(.jellyfinPurple) Image(systemName: "chevron.right") From fad8f14a210b1e129714006a25b658540bced33c Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Thu, 23 Sep 2021 16:35:33 +0900 Subject: [PATCH 16/18] fix tvOS build error --- JellyfinPlayer.xcodeproj/project.pbxproj | 8 ++------ JellyfinPlayer/Coordinators/MainCoordinator.swift | 9 ++++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 12d865cf..d108f1ce 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -184,7 +184,6 @@ 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; }; 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 */; }; @@ -224,6 +223,7 @@ 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; }; 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; + 62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; @@ -265,9 +265,7 @@ E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; }; E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; }; - E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* CardVStackView.swift */; }; E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; }; - E188460526DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; }; E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; @@ -1481,7 +1479,6 @@ E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, 53116A19268B947A003024C9 /* PlainLinkButton.swift in Sources */, 536D3D88267C17350004248C /* PublicUserButton.swift in Sources */, - 6220D0CA26D63F4D00B8E046 /* MainCoordinator.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, @@ -1520,6 +1517,7 @@ 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, + 62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */, @@ -1541,8 +1539,6 @@ 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, - E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, - E188460526DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, diff --git a/JellyfinPlayer/Coordinators/MainCoordinator.swift b/JellyfinPlayer/Coordinators/MainCoordinator.swift index aa00abd9..0d0f7f29 100644 --- a/JellyfinPlayer/Coordinators/MainCoordinator.swift +++ b/JellyfinPlayer/Coordinators/MainCoordinator.swift @@ -77,13 +77,12 @@ import SwiftUI #elseif os(tvOS) // temp for fixing build error final class MainCoordinator: NavigationCoordinatable { - var stack: NavigationStack + var stack = NavigationStack(initial: \MainCoordinator.mainTab) - @Root var mainTab = makeMainTab - @Root var connectToServer = makeMainTab + @Root var mainTab = makeEmpty - func makeMainTab() -> NavigationViewCoordinator { - return NavigationViewCoordinator(MainTabCoordinator()) + @ViewBuilder func makeEmpty() -> some View { + EmptyView() } } #endif From a46ed4592539ad4e3fe1a71ae4136e342b8c7212 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Thu, 23 Sep 2021 18:00:30 +0900 Subject: [PATCH 17/18] modify LatestMediaViewModel.requestLatestMedia params fix build error --- JellyfinPlayer/LatestMediaView.swift | 10 ++++------ Shared/ViewModels/LatestMediaViewModel.swift | 16 +++++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index d1a268ab..300f435e 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -16,12 +16,10 @@ struct LatestMediaView: View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach(viewModel.items, id: \.id) { item in - if item.type == "Series" || item.type == "Movie" { - Button { - homeRouter.route(to: \.item, item) - } label: { - PortraitItemView(item: item) - } + Button { + homeRouter.route(to: \.item, item) + } label: { + PortraitItemView(item: item) } }.padding(.trailing, 16) }.padding(.leading, 20) diff --git a/Shared/ViewModels/LatestMediaViewModel.swift b/Shared/ViewModels/LatestMediaViewModel.swift index 6f791653..d0ebbba8 100644 --- a/Shared/ViewModels/LatestMediaViewModel.swift +++ b/Shared/ViewModels/LatestMediaViewModel.swift @@ -26,15 +26,17 @@ final class LatestMediaViewModel: ViewModel { func requestLatestMedia() { LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.current.user.user_id ?? "NIL")") - UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: libraryID, + UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, + parentId: libraryID, fields: [ - .primaryImageAspectRatio, - .seriesPrimaryImage, - .seasonUserData, - .overview, - .genres, - .people + .primaryImageAspectRatio, + .seriesPrimaryImage, + .seasonUserData, + .overview, + .genres, + .people ], + includeItemTypes: ["Series", "Movie"], enableUserData: true, limit: 12) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in From 34dbc2ac00a838ae4abc4aa37ec234b861309660 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Thu, 23 Sep 2021 18:08:44 +0900 Subject: [PATCH 18/18] fix LibraryFilterView's layout --- JellyfinPlayer/LibraryFilterView.swift | 119 ++++++++++++------------- 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index cbbd6624..0a96a459 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -6,8 +6,8 @@ */ import JellyfinAPI -import SwiftUI import Stinsen +import SwiftUI struct LibraryFilterView: View { @EnvironmentObject var filterRouter: FilterCoordinator.Router @@ -20,75 +20,74 @@ struct LibraryFilterView: View { init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { _filters = filters self.parentId = parentId - _viewModel = StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId)) + _viewModel = + StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId)) } var body: some View { - NavigationView { - VStack { - if viewModel.isLoading { - ProgressView() - } else { - Form { - if viewModel.enabledFilterType.contains(.genre) { - MultiSelector(label: NSLocalizedString("Genres", comment: ""), - options: viewModel.possibleGenres, - optionToString: { $0.name ?? "" }, - selected: $viewModel.modifiedFilters.withGenres) - } - if viewModel.enabledFilterType.contains(.filter) { - MultiSelector(label: NSLocalizedString("Filters", comment: ""), - options: viewModel.possibleItemFilters, - optionToString: { $0.localized }, - selected: $viewModel.modifiedFilters.filters) - } - if viewModel.enabledFilterType.contains(.tag) { - MultiSelector(label: NSLocalizedString("Tags", comment: ""), - options: viewModel.possibleTags, - optionToString: { $0 }, - selected: $viewModel.modifiedFilters.tags) - } - if viewModel.enabledFilterType.contains(.sortBy) { - Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) { - ForEach(viewModel.possibleSortBys, id: \.self) { so in - Text(so.localized).tag(so) - } - } - } - if viewModel.enabledFilterType.contains(.sortOrder) { - Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) { - ForEach(viewModel.possibleSortOrders, id: \.self) { so in - Text(so.rawValue).tag(so) - } + VStack { + if viewModel.isLoading { + ProgressView() + } else { + Form { + if viewModel.enabledFilterType.contains(.genre) { + MultiSelector(label: NSLocalizedString("Genres", comment: ""), + options: viewModel.possibleGenres, + optionToString: { $0.name ?? "" }, + selected: $viewModel.modifiedFilters.withGenres) + } + if viewModel.enabledFilterType.contains(.filter) { + MultiSelector(label: NSLocalizedString("Filters", comment: ""), + options: viewModel.possibleItemFilters, + optionToString: { $0.localized }, + selected: $viewModel.modifiedFilters.filters) + } + if viewModel.enabledFilterType.contains(.tag) { + MultiSelector(label: NSLocalizedString("Tags", comment: ""), + options: viewModel.possibleTags, + optionToString: { $0 }, + selected: $viewModel.modifiedFilters.tags) + } + if viewModel.enabledFilterType.contains(.sortBy) { + Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) { + ForEach(viewModel.possibleSortBys, id: \.self) { so in + Text(so.localized).tag(so) } } } - Button { - viewModel.resetFilters() - self.filters = viewModel.modifiedFilters - filterRouter.dismissCoordinator() - } label: { - Text("Reset") + if viewModel.enabledFilterType.contains(.sortOrder) { + Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) { + ForEach(viewModel.possibleSortOrders, id: \.self) { so in + Text(so.rawValue).tag(so) + } + } } } + Button { + viewModel.resetFilters() + self.filters = viewModel.modifiedFilters + filterRouter.dismissCoordinator() + } label: { + Text("Reset") + } } - .navigationBarTitle(NSLocalizedString("Filter Results", comment: ""), displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - filterRouter.dismissCoordinator() - } label: { - Image(systemName: "xmark") - } + } + .navigationBarTitle(NSLocalizedString("Filter Results", comment: ""), displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + filterRouter.dismissCoordinator() + } label: { + Image(systemName: "xmark") } - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - viewModel.updateModifiedFilter() - self.filters = viewModel.modifiedFilters - filterRouter.dismissCoordinator() - } label: { - Text("Apply") - } + } + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + viewModel.updateModifiedFilter() + self.filters = viewModel.modifiedFilters + filterRouter.dismissCoordinator() + } label: { + Text("Apply") } } }