diff --git a/Shared/Coordinators/BasicLibraryCoordinator.swift b/Shared/Coordinators/BasicLibraryCoordinator.swift new file mode 100644 index 00000000..d42a73c3 --- /dev/null +++ b/Shared/Coordinators/BasicLibraryCoordinator.swift @@ -0,0 +1,55 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +// TODO: See if this and LibraryCoordinator can be merged, +// along with all corresponding views +final class BasicLibraryCoordinator: NavigationCoordinatable { + + struct Parameters { + let title: String? + let viewModel: PagingLibraryViewModel + } + + let stack = NavigationStack(initial: \BasicLibraryCoordinator.start) + + @Root + var start = makeStart + @Route(.push) + var item = makeItem + @Route(.push) + var library = makeLibrary + + private let parameters: Parameters + + init(parameters: Parameters) { + self.parameters = parameters + } + + @ViewBuilder + func makeStart() -> some View { + BasicLibraryView(viewModel: parameters.viewModel) + #if !os(tvOS) + .if(parameters.title != nil) { view in + view.navigationTitle(parameters.title ?? .emptyDash) + } + #endif + } + + func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemCoordinator(item: item)) + } + + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(LibraryCoordinator(parameters: parameters)) + } +} diff --git a/Shared/Coordinators/CastAndCrewLibraryCoordinator.swift b/Shared/Coordinators/CastAndCrewLibraryCoordinator.swift new file mode 100644 index 00000000..ab7ab203 --- /dev/null +++ b/Shared/Coordinators/CastAndCrewLibraryCoordinator.swift @@ -0,0 +1,37 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import Stinsen +import SwiftUI + +final class CastAndCrewLibraryCoordinator: NavigationCoordinatable { + + let stack = NavigationStack(initial: \CastAndCrewLibraryCoordinator.start) + + @Root + var start = makeStart + @Route(.push) + var library = makeLibrary + + let people: [BaseItemPerson] + + init(people: [BaseItemPerson]) { + self.people = people + } + + @ViewBuilder + func makeStart() -> some View { + CastAndCrewLibraryView(people: people) + } + + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { + LibraryCoordinator(parameters: parameters) + } +} diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index 087499d4..855e4934 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -24,11 +24,15 @@ final class HomeCoordinator: NavigationCoordinatable { @Route(.modal) var item = makeItem @Route(.modal) + var basicLibrary = makeBasicLibrary + @Route(.modal) var library = makeLibrary #else @Route(.push) var item = makeItem @Route(.push) + var basicLibrary = makeBasicLibrary + @Route(.push) var library = makeLibrary #endif @@ -41,6 +45,10 @@ final class HomeCoordinator: NavigationCoordinatable { NavigationViewCoordinator(ItemCoordinator(item: item)) } + func makeBasicLibrary(parameters: BasicLibraryCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(BasicLibraryCoordinator(parameters: parameters)) + } + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> NavigationViewCoordinator { NavigationViewCoordinator(LibraryCoordinator(parameters: parameters)) } @@ -49,6 +57,10 @@ final class HomeCoordinator: NavigationCoordinatable { ItemCoordinator(item: item) } + func makeBasicLibrary(parameters: BasicLibraryCoordinator.Parameters) -> BasicLibraryCoordinator { + BasicLibraryCoordinator(parameters: parameters) + } + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { LibraryCoordinator(parameters: parameters) } diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index 2780a43a..3dec8324 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -20,7 +20,11 @@ final class ItemCoordinator: NavigationCoordinatable { @Route(.push) var item = makeItem @Route(.push) + var basicLibrary = makeBasicLibrary + @Route(.push) var library = makeLibrary + @Route(.push) + var castAndCrew = makeCastAndCrew @Route(.modal) var itemOverview = makeItemOverview @Route(.fullScreen) @@ -32,20 +36,24 @@ final class ItemCoordinator: NavigationCoordinatable { self.itemDto = item } - func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { - LibraryCoordinator(parameters: parameters) - } - func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } - func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto)) + func makeBasicLibrary(parameters: BasicLibraryCoordinator.Parameters) -> BasicLibraryCoordinator { + BasicLibraryCoordinator(parameters: parameters) } - func makeSeason(item: BaseItemDto) -> ItemCoordinator { - ItemCoordinator(item: item) + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { + LibraryCoordinator(parameters: parameters) + } + + func makeCastAndCrew(people: [BaseItemPerson]) -> CastAndCrewLibraryCoordinator { + CastAndCrewLibraryCoordinator(people: people) + } + + func makeItemOverview(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto)) } func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator { diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index 287b9536..2c7c6392 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -48,10 +48,10 @@ final class LibraryCoordinator: NavigationCoordinatable { #else @Route(.push) var item = makeItem - @Route(.modal) - var filter = makeFilter @Route(.push) var library = makeLibrary + @Route(.modal) + var filter = makeFilter #endif private let parameters: Parameters @@ -63,9 +63,9 @@ final class LibraryCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { if let parent = parameters.parent { - LibraryView(viewModel: .init(parent: parent, type: parameters.type, filters: parameters.filters)) + LibraryView(viewModel: LibraryViewModel(parent: parent, type: parameters.type, filters: parameters.filters)) } else { - LibraryView(viewModel: .init(filters: parameters.filters)) + LibraryView(viewModel: LibraryViewModel(filters: parameters.filters)) } } @@ -82,12 +82,12 @@ final class LibraryCoordinator: NavigationCoordinatable { ItemCoordinator(item: item) } - func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator { - NavigationViewCoordinator(FilterCoordinator(parameters: parameters)) - } - func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { LibraryCoordinator(parameters: parameters) } + + func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(FilterCoordinator(parameters: parameters)) + } #endif } diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index f7b7d460..a4e10749 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -13,7 +13,7 @@ import SwiftUI final class MainTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ \MainTabCoordinator.home, - \MainTabCoordinator.tv, + \MainTabCoordinator.tvShows, \MainTabCoordinator.movies, \MainTabCoordinator.search, \MainTabCoordinator.media, @@ -23,7 +23,7 @@ final class MainTabCoordinator: TabCoordinatable { @Route(tabItem: makeHomeTab) var home = makeHome @Route(tabItem: makeTvTab) - var tv = makeTv + var tvShows = makeTVShows @Route(tabItem: makeMoviesTab) var movies = makeMovies @Route(tabItem: makeSearchTab) @@ -45,8 +45,12 @@ final class MainTabCoordinator: TabCoordinatable { } } - func makeTv() -> NavigationViewCoordinator { - NavigationViewCoordinator(TVLibrariesCoordinator(viewModel: TVLibrariesViewModel(), title: L10n.tvShows)) + func makeTVShows() -> NavigationViewCoordinator { + let parameters = BasicLibraryCoordinator.Parameters( + title: nil, + viewModel: ItemTypeLibraryViewModel(itemTypes: [.series], filters: .init()) + ) + return NavigationViewCoordinator(BasicLibraryCoordinator(parameters: parameters)) } @ViewBuilder @@ -57,8 +61,12 @@ final class MainTabCoordinator: TabCoordinatable { } } - func makeMovies() -> NavigationViewCoordinator { - NavigationViewCoordinator(MovieLibrariesCoordinator(viewModel: MovieLibrariesViewModel(), title: L10n.movies)) + func makeMovies() -> NavigationViewCoordinator { + let parameters = BasicLibraryCoordinator.Parameters( + title: nil, + viewModel: ItemTypeLibraryViewModel(itemTypes: [.movie], filters: .init()) + ) + return NavigationViewCoordinator(BasicLibraryCoordinator(parameters: parameters)) } @ViewBuilder diff --git a/Shared/Coordinators/MoviesLibrariesCoordinator.swift b/Shared/Coordinators/MoviesLibrariesCoordinator.swift deleted file mode 100644 index d6c4479c..00000000 --- a/Shared/Coordinators/MoviesLibrariesCoordinator.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation -import JellyfinAPI -import Stinsen -import SwiftUI - -final class MovieLibrariesCoordinator: NavigationCoordinatable { - - let stack = NavigationStack(initial: \MovieLibrariesCoordinator.start) - - @Root - var start = makeStart - @Root - var rootLibrary = makeRootLibrary - @Route(.push) - var library = makeLibrary - - let viewModel: MovieLibrariesViewModel - let title: String - - init(viewModel: MovieLibrariesViewModel, title: String) { - self.viewModel = viewModel - self.title = title - } - - @ViewBuilder - func makeStart() -> some View { - MovieLibrariesView(viewModel: self.viewModel, title: title) - } - - func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) - } - - func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) - } -} diff --git a/Shared/Coordinators/TVLibrariesCoordinator.swift b/Shared/Coordinators/TVLibrariesCoordinator.swift deleted file mode 100644 index 35cb2b11..00000000 --- a/Shared/Coordinators/TVLibrariesCoordinator.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Foundation -import JellyfinAPI -import Stinsen -import SwiftUI - -final class TVLibrariesCoordinator: NavigationCoordinatable { - - let stack = NavigationStack(initial: \TVLibrariesCoordinator.start) - - @Root - var start = makeStart - @Root - var rootLibrary = makeRootLibrary - @Route(.push) - var library = makeLibrary - - let viewModel: TVLibrariesViewModel - let title: String - - init(viewModel: TVLibrariesViewModel, title: String) { - self.viewModel = viewModel - self.title = title - } - - @ViewBuilder - func makeStart() -> some View { - TVLibrariesView(viewModel: self.viewModel, title: title) - } - - func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) - } - - func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) - } -} diff --git a/Shared/Extensions/ArrayExtensions.swift b/Shared/Extensions/ArrayExtensions.swift index f3f95b62..eacee289 100644 --- a/Shared/Extensions/ArrayExtensions.swift +++ b/Shared/Extensions/ArrayExtensions.swift @@ -25,3 +25,9 @@ extension Array { self + contents } } + +extension ArraySlice { + var asArray: [Element] { + Array(self) + } +} diff --git a/Shared/Extensions/ColorExtension.swift b/Shared/Extensions/ColorExtensions.swift similarity index 100% rename from Shared/Extensions/ColorExtension.swift rename to Shared/Extensions/ColorExtensions.swift diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 91adba78..c5f3390c 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -10,6 +10,12 @@ import Foundation import JellyfinAPI import UIKit +extension BaseItemDto: Displayable { + var displayName: String { + name ?? .emptyDash + } +} + extension BaseItemDto: Identifiable {} extension BaseItemDto: LibraryParent {} @@ -86,10 +92,6 @@ extension BaseItemDto { return 0 } - var displayName: String { - name ?? .emptyDash - } - // MARK: ItemDetail struct ItemDetail { diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift index cb143f56..2c0d82b2 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift @@ -10,8 +10,6 @@ import Foundation import JellyfinAPI import UIKit -// MARK: PortraitImageStackable - extension BaseItemPerson: Poster { var subtitle: String? { diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index f9e5d33d..a65bea77 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -10,6 +10,14 @@ import Foundation import JellyfinAPI import UIKit +extension BaseItemPerson: Displayable { + var displayName: String { + self.name ?? .emptyDash + } +} + +extension BaseItemPerson: LibraryParent {} + extension BaseItemPerson { // MARK: First Role @@ -50,11 +58,3 @@ extension BaseItemPerson { return DisplayedType(rawValue: type) != nil } } - -extension BaseItemPerson: Displayable { - var displayName: String { - self.name ?? .emptyDash - } -} - -extension BaseItemPerson: LibraryParent {} diff --git a/Shared/Objects/LibraryItem.swift b/Shared/Objects/MediaLibraryItem.swift similarity index 81% rename from Shared/Objects/LibraryItem.swift rename to Shared/Objects/MediaLibraryItem.swift index fc796db5..a32964f1 100644 --- a/Shared/Objects/LibraryItem.swift +++ b/Shared/Objects/MediaLibraryItem.swift @@ -11,7 +11,7 @@ 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 { +struct MediaLibraryItem: Equatable, Poster { var library: BaseItemDto var viewModel: MediaViewModel @@ -27,16 +27,16 @@ struct LibraryItem: Equatable, Poster { viewModel.libraryImages[library.id ?? ""] ?? [] } - static func == (lhs: LibraryItem, rhs: LibraryItem) -> Bool { + static func == (lhs: MediaLibraryItem, rhs: MediaLibraryItem) -> Bool { lhs.library == rhs.library && lhs.viewModel.libraryImages[lhs.library.id ?? ""] == rhs.viewModel.libraryImages[rhs.library.id ?? ""] } - static func favorites(viewModel: MediaViewModel) -> LibraryItem { + static func favorites(viewModel: MediaViewModel) -> MediaLibraryItem { .init(library: .init(name: L10n.favorites, collectionType: "favorites"), viewModel: viewModel) } - static func liveTV(viewModel: MediaViewModel) -> LibraryItem { + static func liveTV(viewModel: MediaViewModel) -> MediaLibraryItem { .init(library: .init(name: "LiveTV", collectionType: "liveTV"), viewModel: viewModel) } } diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 2772c34c..85f24224 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -13,12 +13,12 @@ import JellyfinAPI final class HomeViewModel: ViewModel { - @Published - var latestAddedItems: [BaseItemDto] = [] @Published var resumeItems: [BaseItemDto] = [] @Published - var nextUpItems: [BaseItemDto] = [] + var hasNextUp: Bool = false + @Published + var hasRecentlyAdded: Bool = false @Published var librariesShowRecentlyAddedIDs: [String] = [] @Published @@ -44,7 +44,6 @@ final class HomeViewModel: ViewModel { librariesShowRecentlyAddedIDs = [] libraries = [] resumeItems = [] - nextUpItems = [] refresh() } @@ -119,36 +118,23 @@ final class HomeViewModel: ViewModel { .store(in: &cancellables) } - // MARK: Latest Added Items + // MARK: Recently Added Items private func refreshLatestAddedItems() { UserLibraryAPI.getLatestMedia( userId: SessionManager.main.currentLogin.user.id, - fields: [ - .primaryImageAspectRatio, - .seriesPrimaryImage, - .seasonUserData, - .overview, - .genres, - .people, - .chapters, - ], includeItemTypes: [.movie, .series], - enableImageTypes: [.primary, .backdrop, .thumb], - enableUserData: true, - limit: 20 + limit: 1 ) .sink { completion in switch completion { case .finished: () case .failure: - self.nextUpItems = [] + self.hasRecentlyAdded = false self.handleAPIRequestError(completion: completion) } } receiveValue: { items in - LogManager.log.debug("Retrieved \(String(items.count)) resume items") - - self.latestAddedItems = items + self.hasRecentlyAdded = items.count > 0 } .store(in: &cancellables) } @@ -207,30 +193,18 @@ final class HomeViewModel: ViewModel { private func refreshNextUpItems() { TvShowsAPI.getNextUp( userId: SessionManager.main.currentLogin.user.id, - limit: 20, - fields: [ - .primaryImageAspectRatio, - .seriesPrimaryImage, - .seasonUserData, - .overview, - .genres, - .people, - .chapters, - ], - enableUserData: true + limit: 1 ) .trackActivity(loading) .sink(receiveCompletion: { completion in switch completion { case .finished: () case .failure: - self.nextUpItems = [] + self.hasNextUp = false self.handleAPIRequestError(completion: completion) } }, receiveValue: { response in - LogManager.log.debug("Retrieved \(String(response.items!.count)) nextup items") - - self.nextUpItems = response.items ?? [] + self.hasNextUp = (response.items ?? []).count > 0 }) .store(in: &cancellables) } diff --git a/Shared/ViewModels/ItemTypeLibraryViewModel.swift b/Shared/ViewModels/ItemTypeLibraryViewModel.swift new file mode 100644 index 00000000..24f7b600 --- /dev/null +++ b/Shared/ViewModels/ItemTypeLibraryViewModel.swift @@ -0,0 +1,75 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +final class ItemTypeLibraryViewModel: PagingLibraryViewModel { + + let itemTypes: [BaseItemKind] + let filterViewModel: FilterViewModel + + init(itemTypes: [BaseItemKind], filters: ItemFilters) { + self.itemTypes = itemTypes + self.filterViewModel = .init(parent: nil, currentFilters: filters) + super.init() + + filterViewModel.$currentFilters + .sink { newFilters in + self.requestItems(with: newFilters, replaceCurrentItems: true) + } + .store(in: &cancellables) + } + + private func requestItems(with filters: ItemFilters, replaceCurrentItems: Bool = false) { + + if replaceCurrentItems { + self.items = [] + self.currentPage = 0 + self.hasNextPage = true + } + + let genreIDs = filters.genres.compactMap(\.id) + let sortBy: [String] = filters.sortBy.map(\.filterName).appending("IsFolder") + let sortOrder = filters.sortOrder.map { SortOrder(rawValue: $0.filterName) ?? .ascending } + let itemFilters: [ItemFilter] = filters.filters.compactMap { .init(rawValue: $0.filterName) } + let tags: [String] = filters.tags.map(\.filterName) + + ItemsAPI.getItemsByUserId( + userId: SessionManager.main.currentLogin.user.id, + startIndex: currentPage * pageItemSize, + limit: pageItemSize, + recursive: true, + sortOrder: sortOrder, + fields: ItemFields.allCases, + includeItemTypes: itemTypes, + filters: itemFilters, + sortBy: sortBy, + tags: tags, + enableUserData: true, + genreIds: genreIDs + ) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] response in + guard let items = response.items, !items.isEmpty else { + self?.hasNextPage = false + return + } + + self?.items.append(contentsOf: items) + } + .store(in: &cancellables) + } + + override func _requestNextPage() { + requestItems(with: filterViewModel.currentFilters) + } +} diff --git a/Shared/ViewModels/LatestMediaViewModel.swift b/Shared/ViewModels/LatestMediaViewModel.swift deleted file mode 100644 index 4ad903ef..00000000 --- a/Shared/ViewModels/LatestMediaViewModel.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -final class LatestMediaViewModel: ViewModel { - - @Published - var items = [BaseItemDto]() - - let library: BaseItemDto - - init(library: BaseItemDto) { - self.library = library - super.init() - - requestLatestMedia() - } - - func requestLatestMedia() { - LogManager.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)") - UserLibraryAPI.getLatestMedia( - userId: SessionManager.main.currentLogin.user.id, - parentId: library.id ?? "", - fields: ItemFields.allCases, - includeItemTypes: [.series, .movie], - enableUserData: true, - limit: 12 - ) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] response in - self?.items = response - LogManager.log.debug("Retrieved \(String(self?.items.count ?? 0)) items") - }) - .store(in: &cancellables) - } -} diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index a8177ed5..4f06a1c5 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -7,27 +7,26 @@ // import Combine -import Defaults import JellyfinAPI import SwiftUI import UIKit // TODO: Look at refactoring -final class LibraryViewModel: ViewModel { - - @Default(.Customization.Library.gridPosterType) - private var libraryGridPosterType - - @Published - var items: [BaseItemDto] = [] +final class LibraryViewModel: PagingLibraryViewModel { let filterViewModel: FilterViewModel - private var currentPage = 0 - private var hasNextPage = true let parent: LibraryParent? let type: LibraryParentType + var libraryCoordinatorParameters: LibraryCoordinator.Parameters { + if let parent = parent { + return .init(parent: parent, type: type, filters: filterViewModel.currentFilters) + } else { + return .init(filters: filterViewModel.currentFilters) + } + } + init(filters: ItemFilters) { self.parent = nil self.type = .library @@ -58,11 +57,6 @@ final class LibraryViewModel: ViewModel { .store(in: &cancellables) } - private var pageItemSize: Int { - let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77 - return UIScreen.main.maxChildren(width: libraryGridPosterType.width, height: height) - } - private func requestItems(with filters: ItemFilters, replaceCurrentItems: Bool = false) { if replaceCurrentItems { @@ -156,9 +150,7 @@ final class LibraryViewModel: ViewModel { .store(in: &cancellables) } - func requestNextPage() { - guard hasNextPage else { return } - currentPage += 1 + override func _requestNextPage() { requestItems(with: filterViewModel.currentFilters) } } diff --git a/Shared/ViewModels/MediaViewModel.swift b/Shared/ViewModels/MediaViewModel.swift index a3aeddf9..bbb391e9 100644 --- a/Shared/ViewModels/MediaViewModel.swift +++ b/Shared/ViewModels/MediaViewModel.swift @@ -13,14 +13,14 @@ import JellyfinAPI final class MediaViewModel: ViewModel { @Published - private var libraries: [LibraryItem] = [] + private var libraries: [MediaLibraryItem] = [] @Published var libraryImages: [String: [ImageSource]] = [:] @Default(.Experimental.liveTVAlphaEnabled) private var liveTVEnabled - var libraryItems: [LibraryItem] { + var libraryItems: [MediaLibraryItem] { [.init(library: .init(name: L10n.favorites, collectionType: "favorites"), viewModel: self)] .appending(.init(library: .init(name: "LiveTV", collectionType: "liveTV"), viewModel: self), if: liveTVEnabled) .appending(libraries) diff --git a/Shared/ViewModels/MovieLibrariesViewModel.swift b/Shared/ViewModels/MovieLibrariesViewModel.swift deleted file mode 100644 index 75d41c34..00000000 --- a/Shared/ViewModels/MovieLibrariesViewModel.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI -import Stinsen -import SwiftUICollection - -final class MovieLibrariesViewModel: ViewModel { - - @Published - var rows = [LibraryRow]() - @Published - var totalPages = 0 - @Published - var currentPage = 0 - @Published - var hasNextPage = false - @Published - var hasPreviousPage = false - - private var libraries = [BaseItemDto]() - private let columns: Int - - @RouterObject - private var router: MovieLibrariesCoordinator.Router? - - init(columns: Int = 7) { - self.columns = columns - super.init() - - requestLibraries() - } - - func requestLibraries() { - - UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) - }, receiveValue: { response in - if let responseItems = response.items { - self.libraries = [] - for library in responseItems { - if library.collectionType == "movies" { - self.libraries.append(library) - } - } - self.rows = self.calculateRows() - if self.libraries.count == 1, let library = self.libraries.first { - // make this library the root of this stack - self.router?.coordinator.root(\.rootLibrary, library) - } - } - }) - .store(in: &cancellables) - } - - private func calculateRows() -> [LibraryRow] { - guard !libraries.isEmpty else { return [] } - let rowCount = libraries.count / columns - var calculatedRows = [LibraryRow]() - for i in 0 ... rowCount { - let firstItemIndex = i * columns - var lastItemIndex = firstItemIndex + columns - if lastItemIndex > libraries.count { - lastItemIndex = libraries.count - } - - var rowCells = [LibraryRowCell]() - for item in libraries[firstItemIndex ..< lastItemIndex] { - let newCell = LibraryRowCell(item: item) - rowCells.append(newCell) - } - if i == rowCount && hasNextPage { - var loadingCell = LibraryRowCell(item: nil) - loadingCell.loadingCell = true - rowCells.append(loadingCell) - } - - calculatedRows.append(LibraryRow( - section: i, - items: rowCells - )) - } - return calculatedRows - } -} diff --git a/Shared/ViewModels/NextUpLibraryViewModel.swift b/Shared/ViewModels/NextUpLibraryViewModel.swift new file mode 100644 index 00000000..5aea62e9 --- /dev/null +++ b/Shared/ViewModels/NextUpLibraryViewModel.swift @@ -0,0 +1,51 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +final class NextUpLibraryViewModel: PagingLibraryViewModel { + + override init() { + super.init() + + _requestNextPage() + } + + override func _requestNextPage() { + + TvShowsAPI.getNextUp( + userId: SessionManager.main.currentLogin.user.id, + startIndex: currentPage * pageItemSize, + limit: pageItemSize, + fields: [ + .primaryImageAspectRatio, + .seriesPrimaryImage, + .seasonUserData, + .overview, + .genres, + .people, + .chapters, + ], + enableUserData: true + ) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] response in + guard let items = response.items, !items.isEmpty else { + self?.hasNextPage = false + return + } + + self?.items.append(contentsOf: items) + } + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/PagingLibraryViewModel.swift b/Shared/ViewModels/PagingLibraryViewModel.swift new file mode 100644 index 00000000..5989bda3 --- /dev/null +++ b/Shared/ViewModels/PagingLibraryViewModel.swift @@ -0,0 +1,37 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import JellyfinAPI +import UIKit + +class PagingLibraryViewModel: ViewModel { + + @Default(.Customization.Library.gridPosterType) + private var libraryGridPosterType + + @Published + var items: [BaseItemDto] = [] + + var currentPage = 0 + var hasNextPage = true + + var pageItemSize: Int { + let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77 + return UIScreen.main.maxChildren(width: libraryGridPosterType.width, height: height) + } + + func requestNextPage() { + guard hasNextPage else { return } + currentPage += 1 + _requestNextPage() + } + + func _requestNextPage() {} +} diff --git a/Shared/ViewModels/RecentlyAddedViewModel.swift b/Shared/ViewModels/RecentlyAddedViewModel.swift new file mode 100644 index 00000000..ebbdf80d --- /dev/null +++ b/Shared/ViewModels/RecentlyAddedViewModel.swift @@ -0,0 +1,46 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +final class RecentlyAddedLibraryViewModel: PagingLibraryViewModel { + + override init() { + super.init() + + _requestNextPage() + } + + override func _requestNextPage() { + ItemsAPI.getItemsByUserId( + userId: SessionManager.main.currentLogin.user.id, + startIndex: currentPage * pageItemSize, + limit: pageItemSize, + recursive: true, + sortOrder: [.descending], + fields: ItemFields.allCases, + includeItemTypes: [.movie, .series], + sortBy: [SortBy.dateAdded.rawValue], + enableUserData: true + ) + .trackActivity(loading) + .sink { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + } receiveValue: { [weak self] response in + guard let items = response.items, !items.isEmpty else { + self?.hasNextPage = false + return + } + + self?.items.append(contentsOf: items) + } + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/StaticLibraryViewModel.swift b/Shared/ViewModels/StaticLibraryViewModel.swift new file mode 100644 index 00000000..2d4b17c3 --- /dev/null +++ b/Shared/ViewModels/StaticLibraryViewModel.swift @@ -0,0 +1,19 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +class StaticLibraryViewModel: PagingLibraryViewModel { + + init(items: [BaseItemDto]) { + super.init() + + self.items = items + } +} diff --git a/Shared/ViewModels/TVLibrariesViewModel.swift b/Shared/ViewModels/TVLibrariesViewModel.swift deleted file mode 100644 index 2bff284a..00000000 --- a/Shared/ViewModels/TVLibrariesViewModel.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI -import Stinsen -import SwiftUICollection - -typealias LibraryRow = CollectionRow - -struct LibraryRowCell: Hashable { - let id = UUID() - let item: BaseItemDto? - var loadingCell: Bool = false -} - -final class TVLibrariesViewModel: ViewModel { - - @Published - var rows = [LibraryRow]() - @Published - var totalPages = 0 - @Published - var currentPage = 0 - @Published - var hasNextPage = false - @Published - var hasPreviousPage = false - - private var libraries = [BaseItemDto]() - private let columns: Int - - @RouterObject - private var router: TVLibrariesCoordinator.Router? - - init(columns: Int = 7) { - self.columns = columns - super.init() - - requestLibraries() - } - - func requestLibraries() { - - UserViewsAPI.getUserViews(userId: SessionManager.main.currentLogin.user.id) - .trackActivity(loading) - .sink(receiveCompletion: { completion in - self.handleAPIRequestError(completion: completion) - }, receiveValue: { response in - if let responseItems = response.items { - self.libraries = [] - for library in responseItems { - if library.collectionType == "tvshows" { - self.libraries.append(library) - } - } - self.rows = self.calculateRows() - if self.libraries.count == 1, let library = self.libraries.first { - // make this library the root of this stack - self.router?.coordinator.root(\.rootLibrary, library) - } - } - }) - .store(in: &cancellables) - } - - private func calculateRows() -> [LibraryRow] { - guard !libraries.isEmpty else { return [] } - let rowCount = libraries.count / columns - var calculatedRows = [LibraryRow]() - for i in 0 ... rowCount { - let firstItemIndex = i * columns - var lastItemIndex = firstItemIndex + columns - if lastItemIndex > libraries.count { - lastItemIndex = libraries.count - } - - var rowCells = [LibraryRowCell]() - for item in libraries[firstItemIndex ..< lastItemIndex] { - let newCell = LibraryRowCell(item: item) - rowCells.append(newCell) - } - if i == rowCount && hasNextPage { - var loadingCell = LibraryRowCell(item: nil) - loadingCell.loadingCell = true - rowCells.append(loadingCell) - } - - calculatedRows.append(LibraryRow( - section: i, - items: rowCells - )) - } - return calculatedRows - } -} diff --git a/Swiftfin tvOS/Components/CinematicItemSelector.swift b/Swiftfin tvOS/Components/CinematicItemSelector.swift index 8cb2b71f..0f03b817 100644 --- a/Swiftfin tvOS/Components/CinematicItemSelector.swift +++ b/Swiftfin tvOS/Components/CinematicItemSelector.swift @@ -11,7 +11,14 @@ import JellyfinAPI import Nuke import SwiftUI -struct CinematicItemSelector: View { +struct CinematicItemSelector< + Item: Poster, + TopContent: View, + ItemContent: View, + ItemImageOverlay: View, + ItemContextMenu: View, + TrailingContent: View +>: View { @ObservedObject private var viewModel: CinematicBackgroundView.ViewModel = .init() @@ -20,6 +27,7 @@ struct CinematicItemSelector ItemContent private var itemImageOverlay: (Item) -> ItemImageOverlay private var itemContextMenu: (Item) -> ItemContextMenu + private var trailingContent: () -> TrailingContent private var onSelect: (Item) -> Void let items: [Item] @@ -62,6 +70,7 @@ struct CinematicItemSelector(@ViewBuilder _ content: @escaping (Item) -> T) - -> CinematicItemSelector { - CinematicItemSelector( + -> CinematicItemSelector { + CinematicItemSelector( topContent: content, itemContent: itemContent, itemImageOverlay: itemImageOverlay, itemContextMenu: itemContextMenu, + trailingContent: trailingContent, onSelect: onSelect, items: items ) @@ -200,12 +212,13 @@ extension CinematicItemSelector { @ViewBuilder func content(@ViewBuilder _ content: @escaping (Item) -> C) - -> CinematicItemSelector { - CinematicItemSelector( + -> CinematicItemSelector { + CinematicItemSelector( topContent: topContent, itemContent: content, itemImageOverlay: itemImageOverlay, itemContextMenu: itemContextMenu, + trailingContent: trailingContent, onSelect: onSelect, items: items ) @@ -213,12 +226,13 @@ extension CinematicItemSelector { @ViewBuilder func itemImageOverlay(@ViewBuilder _ imageOverlay: @escaping (Item) -> O) - -> CinematicItemSelector { - CinematicItemSelector( + -> CinematicItemSelector { + CinematicItemSelector( topContent: topContent, itemContent: itemContent, itemImageOverlay: imageOverlay, itemContextMenu: itemContextMenu, + trailingContent: trailingContent, onSelect: onSelect, items: items ) @@ -226,12 +240,27 @@ extension CinematicItemSelector { @ViewBuilder func contextMenu(@ViewBuilder _ contextMenu: @escaping (Item) -> M) - -> CinematicItemSelector { - CinematicItemSelector( + -> CinematicItemSelector { + CinematicItemSelector( topContent: topContent, itemContent: itemContent, itemImageOverlay: itemImageOverlay, itemContextMenu: contextMenu, + trailingContent: trailingContent, + onSelect: onSelect, + items: items + ) + } + + @ViewBuilder + func trailingContent(@ViewBuilder _ content: @escaping () -> T) + -> CinematicItemSelector { + CinematicItemSelector( + topContent: topContent, + itemContent: itemContent, + itemImageOverlay: itemImageOverlay, + itemContextMenu: itemContextMenu, + trailingContent: content, onSelect: onSelect, items: items ) diff --git a/Swiftfin tvOS/Components/PagingLibraryView.swift b/Swiftfin tvOS/Components/PagingLibraryView.swift new file mode 100644 index 00000000..4d884159 --- /dev/null +++ b/Swiftfin tvOS/Components/PagingLibraryView.swift @@ -0,0 +1,56 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct PagingLibraryView: View { + + @ObservedObject + var viewModel: PagingLibraryViewModel + private var onSelect: (BaseItemDto) -> Void + + @Default(.Customization.Library.gridPosterType) + private var libraryPosterType + + var body: some View { + CollectionView(items: viewModel.items) { _, item, _ in + PosterButton(item: item, type: libraryPosterType) + .onSelect { + onSelect(item) + } + } + .layout { _, layoutEnvironment in + .grid( + layoutEnvironment: layoutEnvironment, + layoutMode: .fixedNumberOfColumns(7), + lineSpacing: 50 + ) + } + .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 600, trailing: 0)) { edge in + if !viewModel.isLoading && edge == .bottom { + viewModel.requestNextPage() + } + } + } +} + +extension PagingLibraryView { + init(viewModel: PagingLibraryViewModel) { + self.viewModel = viewModel + self.onSelect = { _ in } + } + + func onSelect(_ action: @escaping (BaseItemDto) -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin tvOS/Components/PortraitItemElement.swift b/Swiftfin tvOS/Components/PortraitItemElement.swift deleted file mode 100644 index db3b1d66..00000000 --- a/Swiftfin tvOS/Components/PortraitItemElement.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -// TODO: Transition to PosterButton` -struct PortraitItemElement: View { - - @Environment(\.isFocused) - var envFocused: Bool - @State - var focused: Bool = false - @State - var backgroundURL: URL? - - var item: BaseItemDto - - var body: some View { - VStack { - ImageView(item.type == .episode ? item.seriesImageSource(.primary, maxWidth: 200) : item.imageSource(.primary, maxWidth: 200)) - .frame(width: 200, height: 300) - .cornerRadius(10) - .shadow(radius: focused ? 10.0 : 0) - .shadow(radius: focused ? 10.0 : 0) - .overlay( - ZStack { - if item.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } - } - .padding(2) - .opacity(1), - alignment: .bottomLeading - ) - .overlay( - ZStack { - if item.userData?.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } else { - if item.userData?.unplayedItemCount != nil { - Image(systemName: "circle.fill") - .foregroundColor(Color(.systemBlue)) - Text(String(item.userData!.unplayedItemCount ?? 0)) - .foregroundColor(.white) - .font(.caption2) - } - } - }.padding(2) - .opacity(1), - alignment: .topTrailing - ).opacity(1) - Text(item.title) - .frame(width: 200, height: 30, alignment: .center) - if item.type == .movie || item.type == .series { - Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else if item.type == .season { - Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text(L10n.seasonAndEpisode(String(item.parentIndexNumber ?? 0), String(item.indexNumber ?? 0))) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } - } - .onChange(of: envFocused) { envFocus in - withAnimation(.linear(duration: 0.15)) { - self.focused = envFocus - } - } - .scaleEffect(focused ? 1.1 : 1) - } -} diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index 63798e79..58c09853 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -17,10 +17,10 @@ struct PosterButton Content - private var imageOverlay: (Item) -> ImageOverlay - private var contextMenu: (Item) -> ContextMenu - private var onSelect: (Item) -> Void + private var content: () -> Content + private var imageOverlay: () -> ImageOverlay + private var contextMenu: () -> ContextMenu + private var onSelect: () -> Void private var onFocus: () -> Void private var singleImage: Bool @@ -31,31 +31,37 @@ struct PosterButton, type: type, itemScale: 1, horizontalAlignment: .leading, - content: { PosterButtonDefaultContentView(item: $0) }, - imageOverlay: { _ in EmptyView() }, - contextMenu: { _ in EmptyView() }, - onSelect: { _ in }, + content: { PosterButtonDefaultContentView(item: item) }, + imageOverlay: { EmptyView() }, + contextMenu: { EmptyView() }, + onSelect: {}, onFocus: {}, singleImage: singleImage ) @@ -100,7 +106,7 @@ extension PosterButton { } @ViewBuilder - func content(@ViewBuilder _ content: @escaping (Item) -> C) -> PosterButton { + func content(@ViewBuilder _ content: @escaping () -> C) -> PosterButton { PosterButton( item: item, type: type, @@ -116,7 +122,7 @@ extension PosterButton { } @ViewBuilder - func imageOverlay(@ViewBuilder _ imageOverlay: @escaping (Item) -> O) -> PosterButton { + func imageOverlay(@ViewBuilder _ imageOverlay: @escaping () -> O) -> PosterButton { PosterButton( item: item, type: type, @@ -132,7 +138,7 @@ extension PosterButton { } @ViewBuilder - func contextMenu(@ViewBuilder _ contextMenu: @escaping (Item) -> M) -> PosterButton { + func contextMenu(@ViewBuilder _ contextMenu: @escaping () -> M) -> PosterButton { PosterButton( item: item, type: type, @@ -147,7 +153,7 @@ extension PosterButton { ) } - func onSelect(_ action: @escaping (Item) -> Void) -> Self { + func onSelect(_ action: @escaping () -> Void) -> Self { var copy = self copy.onSelect = action return copy diff --git a/Swiftfin tvOS/Components/PosterHStack.swift b/Swiftfin tvOS/Components/PosterHStack.swift index e21afee9..ec7158b6 100644 --- a/Swiftfin tvOS/Components/PosterHStack.swift +++ b/Swiftfin tvOS/Components/PosterHStack.swift @@ -41,10 +41,10 @@ struct PosterHStack Void + + var body: some View { + Button { + onSelect() + } label: { + ZStack { + Color(UIColor.darkGray) + .opacity(0.5) + + VStack(spacing: 20) { + Image(systemName: "chevron.right") + .font(.title) + + L10n.seeAll.text + .font(.title3) + } + } + .posterStyle(type: type, width: type.width) + } + .buttonStyle(.plain) + } +} + +extension SeeAllPoster { + init(type: PosterType) { + self.type = type + self.onSelect = {} + } + + func onSelect(_ action: @escaping () -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin tvOS/Views/BasicLibraryView.swift b/Swiftfin tvOS/Views/BasicLibraryView.swift new file mode 100644 index 00000000..ad2f8c63 --- /dev/null +++ b/Swiftfin tvOS/Views/BasicLibraryView.swift @@ -0,0 +1,51 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct BasicLibraryView: View { + + @EnvironmentObject + private var router: BasicLibraryCoordinator.Router + @ObservedObject + var viewModel: PagingLibraryViewModel + + @ViewBuilder + private var loadingView: some View { + ProgressView() + } + + @ViewBuilder + private var noResultsView: some View { + L10n.noResults.text + } + + @ViewBuilder + private var libraryItemsView: some View { + PagingLibraryView(viewModel: viewModel) + .onSelect { item in + router.route(to: \.item, item) + } + .ignoresSafeArea() + } + + var body: some View { + Group { + if viewModel.isLoading && viewModel.items.isEmpty { + loadingView + } else if viewModel.items.isEmpty { + noResultsView + } else { + libraryItemsView + } + } + } +} diff --git a/Swiftfin tvOS/Views/CastAndCrewLibraryView.swift b/Swiftfin tvOS/Views/CastAndCrewLibraryView.swift new file mode 100644 index 00000000..9c459cfe --- /dev/null +++ b/Swiftfin tvOS/Views/CastAndCrewLibraryView.swift @@ -0,0 +1,54 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import JellyfinAPI +import SwiftUI + +struct CastAndCrewLibraryView: View { + + @EnvironmentObject + private var router: CastAndCrewLibraryCoordinator.Router + let people: [BaseItemPerson] + + @ViewBuilder + private var noResultsView: some View { + L10n.noResults.text + } + + @ViewBuilder + private var libraryGridView: some View { + CollectionView(items: people) { _, person, _ in + PosterButton(item: person, type: .portrait) + .onSelect { + router.route(to: \.library, .init(parent: person, type: .person, filters: .init())) + } + } + .layout { _, layoutEnvironment in + .grid( + layoutEnvironment: layoutEnvironment, + layoutMode: .fixedNumberOfColumns(7), + lineSpacing: 50 + ) + } + .configure { configuration in + configuration.showsVerticalScrollIndicator = false + } + } + + var body: some View { + Group { + if people.isEmpty { + noResultsView + } else { + libraryGridView + } + } + .ignoresSafeArea() + } +} diff --git a/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift new file mode 100644 index 00000000..45e420f4 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift @@ -0,0 +1,63 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension HomeView { + + struct CinematicRecentlyAddedView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: ItemTypeLibraryViewModel + + private func itemSelectorImageSource(for item: BaseItemDto) -> ImageSource { + if item.type == .episode { + return item.seriesImageSource( + .logo, + maxWidth: UIScreen.main.bounds.width * 0.4, + maxHeight: 200 + ) + } else { + return item.imageSource( + .logo, + maxWidth: UIScreen.main.bounds.width * 0.4, + maxHeight: 200 + ) + } + } + + var body: some View { + CinematicItemSelector(items: viewModel.items.prefix(20).asArray) + .topContent { item in + ImageView(itemSelectorImageSource(for: item)) + .resizingMode(.bottomLeft) + .placeholder { + EmptyView() + } + .failure { + Text(item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + } + .padding2(.leading) + } + .onSelect { item in + router.route(to: \.item, item) + } + .trailingContent { + SeeAllPoster(type: .landscape) + .onSelect { + router.route(to: \.basicLibrary, .init(title: L10n.recentlyAdded, viewModel: viewModel)) + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift b/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift new file mode 100644 index 00000000..fc824c86 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift @@ -0,0 +1,72 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension HomeView { + + struct CinematicResumeView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + private func itemSelectorImageSource(for item: BaseItemDto) -> ImageSource { + if item.type == .episode { + return item.seriesImageSource( + .logo, + maxWidth: UIScreen.main.bounds.width * 0.4, + maxHeight: 200 + ) + } else { + return item.imageSource( + .logo, + maxWidth: UIScreen.main.bounds.width * 0.4, + maxHeight: 200 + ) + } + } + + var body: some View { + CinematicItemSelector(items: viewModel.resumeItems) + .topContent { item in + ImageView(itemSelectorImageSource(for: item)) + .resizingMode(.bottomLeft) + .placeholder { + EmptyView() + } + .failure { + Text(item.displayName) + .font(.largeTitle) + .fontWeight(.semibold) + } + .padding2(.leading) + } + .content { item in + if let subtitle = item.subtitle { + Text(subtitle) + .font(.caption) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(2) + } + } + .itemImageOverlay { item in + LandscapePosterProgressBar( + title: item.progress ?? L10n.continue, + progress: (item.userData?.playedPercentage ?? 0) / 100 + ) + } + .onSelect { item in + router.route(to: \.item, item) + } + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift new file mode 100644 index 00000000..69e4e198 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift @@ -0,0 +1,38 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension HomeView { + + struct LatestInLibraryView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @StateObject + var viewModel: LibraryViewModel + + var body: some View { + PosterHStack( + title: L10n.latestWithString(viewModel.parent?.displayName ?? .emptyDash), + type: .portrait, + items: viewModel.items + ) + .trailing { + SeeAllPoster(type: .portrait) + .onSelect { + router.route(to: \.library, viewModel.libraryCoordinatorParameters) + } + } + .onSelect { item in + router.route(to: \.item, item) + } + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift new file mode 100644 index 00000000..b42e791b --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift @@ -0,0 +1,52 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension HomeView { + + struct NextUpView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: NextUpLibraryViewModel + + @Default(.Customization.nextUpPosterType) + private var nextUpPosterType + + var body: some View { + PosterHStack( + title: L10n.nextUp, + type: nextUpPosterType, + items: viewModel.items.prefix(20).asArray + ) + .trailing { + Button { + router.route(to: \.basicLibrary, .init(title: L10n.nextUp, viewModel: viewModel)) + } label: { + HStack { + L10n.seeAll.text + Image(systemName: "chevron.right") + } + .font(.subheadline.bold()) + } + } + .onSelect { item in + router.route(to: \.item, item) + } + .trailing { + SeeAllPoster(type: nextUpPosterType) + .onSelect { + router.route(to: \.basicLibrary, .init(title: L10n.nextUp, viewModel: viewModel)) + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift new file mode 100644 index 00000000..67bdaf08 --- /dev/null +++ b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift @@ -0,0 +1,41 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension HomeView { + + struct RecentlyAddedView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: ItemTypeLibraryViewModel + + @Default(.Customization.recentlyAddedPosterType) + private var recentlyAddedPosterType + + var body: some View { + PosterHStack( + title: L10n.recentlyAdded, + type: recentlyAddedPosterType, + items: viewModel.items.prefix(20).asArray + ) + .onSelect { item in + router.route(to: \.item, item) + } + .trailing { + SeeAllPoster(type: recentlyAddedPosterType) + .onSelect { + router.route(to: \.basicLibrary, .init(title: L10n.recentlyAdded, viewModel: viewModel)) + } + } + } + } +} diff --git a/Swiftfin tvOS/Views/HomeView/HomeContentView.swift b/Swiftfin tvOS/Views/HomeView/HomeContentView.swift index 882c10a4..071cd56d 100644 --- a/Swiftfin tvOS/Views/HomeView/HomeContentView.swift +++ b/Swiftfin tvOS/Views/HomeView/HomeContentView.swift @@ -18,111 +18,36 @@ extension HomeView { @ObservedObject var viewModel: HomeViewModel - private func itemSelectorImageSource(for item: BaseItemDto) -> ImageSource { - if item.type == .episode { - return item.seriesImageSource( - .logo, - maxWidth: UIScreen.main.bounds.width * 0.4, - maxHeight: 200 - ) - } else { - return item.imageSource( - .logo, - maxWidth: UIScreen.main.bounds.width * 0.4, - maxHeight: 200 - ) - } - } - - @ViewBuilder - private var cinematicResumeItems: some View { - CinematicItemSelector(items: viewModel.resumeItems) - .topContent { item in - ImageView(itemSelectorImageSource(for: item)) - .resizingMode(.bottomLeft) - .placeholder { - EmptyView() - } - .failure { - Text(item.displayName) - .font(.largeTitle) - .fontWeight(.semibold) - } - .padding2(.leading) - } - .content { item in - if let subtitle = item.subtitle { - Text(subtitle) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(2) - } - } - .itemImageOverlay { item in - LandscapePosterProgressBar( - title: item.progress ?? L10n.continue, - progress: (item.userData?.playedPercentage ?? 0) / 100 - ) - } - .onSelect { item in - router.route(to: \.item, item) - } - } - - @ViewBuilder - private var cinematicLatestAddedItems: some View { - CinematicItemSelector(items: viewModel.latestAddedItems) - .topContent { item in - ImageView(itemSelectorImageSource(for: item)) - .resizingMode(.bottomLeft) - .placeholder { - EmptyView() - } - .failure { - Text(item.displayName) - .font(.largeTitle) - .fontWeight(.semibold) - } - .padding2(.leading) - } - .onSelect { item in - router.route(to: \.item, item) - } - } - var body: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 0) { - if viewModel.resumeItems.isEmpty { - cinematicLatestAddedItems - if !viewModel.nextUpItems.isEmpty { - PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems) - .onSelect { item in - router.route(to: \.item, item) - } + if viewModel.resumeItems.isEmpty { + CinematicRecentlyAddedView(viewModel: .init( + itemTypes: [.movie, .series], + filters: .init(sortOrder: [APISortOrder.descending.filter], sortBy: [SortBy.dateAdded.filter]) + )) + + if viewModel.hasNextUp { + NextUpView(viewModel: .init()) } } else { - cinematicResumeItems + CinematicResumeView(viewModel: viewModel) - if !viewModel.nextUpItems.isEmpty { - PosterHStack(title: L10n.nextUp, type: .portrait, items: viewModel.nextUpItems) - .onSelect { item in - router.route(to: \.item, item) - } + if viewModel.hasNextUp { + NextUpView(viewModel: .init()) } - if !viewModel.latestAddedItems.isEmpty { - PosterHStack(title: L10n.recentlyAdded, type: .portrait, items: viewModel.latestAddedItems) - .onSelect { item in - router.route(to: \.item, item) - } + if viewModel.hasRecentlyAdded { + RecentlyAddedView(viewModel: .init( + itemTypes: [.movie, .series], + filters: .init(sortOrder: [APISortOrder.descending.filter], sortBy: [SortBy.dateAdded.filter]) + )) } } ForEach(viewModel.libraries, id: \.self) { library in - LatestInLibraryView(viewModel: LatestMediaViewModel(library: library)) + LatestInLibraryView(viewModel: .init(parent: library, type: .library, filters: .recent)) } } } diff --git a/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift index ad105c33..865323e1 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift @@ -10,6 +10,7 @@ import JellyfinAPI import SwiftUI extension ItemView { + struct CastAndCrewHStack: View { @EnvironmentObject @@ -20,8 +21,14 @@ extension ItemView { PosterHStack( title: L10n.castAndCrew, type: .portrait, - items: people + items: people.filter(\.isDisplayed).prefix(20).asArray ) + .trailing { + SeeAllPoster(type: .portrait) + .onSelect { + router.route(to: \.castAndCrew, people) + } + } .onSelect { person in router.route(to: \.library, .init(parent: person, type: .person, filters: .init())) } diff --git a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift index 0ec9b27e..009e14b0 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift @@ -11,6 +11,7 @@ import JellyfinAPI import SwiftUI extension ItemView { + struct SimilarItemsHStack: View { @Default(.Customization.similarPosterType) @@ -26,6 +27,13 @@ extension ItemView { type: similarPosterType, items: items ) + .trailing { + SeeAllPoster(type: similarPosterType) + .onSelect { + let viewModel = StaticLibraryViewModel(items: items) + router.route(to: \.basicLibrary, .init(title: L10n.recommended, viewModel: viewModel)) + } + } .onSelect { item in router.route(to: \.item, item) } diff --git a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift index fa5a3a16..3c8b23f6 100644 --- a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift @@ -25,7 +25,7 @@ extension EpisodeItemView { .frame(height: UIScreen.main.bounds.height - 150) .padding(.bottom, 50) - ItemView.CastAndCrewHStack(people: viewModel.item.people?.filter(\.isDisplayed) ?? []) + ItemView.CastAndCrewHStack(people: viewModel.item.people ?? []) if let seriesItem = viewModel.seriesItem { PosterHStack(title: L10n.series, type: .portrait, items: [seriesItem]) diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift index 881a4dc1..9aa6f9e9 100644 --- a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift @@ -25,7 +25,7 @@ extension MovieItemView { .frame(height: UIScreen.main.bounds.height - 150) .padding(.bottom, 50) - ItemView.CastAndCrewHStack(people: viewModel.item.people?.filter(\.isDisplayed) ?? []) + ItemView.CastAndCrewHStack(people: viewModel.item.people ?? []) ItemView.SimilarItemsHStack(items: viewModel.similarItems) diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift index e5908485..38dcd146 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift @@ -30,7 +30,7 @@ extension SeriesItemView { SeriesEpisodesView(viewModel: viewModel) .environmentObject(focusGuide) - ItemView.CastAndCrewHStack(people: viewModel.item.people?.filter(\.isDisplayed) ?? []) + ItemView.CastAndCrewHStack(people: viewModel.item.people ?? []) ItemView.SimilarItemsHStack(items: viewModel.similarItems) diff --git a/Swiftfin tvOS/Views/LatestInLibraryView.swift b/Swiftfin tvOS/Views/LatestInLibraryView.swift deleted file mode 100644 index 1965adbb..00000000 --- a/Swiftfin tvOS/Views/LatestInLibraryView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct LatestInLibraryView: View { - - @EnvironmentObject - private var router: HomeCoordinator.Router - @StateObject - var viewModel: LatestMediaViewModel - - var body: some View { - PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: .portrait, items: viewModel.items) - .trailing { - Button { - router.route(to: \.library, .init(parent: viewModel.library, type: .library, filters: .recent)) - } label: { - ZStack { - Color(UIColor.darkGray) - .opacity(0.5) - - VStack(spacing: 20) { - Image(systemName: "chevron.right") - .font(.title) - - L10n.seeAll.text - .font(.title3) - } - } - .posterStyle(type: .portrait, width: PosterType.portrait.width) - } - .buttonStyle(.plain) - } - .onSelect { item in - router.route(to: \.item, item) - } - } -} diff --git a/Swiftfin tvOS/Views/LibraryView.swift b/Swiftfin tvOS/Views/LibraryView.swift index ad65bd0d..94044b86 100644 --- a/Swiftfin tvOS/Views/LibraryView.swift +++ b/Swiftfin tvOS/Views/LibraryView.swift @@ -8,7 +8,6 @@ import CollectionView import Defaults -import Introspect import JellyfinAPI import SwiftUI @@ -18,11 +17,6 @@ struct LibraryView: View { private var router: LibraryCoordinator.Router @ObservedObject var viewModel: LibraryViewModel - @State - private var scrollViewOffset: CGPoint = .zero - - @Default(.Customization.Library.gridPosterType) - private var libraryPosterType @ViewBuilder private var loadingView: some View { @@ -50,26 +44,11 @@ struct LibraryView: View { @ViewBuilder private var libraryItemsView: some View { - CollectionView(items: viewModel.items) { _, item, _ in - PosterButton(item: item, type: libraryPosterType) - .onSelect { item in - baseItemOnSelect(item) - } - } - .layout { _, layoutEnvironment in - .grid( - layoutEnvironment: layoutEnvironment, - layoutMode: .fixedNumberOfColumns(7), - lineSpacing: 50 - ) - } - .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 600, trailing: 0)) { edge in - if !viewModel.isLoading && edge == .bottom { - viewModel.requestNextPage() + PagingLibraryView(viewModel: viewModel) + .onSelect { item in + baseItemOnSelect(item) } - } - .scrollViewOffset($scrollViewOffset) - .ignoresSafeArea() + .ignoresSafeArea() } var body: some View { diff --git a/Swiftfin tvOS/Views/MediaView.swift b/Swiftfin tvOS/Views/MediaView.swift index c7f31cc1..b832a55b 100644 --- a/Swiftfin tvOS/Views/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView.swift @@ -24,7 +24,7 @@ struct MediaView: View { CollectionView(items: viewModel.libraryItems) { _, item, _ in PosterButton(item: item, type: .landscape) .scaleItem(1.12) - .onSelect { _ in + .onSelect { switch item.library.collectionType { case "favorites": router.route(to: \.library, .init(parent: item.library, type: .library, filters: .favorites)) @@ -36,7 +36,7 @@ struct MediaView: View { router.route(to: \.library, .init(parent: item.library, type: .library, filters: .init())) } } - .imageOverlay { _ in + .imageOverlay { ZStack { Color.black .opacity(0.5) diff --git a/Swiftfin tvOS/Views/MovieLibrariesView.swift b/Swiftfin tvOS/Views/MovieLibrariesView.swift deleted file mode 100644 index 109117e0..00000000 --- a/Swiftfin tvOS/Views/MovieLibrariesView.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI -import SwiftUICollection - -struct MovieLibrariesView: View { - - @EnvironmentObject - private var movieLibrariesRouter: MovieLibrariesCoordinator.Router - @StateObject - var viewModel: MovieLibrariesViewModel - var title: String - - var body: some View { - if viewModel.isLoading == true { - ProgressView() - } else if !viewModel.rows.isEmpty { - CollectionView(rows: viewModel.rows) { _, _ in - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .absolute(200), - heightDimension: .absolute(300) - ) - let group = NSCollectionLayoutGroup.horizontal( - layoutSize: groupSize, - subitems: [item] - ) - - let header = - NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .absolute(44) - ), - elementKind: UICollectionView.elementKindSectionHeader, - alignment: .topLeading - ) - - let section = NSCollectionLayoutSection(group: group) - - section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) - section.interGroupSpacing = 48 - section.orthogonalScrollingBehavior = .continuous - section.boundarySupplementaryItems = [header] - return section - } cell: { _, cell in - GeometryReader { _ in - if let item = cell.item { - if item.type != .folder { - Button { - self.movieLibrariesRouter.route(to: \.library, item) - } label: { - PortraitItemElement(item: item) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - } - } else if cell.loadingCell { - ProgressView() - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) - } - } - } supplementaryView: { _, indexPath in - HStack { - Spacer() - }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .ignoresSafeArea(.all) - } else { - VStack { - L10n.noResults.text - Button { - print("movieLibraries reload") - } label: { - L10n.refresh.text - } - } - } - } -} diff --git a/Swiftfin tvOS/Views/TVLibrariesView.swift b/Swiftfin tvOS/Views/TVLibrariesView.swift deleted file mode 100644 index 5e5179c4..00000000 --- a/Swiftfin tvOS/Views/TVLibrariesView.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI -import SwiftUICollection - -struct TVLibrariesView: View { - @EnvironmentObject - private var tvLibrariesRouter: TVLibrariesCoordinator.Router - @StateObject - var viewModel: TVLibrariesViewModel - var title: String - - var body: some View { - if viewModel.isLoading == true { - ProgressView() - } else if !viewModel.rows.isEmpty { - CollectionView(rows: viewModel.rows) { _, _ in - let itemSize = NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .fractionalHeight(1) - ) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize( - widthDimension: .absolute(200), - heightDimension: .absolute(300) - ) - let group = NSCollectionLayoutGroup.horizontal( - layoutSize: groupSize, - subitems: [item] - ) - - let header = - NSCollectionLayoutBoundarySupplementaryItem( - layoutSize: NSCollectionLayoutSize( - widthDimension: .fractionalWidth(1), - heightDimension: .absolute(44) - ), - elementKind: UICollectionView.elementKindSectionHeader, - alignment: .topLeading - ) - - let section = NSCollectionLayoutSection(group: group) - - section.contentInsets = NSDirectionalEdgeInsets(top: 30, leading: 0, bottom: 80, trailing: 80) - section.interGroupSpacing = 48 - section.orthogonalScrollingBehavior = .continuous - section.boundarySupplementaryItems = [header] - return section - } cell: { _, cell in - GeometryReader { _ in - if let item = cell.item { - if item.type != .folder { - Button { - self.tvLibrariesRouter.route(to: \.library, item) - } label: { - PortraitItemElement(item: item) - } - .buttonStyle(PlainNavigationLinkButtonStyle()) - } - } else if cell.loadingCell { - ProgressView() - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) - } - } - } supplementaryView: { _, indexPath in - HStack { - Spacer() - }.accessibilityIdentifier("\(indexPath.section).\(indexPath.row)") - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .ignoresSafeArea(.all) - } else { - VStack { - L10n.noResults.text - Button { - print("tvLibraries reload") - } label: { - L10n.refresh.text - } - } - } - } -} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 6a9ee118..8531202d 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -39,7 +39,6 @@ 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; - 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D80267BDFC60004248C /* PortraitItemElement.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; }; @@ -153,10 +152,8 @@ 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C83B07288C6A630004ED0C /* FontPicker.swift */; }; 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; - 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* SearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* SearchViewModel.swift */; }; - 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; }; 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; }; 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */; }; @@ -180,9 +177,7 @@ C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */; }; C409CE9C284EA6EA00CABC12 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C409CE9B284EA6EA00CABC12 /* SwiftUICollection */; }; C409CE9E285044C800CABC12 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = C409CE9D285044C800CABC12 /* SwiftUICollection */; }; - C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; }; - C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; }; - C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; }; + C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; }; C4464953281616AE00DDB461 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C453497E279A2DA50045F1E2 /* LiveTVPlayerViewController.swift */; }; C4534981279A3F140045F1E2 /* tvOSLiveTVOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */; }; @@ -195,13 +190,10 @@ C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942CA27F6984100C54FE7 /* LiveTVPlayerViewController.swift */; }; C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45942CC27F6994A00C54FE7 /* LiveTVPlayerView.swift */; }; C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; }; - C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; + C45B29BB26FAC5B600CEF5E0 /* ColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtensions.swift */; }; C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; }; C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */; }; C4B9B91427E1921B0063535C /* LiveTVNativeVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B9B91327E1921B0063535C /* LiveTVNativeVideoPlayerView.swift */; }; - C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; }; - C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; }; - C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; }; C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; }; C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */; }; @@ -233,6 +225,10 @@ E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; }; + E111D8F528D03B7500400001 /* PagingLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111D8F428D03B7500400001 /* PagingLibraryViewModel.swift */; }; + E111D8F628D03B7500400001 /* PagingLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111D8F428D03B7500400001 /* PagingLibraryViewModel.swift */; }; + E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111D8F728D03BF900400001 /* PagingLibraryView.swift */; }; + E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111D8F928D0400900400001 /* PagingLibraryView.swift */; }; E113132B28BDB4B500930F75 /* NavBarDrawerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */; }; E113132F28BDB66A00930F75 /* NavBarDrawerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */; }; E113133228BDC72000930F75 /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133128BDC72000930F75 /* FilterView.swift */; }; @@ -267,6 +263,22 @@ E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E12B835F28C07D8500878399 /* LibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133728BEADBA00930F75 /* LibraryParent.swift */; }; + E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1AD28D0FAEA00678D5D /* NextUpLibraryViewModel.swift */; }; + E12CC1AF28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1AD28D0FAEA00678D5D /* NextUpLibraryViewModel.swift */; }; + E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1B028D1008F00678D5D /* NextUpView.swift */; }; + E12CC1B528D1124400678D5D /* BasicLibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1B428D1124400678D5D /* BasicLibraryCoordinator.swift */; }; + E12CC1B628D1124400678D5D /* BasicLibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1B428D1124400678D5D /* BasicLibraryCoordinator.swift */; }; + E12CC1B928D11A1D00678D5D /* BasicLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1B828D11A1D00678D5D /* BasicLibraryView.swift */; }; + E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1BA28D11E1000678D5D /* RecentlyAddedViewModel.swift */; }; + E12CC1BC28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1BA28D11E1000678D5D /* RecentlyAddedViewModel.swift */; }; + E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1BD28D11F4500678D5D /* RecentlyAddedView.swift */; }; + E12CC1BF28D1260600678D5D /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; }; + E12CC1C128D12B0A00678D5D /* BasicLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1C028D12B0A00678D5D /* BasicLibraryView.swift */; }; + E12CC1C528D12D9B00678D5D /* SeeAllPoster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1C428D12D9B00678D5D /* SeeAllPoster.swift */; }; + E12CC1C728D12FD600678D5D /* CinematicRecentlyAddedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1C628D12FD600678D5D /* CinematicRecentlyAddedView.swift */; }; + E12CC1C928D132B800678D5D /* RecentlyAddedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1C828D132B800678D5D /* RecentlyAddedView.swift */; }; + E12CC1CB28D1333400678D5D /* CinematicResumeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1CA28D1333400678D5D /* CinematicResumeItemView.swift */; }; + E12CC1CD28D135C700678D5D /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1CC28D135C700678D5D /* NextUpView.swift */; }; E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB1279E3C6200BC6161 /* Puppy */; }; E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB5279E3CA500BC6161 /* Puppy */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; @@ -306,7 +318,6 @@ E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryViewType.swift */; }; E13F05ED28BC9000003499D2 /* LibraryViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EB28BC9000003499D2 /* LibraryViewType.swift */; }; E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; }; - E13F05F228BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; }; E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05F028BC9016003499D2 /* LibraryView.swift */; }; E148128328C1443D003B8787 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E148128528C15472003B8787 /* APISortOrderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128428C15472003B8787 /* APISortOrderExtensions.swift */; }; @@ -326,7 +337,7 @@ E1734D7C28B9577700C66367 /* CollectionView in Frameworks */ = {isa = PBXBuildFile; productRef = E1734D7B28B9577700C66367 /* CollectionView */; }; E1734D7E28B9578100C66367 /* CollectionView in Frameworks */ = {isa = PBXBuildFile; productRef = E1734D7D28B9578100C66367 /* CollectionView */; }; E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; - E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; + E173DA5226D04AAF00CC4EB7 /* ColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtensions.swift */; }; E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; E176DE6D278E30D2001EFD8D /* EpisodeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */; }; E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; }; @@ -481,6 +492,15 @@ E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */; }; E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */; }; E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; }; + E1D3043228D175CE00587289 /* StaticLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043128D175CE00587289 /* StaticLibraryViewModel.swift */; }; + E1D3043328D175CE00587289 /* StaticLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043128D175CE00587289 /* StaticLibraryViewModel.swift */; }; + E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043428D1763100587289 /* SeeAllButton.swift */; }; + E1D3043A28D189C500587289 /* CastAndCrewLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043928D189C500587289 /* CastAndCrewLibraryView.swift */; }; + E1D3043C28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043B28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift */; }; + E1D3043D28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043B28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift */; }; + E1D3043F28D18F5700587289 /* CastAndCrewLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043E28D18F5700587289 /* CastAndCrewLibraryView.swift */; }; + E1D3044228D1976600587289 /* CastAndCrewItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3044128D1976600587289 /* CastAndCrewItemRow.swift */; }; + E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3044328D1991900587289 /* LibraryViewTypeToggle.swift */; }; E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */; }; E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */; }; E1D4BF7F2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */; }; @@ -500,8 +520,8 @@ E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* SelectorView.swift */; }; E1E1644128BB301900323B0A /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644028BB301900323B0A /* ArrayExtensions.swift */; }; E1E1644228BB301900323B0A /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644028BB301900323B0A /* ArrayExtensions.swift */; }; - E1E1644428BC60C600323B0A /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644328BC60C600323B0A /* LibraryItem.swift */; }; - E1E1644528BC60C600323B0A /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644328BC60C600323B0A /* LibraryItem.swift */; }; + E1E1644428BC60C600323B0A /* MediaLibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644328BC60C600323B0A /* MediaLibraryItem.swift */; }; + E1E1644528BC60C600323B0A /* MediaLibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644328BC60C600323B0A /* MediaLibraryItem.swift */; }; E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; }; E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */; }; E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */; }; @@ -617,7 +637,6 @@ 5362E4C8267D40F7000E2F71 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 53649AB0269CFB1900A2D8B7 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemPersonExtensions.swift; sourceTree = ""; }; - 536D3D80267BDFC60004248C /* PortraitItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemElement.swift; sourceTree = ""; }; 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Swiftfin iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; 5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -700,7 +719,6 @@ 62C29EA726D103D500C1D2E7 /* MediaCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCoordinator.swift; sourceTree = ""; }; 62C83B07288C6A630004ED0C /* FontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPicker.swift; sourceTree = ""; }; 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; - 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; 62E632DB267D2E130063E547 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemViewModel.swift; sourceTree = ""; }; @@ -716,9 +734,7 @@ AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemWideElement.swift; sourceTree = ""; }; - C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLibrariesCoordinator.swift; sourceTree = ""; }; - C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesViewModel.swift; sourceTree = ""; }; - C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieLibrariesView.swift; sourceTree = ""; }; + C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = ""; }; C453497E279A2DA50045F1E2 /* LiveTVPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVPlayerViewController.swift; sourceTree = ""; }; C4534980279A3F140045F1E2 /* tvOSLiveTVOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVOverlay.swift; sourceTree = ""; }; C4534982279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tvOSLiveTVVideoPlayerCoordinator.swift; sourceTree = ""; }; @@ -731,9 +747,6 @@ C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = ""; }; C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = ""; }; C4B9B91327E1921B0063535C /* LiveTVNativeVideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVNativeVideoPlayerView.swift; sourceTree = ""; }; - C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesCoordinator.swift; sourceTree = ""; }; - C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesViewModel.swift; sourceTree = ""; }; - C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVLibrariesView.swift; sourceTree = ""; }; C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsCoordinator.swift; sourceTree = ""; }; C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsView.swift; sourceTree = ""; }; C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVProgramsViewModel.swift; sourceTree = ""; }; @@ -753,6 +766,9 @@ E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = ""; }; E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = ""; }; E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = ""; }; + E111D8F428D03B7500400001 /* PagingLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingLibraryViewModel.swift; sourceTree = ""; }; + E111D8F728D03BF900400001 /* PagingLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingLibraryView.swift; sourceTree = ""; }; + E111D8F928D0400900400001 /* PagingLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingLibraryView.swift; sourceTree = ""; }; E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarDrawerView.swift; sourceTree = ""; }; E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarDrawerModifier.swift; sourceTree = ""; }; E113133128BDC72000930F75 /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = ""; }; @@ -775,6 +791,18 @@ E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = ""; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = ""; }; E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = ""; }; + E12CC1AD28D0FAEA00678D5D /* NextUpLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpLibraryViewModel.swift; sourceTree = ""; }; + E12CC1B028D1008F00678D5D /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; + E12CC1B428D1124400678D5D /* BasicLibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicLibraryCoordinator.swift; sourceTree = ""; }; + E12CC1B828D11A1D00678D5D /* BasicLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicLibraryView.swift; sourceTree = ""; }; + E12CC1BA28D11E1000678D5D /* RecentlyAddedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyAddedViewModel.swift; sourceTree = ""; }; + E12CC1BD28D11F4500678D5D /* RecentlyAddedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyAddedView.swift; sourceTree = ""; }; + E12CC1C028D12B0A00678D5D /* BasicLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicLibraryView.swift; sourceTree = ""; }; + E12CC1C428D12D9B00678D5D /* SeeAllPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeeAllPoster.swift; sourceTree = ""; }; + E12CC1C628D12FD600678D5D /* CinematicRecentlyAddedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicRecentlyAddedView.swift; sourceTree = ""; }; + E12CC1C828D132B800678D5D /* RecentlyAddedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyAddedView.swift; sourceTree = ""; }; + E12CC1CA28D1333400678D5D /* CinematicResumeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeItemView.swift; sourceTree = ""; }; + E12CC1CC28D135C700678D5D /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; E1399473289B1EA900401ABC /* Defaults+Workaround.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Defaults+Workaround.swift"; sourceTree = ""; }; E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; @@ -809,7 +837,7 @@ E168BD0F289A4162001A6922 /* HomeErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; E16AA60728A364A6009A983C /* PosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterButton.swift; sourceTree = ""; }; E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; - E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; + E173DA5126D04AAF00CC4EB7 /* ColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtensions.swift; sourceTree = ""; }; E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = ""; }; E176DE6C278E30D2001EFD8D /* EpisodeCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCard.swift; sourceTree = ""; }; E178859A2780F1F40094FBCF /* tvOSSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSSlider.swift; sourceTree = ""; }; @@ -921,6 +949,13 @@ E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterHStack.swift; sourceTree = ""; }; E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; + E1D3043128D175CE00587289 /* StaticLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLibraryViewModel.swift; sourceTree = ""; }; + E1D3043428D1763100587289 /* SeeAllButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeeAllButton.swift; sourceTree = ""; }; + E1D3043928D189C500587289 /* CastAndCrewLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewLibraryView.swift; sourceTree = ""; }; + E1D3043B28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewLibraryCoordinator.swift; sourceTree = ""; }; + E1D3043E28D18F5700587289 /* CastAndCrewLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewLibraryView.swift; sourceTree = ""; }; + E1D3044128D1976600587289 /* CastAndCrewItemRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewItemRow.swift; sourceTree = ""; }; + E1D3044328D1991900587289 /* LibraryViewTypeToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewTypeToggle.swift; sourceTree = ""; }; E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsViewModel.swift; sourceTree = ""; }; E1D4BF802719D22800A11E64 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = ""; }; @@ -932,7 +967,7 @@ E1E1643928BAC2EF00323B0A /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; E1E1643D28BB074000323B0A /* SelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorView.swift; sourceTree = ""; }; E1E1644028BB301900323B0A /* ArrayExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; - E1E1644328BC60C600323B0A /* LibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItem.swift; sourceTree = ""; }; + E1E1644328BC60C600323B0A /* MediaLibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLibraryItem.swift; sourceTree = ""; }; E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = ""; }; E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailsView.swift; sourceTree = ""; }; E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = ""; }; @@ -1071,19 +1106,21 @@ E10D87E127852FD000BD264C /* EpisodesRowManager.swift */, E113133928BEB71D00930F75 /* FilterViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */, + C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */, E107BB9127880A4000354E07 /* ItemViewModel */, - 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, 625CB5742678C33500530A6E /* MediaViewModel.swift */, - C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, + E12CC1AD28D0FAEA00678D5D /* NextUpLibraryViewModel.swift */, + E111D8F428D03B7500400001 /* PagingLibraryViewModel.swift */, 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */, + E12CC1BA28D11E1000678D5D /* RecentlyAddedViewModel.swift */, 62E632DB267D2E130063E547 /* SearchViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, - C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */, + E1D3043128D175CE00587289 /* StaticLibraryViewModel.swift */, E13DD3F82717E961009D4DAF /* UserListViewModel.swift */, E13DD3EB27178A54009D4DAF /* UserSignInViewModel.swift */, 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, @@ -1190,9 +1227,9 @@ E19169CD272514760085832A /* HTTPScheme.swift */, 535870AC2669D8DD00D05A09 /* ItemFilters.swift */, E1C925F328875037002A7A66 /* ItemViewType.swift */, - E1E1644328BC60C600323B0A /* LibraryItem.swift */, E113133728BEADBA00930F75 /* LibraryParent.swift */, E13F05EB28BC9000003499D2 /* LibraryViewType.swift */, + E1E1644328BC60C600323B0A /* MediaLibraryItem.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, @@ -1216,9 +1253,10 @@ E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */, 531690F6267ACC00005D8AB9 /* LandscapeItemElement.swift */, E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */, - 536D3D80267BDFC60004248C /* PortraitItemElement.swift */, + E111D8F928D0400900400001 /* PagingLibraryView.swift */, E1C92617288756BD002A7A66 /* PosterButton.swift */, E1C92619288756BD002A7A66 /* PosterHStack.swift */, + E12CC1C428D12D9B00678D5D /* SeeAllPoster.swift */, E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, E1CCC3D128C858A50020ED54 /* UserProfileButton.swift */, @@ -1453,11 +1491,15 @@ E18E01A7288746AF0022598C /* DotHStack.swift */, E1FE69AF28C2DA4A0021BC93 /* FilterDrawerHStack */, E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */, + E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */, + E1D3044328D1991900587289 /* LibraryViewTypeToggle.swift */, + E111D8F728D03BF900400001 /* PagingLibraryView.swift */, E18E01A5288746AF0022598C /* PillHStack.swift */, E16AA60728A364A6009A983C /* PosterButton.swift */, E1CCF13028AC07EC006CAC9E /* PosterHStack.swift */, E1AA331C2782541500F6439C /* PrimaryButton.swift */, E18E01A4288746AF0022598C /* RefreshableScrollView.swift */, + E1D3043428D1763100587289 /* SeeAllButton.swift */, ); path = Components; sourceTree = ""; @@ -1478,7 +1520,7 @@ E1A2C157279A7D76005EC829 /* BundleExtensions.swift */, E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, - E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */, + E173DA5126D04AAF00CC4EB7 /* ColorExtensions.swift */, E1399473289B1EA900401ABC /* Defaults+Workaround.swift */, E1E00A34278628A40022235B /* DoubleExtensions.swift */, E11CEB8C28999B4A003E74C7 /* FontExtensions.swift */, @@ -1508,6 +1550,8 @@ isa = PBXGroup; children = ( E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */, + E1D3043B28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift */, + E12CC1B428D1124400678D5D /* BasicLibraryCoordinator.swift */, 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */, 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */, 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, @@ -1520,13 +1564,11 @@ C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */, E193D5412719404B00900D82 /* MainCoordinator */, 62C29EA726D103D500C1D2E7 /* MediaCoordinator.swift */, - C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */, E18CE0B828A2322D0092E7F1 /* QuickConnectCoordinator.swift */, 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */, E13DD3E827177ED6009D4DAF /* ServerListCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, - C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */, E13DD4012717EE79009D4DAF /* UserListCoordinator.swift */, E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */, E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */, @@ -1661,22 +1703,21 @@ children = ( E1A2C15F279A7DCA005EC829 /* AboutAppView.swift */, E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, + E1D3043E28D18F5700587289 /* CastAndCrewLibraryView.swift */, + E12CC1C028D12B0A00678D5D /* BasicLibraryView.swift */, 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, E1A42E4D28CBD3B200A14DCB /* HomeView */, E193D54E271942C000900D82 /* ItemView */, - E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */, C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */, C4E508172703E8190045C9AB /* MediaView.swift */, - C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, E1E1643928BAC2EF00323B0A /* SearchView.swift */, E193D54F2719430400900D82 /* ServerDetailView.swift */, E193D54A271941D300900D82 /* ServerListView.swift */, E1E5D54D2783E66600692DFE /* SettingsView */, - C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */, E193D546271941C500900D82 /* UserListView.swift */, E193D548271941CC00900D82 /* UserSignInView.swift */, 5310694F2684E7EE00CFFDBA /* VideoPlayer */, @@ -1693,6 +1734,18 @@ path = VideoPlayerViewModel; sourceTree = ""; }; + E12CC1C328D12D6300678D5D /* Components */ = { + isa = PBXGroup; + children = ( + E12CC1C628D12FD600678D5D /* CinematicRecentlyAddedView.swift */, + E12CC1CA28D1333400678D5D /* CinematicResumeItemView.swift */, + E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, + E12CC1CC28D135C700678D5D /* NextUpView.swift */, + E12CC1C828D132B800678D5D /* RecentlyAddedView.swift */, + ); + path = Components; + sourceTree = ""; + }; E13DD3BB27163C3E009D4DAF /* App */ = { isa = PBXGroup; children = ( @@ -1717,13 +1770,15 @@ children = ( E18E01F3288747580022598C /* AboutAppView.swift */, E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */, + E12CC1B828D11A1D00678D5D /* BasicLibraryView.swift */, + E1D3044028D1974700587289 /* CastAndCrewLibraryView */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, + E113133128BDC72000930F75 /* FilterView.swift */, 62C83B07288C6A630004ED0C /* FontPicker.swift */, E168BD07289A4162001A6922 /* HomeView */, E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */, E14F7D0A26DB3714007C3AE6 /* ItemView */, - E113133128BDC72000930F75 /* FilterView.swift */, - E13F05EE28BC9016003499D2 /* LibraryView */, + E13F05F028BC9016003499D2 /* LibraryView.swift */, C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */, C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */, C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */, @@ -1742,15 +1797,6 @@ path = Views; sourceTree = ""; }; - E13F05EE28BC9016003499D2 /* LibraryView */ = { - isa = PBXGroup; - children = ( - E1C55AB228BD051700A9AD88 /* Components */, - E13F05F028BC9016003499D2 /* LibraryView.swift */, - ); - path = LibraryView; - sourceTree = ""; - }; E14F7D0A26DB3714007C3AE6 /* ItemView */ = { isa = PBXGroup; children = ( @@ -1787,6 +1833,8 @@ children = ( E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */, E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */, + E12CC1B028D1008F00678D5D /* NextUpView.swift */, + E12CC1BD28D11F4500678D5D /* RecentlyAddedView.swift */, ); path = Components; sourceTree = ""; @@ -2010,6 +2058,7 @@ E1A42E4D28CBD3B200A14DCB /* HomeView */ = { isa = PBXGroup; children = ( + E12CC1C328D12D6300678D5D /* Components */, E1A42E4B28CBD39300A14DCB /* HomeContentView.swift */, E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */, 531690E6267ABD79005D8AB9 /* HomeView.swift */, @@ -2056,14 +2105,6 @@ path = Views; sourceTree = ""; }; - E1C55AB228BD051700A9AD88 /* Components */ = { - isa = PBXGroup; - children = ( - E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */, - ); - path = Components; - sourceTree = ""; - }; E1C812CF277AE4C700918266 /* VideoPlayerCoordinator */ = { isa = PBXGroup; children = ( @@ -2124,6 +2165,15 @@ path = Components; sourceTree = ""; }; + E1D3044028D1974700587289 /* CastAndCrewLibraryView */ = { + isa = PBXGroup; + children = ( + E1D3044128D1976600587289 /* CastAndCrewItemRow.swift */, + E1D3043928D189C500587289 /* CastAndCrewLibraryView.swift */, + ); + path = CastAndCrewLibraryView; + sourceTree = ""; + }; E1DD1127271E7D15005BE12F /* Objects */ = { isa = PBXGroup; children = ( @@ -2441,7 +2491,6 @@ E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, C4534985279A40C60045F1E2 /* LiveTVVideoPlayerView.swift in Sources */, E1A2C15A279A7D76005EC829 /* BundleExtensions.swift in Sources */, - C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */, @@ -2453,8 +2502,8 @@ 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, - C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, - 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, + C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */, + E1D3043F28D18F5700587289 /* CastAndCrewLibraryView.swift in Sources */, E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, E1C926162887565C002A7A66 /* SeriesItemView.swift in Sources */, @@ -2463,6 +2512,7 @@ E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */, E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */, E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */, + E12CC1C128D12B0A00678D5D /* BasicLibraryView.swift in Sources */, E1E9EFEB28C7EA2C00CC1F8B /* UserDtoExtensions.swift in Sources */, 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */, @@ -2473,11 +2523,14 @@ E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, + E12CC1C528D12D9B00678D5D /* SeeAllPoster.swift in Sources */, E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E12B835F28C07D8500878399 /* LibraryParent.swift in Sources */, + E111D8FA28D0400900400001 /* PagingLibraryView.swift in Sources */, E18E021A2887492B0022598C /* AppIcon.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */, + E1D3043328D175CE00587289 /* StaticLibraryViewModel.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, @@ -2485,6 +2538,7 @@ E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */, C453497F279A2DA50045F1E2 /* LiveTVPlayerViewController.swift in Sources */, C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */, + E12CC1BC28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */, E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */, E13DD3E227176BD3009D4DAF /* ServerListViewModel.swift in Sources */, E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */, @@ -2501,16 +2555,17 @@ 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, E193D54B271941D300900D82 /* ServerListView.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, + E111D8F628D03B7500400001 /* PagingLibraryViewModel.swift in Sources */, C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */, 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */, - C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */, E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */, E18E023C288749540022598C /* UIScrollViewExtensions.swift in Sources */, E1A42E4C28CBD39300A14DCB /* HomeContentView.swift in Sources */, C4534983279A40990045F1E2 /* tvOSLiveTVVideoPlayerCoordinator.swift in Sources */, + E12CC1CB28D1333400678D5D /* CinematicResumeItemView.swift in Sources */, E1C9260F2887565C002A7A66 /* AttributeHStack.swift in Sources */, E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */, E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */, @@ -2528,6 +2583,7 @@ E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */, E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, E18E02202887492B0022598C /* AttributeFillView.swift in Sources */, + E12CC1C928D132B800678D5D /* RecentlyAddedView.swift in Sources */, E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */, E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, @@ -2539,15 +2595,14 @@ E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */, 62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */, - 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */, - E13F05F228BC9016003499D2 /* LibraryItemRow.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, E1CCF12F28ABF989006CAC9E /* PosterType.swift in Sources */, E18E021F2887492B0022598C /* InitialFailureView.swift in Sources */, E1D4BF882719D27100A11E64 /* Bitrates.swift in Sources */, + E1D3043D28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift in Sources */, E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */, E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, @@ -2561,21 +2616,24 @@ E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */, E19169CF272514760085832A /* HTTPScheme.swift in Sources */, E148128628C15475003B8787 /* APISortOrderExtensions.swift in Sources */, - E1E1644528BC60C600323B0A /* LibraryItem.swift in Sources */, + E1E1644528BC60C600323B0A /* MediaLibraryItem.swift in Sources */, E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */, E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */, - C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, + C45B29BB26FAC5B600CEF5E0 /* ColorExtensions.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, E1C926132887565C002A7A66 /* SeriesEpisodesView.swift in Sources */, - C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, + E12CC1CD28D135C700678D5D /* NextUpView.swift in Sources */, E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */, E18E02232887492B0022598C /* ImageView.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, + E12CC1AF28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */, E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */, + E12CC1B628D1124400678D5D /* BasicLibraryCoordinator.swift in Sources */, E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */, E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */, E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */, + E12CC1C728D12FD600678D5D /* CinematicRecentlyAddedView.swift in Sources */, E1E1644228BB301900323B0A /* ArrayExtensions.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */, @@ -2614,10 +2672,8 @@ 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E1937A62288F32DB00CB80AA /* Poster.swift in Sources */, E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */, - C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */, E1A2C160279A7DCA005EC829 /* AboutAppView.swift in Sources */, - C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, E1FE69A828C29B720021BC93 /* ProgressBar.swift in Sources */, E1A16C9D2889AF1E00EA4679 /* AboutView.swift in Sources */, E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */, @@ -2656,6 +2712,7 @@ E148128828C154BF003B8787 /* ItemFilterExtensions.swift in Sources */, C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, + E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */, E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */, E168BD11289A4162001A6922 /* HomeContentView.swift in Sources */, E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */, @@ -2682,8 +2739,11 @@ 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, + E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, + E1D3043228D175CE00587289 /* StaticLibraryViewModel.swift in Sources */, + E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */, E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */, E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, @@ -2691,6 +2751,7 @@ E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */, C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */, E17FB55228C119D400311DFE /* Displayable.swift in Sources */, E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, @@ -2704,7 +2765,6 @@ 62133890265F83A900A81A2A /* MediaView.swift in Sources */, 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */, E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */, - 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */, E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, E18E0204288749200022598C /* Divider.swift in Sources */, @@ -2715,7 +2775,8 @@ E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */, E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */, E18E01EF288747230022598C /* ListDetailsView.swift in Sources */, - E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, + E173DA5226D04AAF00CC4EB7 /* ColorExtensions.swift in Sources */, + E12CC1B928D11A1D00678D5D /* BasicLibraryView.swift in Sources */, E1399474289B1EA900401ABC /* Defaults+Workaround.swift in Sources */, C45942D027F69C2400C54FE7 /* LiveTVChannelsCoordinator.swift in Sources */, E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */, @@ -2739,20 +2800,24 @@ E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */, E18E0205288749200022598C /* AppIcon.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */, + E1D3043C28D18CD400587289 /* CastAndCrewLibraryCoordinator.swift in Sources */, E18E01AB288746AF0022598C /* PillHStack.swift in Sources */, E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */, E18E0207288749200022598C /* AttributeOutlineView.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, + E1D3043A28D189C500587289 /* CastAndCrewLibraryView.swift in Sources */, E18E01AD288746AF0022598C /* DotHStack.swift in Sources */, E1937A61288F32DB00CB80AA /* Poster.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, + E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */, E148128B28C15526003B8787 /* SortBy.swift in Sources */, 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, E18CE0B928A2322D0092E7F1 /* QuickConnectCoordinator.swift in Sources */, + E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */, 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */, E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, @@ -2793,7 +2858,7 @@ E18E01EE288747230022598C /* AboutView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */, - E1E1644428BC60C600323B0A /* LibraryItem.swift in Sources */, + E1E1644428BC60C600323B0A /* MediaLibraryItem.swift in Sources */, E18E0206288749200022598C /* AttributeFillView.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, @@ -2821,6 +2886,7 @@ E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */, C45640D0281A43EF007096DE /* LiveTVNativePlayerViewController.swift in Sources */, E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, + E1D3044228D1976600587289 /* CastAndCrewItemRow.swift in Sources */, E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* ItemFilters.swift in Sources */, @@ -2832,17 +2898,21 @@ E18E01E6288747230022598C /* CollectionItemView.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, + E111D8F528D03B7500400001 /* PagingLibraryViewModel.swift in Sources */, E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */, E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */, E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */, + E12CC1BF28D1260600678D5D /* ItemTypeLibraryViewModel.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, E18E01EA288747230022598C /* MovieItemView.swift in Sources */, + E12CC1B528D1124400678D5D /* BasicLibraryCoordinator.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E148128528C15472003B8787 /* APISortOrderExtensions.swift in Sources */, + E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */, E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */, diff --git a/Swiftfin/Views/LibraryView/Components/LibraryItemRow.swift b/Swiftfin/Components/LibraryItemRow.swift similarity index 89% rename from Swiftfin/Views/LibraryView/Components/LibraryItemRow.swift rename to Swiftfin/Components/LibraryItemRow.swift index 43979873..3102a95d 100644 --- a/Swiftfin/Views/LibraryView/Components/LibraryItemRow.swift +++ b/Swiftfin/Components/LibraryItemRow.swift @@ -34,7 +34,9 @@ struct LibraryItemRow: View { .fixedSize(horizontal: false, vertical: true) DotHStack { - if let premiereYear = item.premiereDateYear { + if item.type == .episode, let seasonEpisodeLocator = item.seasonEpisodeLocator { + Text(seasonEpisodeLocator) + } else if let premiereYear = item.premiereDateYear { Text(premiereYear) } diff --git a/Swiftfin/Components/LibraryViewTypeToggle.swift b/Swiftfin/Components/LibraryViewTypeToggle.swift new file mode 100644 index 00000000..b05f1f9d --- /dev/null +++ b/Swiftfin/Components/LibraryViewTypeToggle.swift @@ -0,0 +1,34 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct LibraryViewTypeToggle: View { + + @Binding + var libraryViewType: LibraryViewType + + var body: some View { + Button { + switch libraryViewType { + case .grid: + libraryViewType = .list + case .list: + libraryViewType = .grid + } + } label: { + switch libraryViewType { + case .grid: + Image(systemName: "list.dash") + case .list: + Image(systemName: "square.grid.2x2") + } + } + } +} diff --git a/Swiftfin/Components/PagingLibraryView.swift b/Swiftfin/Components/PagingLibraryView.swift new file mode 100644 index 00000000..98782cf1 --- /dev/null +++ b/Swiftfin/Components/PagingLibraryView.swift @@ -0,0 +1,112 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct PagingLibraryView: View { + + @ObservedObject + var viewModel: PagingLibraryViewModel + private var onSelect: (BaseItemDto) -> Void + + @Default(.Customization.Library.gridPosterType) + private var libraryGridPosterType + @Default(.Customization.Library.viewType) + private var libraryViewType + + private var gridLayout: NSCollectionLayoutSection.GridLayoutMode { + if libraryGridPosterType == .landscape && UIDevice.isPhone { + return .fixedNumberOfColumns(2) + } else { + return .adaptive(withMinItemSize: libraryGridPosterType.width + (UIDevice.isIPad ? 10 : 0)) + } + } + + @ViewBuilder + private var libraryListView: some View { + CollectionView(items: viewModel.items) { _, item, _ in + LibraryItemRow(item: item) + .onSelect { + onSelect(item) + } + .padding() + } + .layout { _, layoutEnvironment in + .list(using: .init(appearance: .plain), layoutEnvironment: layoutEnvironment) + } + .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in + if !viewModel.isLoading && edge == .bottom { + viewModel.requestNextPage() + } + } + .onEdgeReached { edge in + if viewModel.hasNextPage, !viewModel.isLoading, edge == .bottom { + viewModel.requestNextPage() + } + } + .configure { configuration in + configuration.showsVerticalScrollIndicator = false + } + } + + @ViewBuilder + private var libraryGridView: some View { + CollectionView(items: viewModel.items) { _, item, _ in + PosterButton(item: item, type: libraryGridPosterType) + .scaleItem(libraryGridPosterType == .landscape && UIDevice.isPhone ? 0.85 : 1) + .onSelect { + onSelect(item) + } + } + .layout { _, layoutEnvironment in + .grid( + layoutEnvironment: layoutEnvironment, + layoutMode: gridLayout, + sectionInsets: .init(top: 0, leading: 10, bottom: 0, trailing: 10) + ) + } + .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in + if !viewModel.isLoading && edge == .bottom { + viewModel.requestNextPage() + } + } + .onEdgeReached { edge in + if viewModel.hasNextPage, !viewModel.isLoading, edge == .bottom { + viewModel.requestNextPage() + } + } + .configure { configuration in + configuration.showsVerticalScrollIndicator = false + } + } + + var body: some View { + switch libraryViewType { + case .grid: + libraryGridView + case .list: + libraryListView + } + } +} + +extension PagingLibraryView { + init(viewModel: PagingLibraryViewModel) { + self.viewModel = viewModel + self.onSelect = { _ in } + } + + func onSelect(_ action: @escaping (BaseItemDto) -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin/Components/PosterHStack.swift b/Swiftfin/Components/PosterHStack.swift index 1319dcb2..db7a6851 100644 --- a/Swiftfin/Components/PosterHStack.swift +++ b/Swiftfin/Components/PosterHStack.swift @@ -65,6 +65,7 @@ struct PosterHStack Void + + var body: some View { + Button { + onSelect() + } label: { + HStack { + L10n.seeAll.text + Image(systemName: "chevron.right") + } + .font(.subheadline.bold()) + } + } +} + +extension SeeAllButton { + init() { + self.onSelect = {} + } + + func onSelect(_ action: @escaping () -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin/Views/BasicLibraryView.swift b/Swiftfin/Views/BasicLibraryView.swift new file mode 100644 index 00000000..5087d6d1 --- /dev/null +++ b/Swiftfin/Views/BasicLibraryView.swift @@ -0,0 +1,65 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct BasicLibraryView: View { + + @Default(.Customization.Library.viewType) + private var libraryViewType + + @EnvironmentObject + private var router: BasicLibraryCoordinator.Router + @ObservedObject + var viewModel: PagingLibraryViewModel + + @ViewBuilder + private var loadingView: some View { + ProgressView() + } + + @ViewBuilder + private var noResultsView: some View { + L10n.noResults.text + } + + @ViewBuilder + private var libraryItemsView: some View { + PagingLibraryView(viewModel: viewModel) + .onSelect { item in + router.route(to: \.item, item) + } + .ignoresSafeArea() + } + + var body: some View { + Group { + if viewModel.isLoading && viewModel.items.isEmpty { + loadingView + } else if viewModel.items.isEmpty { + noResultsView + } else { + libraryItemsView + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + + if viewModel.isLoading && !viewModel.items.isEmpty { + ProgressView() + } + + LibraryViewTypeToggle(libraryViewType: $libraryViewType) + } + } + } +} diff --git a/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift new file mode 100644 index 00000000..26d4ec43 --- /dev/null +++ b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewItemRow.swift @@ -0,0 +1,64 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension CastAndCrewLibraryView { + + struct CastAndCrewItemRow: View { + + @EnvironmentObject + private var router: CastAndCrewLibraryCoordinator.Router + + let person: BaseItemPerson + private var onSelect: () -> Void + + var body: some View { + Button { + onSelect() + } label: { + HStack(alignment: .bottom) { + ImageView(person.portraitPosterImageSource(maxWidth: 60)) + .posterStyle(type: .portrait, width: 60) + + VStack(alignment: .leading) { + Text(person.displayName) + .foregroundColor(.primary) + .fontWeight(.semibold) + .lineLimit(2) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + + if let subtitle = person.subtitle { + Text(subtitle) + .font(.caption) + .foregroundColor(Color(UIColor.lightGray)) + } + } + .padding(.vertical) + + Spacer() + } + } + } + } +} + +extension CastAndCrewLibraryView.CastAndCrewItemRow { + init(person: BaseItemPerson) { + self.person = person + self.onSelect = {} + } + + func onSelect(_ action: @escaping () -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift new file mode 100644 index 00000000..5a83a56e --- /dev/null +++ b/Swiftfin/Views/CastAndCrewLibraryView/CastAndCrewLibraryView.swift @@ -0,0 +1,86 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct CastAndCrewLibraryView: View { + + @Default(.Customization.Library.viewType) + private var libraryViewType + + @EnvironmentObject + private var router: CastAndCrewLibraryCoordinator.Router + let people: [BaseItemPerson] + + @ViewBuilder + private var noResultsView: some View { + L10n.noResults.text + } + + @ViewBuilder + private var libraryListView: some View { + CollectionView(items: people) { _, person, _ in + CastAndCrewItemRow(person: person) + .onSelect { + router.route(to: \.library, .init(parent: person, type: .person, filters: .init())) + } + .padding() + } + .layout { _, layoutEnvironment in + .list(using: .init(appearance: .plain), layoutEnvironment: layoutEnvironment) + } + .configure { configuration in + configuration.showsVerticalScrollIndicator = false + } + } + + @ViewBuilder + private var libraryGridView: some View { + CollectionView(items: people) { _, person, _ in + PosterButton(item: person, type: .portrait) + .onSelect { + router.route(to: \.library, .init(parent: person, type: .person, filters: .init())) + } + } + .layout { _, layoutEnvironment in + .grid( + layoutEnvironment: layoutEnvironment, + layoutMode: .adaptive(withMinItemSize: PosterType.portrait.width + (UIDevice.isIPad ? 10 : 0)), + sectionInsets: .init(top: 0, leading: 10, bottom: 0, trailing: 10) + ) + } + .configure { configuration in + configuration.showsVerticalScrollIndicator = false + } + } + + var body: some View { + Group { + if people.isEmpty { + noResultsView + } else { + switch libraryViewType { + case .grid: + libraryGridView + case .list: + libraryListView + } + } + } + .navigationTitle(L10n.castAndCrew) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + LibraryViewTypeToggle(libraryViewType: $libraryViewType) + } + } + } +} diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift index 06ff5728..82114b71 100644 --- a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift @@ -9,31 +9,34 @@ import JellyfinAPI import SwiftUI -struct ContinueWatchingView: View { +extension HomeView { - @EnvironmentObject - private var homeRouter: HomeCoordinator.Router - @ObservedObject - var viewModel: HomeViewModel + struct ContinueWatchingView: View { - var body: some View { - PosterHStack(title: "", type: .landscape, items: viewModel.resumeItems) - .scaleItems(1.5) - .onSelect { item in - homeRouter.route(to: \.item, item) - } - .contextMenu { item in - Button(role: .destructive) { - viewModel.removeItemFromResume(item) - } label: { - Label(L10n.removeFromResume, systemImage: "minus.circle") + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: HomeViewModel + + var body: some View { + PosterHStack(title: "", type: .landscape, items: viewModel.resumeItems) + .scaleItems(1.5) + .onSelect { item in + router.route(to: \.item, item) } - } - .imageOverlay { item in - LandscapePosterProgressBar( - title: item.progress ?? L10n.continue, - progress: (item.userData?.playedPercentage ?? 0) / 100 - ) - } + .contextMenu { item in + Button(role: .destructive) { + viewModel.removeItemFromResume(item) + } label: { + Label(L10n.removeFromResume, systemImage: "minus.circle") + } + } + .imageOverlay { item in + LandscapePosterProgressBar( + title: item.progress ?? L10n.continue, + progress: (item.userData?.playedPercentage ?? 0) / 100 + ) + } + } } } diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index fe221cbb..fbf64307 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -7,34 +7,35 @@ // import Defaults -import JellyfinAPI import SwiftUI -struct LatestInLibraryView: View { +extension HomeView { - @EnvironmentObject - private var homeRouter: HomeCoordinator.Router - @ObservedObject - var viewModel: LatestMediaViewModel + struct LatestInLibraryView: View { - @Default(.Customization.latestInLibraryPosterType) - var latestInLibraryPosterType + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: LibraryViewModel - var body: some View { - PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: latestInLibraryPosterType, items: viewModel.items) + @Default(.Customization.latestInLibraryPosterType) + var latestInLibraryPosterType + + var body: some View { + PosterHStack( + title: L10n.latestWithString(viewModel.parent?.displayName ?? .emptyDash), + type: latestInLibraryPosterType, + items: viewModel.items.prefix(20).asArray + ) .trailing { - Button { - homeRouter.route(to: \.library, .init(parent: viewModel.library, type: .library, filters: .recent)) - } label: { - HStack { - L10n.seeAll.text - Image(systemName: "chevron.right") + SeeAllButton() + .onSelect { + router.route(to: \.library, viewModel.libraryCoordinatorParameters) } - .font(.subheadline.bold()) - } } .onSelect { item in - homeRouter.route(to: \.item, item) + router.route(to: \.item, item) } + } } } diff --git a/Swiftfin/Views/HomeView/Components/NextUpView.swift b/Swiftfin/Views/HomeView/Components/NextUpView.swift new file mode 100644 index 00000000..ffcb42f9 --- /dev/null +++ b/Swiftfin/Views/HomeView/Components/NextUpView.swift @@ -0,0 +1,41 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension HomeView { + + struct NextUpView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: NextUpLibraryViewModel + + @Default(.Customization.nextUpPosterType) + private var nextUpPosterType + + var body: some View { + PosterHStack( + title: L10n.nextUp, + type: nextUpPosterType, + items: viewModel.items.prefix(20).asArray + ) + .trailing { + SeeAllButton() + .onSelect { + router.route(to: \.basicLibrary, .init(title: L10n.nextUp, viewModel: viewModel)) + } + } + .onSelect { item in + router.route(to: \.item, item) + } + } + } +} diff --git a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift new file mode 100644 index 00000000..6851eac3 --- /dev/null +++ b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift @@ -0,0 +1,41 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension HomeView { + + struct RecentlyAddedView: View { + + @EnvironmentObject + private var router: HomeCoordinator.Router + @ObservedObject + var viewModel: ItemTypeLibraryViewModel + + @Default(.Customization.recentlyAddedPosterType) + private var recentlyAddedPosterType + + var body: some View { + PosterHStack( + title: L10n.recentlyAdded, + type: recentlyAddedPosterType, + items: viewModel.items.prefix(20).asArray + ) + .trailing { + SeeAllButton() + .onSelect { + router.route(to: \.basicLibrary, .init(title: L10n.recentlyAdded, viewModel: viewModel)) + } + } + .onSelect { item in + router.route(to: \.item, item) + } + } + } +} diff --git a/Swiftfin/Views/HomeView/HomeContentView.swift b/Swiftfin/Views/HomeView/HomeContentView.swift index 58c0d3f4..4f7deb8f 100644 --- a/Swiftfin/Views/HomeView/HomeContentView.swift +++ b/Swiftfin/Views/HomeView/HomeContentView.swift @@ -8,6 +8,7 @@ import CollectionView import Defaults +import JellyfinAPI import SwiftUI extension HomeView { @@ -31,22 +32,21 @@ extension HomeView { ContinueWatchingView(viewModel: viewModel) } - if !viewModel.nextUpItems.isEmpty { - PosterHStack(title: L10n.nextUp, type: nextUpPosterType, items: viewModel.nextUpItems) - .onSelect { item in - homeRouter.route(to: \.item, item) - } + if viewModel.hasNextUp { + NextUpView(viewModel: .init()) } - if !viewModel.latestAddedItems.isEmpty { - PosterHStack(title: L10n.recentlyAdded, type: recentlyAddedPosterType, items: viewModel.latestAddedItems) - .onSelect { item in - homeRouter.route(to: \.item, item) - } + if viewModel.hasRecentlyAdded { + RecentlyAddedView( + viewModel: .init( + itemTypes: [.movie, .series], + filters: .init(sortOrder: [APISortOrder.descending.filter], sortBy: [SortBy.dateAdded.filter]) + ) + ) } ForEach(viewModel.libraries, id: \.self) { library in - LatestInLibraryView(viewModel: .init(library: library)) + LatestInLibraryView(viewModel: .init(parent: library, type: .library, filters: .recent)) } } .padding(.bottom, 50) diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index 42c6ea94..6fc31879 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -12,7 +12,7 @@ import SwiftUI struct HomeView: View { @EnvironmentObject - private var homeRouter: HomeCoordinator.Router + private var router: HomeCoordinator.Router @ObservedObject var viewModel: HomeViewModel @@ -30,7 +30,7 @@ struct HomeView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - homeRouter.route(to: \.settings) + router.route(to: \.settings) } label: { Image(systemName: "gearshape.fill") .accessibilityLabel(L10n.settings) diff --git a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift index 91716923..6f0e096f 100644 --- a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift +++ b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift @@ -36,7 +36,6 @@ extension ItemView { ) } else { Image(systemName: "checkmark.circle") -// .foregroundStyle(.white) } } .buttonStyle(.plain) @@ -54,7 +53,6 @@ extension ItemView { .foregroundStyle(Color.red) } else { Image(systemName: "heart") -// .foregroundStyle(.white) } } .buttonStyle(.plain) diff --git a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift index ad105c33..8db6193c 100644 --- a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift @@ -10,6 +10,7 @@ import JellyfinAPI import SwiftUI extension ItemView { + struct CastAndCrewHStack: View { @EnvironmentObject @@ -20,8 +21,14 @@ extension ItemView { PosterHStack( title: L10n.castAndCrew, type: .portrait, - items: people + items: people.filter(\.isDisplayed).prefix(20).asArray ) + .trailing { + SeeAllButton() + .onSelect { + router.route(to: \.castAndCrew, people) + } + } .onSelect { person in router.route(to: \.library, .init(parent: person, type: .person, filters: .init())) } diff --git a/Swiftfin/Views/ItemView/Components/GenresHStack.swift b/Swiftfin/Views/ItemView/Components/GenresHStack.swift index 303193a4..d4f8cd98 100644 --- a/Swiftfin/Views/ItemView/Components/GenresHStack.swift +++ b/Swiftfin/Views/ItemView/Components/GenresHStack.swift @@ -10,6 +10,7 @@ import JellyfinAPI import SwiftUI extension ItemView { + struct GenresHStack: View { @EnvironmentObject diff --git a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift index 0ec9b27e..09ca1e8a 100644 --- a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift @@ -11,6 +11,7 @@ import JellyfinAPI import SwiftUI extension ItemView { + struct SimilarItemsHStack: View { @Default(.Customization.similarPosterType) @@ -26,6 +27,13 @@ extension ItemView { type: similarPosterType, items: items ) + .trailing { + SeeAllButton() + .onSelect { + let viewModel = StaticLibraryViewModel(items: items) + router.route(to: \.basicLibrary, .init(title: L10n.recommended, viewModel: viewModel)) + } + } .onSelect { item in router.route(to: \.item, item) } diff --git a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift index 3fe318db..0a7ec1ef 100644 --- a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift +++ b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift @@ -10,6 +10,7 @@ import JellyfinAPI import SwiftUI extension ItemView { + struct StudiosHStack: View { @EnvironmentObject diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 8403f39a..4156e66e 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -43,7 +43,8 @@ struct ItemView: View { CollectionItemView(viewModel: .init(item: item)) } case .person: - LibraryView(viewModel: .init(parent: item, type: .person)) + LibraryView(viewModel: LibraryViewModel(parent: item, type: .person)) +// LibraryView(viewModel: .init(parent: item, type: .person)) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift index 9a7e0b94..f251f529 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift @@ -60,7 +60,7 @@ extension EpisodeItemView { // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift index f34ae937..8981d638 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift @@ -40,7 +40,7 @@ extension MovieItemView { // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift index ab7db080..3bff2d30 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift @@ -44,7 +44,7 @@ extension SeriesItemView { // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift index 50dcc2e6..fc5ff156 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift @@ -39,7 +39,7 @@ extension iPadOSEpisodeItemView { // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift index 947cba2c..5ef41fe8 100644 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift @@ -39,7 +39,7 @@ extension iPadOSMovieItemView { // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift index 1d478043..47dc7c9e 100644 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift @@ -43,7 +43,7 @@ extension iPadOSSeriesItemView { // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + if let castAndCrew = viewModel.item.people, !castAndCrew.isEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) diff --git a/Swiftfin/Views/LibraryView.swift b/Swiftfin/Views/LibraryView.swift new file mode 100644 index 00000000..2ea842e9 --- /dev/null +++ b/Swiftfin/Views/LibraryView.swift @@ -0,0 +1,90 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct LibraryView: View { + + @EnvironmentObject + private var router: LibraryCoordinator.Router + @ObservedObject + var viewModel: LibraryViewModel + + @Default(.Customization.Library.viewType) + private var libraryViewType + + @ViewBuilder + private var loadingView: some View { + ProgressView() + } + + @ViewBuilder + private var noResultsView: some View { + L10n.noResults.text + } + + private func baseItemOnSelect(_ item: BaseItemDto) { + if let baseParent = viewModel.parent as? BaseItemDto { + if baseParent.collectionType == "folders" { + router.route(to: \.library, .init(parent: item, type: .folders, filters: .init())) + } else if item.type == .folder { + router.route(to: \.library, .init(parent: item, type: .library, filters: .init())) + } else { + router.route(to: \.item, item) + } + } else { + router.route(to: \.item, item) + } + } + + @ViewBuilder + private var libraryItemsView: some View { + PagingLibraryView(viewModel: viewModel) + .onSelect { item in + baseItemOnSelect(item) + } + .ignoresSafeArea() + } + + var body: some View { + Group { + if viewModel.isLoading && viewModel.items.isEmpty { + loadingView + } else if viewModel.items.isEmpty { + noResultsView + } else { + libraryItemsView + } + } + .navigationTitle(viewModel.parent?.displayName ?? "") + .navigationBarTitleDisplayMode(.inline) + .navBarDrawer { + ScrollView(.horizontal, showsIndicators: false) { + FilterDrawerHStack(viewModel: viewModel.filterViewModel) + .onSelect { filterCoordinatorParameters in + router.route(to: \.filter, filterCoordinatorParameters) + } + .padding(.horizontal) + .padding(.vertical, 1) + } + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + + if viewModel.isLoading && !viewModel.items.isEmpty { + ProgressView() + } + + LibraryViewTypeToggle(libraryViewType: $libraryViewType) + } + } + } +} diff --git a/Swiftfin/Views/LibraryView/LibraryView.swift b/Swiftfin/Views/LibraryView/LibraryView.swift deleted file mode 100644 index feb6fe4a..00000000 --- a/Swiftfin/Views/LibraryView/LibraryView.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// 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 (c) 2022 Jellyfin & Jellyfin Contributors -// - -import CollectionView -import Defaults -import JellyfinAPI -import SwiftUI - -struct LibraryView: View { - - @EnvironmentObject - private var router: LibraryCoordinator.Router - @ObservedObject - var viewModel: LibraryViewModel - - @Default(.Customization.Library.gridPosterType) - private var libraryGridPosterType - @Default(.Customization.Library.viewType) - private var libraryViewType - - @ViewBuilder - private var loadingView: some View { - ProgressView() - } - - @ViewBuilder - private var noResultsView: some View { - L10n.noResults.text - } - - private var gridLayout: NSCollectionLayoutSection.GridLayoutMode { - if libraryGridPosterType == .landscape && UIDevice.isPhone { - return .fixedNumberOfColumns(2) - } else { - return .adaptive(withMinItemSize: libraryGridPosterType.width + (UIDevice.isIPad ? 10 : 0)) - } - } - - private func baseItemOnSelect(_ item: BaseItemDto) { - if let baseParent = viewModel.parent as? BaseItemDto { - if baseParent.collectionType == "folders" { - router.route(to: \.library, .init(parent: item, type: .folders, filters: .init())) - } else if item.type == .folder { - router.route(to: \.library, .init(parent: item, type: .library, filters: .init())) - } else { - router.route(to: \.item, item) - } - } else { - router.route(to: \.item, item) - } - } - - @ViewBuilder - private var libraryListView: some View { - CollectionView(items: viewModel.items) { _, item, _ in - LibraryItemRow(item: item) - .onSelect { - baseItemOnSelect(item) - } - .padding() - } - .layout { _, layoutEnvironment in - .list(using: .init(appearance: .plain), layoutEnvironment: layoutEnvironment) - } - .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in - if !viewModel.isLoading && edge == .bottom { - viewModel.requestNextPage() - } - } - .configure { configuration in - configuration.showsVerticalScrollIndicator = false - } - .ignoresSafeArea() - } - - @ViewBuilder - private var libraryGridView: some View { - CollectionView(items: viewModel.items) { _, item, _ in - PosterButton(item: item, type: libraryGridPosterType) - .scaleItem(libraryGridPosterType == .landscape && UIDevice.isPhone ? 0.85 : 1) - .onSelect { - baseItemOnSelect(item) - } - } - .layout { _, layoutEnvironment in - .grid( - layoutEnvironment: layoutEnvironment, - layoutMode: gridLayout, - sectionInsets: .init(top: 0, leading: 10, bottom: 0, trailing: 10) - ) - } - .willReachEdge(insets: .init(top: 0, leading: 0, bottom: 200, trailing: 0)) { edge in - if !viewModel.isLoading && edge == .bottom { - viewModel.requestNextPage() - } - } - .configure { configuration in - configuration.showsVerticalScrollIndicator = false - } - .ignoresSafeArea() - } - - var body: some View { - Group { - if viewModel.isLoading && viewModel.items.isEmpty { - loadingView - } else if viewModel.items.isEmpty { - noResultsView - } else { - switch libraryViewType { - case .grid: - libraryGridView - case .list: - libraryListView - } - } - } - .navigationTitle(viewModel.parent?.displayName ?? "") - .navigationBarTitleDisplayMode(.inline) - .navBarDrawer { - ScrollView(.horizontal, showsIndicators: false) { - FilterDrawerHStack(viewModel: viewModel.filterViewModel) - .onSelect { filterCoordinatorParameters in - router.route(to: \.filter, filterCoordinatorParameters) - } - .padding(.horizontal) - .padding(.vertical, 1) - } - } - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - - if viewModel.isLoading && !viewModel.items.isEmpty { - ProgressView() - } - - Button { - switch libraryViewType { - case .grid: - libraryViewType = .list - case .list: - libraryViewType = .grid - } - } label: { - switch libraryViewType { - case .grid: - Image(systemName: "list.dash") - case .list: - Image(systemName: "square.grid.2x2") - } - } - } - } - } -}