Skip to content

Commit 9e73af0

Browse files
committed
Trie implementation
1 parent a32adc9 commit 9e73af0

File tree

11 files changed

+143
-57
lines changed

11 files changed

+143
-57
lines changed

Shared/Coordinators/ItemEditorCoordinator.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
8181

8282
func makeAddGenre(viewModel: GenreEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
8383
NavigationViewCoordinator {
84-
AddItemComponentView(viewModel: viewModel, type: .genres)
84+
AddItemElementView(viewModel: viewModel, type: .genres)
8585
}
8686
}
8787

@@ -100,7 +100,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
100100

101101
func makeAddTag(viewModel: TagEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
102102
NavigationViewCoordinator {
103-
AddItemComponentView(viewModel: viewModel, type: .tags)
103+
AddItemElementView(viewModel: viewModel, type: .tags)
104104
}
105105
}
106106

@@ -119,7 +119,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
119119

120120
func makeAddStudio(viewModel: StudioEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
121121
NavigationViewCoordinator {
122-
AddItemComponentView(viewModel: viewModel, type: .studios)
122+
AddItemElementView(viewModel: viewModel, type: .studios)
123123
}
124124
}
125125

@@ -138,7 +138,7 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable {
138138

139139
func makeAddPeople(viewModel: PeopleEditorViewModel) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
140140
NavigationViewCoordinator {
141-
AddItemComponentView(viewModel: viewModel, type: .people)
141+
AddItemElementView(viewModel: viewModel, type: .people)
142142
}
143143
}
144144

Shared/Objects/Trie.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// Swiftfin is subject to the terms of the Mozilla Public
3+
// License, v2.0. If a copy of the MPL was not distributed with this
4+
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
//
6+
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
7+
//
8+
9+
class Trie {
10+
11+
private let root = TrieNode()
12+
13+
// MARK: - Insert Word into Trie
14+
15+
func insert(_ word: String) {
16+
guard !word.isEmpty else { return }
17+
var currentNode = root
18+
19+
for char in word.lowercased() {
20+
if currentNode.children[char] == nil {
21+
currentNode.children[char] = TrieNode()
22+
}
23+
currentNode = currentNode.children[char]!
24+
currentNode.words.append(word)
25+
}
26+
currentNode.isEndOfWord = true
27+
}
28+
29+
// MARK: - Search for Prefix Matches
30+
31+
func search(prefix: String) -> [String] {
32+
guard !prefix.isEmpty else { return [] }
33+
var currentNode = root
34+
35+
for char in prefix.lowercased() {
36+
guard let nextNode = currentNode.children[char] else {
37+
return []
38+
}
39+
currentNode = nextNode
40+
}
41+
42+
return currentNode.words
43+
}
44+
}
45+
46+
extension Trie {
47+
48+
class TrieNode {
49+
50+
var children: [Character: TrieNode] = [:]
51+
var isEndOfWord: Bool = false
52+
var words: [String] = []
53+
}
54+
}

Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import JellyfinAPI
1212

1313
class GenreEditorViewModel: ItemEditorViewModel<String> {
1414

15+
// MARK: - Populate the Trie
16+
17+
override func populateTrie() {
18+
for element in self.elements {
19+
trie.insert(element)
20+
}
21+
}
22+
1523
// MARK: - Add Genre(s)
1624

1725
override func addComponents(_ genres: [String]) async throws {
@@ -52,15 +60,13 @@ class GenreEditorViewModel: ItemEditorViewModel<String> {
5260
}
5361
}
5462

55-
// MARK: - Get Genres Matches from Population
63+
// MARK: - Search For Matching Genres
5664

5765
override func searchElements(_ searchTerm: String) async throws -> [String] {
58-
guard !searchTerm.isEmpty else {
59-
return []
60-
}
66+
guard !searchTerm.isEmpty else { return [] }
6167

62-
return self.elements.filter {
63-
$0.range(of: searchTerm, options: .caseInsensitive) != nil
64-
}
68+
var items = trie.search(prefix: searchTerm)
69+
70+
return trie.search(prefix: searchTerm)
6571
}
6672
}

Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,17 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
5151

5252
@Published
5353
var backgroundStates: OrderedSet<BackgroundState> = []
54-
5554
@Published
5655
var item: BaseItemDto
5756
@Published
5857
var elements: [Element] = []
5958
@Published
6059
var matches: [Element] = []
61-
6260
@Published
6361
var state: State = .initial
6462

63+
var trie = Trie()
64+
6565
private var loadTask: AnyCancellable?
6666
private var updateTask: AnyCancellable?
6767
private var searchTask: AnyCancellable?
@@ -73,10 +73,11 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
7373
eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher()
7474
}
7575

76-
// MARK: - Init
76+
// MARK: - Initializer
7777

