66//
77
88import CocoaLumberjackSwift
9+ import Combine
910import SwiftUI
1011
1112private let gDateFormatter : DateFormatter = {
@@ -30,13 +31,13 @@ struct InjectedPlugIn: Identifiable {
3031struct PlugInCell : View {
3132 let plugIn : InjectedPlugIn
3233
33- @EnvironmentObject var searchOptions : FilterOptions
34+ @EnvironmentObject var filter : FilterOptions
3435
3536 @available ( iOS 15 . 0 , * )
3637 var highlightedName : AttributedString {
3738 let name = plugIn. url. lastPathComponent
3839 var attributedString = AttributedString ( name)
39- if let range = attributedString. range ( of: searchOptions . searchKeyword, options: [ . caseInsensitive, . diacriticInsensitive] ) {
40+ if let range = attributedString. range ( of: filter . searchKeyword, options: [ . caseInsensitive, . diacriticInsensitive] ) {
4041 attributedString [ range] . foregroundColor = . accentColor
4142 }
4243 return attributedString
@@ -80,31 +81,61 @@ struct PlugInCell: View {
8081 }
8182}
8283
83- struct EjectListView : View {
84+ final class EjectListModel : ObservableObject {
8485 let app : App
86+ private var _injectedPlugIns : [ InjectedPlugIn ] = [ ]
87+
88+ @Published var filter = FilterOptions ( )
89+ @Published var filteredPlugIns : [ InjectedPlugIn ] = [ ]
90+
91+ private var cancellables = Set < AnyCancellable > ( )
8592
8693 init ( _ app: App ) {
8794 self . app = app
95+ reload ( )
96+
97+ filter. $searchKeyword
98+ . throttle ( for: 0.5 , scheduler: DispatchQueue . main, latest: true )
99+ . sink { [ weak self] _ in
100+ withAnimation {
101+ self ? . performFilter ( )
102+ }
103+ }
104+ . store ( in: & cancellables)
88105 }
89106
90- @State var injectedPlugIns : [ InjectedPlugIn ] = [ ]
91- @State var isErrorOccurred : Bool = false
92- @State var errorMessage : String = " "
107+ func reload( ) {
108+ self . _injectedPlugIns = Injector . injectedPlugInURLs ( app. url)
109+ . map { InjectedPlugIn ( url: $0) }
110+ performFilter ( )
111+ }
93112
94- @ State var searchResults : [ InjectedPlugIn ] = [ ]
95- @ StateObject var searchOptions = FilterOptions ( )
113+ func performFilter ( ) {
114+ var filteredPlugIns = _injectedPlugIns
96115
97- @State var isDeletingAll = false
98- @StateObject var viewControllerHost = ViewControllerHost ( )
116+ if !filter. searchKeyword. isEmpty {
117+ filteredPlugIns = filteredPlugIns. filter {
118+ $0. url. lastPathComponent. localizedCaseInsensitiveContains ( filter. searchKeyword)
119+ }
120+ }
99121
100- var isSearching : Bool {
101- return !searchOptions. searchKeyword. isEmpty
122+ self . filteredPlugIns = filteredPlugIns
102123 }
124+ }
125+
126+ struct EjectListView : View {
127+ @StateObject var vm : EjectListModel
103128
104- var filteredPlugIns : [ InjectedPlugIn ] {
105- isSearching ? searchResults : injectedPlugIns
129+ init ( _ app : App ) {
130+ _vm = StateObject ( wrappedValue : EjectListModel ( app ) )
106131 }
107132
133+ @State var isErrorOccurred : Bool = false
134+ @State var errorMessage : String = " "
135+
136+ @State var isDeletingAll = false
137+ @StateObject var viewControllerHost = ViewControllerHost ( )
138+
108139 var deleteAllButtonLabel : some View {
109140 HStack {
110141 Label ( NSLocalizedString ( " Eject All " , comment: " " ) , systemImage: " eject " )
@@ -135,31 +166,31 @@ struct EjectListView: View {
135166 var ejectList : some View {
136167 List {
137168 Section {
138- ForEach ( filteredPlugIns) { plugin in
169+ ForEach ( vm . filteredPlugIns) { plugin in
139170 if #available( iOS 16 . 0 , * ) {
140171 PlugInCell ( plugIn: plugin)
141- . environmentObject ( searchOptions )
172+ . environmentObject ( vm . filter )
142173 } else {
143174 PlugInCell ( plugIn: plugin)
144- . environmentObject ( searchOptions )
175+ . environmentObject ( vm . filter )
145176 . padding ( . vertical, 4 )
146177 }
147178 }
148179 . onDelete ( perform: delete)
149180 } header: {
150- Text ( filteredPlugIns. isEmpty
181+ Text ( vm . filteredPlugIns. isEmpty
151182 ? NSLocalizedString ( " No Injected Plug-Ins " , comment: " " )
152183 : NSLocalizedString ( " Injected Plug-Ins " , comment: " " ) )
153184 . font ( . footnote)
154185 }
155186
156- if !isSearching && !filteredPlugIns. isEmpty {
187+ if !vm . filter . isSearching && !vm . filteredPlugIns. isEmpty {
157188 Section {
158189 deleteAllButton
159190 . disabled ( isDeletingAll)
160191 . foregroundColor ( isDeletingAll ? . secondary : . red)
161192 } footer: {
162- if app. isFromTroll {
193+ if vm . app. isFromTroll {
163194 Text ( NSLocalizedString ( " Some plug-ins were not injected by TrollFools, please eject them with caution. " , comment: " " ) )
164195 . font ( . footnote)
165196 }
@@ -173,53 +204,41 @@ struct EjectListView: View {
173204 }
174205 . listStyle ( . insetGrouped)
175206 . navigationTitle ( NSLocalizedString ( " Plug-Ins " , comment: " " ) )
176- . navigationBarTitleDisplayMode ( . inline )
207+ . animation ( . easeOut , value : vm . filter . isSearching )
177208 . onViewWillAppear { viewController in
178209 viewControllerHost. viewController = viewController
179210 }
180- . onAppear {
181- reloadPlugIns ( )
182- }
183211 }
184212
185213 var body : some View {
186214 if #available( iOS 15 . 0 , * ) {
187215 ejectList
188216 . refreshable {
189217 withAnimation {
190- reloadPlugIns ( )
218+ vm . reload ( )
191219 }
192220 }
193221 . searchable (
194- text: $searchOptions . searchKeyword,
222+ text: $vm . filter . searchKeyword,
195223 placement: . automatic,
196224 prompt: NSLocalizedString ( " Search… " , comment: " " )
197225 )
198226 . textInputAutocapitalization ( . never)
199- . onChange ( of: searchOptions. searchKeyword) { keyword in
200- fetchSearchResults ( for: keyword)
201- }
202227 } else {
203228 // Fallback on earlier versions
204229 ejectList
205230 }
206231 }
207232
208- func reloadPlugIns( ) {
209- searchOptions. reset ( )
210- injectedPlugIns = Injector . injectedPlugInURLs ( app. url)
211- . map { InjectedPlugIn ( url: $0) }
212- }
213-
214233 func delete( at offsets: IndexSet ) {
215234 do {
216- let plugInsToRemove = offsets. map { filteredPlugIns [ $0] }
235+ let plugInsToRemove = offsets. map { vm . filteredPlugIns [ $0] }
217236 let plugInURLsToRemove = plugInsToRemove. map { $0. url }
218- let injector = try Injector ( bundleURL: app. url, teamID: app. teamID)
237+ let injector = try Injector ( bundleURL: vm . app. url, teamID: vm . app. teamID)
219238 try injector. eject ( plugInURLsToRemove)
220239
221- app. reloadInjectedStatus ( )
222- reloadPlugIns ( )
240+ vm . app. reloadInjectedStatus ( )
241+ vm . reload ( )
223242 } catch {
224243 DDLogError ( " \( error) " )
225244
@@ -230,7 +249,7 @@ struct EjectListView: View {
230249
231250 func deleteAll( ) {
232251 do {
233- let injector = try Injector ( bundleURL: app. url, teamID: app. teamID)
252+ let injector = try Injector ( bundleURL: vm . app. url, teamID: vm . app. teamID)
234253
235254 let view = viewControllerHost. viewController?
236255 . navigationController? . view
@@ -245,8 +264,8 @@ struct EjectListView: View {
245264 defer {
246265 DispatchQueue . main. async {
247266 withAnimation {
248- app. reloadInjectedStatus ( )
249- reloadPlugIns ( )
267+ vm . app. reloadInjectedStatus ( )
268+ vm . reload ( )
250269 isDeletingAll = false
251270 }
252271
@@ -274,10 +293,4 @@ struct EjectListView: View {
274293 isErrorOccurred = true
275294 }
276295 }
277-
278- func fetchSearchResults( for query: String ) {
279- searchResults = injectedPlugIns. filter { plugin in
280- plugin. url. lastPathComponent. localizedCaseInsensitiveContains ( query)
281- }
282- }
283296}
0 commit comments