mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-01 05:01:35 +02:00
86eaa95019
### Features - **Open Instagram links in app (Safari extension)** — bundled Safari web extension (sideload IPA only). Enable in Safari → Extensions; instagram.com links open in the app. - **Localization** — every user-facing string flows through a central translation layer. Globe button in Settings; missing keys fall back to English. Ships English only — see the "Translating RyukGram" section in the README to add more. - **Action buttons** — context-aware menus on feed, reels, and stories (expand, repost, download, copy caption, etc.) with per-context default tap action and carousel/multi-story bulk download - **Enhanced HD downloads** — up to 1080p via DASH + FFmpegKit with quality picker, preview playback, encoding-speed options, and 720p fallback - **Repost**, **media viewer**, **media zoom** (long-press), **download pill** (frosted glass, stacks concurrent downloads) - **Fake location** — overrides CoreLocation app-wide, map picker + saved presets, optional quick-toggle button on the Friends Map - **Messages-only mode** — strips every tab except DM inbox + profile - **Launch tab** — pick which tab the app opens to - Full last active date in DMs — show full date instead of "Active 2h ago" - Custom date format — 12 formats with per-surface toggles (feed, notes/comments/stories, DMs) - Send files in DMs (experimental) - View story mentions - Hide suggested stories - Story tray long-press actions — view HD profile picture from the tray menu - Advance on story reply — auto-skip to next story after sending a reply or reaction - Mark story as seen on reply or emoji reaction - Hide metrics (likes, comments, shares counts) - Hide messages tab - Hide voice/video call buttons in DM thread header (independent toggles) - Disable app haptics - Disable reels tab refresh - Disable disappearing messages mode in DMs - Follow indicator — shows whether the profile user follows you - Copy note text on long press - Zoom profile photo — long press opens full-screen viewer - Notes actions — copy text, download GIF/audio from notes long-press menu - Confirm unfollow - Feed refresh controls — disable background refresh, home button refresh, and home button scroll ### Improvements - Default tap action: added copy URL, repost, and view mentions options; dynamic menu generation per context - Settings pages reordered: General → Feed → Stories → Reels → Messages → Profile → Navigation → Saving → Confirmations - Fake location picker: native Apple Maps-style UI (search, long-press to drop pin, current location) - Liquid glass floating tab bar + dynamic sizing - Upload audio: FFmpegKit re-encode + trim for any audio/video input - Settings reorganized with per-context action button config; new Profile page - Highlight cover: full-screen viewer replaces direct download - Switched HD encoder to `h264_videotoolbox` (hardware) — no GPL FFmpegKit required - Legacy long-press download deprecated (off by default), replaced by action buttons ### Fixes - Hide suggested stories no longer removes followed users' stories on scroll - Settings search bar transparency with liquid glass off; auto-deactivates on push - HD download cancel: tapping pill aborts in-flight downloads + FFmpeg sessions cleanly - Download pill stuck state on background/foreground, progress reset per download - Disappearing messages mode confirmation not firing on swipe - Detailed color picker not working on story draw `†` - DM seen toggle menu not updating after tap - Reel refresh confirmation appearing on first app launch `†` - Reels action button displacing profile pictures on photo reels - Disappearing DM media download (expand, share, save to Photos with progress pill) - Carousel "Download all" not showing item count in feed - Encoding speed setting being ignored for HD downloads - Various upstream SCInsta merges (Meta AI hiding, suggested chats hiding, notes tray) — marked `†` > `†` Merged from upstream [SCInsta](https://github.com/SoCuul/SCInsta) by SoCuul ### Credits - Thanks to [@erupts0](https://github.com/erupts0) (John) for testing and feature suggestions - Thanks to [@euoradan](https://t.me/euoradan) (Radan) for experimental Instagram feature flag research - Safari extension forked/cleaned from [BillyCurtis/OpenInstagramSafariExtension](https://github.com/BillyCurtis/OpenInstagramSafariExtension) ### Known Issues - Preserved unsent messages can't be removed via "Delete for you"; pull-to-refresh clears them (warning available in settings) - "Delete for you" detection uses a ~2s window after the local action — a real unsend landing in that window may be missed (rare)
386 lines
15 KiB
Objective-C
386 lines
15 KiB
Objective-C
#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<NSString *, id> *)sciRegisteredDefaults { return sciRegisteredDefaultsRef ?: @{}; }
|
|
+ (void)setSciRegisteredDefaults:(NSDictionary<NSString *, id> *)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<NSError *> *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<id> *)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<NSDictionary *> *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 |