Skip to content

Commit 7a37824

Browse files
committed
feat: unified data source
Signed-off-by: 82Flex <[email protected]>
1 parent f55c06f commit 7a37824

File tree

3 files changed

+78
-80
lines changed

3 files changed

+78
-80
lines changed

TrollFools/AppListView.swift

Lines changed: 70 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Lessica on 2024/7/19.
66
//
77

8+
import Combine
89
import SwiftUI
910

1011
final class App: Identifiable, ObservableObject {
@@ -51,17 +52,54 @@ final class App: Identifiable, ObservableObject {
5152

5253
final class AppListModel: ObservableObject {
5354
static let shared = AppListModel()
55+
private var _allApplications: [App] = []
56+
57+
@Published var filter = FilterOptions()
58+
@Published var userApplications: [App] = []
59+
@Published var trollApplications: [App] = []
60+
@Published var appleApplications: [App] = []
5461

55-
@Published var allApplications: [App] = []
5662
@Published var hasTrollRecorder: Bool = false
5763
@Published var unsupportedCount: Int = 0
5864

65+
private var cancellables = Set<AnyCancellable>()
66+
5967
private init() {
60-
refresh()
68+
reload()
69+
70+
filter.$searchKeyword
71+
.combineLatest(filter.$showPatchedOnly)
72+
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
73+
.sink { _ in
74+
withAnimation {
75+
self.performFilter()
76+
}
77+
}
78+
.store(in: &cancellables)
6179
}
6280

63-
func refresh() {
64-
self.allApplications = Self.fetchApplications(&hasTrollRecorder, &unsupportedCount)
81+
func reload() {
82+
let allApplications = Self.fetchApplications(&hasTrollRecorder, &unsupportedCount)
83+
self._allApplications = allApplications
84+
performFilter()
85+
}
86+
87+
func performFilter() {
88+
var filteredApplications = _allApplications
89+
90+
if !filter.searchKeyword.isEmpty {
91+
filteredApplications = filteredApplications.filter {
92+
$0.name.localizedCaseInsensitiveContains(filter.searchKeyword) || $0.id.localizedCaseInsensitiveContains(filter.searchKeyword)
93+
}
94+
}
95+
96+
if filter.showPatchedOnly {
97+
filteredApplications = filteredApplications.filter { $0.isInjected }
98+
}
99+
100+
userApplications = filteredApplications.filter { $0.isUser }
101+
trollApplications = filteredApplications.filter { $0.isFromTroll }
102+
appleApplications = filteredApplications.filter { $0.isFromApple }
65103
}
66104

67105
private static let excludedIdentifiers: Set<String> = [
@@ -125,23 +163,27 @@ final class AppListModel: ObservableObject {
125163
}
126164
}
127165

128-
final class SearchOptions: ObservableObject {
129-
@Published var keyword = ""
166+
final class FilterOptions: ObservableObject {
167+
@Published var searchKeyword = ""
168+
@Published var showPatchedOnly = false
169+
170+
var isSearching: Bool { !searchKeyword.isEmpty }
130171

131172
func reset() {
132-
keyword = ""
173+
searchKeyword = ""
174+
showPatchedOnly = false
133175
}
134176
}
135177

136178
struct AppListCell: View {
137179
@StateObject var app: App
138-
@EnvironmentObject var searchOptions: SearchOptions
180+
@EnvironmentObject var filter: FilterOptions
139181

140182
@available(iOS 15.0, *)
141183
var highlightedName: AttributedString {
142184
let name = app.name
143185
var attributedString = AttributedString(name)
144-
if let range = attributedString.range(of: searchOptions.keyword, options: [.caseInsensitive, .diacriticInsensitive]) {
186+
if let range = attributedString.range(of: filter.searchKeyword, options: [.caseInsensitive, .diacriticInsensitive]) {
145187
attributedString[range].foregroundColor = .accentColor
146188
}
147189
return attributedString
@@ -151,7 +193,7 @@ struct AppListCell: View {
151193
var highlightedId: AttributedString {
152194
let id = app.id
153195
var attributedString = AttributedString(id)
154-
if let range = attributedString.range(of: searchOptions.keyword, options: [.caseInsensitive, .diacriticInsensitive]) {
196+
if let range = attributedString.range(of: filter.searchKeyword, options: [.caseInsensitive, .diacriticInsensitive]) {
155197
attributedString[range].foregroundColor = .accentColor
156198
}
157199
return attributedString
@@ -206,11 +248,6 @@ struct AppListCell: View {
206248
struct AppListView: View {
207249
@StateObject var vm = AppListModel.shared
208250

209-
@State var showPatchedOnly = false
210-
@State var searchResults: [App] = []
211-
212-
@StateObject var searchOptions = SearchOptions()
213-
214251
var appNameString: String {
215252
Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "TrollFools"
216253
}
@@ -230,46 +267,17 @@ struct AppListView: View {
230267

231268
let repoURL = URL(string: "https://github.com/Lessica/TrollFools")
232269

233-
var isSearching: Bool {
234-
return !searchOptions.keyword.isEmpty
235-
}
236-
237-
var filteredApps: [App] {
238-
if showPatchedOnly {
239-
(isSearching ? searchResults : vm.allApplications)
240-
.filter { $0.isInjected }
241-
} else {
242-
isSearching ? searchResults : vm.allApplications
243-
}
244-
}
245-
246-
var filteredUserApps: [App] {
247-
filteredApps.filter { $0.isUser }
248-
}
249-
250-
var filteredSystemApps: [App] {
251-
filteredApps.filter { $0.isSystem }
252-
}
253-
254-
var filteredTrollApps: [App] {
255-
filteredSystemApps.filter { !$0.id.hasPrefix("com.apple.") }
256-
}
257-
258-
var filteredAppleApps: [App] {
259-
filteredSystemApps.filter { $0.id.hasPrefix("com.apple.") }
260-
}
261-
262270
func filteredAppList(_ apps: [App]) -> some View {
263271
ForEach(apps, id: \.id) { app in
264272
NavigationLink {
265273
OptionView(app)
266274
} label: {
267275
if #available(iOS 16.0, *) {
268276
AppListCell(app: app)
269-
.environmentObject(searchOptions)
277+
.environmentObject(vm.filter)
270278
} else {
271279
AppListCell(app: app)
272-
.environmentObject(searchOptions)
280+
.environmentObject(vm.filter)
273281
.padding(.vertical, 4)
274282
}
275283
}
@@ -296,33 +304,33 @@ struct AppListView: View {
296304
var appList: some View {
297305
List {
298306
Section {
299-
filteredAppList(filteredUserApps)
307+
filteredAppList(vm.userApplications)
300308
} header: {
301309
Text(NSLocalizedString("User Applications", comment: ""))
302310
.font(.footnote)
303311
} footer: {
304-
if !isSearching && !showPatchedOnly && vm.unsupportedCount > 0 {
312+
if !vm.filter.isSearching && !vm.filter.showPatchedOnly && vm.unsupportedCount > 0 {
305313
Text(String(format: NSLocalizedString("And %d more unsupported user applications.", comment: ""), vm.unsupportedCount))
306314
.font(.footnote)
307315
}
308316
}
309317

310318
Section {
311-
filteredAppList(filteredTrollApps)
319+
filteredAppList(vm.trollApplications)
312320
} header: {
313321
Text(NSLocalizedString("TrollStore Applications", comment: ""))
314322
.font(.footnote)
315323
}
316324

317325
Section {
318-
filteredAppList(filteredAppleApps)
326+
filteredAppList(vm.appleApplications)
319327
} header: {
320328
Text(NSLocalizedString("Injectable System Applications", comment: ""))
321329
.font(.footnote)
322330
} footer: {
323-
if !isSearching {
331+
if !vm.filter.isSearching {
324332
VStack(alignment: .leading, spacing: 20) {
325-
if !showPatchedOnly {
333+
if !vm.filter.showPatchedOnly {
326334
Text(NSLocalizedString("Only removable system applications are eligible and listed.", comment: ""))
327335
.font(.footnote)
328336
}
@@ -343,14 +351,16 @@ struct AppListView: View {
343351
.toolbar {
344352
ToolbarItem(placement: .navigationBarTrailing) {
345353
Button {
346-
withAnimation {
347-
showPatchedOnly.toggle()
348-
}
354+
vm.filter.showPatchedOnly.toggle()
349355
} label: {
350356
if #available(iOS 15.0, *) {
351-
Image(systemName: showPatchedOnly ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
357+
Image(systemName: vm.filter.showPatchedOnly
358+
? "line.3.horizontal.decrease.circle.fill"
359+
: "line.3.horizontal.decrease.circle")
352360
} else {
353-
Image(systemName: showPatchedOnly ? "eject.circle.fill" : "eject.circle")
361+
Image(systemName: vm.filter.showPatchedOnly
362+
? "eject.circle.fill"
363+
: "eject.circle")
354364
}
355365
}
356366
.accessibilityLabel(NSLocalizedString("Show Patched Only", comment: ""))
@@ -363,32 +373,20 @@ struct AppListView: View {
363373
if #available(iOS 15.0, *) {
364374
appList
365375
.refreshable {
366-
withAnimation {
367-
vm.refresh()
368-
}
376+
vm.reload()
369377
}
370378
.searchable(
371-
text: $searchOptions.keyword,
379+
text: $vm.filter.searchKeyword,
372380
placement: .automatic,
373-
prompt: (showPatchedOnly
381+
prompt: (vm.filter.showPatchedOnly
374382
? NSLocalizedString("Search Patched…", comment: "")
375383
: NSLocalizedString("Search…", comment: ""))
376384
)
377385
.textInputAutocapitalization(.never)
378-
.onChange(of: searchOptions.keyword) { keyword in
379-
fetchSearchResults(for: keyword)
380-
}
381386
} else {
382387
// Fallback on earlier versions
383388
appList
384389
}
385390
}
386391
}
387-
388-
func fetchSearchResults(for query: String) {
389-
searchResults = vm.allApplications.filter { app in
390-
app.name.localizedCaseInsensitiveContains(query) ||
391-
app.id.localizedCaseInsensitiveContains(query)
392-
}
393-
}
394392
}

TrollFools/EjectListView.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ struct InjectedPlugIn: Identifiable {
3030
struct PlugInCell: View {
3131
let plugIn: InjectedPlugIn
3232

33-
@EnvironmentObject var searchOptions: SearchOptions
33+
@EnvironmentObject var searchOptions: FilterOptions
3434

3535
@available(iOS 15.0, *)
3636
var highlightedName: AttributedString {
3737
let name = plugIn.url.lastPathComponent
3838
var attributedString = AttributedString(name)
39-
if let range = attributedString.range(of: searchOptions.keyword, options: [.caseInsensitive, .diacriticInsensitive]) {
39+
if let range = attributedString.range(of: searchOptions.searchKeyword, options: [.caseInsensitive, .diacriticInsensitive]) {
4040
attributedString[range].foregroundColor = .accentColor
4141
}
4242
return attributedString
@@ -92,13 +92,13 @@ struct EjectListView: View {
9292
@State var errorMessage: String = ""
9393

9494
@State var searchResults: [InjectedPlugIn] = []
95-
@StateObject var searchOptions = SearchOptions()
95+
@StateObject var searchOptions = FilterOptions()
9696

9797
@State var isDeletingAll = false
9898
@StateObject var viewControllerHost = ViewControllerHost()
9999

100100
var isSearching: Bool {
101-
return !searchOptions.keyword.isEmpty
101+
return !searchOptions.searchKeyword.isEmpty
102102
}
103103

104104
var filteredPlugIns: [InjectedPlugIn] {
@@ -191,12 +191,12 @@ struct EjectListView: View {
191191
}
192192
}
193193
.searchable(
194-
text: $searchOptions.keyword,
194+
text: $searchOptions.searchKeyword,
195195
placement: .automatic,
196196
prompt: NSLocalizedString("Search…", comment: "")
197197
)
198198
.textInputAutocapitalization(.never)
199-
.onChange(of: searchOptions.keyword) { keyword in
199+
.onChange(of: searchOptions.searchKeyword) { keyword in
200200
fetchSearchResults(for: keyword)
201201
}
202202
} else {

TrollFools/SuccessView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ struct SuccessView: View {
1111
let title: String
1212

1313
@StateObject var vm = AppListModel.shared
14-
@StateObject var searchOptions = SearchOptions()
14+
@StateObject var filter = FilterOptions()
1515

1616
var possibleApp: App? {
1717
[
@@ -59,7 +59,7 @@ struct SuccessView: View {
5959
.padding()
6060
.foregroundColor(.primary)
6161
.multilineTextAlignment(.leading)
62-
.environmentObject(searchOptions)
62+
.environmentObject(filter)
6363
.background(RoundedRectangle(cornerRadius: 12, style: .continuous)
6464
.foregroundColor(Color(.systemBackground))
6565
.shadow(radius: 4))

0 commit comments

Comments
 (0)