mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-01 05:01:35 +02:00
[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:
@@ -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:@"&" 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
|
||||
Reference in New Issue
Block a user