initial collections implementation

This commit is contained in:
Ethan Pippin 2022-01-06 23:01:17 -07:00
parent e170e0671e
commit dbe95f29db
13 changed files with 152 additions and 29 deletions

View File

@ -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 = "<group>"; };
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = "<group>"; };
E107BB9227880A8F00354E07 /* CollectionItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemViewModel.swift; sourceTree = "<group>"; };
E10D87D92784E4F100BD264C /* ItemViewDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewDetailsView.swift; sourceTree = "<group>"; };
E10D87DB2784EC5200BD264C /* EpisodesRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowView.swift; sourceTree = "<group>"; };
E10D87DD278510E300BD264C /* PosterSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterSize.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
};
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;

View File

@ -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)
}

View File

@ -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)
}
}
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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