[iOS & tvOS] ItemLibraryViewModel - Cleanup (#1411)

* Move ItemType to Filter

* Init but normally...

* filter on people?

* Default to easiest / least change solution.

* Reset `.collectionFolder`, `.folder`, and `.BaseItemPerson` in `PagingLibraryView` to have the default filters. This was originally in place. This Commit just ensures that iOS and tvOS have the same implementation.

* wip

* Update ItemLibraryViewModel.swift

* Update ItemLibraryViewModel.swift

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-02-02 10:17:46 -07:00 committed by GitHub
parent 21cf7865c3
commit 3ee2abec5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 126 additions and 225 deletions

View File

@ -48,9 +48,8 @@ final class MainTabCoordinator: TabCoordinatable {
}
func makeTVShows() -> NavigationViewCoordinator<LibraryCoordinator<BaseItemDto>> {
let viewModel = ItemTypeLibraryViewModel(
itemTypes: [.series],
filters: .default
let viewModel = ItemLibraryViewModel(
filters: .init(itemTypes: [.series])
)
return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel))
}
@ -65,9 +64,8 @@ final class MainTabCoordinator: TabCoordinatable {
}
func makeMovies() -> NavigationViewCoordinator<LibraryCoordinator<BaseItemDto>> {
let viewModel = ItemTypeLibraryViewModel(
itemTypes: [.movie],
filters: .default
let viewModel = ItemLibraryViewModel(
filters: .init(itemTypes: [.movie])
)
return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel))
}

View File

