@@ -89,10 +89,18 @@ - (void)updateBusyPrompt {
8989 }
9090}
9191
92+ - (void )updateBusyControls {
93+ BOOL busy = self.checkingUpdates || self.checkingDependencies ;
94+ self.tableView .userInteractionEnabled = !busy;
95+ self.searchController .searchBar .userInteractionEnabled = !busy;
96+ for (UIBarButtonItem *item in self.navigationItem .rightBarButtonItems ) {
97+ item.enabled = !busy;
98+ }
99+ }
100+
92101- (void )setCheckingUpdates : (BOOL )checkingUpdates {
93102 _checkingUpdates = checkingUpdates;
94103 UIBarButtonItem *updateItem = self.navigationItem .rightBarButtonItems .lastObject ;
95- updateItem.enabled = !checkingUpdates;
96104 if (checkingUpdates) {
97105 UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc ] initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleMedium];
98106 [indicator startAnimating ];
@@ -101,80 +109,148 @@ - (void)setCheckingUpdates:(BOOL)checkingUpdates {
101109 updateItem.customView = nil ;
102110 updateItem.image = [UIImage systemImageNamed: @" arrow.triangle.2.circlepath" ];
103111 }
112+ [self updateBusyControls ];
104113 [self updateBusyPrompt ];
105114}
106115
107116- (void )setCheckingDependencies : (BOOL )checkingDependencies {
108117 _checkingDependencies = checkingDependencies;
109- self. tableView . userInteractionEnabled = !checkingDependencies ;
118+ [ self updateBusyControls ] ;
110119 [self updateBusyPrompt ];
111120}
112121
113122- (BOOL )modCanCheckForUpdate : (NSDictionary *)mod {
114123 NSString *source = mod[@" source" ];
124+ id projectID = mod[@" projectId" ];
125+ BOOL hasProjectID = projectID && projectID != NSNull .null && [[projectID description ] length ] > 0 ;
115126 return [source isKindOfClass: NSString .class] &&
116127 ![source isEqualToString: @" manual" ] &&
117- mod[ @" projectId " ] &&
128+ hasProjectID &&
118129 ![mod[@" missing" ] boolValue ];
119130}
120131
132+ - (NSArray <NSDictionary *> *)modsNeedingUpdateMetadataRefreshFromMods : (NSArray <NSDictionary *> *)mods {
133+ NSMutableArray *result = [NSMutableArray new ];
134+ for (NSDictionary *mod in mods) {
135+ NSString *source = mod[@" source" ];
136+ id projectID = mod[@" projectId" ];
137+ NSString *sha = [mod[@" sha1" ] isKindOfClass: NSString .class] ? mod[@" sha1" ] : nil ;
138+ NSString *path = [mod[@" path" ] isKindOfClass: NSString .class] ? mod[@" path" ] : nil ;
139+ if ([source isEqualToString: @" modrinth" ] &&
140+ ![mod[@" missing" ] boolValue ] &&
141+ (!projectID || projectID == NSNull .null || [[projectID description ] length ] == 0 ) &&
142+ (sha.length > 0 || path.length > 0 )) {
143+ [result addObject: mod];
144+ }
145+ }
146+ return result;
147+ }
148+
149+ - (NSString *)updateIdentityForMod : (NSDictionary *)mod {
150+ NSString *source = mod[@" source" ];
151+ id projectID = mod[@" projectId" ];
152+ if ([source isKindOfClass: NSString .class] && projectID && projectID != NSNull .null && [[projectID description ] length ] > 0 ) {
153+ return [NSString stringWithFormat: @" %@ :%@ " , source, [[projectID description ] lowercaseString ]];
154+ }
155+
156+ NSString *fileName = mod[@" fileName" ];
157+ if ([fileName isKindOfClass: NSString .class] && fileName.length > 0 ) {
158+ return [@" file:" stringByAppendingString: fileName.lowercaseString];
159+ }
160+ return nil ;
161+ }
162+
163+ - (NSArray <NSMutableDictionary *> *)modsByMergingUpdateResults : (NSDictionary <NSString *, NSDictionary *> *)updatesByIdentity {
164+ NSArray <NSMutableDictionary *> *freshMods = [self .store installedModsMatchingQuery: self .searchController.searchBar.text ?: @" " ];
165+ NSMutableArray <NSMutableDictionary *> *mergedMods = [NSMutableArray arrayWithCapacity: freshMods.count];
166+ for (NSDictionary *mod in freshMods) {
167+ NSMutableDictionary *copy = mod.mutableCopy ;
168+ NSString *identity = [self updateIdentityForMod: mod];
169+ NSDictionary *update = identity ? updatesByIdentity[identity] : nil ;
170+ if (update) {
171+ copy[@" availableUpdate" ] = update;
172+ } else {
173+ [copy removeObjectForKey: @" availableUpdate" ];
174+ }
175+ [mergedMods addObject: copy];
176+ }
177+ return mergedMods;
178+ }
179+
121180- (void )actionCheckUpdates {
122181 if (self.checkingUpdates ) {
123182 return ;
124183 }
125- NSArray *mods = [self .store installedMods ];
126- NSMutableArray *updatedMods = [NSMutableArray arrayWithCapacity: mods.count];
127- NSMutableArray <NSNumber *> *updateIndexes = [NSMutableArray new ];
128- for (NSUInteger i = 0 ; i < mods.count ; i++) {
129- NSDictionary *mod = mods[i];
130- [updatedMods addObject: mod.mutableCopy];
131- if ([self modCanCheckForUpdate: mod]) {
132- [updateIndexes addObject: @(i)];
133- }
134- }
135184 self.updateCheckCompleted = 0 ;
136- self.updateCheckTotal = updateIndexes. count ;
185+ self.updateCheckTotal = 0 ;
137186 self.checkingUpdates = YES ;
138- if (updateIndexes.count == 0 ) {
139- self.checkingUpdates = NO ;
140- self.mods = updatedMods;
141- [self .tableView reloadData ];
142- return ;
143- }
144187
145188 dispatch_queue_t queue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT , 0 );
146- dispatch_group_t group = dispatch_group_create ();
147- dispatch_semaphore_t limit = dispatch_semaphore_create (4 );
148189 NSDictionary *profileInfo = self.store .profileInfo ;
149- for (NSNumber *indexNumber in updateIndexes) {
150- dispatch_group_async (group, queue, ^{
151- dispatch_semaphore_wait (limit, DISPATCH_TIME_FOREVER );
152- NSUInteger index = indexNumber.unsignedIntegerValue ;
153- NSDictionary *mod = mods[index];
190+ dispatch_async (queue, ^{
191+ NSArray *mods = [self .store installedMods ];
192+ NSArray *refreshCandidates = [self modsNeedingUpdateMetadataRefreshFromMods: mods];
193+ if (refreshCandidates.count > 0 ) {
154194 ModManagerAPI *api = [ModManagerAPI new ];
155- NSError *error = nil ;
156- NSDictionary *update = [api latestVersionForInstalledMod: mod profileInfo: profileInfo error: &error];
157- NSMutableDictionary *copy = mod.mutableCopy ;
158- if (update) {
159- copy[@" availableUpdate" ] = update;
160- } else {
161- [copy removeObjectForKey: @" availableUpdate" ];
195+ NSArray *records = [api refreshedMetadataForInstalledMods: refreshCandidates profileInfo: profileInfo];
196+ if (records.count > 0 ) {
197+ NSError *saveError = [self .store saveMetadataRecords: records replacingFileNames: @[] replacements: @[]];
198+ if (saveError) {
199+ NSLog (@" [ModManager] Failed to refresh update metadata: %@ " , saveError.localizedDescription );
200+ } else {
201+ mods = [self .store installedMods ];
202+ }
162203 }
163- @synchronized (updatedMods) {
164- updatedMods[index] = copy;
204+ }
205+
206+ NSMutableArray <NSDictionary *> *checkMods = [NSMutableArray new ];
207+ for (NSDictionary *mod in mods) {
208+ if ([self modCanCheckForUpdate: mod]) {
209+ [checkMods addObject: mod];
210+ }
211+ }
212+
213+ dispatch_async (dispatch_get_main_queue (), ^{
214+ self.updateCheckTotal = checkMods.count ;
215+ [self updateBusyPrompt ];
216+ if (checkMods.count == 0 ) {
217+ self.mods = [self modsByMergingUpdateResults: @{}];
218+ [self .tableView reloadData ];
219+ self.checkingUpdates = NO ;
165220 }
166- dispatch_async (dispatch_get_main_queue (), ^{
167- self.updateCheckCompleted += 1 ;
168- [self updateBusyPrompt ];
169- });
170- dispatch_semaphore_signal (limit);
171221 });
172- }
222+ if (checkMods.count == 0 ) {
223+ return ;
224+ }
225+
226+ NSMutableDictionary <NSString *, NSDictionary *> *updatesByIdentity = [NSMutableDictionary new ];
227+ dispatch_group_t group = dispatch_group_create ();
228+ dispatch_semaphore_t limit = dispatch_semaphore_create (4 );
229+ for (NSDictionary *mod in checkMods) {
230+ dispatch_group_async (group, queue, ^{
231+ dispatch_semaphore_wait (limit, DISPATCH_TIME_FOREVER );
232+ ModManagerAPI *api = [ModManagerAPI new ];
233+ NSError *error = nil ;
234+ NSDictionary *update = [api latestVersionForInstalledMod: mod profileInfo: profileInfo error: &error];
235+ NSString *identity = [self updateIdentityForMod: mod];
236+ if (update && identity.length > 0 ) {
237+ @synchronized (updatesByIdentity) {
238+ updatesByIdentity[identity] = update;
239+ }
240+ }
241+ dispatch_async (dispatch_get_main_queue (), ^{
242+ self.updateCheckCompleted += 1 ;
243+ [self updateBusyPrompt ];
244+ });
245+ dispatch_semaphore_signal (limit);
246+ });
247+ }
173248
174- dispatch_group_notify (group, dispatch_get_main_queue (), ^{
175- self.checkingUpdates = NO ;
176- self.mods = updatedMods;
177- [self .tableView reloadData ];
249+ dispatch_group_notify (group, dispatch_get_main_queue (), ^{
250+ self.mods = [self modsByMergingUpdateResults: updatesByIdentity];
251+ [self .tableView reloadData ];
252+ self.checkingUpdates = NO ;
253+ });
178254 });
179255}
180256
0 commit comments