From d17fba57781b8ba5b974ae4d66f9e4398506024b Mon Sep 17 00:00:00 2001 From: faroukbmiled Date: Fri, 10 Apr 2026 16:06:25 +0100 Subject: [PATCH] feat: Add ability to download highlight cover images from the profile page via long press menu --- README.md | 1 + .../General/HighlightCoverDownload.xm | 120 ++++++++++++++++++ src/Settings/TweakSettings.m | 3 +- src/Tweak.x | 1 + 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/Features/General/HighlightCoverDownload.xm diff --git a/README.md b/README.md index 14a2323..2a93038 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com - Save profile picture - Download buttons on media — tap a button directly on feed posts, reels sidebar, and story overlay **\*** - Download method — choose between download button or long-press gesture **\*** +- Download highlight cover from profile long-press menu **\*** - Save action — choose between share sheet or save directly to Photos **\*** - Save to RyukGram album — optional toggle that routes downloads (and share-sheet "Save to Photos" picks) into a dedicated "RyukGram" album in Photos **\*** - Download confirmation — optional confirmation dialog before downloading **\*** diff --git a/src/Features/General/HighlightCoverDownload.xm b/src/Features/General/HighlightCoverDownload.xm new file mode 100644 index 0000000..3870095 --- /dev/null +++ b/src/Features/General/HighlightCoverDownload.xm @@ -0,0 +1,120 @@ +// Download highlight cover image from the profile long-press menu. +// Captures the long-pressed IGStoryTrayCell, finds the IGImageView inside it, +// and saves the cover using the user's download settings. + +#import "../../Utils.h" +#import "../../Downloader/Download.h" +#import +#import +#import + +static SCIDownloadDelegate *sciHighlightDl = nil; + +// Find the IGStoryTrayCell with an active long-press gesture +static UIView *sciFindLongPressedCell(UIView *root) { + Class cellCls = NSClassFromString(@"IGStoryTrayCell"); + if (!cellCls) return nil; + NSMutableArray *stack = [NSMutableArray arrayWithObject:root]; + while (stack.count) { + UIView *v = stack.lastObject; [stack removeLastObject]; + if ([v isKindOfClass:cellCls]) { + for (UIGestureRecognizer *gr in v.gestureRecognizers) { + if ([gr isKindOfClass:[UILongPressGestureRecognizer class]] && + (gr.state == UIGestureRecognizerStateBegan || gr.state == UIGestureRecognizerStateChanged)) + return v; + } + } + for (UIView *sub in v.subviews) [stack addObject:sub]; + } + return nil; +} + +// Find the IGImageView inside a specific cell +static UIImage *sciCoverImageFromCell(UIView *cell) { + if (!cell) return nil; + Class igImageView = NSClassFromString(@"IGImageView"); + if (!igImageView) igImageView = [UIImageView class]; + NSMutableArray *stack = [NSMutableArray arrayWithObject:cell]; + while (stack.count) { + UIView *v = stack.lastObject; [stack removeLastObject]; + if ([v isKindOfClass:igImageView] && [v isKindOfClass:[UIImageView class]]) { + UIImage *img = [(UIImageView *)v image]; + if (img && img.size.width > 10) return img; + } + for (UIView *sub in v.subviews) [stack addObject:sub]; + } + return nil; +} + +static void sciSaveCoverImage(UIImage *image, UIViewController *presenter) { + if (!image) { + [SCIUtils showErrorHUDWithDescription:@"Could not find cover image"]; + return; + } + + NSString *method = [SCIUtils getStringPref:@"dw_save_action"]; + if ([method isEqualToString:@"photos"]) { + // Save to Photos (respects RyukGram album pref) + NSData *data = UIImageJPEGRepresentation(image, 1.0); + if (!data) return; + NSString *tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]]; + [data writeToFile:tmpPath atomically:YES]; + NSURL *tmpURL = [NSURL fileURLWithPath:tmpPath]; + sciHighlightDl = [[SCIDownloadDelegate alloc] initWithAction:saveToPhotos showProgress:NO]; + [sciHighlightDl downloadDidFinishWithFileURL:tmpURL]; + } else { + // Share sheet + UIActivityViewController *activityVC = [[UIActivityViewController alloc] + initWithActivityItems:@[image] applicationActivities:nil]; + if (presenter) [presenter presentViewController:activityVC animated:YES completion:nil]; + } +} + +// Stored reference to the long-pressed cell (captured at presentation time) +static __weak UIView *sciLongPressedHighlightCell = nil; + +static void (*orig_present)(id, SEL, id, BOOL, id); +static void new_present(id self, SEL _cmd, id vc, BOOL animated, id completion) { + if ([SCIUtils getBoolPref:@"download_highlight_cover"] && + [NSStringFromClass([vc class]) containsString:@"ActionSheet"] && + [NSStringFromClass([self class]) containsString:@"Profile"]) { + + // Capture the long-pressed cell NOW while the gesture is still active + UIView *cell = sciFindLongPressedCell([(UIViewController *)self view]); + sciLongPressedHighlightCell = cell; + + if (cell) { + Ivar actIvar = class_getInstanceVariable([vc class], "_actions"); + NSArray *actions = actIvar ? object_getIvar(vc, actIvar) : nil; + if (actions && actions.count >= 2 && actions.count <= 6) { + Class actionCls = NSClassFromString(@"IGActionSheetControllerAction"); + if (actionCls) { + __weak UIViewController *weakSelf = (UIViewController *)self; + void (^handler)(void) = ^{ + UIImage *cover = sciCoverImageFromCell(sciLongPressedHighlightCell); + sciSaveCoverImage(cover, weakSelf); + }; + + SEL initSel = @selector(initWithTitle:subtitle:style:handler:accessibilityIdentifier:accessibilityLabel:); + typedef id (*InitFn)(id, SEL, id, id, NSInteger, id, id, id); + id newAction = ((InitFn)objc_msgSend)([actionCls alloc], initSel, + @"Download cover", nil, 0, handler, nil, nil); + + if (newAction) { + NSMutableArray *newActions = [actions mutableCopy]; + [newActions addObject:newAction]; + object_setIvar(vc, actIvar, [newActions copy]); + } + } + } + } + } + + orig_present(self, _cmd, vc, animated, completion); +} + +__attribute__((constructor)) static void _highlightInit(void) { + MSHookMessageEx([UIViewController class], @selector(presentViewController:animated:completion:), + (IMP)new_present, (IMP *)&orig_present); +} diff --git a/src/Settings/TweakSettings.m b/src/Settings/TweakSettings.m index 3e7b31a..9583081 100644 --- a/src/Settings/TweakSettings.m +++ b/src/Settings/TweakSettings.m @@ -166,7 +166,8 @@ [SCISetting switchCellWithTitle:@"Download feed posts" subtitle:@"Long-press with finger(s) to download posts in the home tab" defaultsKey:@"dw_feed_posts"], [SCISetting switchCellWithTitle:@"Download reels" subtitle:@"Long-press with finger(s) on a reel to download" defaultsKey:@"dw_reels"], [SCISetting switchCellWithTitle:@"Download stories" subtitle:@"Long-press with finger(s) while viewing someone's story to download" defaultsKey:@"dw_story"], - [SCISetting switchCellWithTitle:@"Save profile picture" subtitle:@"On someone's profile, click their profile picture to enlarge it, then hold to download" defaultsKey:@"save_profile"] + [SCISetting switchCellWithTitle:@"Save profile picture" subtitle:@"On someone's profile, click their profile picture to enlarge it, then hold to download" defaultsKey:@"save_profile"], + [SCISetting switchCellWithTitle:@"Download highlight cover" subtitle:@"Adds a download option to the highlight long-press menu on profiles" defaultsKey:@"download_highlight_cover"] ] }, @{ diff --git a/src/Tweak.x b/src/Tweak.x index ac9ec1d..ce83f54 100644 --- a/src/Tweak.x +++ b/src/Tweak.x @@ -74,6 +74,7 @@ BOOL dmVisualMsgsViewedButtonEnabled = false; @"embed_links": @(NO), @"embed_link_domain": @"kkinstagram.com", @"strip_tracking_params": @(NO), + @"download_highlight_cover": @(YES), @"open_links_external": @(NO), @"strip_browser_tracking": @(NO), @"hide_feed_repost": @(NO),