7878
init(item: BaseItemDto) {
7979
self.item = item
80+
8081
super.init()
8182

8283
setupSearchDebounce()
@@ -123,6 +124,9 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
123124

124125
_ = self.backgroundStates.remove(.loading)
125126
}
127+
128+
populateTrie()
129+
126130
} catch {
127131
let apiError = JellyfinAPIError(error.localizedDescription)
128132
await MainActor.run {
@@ -255,6 +259,12 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
255259
}
256260
}
257261

262+
// MARK: - Populate the Trie
263+
264+
func populateTrie() {
265+
fatalError("This method should be overridden in subclasses")
266+
}
267+
258268
// MARK: - Add Element Component to Item (To Be Overridden)
259269

260270
func addComponents(_ components: [Element]) async throws {
@@ -279,7 +289,7 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
279289
fatalError("This method should be overridden in subclasses")
280290
}
281291

282-
// MARK: - Search for Matches in the Element Population (To Be Overridden)
292+
// MARK: - Return Matching Elements (To Be Overridden)
283293

284294
func searchElements(_ searchTerm: String) async throws -> [Element] {
285295
fatalError("This method should be overridden in subclasses")

Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ import JellyfinAPI
1212

1313
class PeopleEditorViewModel: ItemEditorViewModel<BaseItemPerson> {
1414

15+
// MARK: - Populate the Trie
16+
17+
override func populateTrie() {
18+
for element in self.elements {
19+
if let name = element.name {
20+
trie.insert(name)
21+
}
22+
}
23+
}
24+
1525
// MARK: - Add People(s)
1626

1727
override func addComponents(_ people: [BaseItemPerson]) async throws {
@@ -54,20 +64,13 @@ class PeopleEditorViewModel: ItemEditorViewModel<BaseItemPerson> {
5464
}
5565
}
5666

57-
// MARK: - Get People Matches from Population
67+
// MARK: - Search For Matching People
5868

5969
override func searchElements(_ searchTerm: String) async throws -> [BaseItemPerson] {
60-
guard !searchTerm.isEmpty else {
61-
return []
62-
}
70+
guard !searchTerm.isEmpty else { return [] }
6371

64-
return self.elements.compactMap {
65-
guard let name = $0.name,
66-
name.range(of: searchTerm, options: .caseInsensitive) != nil
67-
else {
68-
return nil
69-
}
70-
return BaseItemPerson(id: $0.id, name: name)
71-
}
72+
let matchingItems = Set(trie.search(prefix: searchTerm))
73+
74+
return elements.filter { matchingItems.contains($0.name!) }
7275
}
7376
}

Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ import JellyfinAPI
1212

1313
class StudioEditorViewModel: ItemEditorViewModel<NameGuidPair> {
1414

15+
// MARK: - Populate the Trie
16+
17+
override func populateTrie() {
18+
for element in self.elements {
19+
if let name = element.name {
20+
trie.insert(name)
21+
}
22+
}
23+
}
24+
1525
// MARK: - Add Studio(s)
1626

1727
override func addComponents(_ studios: [NameGuidPair]) async throws {
@@ -54,20 +64,13 @@ class StudioEditorViewModel: ItemEditorViewModel<NameGuidPair> {
5464
}
5565
}
5666

57-
// MARK: - Get Studio Matches from Population
67+
// MARK: - Search For Matching Studios
5868

5969
override func searchElements(_ searchTerm: String) async throws -> [NameGuidPair] {
60-
guard !searchTerm.isEmpty else {
61-
return []
62-
}
70+
guard !searchTerm.isEmpty else { return [] }
6371

64-
return self.elements.compactMap {
65-
guard let name = $0.name,
66-
name.range(of: searchTerm, options: .caseInsensitive) != nil
67-
else {
68-
return nil
69-
}
70-
return NameGuidPair(id: $0.id, name: name)
71-
}
72+
let matchingItems = Set(trie.search(prefix: searchTerm))
73+
74+
return elements.filter { matchingItems.contains($0.name!) }
7275
}
7376
}

Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import JellyfinAPI
1212

1313
class TagEditorViewModel: ItemEditorViewModel<String> {
1414

15+
// MARK: - Populate the Trie
16+
17+
override func populateTrie() {
18+
for element in self.elements {
19+
trie.insert(element)
20+
}
21+
}
22+
1523
// MARK: - Add Tag(s)
1624

1725
override func addComponents(_ tags: [String]) async throws {
@@ -49,15 +57,11 @@ class TagEditorViewModel: ItemEditorViewModel<String> {
4957
return response.value.tags ?? []
5058
}
5159

52-
// MARK: - Get Tag Matches from Population
60+
// MARK: - Search For Matching Tags
5361

5462
override func searchElements(_ searchTerm: String) async throws -> [String] {
55-
guard !searchTerm.isEmpty else {
56-
return []
57-
}
63+
guard !searchTerm.isEmpty else { return [] }
5864

59-
return self.elements.filter {
60-
$0.range(of: searchTerm, options: .caseInsensitive) != nil
61-
}
65+
return trie.search(prefix: searchTerm)
6266
}
6367
}

Swiftfin.xcodeproj/project.pbxproj

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
/* Begin PBXBuildFile section */
1010
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
1111
091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; };
12+
4E01446C2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
13+
4E01446D2D0292E200193038 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E01446B2D0292E000193038 /* Trie.swift */; };
1214
4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0195E32CE04678007844F4 /* ItemSection.swift */; };
1315
4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; };
1416
4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */; };
@@ -87,7 +89,7 @@
8789
4E5071D82CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */; };
8890
4E5071DA2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; };
8991
4E5071DB2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; };
90-
4E5071E42CFCEFD3003FA2AD /* AddItemComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071E32CFCEFD1003FA2AD /* AddItemComponentView.swift */; };
92+
4E5071E42CFCEFD3003FA2AD /* AddItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */; };
9193
4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; };
9294
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
9395
4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; };
@@ -1142,6 +1144,7 @@
11421144

