mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-06 15:33:53 +02:00
Added download btn to stories ,fixed finger gestures for stories ,fixed manual view ,added not setting visually seen, added stop auto advance stories and more
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
Package: com.socuul.scinsta
|
||||
Name: SCInsta
|
||||
Version: 1.1.2
|
||||
Version: 1.1.3
|
||||
Architecture: iphoneos-arm
|
||||
Description: A feature-rich tweak for Instagram on iOS!
|
||||
Homepage: https://github.com/SoCuul/SCInsta
|
||||
|
||||
@@ -145,9 +145,9 @@
|
||||
[weakSelf.downloadManager cancelDownload];
|
||||
};
|
||||
|
||||
UIViewController *topVC = topMostController();
|
||||
UIView *hostView = topVC.view;
|
||||
if (!hostView) hostView = [UIApplication sharedApplication].keyWindow;
|
||||
// Show on keyWindow so it survives VC transitions (e.g. leaving stories)
|
||||
UIView *hostView = [UIApplication sharedApplication].keyWindow;
|
||||
if (!hostView) hostView = topMostController().view;
|
||||
if (!hostView) {
|
||||
NSLog(@"[SCInsta] Download: No valid view");
|
||||
return;
|
||||
|
||||
@@ -1,245 +1,384 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Downloader/Download.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
|
||||
// Bypass flag: when YES, all hooks let calls through (for manual mark as seen)
|
||||
// === State ===
|
||||
static BOOL sciSeenBypassActive = NO;
|
||||
static NSMutableSet *sciAllowedSeenPKs = nil;
|
||||
|
||||
static BOOL sciShouldBlockSeen() {
|
||||
// === Helpers ===
|
||||
typedef id (*SCIMsgSend)(id, SEL);
|
||||
typedef id (*SCIMsgSend1)(id, SEL, id);
|
||||
|
||||
static id sciCall(id obj, SEL sel) {
|
||||
if (!obj || ![obj respondsToSelector:sel]) return nil;
|
||||
return ((SCIMsgSend)objc_msgSend)(obj, sel);
|
||||
}
|
||||
static id sciCall1(id obj, SEL sel, id arg1) {
|
||||
if (!obj || ![obj respondsToSelector:sel]) return nil;
|
||||
return ((SCIMsgSend1)objc_msgSend)(obj, sel, arg1);
|
||||
}
|
||||
|
||||
static void sciAllowSeenForPK(id media) {
|
||||
if (!media) return;
|
||||
id pk = sciCall(media, @selector(pk));
|
||||
if (!pk) return;
|
||||
if (!sciAllowedSeenPKs) sciAllowedSeenPKs = [NSMutableSet set];
|
||||
NSString *pkStr = [NSString stringWithFormat:@"%@", pk];
|
||||
[sciAllowedSeenPKs addObject:pkStr];
|
||||
NSLog(@"[SCInsta] Allow-listed PK: %@", pkStr);
|
||||
}
|
||||
|
||||
static BOOL sciIsPKAllowed(id media) {
|
||||
if (!media || !sciAllowedSeenPKs || sciAllowedSeenPKs.count == 0) return NO;
|
||||
id pk = sciCall(media, @selector(pk));
|
||||
if (!pk) return NO;
|
||||
return [sciAllowedSeenPKs containsObject:[NSString stringWithFormat:@"%@", pk]];
|
||||
}
|
||||
|
||||
static BOOL sciShouldBlockSeenNetwork() {
|
||||
if (sciSeenBypassActive) return NO;
|
||||
return [SCIUtils getBoolPref:@"no_seen_receipt"];
|
||||
}
|
||||
|
||||
// Block story seen receipts by intercepting all known upload/send paths
|
||||
%hook IGStorySeenStateUploader
|
||||
- (id)initWithUserSessionPK:(id)arg1 networker:(id)arg2 {
|
||||
if (sciShouldBlockSeen()) {
|
||||
NSLog(@"[SCInsta] Blocked story seen uploader init");
|
||||
return nil;
|
||||
}
|
||||
return %orig;
|
||||
static BOOL sciShouldBlockSeenVisual() {
|
||||
if (sciSeenBypassActive) return NO;
|
||||
return [SCIUtils getBoolPref:@"no_seen_receipt"] && [SCIUtils getBoolPref:@"no_seen_visual"];
|
||||
}
|
||||
|
||||
static UIViewController * _Nullable sciFindVC(UIResponder *start, NSString *className) {
|
||||
Class cls = NSClassFromString(className);
|
||||
if (!cls) return nil;
|
||||
UIResponder *r = start;
|
||||
while (r) {
|
||||
if ([r isKindOfClass:cls]) return (UIViewController *)r;
|
||||
r = [r nextResponder];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
static IGMedia * _Nullable sciExtractMediaFromItem(id item) {
|
||||
if (!item) return nil;
|
||||
Class mediaClass = NSClassFromString(@"IGMedia");
|
||||
if (!mediaClass) return nil;
|
||||
NSArray *trySelectors = @[@"media", @"mediaItem", @"storyItem", @"item",
|
||||
@"feedItem", @"igMedia", @"model", @"backingModel",
|
||||
@"storyMedia", @"mediaModel"];
|
||||
for (NSString *selName in trySelectors) {
|
||||
id val = sciCall(item, NSSelectorFromString(selName));
|
||||
if (val && [val isKindOfClass:mediaClass]) return (IGMedia *)val;
|
||||
}
|
||||
unsigned int iCount = 0;
|
||||
Ivar *ivars = class_copyIvarList([item class], &iCount);
|
||||
for (unsigned int i = 0; i < iCount; i++) {
|
||||
const char *type = ivar_getTypeEncoding(ivars[i]);
|
||||
if (type && type[0] == '@') {
|
||||
id val = object_getIvar(item, ivars[i]);
|
||||
if (val && [val isKindOfClass:mediaClass]) { free(ivars); return (IGMedia *)val; }
|
||||
}
|
||||
}
|
||||
if (ivars) free(ivars);
|
||||
return nil;
|
||||
}
|
||||
|
||||
static id _Nullable sciGetCurrentStoryItem(UIResponder *start) {
|
||||
UIViewController *storyVC = sciFindVC(start, @"IGStoryViewerViewController");
|
||||
if (!storyVC) return nil;
|
||||
id vm = sciCall(storyVC, @selector(currentViewModel));
|
||||
if (!vm) return nil;
|
||||
return sciCall1(storyVC, @selector(currentStoryItemForViewModel:), vm);
|
||||
}
|
||||
|
||||
// Find section controller: VC -> collectionView -> visibleCell -> containerView -> delegate
|
||||
static id _Nullable sciFindSectionController(UIViewController *storyVC) {
|
||||
Class sectionClass = NSClassFromString(@"IGStoryFullscreenSectionController");
|
||||
if (!sectionClass || !storyVC) return nil;
|
||||
|
||||
// Find collection view in VC ivars
|
||||
unsigned int count = 0;
|
||||
Ivar *ivars = class_copyIvarList([storyVC class], &count);
|
||||
UICollectionView *cv = nil;
|
||||
for (unsigned int i = 0; i < count; i++) {
|
||||
const char *type = ivar_getTypeEncoding(ivars[i]);
|
||||
if (!type || type[0] != '@') continue;
|
||||
id val = object_getIvar(storyVC, ivars[i]);
|
||||
if (val && [val isKindOfClass:[UICollectionView class]]) { cv = val; break; }
|
||||
}
|
||||
if (ivars) free(ivars);
|
||||
if (!cv) return nil;
|
||||
|
||||
// Scan visible cells -> containerView -> delegate
|
||||
for (UICollectionViewCell *cell in cv.visibleCells) {
|
||||
unsigned int cCount = 0;
|
||||
Ivar *cIvars = class_copyIvarList([cell class], &cCount);
|
||||
for (unsigned int i = 0; i < cCount; i++) {
|
||||
const char *type = ivar_getTypeEncoding(cIvars[i]);
|
||||
if (!type || type[0] != '@') continue;
|
||||
id val = object_getIvar(cell, cIvars[i]);
|
||||
if (!val) continue;
|
||||
// Check val's ivars for section controller (L4: cell.containerView.delegate)
|
||||
unsigned int vCount = 0;
|
||||
Ivar *vIvars = class_copyIvarList([val class], &vCount);
|
||||
for (unsigned int j = 0; j < vCount; j++) {
|
||||
const char *type2 = ivar_getTypeEncoding(vIvars[j]);
|
||||
if (!type2 || type2[0] != '@') continue;
|
||||
id val2 = object_getIvar(val, vIvars[j]);
|
||||
if (val2 && [val2 isKindOfClass:sectionClass]) { free(vIvars); free(cIvars); return val2; }
|
||||
}
|
||||
if (vIvars) free(vIvars);
|
||||
}
|
||||
if (cIvars) free(cIvars);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Story downloaders
|
||||
static SCIDownloadDelegate *sciStoryVideoDl = nil;
|
||||
static SCIDownloadDelegate *sciStoryImageDl = nil;
|
||||
|
||||
static void sciInitStoryDownloaders() {
|
||||
NSString *method = [SCIUtils getStringPref:@"dw_save_action"];
|
||||
DownloadAction action = [method isEqualToString:@"photos"] ? saveToPhotos : share;
|
||||
DownloadAction imgAction = [method isEqualToString:@"photos"] ? saveToPhotos : quickLook;
|
||||
sciStoryVideoDl = [[SCIDownloadDelegate alloc] initWithAction:action showProgress:YES];
|
||||
sciStoryImageDl = [[SCIDownloadDelegate alloc] initWithAction:imgAction showProgress:NO];
|
||||
}
|
||||
|
||||
static void sciDownloadMedia(IGMedia *media) {
|
||||
sciInitStoryDownloaders();
|
||||
NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:media];
|
||||
if (videoUrl) {
|
||||
[sciStoryVideoDl downloadFileWithURL:videoUrl fileExtension:[[videoUrl lastPathComponent] pathExtension] hudLabel:nil];
|
||||
return;
|
||||
}
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrlForMedia:media];
|
||||
if (photoUrl) {
|
||||
[sciStoryImageDl downloadFileWithURL:photoUrl fileExtension:[[photoUrl lastPathComponent] pathExtension] hudLabel:nil];
|
||||
return;
|
||||
}
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract URL from story"];
|
||||
}
|
||||
|
||||
// ============ BLOCK NETWORK SEEN ============
|
||||
|
||||
%hook IGStorySeenStateUploader
|
||||
- (void)uploadSeenStateWithMedia:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
// Allow if: bypass active, or this specific media was manually marked
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)uploadSeenState {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
// Batch upload — allow if bypass or any manual PKs are pending
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !(sciAllowedSeenPKs && sciAllowedSeenPKs.count > 0)) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)_uploadSeenState:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)sendSeenReceipt:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
|
||||
%orig;
|
||||
}
|
||||
- (id)networker {
|
||||
if (sciShouldBlockSeen()) return nil;
|
||||
return %orig;
|
||||
}
|
||||
// NEVER block networker — returning nil breaks the uploader permanently
|
||||
- (id)networker { return %orig; }
|
||||
%end
|
||||
|
||||
// Block seen tracking on fullscreen section controller
|
||||
// ============ BLOCK VISUAL SEEN ============
|
||||
|
||||
%hook IGStoryFullscreenSectionController
|
||||
- (void)markItemAsSeen:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
// Visual seen blocking
|
||||
- (void)markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
|
||||
- (void)_markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
|
||||
- (void)storySeenStateDidChange:(id)arg1 { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
- (void)markCurrentItemAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
|
||||
// Stop auto-advance: block timer-triggered advances, allow manual taps
|
||||
- (void)storyPlayerMediaViewDidPlayToEnd:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"stop_story_auto_advance"]) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)_markItemAsSeen:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)storySeenStateDidChange:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)sendSeenRequestForCurrentItem {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)markCurrentItemAsSeen {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
- (void)advanceToNextReelForAutoScroll {
|
||||
if ([SCIUtils getBoolPref:@"stop_story_auto_advance"]) return;
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Block seen on viewer controller
|
||||
%hook IGStoryViewerViewController
|
||||
- (void)markAsSeen {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)markStoryAsSeen:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)_markCurrentStoryAsSeen {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)markCurrentMediaAsSeen {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
- (void)fullscreenSectionController:(id)arg1 didMarkItemAsSeen:(id)arg2 {
|
||||
if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg2)) return;
|
||||
%orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Block local visual seen state updates on the story tray
|
||||
// This prevents the colored ring from turning grey after viewing
|
||||
%hook IGStoryTrayViewModel
|
||||
- (void)markAsSeen {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)setHasUnseenMedia:(BOOL)arg1 {
|
||||
if (sciShouldBlockSeen()) {
|
||||
// Always keep as unseen visually
|
||||
%orig(YES);
|
||||
return;
|
||||
}
|
||||
%orig;
|
||||
}
|
||||
- (BOOL)hasUnseenMedia {
|
||||
if (sciShouldBlockSeen()) return YES;
|
||||
return %orig;
|
||||
}
|
||||
- (void)setIsSeen:(BOOL)arg1 {
|
||||
if (sciShouldBlockSeen()) {
|
||||
%orig(NO);
|
||||
return;
|
||||
}
|
||||
%orig;
|
||||
}
|
||||
- (BOOL)isSeen {
|
||||
if (sciShouldBlockSeen()) return NO;
|
||||
return %orig;
|
||||
}
|
||||
- (void)markAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; }
|
||||
- (void)setHasUnseenMedia:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(YES); return; } %orig; }
|
||||
- (BOOL)hasUnseenMedia { if (sciShouldBlockSeenVisual()) return YES; return %orig; }
|
||||
- (void)setIsSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||
- (BOOL)isSeen { if (sciShouldBlockSeenVisual()) return NO; return %orig; }
|
||||
%end
|
||||
|
||||
// Also try to block on the story item model level
|
||||
%hook IGStoryItem
|
||||
- (void)setHasSeen:(BOOL)arg1 {
|
||||
if (sciShouldBlockSeen()) {
|
||||
%orig(NO);
|
||||
return;
|
||||
}
|
||||
%orig;
|
||||
}
|
||||
- (BOOL)hasSeen {
|
||||
if (sciShouldBlockSeen()) return NO;
|
||||
return %orig;
|
||||
}
|
||||
- (void)setHasSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||
- (BOOL)hasSeen { if (sciShouldBlockSeenVisual()) return NO; return %orig; }
|
||||
%end
|
||||
|
||||
// Manual "mark as seen" button on story overlay
|
||||
%hook IGStoryGradientRingView
|
||||
- (void)setIsSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||
- (void)setSeen:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||
- (void)updateRingForSeenState:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
|
||||
%end
|
||||
|
||||
// ============ OVERLAY BUTTONS ============
|
||||
|
||||
%hook IGStoryFullscreenOverlayView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
if (!self.superview) return;
|
||||
|
||||
if (!sciShouldBlockSeen()) return;
|
||||
if ([self viewWithTag:1339]) return;
|
||||
|
||||
UIButton *seenBtn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
seenBtn.tag = 1339;
|
||||
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:14 weight:UIImageSymbolWeightMedium];
|
||||
UIImage *icon = [UIImage systemImageNamed:@"eye" withConfiguration:config];
|
||||
[seenBtn setImage:icon forState:UIControlStateNormal];
|
||||
[seenBtn setTitle:@" Mark seen" forState:UIControlStateNormal];
|
||||
[seenBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
seenBtn.titleLabel.font = [UIFont systemFontOfSize:11 weight:UIFontWeightMedium];
|
||||
seenBtn.tintColor = [UIColor whiteColor];
|
||||
seenBtn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||
seenBtn.layer.cornerRadius = 14;
|
||||
seenBtn.clipsToBounds = YES;
|
||||
seenBtn.contentEdgeInsets = UIEdgeInsetsMake(6, 10, 6, 12);
|
||||
|
||||
seenBtn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[seenBtn addTarget:self action:@selector(sciMarkSeenTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:seenBtn];
|
||||
|
||||
// Bottom right, moved up to avoid overlapping existing buttons
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[seenBtn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-110],
|
||||
[seenBtn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12],
|
||||
[seenBtn.heightAnchor constraintEqualToConstant:28]
|
||||
]];
|
||||
}
|
||||
|
||||
%new - (void)sciMarkSeenTapped:(UIButton *)sender {
|
||||
// Haptic feedback
|
||||
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[haptic impactOccurred];
|
||||
|
||||
// Visual feedback
|
||||
[UIView animateWithDuration:0.1 animations:^{
|
||||
sender.transform = CGAffineTransformMakeScale(0.85, 0.85);
|
||||
sender.alpha = 0.6;
|
||||
} completion:^(BOOL finished) {
|
||||
[UIView animateWithDuration:0.15 animations:^{
|
||||
sender.transform = CGAffineTransformIdentity;
|
||||
sender.alpha = 1.0;
|
||||
}];
|
||||
}];
|
||||
|
||||
// Enable bypass so all our hooks let the calls through
|
||||
sciSeenBypassActive = YES;
|
||||
|
||||
BOOL didMark = NO;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
// Try all view controllers in responder chain
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
// IGStoryViewerViewController
|
||||
if ([responder isKindOfClass:NSClassFromString(@"IGStoryViewerViewController")]) {
|
||||
SEL selectors[] = {
|
||||
@selector(markAsSeen), @selector(markStoryAsSeen:),
|
||||
@selector(_markCurrentStoryAsSeen), @selector(markCurrentMediaAsSeen)
|
||||
};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if ([responder respondsToSelector:selectors[i]]) {
|
||||
NSLog(@"[SCInsta] Manual seen: calling %@ on IGStoryViewerViewController", NSStringFromSelector(selectors[i]));
|
||||
if (selectors[i] == @selector(markStoryAsSeen:)) {
|
||||
[responder performSelector:selectors[i] withObject:nil];
|
||||
} else {
|
||||
[responder performSelector:selectors[i]];
|
||||
}
|
||||
didMark = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
// IGStoryFullscreenSectionController (might be in responder chain as next responder of a child VC)
|
||||
if ([responder isKindOfClass:NSClassFromString(@"IGStoryFullscreenSectionController")]) {
|
||||
SEL selectors[] = {
|
||||
@selector(markItemAsSeen:), @selector(markCurrentItemAsSeen),
|
||||
@selector(sendSeenRequestForCurrentItem)
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if ([responder respondsToSelector:selectors[i]]) {
|
||||
NSLog(@"[SCInsta] Manual seen: calling %@ on IGStoryFullscreenSectionController", NSStringFromSelector(selectors[i]));
|
||||
if (selectors[i] == @selector(markItemAsSeen:)) {
|
||||
[responder performSelector:selectors[i] withObject:nil];
|
||||
} else {
|
||||
[responder performSelector:selectors[i]];
|
||||
}
|
||||
didMark = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
responder = [responder nextResponder];
|
||||
if ([SCIUtils getBoolPref:@"dw_story"] && ![self viewWithTag:1340]) {
|
||||
UIButton *dlBtn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
dlBtn.tag = 1340;
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold];
|
||||
[dlBtn setImage:[UIImage systemImageNamed:@"arrow.down" withConfiguration:config] forState:UIControlStateNormal];
|
||||
dlBtn.tintColor = [UIColor whiteColor];
|
||||
dlBtn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||
dlBtn.layer.cornerRadius = 18;
|
||||
dlBtn.clipsToBounds = YES;
|
||||
dlBtn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[dlBtn addTarget:self action:@selector(sciStoryDownloadTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:dlBtn];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[dlBtn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-100],
|
||||
[dlBtn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12],
|
||||
[dlBtn.widthAnchor constraintEqualToConstant:36],
|
||||
[dlBtn.heightAnchor constraintEqualToConstant:36]
|
||||
]];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
if ([SCIUtils getBoolPref:@"no_seen_receipt"] && ![self viewWithTag:1339]) {
|
||||
UIButton *seenBtn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
seenBtn.tag = 1339;
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold];
|
||||
[seenBtn setImage:[UIImage systemImageNamed:@"eye" withConfiguration:config] forState:UIControlStateNormal];
|
||||
seenBtn.tintColor = [UIColor whiteColor];
|
||||
seenBtn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||
seenBtn.layer.cornerRadius = 18;
|
||||
seenBtn.clipsToBounds = YES;
|
||||
seenBtn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[seenBtn addTarget:self action:@selector(sciMarkSeenTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:seenBtn];
|
||||
UIView *dlBtn = [self viewWithTag:1340];
|
||||
if (dlBtn) {
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[seenBtn.centerYAnchor constraintEqualToAnchor:dlBtn.centerYAnchor],
|
||||
[seenBtn.trailingAnchor constraintEqualToAnchor:dlBtn.leadingAnchor constant:-10],
|
||||
[seenBtn.widthAnchor constraintEqualToConstant:36],
|
||||
[seenBtn.heightAnchor constraintEqualToConstant:36]
|
||||
]];
|
||||
} else {
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[seenBtn.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor constant:-100],
|
||||
[seenBtn.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-12],
|
||||
[seenBtn.widthAnchor constraintEqualToConstant:36],
|
||||
[seenBtn.heightAnchor constraintEqualToConstant:36]
|
||||
]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable blocking
|
||||
sciSeenBypassActive = NO;
|
||||
// ============ STORY DOWNLOAD ============
|
||||
|
||||
if (didMark) {
|
||||
[SCIUtils showToastForDuration:1.5 title:@"Marked as seen"];
|
||||
} else {
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Could not mark as seen" subtitle:@"Method not found"];
|
||||
%new - (void)sciStoryDownloadTapped:(UIButton *)sender {
|
||||
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[haptic impactOccurred];
|
||||
[UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformMakeScale(0.8, 0.8); }
|
||||
completion:^(BOOL f) { [UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformIdentity; }]; }];
|
||||
@try {
|
||||
id item = sciGetCurrentStoryItem(self);
|
||||
IGMedia *media = sciExtractMediaFromItem(item);
|
||||
if (media) {
|
||||
if ([SCIUtils getBoolPref:@"dw_confirm"]) {
|
||||
[SCIUtils showConfirmation:^{ sciDownloadMedia(media); } title:@"Download story?"];
|
||||
} else {
|
||||
sciDownloadMedia(media);
|
||||
}
|
||||
return;
|
||||
}
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not find story media"];
|
||||
} @catch (NSException *e) {
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]];
|
||||
}
|
||||
}
|
||||
|
||||
// ============ MARK SEEN ============
|
||||
|
||||
%new - (void)sciMarkSeenTapped:(UIButton *)sender {
|
||||
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[haptic impactOccurred];
|
||||
[UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformMakeScale(0.8, 0.8); sender.alpha = 0.6; }
|
||||
completion:^(BOOL f) { [UIView animateWithDuration:0.15 animations:^{ sender.transform = CGAffineTransformIdentity; sender.alpha = 1.0; }]; }];
|
||||
|
||||
@try {
|
||||
UIViewController *storyVC = sciFindVC(self, @"IGStoryViewerViewController");
|
||||
if (!storyVC) { [SCIUtils showErrorHUDWithDescription:@"Story VC not found"]; return; }
|
||||
|
||||
// Get current story media
|
||||
id sectionCtrl = sciFindSectionController(storyVC);
|
||||
id storyItem = sectionCtrl ? sciCall(sectionCtrl, NSSelectorFromString(@"currentStoryItem")) : nil;
|
||||
if (!storyItem) storyItem = sciGetCurrentStoryItem(self);
|
||||
IGMedia *media = (storyItem && [storyItem isKindOfClass:NSClassFromString(@"IGMedia")]) ? storyItem : sciExtractMediaFromItem(storyItem);
|
||||
|
||||
if (!media) { [SCIUtils showErrorHUDWithDescription:@"Could not find story media"]; return; }
|
||||
|
||||
// Add this media PK to the permanent allow list
|
||||
// When Instagram's deferred upload eventually fires, our hooks will let this PK through
|
||||
sciAllowSeenForPK(media);
|
||||
|
||||
// Also set bypass for immediate calls
|
||||
sciSeenBypassActive = YES;
|
||||
|
||||
// Trigger the visual seen update via VC delegate
|
||||
SEL delegateSel = @selector(fullscreenSectionController:didMarkItemAsSeen:);
|
||||
if ([storyVC respondsToSelector:delegateSel]) {
|
||||
typedef void (*Func)(id, SEL, id, id);
|
||||
((Func)objc_msgSend)(storyVC, delegateSel, sectionCtrl, media);
|
||||
}
|
||||
|
||||
// Trigger the section controller's mark flow
|
||||
if (sectionCtrl) {
|
||||
SEL markSel = NSSelectorFromString(@"markItemAsSeen:");
|
||||
if ([sectionCtrl respondsToSelector:markSel]) {
|
||||
((SCIMsgSend1)objc_msgSend)(sectionCtrl, markSel, media);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the session seen state manager
|
||||
id seenManager = sciCall(storyVC, @selector(viewingSessionSeenStateManager));
|
||||
id vm = sciCall(storyVC, @selector(currentViewModel));
|
||||
if (seenManager && vm) {
|
||||
SEL setSeenSel = NSSelectorFromString(@"setSeenMediaId:forReelPK:");
|
||||
if ([seenManager respondsToSelector:setSeenSel]) {
|
||||
id mediaPK = sciCall(media, @selector(pk));
|
||||
id reelPK = sciCall(vm, NSSelectorFromString(@"reelPK"));
|
||||
if (!reelPK) reelPK = sciCall(vm, @selector(pk));
|
||||
if (mediaPK && reelPK) {
|
||||
typedef void (*SetFunc)(id, SEL, id, id);
|
||||
((SetFunc)objc_msgSend)(seenManager, setSeenSel, mediaPK, reelPK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sciSeenBypassActive = NO;
|
||||
|
||||
[SCIUtils showToastForDuration:2.0 title:@"Marked as seen" subtitle:@"Will sync when leaving stories"];
|
||||
} @catch (NSException *e) {
|
||||
sciSeenBypassActive = NO;
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Error: %@", e.reason]];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
@@ -150,6 +150,8 @@
|
||||
[SCISetting switchCellWithTitle:@"Disable view-once limitations" subtitle:@"Makes view-once messages behave like normal visual messages (loopable/pauseable)" defaultsKey:@"disable_view_once_limitations"],
|
||||
[SCISetting switchCellWithTitle:@"Disable screenshot detection" subtitle:@"Removes the screenshot-prevention features for visual messages in DMs" defaultsKey:@"remove_screenshot_alert"],
|
||||
[SCISetting switchCellWithTitle:@"Disable story seen receipt" subtitle:@"Hides the notification for others when you view their story" defaultsKey:@"no_seen_receipt"],
|
||||
[SCISetting switchCellWithTitle:@"Keep stories visually unseen" subtitle:@"Prevents stories from visually marking as seen in the tray (keeps colorful ring)" defaultsKey:@"no_seen_visual"],
|
||||
[SCISetting switchCellWithTitle:@"Stop story auto-advance" subtitle:@"Stories won't auto-skip to the next one when the timer ends. Tap to advance manually" defaultsKey:@"stop_story_auto_advance"],
|
||||
[SCISetting switchCellWithTitle:@"Disable instants creation" subtitle:@"Hides the functionality to create/send instants" defaultsKey:@"disable_instants_creation" requiresRestart:YES]
|
||||
]
|
||||
}]
|
||||
@@ -282,9 +284,10 @@
|
||||
@"header": @"Credits",
|
||||
@"rows": @[
|
||||
[SCISetting linkCellWithTitle:@"Developer" subtitle:@"SoCuul" imageUrl:@"https://i.imgur.com/c9CbytZ.png" url:@"https://socuul.dev"],
|
||||
[SCISetting linkCellWithTitle:@"Modded by" subtitle:@"Ryuk" imageUrl:@"https://github.com/faroukbmiled.png" url:@"https://github.com/faroukbmiled"],
|
||||
[SCISetting linkCellWithTitle:@"View Repo" subtitle:@"View the tweak's source code on GitHub" imageUrl:@"https://i.imgur.com/BBUNzeP.png" url:@"https://github.com/SoCuul/SCInsta"]
|
||||
],
|
||||
@"footer": [NSString stringWithFormat:@"SCInsta %@\n\nInstagram v%@", SCIVersionString, [SCIUtils IGVersionString]]
|
||||
@"footer": [NSString stringWithFormat:@"SCInsta %@\n\nInstagram v%@\n\nModded by Ryuk", SCIVersionString, [SCIUtils IGVersionString]]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
+3
-2
@@ -13,7 +13,7 @@
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// * Tweak version *
|
||||
NSString *SCIVersionString = @"v1.1.2";
|
||||
NSString *SCIVersionString = @"v1.1.3";
|
||||
|
||||
// Variables that work across features
|
||||
BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
@@ -44,7 +44,8 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
@"enable_notes_customization": @(YES),
|
||||
@"custom_note_themes": @(YES),
|
||||
@"disable_auto_unmuting_reels": @(YES),
|
||||
@"doom_scrolling_reel_count": @(1)
|
||||
@"doom_scrolling_reel_count": @(1),
|
||||
@"no_seen_visual": @(YES)
|
||||
};
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:sciDefaults];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user