undo filter change attempt

This commit is contained in:
Ethan Pippin 2022-01-13 22:49:25 -07:00
parent 98b2b5071f
commit 0c95fb48a7
17 changed files with 319 additions and 306 deletions

View File

@ -7,11 +7,10 @@
// //
import Foundation import Foundation
import JellyfinAPI
import Stinsen import Stinsen
import SwiftUI import SwiftUI
typealias FilterCoordinatorParams = (libraryItem: BaseItemDto, filters: Binding<LibraryFilters>, enabledFilterType: [FilterType]) typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
final class FilterCoordinator: NavigationCoordinatable { final class FilterCoordinator: NavigationCoordinatable {
@ -20,19 +19,19 @@ final class FilterCoordinator: NavigationCoordinatable {
@Root @Root
var start = makeStart var start = makeStart
let libraryItem: BaseItemDto
@Binding @Binding
var filters: LibraryFilters var filters: LibraryFilters
var enabledFilterType: [FilterType] var enabledFilterType: [FilterType]
var parentId: String = ""
init(libraryItem: BaseItemDto, filters: Binding<LibraryFilters>, enabledFilterType: [FilterType]) { init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
self.libraryItem = libraryItem
_filters = filters _filters = filters
self.enabledFilterType = enabledFilterType self.enabledFilterType = enabledFilterType
self.parentId = parentId
} }
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: libraryItem.id!) LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId)
} }
} }

View File

@ -32,8 +32,8 @@ final class HomeCoordinator: NavigationCoordinatable {
NavigationViewCoordinator(SettingsCoordinator()) NavigationViewCoordinator(SettingsCoordinator())
} }
func makeLibrary(viewModel: LibraryViewModel) -> LibraryCoordinator { func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: viewModel) LibraryCoordinator(viewModel: params.viewModel, title: params.title)
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
@ -44,8 +44,8 @@ final class HomeCoordinator: NavigationCoordinatable {
NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(ItemCoordinator(item: item))
} }
func makeModalLibrary(viewModel: LibraryViewModel) -> NavigationViewCoordinator<LibraryCoordinator> { func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel)) NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
} }
@ViewBuilder @ViewBuilder

View File

@ -32,8 +32,8 @@ final class ItemCoordinator: NavigationCoordinatable {
self.itemDto = item self.itemDto = item
} }
func makeLibrary(viewModel: LibraryViewModel) -> LibraryCoordinator { func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: viewModel) LibraryCoordinator(viewModel: params.viewModel, title: params.title)
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {

View File

@ -11,6 +11,8 @@ import JellyfinAPI
import Stinsen import Stinsen
import SwiftUI import SwiftUI
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
final class LibraryCoordinator: NavigationCoordinatable { final class LibraryCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LibraryCoordinator.start) let stack = NavigationStack(initial: \LibraryCoordinator.start)
@ -27,14 +29,16 @@ final class LibraryCoordinator: NavigationCoordinatable {
var modalItem = makeModalItem var modalItem = makeModalItem
let viewModel: LibraryViewModel let viewModel: LibraryViewModel
let title: String
init(viewModel: LibraryViewModel) { init(viewModel: LibraryViewModel, title: String) {
self.viewModel = viewModel self.viewModel = viewModel
self.title = title
} }
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
LibraryView(viewModel: self.viewModel) LibraryView(viewModel: self.viewModel, title: title)
} }
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
@ -42,9 +46,9 @@ final class LibraryCoordinator: NavigationCoordinatable {
} }
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> { func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(libraryItem: viewModel.libraryItem, NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
filters: params.filters, enabledFilterType: params.enabledFilterType,
enabledFilterType: params.enabledFilterType)) parentId: params.parentId))
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {

View File

@ -27,8 +27,8 @@ final class LibraryListCoordinator: NavigationCoordinatable {
self.viewModel = viewModel self.viewModel = viewModel
} }
func makeLibrary(viewModel: LibraryViewModel) -> LibraryCoordinator { func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
LibraryCoordinator(viewModel: viewModel) LibraryCoordinator(viewModel: params.viewModel, title: params.title)
} }
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {

View File

@ -34,6 +34,6 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
} }
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { func makeLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: .init(libraryItem: <#T##BaseItemDto#>)) LibraryCoordinator(viewModel: .init(libraryItem: <#T##BaseItemDto#>))
} }
} }

View File