11431145
/* Begin PBXFileReference section */
11441146
091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = "<group>"; };
1147+
4E01446B2D0292E000193038 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
11451148
4E0195E32CE04678007844F4 /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; };
11461149
4E026A8A2CE804E7005471B5 /* ResetUserPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetUserPasswordView.swift; sourceTree = "<group>"; };
11471150
4E0A8FFA2CAF74CD0014B047 /* TaskCompletionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCompletionStatus.swift; sourceTree = "<group>"; };
@@ -1197,7 +1200,7 @@
11971200
4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleEditorViewModel.swift; sourceTree = "<group>"; };
11981201
4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagEditorViewModel.swift; sourceTree = "<group>"; };
11991202
4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreEditorViewModel.swift; sourceTree = "<group>"; };
1200-
4E5071E32CFCEFD1003FA2AD /* AddItemComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemComponentView.swift; sourceTree = "<group>"; };
1203+
4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemElementView.swift; sourceTree = "<group>"; };
12011204
4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
12021205
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
12031206
4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = "<group>"; };
@@ -2214,7 +2217,7 @@
22142217
4E5071E22CFCEFC3003FA2AD /* AddItemElementView */ = {
22152218
isa = PBXGroup;
22162219
children = (
2217-
4E5071E32CFCEFD1003FA2AD /* AddItemComponentView.swift */,
2220+
4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */,
22182221
4E31EF972CFFB9B70053DFE7 /* Components */,
22192222
);
22202223
path = AddItemElementView;
@@ -2784,6 +2787,7 @@
27842787
E1E306CC28EF6E8000537998 /* TimerProxy.swift */,
27852788
E129428F28F0BDC300796AC6 /* TimeStampType.swift */,
27862789
E1C8CE7B28FF015000DF5D7B /* TrailingTimestampType.swift */,
2790+
4E01446B2D0292E000193038 /* Trie.swift */,
27872791
E1EA09682BED78BB004CDE76 /* UserAccessPolicy.swift */,
27882792
DFB7C3DE2C7AA42700CE7CDC /* UserSignInState.swift */,
27892793
E1D8429229340B8300D1041A /* Utilities.swift */,
@@ -4999,6 +5003,7 @@
49995003
E1549661296CA2EF00C4EF88 /* SwiftfinDefaults.swift in Sources */,
50005004
E158C8D12A31947500C527C5 /* MediaSourceInfoView.swift in Sources */,
50015005
E11BDF782B8513B40045C54A /* ItemGenre.swift in Sources */,
5006+
4E01446C2D0292E200193038 /* Trie.swift in Sources */,
50025007
E14EA16A2BF7333B00DE757A /* UserProfileImageViewModel.swift in Sources */,
50035008
4EBE06542C7ED0E1004A6C03 /* DeviceProfile.swift in Sources */,
50045009
E1575E98293E7B1E001665B1 /* UIApplication.swift in Sources */,
@@ -5366,7 +5371,7 @@
53665371
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
53675372
E1B4E4382CA7795200DC49DE /* OrderedDictionary.swift in Sources */,
53685373
E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */,
5369-
4E5071E42CFCEFD3003FA2AD /* AddItemComponentView.swift in Sources */,
5374+
4E5071E42CFCEFD3003FA2AD /* AddItemElementView.swift in Sources */,
53705375
E16AA60828A364A6009A983C /* PosterButton.swift in Sources */,
53715376
E1E1644128BB301900323B0A /* Array.swift in Sources */,
53725377
E18CE0AF28A222240092E7F1 /* PublicUserRow.swift in Sources */,
@@ -5569,6 +5574,7 @@
55695574
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */,
55705575
E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */,
55715576
E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */,
5577+
4E01446D2D0292E200193038 /* Trie.swift in Sources */,
55725578
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */,
55735579
4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */,
55745580
E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */,

0 commit comments

Comments
 (0)