diff --git a/Shared/Objects/ItemFilter/ItemFilterCollection.swift b/Shared/Objects/ItemFilter/ItemFilterCollection.swift index 317bab8a..3b3f4e9e 100644 --- a/Shared/Objects/ItemFilter/ItemFilterCollection.swift +++ b/Shared/Objects/ItemFilter/ItemFilterCollection.swift @@ -14,6 +14,7 @@ import JellyfinAPI struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable { var genres: [ItemGenre] = [] + var letter: [ItemLetter] = [] var sortBy: [ItemSortBy] = [ItemSortBy.name] var sortOrder: [ItemSortOrder] = [ItemSortOrder.ascending] var tags: [ItemTag] = [] @@ -33,6 +34,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable { /// A collection that has all statically available values static let all: ItemFilterCollection = .init( + letter: ItemLetter.allCases, sortBy: ItemSortBy.allCases, sortOrder: ItemSortOrder.allCases, traits: ItemTrait.supportedCases diff --git a/Shared/Objects/ItemFilter/ItemFilterType.swift b/Shared/Objects/ItemFilter/ItemFilterType.swift index 94d06dd0..b37ec030 100644 --- a/Shared/Objects/ItemFilter/ItemFilterType.swift +++ b/Shared/Objects/ItemFilter/ItemFilterType.swift @@ -12,6 +12,7 @@ import Foundation enum ItemFilterType: String, CaseIterable, Defaults.Serializable { case genres + case letter case sortBy case sortOrder case tags @@ -23,7 +24,7 @@ enum ItemFilterType: String, CaseIterable, Defaults.Serializable { switch self { case .genres, .tags, .traits, .years: return .multi - case .sortBy, .sortOrder: + case .letter, .sortBy, .sortOrder: return .single } } @@ -32,6 +33,8 @@ enum ItemFilterType: String, CaseIterable, Defaults.Serializable { switch self { case .genres: \ItemFilterCollection.genres.asAnyItemFilter + case .letter: + \ItemFilterCollection.letter.asAnyItemFilter case .sortBy: \ItemFilterCollection.sortBy.asAnyItemFilter case .sortOrder: @@ -52,6 +55,8 @@ extension ItemFilterType: Displayable { switch self { case .genres: L10n.genres + case .letter: + "Letter" case .sortBy: L10n.sort case .sortOrder: diff --git a/Shared/Objects/ItemFilter/ItemLetter.swift b/Shared/Objects/ItemFilter/ItemLetter.swift new file mode 100644 index 00000000..9b8156b3 --- /dev/null +++ b/Shared/Objects/ItemFilter/ItemLetter.swift @@ -0,0 +1,36 @@ +// +// 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 +import UIKit + +struct ItemLetter: CaseIterable, Codable, ExpressibleByStringLiteral, Hashable, ItemFilter { + + let value: String + + var displayTitle: String { + value + } + + init(stringLiteral value: String) { + self.value = value + } + + init(from anyFilter: AnyItemFilter) { + self.value = anyFilter.value + } + + static var allCases: [ItemLetter] { + UILocalizedIndexedCollation + .current() + .sectionTitles + .subtracting(["#"]) + .prepending("#") + .map(Self.init) + } +} diff --git a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift index 40f0021c..a76634d4 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift @@ -97,6 +97,15 @@ final class ItemLibraryViewModel: PagingLibraryViewModel { 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 diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index 4a9d3eea..ed52c742 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -136,8 +136,8 @@ class PagingLibraryViewModel: ViewModel, Eventful, Stateful { if let filterViewModel { filterViewModel.$currentFilters - .dropFirst() // prevents a refresh on subscription - .debounce(for: 0.5, scheduler: RunLoop.main) + .dropFirst() + .debounce(for: 1, scheduler: RunLoop.main) .removeDuplicates() .sink { [weak self] _ in guard let self else { return } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 5265c3b8..4a79c5ae 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -236,8 +236,6 @@ E1153D9A2BBA3E9800424D36 /* ErrorCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153D992BBA3E9800424D36 /* ErrorCard.swift */; }; E1153D9C2BBA3E9D00424D36 /* LoadingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1153D9B2BBA3E9D00424D36 /* LoadingCard.swift */; }; E1153DA42BBA614F00424D36 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DA32BBA614F00424D36 /* CollectionVGrid */; }; - E1153DA72BBA641000424D36 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DA62BBA641000424D36 /* CollectionVGrid */; }; - E1153DA92BBA642A00424D36 /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DA82BBA642A00424D36 /* CollectionVGrid */; }; E1153DAC2BBA6AD200424D36 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DAB2BBA6AD200424D36 /* CollectionHStack */; }; E1153DAF2BBA734200424D36 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DAE2BBA734200424D36 /* CollectionHStack */; }; E1153DB12BBA734C00424D36 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1153DB02BBA734C00424D36 /* CollectionHStack */; }; @@ -303,6 +301,9 @@ E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F229638B140022FAC9 /* ChevronButton.swift */; }; E12E30F5296392EC0022FAC9 /* EnumPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */; }; E12F038C28F8B0B100976CC3 /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */; }; + E132D3C82BD200C10058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3C72BD200C10058A2DF /* CollectionVGrid */; }; + E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CC2BD2179C0058A2DF /* CollectionVGrid */; }; + E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3CE2BD217AA0058A2DF /* CollectionVGrid */; }; E13316FE2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; }; E13316FF2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13316FD2ADE42B6009BF865 /* RatioCornerRadiusModifier.swift */; }; E133328829538D8D00EE76AB /* Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = E133328729538D8D00EE76AB /* Files.swift */; }; @@ -358,6 +359,8 @@ E14A08CB28E6831D004FC984 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */; }; E14CB6862A9FF62A001586C6 /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E14CB6852A9FF62A001586C6 /* JellyfinAPI */; }; E14CB6882A9FF71F001586C6 /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E14CB6872A9FF71F001586C6 /* JellyfinAPI */; }; + E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */; }; + E14E9DF22BCF7A99004E3371 /* ItemLetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */; }; E14EDEC52B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EDEC42B8FB64E000F00A4 /* AnyItemFilter.swift */; }; E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EDEC42B8FB64E000F00A4 /* AnyItemFilter.swift */; }; E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */; }; @@ -1091,6 +1094,7 @@ E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemFilter+ItemTrait.swift"; sourceTree = ""; }; E148128A28C15526003B8787 /* ItemSortBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSortBy.swift; sourceTree = ""; }; E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; + E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLetter.swift; sourceTree = ""; }; E14EDEC42B8FB64E000F00A4 /* AnyItemFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyItemFilter.swift; sourceTree = ""; }; E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemFilterType.swift; sourceTree = ""; }; E14EDECB2B8FB709000F00A4 /* ItemYear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemYear.swift; sourceTree = ""; }; @@ -1404,9 +1408,9 @@ 62666E1F27E501DF00EC0ECD /* CoreText.framework in Frameworks */, E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */, E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */, - E1153DA92BBA642A00424D36 /* CollectionVGrid in Frameworks */, E1153DD22BBB649C00424D36 /* SVGKit in Frameworks */, 62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */, + E132D3CF2BD217AA0058A2DF /* CollectionVGrid in Frameworks */, E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */, E1153DB12BBA734C00424D36 /* CollectionHStack in Frameworks */, 62666E1327E501C300EC0ECD /* AudioToolbox.framework in Frameworks */, @@ -1452,13 +1456,14 @@ 62666E0127E5016900EC0ECD /* CoreFoundation.framework in Frameworks */, E14CB6862A9FF62A001586C6 /* JellyfinAPI in Frameworks */, 62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */, + E132D3C82BD200C10058A2DF /* CollectionVGrid in Frameworks */, E18A8E7A28D5FEDF00333B9A /* VLCUI in Frameworks */, - E1153DA72BBA641000424D36 /* CollectionVGrid in Frameworks */, E114DB332B1944FA00B75FB3 /* CollectionVGrid in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, E15210562946DF1B00375CC2 /* PulseLogHandler in Frameworks */, E1153DAF2BBA734200424D36 /* CollectionHStack in Frameworks */, 62666E0427E5017500EC0ECD /* CoreText.framework in Frameworks */, + E132D3CD2BD2179C0058A2DF /* CollectionVGrid in Frameworks */, E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */, 62666E0E27E501AF00EC0ECD /* Security.framework in Frameworks */, E1DC9814296DC06200982F06 /* PulseLogHandler in Frameworks */, @@ -2420,6 +2425,7 @@ 535870AC2669D8DD00D05A09 /* ItemFilterCollection.swift */, E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */, E11BDF762B8513B40045C54A /* ItemGenre.swift */, + E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */, E148128A28C15526003B8787 /* ItemSortBy.swift */, E11BDF962B865F550045C54A /* ItemTag.swift */, E14EDECB2B8FB709000F00A4 /* ItemYear.swift */, @@ -3198,9 +3204,9 @@ E14CB6872A9FF71F001586C6 /* JellyfinAPI */, E1A7B1642B9A9F7800152546 /* PreferencesView */, E1392FEC2BA218A80034110D /* SwiftUIIntrospect */, - E1153DA82BBA642A00424D36 /* CollectionVGrid */, E1153DB02BBA734C00424D36 /* CollectionHStack */, E1153DD12BBB649C00424D36 /* SVGKit */, + E132D3CE2BD217AA0058A2DF /* CollectionVGrid */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; @@ -3251,10 +3257,11 @@ E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */, E18D6AA52BAA96F000A0D167 /* CollectionHStack */, E1153DA32BBA614F00424D36 /* CollectionVGrid */, - E1153DA62BBA641000424D36 /* CollectionVGrid */, E1153DAB2BBA6AD200424D36 /* CollectionHStack */, E1153DAE2BBA734200424D36 /* CollectionHStack */, E1153DCF2BBB634F00424D36 /* SVGKit */, + E132D3C72BD200C10058A2DF /* CollectionVGrid */, + E132D3CC2BD2179C0058A2DF /* CollectionVGrid */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; @@ -3324,9 +3331,9 @@ E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */, E14CB6842A9FF62A001586C6 /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, E15D4F032B1B0C3C00442DB8 /* XCLocalSwiftPackageReference "PreferencesView" */, - E1153DA52BBA641000424D36 /* XCRemoteSwiftPackageReference "CollectionVGrid" */, E1153DAD2BBA734200424D36 /* XCRemoteSwiftPackageReference "CollectionHStack" */, E1153DCE2BBB634F00424D36 /* XCRemoteSwiftPackageReference "SVGKit" */, + E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -3522,6 +3529,7 @@ E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */, E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */, E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */, + E14E9DF22BCF7A99004E3371 /* ItemLetter.swift in Sources */, E1C9260C2887565C002A7A66 /* MovieItemContentView.swift in Sources */, E1C9260B2887565C002A7A66 /* MovieItemView.swift in Sources */, E1E6C45629B130F50064123F /* ChapterOverlay.swift in Sources */, @@ -3865,6 +3873,7 @@ E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */, E187F7672B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, + E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */, E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, @@ -4626,14 +4635,6 @@ minimumVersion = 2.0.0; }; }; - E1153DA52BBA641000424D36 /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/LePips/CollectionVGrid"; - requirement = { - branch = main; - kind = branch; - }; - }; E1153DAD2BBA734200424D36 /* XCRemoteSwiftPackageReference "CollectionHStack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/LePips/CollectionHStack"; @@ -4650,6 +4651,14 @@ minimumVersion = 3.0.0; }; }; + E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LePips/CollectionVGrid"; + requirement = { + branch = main; + kind = branch; + }; + }; E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; @@ -4823,16 +4832,6 @@ isa = XCSwiftPackageProductDependency; productName = CollectionVGrid; }; - E1153DA62BBA641000424D36 /* CollectionVGrid */ = { - isa = XCSwiftPackageProductDependency; - package = E1153DA52BBA641000424D36 /* XCRemoteSwiftPackageReference "CollectionVGrid" */; - productName = CollectionVGrid; - }; - E1153DA82BBA642A00424D36 /* CollectionVGrid */ = { - isa = XCSwiftPackageProductDependency; - package = E1153DA52BBA641000424D36 /* XCRemoteSwiftPackageReference "CollectionVGrid" */; - productName = CollectionVGrid; - }; E1153DAB2BBA6AD200424D36 /* CollectionHStack */ = { isa = XCSwiftPackageProductDependency; productName = CollectionHStack; @@ -4862,6 +4861,20 @@ package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; + E132D3C72BD200C10058A2DF /* CollectionVGrid */ = { + isa = XCSwiftPackageProductDependency; + productName = CollectionVGrid; + }; + E132D3CC2BD2179C0058A2DF /* CollectionVGrid */ = { + isa = XCSwiftPackageProductDependency; + package = E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */; + productName = CollectionVGrid; + }; + E132D3CE2BD217AA0058A2DF /* CollectionVGrid */ = { + isa = XCSwiftPackageProductDependency; + package = E132D3CB2BD2179C0058A2DF /* XCRemoteSwiftPackageReference "CollectionVGrid" */; + productName = CollectionVGrid; + }; E1388A45293F0ABA009721B1 /* SwizzleSwift */ = { isa = XCSwiftPackageProductDependency; package = 62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */; diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9853680e..73040d4c 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "014f7f9b582fe941e86e045d02b91ba05b36a8317ab6be0cd706443a529fc2da", + "originHash" : "20f168223d2d1133c4837df32ef688f816b79816393b1172be8e57d39c47d619", "pins" : [ { "identity" : "blurhashkit", @@ -34,7 +34,7 @@ "location" : "https://github.com/LePips/CollectionVGrid", "state" : { "branch" : "main", - "revision" : "91513692e56cc564f1bcbd476289ae060eb7e877" + "revision" : "e4e0adc7722430870293e390e32d35c37a0d047b" } }, { diff --git a/Swiftfin/Views/FilterView.swift b/Swiftfin/Views/FilterView.swift index 04bc186f..c4e002ee 100644 --- a/Swiftfin/Views/FilterView.swift +++ b/Swiftfin/Views/FilterView.swift @@ -42,6 +42,8 @@ struct FilterView: View { switch type { case .genres: viewModel.currentFilters.genres = ItemFilterCollection.default.genres + case .letter: + viewModel.currentFilters.letter = ItemFilterCollection.default.letter case .sortBy: viewModel.currentFilters.sortBy = ItemFilterCollection.default.sortBy case .sortOrder: @@ -78,6 +80,8 @@ extension FilterView { switch type { case .genres: viewModel.currentFilters.genres = newValue.map(ItemGenre.init) + case .letter: + viewModel.currentFilters.letter = newValue.map(ItemLetter.init) case .sortBy: viewModel.currentFilters.sortBy = newValue.map(ItemSortBy.init) case .sortOrder: