diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 9dc02810..394ddd7a 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -28,7 +28,7 @@ internal enum L10n { internal static let allGenres = L10n.tr("Localizable", "allGenres", fallback: "All Genres") /// All Media internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media") - /// Appearance + /// Represents the Appearance setting label internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance") /// App Icon internal static let appIcon = L10n.tr("Localizable", "appIcon", fallback: "App Icon") @@ -106,7 +106,7 @@ internal enum L10n { internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: "Current Position") /// Customize internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize") - /// Dark + /// Represents the dark theme setting internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark") /// Default Scheme internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme") @@ -156,6 +156,8 @@ internal enum L10n { internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres") /// Green internal static let green = L10n.tr("Localizable", "green", fallback: "Green") + /// Grid + internal static let grid = L10n.tr("Localizable", "grid", fallback: "Grid") /// Haptic Feedback internal static let hapticFeedback = L10n.tr("Localizable", "hapticFeedback", fallback: "Haptic Feedback") /// Home @@ -194,8 +196,10 @@ internal enum L10n { } /// Library internal static let library = L10n.tr("Localizable", "library", fallback: "Library") - /// Light + /// Represents the light theme setting internal static let light = L10n.tr("Localizable", "light", fallback: "Light") + /// List + internal static let list = L10n.tr("Localizable", "list", fallback: "List") /// Live TV internal static let liveTV = L10n.tr("Localizable", "liveTV", fallback: "Live TV") /// Loading @@ -340,6 +344,8 @@ internal enum L10n { internal static let quickConnectStep3 = L10n.tr("Localizable", "quickConnectStep3", fallback: "3. Enter the following code:") /// Authorizing Quick Connect successful. Please continue on your other device. internal static let quickConnectSuccessMessage = L10n.tr("Localizable", "quickConnectSuccessMessage", fallback: "Authorizing Quick Connect successful. Please continue on your other device.") + /// Random + internal static let random = L10n.tr("Localizable", "random", fallback: "Random") /// Random Image internal static let randomImage = L10n.tr("Localizable", "randomImage", fallback: "Random Image") /// Rated @@ -480,7 +486,7 @@ internal enum L10n { internal static let suggestions = L10n.tr("Localizable", "suggestions", fallback: "Suggestions") /// Switch User internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User") - /// System + /// Represents the system theme setting internal static let system = L10n.tr("Localizable", "system", fallback: "System") /// System Control Gestures Enabled internal static let systemControlGesturesEnabled = L10n.tr("Localizable", "systemControlGesturesEnabled", fallback: "System Control Gestures Enabled") diff --git a/Shared/ViewModels/ItemTypeLibraryViewModel.swift b/Shared/ViewModels/ItemTypeLibraryViewModel.swift index d388dfcf..d602b4b5 100644 --- a/Shared/ViewModels/ItemTypeLibraryViewModel.swift +++ b/Shared/ViewModels/ItemTypeLibraryViewModel.swift @@ -8,6 +8,7 @@ import Combine import Foundation +import Get import JellyfinAPI final class ItemTypeLibraryViewModel: PagingLibraryViewModel { @@ -35,25 +36,8 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { 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) } - Task { - let parameters = Paths.GetItemsParameters( - userID: userSession.user.id, - startIndex: currentPage * pageItemSize, - limit: pageItemSize, - isRecursive: true, - sortOrder: sortOrder, - fields: ItemFields.allCases, - includeItemTypes: itemTypes, - filters: itemFilters, - sortBy: sortBy, - enableUserData: true, - genreIDs: genreIDs - ) + var parameters = self._getDefaultParams() let request = Paths.getItems(parameters: parameters) let response = try await userSession.client.send(request) @@ -68,6 +52,30 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { } } + override func _getDefaultParams() -> Paths.GetItemsParameters? { + let filters = filterViewModel.currentFilters + 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 parameters = Paths.GetItemsParameters( + userID: userSession.user.id, + startIndex: currentPage * pageItemSize, + limit: pageItemSize, + isRecursive: true, + sortOrder: sortOrder, + fields: ItemFields.allCases, + includeItemTypes: itemTypes, + filters: itemFilters, + sortBy: sortBy, + enableUserData: true, + genreIDs: genreIDs + ) + + return parameters + } + override func _requestNextPage() { requestItems(with: filterViewModel.currentFilters) } diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 5fbab98f..1bf62fd1 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -9,6 +9,7 @@ import Combine import Defaults import Factory +import Get import JellyfinAPI import SwiftUI import UIKit @@ -65,67 +66,21 @@ final class LibraryViewModel: PagingLibraryViewModel { self.hasNextPage = true } - var libraryID: String? - var personIDs: [String]? - var studioIDs: [String]? - - if let parent = parent { - switch type { - case .library, .folders: - libraryID = parent.id - case .person: - personIDs = [parent].compactMap(\.id) - case .studio: - studioIDs = [parent].compactMap(\.id) - } - } - - var recursive = true - let includeItemTypes: [BaseItemKind] - - if filters.filters.contains(ItemFilter.isFavorite.filter) { - includeItemTypes = [.movie, .boxSet, .series, .season, .episode] - } else if type == .folders { - recursive = false - includeItemTypes = [.movie, .boxSet, .series, .folder, .collectionFolder] - } else { - includeItemTypes = [.movie, .boxSet, .series] - } - - var excludedIDs: [String]? + var parameters = _getDefaultParams() + parameters?.limit = pageItemSize + parameters?.startIndex = currentPage * pageItemSize + parameters?.sortOrder = filters.sortOrder.map { SortOrder(rawValue: $0.filterName) ?? .ascending } + parameters?.sortBy = filters.sortBy.map(\.filterName).appending("IsFolder") if filters.sortBy.first == SortBy.random.filter { - excludedIDs = items.compactMap(\.id) + parameters?.excludeItemIDs = items.compactMap(\.id) } - 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) } - Task { await MainActor.run { self.isLoading = true } - let parameters = Paths.GetItemsParameters( - userID: userSession.user.id, - excludeItemIDs: excludedIDs, - startIndex: currentPage * pageItemSize, - limit: pageItemSize, - isRecursive: recursive, - sortOrder: sortOrder, - parentID: libraryID, - fields: ItemFields.allCases, - includeItemTypes: includeItemTypes, - filters: itemFilters, - sortBy: sortBy, - enableUserData: true, - personIDs: personIDs, - studioIDs: studioIDs, - genreIDs: genreIDs, - enableImages: true - ) let request = Paths.getItems(parameters: parameters) let response = try await userSession.client.send(request) @@ -144,4 +99,53 @@ final class LibraryViewModel: PagingLibraryViewModel { override func _requestNextPage() { requestItems(with: filterViewModel.currentFilters) } + + override func _getDefaultParams() -> Paths.GetItemsParameters? { + + let filters = filterViewModel.currentFilters + var libraryID: String? + var personIDs: [String]? + var studioIDs: [String]? + let includeItemTypes: [BaseItemKind] + var recursive = true + + if let parent = parent { + switch type { + case .library, .folders: + libraryID = parent.id + case .person: + personIDs = [parent].compactMap(\.id) + case .studio: + studioIDs = [parent].compactMap(\.id) + } + } + + if filters.filters.contains(ItemFilter.isFavorite.filter) { + includeItemTypes = [.movie, .boxSet, .series, .season, .episode] + } else if type == .folders { + recursive = false + includeItemTypes = [.movie, .boxSet, .series, .folder, .collectionFolder] + } else { + includeItemTypes = [.movie, .boxSet, .series] + } + + let genreIDs = filters.genres.compactMap(\.id) + let itemFilters: [ItemFilter] = filters.filters.compactMap { .init(rawValue: $0.filterName) } + + let parameters = Paths.GetItemsParameters( + userID: userSession.user.id, + isRecursive: recursive, + parentID: libraryID, + fields: ItemFields.allCases, + includeItemTypes: includeItemTypes, + filters: itemFilters, + enableUserData: true, + personIDs: personIDs, + studioIDs: studioIDs, + genreIDs: genreIDs, + enableImages: true + ) + + return parameters + } } diff --git a/Shared/ViewModels/PagingLibraryViewModel.swift b/Shared/ViewModels/PagingLibraryViewModel.swift index 042c93a7..42d7408e 100644 --- a/Shared/ViewModels/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/PagingLibraryViewModel.swift @@ -8,6 +8,7 @@ import Defaults import Foundation +import Get import JellyfinAPI import OrderedCollections import UIKit @@ -28,6 +29,30 @@ class PagingLibraryViewModel: ViewModel { return UIScreen.main.maxChildren(width: libraryGridPosterType.width, height: height) } + public func getRandomItemFromLibrary() async throws -> BaseItemDtoQueryResult { + + var parameters = _getDefaultParams() + parameters?.limit = 1 + parameters?.sortBy = [SortBy.random.rawValue] + + await MainActor.run { + self.isLoading = true + } + + let request = Paths.getItems(parameters: parameters) + let response = try await userSession.client.send(request) + + await MainActor.run { + self.isLoading = false + } + + return response.value + } + + func _getDefaultParams() -> Paths.GetItemsParameters? { + Paths.GetItemsParameters() + } + func refresh() { currentPage = 0 hasNextPage = true diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 39a51896..e7f55515 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 4E8B34EA2AB91B6E0018F305 /* FilterDrawerSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* FilterDrawerSelection.swift */; }; 4E8B34EB2AB91B6E0018F305 /* FilterDrawerSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* FilterDrawerSelection.swift */; }; 4EAA35BB2AB9699B00D840DD /* FilterDrawerButtonSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EAA35BA2AB9699B00D840DD /* FilterDrawerButtonSelectorView.swift */; }; + 4F1282B12A7F3E8F005BCA29 /* RandomItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1282B02A7F3E8F005BCA29 /* RandomItemButton.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; @@ -777,6 +778,7 @@ 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* FilterDrawerSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterDrawerSelection.swift; sourceTree = ""; }; 4EAA35BA2AB9699B00D840DD /* FilterDrawerButtonSelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterDrawerButtonSelectorView.swift; sourceTree = ""; }; + 4F1282B02A7F3E8F005BCA29 /* RandomItemButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomItemButton.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = ""; }; 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; @@ -1817,6 +1819,7 @@ E1581E26291EF59800D6C640 /* SplitContentView.swift */, E157562F29355B7900976E1F /* UpdateView.swift */, E192607F28D28AAD002314B4 /* UserProfileButton.swift */, + 4F1282B02A7F3E8F005BCA29 /* RandomItemButton.swift */, ); path = Components; sourceTree = ""; @@ -3374,6 +3377,7 @@ 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, E17AC9712954F636003D2BC2 /* DownloadListCoordinator.swift in Sources */, E10EAA4F277BBCC4000269ED /* CGSize.swift in Sources */, + 4F1282B12A7F3E8F005BCA29 /* RandomItemButton.swift in Sources */, E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */, E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */, E18E01FA288747580022598C /* AboutAppView.swift in Sources */, diff --git a/Swiftfin/Components/LibraryViewTypeToggle.swift b/Swiftfin/Components/LibraryViewTypeToggle.swift index efd1ff13..3b3564dd 100644 --- a/Swiftfin/Components/LibraryViewTypeToggle.swift +++ b/Swiftfin/Components/LibraryViewTypeToggle.swift @@ -25,9 +25,9 @@ struct LibraryViewTypeToggle: View { } label: { switch libraryViewType { case .grid: - Image(systemName: "list.dash") + Label(L10n.list, systemImage: "list.dash") case .list: - Image(systemName: "square.grid.2x2") + Label(L10n.grid, systemImage: "square.grid.2x2") } } } diff --git a/Swiftfin/Components/RandomItemButton.swift b/Swiftfin/Components/RandomItemButton.swift new file mode 100644 index 00000000..8ea41889 --- /dev/null +++ b/Swiftfin/Components/RandomItemButton.swift @@ -0,0 +1,42 @@ +// +// 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) 2023 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct RandomItemButton: View { + + @ObservedObject + private var viewModel: PagingLibraryViewModel + private var onSelect: (BaseItemDtoQueryResult) -> Void + + var body: some View { + Button { + Task { + let response = try await viewModel.getRandomItemFromLibrary() + onSelect(response) + } + } label: { + Label(L10n.random, systemImage: "dice.fill") + } + } +} + +extension RandomItemButton { + init(viewModel: PagingLibraryViewModel) { + self.init( + viewModel: viewModel, + onSelect: { _ in } + ) + } + + func onSelect(_ action: @escaping (BaseItemDtoQueryResult) -> Void) -> Self { + copy(modifying: \.onSelect, with: action) + } +} diff --git a/Swiftfin/Views/BasicLibraryView.swift b/Swiftfin/Views/BasicLibraryView.swift index ee6dd06a..f69d19ed 100644 --- a/Swiftfin/Views/BasicLibraryView.swift +++ b/Swiftfin/Views/BasicLibraryView.swift @@ -58,8 +58,17 @@ struct BasicLibraryView: View { if viewModel.isLoading && !viewModel.items.isEmpty { ProgressView() } - - LibraryViewTypeToggle(libraryViewType: $libraryViewType) + Menu { + LibraryViewTypeToggle(libraryViewType: $libraryViewType) + RandomItemButton(viewModel: viewModel) + .onSelect { response in + if let item = response.items?.first { + router.route(to: \.item, item) + } + } + } label: { + Image(systemName: "ellipsis.circle") + } } } } diff --git a/Swiftfin/Views/LibraryView.swift b/Swiftfin/Views/LibraryView.swift index ce378034..ee16e74a 100644 --- a/Swiftfin/Views/LibraryView.swift +++ b/Swiftfin/Views/LibraryView.swift @@ -84,12 +84,20 @@ struct LibraryView: View { } .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { - if viewModel.isLoading && !viewModel.items.isEmpty { ProgressView() } - - LibraryViewTypeToggle(libraryViewType: $libraryViewType) + Menu { + LibraryViewTypeToggle(libraryViewType: $libraryViewType) + RandomItemButton(viewModel: viewModel) + .onSelect { response in + if let item = response.items?.first { + router.route(to: \.item, item) + } + } + } label: { + Image(systemName: "ellipsis.circle") + } } } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 013b2721..ac6a47f3 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