From 74a930202101899c9b2c22602c7622daf5633ff5 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 25 Aug 2021 16:46:59 +0900 Subject: [PATCH] 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) + } +}