From 16e3cd6ea5bf12f71abe33512da08403c429275c Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Mon, 20 Sep 2021 20:32:04 +0900 Subject: [PATCH] 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) }