From 3f52223be541c32b78fe52746f0a9db3dbbcf311 Mon Sep 17 00:00:00 2001 From: Min-Yih Hsu Date: Tue, 25 Jan 2022 21:14:58 +0800 Subject: [PATCH 1/5] Basic support for folder-type library items - Add a new BaseItemDto.ItemType member: .folder. - Use CollectionItemViewModel to display folder-type item. - For each episode in a folder, fallback to primary image if series primary image is not available. --- .../BaseItemDtoExtensions.swift | 9 +++- Shared/ViewModels/LibraryViewModel.swift | 8 ++- Swiftfin/Views/ItemView/ItemView.swift | 2 +- Swiftfin/Views/LibraryListView.swift | 54 +++++++++---------- Swiftfin/Views/LibraryView.swift | 6 +-- 5 files changed, 41 insertions(+), 38 deletions(-) diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 9a4dcf1d..2cc57295 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -120,8 +120,12 @@ public extension BaseItemDto { } func getSeriesPrimaryImage(maxWidth: Int) -> URL { + guard let seriesId = seriesId else { + return getPrimaryImage(maxWidth: maxWidth) + } + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "", + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId, imageType: .primary, maxWidth: Int(x), quality: 96, @@ -227,6 +231,7 @@ public extension BaseItemDto { case series = "Series" case boxset = "BoxSet" case collectionFolder = "CollectionFolder" + case folder = "Folder" case unknown @@ -249,7 +254,7 @@ public extension BaseItemDto { func portraitHeaderViewURL(maxWidth: Int) -> URL { switch itemType { - case .movie, .season, .series, .boxset, .collectionFolder: + case .movie, .season, .series, .boxset, .collectionFolder, .folder: return getPrimaryImage(maxWidth: maxWidth) case .episode: return getSeriesPrimaryImage(maxWidth: maxWidth) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 12581b39..3f65aed9 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -94,10 +94,14 @@ final class LibraryViewModel: ViewModel { genreIDs = filters.withGenres.compactMap(\.id) } let sortBy = filters.sortBy.map(\.rawValue) + let queryRecursive = filters.filters.contains(.isFavorite) || + self.person != nil || + self.genre != nil || + self.studio != nil ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * pageItemSize, limit: pageItemSize, - recursive: true, + recursive: queryRecursive, searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, @@ -112,7 +116,7 @@ final class LibraryViewModel: ViewModel { ], includeItemTypes: filters.filters .contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode", "BoxSet"] : - ["Movie", "Series", "BoxSet"], + ["Movie", "Series", "BoxSet", "Folder"], filters: filters.filters, sortBy: sortBy, tags: filters.tags, diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 2433b1dc..4090aa3d 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -51,7 +51,7 @@ private struct ItemView: View { self.viewModel = EpisodeItemViewModel(item: item) case .series: self.viewModel = SeriesItemViewModel(item: item) - case .boxset: + case .boxset, .folder: self.viewModel = CollectionItemViewModel(item: item) default: self.viewModel = ItemViewModel(item: item) diff --git a/Swiftfin/Views/LibraryListView.swift b/Swiftfin/Views/LibraryListView.swift index 26c25fb6..0ad8ee53 100644 --- a/Swiftfin/Views/LibraryListView.swift +++ b/Swiftfin/Views/LibraryListView.swift @@ -73,36 +73,32 @@ struct LibraryListView: View { } ForEach(viewModel.libraries, id: \.id) { library in - if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { - Button { - libraryListRouter.route(to: \.library, - (viewModel: LibraryViewModel(parentID: library.id), - title: library.name ?? "")) - } label: { - ZStack { - ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) - .opacity(0.4) - .accessibilityIgnoresInvertColors() - HStack { - Spacer() - VStack { - Text(library.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) - } else { - EmptyView() + Button { + libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(parentID: library.id), + title: library.name ?? "")) + } label: { + ZStack { + ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) + .opacity(0.4) + .accessibilityIgnoresInvertColors() + HStack { + Spacer() + VStack { + Text(library.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) } } else { ProgressView() diff --git a/Swiftfin/Views/LibraryView.swift b/Swiftfin/Views/LibraryView.swift index d2efe0f3..57fa4ede 100644 --- a/Swiftfin/Views/LibraryView.swift +++ b/Swiftfin/Views/LibraryView.swift @@ -45,10 +45,8 @@ struct LibraryView: View { VStack { LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.id) { item in - if item.type != "Folder" { - PortraitItemButton(item: item) { item in - libraryRouter.route(to: \.item, item) - } + PortraitItemButton(item: item) { item in + libraryRouter.route(to: \.item, item) } } } From 87331444be5c14fcbe9fbe106f474ff69d48c137 Mon Sep 17 00:00:00 2001 From: Min-Yih Hsu Date: Wed, 26 Jan 2022 22:59:20 +0800 Subject: [PATCH 2/5] Support folder-type libraries on tvOS And fixed a bug in LibraryListView where proper parent ID was not passed to LibraryViewModel. --- Swiftfin tvOS/Views/ItemView/ItemView.swift | 2 +- Swiftfin tvOS/Views/LibraryListView.swift | 3 ++- Swiftfin tvOS/Views/LibraryView.swift | 20 +++++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index 0d78e0dc..1667deec 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -62,7 +62,7 @@ struct ItemView: View { } else { SeriesItemView(viewModel: SeriesItemViewModel(item: item)) } - case .boxset: + case .boxset, .folder: CinematicCollectionItemView(viewModel: CollectionItemViewModel(item: item)) default: Text(L10n.notImplementedYetWithType(item.type ?? "")) diff --git a/Swiftfin tvOS/Views/LibraryListView.swift b/Swiftfin tvOS/Views/LibraryListView.swift index d4e5436d..480e20b6 100644 --- a/Swiftfin tvOS/Views/LibraryListView.swift +++ b/Swiftfin tvOS/Views/LibraryListView.swift @@ -81,7 +81,8 @@ struct LibraryListView: View { } } else { Button { - self.libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(), title: library.name ?? "")) + self.libraryListRouter.route(to: \.library, + (viewModel: LibraryViewModel(parentID: library.id), title: library.name ?? "")) } label: { ZStack { diff --git a/Swiftfin tvOS/Views/LibraryView.swift b/Swiftfin tvOS/Views/LibraryView.swift index 6ee1fe63..5c1f397b 100644 --- a/Swiftfin tvOS/Views/LibraryView.swift +++ b/Swiftfin tvOS/Views/LibraryView.swift @@ -56,17 +56,15 @@ struct LibraryView: View { } cell: { _, cell in GeometryReader { _ in if let item = cell.item { - if item.type != "Folder" { - Button { - libraryRouter.route(to: \.modalItem, item) - } label: { - PortraitItemElement(item: item) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - .onAppear { - if item == viewModel.items.last && viewModel.hasNextPage { - viewModel.requestNextPageAsync() - } + Button { + libraryRouter.route(to: \.modalItem, item) + } label: { + PortraitItemElement(item: item) + } + .buttonStyle(PlainNavigationLinkButtonStyle()) + .onAppear { + if item == viewModel.items.last && viewModel.hasNextPage { + viewModel.requestNextPageAsync() } } } else if cell.loadingCell { From f84d7965365dac912fe94881c97574d2ed6d7d4f Mon Sep 17 00:00:00 2001 From: Min-Yih Hsu Date: Thu, 27 Jan 2022 22:45:35 +0800 Subject: [PATCH 3/5] Simplify library display logics in LibraryListView And restore the filter that only shows supported library types. --- Swiftfin tvOS/Views/LibraryListView.swift | 34 ++++----------------- Swiftfin/Views/LibraryListView.swift | 37 ++++------------------- 2 files changed, 12 insertions(+), 59 deletions(-) diff --git a/Swiftfin tvOS/Views/LibraryListView.swift b/Swiftfin tvOS/Views/LibraryListView.swift index 480e20b6..9001544d 100644 --- a/Swiftfin tvOS/Views/LibraryListView.swift +++ b/Swiftfin tvOS/Views/LibraryListView.swift @@ -21,39 +21,17 @@ struct LibraryListView: View { @Default(.Experimental.liveTVAlphaEnabled) var liveTVAlphaEnabled + let supportedCollectionTypes = ["movies", "tvshows", "boxsets", "livetv", "other"] + var body: some View { ScrollView { LazyVStack { if !viewModel.isLoading { - 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) - } - 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 + ForEach(viewModel.libraries.filter { [self] library in + let collectionType = library.collectionType ?? "other" + return self.supportedCollectionTypes.contains(collectionType) + }, id: \.id) { library in if library.collectionType == "livetv" { if liveTVAlphaEnabled { Button { diff --git a/Swiftfin/Views/LibraryListView.swift b/Swiftfin/Views/LibraryListView.swift index 0ad8ee53..a7ebe468 100644 --- a/Swiftfin/Views/LibraryListView.swift +++ b/Swiftfin/Views/LibraryListView.swift @@ -16,6 +16,8 @@ struct LibraryListView: View { @StateObject var viewModel = LibraryListViewModel() + let supportedCollectionTypes = ["movies", "tvshows", "boxsets", "other"] + var body: some View { ScrollView { LazyVStack { @@ -42,37 +44,10 @@ 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 + ForEach(viewModel.libraries.filter { [self] library in + let collectionType = library.collectionType ?? "other" + return self.supportedCollectionTypes.contains(collectionType) + }, id: \.id) { library in Button { libraryListRouter.route(to: \.library, (viewModel: LibraryViewModel(parentID: library.id), From e760a586d7371635867d58ab28894ad046241609 Mon Sep 17 00:00:00 2001 From: Min-Yih Hsu Date: Fri, 28 Jan 2022 09:59:31 +0800 Subject: [PATCH 4/5] Add an option to switch between flatten/grouped library view By default, a library shows its items in a flatten view. This patch further adds an option that allows users to show items grouped in their own folders as well. --- Shared/Generated/Strings.swift | 2 ++ .../SwiftfinStore/SwiftfinStoreDefaults.swift | 1 + Shared/ViewModels/LibraryViewModel.swift | 13 +++++++++---- .../SettingsView/CustomizeViewsSettings.swift | 3 +++ .../SettingsView/CustomizeViewsSettings.swift | 3 +++ Translations/en.lproj/Localizable.strings | Bin 11938 -> 12036 bytes 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 561ba3af..dd87548e 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -324,6 +324,8 @@ internal enum L10n { internal static var settings: String { return L10n.tr("Localizable", "settings") } /// Show Cast & Crew internal static var showCastAndCrew: String { return L10n.tr("Localizable", "showCastAndCrew") } + /// Show Flatten Library View + internal static var showFlattenView: String { return L10n.tr("Localizable", "showFlattenView") } /// Show Missing Episodes internal static var showMissingEpisodes: String { return L10n.tr("Localizable", "showMissingEpisodes") } /// Show Missing Seasons diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 3cf1480c..c94abfc8 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -41,6 +41,7 @@ extension Defaults.Keys { // Customize settings static let showPosterLabels = Key("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let showCastAndCrew = Key("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let showFlattenView = Key("showFlattenView", default: true, suite: SwiftfinStore.Defaults.generalSuite) // Video player / overlay settings static let overlayType = Key("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 3f65aed9..0a9bd1db 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -7,6 +7,7 @@ // import Combine +import Defaults import Foundation import JellyfinAPI import SwiftUICollection @@ -94,10 +95,16 @@ final class LibraryViewModel: ViewModel { genreIDs = filters.withGenres.compactMap(\.id) } let sortBy = filters.sortBy.map(\.rawValue) - let queryRecursive = filters.filters.contains(.isFavorite) || + let queryRecursive = Defaults[.showFlattenView] || filters.filters.contains(.isFavorite) || self.person != nil || self.genre != nil || self.studio != nil + let includeItemTypes: [String] + if filters.filters.contains(.isFavorite) { + includeItemTypes = ["Movie", "Series", "Season", "Episode", "BoxSet"] + } else { + includeItemTypes = ["Movie", "Series", "BoxSet"] + (Defaults[.showFlattenView] ? [] : ["Folder"]) + } ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * pageItemSize, limit: pageItemSize, @@ -114,9 +121,7 @@ final class LibraryViewModel: ViewModel { .people, .chapters, ], - includeItemTypes: filters.filters - .contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode", "BoxSet"] : - ["Movie", "Series", "BoxSet", "Folder"], + includeItemTypes: includeItemTypes, filters: filters.filters, sortBy: sortBy, tags: filters.tags, diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift index 65764e6f..78ccb5b6 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift @@ -15,6 +15,8 @@ struct CustomizeViewsSettings: View { var showPosterLabels @Default(.showCastAndCrew) var showCastAndCrew + @Default(.showFlattenView) + var showFlattenView var body: some View { Form { @@ -24,6 +26,7 @@ struct CustomizeViewsSettings: View { // TODO: Uncomment when cast and crew implemented in item views // Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew) + Toggle(L10n.showFlattenView, isOn: $showFlattenView) } header: { L10n.customize.text diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift index 2cbe816d..e443a738 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift @@ -15,6 +15,8 @@ struct CustomizeViewsSettings: View { var showPosterLabels @Default(.showCastAndCrew) var showCastAndCrew + @Default(.showFlattenView) + var showFlattenView var body: some View { Form { @@ -22,6 +24,7 @@ struct CustomizeViewsSettings: View { Toggle(L10n.showPosterLabels, isOn: $showPosterLabels) Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew) + Toggle(L10n.showFlattenView, isOn: $showFlattenView) } header: { L10n.customize.text diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 454c8e7e7bebd2e952ebfff3e866bef1d15a69f1..d83b11a8aed99f1cbd7aeb5f16b741a58658be29 100644 GIT binary patch delta 86 zcmZ1!+Y+~7k)E^?Loq`JLq0<}gBwE*Ln1>75T-KZF@!N>POepu)I$|jVDMqcWJqEt Y0;()xsANz8sRODnXHa6WX5eA~0F Date: Fri, 28 Jan 2022 18:58:03 +0800 Subject: [PATCH 5/5] Revise label text on the flatten library view option --- Shared/Generated/Strings.swift | 2 +- Translations/en.lproj/Localizable.strings | Bin 12036 -> 12028 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index dd87548e..854d717b 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -324,7 +324,7 @@ internal enum L10n { internal static var settings: String { return L10n.tr("Localizable", "settings") } /// Show Cast & Crew internal static var showCastAndCrew: String { return L10n.tr("Localizable", "showCastAndCrew") } - /// Show Flatten Library View + /// Flatten Library Items internal static var showFlattenView: String { return L10n.tr("Localizable", "showFlattenView") } /// Show Missing Episodes internal static var showMissingEpisodes: String { return L10n.tr("Localizable", "showMissingEpisodes") } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index d83b11a8aed99f1cbd7aeb5f16b741a58658be29..abc407bafe5986dd4a07c08bb684c2aab3e0a87f 100644 GIT binary patch delta 28 jcmZpP`xCq2jNasHdI|!b3?&Sy47m)&3`z{v3|tHVp0)@U delta 30 lcmewp+Y-0ojNarP1+K|w^c48Q7%~}B8Oj-y7_1q%7y!9}2