diff --git a/Shared/Coordinators/MoviesLibrariesCoordinator.swift b/Shared/Coordinators/MoviesLibrariesCoordinator.swift index 22f53ca3..66d37b3f 100644 --- a/Shared/Coordinators/MoviesLibrariesCoordinator.swift +++ b/Shared/Coordinators/MoviesLibrariesCoordinator.swift @@ -36,10 +36,10 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable { } func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) } func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) } } diff --git a/Shared/Coordinators/TVLibrariesCoordinator.swift b/Shared/Coordinators/TVLibrariesCoordinator.swift index 28f3ddaa..51bd4117 100644 --- a/Shared/Coordinators/TVLibrariesCoordinator.swift +++ b/Shared/Coordinators/TVLibrariesCoordinator.swift @@ -36,10 +36,10 @@ final class TVLibrariesCoordinator: NavigationCoordinatable { } func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) } func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(parentID: library.id), title: library.title) + LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift index 66a24733..a67c7678 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+Poster.swift @@ -35,7 +35,7 @@ extension BaseItemDto: Poster { var showTitle: Bool { switch type { - case .episode, .series, .movie, .boxSet: + case .episode, .series, .movie, .boxSet, .collectionFolder: return Defaults[.Customization.showPosterLabels] default: return true diff --git a/Shared/Objects/LibraryItem.swift b/Shared/Objects/LibraryItem.swift index c2d2dfc6..9afbfeef 100644 --- a/Shared/Objects/LibraryItem.swift +++ b/Shared/Objects/LibraryItem.swift @@ -9,6 +9,8 @@ import JellyfinAPI import SwiftUI +// TODO: Look at something better that possibly doesn't depend on the viewmodel +// and accomodates favorites and liveTV better struct LibraryItem: Equatable, Poster { var library: BaseItemDto @@ -29,4 +31,12 @@ struct LibraryItem: Equatable, Poster { lhs.library == rhs.library && lhs.viewModel.libraryImages[lhs.library.id ?? ""] == rhs.viewModel.libraryImages[rhs.library.id ?? ""] } + + static func favorites(viewModel: MediaViewModel) -> LibraryItem { + .init(library: .init(name: L10n.favorites, collectionType: "favorites"), viewModel: viewModel) + } + + static func liveTV(viewModel: MediaViewModel) -> LibraryItem { + .init(library: .init(name: "LiveTV", collectionType: "liveTV"), viewModel: viewModel) + } } diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 41ecd2c9..e2ef839e 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -28,7 +28,6 @@ extension Defaults.Keys { static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: .generalSuite) enum Customization { - static let showFlattenView = Key("showFlattenView", default: true, suite: .generalSuite) static let itemViewType = Key("itemViewType", default: .compactLogo, suite: .generalSuite) static let showPosterLabels = Key("showPosterLabels", default: true, suite: .generalSuite) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 9eaf6986..6b9eb078 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -11,28 +11,24 @@ import Defaults import JellyfinAPI import UIKit +// TODO: Look at refactoring final class LibraryViewModel: ViewModel { - @Default(.Customization.Library.gridPosterType) - var libraryGridPosterType - @Published var items: [BaseItemDto] = [] @Published - var totalPages = 0 - @Published - var currentPage = 0 - @Published - var hasNextPage = false - - // temp + private var currentPage = 0 + private var hasNextPage = true @Published var filters: LibraryFilters - var parentID: String? - var person: BaseItemPerson? - var genre: NameGuidPair? - var studio: NameGuidPair? + @Default(.Customization.Library.gridPosterType) + private var libraryGridPosterType + + let library: BaseItemDto? + let person: BaseItemPerson? + let genre: NameGuidPair? + let studio: NameGuidPair? private var pageItemSize: Int { let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77 @@ -48,13 +44,13 @@ final class LibraryViewModel: ViewModel { } init( - parentID: String? = nil, + library: BaseItemDto? = nil, person: BaseItemPerson? = nil, genre: NameGuidPair? = nil, studio: NameGuidPair? = nil, filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]) ) { - self.parentID = parentID + self.library = library self.person = person self.genre = genre self.studio = studio @@ -78,31 +74,33 @@ final class LibraryViewModel: ViewModel { let personIDs: [String] = [person].compactMap(\.?.id) let studioIDs: [String] = [studio].compactMap(\.?.id) let genreIDs: [String] + if filters.withGenres.isEmpty { genreIDs = [genre].compactMap(\.?.id) } else { genreIDs = filters.withGenres.compactMap(\.id) } + let sortBy = filters.sortBy.map(\.rawValue) - let queryRecursive = Defaults[.Customization.showFlattenView] || filters.filters.contains(.isFavorite) || - self.person != nil || - self.genre != nil || - self.studio != nil + let includeItemTypes: [BaseItemKind] + if filters.filters.contains(.isFavorite) { - includeItemTypes = [.movie, .series, .season, .episode, .boxSet] + includeItemTypes = [.movie, .boxSet, .series, .season, .episode] + } else if library?.collectionType == "folders" { + includeItemTypes = [.collectionFolder] } else { - includeItemTypes = [.movie, .series, .boxSet] + (Defaults[.Customization.showFlattenView] ? [] : [.folder]) + includeItemTypes = [.movie, .series, .boxSet] } ItemsAPI.getItemsByUserId( userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * pageItemSize, limit: pageItemSize, - recursive: queryRecursive, + recursive: true, searchTerm: nil, sortOrder: filters.sortOrder.compactMap { SortOrder(rawValue: $0.rawValue) }, - parentId: parentID, + parentId: library?.id, fields: ItemFields.allCases, includeItemTypes: includeItemTypes, filters: filters.filters, @@ -118,18 +116,18 @@ final class LibraryViewModel: ViewModel { .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestError(completion: completion) }, receiveValue: { [weak self] response in + guard !(response.items?.isEmpty ?? false) else { + self?.hasNextPage = false + return + } - guard let self = self else { return } - let totalPages = ceil(Double(response.totalRecordCount ?? 0) / Double(self.pageItemSize)) - - self.totalPages = Int(totalPages) - self.hasNextPage = self.currentPage < self.totalPages - 1 - self.items.append(contentsOf: response.items ?? []) + self?.items.append(contentsOf: response.items ?? []) }) .store(in: &cancellables) } func requestNextPageAsync() { + guard hasNextPage else { return } currentPage += 1 requestItemsAsync(with: filters) } diff --git a/Shared/ViewModels/MediaViewModel.swift b/Shared/ViewModels/MediaViewModel.swift index e2ae588d..320884cc 100644 --- a/Shared/ViewModels/MediaViewModel.swift +++ b/Shared/ViewModels/MediaViewModel.swift @@ -13,15 +13,21 @@ import JellyfinAPI final class MediaViewModel: ViewModel { @Published - var libraries: [BaseItemDto] = [] + private var libraries: [LibraryItem] = [] @Published var libraryImages: [String: [ImageSource]] = [:] - private var supportedLibraries: [String] { - ["movies", "tvshows", "unknown"] - .appending("livetv", if: Defaults[.Experimental.liveTVAlphaEnabled]) + @Default(.Experimental.liveTVAlphaEnabled) + private var liveTVEnabled + + var libraryItems: [LibraryItem] { + [.init(library: .init(name: L10n.favorites, collectionType: "favorites"), viewModel: self)] + .appending(.init(library: .init(name: "LiveTV", collectionType: "liveTV"), viewModel: self), if: liveTVEnabled) + .appending(libraries) } + private static let supportedCollectionTypes: [String] = ["boxsets", "folders", "movies", "tvshows", "unknown"] + override init() { super.init() @@ -31,15 +37,16 @@ final class MediaViewModel: ViewModel { func requestLibraries() { UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) - .trackActivity(loading) .sink(receiveCompletion: { completion in self.handleAPIRequestError(completion: completion) }, receiveValue: { response in guard let items = response.items else { return } - self.libraries = items.filter { self.supportedLibraries.contains($0.collectionType ?? "unknown") } - self.libraries.forEach { + let filteredLibraries = items.filter { Self.supportedCollectionTypes.contains($0.collectionType ?? "unknown") } + filteredLibraries.forEach { self.getRandomItemImageSource(with: nil, id: $0.id, key: $0.id ?? "") } + + self.libraries = filteredLibraries.map { .init(library: $0, viewModel: self) } }) .store(in: &cancellables) } diff --git a/Swiftfin tvOS/Views/LatestInLibraryView.swift b/Swiftfin tvOS/Views/LatestInLibraryView.swift index ec80bba5..aa6981d3 100644 --- a/Swiftfin tvOS/Views/LatestInLibraryView.swift +++ b/Swiftfin tvOS/Views/LatestInLibraryView.swift @@ -22,7 +22,7 @@ struct LatestInLibraryView: View { Button { router.route(to: \.library, ( viewModel: .init( - parentID: viewModel.library.id!, + library: viewModel.library, filters: LibraryFilters( filters: [], sortOrder: [.descending], diff --git a/Swiftfin tvOS/Views/MediaView.swift b/Swiftfin tvOS/Views/MediaView.swift index 1d7389a3..f55f9ca2 100644 --- a/Swiftfin tvOS/Views/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView.swift @@ -13,25 +13,25 @@ import SwiftUI struct MediaView: View { + @EnvironmentObject + private var tabRouter: MainCoordinator.Router @EnvironmentObject private var router: MediaCoordinator.Router @ObservedObject var viewModel: MediaViewModel - private var libraryItems: [LibraryItem] { - [LibraryItem(library: .init(name: L10n.favorites, id: "favorites"), viewModel: viewModel)] + - viewModel.libraries.map { LibraryItem(library: $0, viewModel: viewModel) } - } - var body: some View { - CollectionView(items: libraryItems) { _, item, _ in + CollectionView(items: viewModel.libraryItems) { _, item, _ in PosterButton(item: item, type: .landscape) .scaleItem(0.8) .onSelect { _ in - if item.library.id == "favorites" { + switch item.library.collectionType { + case "favorites": router.route(to: \.library, (viewModel: .init(filters: .favorites), title: "")) - } else { - router.route(to: \.library, (viewModel: .init(parentID: item.library.id), title: "")) + case "liveTV": + tabRouter.root(\.liveTV) + default: + router.route(to: \.library, (viewModel: .init(library: item.library), title: "")) } } .imageOverlay { _ in diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift index ef45edbf..2182944d 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings.swift @@ -13,8 +13,6 @@ struct CustomizeViewsSettings: View { @Default(.Customization.showPosterLabels) var showPosterLabels - @Default(.Customization.showFlattenView) - var showFlattenView var body: some View { Form { @@ -22,8 +20,6 @@ struct CustomizeViewsSettings: View { Toggle(L10n.showPosterLabels, isOn: $showPosterLabels) - Toggle(L10n.showFlattenView, isOn: $showFlattenView) - } header: { L10n.customize.text } diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index 9078a0e0..2dd9b4b2 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -24,7 +24,7 @@ struct LatestInLibraryView: View { PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: latestInLibraryPosterType, items: viewModel.items) .trailing { Button { - let libraryViewModel = LibraryViewModel(parentID: viewModel.library.id, filters: HomeViewModel.recentFilterSet) + let libraryViewModel = LibraryViewModel(library: viewModel.library, filters: HomeViewModel.recentFilterSet) homeRouter.route(to: \.library, (viewModel: libraryViewModel, title: viewModel.library.displayName)) } label: { HStack { diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index e24d47b3..1b8b5461 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -44,6 +44,8 @@ struct ItemView: View { } case .person: LibraryView(viewModel: .init(person: .init(id: item.id))) + case .collectionFolder: + LibraryView(viewModel: .init(library: item)) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin/Views/LibraryView/LibraryView.swift b/Swiftfin/Views/LibraryView/LibraryView.swift index f6e00ecb..e13a3964 100644 --- a/Swiftfin/Views/LibraryView/LibraryView.swift +++ b/Swiftfin/Views/LibraryView/LibraryView.swift @@ -125,7 +125,7 @@ struct LibraryView: View { .route(to: \.filter, ( filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, - parentId: viewModel.parentID ?? "" + parentId: viewModel.library?.id ?? "" )) } label: { Image(systemName: "line.horizontal.3.decrease.circle") diff --git a/Swiftfin/Views/MediaView.swift b/Swiftfin/Views/MediaView.swift index eb366bd8..998a3e67 100644 --- a/Swiftfin/Views/MediaView.swift +++ b/Swiftfin/Views/MediaView.swift @@ -7,7 +7,6 @@ // import CollectionView -import Defaults import JellyfinAPI import Stinsen import SwiftUI @@ -18,14 +17,6 @@ struct MediaView: View { private var router: MediaCoordinator.Router @ObservedObject var viewModel: MediaViewModel - @Default(.Experimental.liveTVAlphaEnabled) - var liveTVEnabled - - private var libraryItems: [LibraryItem] { - [LibraryItem(library: .init(name: L10n.favorites, id: "favorites"), viewModel: viewModel)] - .appending(.init(library: .init(name: "LiveTV", id: "liveTV"), viewModel: viewModel), if: liveTVEnabled) - .appending(viewModel.libraries.map { LibraryItem(library: $0, viewModel: viewModel) }) - } private var gridLayout: NSCollectionLayoutSection.GridLayoutMode { if UIDevice.isPhone { @@ -36,16 +27,17 @@ struct MediaView: View { } var body: some View { - CollectionView(items: libraryItems) { _, item, _ in + CollectionView(items: viewModel.libraryItems) { _, item, _ in PosterButton(item: item, type: .landscape) .scaleItem(UIDevice.isPhone ? 0.9 : 1) .onSelect { _ in - if item.library.id == "favorites" { + switch item.library.collectionType { + case "favorites": router.route(to: \.library, (viewModel: .init(filters: .favorites), title: "")) - } else if item.library.id == "liveTV" { + case "liveTV": router.route(to: \.liveTV) - } else { - router.route(to: \.library, (viewModel: .init(parentID: item.library.id), title: "")) + default: + router.route(to: \.library, (viewModel: .init(library: item.library), title: "")) } } .imageOverlay { _ in diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift index a028ee9c..0572ff55 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift @@ -11,8 +11,6 @@ import SwiftUI struct CustomizeViewsSettings: View { - @Default(.Customization.showFlattenView) - var showFlattenView @Default(.Customization.itemViewType) var itemViewType @@ -45,9 +43,6 @@ struct CustomizeViewsSettings: View { var body: some View { List { Section { - - Toggle(L10n.showFlattenView, isOn: $showFlattenView) - Picker(L10n.items, selection: $itemViewType) { ForEach(ItemViewType.allCases, id: \.self) { type in Text(type.localizedName).tag(type.rawValue)