@ -206,7 +206,7 @@ public extension BaseItemDto {
case episode = "Episode" case episode = "Episode"
case series = "Series" case series = "Series"
case boxset = "BoxSet" case boxset = "BoxSet"
case collectionFolder = "CollectionFolder" case collectionFolder = "CollectionFolder"
case unknown case unknown
@ -229,7 +229,7 @@ public extension BaseItemDto {
func portraitHeaderViewURL(maxWidth: Int) -> URL { func portraitHeaderViewURL(maxWidth: Int) -> URL {
switch itemType { switch itemType {
case .movie, .season, .series, .boxset, .collectionFolder: case .movie, .season, .series, .boxset, .collectionFolder:
return getPrimaryImage(maxWidth: maxWidth) return getPrimaryImage(maxWidth: maxWidth)
case .episode: case .episode:
return getSeriesPrimaryImage(maxWidth: maxWidth) return getSeriesPrimaryImage(maxWidth: maxWidth)

View File

@ -67,7 +67,8 @@ final class LibraryFilterViewModel: ViewModel {
} }
func requestQueryFilters() { func requestQueryFilters() {
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId) FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id,
parentId: self.parentId)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)

View File

@ -13,8 +13,8 @@ final class LibraryListViewModel: ViewModel {
@Published @Published
var libraries: [BaseItemDto] = [] var libraries: [BaseItemDto] = []
@Published @Published
var libraryRandomItems: [BaseItemDto: BaseItemDto] = [:] var libraryRandomItems: [BaseItemDto: BaseItemDto] = [:]
// temp // temp
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: []) var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
@ -31,34 +31,34 @@ final class LibraryListViewModel: ViewModel {
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
}, receiveValue: { response in }, receiveValue: { response in
if let libraries = response.items { if let libraries = response.items {
self.libraries = libraries self.libraries = libraries
for library in libraries { for library in libraries {
self.getRandomLibraryItem(for: library) self.getRandomLibraryItem(for: library)
} }
} }
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
// MARK: Library random item // MARK: Library random item
func getRandomLibraryItem(for library: BaseItemDto) { func getRandomLibraryItem(for library: BaseItemDto) {
guard library.itemType == .collectionFolder else { return } guard library.itemType == .collectionFolder else { return }
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id, ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
limit: 1, limit: 1,
parentId: library.id) parentId: library.id)
.sink { completion in .sink { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} receiveValue: { result in } receiveValue: { result in
if let item = result.items?.first { if let item = result.items?.first {
self.libraryRandomItems[library] = item self.libraryRandomItems[library] = item
} else { } else {
self.libraryRandomItems[library] = library self.libraryRandomItems[library] = library
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
} }

View File

@ -23,9 +23,9 @@ struct LibraryRowCell: Hashable {
final class LibraryViewModel: ViewModel { final class LibraryViewModel: ViewModel {
@Published @Published
var items: [BaseItemDto] = [] var items: [BaseItemDto] = []
@Published @Published
var rows: [LibraryRow] = [] var rows: [LibraryRow] = []
@Published @Published
var totalPages = 0 var totalPages = 0
@ -37,13 +37,13 @@ final class LibraryViewModel: ViewModel {
// temp // temp
@Published @Published
var filters: LibraryFilters var filters: LibraryFilters
let libraryItem: BaseItemDto var parentID: String?
var person: BaseItemPerson? var person: BaseItemPerson?
var genre: NameGuidPair? var genre: NameGuidPair?
var studio: NameGuidPair? var studio: NameGuidPair?
private let columns: Int private let columns: Int
private let pageItemSize: Int private let pageItemSize: Int
var enabledFilterType: [FilterType] { var enabledFilterType: [FilterType] {
if genre == nil { if genre == nil {
@ -53,40 +53,38 @@ final class LibraryViewModel: ViewModel {
} }
} }
init(libraryItem: BaseItemDto, init(parentID: String? = nil,
person: BaseItemPerson? = nil, person: BaseItemPerson? = nil,
genre: NameGuidPair? = nil, genre: NameGuidPair? = nil,
studio: NameGuidPair? = nil, studio: NameGuidPair? = nil,
filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]), filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]),
columns: Int = 7) columns: Int = 7)
{ {
self.libraryItem = libraryItem self.parentID = parentID
self.person = person self.person = person
self.genre = genre self.genre = genre
self.studio = studio self.studio = studio
self.filters = filters self.filters = filters
self.columns = columns self.columns = columns
// Size is typical size of portrait items // Size is typical size of portrait items
self.pageItemSize = UIScreen.itemsFillableOnScreen(width: 130, height: 185) self.pageItemSize = UIScreen.itemsFillableOnScreen(width: 130, height: 185)
print("Page item size: \(pageItemSize)")
super.init() super.init()
$filters $filters
.sink(receiveValue: { newFilters in .sink(receiveValue: { newFilters in
self.requestItemsAsync(with: newFilters, replaceCurrentItems: true) self.requestItemsAsync(with: newFilters, replaceCurrentItems: true)
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }
func requestItemsAsync(with filters: LibraryFilters, replaceCurrentItems: Bool = false) { func requestItemsAsync(with filters: LibraryFilters, replaceCurrentItems: Bool = false) {
if replaceCurrentItems { if replaceCurrentItems {
self.items = [] self.items = []
} }
let personIDs: [String] = [person].compactMap(\.?.id) let personIDs: [String] = [person].compactMap(\.?.id)
let studioIDs: [String] = [studio].compactMap(\.?.id) let studioIDs: [String] = [studio].compactMap(\.?.id)
let genreIDs: [String] let genreIDs: [String]
@ -102,7 +100,7 @@ final class LibraryViewModel: ViewModel {
recursive: true, recursive: true,
searchTerm: nil, searchTerm: nil,
sortOrder: filters.sortOrder, sortOrder: filters.sortOrder,
parentId: libraryItem.id, parentId: parentID,
fields: [ fields: [
.primaryImageAspectRatio, .primaryImageAspectRatio,
.seriesPrimaryImage, .seriesPrimaryImage,
@ -122,14 +120,14 @@ final class LibraryViewModel: ViewModel {
studioIds: studioIDs, studioIds: studioIDs,
genreIds: genreIDs, genreIds: genreIDs,
enableImages: true) enableImages: true)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
guard let self = self else { return } guard let self = self else { return }
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / Double(self.pageItemSize)) let totalPages = ceil(Double(response.totalRecordCount ?? 0) / Double(self.pageItemSize))
self.totalPages = Int(totalPages) self.totalPages = Int(totalPages)
self.hasNextPage = self.currentPage < self.totalPages - 1 self.hasNextPage = self.currentPage < self.totalPages - 1
self.items.append(contentsOf: response.items ?? []) self.items.append(contentsOf: response.items ?? [])
@ -143,7 +141,7 @@ final class LibraryViewModel: ViewModel {
requestItemsAsync(with: filters) requestItemsAsync(with: filters)
} }
// tvOS calculations for collection view // tvOS calculations for collection view
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] { private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
guard !itemList.isEmpty else { return [] } guard !itemList.isEmpty else { return [] }
let rowCount = itemList.count / columns let rowCount = itemList.count / columns
@ -174,12 +172,12 @@ final class LibraryViewModel: ViewModel {
} }
extension UIScreen { extension UIScreen {
static func itemsFillableOnScreen(width: CGFloat, height: CGFloat) -> Int { static func itemsFillableOnScreen(width: CGFloat, height: CGFloat) -> Int {
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
let itemSize = width * height let itemSize = width * height
return Int(screenSize / itemSize) return Int(screenSize / itemSize)
} }
} }

View File

@ -12,6 +12,18 @@
}, },
"idiom" : "iphone" "idiom" : "iphone"
}, },
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.765",
"green" : "0.361",
"red" : "0.667"
}
},
"idiom" : "ipad"
},
{ {
"color" : { "color" : {
"color-space" : "srgb", "color-space" : "srgb",

View File

@ -11,89 +11,86 @@ import SwiftUI
// https://stackoverflow.com/questions/56573373/swiftui-get-size-of-child // https://stackoverflow.com/questions/56573373/swiftui-get-size-of-child
struct ChildSizeReader<Content: View>: View { struct ChildSizeReader<Content: View>: View {
@Binding var size: CGSize @Binding
let content: () -> Content var size: CGSize
var body: some View { let content: () -> Content
ZStack { var body: some View {
content() ZStack {
.background( content()
GeometryReader { proxy in .background(GeometryReader { proxy in
Color.clear Color.clear
.preference(key: SizePreferenceKey.self, value: proxy.size) .preference(key: SizePreferenceKey.self, value: proxy.size)
} })
) }
} .onPreferenceChange(SizePreferenceKey.self) { preferences in
.onPreferenceChange(SizePreferenceKey.self) { preferences in self.size = preferences
self.size = preferences }
} }
}
} }
struct SizePreferenceKey: PreferenceKey { struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize typealias Value = CGSize
static var defaultValue: Value = .zero static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) { static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue() _ = nextValue()
} }
} }
struct ViewOffsetKey: PreferenceKey { struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat typealias Value = CGFloat
static var defaultValue = CGFloat.zero static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) { static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue() value += nextValue()
} }
} }
struct DetectBottomScrollView<Content: View>: View { struct DetectBottomScrollView<Content: View>: View {
private let spaceName = "scroll" private let spaceName = "scroll"
@State private var wholeSize: CGSize = .zero @State
@State private var scrollViewSize: CGSize = .zero private var wholeSize: CGSize = .zero
@State private var previousDidReachBottom = false @State
let content: () -> Content private var scrollViewSize: CGSize = .zero
let didReachBottom: (Bool) -> Void @State
private var previousDidReachBottom = false
init(content: @escaping () -> Content, let content: () -> Content
didReachBottom: @escaping (Bool) -> Void) { let didReachBottom: (Bool) -> Void
self.content = content
self.didReachBottom = didReachBottom
}
var body: some View { init(content: @escaping () -> Content,
ChildSizeReader(size: $wholeSize) { didReachBottom: @escaping (Bool) -> Void)
ScrollView { {
ChildSizeReader(size: $scrollViewSize) { self.content = content
content() self.didReachBottom = didReachBottom
.background( }
GeometryReader { proxy in
Color.clear.preference(
key: ViewOffsetKey.self,
value: -1 * proxy.frame(in: .named(spaceName)).origin.y
)
}
)
.onPreferenceChange(
ViewOffsetKey.self,
perform: { value in
if value >= scrollViewSize.height - wholeSize.height { var body: some View {
if !previousDidReachBottom { ChildSizeReader(size: $wholeSize) {
previousDidReachBottom = true ScrollView {
didReachBottom(true) ChildSizeReader(size: $scrollViewSize) {
} content()
} else { .background(GeometryReader { proxy in
if previousDidReachBottom { Color.clear.preference(key: ViewOffsetKey.self,
previousDidReachBottom = false value: -1 * proxy.frame(in: .named(spaceName)).origin.y)
didReachBottom(false) })
} .onPreferenceChange(ViewOffsetKey.self,
} perform: { value in
}
) if value >= scrollViewSize.height - wholeSize.height {
} if !previousDidReachBottom {
} previousDidReachBottom = true
.coordinateSpace(name: spaceName) didReachBottom(true)
} }
} } else {
if previousDidReachBottom {
previousDidReachBottom = false
didReachBottom(false)
}
}
})
}
}
.coordinateSpace(name: spaceName)
}
}
} }

View File

@ -10,60 +10,60 @@ import JellyfinAPI
import SwiftUI import SwiftUI
struct PortraitItemButton<ItemType: PortraitImageStackable>: View { struct PortraitItemButton<ItemType: PortraitImageStackable>: View {
let item: ItemType
let maxWidth: CGFloat
let horizontalAlignment: HorizontalAlignment
let textAlignment: TextAlignment
let selectedAction: (ItemType) -> Void
init(item: ItemType,
maxWidth: CGFloat = 110,
horizontalAlignment: HorizontalAlignment = .leading,
textAlignment: TextAlignment = .leading,
selectedAction: @escaping (ItemType) -> Void)
{
self.item = item
self.maxWidth = maxWidth
self.horizontalAlignment = horizontalAlignment
self.textAlignment = textAlignment
self.selectedAction = selectedAction
}
var body: some View {
Button {
selectedAction(item)
} label: {
VStack(alignment: horizontalAlignment) {
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
bh: item.blurHash,
failureInitials: item.failureInitials)
.portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2)
if item.showTitle { let item: ItemType
Text(item.title) let maxWidth: CGFloat
.font(.footnote) let horizontalAlignment: HorizontalAlignment
.fontWeight(.regular) let textAlignment: TextAlignment
.foregroundColor(.primary) let selectedAction: (ItemType) -> Void
.multilineTextAlignment(textAlignment)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
}
if let description = item.subtitle { init(item: ItemType,
Text(description) maxWidth: CGFloat = 110,
.font(.caption) horizontalAlignment: HorizontalAlignment = .leading,
.fontWeight(.medium) textAlignment: TextAlignment = .leading,
.foregroundColor(.secondary) selectedAction: @escaping (ItemType) -> Void)
.multilineTextAlignment(textAlignment) {
.fixedSize(horizontal: false, vertical: true) self.item = item
.lineLimit(2) self.maxWidth = maxWidth
} self.horizontalAlignment = horizontalAlignment
} self.textAlignment = textAlignment
.frame(width: maxWidth) self.selectedAction = selectedAction
} }
.frame(alignment: .top)
.padding(.bottom) var body: some View {
} Button {
selectedAction(item)
} label: {
VStack(alignment: horizontalAlignment) {
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
bh: item.blurHash,
failureInitials: item.failureInitials)
.portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2)
if item.showTitle {
Text(item.title)
.font(.footnote)
.fontWeight(.regular)
.foregroundColor(.primary)
.multilineTextAlignment(textAlignment)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
}
if let description = item.subtitle {
Text(description)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(.secondary)
.multilineTextAlignment(textAlignment)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
}
}
.frame(width: maxWidth)
}
.frame(alignment: .top)
.padding(.bottom)
}
} }

View File

@ -90,9 +90,9 @@ struct HomeView: View {
Button { Button {
homeRouter homeRouter
// .route(to: \.library, (viewModel: .init(parentID: library.id!, .route(to: \.library, (viewModel: .init(parentID: library.id!,
// filters: viewModel.recentFilterSet), filters: viewModel.recentFilterSet),
// title: library.name ?? "")) title: library.name ?? ""))
} label: { } label: {
HStack { HStack {
L10n.seeAll.text.font(.subheadline).fontWeight(.bold) L10n.seeAll.text.font(.subheadline).fontWeight(.bold)

View File

@ -12,7 +12,7 @@ import Stinsen
import SwiftUI import SwiftUI
struct LibraryListView: View { struct LibraryListView: View {
@EnvironmentObject @EnvironmentObject
var libraryListRouter: LibraryListCoordinator.Router var libraryListRouter: LibraryListCoordinator.Router
@StateObject @StateObject
@ -23,31 +23,30 @@ struct LibraryListView: View {
LazyVStack { LazyVStack {
Button { Button {
libraryListRouter.route(to: \.library, libraryListRouter.route(to: \.library,
LibraryViewModel(libraryItem: BaseItemDto(), filters: viewModel.withFavorites)) (viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: L10n.favorites))
} label: { } label: {
HStack { HStack {
Spacer() Spacer()
L10n.yourFavorites.text L10n.yourFavorites.text
.foregroundColor(.black) .foregroundColor(.black)
.font(.subheadline) .font(.subheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
Spacer() Spacer()
} }
.frame(height: 100) .frame(height: 100)
.background(Color.white) .background(Color.white)
} }
.cornerRadius(10) .cornerRadius(10)
.shadow(radius: 5) .shadow(radius: 5)
.padding() .padding()
if !viewModel.isLoading { if !viewModel.isLoading {
if let collectionsLibraryItem = viewModel.libraries.first(where: { $0.collectionType == "boxsets" }) { if let collectionsLibraryItem = viewModel.libraries.first(where: { $0.collectionType == "boxsets" }) {
Button { Button {
libraryListRouter.route(to: \.library, libraryListRouter.route(to: \.library,
LibraryViewModel(libraryItem: collectionsLibraryItem)) (viewModel: LibraryViewModel(parentID: collectionsLibraryItem.id),
// (viewModel: LibraryViewModel(parentID: collectionsLibraryItem.id), title: collectionsLibraryItem.name ?? ""))
// title: collectionsLibraryItem.name ?? ""))
} label: { } label: {
ZStack { ZStack {
ImageView(src: collectionsLibraryItem.getPrimaryImage(maxWidth: 500), ImageView(src: collectionsLibraryItem.getPrimaryImage(maxWidth: 500),
@ -64,15 +63,15 @@ struct LibraryListView: View {
Spacer() Spacer()
} }
} }
.background(Color.black) .background(Color.black)
.frame(height: 100) .frame(height: 100)
} }
.cornerRadius(10) .cornerRadius(10)
.shadow(radius: 5) .shadow(radius: 5)
.padding() .padding()
} }
ForEach(Array(viewModel.libraryRandomItems.keys), id: \.id) { library in ForEach(Array(viewModel.libraryRandomItems.keys), id: \.id) { library in
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
Button { Button {
libraryListRouter.route(to: \.library, libraryListRouter.route(to: \.library,
@ -82,22 +81,22 @@ struct LibraryListView: View {
ZStack { ZStack {
// ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) // ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
// .opacity(0.4) // .opacity(0.4)
ImageView(src: viewModel.libraryRandomItems[library]!.getBackdropImage(maxWidth: 500)) ImageView(src: viewModel.libraryRandomItems[library]!.getBackdropImage(maxWidth: 500))
VStack { VStack {
Text(library.name ?? "") Text(library.name ?? "")
.foregroundColor(.white) .foregroundColor(.white)
.font(.title2) .font(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
} }
} }
.background(Color.black) .background(Color.black)
.frame(height: 100) .frame(height: 100)
} }
.cornerRadius(10) .cornerRadius(10)
.shadow(radius: 5) .shadow(radius: 5)
.padding() .padding()
} else { } else {
EmptyView() EmptyView()
} }

View File

@ -88,7 +88,7 @@ struct LibrarySearchView: View {
Button { Button {
searchRouter.route(to: \.item, item) searchRouter.route(to: \.item, item)
} label: { } label: {
PortraitItemElement(item: item) PortraitItemElement(item: item)
} }
} }
} }

View File

@ -10,84 +10,87 @@ import Stinsen
import SwiftUI import SwiftUI
struct LibraryView: View { struct LibraryView: View {
@EnvironmentObject @EnvironmentObject
var libraryRouter: LibraryCoordinator.Router var libraryRouter: LibraryCoordinator.Router
@StateObject @StateObject
var viewModel: LibraryViewModel var viewModel: LibraryViewModel
var title: String
// MARK: tracks for grid // MARK: tracks for grid
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
@State @State
private var tracks: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125) private var tracks: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top),
count: Int(UIScreen.main.bounds.size.width) / 125)
func recalcTracks() { func recalcTracks() {
tracks = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125) tracks = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125)
} }
@ViewBuilder @ViewBuilder
private var loadingView: some View { private var loadingView: some View {
ProgressView() ProgressView()
} }
@ViewBuilder @ViewBuilder
private var noResultsView: some View { private var noResultsView: some View {
L10n.noResults.text L10n.noResults.text
} }
@ViewBuilder @ViewBuilder
private var libraryItemsView: some View { private var libraryItemsView: some View {
DetectBottomScrollView { DetectBottomScrollView {
VStack { VStack {
LazyVGrid(columns: tracks) { LazyVGrid(columns: tracks) {
ForEach(viewModel.items, id: \.id) { item in ForEach(viewModel.items, id: \.id) { item in
if item.type != "Folder" { if item.type != "Folder" {
PortraitItemButton(item: item) { item in PortraitItemButton(item: item) { item in
libraryRouter.route(to: \.item, item) libraryRouter.route(to: \.item, item)
} }
} }
} }
} }
.ignoresSafeArea() .ignoresSafeArea()
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.onRotate { _ in .onRotate { _ in
recalcTracks() recalcTracks()
} }
Spacer() Spacer()
.frame(height: 30) .frame(height: 30)
} }
} didReachBottom: { newValue in } didReachBottom: { newValue in
if newValue && viewModel.hasNextPage { if newValue && viewModel.hasNextPage {
viewModel.requestNextPageAsync() viewModel.requestNextPageAsync()
} }
} }
} }
var body: some View { var body: some View {
Group { Group {
if viewModel.isLoading && viewModel.items.isEmpty { if viewModel.isLoading && viewModel.items.isEmpty {
ProgressView() ProgressView()
} else if !viewModel.items.isEmpty { } else if !viewModel.items.isEmpty {
libraryItemsView libraryItemsView
} else { } else {
noResultsView noResultsView
} }
} }
.navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
Button { Button {
// libraryRouter libraryRouter
// .route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, .route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
// parentId: viewModel.parentID ?? "")) parentId: viewModel.parentID ?? ""))
} label: { } label: {
Image(systemName: "line.horizontal.3.decrease.circle") Image(systemName: "line.horizontal.3.decrease.circle")
} }
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
Button { Button {
// libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID)) // libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
} label: { } label: {