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