diff --git a/Shared/Coordinators/FilterCoordinator.swift b/Shared/Coordinators/FilterCoordinator.swift index 851f57891..99b40e3d3 100644 --- a/Shared/Coordinators/FilterCoordinator.swift +++ b/Shared/Coordinators/FilterCoordinator.swift @@ -31,10 +31,6 @@ final class FilterCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { - #if os(tvOS) - AssertionFailureView("Not implemented") - #else FilterView(viewModel: parameters.viewModel, type: parameters.type) - #endif } } diff --git a/Shared/Objects/ItemFilter/ItemFilterCollection.swift b/Shared/Objects/ItemFilter/ItemFilterCollection.swift index 12cb8bd26..06823b2cb 100644 --- a/Shared/Objects/ItemFilter/ItemFilterCollection.swift +++ b/Shared/Objects/ItemFilter/ItemFilterCollection.swift @@ -44,8 +44,17 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable { traits: ItemTrait.supportedCases ) + // TODO: This is bad and inefficient var hasFilters: Bool { - self != Self.default + var selfCopy = self + let defaultCopy = Self.default + + selfCopy.itemTypes = defaultCopy.itemTypes + + return selfCopy != defaultCopy + + // Previous version: + // self != Self.default } var activeFilterCount: Int { diff --git a/Shared/ViewModels/FilterViewModel.swift b/Shared/ViewModels/FilterViewModel.swift index 94211b6f6..a96418f66 100644 --- a/Shared/ViewModels/FilterViewModel.swift +++ b/Shared/ViewModels/FilterViewModel.swift @@ -109,7 +109,8 @@ final class FilterViewModel: ViewModel, Stateful { if let type { resetCurrentFilters(for: type) } else { - currentFilters = .default + // This is exclusively for tvOS ItemType libraries + currentFilters = .init(itemTypes: currentFilters.itemTypes) } case let .update(type, filters): diff --git a/Swiftfin tvOS/Components/LibraryFilters/FilterView.swift b/Swiftfin tvOS/Components/LibraryFilters/FilterView.swift new file mode 100644 index 000000000..7406f8c1a --- /dev/null +++ b/Swiftfin tvOS/Components/LibraryFilters/FilterView.swift @@ -0,0 +1,83 @@ +// +// 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 +import SwiftUI + +// TODO: multiple filter types? +// - for sort order and sort by combined +struct FilterView: View { + + @Binding + private var selection: [AnyItemFilter] + + @EnvironmentObject + private var router: FilterCoordinator.Router + + @ObservedObject + private var viewModel: FilterViewModel + + private let type: ItemFilterType + + var body: some View { + ZStack { + BlurView() + .ignoresSafeArea() + + contentView + } + .navigationTitle(type.displayTitle) + .topBarTrailing { + Button(L10n.reset) { + viewModel.send(.reset(type)) + } + .environment( + \.isEnabled, + viewModel.isFilterSelected(type: type) + ) + } + } + + private var contentView: some View { + SplitFormWindowView() + .descriptionView { + Image(systemName: type.systemImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + } + .contentView { + SelectorView( + selection: $selection, + sources: viewModel.allFilters[keyPath: type.collectionAnyKeyPath], + type: type.selectorType + ) + } + } +} + +extension FilterView { + + init( + viewModel: FilterViewModel, + type: ItemFilterType + ) { + + let selectionBinding: Binding<[AnyItemFilter]> = Binding { + viewModel.currentFilters[keyPath: type.collectionAnyKeyPath] + } set: { newValue in + viewModel.send(.update(type, newValue)) + } + + self.init( + selection: selectionBinding, + viewModel: viewModel, + type: type + ) + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 9ce426fcc..3d30cf6d2 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; 4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; + 4E81D2B72D72BE3B00CA71CC /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E81D2B62D72BE3900CA71CC /* FilterView.swift */; }; 4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */; }; 4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */; }; 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; @@ -1382,6 +1383,7 @@ 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = ""; }; 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = ""; }; 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = ""; }; + 4E81D2B62D72BE3900CA71CC /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = ""; }; 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsCoordinator.swift; sourceTree = ""; }; 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = ""; }; @@ -2910,6 +2912,7 @@ 4EC07D3A2D6F95E60052DC2C /* LibraryFilters */ = { isa = PBXGroup; children = ( + 4E81D2B62D72BE3900CA71CC /* FilterView.swift */, 4E81D2B42D72B17100CA71CC /* FilterDrawer */, 4EEACAA32D420FEF00F1D54D /* LetterPickerBar */, ); @@ -6148,6 +6151,7 @@ 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */, 4E7315752D1485C900EA2A95 /* UserProfileImage.swift in Sources */, E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */, + 4E81D2B72D72BE3B00CA71CC /* FilterView.swift in Sources */, 4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */, E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */, E146A9DC2BE6E9BF0034DA1E /* StoredValues+User.swift in Sources */,