#import "Utils.h" #import "PhotoAlbum.h" #import "Settings/TweakSettings.h" @implementation SCIUtils + (BOOL)getBoolPref:(NSString *)key { if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return false; return [[NSUserDefaults standardUserDefaults] boolForKey:key]; } + (double)getDoublePref:(NSString *)key { if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return 0; return [[NSUserDefaults standardUserDefaults] doubleForKey:key]; } + (NSString *)getStringPref:(NSString *)key { if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return @""; return [[NSUserDefaults standardUserDefaults] stringForKey:key]; } static NSDictionary *sciRegisteredDefaultsRef = nil; + (NSDictionary *)sciRegisteredDefaults { return sciRegisteredDefaultsRef ?: @{}; } + (void)setSciRegisteredDefaults:(NSDictionary *)defaults { sciRegisteredDefaultsRef = [defaults copy]; } + (_Bool)liquidGlassEnabledBool:(_Bool)fallback { BOOL setting = [SCIUtils getBoolPref:@"liquid_glass_surfaces"]; return setting ? true : fallback; } + (void)cleanCache { NSFileManager *fileManager = [NSFileManager defaultManager]; NSMutableArray *deletionErrors = [NSMutableArray array]; // Temp folder // * disabled bc app crashed trying to delete certain files inside it // todo: remove the above disclaimer if this new code doesn't cause crashing NSArray *tempFolderContents = [fileManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:NSTemporaryDirectory()] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; for (NSURL *fileURL in tempFolderContents) { NSError *cacheItemDeletionError; [fileManager removeItemAtURL:fileURL error:&cacheItemDeletionError]; if (cacheItemDeletionError) [deletionErrors addObject:cacheItemDeletionError]; } // Analytics folder NSString *analyticsFolder = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Application Support/com.burbn.instagram/analytics"]; NSArray *analyticsFolderContents = [fileManager contentsOfDirectoryAtURL:[[NSURL alloc] initFileURLWithPath:analyticsFolder] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; for (NSURL *fileURL in analyticsFolderContents) { NSError *cacheItemDeletionError; [fileManager removeItemAtURL:fileURL error:&cacheItemDeletionError]; if (cacheItemDeletionError) [deletionErrors addObject:cacheItemDeletionError]; } // Caches folder NSString *cachesFolder = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Caches"]; NSArray *cachesFolderContents = [fileManager contentsOfDirectoryAtURL:[[NSURL alloc] initFileURLWithPath:cachesFolder] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; for (NSURL *fileURL in cachesFolderContents) { NSError *cacheItemDeletionError; [fileManager removeItemAtURL:fileURL error:&cacheItemDeletionError]; if (cacheItemDeletionError) [deletionErrors addObject:cacheItemDeletionError]; } // Log errors if (deletionErrors.count > 1) { for (NSError *error in deletionErrors) { NSLog(@"[SCInsta] File Deletion Error: %@", error); } } } // Displaying View Controllers + (void)showQuickLookVC:(NSArray *)items { UIViewController *topVC = topMostController(); if (!topVC) { NSLog(@"[SCInsta] No view controller available to present QuickLook"); return; } QLPreviewController *previewController = [[QLPreviewController alloc] init]; QuickLookDelegate *quickLookDelegate = [[QuickLookDelegate alloc] initWithPreviewItemURLs:items]; previewController.dataSource = quickLookDelegate; [topVC presentViewController:previewController animated:true completion:nil]; } + (void)showShareVC:(id)item { UIViewController *topVC = topMostController(); if (!topVC) { NSLog(@"[SCInsta] No view controller available to present share sheet"); return; } UIActivityViewController *acVC = [[UIActivityViewController alloc] initWithActivityItems:@[item] applicationActivities:nil]; if (is_iPad()) { acVC.popoverPresentationController.sourceView = topVC.view; acVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width / 2.0, topVC.view.bounds.size.height / 2.0, 1.0, 1.0); } // If the user picks "Save to Photos" from the share sheet, route the new // asset into the RyukGram album via a one-shot photo library observer. if ([self getBoolPref:@"save_to_ryukgram_album"]) { [SCIPhotoAlbum watchForNextSavedAsset]; } [topVC presentViewController:acVC animated:true completion:nil]; } + (void)showSettingsVC:(UIWindow *)window { UIViewController *rootController = [window rootViewController]; SCISettingsViewController *settingsViewController = [SCISettingsViewController new]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:settingsViewController]; if ([SCIUtils getBoolPref:@"settings_pause_playback"]) navigationController.modalPresentationStyle = UIModalPresentationFullScreen; [rootController presentViewController:navigationController animated:YES completion:nil]; } // Open settings and push straight into a named top-level entry (e.g. "Messages"). + (void)showSettingsVC:(UIWindow *)window atTopLevelEntry:(NSString *)entryTitle { UIViewController *rootController = [window rootViewController]; while (rootController.presentedViewController) rootController = rootController.presentedViewController; SCISettingsViewController *root = [SCISettingsViewController new]; UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:root]; if ([SCIUtils getBoolPref:@"settings_pause_playback"]) nav.modalPresentationStyle = UIModalPresentationFullScreen; NSArray *targetNavSections = nil; for (NSDictionary *section in [SCITweakSettings sections]) { for (SCISetting *row in section[@"rows"]) { if (row.type == SCITableCellNavigation && [row.title isEqualToString:entryTitle]) { targetNavSections = row.navSections; break; } } if (targetNavSections) break; } if (targetNavSections) { SCISettingsViewController *child = [[SCISettingsViewController alloc] initWithTitle:entryTitle sections:targetNavSections reduceMargin:NO]; child.title = entryTitle; [nav pushViewController:child animated:NO]; } [rootController presentViewController:nav animated:YES completion:nil]; } // Colours + (UIColor *)SCIColor_Primary { return [UIColor colorWithRed:0/255.0 green:152/255.0 blue:254/255.0 alpha:1]; }; // Errors + (NSError *)errorWithDescription:(NSString *)errorDesc { return [self errorWithDescription:errorDesc code:1]; } + (NSError *)errorWithDescription:(NSString *)errorDesc code:(NSInteger)errorCode { NSError *error = [ NSError errorWithDomain:@"com.socuul.scinsta" code:errorCode userInfo:@{ NSLocalizedDescriptionKey: errorDesc } ]; return error; } + (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc { return [self showErrorHUDWithDescription:errorDesc dismissAfterDelay:4.0]; } + (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc dismissAfterDelay:(CGFloat)dismissDelay { JGProgressHUD *hud = [[JGProgressHUD alloc] init]; hud.textLabel.text = errorDesc; hud.indicatorView = [[JGProgressHUDErrorIndicatorView alloc] init]; UIView *hudView = topMostController().view; if (!hudView) hudView = [UIApplication sharedApplication].keyWindow; if (hudView) { [hud showInView:hudView]; [hud dismissAfterDelay:dismissDelay]; } else { NSLog(@"[SCInsta] No valid view for error HUD: %@", errorDesc); } return hud; } // Media + (NSURL *)getPhotoUrl:(IGPhoto *)photo { if (!photo) return nil; // Get highest quality photo link NSURL *photoUrl = [photo imageURLForWidth:100000.00]; return photoUrl; } + (NSURL *)getPhotoUrlForMedia:(IGMedia *)media { if (!media) return nil; IGPhoto *photo = media.photo; return [SCIUtils getPhotoUrl:photo]; } + (NSURL *)getVideoUrl:(IGVideo *)video { if (!video) return nil; // The past (pre v398) if ([video respondsToSelector:@selector(sortedVideoURLsBySize)]) { NSArray *sorted = [video sortedVideoURLsBySize]; NSString *urlString = sorted.firstObject[@"url"]; return urlString.length ? [NSURL URLWithString:urlString] : nil; } // The present (post v398) if ([video respondsToSelector:@selector(allVideoURLs)]) { return [[video allVideoURLs] anyObject]; } return nil; } + (NSURL *)getVideoUrlForMedia:(IGMedia *)media { if (!media) return nil; IGVideo *video = media.video; if (!video) return nil; return [SCIUtils getVideoUrl:video]; } // View Controllers + (UIViewController *)viewControllerForView:(UIView *)view { NSString *viewDelegate = @"viewDelegate"; if ([view respondsToSelector:NSSelectorFromString(viewDelegate)]) { return [view valueForKey:viewDelegate]; } return nil; } + (UIViewController *)viewControllerForAncestralView:(UIView *)view { NSString *_viewControllerForAncestor = @"_viewControllerForAncestor"; if ([view respondsToSelector:NSSelectorFromString(_viewControllerForAncestor)]) { return [view valueForKey:_viewControllerForAncestor]; } return nil; } + (UIViewController *)nearestViewControllerForView:(UIView *)view { return [self viewControllerForView:view] ?: [self viewControllerForAncestralView:view]; } // Functions + (NSString *)IGVersionString { return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; }; + (BOOL)isNotch { return [[[UIApplication sharedApplication] keyWindow] safeAreaInsets].bottom > 0; }; + (BOOL)existingLongPressGestureRecognizerForView:(UIView *)view { NSArray *allRecognizers = view.gestureRecognizers; for (UIGestureRecognizer *recognizer in allRecognizers) { if ([[recognizer class] isSubclassOfClass:[UILongPressGestureRecognizer class]]) { return YES; } } return NO; } // Alerts + (BOOL)showConfirmation:(void(^)(void))okHandler title:(NSString *)title { UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:SCILocalized(@"Are you sure?") preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Yes") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { okHandler(); }]]; [alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"No!") style:UIAlertActionStyleCancel handler:nil]]; [topMostController() presentViewController:alert animated:YES completion:nil]; return nil; }; + (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler title:(NSString *)title { UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:SCILocalized(@"Are you sure?") preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Yes") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { okHandler(); }]]; [alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"No!") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { if (cancelHandler != nil) { cancelHandler(); } }]]; [topMostController() presentViewController:alert animated:YES completion:nil]; return nil; }; + (BOOL)showConfirmation:(void(^)(void))okHandler { return [self showConfirmation:okHandler title:nil]; }; + (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler { return [self showConfirmation:okHandler cancelHandler:cancelHandler title:nil]; } + (void)showRestartConfirmation { UIAlertController* alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Restart required") message:SCILocalized(@"You must restart the app to apply this change") preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Restart") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { exit(0); }]]; [alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Later") style:UIAlertActionStyleCancel handler:nil]]; [topMostController() presentViewController:alert animated:YES completion:nil]; }; // Toasts + (void)showToastForDuration:(double)duration title:(NSString *)title { [SCIUtils showToastForDuration:duration title:title subtitle:nil]; } + (void)showToastForDuration:(double)duration title:(NSString *)title subtitle:(NSString *)subtitle { // Root VC Class rootVCClass = NSClassFromString(@"IGRootViewController"); UIViewController *topMostVC = topMostController(); if (![topMostVC isKindOfClass:rootVCClass]) return; IGRootViewController *rootVC = (IGRootViewController *)topMostVC; // Presenter IGActionableConfirmationToastPresenter *toastPresenter = [rootVC toastPresenter]; if (toastPresenter == nil) return; // View Model Class modelClass = NSClassFromString(@"IGActionableConfirmationToastViewModel"); IGActionableConfirmationToastViewModel *model = [modelClass new]; [model setValue:title forKey:@"text_annotatedTitleText"]; [model setValue:subtitle forKey:@"text_annotatedSubtitleText"]; // Show new toast, after clearing existing one [toastPresenter hideAlert]; [toastPresenter showAlertWithViewModel:model isAnimated:true animationDuration:duration presentationPriority:0 tapActionBlock:nil presentedHandler:nil dismissedHandler:nil]; } // Math + (NSUInteger)decimalPlacesInDouble:(double)value { NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; [formatter setMaximumFractionDigits:15]; // Allow enough digits for double precision [formatter setMinimumFractionDigits:0]; [formatter setDecimalSeparator:@"."]; // Force dot for internal logic, then respect locale for final display if needed NSString *stringValue = [formatter stringFromNumber:@(value)]; // Find decimal separator NSRange decimalRange = [stringValue rangeOfString:formatter.decimalSeparator]; if (decimalRange.location == NSNotFound) { return 0; } else { return stringValue.length - (decimalRange.location + decimalRange.length); } } // Ivars + (id)getIvarForObj:(id)obj name:(const char *)name { Ivar ivar = class_getInstanceVariable(object_getClass(obj), name); if (!ivar) return nil; return object_getIvar(obj, ivar); } + (void)setIvarForObj:(id)obj name:(const char *)name value:(id)value { Ivar ivar = class_getInstanceVariable(object_getClass(obj), name); if (!ivar) return; object_setIvarWithStrongDefault(obj, ivar, value); } @end