From 4e0e6635c2be01041a13af64344722dc5b060d08 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 6 Jan 2022 23:21:15 -0700 Subject: [PATCH] tvos collection support --- .../CinematicCollectionItemView.swift | 69 +++++++++++++++ .../CinematicItemViewTopRow.swift | 65 +++++++++------ .../Views/ItemView/ItemView.swift | 2 + .../Views/LibraryListView.swift | 83 ++++++++++++------- JellyfinPlayer.xcodeproj/project.pbxproj | 6 ++ 5 files changed, 171 insertions(+), 54 deletions(-) create mode 100644 JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift new file mode 100644 index 00000000..d96aa9e0 --- /dev/null +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicCollectionItemView.swift @@ -0,0 +1,69 @@ +// + /* + * 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 Defaults +import Introspect +import SwiftUI + +struct CinematicCollectionItemView: View { + + @EnvironmentObject var itemRouter: ItemCoordinator.Router + @ObservedObject var viewModel: CollectionItemViewModel + @State var wrappedScrollView: UIScrollView? + @Default(.showPosterLabels) var showPosterLabels + + var body: some View { + ZStack { + + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920), + bh: viewModel.item.getBackdropImageBlurHash()) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 0) { + + CinematicItemViewTopRow(viewModel: viewModel, + wrappedScrollView: wrappedScrollView, + title: viewModel.item.name ?? "", + showDetails: false) + .focusSection() + .frame(height: UIScreen.main.bounds.height - 10) + + ZStack(alignment: .topLeading) { + + Color.black.ignoresSafeArea() + + VStack(alignment: .leading, spacing: 20) { + + CinematicItemAboutView(viewModel: viewModel) + + PortraitItemsRowView(rowTitle: "Items", + items: viewModel.collectionItems) { item in + itemRouter.route(to: \.item, item) + } + + if !viewModel.similarItems.isEmpty { + PortraitItemsRowView(rowTitle: "Recommended", + items: viewModel.similarItems, + showItemTitles: showPosterLabels) { item in + itemRouter.route(to: \.item, item) + } + } + } + .padding(.vertical, 50) + } + } + } + .introspectScrollView { scrollView in + wrappedScrollView = scrollView + } + .ignoresSafeArea() + } + } +} diff --git a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift index e6c51402..25965d4d 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/CinematicItemView/CinematicItemViewTopRow.swift @@ -18,6 +18,19 @@ struct CinematicItemViewTopRow: View { @State var wrappedScrollView: UIScrollView? @State var title: String @State var subtitle: String? + let showDetails: Bool + + init(viewModel: ItemViewModel, + wrappedScrollView: UIScrollView? = nil, + title: String, + subtitle: String? = nil, + showDetails: Bool = true) { + self.viewModel = viewModel + self.wrappedScrollView = wrappedScrollView + self.title = title + self.subtitle = subtitle + self.showDetails = showDetails + } var body: some View { ZStack(alignment: .bottom) { @@ -69,35 +82,39 @@ struct CinematicItemViewTopRow: View { HStack(alignment: .PlayInformationAlignmentGuide, spacing: 20) { - if viewModel.item.itemType == .series { - if let airTime = viewModel.item.airTime { - Text(airTime) - .font(.subheadline) - .fontWeight(.medium) - } - } else { - if let runtime = viewModel.item.getItemRuntime() { - Text(runtime) - .font(.subheadline) - .fontWeight(.medium) + if showDetails { + if viewModel.item.itemType == .series { + if let airTime = viewModel.item.airTime { + Text(airTime) + .font(.subheadline) + .fontWeight(.medium) + } + } else { + if let runtime = viewModel.item.getItemRuntime() { + Text(runtime) + .font(.subheadline) + .fontWeight(.medium) + } + + if let productionYear = viewModel.item.productionYear { + Text(String(productionYear)) + .font(.subheadline) + .fontWeight(.medium) + .lineLimit(1) + } } - if let productionYear = viewModel.item.productionYear { - Text(String(productionYear)) + if let officialRating = viewModel.item.officialRating { + Text(officialRating) .font(.subheadline) - .fontWeight(.medium) + .fontWeight(.semibold) .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) } - } - - if let officialRating = viewModel.item.officialRating { - Text(officialRating) - .font(.subheadline) - .fontWeight(.semibold) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) + } else { + Text("") } } .foregroundColor(.secondary) diff --git a/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift index ad443c65..9981506b 100644 --- a/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift +++ b/JellyfinPlayer tvOS/Views/ItemView/ItemView.swift @@ -60,6 +60,8 @@ struct ItemView: View { } else { SeriesItemView(viewModel: SeriesItemViewModel(item: item)) } + case .boxset: + CinematicCollectionItemView(viewModel: CollectionItemViewModel(item: item)) default: Text(L10n.notImplementedYetWithType(item.type ?? "")) } diff --git a/JellyfinPlayer tvOS/Views/LibraryListView.swift b/JellyfinPlayer tvOS/Views/LibraryListView.swift index 3332bbe5..9a3db910 100644 --- a/JellyfinPlayer tvOS/Views/LibraryListView.swift +++ b/JellyfinPlayer tvOS/Views/LibraryListView.swift @@ -22,38 +22,38 @@ struct LibraryListView: View { ScrollView { LazyVStack { if !viewModel.isLoading { - ForEach(viewModel.libraries, id: \.id) { library in - if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" || library.collectionType ?? "" == "music" { - EmptyView() - } else { - if library.collectionType == "livetv" { - if liveTVAlphaEnabled { - Button() { - self.mainCoordinator.root(\.liveTV) + + if let collectionLibraryItem = viewModel.libraries.first(where: { $0.collectionType == "boxsets" }) { + Button() { + self.libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(parentID: collectionLibraryItem.id), title: collectionLibraryItem.name ?? "")) + } + label: { + ZStack { + HStack { + Spacer() + VStack { + Text(collectionLibraryItem.name ?? "") + .foregroundColor(.white) + .font(.title2) + .fontWeight(.semibold) } - label: { - ZStack { - HStack { - Spacer() - VStack { - Text(library.name ?? "") - .foregroundColor(.white) - .font(.title2) - .fontWeight(.semibold) - } - Spacer() - }.padding(32) - } - .frame(minWidth: 100, maxWidth: .infinity) - .frame(height: 100) - } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 5) - } - } else { + Spacer() + }.padding(32) + } + .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 100) + } + .cornerRadius(10) + .shadow(radius: 5) + .padding(.bottom, 5) + } + + ForEach(viewModel.libraries.filter({ $0.collectionType != "boxsets" }), id: \.id) { library in + if library.collectionType == "livetv" { + if liveTVAlphaEnabled { Button() { - self.libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(), title: library.name ?? "")) + self.mainCoordinator.root(\.liveTV) } label: { ZStack { @@ -75,6 +75,29 @@ struct LibraryListView: View { .shadow(radius: 5) .padding(.bottom, 5) } + } else { + Button() { + self.libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(), title: library.name ?? "")) + } + label: { + ZStack { + HStack { + Spacer() + VStack { + Text(library.name ?? "") + .foregroundColor(.white) + .font(.title2) + .fontWeight(.semibold) + } + Spacer() + }.padding(32) + } + .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 100) + } + .cornerRadius(10) + .shadow(radius: 5) + .padding(.bottom, 5) } } } else { diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index de502ee7..263a72c5 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -248,6 +248,8 @@ E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */; }; + E107BB962788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; }; + E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E107BB952788104100354E07 /* CinematicCollectionItemView.swift */; }; E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */; }; E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */; }; E10D87DE278510E400BD264C /* PosterSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10D87DD278510E300BD264C /* PosterSize.swift */; }; @@ -631,6 +633,7 @@ C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = ""; }; + E107BB952788104100354E07 /* CinematicCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicCollectionItemView.swift; sourceTree = ""; }; E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = ""; }; E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = ""; }; E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = ""; }; @@ -1550,6 +1553,7 @@ E1E5D53C2783A85F00692DFE /* CinematicItemView */ = { isa = PBXGroup; children = ( + E107BB952788104100354E07 /* CinematicCollectionItemView.swift */, E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */, E1E5D5452783C28100692DFE /* CinematicItemAboutView.swift */, E1E5D53A2783A80900692DFE /* CinematicItemViewTopRow.swift */, @@ -2015,6 +2019,7 @@ C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, + E107BB972788104100354E07 /* CinematicCollectionItemView.swift in Sources */, 53116A17268B919A003024C9 /* SeriesItemView.swift in Sources */, E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, @@ -2286,6 +2291,7 @@ E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, + E107BB962788104100354E07 /* CinematicCollectionItemView.swift in Sources */, C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,