@ -22,6 +22,7 @@ extension BaseItemDto: Displayable {
}
extension BaseItemDto: LibraryParent {
var libraryType: BaseItemKind? {
type
}

View File

@ -0,0 +1,31 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
extension BaseItemKind: SupportedCaseIterable {
/// The base supported cases for media navigation.
/// This differs from media viewing, which may include
/// `.episode`.
///
/// These is the *base* supported cases and other objects
/// like `LibararyParent` may have additional supported
/// cases for querying a library.
static var supportedCases: [BaseItemKind] {
[.movie, .series, .boxSet]
}
}
extension BaseItemKind: ItemFilter {
// TODO: localize
var displayTitle: String {
rawValue
}
}

View File

@ -17,6 +17,7 @@ extension BaseItemPerson: Displayable {
}
extension BaseItemPerson: LibraryParent {
var libraryType: BaseItemKind? {
.person
}

View File

@ -14,6 +14,7 @@ import JellyfinAPI
struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
var genres: [ItemGenre] = []
var itemTypes: [BaseItemKind] = []
var letter: [ItemLetter] = []
var sortBy: [ItemSortBy] = [ItemSortBy.name]
var sortOrder: [ItemSortOrder] = [ItemSortOrder.ascending]
@ -32,7 +33,10 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
sortOrder: [ItemSortOrder.descending]
)
/// A collection that has all statically available values
/// A collection that has all statically available values.
///
/// These may be altered when used to better represent all
/// available values within the current context.
static let all: ItemFilterCollection = .init(
letter: ItemLetter.allCases,
sortBy: ItemSortBy.allCases,

View File

@ -6,17 +6,58 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
protocol LibraryParent: Displayable, Hashable, Identifiable<String?> {
// 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
// this protocol to be removed entirely and replace just with
// a concrete `BaseItemDto`
//
// edit: studios also implement `LibraryParent` - reconsider above comment
/// The type of the library, reusing `BaseItemKind` for some
/// ease of provided variety like `folder` and `userView`.
var libraryType: BaseItemKind? { get }
/// The `BaseItemKind` types that this library parent
/// support. Mainly used for `.folder` support.
///
/// When using filters, this is used to determine the initial
/// set of supported types and then
var supportedItemTypes: [BaseItemKind] { get }
/// Modifies the parameters for the items request per this library parent.
func setParentParameters(_ parameters: Paths.GetItemsByUserIDParameters) -> Paths.GetItemsByUserIDParameters
}
extension LibraryParent {
var supportedItemTypes: [BaseItemKind] {
switch libraryType {
case .folder:
BaseItemKind.supportedCases
.appending([.folder, .collectionFolder])
default:
BaseItemKind.supportedCases
}
}
func setParentParameters(_ parameters: Paths.GetItemsByUserIDParameters) -> Paths.GetItemsByUserIDParameters {
guard let id else { return parameters }
var parameters = parameters
parameters.isRecursive = true
parameters.includeItemTypes = supportedItemTypes
switch libraryType {
case .collectionFolder, .userView:
parameters.parentID = id
case .folder:
parameters.parentID = id
parameters.isRecursive = nil
case .person:
parameters.personIDs = [id]
case .studio:
parameters.studioIDs = [id]
default: ()
}
return parameters
}
}

View File

@ -19,26 +19,18 @@ final class FilterViewModel: ViewModel {
var allFilters: ItemFilterCollection = .all
private let parent: (any LibraryParent)?
private let itemTypes: [BaseItemKind]?
init(
parent: (any LibraryParent)? = nil,
currentFilters: ItemFilterCollection = .default
) {
self.parent = parent
self.itemTypes = nil
self.currentFilters = currentFilters
super.init()
}
init(
itemTypes: [BaseItemKind],
currentFilters: ItemFilterCollection = .default
) {
self.parent = nil
self.itemTypes = itemTypes
self.currentFilters = currentFilters
super.init()
if let parent {
self.allFilters.itemTypes = parent.supportedItemTypes
}
}
/// Sets the query filters from the parent
@ -53,10 +45,10 @@ final class FilterViewModel: ViewModel {
}
private func getQueryFilters() async -> (genres: [ItemGenre], tags: [ItemTag], years: [ItemYear]) {
let parameters = Paths.GetQueryFiltersLegacyParameters(
userID: userSession.user.id,
parentID: parent?.id as? String,
includeItemTypes: itemTypes
parentID: parent?.id
)
let request = Paths.getQueryFiltersLegacy(parameters: parameters)

View File

@ -48,42 +48,23 @@ final class ItemLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
// MARK: item parameters
func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters {
var libraryID: String?
var personIDs: [String]?
var studioIDs: [String]?
var includeItemTypes: [BaseItemKind] = [.movie, .series, .boxSet]
var isRecursive: Bool? = true
// TODO: this logic should be moved to a `LibraryParent` function
// that transforms a `GetItemsByUserIDParameters` struct, instead
// of having to do this case-by-case.
if let libraryType = parent?.libraryType, let id = parent?.id {
switch libraryType {
case .collectionFolder, .userView:
libraryID = id
case .folder:
libraryID = id
isRecursive = nil
includeItemTypes = [.movie, .series, .boxSet, .folder, .collectionFolder]
case .person:
personIDs = [id]
case .studio:
studioIDs = [id]
default: ()
}
}
private func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters {
var parameters = Paths.GetItemsByUserIDParameters()
parameters.enableUserData = true
parameters.fields = .MinimumFields
parameters.includeItemTypes = includeItemTypes
parameters.isRecursive = isRecursive
parameters.parentID = libraryID
parameters.personIDs = personIDs
parameters.studioIDs = studioIDs
// Default values, expected to be overridden
// by parent or filters
parameters.includeItemTypes = BaseItemKind.supportedCases
parameters.sortOrder = [.ascending]
parameters.sortBy = [ItemSortBy.name.rawValue]
// Parent
if let parent {
parameters = parent.setParentParameters(parameters)
}
// Page size
if let page {
@ -101,6 +82,12 @@ final class ItemLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
parameters.tags = filters.tags.map(\.value)
parameters.years = filters.years.compactMap { Int($0.value) }
// Only set filtering on item types if selected, where
// supported values should have been set by the parent.
if filters.itemTypes.isNotEmpty {
parameters.includeItemTypes = filters.itemTypes
}
if filters.letter.first?.value == "#" {
parameters.nameLessThan = "A"
} else {

View File

@ -1,105 +0,0 @@
//
// 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) 2025 Jellyfin & Jellyfin Contributors
//
import Combine
import Foundation
import Get
import JellyfinAPI
// TODO: filtering on `itemTypes` should be moved to `ItemFilterCollection`,
// but there is additional logic based on the parent type, mainly `.folder`.
final class ItemTypeLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
let itemTypes: [BaseItemKind]
// MARK: Initializer
init(
itemTypes: [BaseItemKind],
filters: ItemFilterCollection? = nil
) {
self.itemTypes = itemTypes
super.init(
itemTypes: itemTypes,
filters: filters
)
}
// MARK: Get Page
override func get(page: Int) async throws -> [BaseItemDto] {
let parameters = itemParameters(for: page)
let request = Paths.getItemsByUserID(userID: userSession.user.id, parameters: parameters)
let response = try await userSession.client.send(request)
return response.value.items ?? []
}
// MARK: Item Parameters
func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters {
var parameters = Paths.GetItemsByUserIDParameters()
parameters.enableUserData = true
parameters.fields = .MinimumFields
parameters.includeItemTypes = itemTypes
parameters.isRecursive = true
// Page size
if let page {
parameters.limit = pageSize
parameters.startIndex = page * pageSize
}
// Filters
if let filterViewModel {
let filters = filterViewModel.currentFilters
parameters.filters = filters.traits
parameters.genres = filters.genres.map(\.value)
parameters.sortBy = filters.sortBy.map(\.rawValue)
parameters.sortOrder = filters.sortOrder
parameters.tags = filters.tags.map(\.value)
parameters.years = filters.years.compactMap { Int($0.value) }
if filters.letter.first?.value == "#" {
parameters.nameLessThan = "A"
} else {
parameters.nameStartsWith = filters.letter
.map(\.value)
.filter { $0 != "#" }
.first
}
// Random sort won't take into account previous items, so
// manual exclusion is necessary. This could possibly be
// a performance issue for loading pages after already loading
// many items, but there's nothing we can do about that.
if filters.sortBy.first == ItemSortBy.random {
parameters.excludeItemIDs = elements.compactMap(\.id)
}
}
return parameters
}
// MARK: Get Random Item
override func getRandomItem() async -> BaseItemDto? {
var parameters = itemParameters(for: nil)
parameters.limit = 1
parameters.sortBy = [ItemSortBy.random.rawValue]
let request = Paths.getItemsByUserID(userID: userSession.user.id, parameters: parameters)
let response = try? await userSession.client.send(request)
return response?.value.items?.first
}
}

View File

@ -173,18 +173,15 @@ class PagingLibraryViewModel<Element: Poster>: ViewModel, Eventful, Stateful {
self.pageSize = pageSize
self.parent = parent
if let filters {
var filters = filters
if var filters {
if let id = parent?.id, Defaults[.Customization.Library.rememberSort] {
// TODO: see `StoredValues.User.libraryFilters` for TODO
// on remembering other filters
let storedFilters = StoredValues[.User.libraryFilters(parentID: id)]
filters = filters
.mutating(\.sortBy, with: storedFilters.sortBy)
.mutating(\.sortOrder, with: storedFilters.sortOrder)
filters.sortBy = storedFilters.sortBy
filters.sortOrder = storedFilters.sortOrder
}
self.filterViewModel = .init(
@ -220,52 +217,6 @@ class PagingLibraryViewModel<Element: Poster>: ViewModel, Eventful, Stateful {
}
}
// paging item type
init(
itemTypes: [BaseItemKind],
filters: ItemFilterCollection? = nil,
pageSize: Int = DefaultPageSize
) {
self.elements = IdentifiedArray([], id: \.unwrappedIDHashOrZero, uniquingIDsWith: { x, _ in x })
self.isStatic = false
self.pageSize = pageSize
self.parent = nil
if let filters {
self.filterViewModel = .init(
itemTypes: itemTypes,
currentFilters: filters
)
} else {
self.filterViewModel = nil
}
super.init()
Notifications[.didDeleteItem]
.publisher
.sink { id in
self.elements.remove(id: id.hashValue)
}
.store(in: &cancellables)
if let filterViewModel {
filterViewModel.$currentFilters
.dropFirst()
.debounce(for: 1, scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] _ in
guard let self else { return }
Task { @MainActor in
self.send(.refresh)
}
}
.store(in: &cancellables)
}
}
convenience init(
title: String,
id: String?,

View File

@ -102,7 +102,7 @@ struct PagingLibraryView<Element: Poster & Identifiable>: View {
private func select(item: BaseItemDto) {
switch item.type {
case .collectionFolder, .folder:
let viewModel = ItemLibraryViewModel(parent: item)
let viewModel = ItemLibraryViewModel(parent: item, filters: .default)
router.route(to: \.library, viewModel)
case .person:
let viewModel = ItemLibraryViewModel(parent: item)

View File

@ -399,7 +399,6 @@
BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */; };
BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */; };
BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */; };
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; };
C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */; };
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */; };
@ -588,6 +587,8 @@
E133328F2953B71000EE76AB /* DownloadTaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328E2953B71000EE76AB /* DownloadTaskView.swift */; };
E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13332902953B91000EE76AB /* DownloadTaskCoordinator.swift */; };
E13332942953BAA100EE76AB /* DownloadTaskContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13332932953BAA100EE76AB /* DownloadTaskContentView.swift */; };
E1343DAD2D4EE4C8003145A8 /* BaseItemKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1343DAC2D4EE4C8003145A8 /* BaseItemKind.swift */; };
E1343DAE2D4EE4C8003145A8 /* BaseItemKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1343DAC2D4EE4C8003145A8 /* BaseItemKind.swift */; };
E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1356E0129A7309D00382563 /* SeparatorHStack.swift */; };
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1356E0129A7309D00382563 /* SeparatorHStack.swift */; };
E1366A222C826DA700A36DED /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; };
@ -960,7 +961,6 @@
E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; };
E1A5056B2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; };
E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; };
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; };
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; };
E1A8FDEC2C0574A800D0A51C /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A8FDEB2C0574A800D0A51C /* ListRow.swift */; };
@ -1535,7 +1535,6 @@
BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserBottomBar.swift; sourceTree = "<group>"; };
BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityView.swift; sourceTree = "<group>"; };
BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsView.swift; sourceTree = "<group>"; };
C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = "<group>"; };
C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = "<group>"; };
C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = "<group>"; };
C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveVideoPlayerManager.swift; sourceTree = "<group>"; };
@ -1664,6 +1663,7 @@
E133328E2953B71000EE76AB /* DownloadTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskView.swift; sourceTree = "<group>"; };
E13332902953B91000EE76AB /* DownloadTaskCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskCoordinator.swift; sourceTree = "<group>"; };
E13332932953BAA100EE76AB /* DownloadTaskContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadTaskContentView.swift; sourceTree = "<group>"; };
E1343DAC2D4EE4C8003145A8 /* BaseItemKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemKind.swift; sourceTree = "<group>"; };
E1356E0129A7309D00382563 /* SeparatorHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorHStack.swift; sourceTree = "<group>"; };
E1388A40293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = "<group>"; };
E1388A41293F0AAD009721B1 /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
@ -4612,6 +4612,7 @@
children = (
4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */,
E1D37F5B2B9CF02600343D2B /* BaseItemDto */,
E1343DAC2D4EE4C8003145A8 /* BaseItemKind.swift */,
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
E1002B632793CEE700E47059 /* ChapterInfo.swift */,
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */,
@ -4979,7 +4980,6 @@
isa = PBXGroup;
children = (
62E632DF267D30CA0063E547 /* ItemLibraryViewModel.swift */,
C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */,
E1ED91142B95897500802036 /* LatestInLibraryViewModel.swift */,
E12CC1AD28D0FAEA00678D5D /* NextUpLibraryViewModel.swift */,
E111D8F428D03B7500400001 /* PagingLibraryViewModel.swift */,
@ -5449,7 +5449,6 @@
E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */,
4E13FAD82D18D5AF007785F6 /* ImageInfo.swift in Sources */,
E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */,
C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */,
E13D98EE2D0664C1005FE96D /* NotificationSet.swift in Sources */,
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */,
E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */,
@ -5510,6 +5509,7 @@
E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */,
E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */,
4E661A222CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */,
E1343DAD2D4EE4C8003145A8 /* BaseItemKind.swift in Sources */,
E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */,
E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */,
E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */,
@ -6268,6 +6268,7 @@
E1DA654C28E69B0500592A73 /* SpecialFeatureType.swift in Sources */,
E11CEB8B28998552003E74C7 /* View-iOS.swift in Sources */,
E10B1ECD2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */,
E1343DAE2D4EE4C8003145A8 /* BaseItemKind.swift in Sources */,
E1401CA92938140700E8B599 /* DarkAppIcon.swift in Sources */,
E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */,
@ -6343,7 +6344,6 @@
BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */,
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */,
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */,
4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */,
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,

View File

@ -72,7 +72,7 @@ extension FilterView {
type: ItemFilterType
) {
let selectionBinding = Binding {
let selectionBinding: Binding<[AnyItemFilter]> = Binding {
viewModel.currentFilters[keyPath: type.collectionAnyKeyPath]
} set: { newValue in
switch type {