From dbe95f29dbff3de6d610b64d03252cbadc7835e0 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 6 Jan 2022 23:01:17 -0700 Subject: [PATCH 1/4] initial collections implementation --- JellyfinPlayer.xcodeproj/project.pbxproj | 42 ++++++++++++------- JellyfinPlayer/Views/ItemView/ItemView.swift | 2 + .../Views/ItemView/ItemViewBody.swift | 23 +++++++++- JellyfinPlayer/Views/LibraryListView.swift | 30 +++++++++++++ .../BaseItemDtoExtensions.swift | 3 +- .../CollectionItemViewModel.swift | 36 ++++++++++++++++ .../EpisodeItemViewModel.swift | 0 .../{ => ItemViewModel}/ItemViewModel.swift | 0 .../MovieItemViewModel.swift | 0 .../SeasonItemViewModel.swift | 0 .../SeriesItemViewModel.swift | 0 Shared/ViewModels/LibraryListViewModel.swift | 4 +- Shared/ViewModels/LibraryViewModel.swift | 41 +++++++++++++----- 13 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift rename Shared/ViewModels/{ => ItemViewModel}/EpisodeItemViewModel.swift (100%) rename Shared/ViewModels/{ => ItemViewModel}/ItemViewModel.swift (100%) rename Shared/ViewModels/{ => ItemViewModel}/MovieItemViewModel.swift (100%) rename Shared/ViewModels/{ => ItemViewModel}/SeasonItemViewModel.swift (100%) rename Shared/ViewModels/{ => ItemViewModel}/SeriesItemViewModel.swift (100%) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 4cdf637d..de502ee7 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -246,6 +246,8 @@ C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; 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 */; }; 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 */; }; @@ -628,6 +630,7 @@ C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -817,22 +820,18 @@ children = ( E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */, 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, - 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, E10D87E127852FD000BD264C /* EpisodesRowViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */, - 62E632F2267D54030063E547 /* ItemViewModel.swift */, + E107BB9127880A4000354E07 /* ItemViewModel */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, + C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, - C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, - 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, - 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */, - 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, @@ -1296,6 +1295,19 @@ path = Pods; sourceTree = ""; }; + E107BB9127880A4000354E07 /* ItemViewModel */ = { + isa = PBXGroup; + children = ( + E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */, + 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, + 62E632F2267D54030063E547 /* ItemViewModel.swift */, + 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, + 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */, + 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, + ); + path = ItemViewModel; + sourceTree = ""; + }; E12186DF2718F2030010884C /* App */ = { isa = PBXGroup; children = ( @@ -1999,6 +2011,7 @@ E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, + E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, @@ -2211,6 +2224,7 @@ E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, + E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, @@ -2671,7 +2685,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2683,7 +2697,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2708,7 +2722,7 @@ CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2720,7 +2734,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2739,7 +2753,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2748,7 +2762,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2766,7 +2780,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2775,7 +2789,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/JellyfinPlayer/Views/ItemView/ItemView.swift b/JellyfinPlayer/Views/ItemView/ItemView.swift index d127c1d6..a18eed1d 100644 --- a/JellyfinPlayer/Views/ItemView/ItemView.swift +++ b/JellyfinPlayer/Views/ItemView/ItemView.swift @@ -46,6 +46,8 @@ private struct ItemView: View { self.viewModel = EpisodeItemViewModel(item: item) case .series: self.viewModel = SeriesItemViewModel(item: item) + case .boxset: + self.viewModel = CollectionItemViewModel(item: item) default: self.viewModel = ItemViewModel(item: item) } diff --git a/JellyfinPlayer/Views/ItemView/ItemViewBody.swift b/JellyfinPlayer/Views/ItemView/ItemViewBody.swift index 64dcdd66..e3860e3e 100644 --- a/JellyfinPlayer/Views/ItemView/ItemViewBody.swift +++ b/JellyfinPlayer/Views/ItemView/ItemViewBody.swift @@ -80,6 +80,19 @@ struct ItemViewBody: View { } } } + + // MARK: Collection Items + + if let collectionViewModel = viewModel as? CollectionItemViewModel { + PortraitImageHStackView(items: collectionViewModel.collectionItems) { + Text("Items") + .fontWeight(.semibold) + .padding(.bottom) + .padding(.horizontal) + } selectedAction: { collectionItem in + itemRouter.route(to: \.item, collectionItem) + } + } // MARK: Cast & Crew @@ -115,8 +128,14 @@ struct ItemViewBody: View { // MARK: Details - ItemViewDetailsView(viewModel: viewModel) - .padding() + switch viewModel.item.itemType { + case .movie, .episode: + ItemViewDetailsView(viewModel: viewModel) + .padding() + default: + EmptyView() + .frame(height: 50) + } } } } diff --git a/JellyfinPlayer/Views/LibraryListView.swift b/JellyfinPlayer/Views/LibraryListView.swift index e6bf0364..2df53f8d 100644 --- a/JellyfinPlayer/Views/LibraryListView.swift +++ b/JellyfinPlayer/Views/LibraryListView.swift @@ -39,6 +39,36 @@ struct LibraryListView: View { .padding(.bottom, 5) if !viewModel.isLoading { + + if let collectionsLibraryItem = viewModel.libraries.first(where: { $0.collectionType == "boxsets" }) { + Button { + libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(parentID: collectionsLibraryItem.id), + title: collectionsLibraryItem.name ?? "")) + } label: { + ZStack { + ImageView(src: collectionsLibraryItem.getPrimaryImage(maxWidth: 500), + bh: collectionsLibraryItem.getPrimaryImageBlurHash()) + .opacity(0.4) + HStack { + Spacer() + VStack { + Text(collectionsLibraryItem.name ?? "") + .foregroundColor(.white) + .font(.title2) + .fontWeight(.semibold) + } + Spacer() + }.padding(32) + }.background(Color.black) + .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 100) + } + .cornerRadius(10) + .shadow(radius: 5) + .padding(.bottom, 5) + } + ForEach(viewModel.libraries, id: \.id) { library in if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { Button { diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 17238cd9..508271ab 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -205,6 +205,7 @@ public extension BaseItemDto { case season = "Season" case episode = "Episode" case series = "Series" + case boxset = "BoxSet" case unknown @@ -227,7 +228,7 @@ public extension BaseItemDto { func portraitHeaderViewURL(maxWidth: Int) -> URL { switch itemType { - case .movie, .season, .series: + case .movie, .season, .series, .boxset: return getPrimaryImage(maxWidth: maxWidth) case .episode: return getSeriesPrimaryImage(maxWidth: maxWidth) diff --git a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift new file mode 100644 index 00000000..0cab922b --- /dev/null +++ b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift @@ -0,0 +1,36 @@ +// + /* + * 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 Combine +import Foundation +import JellyfinAPI + +final class CollectionItemViewModel: ItemViewModel { + + @Published var collectionItems: [BaseItemDto] = [] + + override init(item: BaseItemDto) { + super.init(item: item) + + getCollectionItems() + } + + private func getCollectionItems() { + ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id, + parentId: item.id, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] response in + self?.collectionItems = response.items ?? [] + } + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/EpisodeItemViewModel.swift b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift similarity index 100% rename from Shared/ViewModels/EpisodeItemViewModel.swift rename to Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift diff --git a/Shared/ViewModels/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift similarity index 100% rename from Shared/ViewModels/ItemViewModel.swift rename to Shared/ViewModels/ItemViewModel/ItemViewModel.swift diff --git a/Shared/ViewModels/MovieItemViewModel.swift b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift similarity index 100% rename from Shared/ViewModels/MovieItemViewModel.swift rename to Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift diff --git a/Shared/ViewModels/SeasonItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift similarity index 100% rename from Shared/ViewModels/SeasonItemViewModel.swift rename to Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift diff --git a/Shared/ViewModels/SeriesItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift similarity index 100% rename from Shared/ViewModels/SeriesItemViewModel.swift rename to Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift diff --git a/Shared/ViewModels/LibraryListViewModel.swift b/Shared/ViewModels/LibraryListViewModel.swift index a95c429f..0e8845bd 100644 --- a/Shared/ViewModels/LibraryListViewModel.swift +++ b/Shared/ViewModels/LibraryListViewModel.swift @@ -12,7 +12,7 @@ import JellyfinAPI final class LibraryListViewModel: ViewModel { - @Published var libraries = [BaseItemDto]() + @Published var libraries: [BaseItemDto] = [] // temp var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) @@ -29,7 +29,7 @@ final class LibraryListViewModel: ViewModel { .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) }, receiveValue: { response in - self.libraries.append(contentsOf: response.items ?? []) + self.libraries = response.items ?? [] }) .store(in: &cancellables) } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 1bc9d9d8..cde5c290 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -79,12 +79,23 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) - ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, limit: 100, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, + startIndex: currentPage * 100, + limit: 100, recursive: true, - searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], - filters: filters.filters, sortBy: sortBy, tags: filters.tags, - enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true) + searchTerm: nil, + sortOrder: filters.sortOrder, + parentId: parentID, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], + includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series", "BoxSet"], + filters: filters.filters, + sortBy: sortBy, + tags: filters.tags, + enableUserData: true, + personIds: personIDs, + studioIds: studioIDs, + genreIds: genreIDs, + enableImages: true) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) @@ -112,12 +123,22 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) - ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, limit: 100, + ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100, + limit: 100, recursive: true, - searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], - filters: filters.filters, sortBy: sortBy, tags: filters.tags, - enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true) + searchTerm: nil, + sortOrder: filters.sortOrder, + parentId: parentID, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people, .chapters], + includeItemTypes: filters.filters.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"], + filters: filters.filters, + sortBy: sortBy, + tags: filters.tags, + enableUserData: true, + personIds: personIDs, + studioIds: studioIDs, + genreIds: genreIDs, + enableImages: true) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) }, receiveValue: { [weak self] response in From 4e0e6635c2be01041a13af64344722dc5b060d08 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 6 Jan 2022 23:21:15 -0700 Subject: [PATCH 2/4] 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 */, From 259cda4784499f54b34d2cd44c1b11a22d4b0a78 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 6 Jan 2022 23:34:52 -0700 Subject: [PATCH 3/4] fix iOS build --- JellyfinPlayer.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 263a72c5..180ecb1a 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -248,7 +248,6 @@ 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 */; }; @@ -2291,7 +2290,6 @@ 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 */, From 3841acbc023165b732646da385ce2dfb8f231e7c Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 6 Jan 2022 23:50:10 -0700 Subject: [PATCH 4/4] remove dev team --- JellyfinPlayer.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 180ecb1a..ffcfe4bf 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -2689,7 +2689,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2701,7 +2701,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2726,7 +2726,7 @@ CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2738,7 +2738,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2757,7 +2757,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2766,7 +2766,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2784,7 +2784,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2793,7 +2793,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES;