diff --git a/PreferencesView/Sources/PreferencesView/Box.swift b/PreferencesView/Sources/PreferencesView/Box.swift index e69b6dd9e..dc054e087 100644 --- a/PreferencesView/Sources/PreferencesView/Box.swift +++ b/PreferencesView/Sources/PreferencesView/Box.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // class Box { diff --git a/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift b/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift index 000dacb47..f287d5fa2 100644 --- a/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift +++ b/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift b/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift index 8f1c38878..56ddb7161 100644 --- a/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift +++ b/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/PreferencesView.swift b/PreferencesView/Sources/PreferencesView/PreferencesView.swift index 6910a140f..2ed71ce90 100644 --- a/PreferencesView/Sources/PreferencesView/PreferencesView.swift +++ b/PreferencesView/Sources/PreferencesView/PreferencesView.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift b/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift index b29bf2bfe..1f28fb130 100644 --- a/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift +++ b/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift b/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift index ddbefe8f5..143551d38 100644 --- a/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift +++ b/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwizzleSwift diff --git a/PreferencesView/Sources/PreferencesView/ViewExtensions.swift b/PreferencesView/Sources/PreferencesView/ViewExtensions.swift index e2e5a3153..49678a3d9 100644 --- a/PreferencesView/Sources/PreferencesView/ViewExtensions.swift +++ b/PreferencesView/Sources/PreferencesView/ViewExtensions.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index 7173d2f9b..ec3ac4e6c 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -67,8 +67,7 @@ final class LibraryCoordinator: NavigationCoordinatable { Text("FIX ME") } else { LibraryView(parent: parent, filters: parameters.filters) - - + // LibraryView(viewModel: LibraryViewModel( // parent: parent, // type: parameters.type, @@ -98,7 +97,7 @@ final class LibraryCoordinator: NavigationCoordinatable { func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { LibraryCoordinator(parameters: parameters) } - + // func makeFolderLibrary() -> LibraryCoordinator { // LibraryCoordinator(parameters: .init(parent: <#T##LibraryParent#>, filters: <#T##ItemFilters#>)) // } diff --git a/Shared/Coordinators/MediaCoordinator.swift b/Shared/Coordinators/MediaCoordinator.swift index 8096a6022..6ef7ebe42 100644 --- a/Shared/Coordinators/MediaCoordinator.swift +++ b/Shared/Coordinators/MediaCoordinator.swift @@ -37,7 +37,7 @@ final class MediaCoordinator: NavigationCoordinatable { func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { LibraryCoordinator(parameters: parameters) } - + func makeFolderLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { LibraryCoordinator(parameters: parameters) } diff --git a/Shared/Extensions/Edge.swift b/Shared/Extensions/Edge.swift index 472e687e3..bc505bf4d 100644 --- a/Shared/Extensions/Edge.swift +++ b/Shared/Extensions/Edge.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto.swift index d17b395be..cadf59664 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto.swift @@ -254,14 +254,14 @@ extension BaseItemDto { static var noResults: BaseItemDto { .init(name: L10n.noResults) } - + // TODO: remove when `collectionType` becomes an enum func includedItemTypesForCollectionType() -> [BaseItemKind]? { - + guard let collectionType else { return nil } - + var itemTypes: [BaseItemKind]? - + switch collectionType { case "movies": itemTypes = [.movie] @@ -272,7 +272,7 @@ extension BaseItemDto { default: itemTypes = nil } - + return itemTypes } } diff --git a/Shared/Extensions/JellyfinAPI/NameGuidPair.swift b/Shared/Extensions/JellyfinAPI/NameGuidPair.swift index f8de9f135..57b88f193 100644 --- a/Shared/Extensions/JellyfinAPI/NameGuidPair.swift +++ b/Shared/Extensions/JellyfinAPI/NameGuidPair.swift @@ -16,9 +16,9 @@ extension NameGuidPair: Displayable { } } -//extension NameGuidPair: LibraryParent { +// extension NameGuidPair: LibraryParent { // var libraryType: BaseItemKind? { } -//} +// } extension NameGuidPair { diff --git a/Shared/Extensions/Stateful.swift b/Shared/Extensions/Stateful.swift new file mode 100644 index 000000000..7140f6e25 --- /dev/null +++ b/Shared/Extensions/Stateful.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) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation + +protocol Stateful: AnyObject { + + associatedtype State + associatedtype Action + + var state: State { get set } + + /// Send an action to the `Stateful` object, which will + /// `respond` to the action and set the new state. + @MainActor + func send(_ action: Action) + + /// Respond to a sent action and return the new state + @MainActor + func respond(to action: Action) -> State +} + +extension Stateful { + + @MainActor + func send(_ action: Action) { + state = respond(to: action) + } +} diff --git a/Shared/Extensions/Task.swift b/Shared/Extensions/Task.swift index 05a33c5bc..7ec48f532 100644 --- a/Shared/Extensions/Task.swift +++ b/Shared/Extensions/Task.swift @@ -10,7 +10,7 @@ import Combine import Foundation extension Task { - + func asAnyCancellable() -> AnyCancellable { AnyCancellable(cancel) } diff --git a/Shared/Extensions/UICollectionView.swift b/Shared/Extensions/UICollectionView.swift index a54fa5830..5a8ffb98f 100644 --- a/Shared/Extensions/UICollectionView.swift +++ b/Shared/Extensions/UICollectionView.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/ViewExtensions/Backport.swift b/Shared/Extensions/ViewExtensions/Backport.swift index 32f69e9f5..70906926d 100644 --- a/Shared/Extensions/ViewExtensions/Backport.swift +++ b/Shared/Extensions/ViewExtensions/Backport.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/AfterLastDisappearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/AfterLastDisappearModifier.swift index 614ed360b..a2f20c2ae 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/AfterLastDisappearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/AfterLastDisappearModifier.swift @@ -9,12 +9,12 @@ import SwiftUI struct AfterLastDisappearModifier: ViewModifier { - + @State private var lastDisappear: Date? = nil - + let action: (TimeInterval) -> Void - + func body(content: Content) -> some View { content .onAppear { diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift index 948331e15..888dda70c 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift @@ -9,29 +9,29 @@ import SwiftUI struct OnFinalDisappearModifier: ViewModifier { - + @StateObject private var observer: Observer - + init(action: @escaping () -> Void) { _observer = StateObject(wrappedValue: Observer(action: action)) } - + func body(content: Content) -> some View { content .background { Color.clear } } - + private class Observer: ObservableObject { - + private let action: () -> Void - + init(action: @escaping () -> Void) { self.action = action } - + deinit { action() } diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift index d60112710..c85402d0b 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift @@ -9,12 +9,12 @@ import SwiftUI struct OnFirstAppearModifier: ViewModifier { - + @State private var didAppear = false - + let action: () -> Void - + func body(content: Content) -> some View { content .onAppear { diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift index 77e779122..0d1500b10 100644 --- a/Shared/Extensions/ViewExtensions/ViewExtensions.swift +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -53,7 +53,7 @@ extension View { transformElse(self) } } - + @ViewBuilder @inlinable func ifLet( @@ -237,23 +237,23 @@ extension View { var backport: Backport { Backport(content: self) } - + /// Perform an action on the final disappear of a `View` func onFinalDisappear(perform action: @escaping () -> Void) -> some View { modifier(OnFinalDisappearModifier(action: action)) } - + /// Perform an action only on the first appearance of a `View` func onFirstAppear(perform action: @escaping () -> Void) -> some View { modifier(OnFirstAppearModifier(action: action)) } - + /// Perform an action as a view appears which gives a time interval /// from when this view last disappeared. func afterLastDisappear(perform action: @escaping (TimeInterval) -> Void) -> some View { modifier(AfterLastDisappearModifier(action: action)) } - + func topBarTrailing(@ViewBuilder content: @escaping () -> some View) -> some View { toolbar { ToolbarItemGroup(placement: .topBarTrailing) { diff --git a/Shared/Objects/LibraryParent.swift b/Shared/Objects/LibraryParent.swift index 7b0d08f20..22621456c 100644 --- a/Shared/Objects/LibraryParent.swift +++ b/Shared/Objects/LibraryParent.swift @@ -11,7 +11,7 @@ import JellyfinAPI protocol LibraryParent: Displayable, Identifiable { var id: String? { get } - + // Only called `libraryType` because `BaseItemPerson` has // a different `type` property. However, people should have // different views so this can be renamed when they do, or diff --git a/Shared/ViewModels/FilterViewModel.swift b/Shared/ViewModels/FilterViewModel.swift index 0a039f74b..c86a5ced2 100644 --- a/Shared/ViewModels/FilterViewModel.swift +++ b/Shared/ViewModels/FilterViewModel.swift @@ -34,7 +34,7 @@ final class FilterViewModel: ViewModel { ) let request = Paths.getQueryFilters(parameters: parameters) let response = try? await userSession.client.send(request) - + return response?.value.genres? .map(\.filter) ?? [] } diff --git a/Shared/ViewModels/FoldersViewModel.swift b/Shared/ViewModels/FoldersViewModel.swift index 4255c3162..e90c87253 100644 --- a/Shared/ViewModels/FoldersViewModel.swift +++ b/Shared/ViewModels/FoldersViewModel.swift @@ -13,40 +13,40 @@ import JellyfinAPI // is required to handle `collectionFolder` collectionType items, // which is changed to a `folder` collectionType for view-handling final class FolderViewModel: LibraryViewModel { - + override func get(page: Int) async throws -> [BaseItemDto] { let parameters = getItemParameters(for: page) let request = Paths.getItemsByUserID(userID: userSession.user.id, parameters: parameters) let response = try await userSession.client.send(request) - + var validItems = (response.value.items ?? []).compactMap { item in - + if item.collectionType == "collectionFolder" { var t = item t.collectionType = "folder" return t } - + return item } - + return validItems } - + override func getItemParameters(for page: Int) -> Paths.GetItemsByUserIDParameters { - + let filters = filterViewModel.currentFilters - + var parameters = Paths.GetItemsByUserIDParameters() parameters.parentID = parent?.id parameters.fields = ItemFields.minimumCases parameters.includeItemTypes = [.movie, .series, .boxSet, .folder, .collectionFolder] - + parameters.limit = Self.DefaultPageSize parameters.startIndex = page * Self.DefaultPageSize parameters.sortOrder = filters.sortOrder.map { SortOrder(rawValue: $0.filterName) ?? .ascending } parameters.sortBy = filters.sortBy.map(\.filterName).prepending("IsFolder") - + return parameters } } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 1bc153afb..4b5e8ee7f 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -15,8 +15,22 @@ import SwiftUI import UIKit // TODO: Look at refactoring -class LibraryViewModel: PagingLibraryViewModel { - +class LibraryViewModel: PagingLibraryViewModel, Stateful { + + enum Action { + case error(JellyfinAPIError) + case getNextPage + case getRandomItem + } + + enum State { + case error(JellyfinAPIError) + case gettingFirstPage + case gettingNextPage + case gettingRandomItem + case initial + } + deinit { print("LibraryViewModel.deinit") } @@ -25,6 +39,11 @@ class LibraryViewModel: PagingLibraryViewModel { let parent: (any LibraryParent)? private let saveFilters: Bool + var state: State + + func respond(to action: Action) -> State { + switch action {} + } var libraryCoordinatorParameters: LibraryCoordinator.Parameters { if let parent = parent { @@ -63,25 +82,25 @@ class LibraryViewModel: PagingLibraryViewModel { // if self.saveFilters, let id = self.parent?.id { // Defaults[.libraryFilterStore][id] = newFilters // } -// +// // print("got new filters?") -// +// // Task { // print("refreshing from filter change") -// -// +// +// // } // .asAnyCancellable() // .store(in: &self.cancellables) // } // .store(in: &cancellables) } - + override func get(page: Int) async throws -> [BaseItemDto] { let parameters = getItemParameters(for: page) let request = Paths.getItemsByUserID(userID: userSession.user.id, parameters: parameters) let response = try await userSession.client.send(request) - + // 1 - only care to keep collections that hold valid items // 2 - if parent is type `folder`, then we are in a folder-view // context so change `collectionFolder` types to `folder` @@ -91,17 +110,17 @@ class LibraryViewModel: PagingLibraryViewModel { if let collectionType = item.collectionType { return ["movies", "tvshows", "mixed", "boxsets"].contains(collectionType) } - + return true } .map { item in if parent?.libraryType == .folder, item.type == .collectionFolder { return item.mutating(\.type, with: .folder) } - + return item } - + return validItems } @@ -142,7 +161,7 @@ class LibraryViewModel: PagingLibraryViewModel { parameters.personIDs = personIDs parameters.studioIDs = studioIDs parameters.genreIDs = genreIDs - + parameters.limit = Self.DefaultPageSize parameters.startIndex = page * Self.DefaultPageSize parameters.sortOrder = filters.sortOrder.map { SortOrder(rawValue: $0.filterName) ?? .ascending } diff --git a/Shared/ViewModels/MediaViewModel.swift b/Shared/ViewModels/MediaViewModel.swift index dec33487c..170f4e0e3 100644 --- a/Shared/ViewModels/MediaViewModel.swift +++ b/Shared/ViewModels/MediaViewModel.swift @@ -48,17 +48,17 @@ final class MediaViewModel: ViewModel { let response = try await userSession.client.send(request) guard let items = response.value.items else { return [] } - + // folders has `type = UserView`, but we need to manually // force it to `folders` for better view handling let supportedLibraries = items .filter(using: \.collectionType, by: Self.supportedCollectionTypes) .map { item in - + if item.type == .userView, item.collectionType == "folder" { return item.mutating(\.type, with: .folder) } - + return item } diff --git a/Shared/ViewModels/NextUpLibraryViewModel.swift b/Shared/ViewModels/NextUpLibraryViewModel.swift index 508bc57a4..00a6a974b 100644 --- a/Shared/ViewModels/NextUpLibraryViewModel.swift +++ b/Shared/ViewModels/NextUpLibraryViewModel.swift @@ -13,7 +13,7 @@ import JellyfinAPI final class NextUpLibraryViewModel: PagingLibraryViewModel { // override func getCurrentPage() async throws { -// +// // await MainActor.run { // self.isLoading = true // } diff --git a/Shared/ViewModels/PagingLibraryViewModel.swift b/Shared/ViewModels/PagingLibraryViewModel.swift index da12429b1..fb1e5894d 100644 --- a/Shared/ViewModels/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/PagingLibraryViewModel.swift @@ -27,7 +27,7 @@ class PagingLibraryViewModel: ViewModel { print("did set currentPagingRequest") } } - + deinit { print("PagingLibraryViewModel.deinit") } @@ -54,29 +54,29 @@ class PagingLibraryViewModel: ViewModel { // // let request = Paths.getItems(parameters: parameters) // let response = try? await userSession.client.send(request) -// +// // await MainActor.run { // self.isLoading = false // } -// +// // return response?.value.items?.first } final func refresh() async throws { - + currentPage = -1 hasNextPage = true await MainActor.run { items = [] } - + let a = Task { - return try await getNextPage() + try await getNextPage() } - + currentPagingRequest = a.asAnyCancellable() - + try await a.value } @@ -87,18 +87,18 @@ class PagingLibraryViewModel: ViewModel { /// if there is a next page or not. final func getNextPage() async throws { guard !isLoading, hasNextPage else { return } - + await MainActor.run { isLoading = true } - + currentPage += 1 - + try await Task.sleep(nanoseconds: 10_000_000_000) let pageItems = try await get(page: currentPage) - + hasNextPage = !(pageItems.count < Self.DefaultPageSize) - + await MainActor.run { items.append(contentsOf: pageItems) isLoading = false diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 58596b861..597964422 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -196,6 +196,8 @@ E10EAA4F277BBCC4000269ED /* CGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSize.swift */; }; E11042562B7A9A7500821020 /* FoldersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11042552B7A9A7500821020 /* FoldersViewModel.swift */; }; E11042572B7A9A7500821020 /* FoldersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11042552B7A9A7500821020 /* FoldersViewModel.swift */; }; + E11042752B8013DF00821020 /* Stateful.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11042742B8013DF00821020 /* Stateful.swift */; }; + E11042762B8013DF00821020 /* Stateful.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11042742B8013DF00821020 /* Stateful.swift */; }; 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 */; }; @@ -931,6 +933,7 @@ E10E842B29A589860064EA49 /* NonePosterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonePosterButton.swift; sourceTree = ""; }; E10EAA4E277BBCC4000269ED /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = ""; }; E11042552B7A9A7500821020 /* FoldersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersViewModel.swift; sourceTree = ""; }; + E11042742B8013DF00821020 /* Stateful.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stateful.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 = ""; }; @@ -1860,13 +1863,14 @@ E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */, E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */, E1B5861129E32EEF00E45D6E /* Set.swift */, + E11042742B8013DF00821020 /* Stateful.swift */, 621338922660107500A81A2A /* String.swift */, + E1DD55362B6EE533007501C0 /* Task.swift */, E1A2C153279A7D5A005EC829 /* UIApplication.swift */, E1856DED2AFA007A007FDDBC /* UICollectionView.swift */, E1401CB029386C9200E8B599 /* UIColor.swift */, E13DD3C727164B1E009D4DAF /* UIDevice.swift */, E1E0BEB629EF450B0002E8D3 /* UIGestureRecognizer.swift */, - E1DD55362B6EE533007501C0 /* Task.swift */, E1937A3D288F0D3D00CB80AA /* UIScreen.swift */, E18E0239288749540022598C /* UIScrollView.swift */, 62E1DCC2273CE19800C9AE76 /* URL.swift */, @@ -3272,6 +3276,7 @@ E1E6C45129B104850064123F /* Button.swift in Sources */, E1DC981A296DD1CD00982F06 /* CinematicBackgroundView.swift in Sources */, E1A1528B28FD22F600600579 /* TextPairView.swift in Sources */, + E11042762B8013DF00821020 /* Stateful.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, E1575E66293E77B5001665B1 /* Poster.swift in Sources */, E18E021F2887492B0022598C /* InitialFailureView.swift in Sources */, @@ -3642,6 +3647,7 @@ E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */, E1401CA92938140700E8B599 /* DarkAppIcon.swift in Sources */, E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, + E11042752B8013DF00821020 /* Stateful.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E12376AE2A33D680001F5B44 /* AboutViewCard.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplication.swift in Sources */, diff --git a/Swiftfin/Components/PagingLibraryView.swift b/Swiftfin/Components/PagingLibraryView.swift index f56b5b925..b5fde0081 100644 --- a/Swiftfin/Components/PagingLibraryView.swift +++ b/Swiftfin/Components/PagingLibraryView.swift @@ -25,6 +25,8 @@ struct PagingLibraryView: View { @State private var layout: CollectionVGridLayout + private var onReachedBottomEdge: () -> Void + private var onReachedBottomEdgeOffset: Int private var onSelect: (BaseItemDto) -> Void // lists will add their own insets to manually add the dividers @@ -91,7 +93,6 @@ struct PagingLibraryView: View { // MARK: body - // TODO: `getNextpage` on bottom var body: some View { CollectionVGrid( $viewModel.items, @@ -132,9 +133,11 @@ extension PagingLibraryView { self.init( viewModel: viewModel, layout: .columns(3), + onReachedBottomEdge: {}, + onReachedBottomEdgeOffset: 0, onSelect: { _ in } ) - + if UIDevice.isPhone { layout = phoneLayout(libraryViewType: libraryViewType) } else { @@ -142,6 +145,11 @@ extension PagingLibraryView { } } + func onReachedBottomEdge(offset: Int, perform action: @escaping () -> Void) -> Self { + copy(modifying: \.onReachedBottomEdge, with: action) + .copy(modifying: \.onReachedBottomEdgeOffset, with: offset) + } + func onSelect(_ action: @escaping (BaseItemDto) -> Void) -> Self { copy(modifying: \.onSelect, with: action) } diff --git a/Swiftfin/Components/RandomItemButton.swift b/Swiftfin/Components/RandomItemButton.swift index 43b028f54..f6f57d081 100644 --- a/Swiftfin/Components/RandomItemButton.swift +++ b/Swiftfin/Components/RandomItemButton.swift @@ -14,7 +14,7 @@ struct RandomItemButton: View { @ObservedObject private var viewModel: PagingLibraryViewModel - + private var onSelect: (BaseItemDto?) -> Void var body: some View { @@ -30,7 +30,7 @@ struct RandomItemButton: View { } extension RandomItemButton { - + init(viewModel: PagingLibraryViewModel) { self.init( viewModel: viewModel, diff --git a/Swiftfin/Views/CastAndCrewLibraryView.swift b/Swiftfin/Views/CastAndCrewLibraryView.swift index 0b2f739e7..ed8b09719 100644 --- a/Swiftfin/Views/CastAndCrewLibraryView.swift +++ b/Swiftfin/Views/CastAndCrewLibraryView.swift @@ -51,14 +51,14 @@ struct CastAndCrewLibraryView: View { private var noResultsView: some View { L10n.noResults.text } - + private func gridItemView(person: BaseItemPerson) -> some View { PosterButton(item: person, type: .portrait) .onSelect { router.route(to: \.library, .init(parent: person, filters: .init())) } } - + private func listItemView(person: BaseItemPerson) -> some View { LibraryItemRow(item: person) .onSelect { diff --git a/Swiftfin/Views/FolderLibraryView.swift b/Swiftfin/Views/FolderLibraryView.swift index 8cd3dc713..c2eb6301a 100644 --- a/Swiftfin/Views/FolderLibraryView.swift +++ b/Swiftfin/Views/FolderLibraryView.swift @@ -3,7 +3,7 @@ // 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) 2023 Jellyfin & Jellyfin Contributors +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift index 79abb8373..e72cfe636 100644 --- a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift +++ b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift @@ -22,7 +22,7 @@ extension ItemView { PillHStack( title: L10n.studios, items: studios - ).onSelect { studio in + ).onSelect { _ in // router.route(to: \.library, .init(parent: studio, type: .studio, filters: .init())) } } diff --git a/Swiftfin/Views/LibraryView.swift b/Swiftfin/Views/LibraryView.swift index 737bbc52b..93d65c135 100644 --- a/Swiftfin/Views/LibraryView.swift +++ b/Swiftfin/Views/LibraryView.swift @@ -23,7 +23,7 @@ struct LibraryView: View { @StateObject private var viewModel: LibraryViewModel - + init(parent: any LibraryParent, filters: ItemFilters) { self._viewModel = StateObject( wrappedValue: LibraryViewModel( @@ -56,10 +56,11 @@ struct LibraryView: View { @ViewBuilder private var libraryItemsView: some View { PagingLibraryView(viewModel: viewModel) + .onReachedBottomEdge(offset: 100) {} .onSelect(baseItemOnSelect(_:)) .ignoresSafeArea() } - + @ViewBuilder private var innerBody: some View { if viewModel.isLoading && viewModel.items.isEmpty { @@ -88,7 +89,7 @@ struct LibraryView: View { // } // } .topBarTrailing { - + if viewModel.isLoading && !viewModel.items.isEmpty { ProgressView() }