Shuffle play (#816)

This commit is contained in:
William Martineau 2023-09-29 00:37:17 -04:00 committed by GitHub
parent a2f9da506c
commit eb99dfe30b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 187 additions and 81 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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 = "<group>"; };
4E8B34E92AB91B6E0018F305 /* FilterDrawerSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterDrawerSelection.swift; sourceTree = "<group>"; };
4EAA35BA2AB9699B00D840DD /* FilterDrawerButtonSelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterDrawerButtonSelectorView.swift; sourceTree = "<group>"; };
4F1282B02A7F3E8F005BCA29 /* RandomItemButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomItemButton.swift; sourceTree = "<group>"; };
531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainNavigationLinkButton.swift; sourceTree = "<group>"; };
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = "<group>"; };
@ -1817,6 +1819,7 @@
E1581E26291EF59800D6C640 /* SplitContentView.swift */,
E157562F29355B7900976E1F /* UpdateView.swift */,
E192607F28D28AAD002314B4 /* UserProfileButton.swift */,
4F1282B02A7F3E8F005BCA29 /* RandomItemButton.swift */,
);
path = Components;
sourceTree = "<group>";
@ -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 */,

View File

@ -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")
}
}
}

View File

@ -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)
}
}

View File

@ -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")
}
}
}
}

View File

@ -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")
}
}
}
}