[release] RyukGram v1.2.0

### 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)
This commit is contained in:
faroukbmiled
2026-04-16 03:03:30 +01:00
parent 9b2c7dc202
commit 86eaa95019
124 changed files with 11523 additions and 1393 deletions
+217
View File
@@ -0,0 +1,217 @@
#import "SCIDashParser.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation SCIDashRepresentation
@end
static id sciDashFieldCache(id obj, NSString *key) {
if (!obj || !key) return nil;
static Ivar fcIvar = NULL;
static dispatch_once_t once;
dispatch_once(&once, ^{
Class c = NSClassFromString(@"IGAPIStorableObject");
if (c) fcIvar = class_getInstanceVariable(c, "_fieldCache");
});
if (!fcIvar) return nil;
id fc = nil;
@try { fc = object_getIvar(obj, fcIvar); } @catch (__unused id e) { return nil; }
if (![fc isKindOfClass:[NSDictionary class]]) return nil;
id val = ((NSDictionary *)fc)[key];
if (!val || [val isKindOfClass:[NSNull class]]) return nil;
return val;
}
@implementation SCIDashParser
+ (NSString *)dashManifestForMedia:(id)media {
if (!media) return nil;
NSArray *keys = @[@"video_dash_manifest", @"dash_manifest",
@"video_dash_manifest_url", @"dash_manifest_url"];
for (NSString *key in keys) {
id val = sciDashFieldCache(media, key);
if ([val isKindOfClass:[NSString class]] && [(NSString *)val length] > 10)
return val;
}
id video = nil;
SEL videoSel = @selector(video);
if ([media respondsToSelector:videoSel]) {
video = ((id(*)(id, SEL))objc_msgSend)(media, videoSel);
if (video && ![(id)video isKindOfClass:[NSObject class]]) video = nil;
}
if (video) {
for (NSString *key in keys) {
id val = sciDashFieldCache(video, key);
if ([val isKindOfClass:[NSString class]] && [(NSString *)val length] > 10)
return val;
}
}
return nil;
}
+ (NSArray<SCIDashRepresentation *> *)parseManifest:(NSString *)xmlString {
if (!xmlString.length) return @[];
NSMutableArray<SCIDashRepresentation *> *results = [NSMutableArray array];
NSError *err = nil;
// AdaptationSet blocks (handles both contentType= and mimeType= patterns)
NSRegularExpression *adaptRE = [NSRegularExpression
regularExpressionWithPattern:@"(<AdaptationSet[^>]*>)(.*?)</AdaptationSet>"
options:NSRegularExpressionDotMatchesLineSeparators error:&err];
if (err) return @[];
NSRegularExpression *ctRE = [NSRegularExpression
regularExpressionWithPattern:@"contentType=\"(video|audio)\"" options:NSRegularExpressionCaseInsensitive error:nil];
NSRegularExpression *mtRE = [NSRegularExpression
regularExpressionWithPattern:@"mimeType=\"(video|audio)/[^\"]*\"" options:NSRegularExpressionCaseInsensitive error:nil];
NSRegularExpression *repRE = [NSRegularExpression
regularExpressionWithPattern:@"<Representation[^>]*>"
options:0 error:nil];
NSRegularExpression *baseURLRE = [NSRegularExpression
regularExpressionWithPattern:@"<BaseURL>(.*?)</BaseURL>"
options:0 error:nil];
NSRegularExpression *bwRE = [NSRegularExpression
regularExpressionWithPattern:@"bandwidth=\"(\\d+)\"" options:0 error:nil];
NSRegularExpression *widthRE = [NSRegularExpression
regularExpressionWithPattern:@"(?:^|\\s)width=\"(\\d+)\"" options:0 error:nil];
NSRegularExpression *heightRE = [NSRegularExpression
regularExpressionWithPattern:@"(?:^|\\s)height=\"(\\d+)\"" options:0 error:nil];
NSRegularExpression *labelRE = [NSRegularExpression
regularExpressionWithPattern:@"FBQualityLabel=\"([^\"]+)\"" options:0 error:nil];
NSRegularExpression *fpsRE = [NSRegularExpression
regularExpressionWithPattern:@"frameRate=\"([0-9./]+)\"" options:0 error:nil];
NSRegularExpression *codecsRE = [NSRegularExpression
regularExpressionWithPattern:@"codecs=\"([^\"]+)\"" options:0 error:nil];
[adaptRE enumerateMatchesInString:xmlString options:0
range:NSMakeRange(0, xmlString.length)
usingBlock:^(NSTextCheckingResult *adaptMatch, __unused NSMatchingFlags flags, __unused BOOL *stop) {
NSString *adaptTag = [xmlString substringWithRange:[adaptMatch rangeAtIndex:1]];
NSString *adaptBody = [xmlString substringWithRange:[adaptMatch rangeAtIndex:2]];
NSString *contentType = nil;
NSTextCheckingResult *ctMatch = [ctRE firstMatchInString:adaptTag options:0
range:NSMakeRange(0, adaptTag.length)];
if (ctMatch) {
contentType = [[adaptTag substringWithRange:[ctMatch rangeAtIndex:1]] lowercaseString];
} else {
NSTextCheckingResult *mtMatch = [mtRE firstMatchInString:adaptTag options:0
range:NSMakeRange(0, adaptTag.length)];
if (mtMatch) {
contentType = [[adaptTag substringWithRange:[mtMatch rangeAtIndex:1]] lowercaseString];
}
}
if (!contentType) return;
NSArray<NSTextCheckingResult *> *repMatches =
[repRE matchesInString:adaptBody options:0 range:NSMakeRange(0, adaptBody.length)];
NSArray<NSTextCheckingResult *> *urlMatches =
[baseURLRE matchesInString:adaptBody options:0 range:NSMakeRange(0, adaptBody.length)];
for (NSUInteger i = 0; i < repMatches.count && i < urlMatches.count; i++) {
NSString *repTag = [adaptBody substringWithRange:repMatches[i].range];
NSString *baseURL = [adaptBody substringWithRange:[urlMatches[i] rangeAtIndex:1]];
if (!baseURL.length) continue;
baseURL = [baseURL stringByReplacingOccurrencesOfString:@"&amp;" withString:@"&"];
SCIDashRepresentation *rep = [SCIDashRepresentation new];
rep.url = [NSURL URLWithString:baseURL];
rep.contentType = contentType;
NSTextCheckingResult *bwMatch = [bwRE firstMatchInString:repTag options:0
range:NSMakeRange(0, repTag.length)];
if (bwMatch) rep.bandwidth = [[repTag substringWithRange:[bwMatch rangeAtIndex:1]] integerValue];
NSTextCheckingResult *wMatch = [widthRE firstMatchInString:repTag options:0
range:NSMakeRange(0, repTag.length)];
if (wMatch) rep.width = [[repTag substringWithRange:[wMatch rangeAtIndex:1]] integerValue];
NSTextCheckingResult *hMatch = [heightRE firstMatchInString:repTag options:0
range:NSMakeRange(0, repTag.length)];
if (hMatch) rep.height = [[repTag substringWithRange:[hMatch rangeAtIndex:1]] integerValue];
NSTextCheckingResult *fpsMatch = [fpsRE firstMatchInString:repTag options:0
range:NSMakeRange(0, repTag.length)];
if (fpsMatch) {
NSString *raw = [repTag substringWithRange:[fpsMatch rangeAtIndex:1]];
NSArray *parts = [raw componentsSeparatedByString:@"/"];
if (parts.count == 2) {
float num = [parts[0] floatValue], den = [parts[1] floatValue];
if (den > 0) rep.frameRate = num / den;
} else {
rep.frameRate = [raw floatValue];
}
}
NSTextCheckingResult *codecsMatch = [codecsRE firstMatchInString:repTag options:0
range:NSMakeRange(0, repTag.length)];
if (codecsMatch) rep.codecs = [repTag substringWithRange:[codecsMatch rangeAtIndex:1]];
// Quality label from shorter dimension (1080x1920 → "1080p")
if (rep.width > 0 && rep.height > 0) {
NSInteger shortSide = MIN(rep.width, rep.height);
rep.qualityLabel = [NSString stringWithFormat:@"%ldp", (long)shortSide];
} else if (rep.height > 0) {
rep.qualityLabel = [NSString stringWithFormat:@"%ldp", (long)rep.height];
} else {
NSTextCheckingResult *lMatch = [labelRE firstMatchInString:repTag options:0
range:NSMakeRange(0, repTag.length)];
if (lMatch) rep.qualityLabel = [repTag substringWithRange:[lMatch rangeAtIndex:1]];
}
if (rep.url) [results addObject:rep];
}
}];
return [results copy];
}
+ (SCIDashRepresentation *)bestVideoFromRepresentations:(NSArray<SCIDashRepresentation *> *)reps {
return [[self videoRepresentations:reps] firstObject];
}
+ (SCIDashRepresentation *)bestAudioFromRepresentations:(NSArray<SCIDashRepresentation *> *)reps {
SCIDashRepresentation *best = nil;
for (SCIDashRepresentation *r in reps) {
if (![r.contentType isEqualToString:@"audio"]) continue;
if (!best || r.bandwidth > best.bandwidth) best = r;
}
return best;
}
+ (NSArray<SCIDashRepresentation *> *)videoRepresentations:(NSArray<SCIDashRepresentation *> *)reps {
NSMutableArray *videos = [NSMutableArray array];
for (SCIDashRepresentation *r in reps) {
if ([r.contentType isEqualToString:@"video"]) [videos addObject:r];
}
return [videos sortedArrayUsingComparator:^NSComparisonResult(SCIDashRepresentation *a, SCIDashRepresentation *b) {
return [@(b.bandwidth) compare:@(a.bandwidth)]; // descending
}];
}
+ (SCIDashRepresentation *)representationForQuality:(SCIVideoQuality)quality
fromRepresentations:(NSArray<SCIDashRepresentation *> *)reps {
NSArray *sorted = [self videoRepresentations:reps];
if (!sorted.count) return nil;
switch (quality) {
case SCIVideoQualityHighest: return sorted.firstObject;
case SCIVideoQualityLowest: return sorted.lastObject;
case SCIVideoQualityMedium: return sorted[sorted.count / 2];
case SCIVideoQualityAsk: return sorted.firstObject; // caller handles the picker
}
return sorted.firstObject;
}
@end