mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-07 16:03:54 +02:00
2977873932
- Profile Analyzer (beta) — follower/following scans with mutuals, non-followbacks, new/lost trackers, and profile change history; searchable lists with batch follow/unfollow - Theme settings — force dark mode, Full OLED, OLED chat theme, and keyboard theme picker - Confirm story like - Confirm story emoji reaction - Swipe down to dismiss media viewer - Manually add users to story/chat exclusion lists by username - Keep stories visually seen locally - Auto-scroll reels mode - Quality picker: audio-only and raw photo download rows - Clear cache button with optional auto-clear interval - Spanish, Russian, Korean, Arabic, and Chinese (Traditional) translations - About page with version, credits, and links - Release notes popup on first launch of a new version - Anonymous live viewing - Toggle live comments - Disappearing DM media overlay — action button, mark-as-viewed eye, and audio toggle - Hide RyukGram UI on screenshots, screen recordings, and mirroring - Open link from clipboard — long-press the search tab - Messages-only mode: optional "Hide tab bar" sub-toggle - Fake profile stats — verified badge and follower/following/post counts on your own profile - Language switcher + import/export localization from Debug - Reveal poll/slider vote counts and quiz answers on stories and reels before interacting - Force legacy Quiz sticker back into the story composer tray - Advanced experimental features menu — toggle hidden IG experiments (QuickSnap, Homecoming, Prism, Direct Notes reply types) with apply-on-restart batching and a crash-loop auto-reset - Shortcut to Advanced experimental features from the General experimental features section - Push notifications render with rich previews on sideload again - IG 426 compatibility across story audio toggle, like confirmation, seen-on-like, live comments, notes audio download - Call confirm split into separate voice-call and video-call toggles - Messages-only mode: tab swiping disabled - Settings quick-access broken in non-English languages - Story seen-receipt block restored on IG v426 - Block selected mode no longer marks listed stories as seen - Hide explore posts grid works again on recent IG versions - Hide suggested stories no longer breaks profile highlights - Hide trending searches now also hides the category chip bar - Story eye long-press menu opens next to the button - Disable video autoplay: tap-to-play now works on videos inside carousels - Disable vanish mode swipe fixed on IG 426 - "Confirm shh mode" renamed to "Confirm vanish mode" across all languages - Confirm sticker interaction split into separate story and highlight toggles - Shared link embed presets: added eeinstagram.com and vxinstagram.com - Downloaded media filenames follow `@username_context_timestamp` - Reels pause mode: optional tap-to-mute on photo reels - Backup & Restore — scope picker with live preview for Settings / Excluded lists / Analyzer data - Profile Analyzer: filter by Not verified - Settings header: tap to open a sheet with GitHub and Telegram channel links - Thanks to Furamako for the Spanish translation - Thanks to [ZomkaDEV](https://github.com/ZomkaDEV) for the Russian translation - Thanks to [@ch1tmdgus](https://github.com/ch1tmdgus) (N4C) for the Korean translation - Thanks to [@bruuhim](https://github.com/bruuhim) for the Arabic translation - Thanks to [@jaydenjcpy](https://github.com/jaydenjcpy) for the Chinese (Traditional) translation - Thanks to [@darthplagueiswise](https://github.com/darthplagueiswise) (Radan) for the experimental flag feature set - Thanks to [@asdfzxcvbn](https://github.com/asdfzxcvbn) for [zxPluginsInject](https://github.com/asdfzxcvbn/zxPluginsInject) and [ipapatch](https://github.com/asdfzxcvbn/ipapatch) - 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) - With Liquid Glass buttons + Hide UI on capture both on, the DM eye leaves an empty glass bubble in captures
178 lines
6.7 KiB
Plaintext
178 lines
6.7 KiB
Plaintext
// Reels action button — injects a RyukGram action button above the reel's
|
|
// vertical like/comment/share sidebar (IGSundialViewerVerticalUFI).
|
|
|
|
#import "../../InstagramHeaders.h"
|
|
#import "../../Utils.h"
|
|
#import "../../SCIChrome.h"
|
|
#import "../../ActionButton/SCIActionButton.h"
|
|
#import "../../ActionButton/SCIMediaActions.h"
|
|
#import <objc/runtime.h>
|
|
#import <objc/message.h>
|
|
|
|
static const NSInteger kReelActionBtnTag = 1337;
|
|
|
|
static UIView *sciFindSuperviewOfClass(UIView *view, NSString *className) {
|
|
Class cls = NSClassFromString(className);
|
|
if (!cls) return nil;
|
|
UIView *current = view.superview;
|
|
for (int depth = 0; current && depth < 20; depth++) {
|
|
if ([current isKindOfClass:cls]) return current;
|
|
current = current.superview;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
static id sciFindMediaIvar(UIView *view) {
|
|
if (!view) return nil;
|
|
Class mediaClass = NSClassFromString(@"IGMedia");
|
|
if (!mediaClass) return nil;
|
|
unsigned int count = 0;
|
|
Ivar *ivars = class_copyIvarList([view class], &count);
|
|
id found = nil;
|
|
for (unsigned int i = 0; i < count; i++) {
|
|
const char *type = ivar_getTypeEncoding(ivars[i]);
|
|
if (!type || type[0] != '@') continue;
|
|
@try {
|
|
id val = object_getIvar(view, ivars[i]);
|
|
if (val && [val isKindOfClass:mediaClass]) { found = val; break; }
|
|
} @catch (__unused id e) {}
|
|
}
|
|
if (ivars) free(ivars);
|
|
return found;
|
|
}
|
|
|
|
// Resolve the current carousel child from _currentIndex.
|
|
static id sciCurrentCarouselChildMedia(UIView *carouselCell, id parentMedia) {
|
|
if (!carouselCell || !parentMedia) return parentMedia;
|
|
|
|
// Try _currentIndex ivar
|
|
Ivar idxIvar = class_getInstanceVariable([carouselCell class], "_currentIndex");
|
|
NSInteger currentIdx = 0;
|
|
if (idxIvar) {
|
|
ptrdiff_t offset = ivar_getOffset(idxIvar);
|
|
currentIdx = *(NSInteger *)((char *)(__bridge void *)carouselCell + offset);
|
|
}
|
|
|
|
// Fallback: _currentFractionalIndex
|
|
if (!idxIvar || currentIdx == 0) {
|
|
Ivar fracIvar = class_getInstanceVariable([carouselCell class], "_currentFractionalIndex");
|
|
if (fracIvar) {
|
|
ptrdiff_t fOffset = ivar_getOffset(fracIvar);
|
|
double fracIdx = *(double *)((char *)(__bridge void *)carouselCell + fOffset);
|
|
NSInteger roundedIdx = (NSInteger)round(fracIdx);
|
|
if (roundedIdx > 0) currentIdx = roundedIdx;
|
|
}
|
|
}
|
|
|
|
// Fallback: inner collection view content offset
|
|
Ivar cvIvar = class_getInstanceVariable([carouselCell class], "_collectionView");
|
|
if (cvIvar) {
|
|
UICollectionView *cv = object_getIvar(carouselCell, cvIvar);
|
|
if (cv) {
|
|
CGFloat pageWidth = cv.bounds.size.width;
|
|
if (pageWidth > 0) {
|
|
NSInteger cvIdx = (NSInteger)round(cv.contentOffset.x / pageWidth);
|
|
if (cvIdx > currentIdx) currentIdx = cvIdx;
|
|
}
|
|
}
|
|
}
|
|
|
|
NSArray *children = [SCIMediaActions carouselChildrenForMedia:parentMedia];
|
|
if (currentIdx >= 0 && (NSUInteger)currentIdx < children.count) {
|
|
return children[currentIdx];
|
|
}
|
|
return parentMedia;
|
|
}
|
|
|
|
// Media provider for reels. Returns current page's child for carousels.
|
|
static id sciReelsMediaProvider(UIView *sourceView) {
|
|
// Video reel
|
|
UIView *videoCell = sciFindSuperviewOfClass(sourceView, @"IGSundialViewerVideoCell");
|
|
if (videoCell) {
|
|
id m = sciFindMediaIvar(videoCell);
|
|
if (m) return m;
|
|
}
|
|
|
|
// Photo reel
|
|
UIView *photoCell = sciFindSuperviewOfClass(sourceView, @"IGSundialViewerPhotoCell");
|
|
if (photoCell) {
|
|
id m = sciFindMediaIvar(photoCell);
|
|
if (m) return m;
|
|
}
|
|
|
|
// Carousel reel
|
|
UIView *carouselCell = sciFindSuperviewOfClass(sourceView, @"IGSundialViewerCarouselCell");
|
|
if (carouselCell) {
|
|
id parentMedia = sciFindMediaIvar(carouselCell);
|
|
if (parentMedia) {
|
|
return sciCurrentCarouselChildMedia(carouselCell, parentMedia);
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
%hook IGSundialViewerVerticalUFI
|
|
|
|
- (void)didMoveToSuperview {
|
|
%orig;
|
|
|
|
if (![SCIUtils getBoolPref:@"reels_action_button"]) return;
|
|
if (!self.superview) return;
|
|
|
|
SCIChromeButton *btn = (SCIChromeButton *)[self viewWithTag:kReelActionBtnTag];
|
|
if (![btn isKindOfClass:[SCIChromeButton class]]) btn = nil;
|
|
|
|
if (!btn) {
|
|
UIImageSymbolConfiguration *symCfg =
|
|
[UIImageSymbolConfiguration configurationWithPointSize:24 weight:UIImageSymbolWeightSemibold];
|
|
UIImage *base = [UIImage systemImageNamed:@"ellipsis.circle" withConfiguration:symCfg];
|
|
// Bake the drop shadow into the image so no CALayer shadow is needed.
|
|
CGFloat pad = 8;
|
|
CGSize sz = CGSizeMake(base.size.width + pad * 2, base.size.height + pad * 2);
|
|
UIGraphicsImageRenderer *r = [[UIGraphicsImageRenderer alloc] initWithSize:sz];
|
|
UIImage *icon = [r imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
|
|
CGContextRef c = ctx.CGContext;
|
|
CGContextSaveGState(c);
|
|
CGContextSetShadowWithColor(c, CGSizeMake(0, 1), 3,
|
|
[UIColor colorWithWhite:0 alpha:0.55].CGColor);
|
|
UIImage *tinted = [base imageWithTintColor:[UIColor whiteColor]
|
|
renderingMode:UIImageRenderingModeAlwaysOriginal];
|
|
[tinted drawInRect:CGRectMake(pad, pad, base.size.width, base.size.height)];
|
|
CGContextRestoreGState(c);
|
|
}];
|
|
|
|
btn = [[SCIChromeButton alloc] initWithSymbol:@"" pointSize:0 diameter:40];
|
|
btn.tag = kReelActionBtnTag;
|
|
btn.bubbleColor = [UIColor clearColor];
|
|
btn.iconView.image = icon;
|
|
|
|
// Capsule configuration gives us the native dark platter animation
|
|
// when the menu opens/closes — behaviour parity with IG's own chrome.
|
|
UIButtonConfiguration *cfg = [UIButtonConfiguration plainButtonConfiguration];
|
|
cfg.cornerStyle = UIButtonConfigurationCornerStyleCapsule;
|
|
cfg.background.backgroundColor = [UIColor clearColor];
|
|
cfg.contentInsets = NSDirectionalEdgeInsetsZero;
|
|
btn.configuration = cfg;
|
|
|
|
self.clipsToBounds = NO;
|
|
[self addSubview:btn];
|
|
|
|
[NSLayoutConstraint activateConstraints:@[
|
|
[btn.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
|
[btn.bottomAnchor constraintEqualToAnchor:self.topAnchor constant:-10],
|
|
[btn.widthAnchor constraintEqualToConstant:40],
|
|
[btn.heightAnchor constraintEqualToConstant:40]
|
|
]];
|
|
}
|
|
|
|
[SCIActionButton configureButton:btn
|
|
context:SCIActionContextReels
|
|
prefKey:@"reels_action_default"
|
|
mediaProvider:^id (UIView *sourceView) {
|
|
return sciReelsMediaProvider(sourceView);
|
|
}];
|
|
}
|
|
|
|
%end
|