mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-06 15:33:53 +02:00
modded scinsta with additional features and fixes for recent instagram version
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "../../modules/JGProgressHUD/JGProgressHUD.h"
|
||||
|
||||
#import "../InstagramHeaders.h"
|
||||
#import "../Utils.h"
|
||||
|
||||
#import "Manager.h"
|
||||
|
||||
@interface SCIDownloadPillView : UIView
|
||||
@property (nonatomic, strong) UIProgressView *progressRing;
|
||||
@property (nonatomic, strong) UILabel *textLabel;
|
||||
@property (nonatomic, strong) UILabel *subtitleLabel;
|
||||
@property (nonatomic, strong) UIButton *cancelButton;
|
||||
@property (nonatomic, copy) void (^onCancel)(void);
|
||||
|
||||
- (void)showInView:(UIView *)view;
|
||||
- (void)dismiss;
|
||||
- (void)dismissAfterDelay:(NSTimeInterval)delay;
|
||||
- (void)setProgress:(float)progress;
|
||||
- (void)setText:(NSString *)text;
|
||||
@end
|
||||
|
||||
@interface SCIDownloadDelegate : NSObject <SCIDownloadDelegateProtocol>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, DownloadAction) {
|
||||
share,
|
||||
quickLook,
|
||||
saveToPhotos
|
||||
};
|
||||
@property (nonatomic, readonly) DownloadAction action;
|
||||
@property (nonatomic, readonly) BOOL showProgress;
|
||||
|
||||
@property (nonatomic, strong) SCIDownloadManager *downloadManager;
|
||||
@property (nonatomic, strong) SCIDownloadPillView *pill;
|
||||
|
||||
- (instancetype)initWithAction:(DownloadAction)action showProgress:(BOOL)showProgress;
|
||||
|
||||
- (void)downloadFileWithURL:(NSURL *)url fileExtension:(NSString *)fileExtension hudLabel:(NSString *)hudLabel;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,261 @@
|
||||
#import "Download.h"
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
#pragma mark - SCIDownloadPillView
|
||||
|
||||
@implementation SCIDownloadPillView
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor colorWithWhite:0.1 alpha:0.92];
|
||||
self.layer.cornerRadius = 20;
|
||||
self.clipsToBounds = YES;
|
||||
self.alpha = 0;
|
||||
|
||||
// Circular progress (using a small CAShapeLayer ring)
|
||||
_progressRing = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
|
||||
_progressRing.progressTintColor = [UIColor systemBlueColor];
|
||||
_progressRing.trackTintColor = [UIColor colorWithWhite:0.3 alpha:1.0];
|
||||
_progressRing.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_progressRing.layer.cornerRadius = 2;
|
||||
_progressRing.clipsToBounds = YES;
|
||||
[self addSubview:_progressRing];
|
||||
|
||||
// Text
|
||||
_textLabel = [[UILabel alloc] init];
|
||||
_textLabel.text = @"Downloading 0%";
|
||||
_textLabel.textColor = [UIColor whiteColor];
|
||||
_textLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightSemibold];
|
||||
_textLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_textLabel];
|
||||
|
||||
// Subtitle
|
||||
_subtitleLabel = [[UILabel alloc] init];
|
||||
_subtitleLabel.text = @"Tap to cancel";
|
||||
_subtitleLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0];
|
||||
_subtitleLabel.font = [UIFont systemFontOfSize:10 weight:UIFontWeightRegular];
|
||||
_subtitleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self addSubview:_subtitleLabel];
|
||||
|
||||
// Tap gesture for cancel
|
||||
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap)];
|
||||
[self addGestureRecognizer:tap];
|
||||
|
||||
// Layout: [progress bar]
|
||||
// [text centered]
|
||||
// [subtitle centered]
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_progressRing.topAnchor constraintEqualToAnchor:self.topAnchor constant:12],
|
||||
[_progressRing.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:16],
|
||||
[_progressRing.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-16],
|
||||
[_progressRing.heightAnchor constraintEqualToConstant:4],
|
||||
|
||||
[_textLabel.topAnchor constraintEqualToAnchor:_progressRing.bottomAnchor constant:6],
|
||||
[_textLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
||||
|
||||
[_subtitleLabel.topAnchor constraintEqualToAnchor:_textLabel.bottomAnchor constant:2],
|
||||
[_subtitleLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
||||
[_subtitleLabel.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-10],
|
||||
]];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleTap {
|
||||
if (self.onCancel) self.onCancel();
|
||||
}
|
||||
|
||||
- (void)showInView:(UIView *)view {
|
||||
[self removeFromSuperview];
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[view addSubview:self];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.topAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.topAnchor constant:4],
|
||||
[self.centerXAnchor constraintEqualToAnchor:view.centerXAnchor],
|
||||
[self.widthAnchor constraintGreaterThanOrEqualToConstant:160],
|
||||
[self.widthAnchor constraintLessThanOrEqualToConstant:220],
|
||||
]];
|
||||
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
self.alpha = 1;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismiss {
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
self.alpha = 0;
|
||||
} completion:^(BOOL finished) {
|
||||
[self removeFromSuperview];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)dismissAfterDelay:(NSTimeInterval)delay {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self dismiss];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setProgress:(float)progress {
|
||||
[self.progressRing setProgress:progress animated:YES];
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *)text {
|
||||
self.textLabel.text = text;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - SCIDownloadDelegate
|
||||
|
||||
@implementation SCIDownloadDelegate
|
||||
|
||||
- (instancetype)initWithAction:(DownloadAction)action showProgress:(BOOL)showProgress {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_action = action;
|
||||
_showProgress = showProgress;
|
||||
self.downloadManager = [[SCIDownloadManager alloc] initWithDelegate:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)downloadFileWithURL:(NSURL *)url fileExtension:(NSString *)fileExtension hudLabel:(NSString *)hudLabel {
|
||||
// Dismiss any existing pill
|
||||
[self.pill dismiss];
|
||||
|
||||
self.pill = [[SCIDownloadPillView alloc] init];
|
||||
|
||||
if (hudLabel) {
|
||||
[self.pill setText:hudLabel];
|
||||
}
|
||||
|
||||
if (!self.showProgress) {
|
||||
self.pill.progressRing.hidden = YES;
|
||||
self.pill.subtitleLabel.text = nil;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.pill.onCancel = ^{
|
||||
[weakSelf.downloadManager cancelDownload];
|
||||
};
|
||||
|
||||
UIViewController *topVC = topMostController();
|
||||
UIView *hostView = topVC.view;
|
||||
if (!hostView) hostView = [UIApplication sharedApplication].keyWindow;
|
||||
if (!hostView) {
|
||||
NSLog(@"[SCInsta] Download: No valid view");
|
||||
return;
|
||||
}
|
||||
[self.pill showInView:hostView];
|
||||
|
||||
NSLog(@"[SCInsta] Download: Will start download for url \"%@\" with file extension: \".%@\"", url, fileExtension);
|
||||
[self.downloadManager downloadFileWithURL:url fileExtension:fileExtension];
|
||||
}
|
||||
|
||||
- (void)downloadDidStart {
|
||||
NSLog(@"[SCInsta] Download: Download started");
|
||||
}
|
||||
|
||||
- (void)downloadDidCancel {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.pill setText:@"Cancelled"];
|
||||
self.pill.subtitleLabel.text = nil;
|
||||
self.pill.progressRing.hidden = YES;
|
||||
[self.pill dismissAfterDelay:0.8];
|
||||
});
|
||||
NSLog(@"[SCInsta] Download: Download cancelled");
|
||||
}
|
||||
|
||||
- (void)downloadDidProgress:(float)progress {
|
||||
if (self.showProgress) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.pill setProgress:progress];
|
||||
[self.pill setText:[NSString stringWithFormat:@"Downloading %d%%", (int)(progress * 100)]];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)downloadDidFinishWithError:(NSError *)error {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error && error.code != NSURLErrorCancelled) {
|
||||
NSLog(@"[SCInsta] Download: Download failed with error: \"%@\"", error);
|
||||
[self.pill setText:@"Download failed"];
|
||||
self.pill.subtitleLabel.text = nil;
|
||||
self.pill.progressRing.hidden = YES;
|
||||
[self.pill dismissAfterDelay:2.0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)downloadDidFinishWithFileURL:(NSURL *)fileURL {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.pill dismiss];
|
||||
|
||||
NSLog(@"[SCInsta] Download: Finished with url: \"%@\"", [fileURL absoluteString]);
|
||||
|
||||
switch (self.action) {
|
||||
case share:
|
||||
[SCIUtils showShareVC:fileURL];
|
||||
break;
|
||||
|
||||
case quickLook:
|
||||
[SCIUtils showQuickLookVC:@[fileURL]];
|
||||
break;
|
||||
|
||||
case saveToPhotos: {
|
||||
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
||||
if (status != PHAuthorizationStatusAuthorized) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[SCIUtils showErrorHUDWithDescription:@"Photo library access denied"];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
||||
NSString *ext = [[fileURL pathExtension] lowercaseString];
|
||||
BOOL isVideo = [@[@"mp4", @"mov", @"m4v"] containsObject:ext];
|
||||
|
||||
if (isVideo) {
|
||||
PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset];
|
||||
PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init];
|
||||
opts.shouldMoveFile = YES;
|
||||
[req addResourceWithType:PHAssetResourceTypeVideo fileURL:fileURL options:opts];
|
||||
req.creationDate = [NSDate date];
|
||||
} else {
|
||||
PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset];
|
||||
PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init];
|
||||
opts.shouldMoveFile = YES;
|
||||
[req addResourceWithType:PHAssetResourceTypePhoto fileURL:fileURL options:opts];
|
||||
req.creationDate = [NSDate date];
|
||||
}
|
||||
} completionHandler:^(BOOL success, NSError *error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (success) {
|
||||
SCIDownloadPillView *donePill = [[SCIDownloadPillView alloc] init];
|
||||
donePill.progressRing.hidden = YES;
|
||||
donePill.subtitleLabel.text = nil;
|
||||
[donePill setText:@"Saved to Photos"];
|
||||
UIView *hostView = topMostController().view;
|
||||
if (hostView) {
|
||||
[donePill showInView:hostView];
|
||||
[donePill dismissAfterDelay:1.5];
|
||||
}
|
||||
} else {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Failed to save to Photos"];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol SCIDownloadDelegateProtocol <NSObject>
|
||||
|
||||
// Methods
|
||||
- (void)downloadDidStart;
|
||||
- (void)downloadDidCancel;
|
||||
- (void)downloadDidProgress:(float)progress;
|
||||
- (void)downloadDidFinishWithError:(NSError *)error;
|
||||
- (void)downloadDidFinishWithFileURL:(NSURL *)fileURL;
|
||||
|
||||
@end
|
||||
|
||||
@interface SCIDownloadManager : NSObject <NSURLSessionDownloadDelegate>
|
||||
|
||||
// Properties
|
||||
@property (nonatomic, weak) id<SCIDownloadDelegateProtocol> delegate;
|
||||
@property (nonatomic, strong) NSURLSession *session;
|
||||
@property (nonatomic, strong) NSURLSessionDownloadTask *task;
|
||||
@property (nonatomic, strong) NSString *fileExtension;
|
||||
|
||||
// Methods
|
||||
- (instancetype)initWithDelegate:(id<SCIDownloadDelegateProtocol>)downloadDelegate;
|
||||
|
||||
- (void)downloadFileWithURL:(NSURL *)url fileExtension:(NSString *)fileExtension;
|
||||
|
||||
- (void)cancelDownload;
|
||||
|
||||
- (NSURL *)moveFileToCacheDir:(NSURL *)oldPath;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,75 @@
|
||||
#import "Manager.h"
|
||||
|
||||
@implementation SCIDownloadManager
|
||||
|
||||
- (instancetype)initWithDelegate:(id<SCIDownloadDelegateProtocol>)downloadDelegate {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
self.delegate = downloadDelegate;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)downloadFileWithURL:(NSURL *)url fileExtension:(NSString *)fileExtension {
|
||||
// Properties
|
||||
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
|
||||
self.task = [self.session downloadTaskWithURL:url];
|
||||
|
||||
// Default to jpg if no other reasonable length extension is provided
|
||||
self.fileExtension = [fileExtension length] >= 3 ? fileExtension : @"jpg";
|
||||
|
||||
[self.task resume];
|
||||
[self.delegate downloadDidStart];
|
||||
}
|
||||
|
||||
- (void)cancelDownload {
|
||||
[self.task cancel];
|
||||
[self.delegate downloadDidCancel];
|
||||
}
|
||||
|
||||
// URLSession methods
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
||||
NSLog(@"Task wrote %lld bytes of %lld bytes", bytesWritten, totalBytesExpectedToWrite);
|
||||
|
||||
float progress = (float)totalBytesWritten / (float)totalBytesExpectedToWrite;
|
||||
|
||||
[self.delegate downloadDidProgress:progress];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
|
||||
// Move downloaded file to cache directory
|
||||
NSURL *finalLocation = [self moveFileToCacheDir:location];
|
||||
|
||||
[self.delegate downloadDidFinishWithFileURL:finalLocation];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
|
||||
NSLog(@"Task completed with error: %@", error);
|
||||
|
||||
[self.delegate downloadDidFinishWithError:error];
|
||||
}
|
||||
|
||||
// Rename downloaded file & move from documents dir -> cache dir
|
||||
- (NSURL *)moveFileToCacheDir:(NSURL *)oldPath {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
|
||||
NSString *cacheDirectoryPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
|
||||
NSURL *newPath = [[NSURL fileURLWithPath:cacheDirectoryPath] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", NSUUID.UUID.UUIDString, self.fileExtension]];
|
||||
|
||||
NSLog(@"[SCInsta] Download Handler: Moving file from: %@ to: %@", oldPath.absoluteString, newPath.absoluteString);
|
||||
|
||||
// Move file to cache directory
|
||||
NSError *fileMoveError;
|
||||
[fileManager moveItemAtURL:oldPath toURL:newPath error:&fileMoveError];
|
||||
|
||||
if (fileMoveError) {
|
||||
NSLog(@"[SCInsta] Download Handler: Error while moving file: %@", oldPath.absoluteString);
|
||||
NSLog(@"[SCInsta] Download Handler: %@", fileMoveError);
|
||||
}
|
||||
|
||||
return newPath;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,25 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGDirectThreadCallButtonsCoordinator
|
||||
// Voice Call
|
||||
- (void)_didTapAudioButton:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"call_confirm"]) {
|
||||
NSLog(@"[SCInsta] Call confirm triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
// Video Call
|
||||
- (void)_didTapVideoButton:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"call_confirm"]) {
|
||||
NSLog(@"[SCInsta] Call confirm triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,35 @@
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGDirectThreadThemePickerViewController
|
||||
- (void)themeNewPickerSectionController:(id)arg1 didSelectTheme:(id)arg2 atIndex:(NSInteger)arg3 {
|
||||
if ([SCIUtils getBoolPref:@"change_direct_theme_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm change direct theme triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
- (void)themePickerSectionController:(id)arg1 didSelectThemeId:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"change_direct_theme_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm change direct theme triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGDirectThreadThemeKitSwift.IGDirectThreadThemePreviewController
|
||||
- (void)primaryButtonTapped {
|
||||
if ([SCIUtils getBoolPref:@"change_direct_theme_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm change direct theme triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,38 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
// Legacy hook (for non ai voices interface)
|
||||
%hook IGDirectThreadViewController
|
||||
- (void)voiceRecordViewController:(id)arg1 didRecordAudioClipWithURL:(id)arg2 waveform:(id)arg3 duration:(CGFloat)arg4 entryPoint:(NSInteger)arg5 {
|
||||
if ([SCIUtils getBoolPref:@"voice_message_confirm"]) {
|
||||
NSLog(@"[SCInsta] DM audio message confirm triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Workaround until I can figure out how to stop long press recording from automatically sending
|
||||
%hook IGDirectComposer
|
||||
- (void)_didLongPressVoiceMessage:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"voice_message_confirm"]) {
|
||||
return;
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Demangled name: IGDirectAIVoiceUIKit.CompactBarContentView
|
||||
%hook _TtC20IGDirectAIVoiceUIKitP33_5754F7617E0D924F9A84EFA352BBD29A21CompactBarContentView
|
||||
- (void)didTapSend {
|
||||
if ([SCIUtils getBoolPref:@"voice_message_confirm"]) {
|
||||
NSLog(@"[SCInsta] DM audio message confirm triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,103 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
#define CONFIRMFOLLOW(orig) \
|
||||
if ([SCIUtils getBoolPref:@"follow_confirm"]) { \
|
||||
NSLog(@"[SCInsta] Confirm follow triggered"); \
|
||||
\
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
} \
|
||||
else { \
|
||||
return orig; \
|
||||
} \
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
// Follow button on profile page
|
||||
%hook IGFollowController
|
||||
- (void)_didPressFollowButton {
|
||||
// Get user follow status (check if already following user)
|
||||
NSInteger UserFollowStatus = self.user.followStatus;
|
||||
|
||||
// Only show confirm dialog if user is not following
|
||||
if (UserFollowStatus == 2) {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Follow button on discover people page
|
||||
%hook IGDiscoverPeopleButtonGroupView
|
||||
- (void)_onFollowButtonTapped:(id)arg1 {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
- (void)_onFollowingButtonTapped:(id)arg1 {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Suggested for you (home feed & profile) follow button
|
||||
%hook IGHScrollAYMFCell
|
||||
- (void)_didTapAYMFActionButton {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
%end
|
||||
%hook IGHScrollAYMFActionButton
|
||||
- (void)_didTapTextActionButton {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Follow button on reels
|
||||
%hook IGUnifiedVideoFollowButton
|
||||
- (void)_hackilyHandleOurOwnButtonTaps:(id)arg1 event:(id)arg2 {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Follow text on profile (when collapsed into top bar)
|
||||
%hook IGProfileViewController
|
||||
- (void)navigationItemsControllerDidTapHeaderFollowButton:(id)arg1 {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Follow button on suggested friends (in story section)
|
||||
%hook IGStorySectionController
|
||||
- (void)followButtonTapped:(id)arg1 cell:(id)arg2 {
|
||||
CONFIRMFOLLOW(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Follow all button in group chats (3+ members) people view
|
||||
static void (*orig_listSectionController)(id, SEL, id, id);
|
||||
|
||||
static void hooked_listSectionController(id self, SEL _cmd, id arg1, id arg2) {
|
||||
if ([SCIUtils getBoolPref:@"follow_confirm"]) {
|
||||
|
||||
[SCIUtils showConfirmation:^{
|
||||
orig_listSectionController(self, _cmd, arg1, arg2);
|
||||
}];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
orig_listSectionController(self, _cmd, arg1, arg2);
|
||||
}
|
||||
|
||||
%ctor {
|
||||
Class cls = objc_getClass("IGDirectDetailMembersKit.IGDirectThreadDetailsMembersListViewController");
|
||||
if (!cls) return;
|
||||
|
||||
MSHookMessageEx(
|
||||
cls,
|
||||
@selector(listSectionController:didTapHeaderButtonWithViewModel:),
|
||||
(IMP)hooked_listSectionController,
|
||||
(IMP *)&orig_listSectionController
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGPendingRequestView
|
||||
- (void)_onApproveButtonTapped {
|
||||
if ([SCIUtils getBoolPref:@"follow_request_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm follow request triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
- (void)_onIgnoreButtonTapped {
|
||||
if ([SCIUtils getBoolPref:@"follow_request_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm follow request triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,150 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// Confirmation handlers
|
||||
|
||||
#define CONFIRMPOSTLIKE(orig) \
|
||||
if ([SCIUtils getBoolPref:@"like_confirm"]) { \
|
||||
NSLog(@"[SCInsta] Confirm post like triggered"); \
|
||||
\
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
} \
|
||||
else { \
|
||||
return orig; \
|
||||
} \
|
||||
|
||||
#define CONFIRMREELSLIKE(orig) \
|
||||
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) { \
|
||||
NSLog(@"[SCInsta] Confirm reels like triggered"); \
|
||||
\
|
||||
[SCIUtils showConfirmation:^(void) { orig; }]; \
|
||||
} \
|
||||
else { \
|
||||
return orig; \
|
||||
} \
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// Liking posts
|
||||
%hook IGUFIButtonBarView
|
||||
- (void)_onLikeButtonPressed:(id)arg1 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
%hook IGFeedPhotoView
|
||||
- (void)_onDoubleTap:(id)arg1 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
%hook IGVideoPlayerOverlayContainerView
|
||||
- (void)_handleDoubleTapGesture:(id)arg1 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Liking reels
|
||||
%hook IGSundialViewerVideoCell
|
||||
- (void)controlsOverlayControllerDidTapLikeButton:(id)arg1 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
- (void)controlsOverlayControllerDidLongPressLikeButton:(id)arg1 gestureRecognizer:(id)arg2 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
- (void)gestureController:(id)arg1 didObserveDoubleTap:(id)arg2 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
%hook IGSundialViewerPhotoCell
|
||||
- (void)controlsOverlayControllerDidTapLikeButton:(id)arg1 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
- (void)gestureController:(id)arg1 didObserveDoubleTap:(id)arg2 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
%hook IGSundialViewerCarouselCell
|
||||
- (void)controlsOverlayControllerDidTapLikeButton:(id)arg1 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
- (void)gestureController:(id)arg1 didObserveDoubleTap:(id)arg2 {
|
||||
CONFIRMREELSLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Liking comments
|
||||
%hook IGCommentCellController
|
||||
- (void)commentCell:(id)arg1 didTapLikeButton:(id)arg2 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)commentCell:(id)arg1 didTapLikedByButtonForUser:(id)arg2 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)commentCellDidLongPressOnLikeButton:(id)arg1 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)commentCellDidEndLongPressOnLikeButton:(id)arg1 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)commentCellDidDoubleTap:(id)arg1 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
%hook IGFeedItemPreviewCommentCell
|
||||
- (void)_didTapLikeButton {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
|
||||
// Liking stories
|
||||
%hook IGStoryFullscreenDefaultFooterView
|
||||
- (void)_handleLikeTapped {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)_likeTapped {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
- (void)inputView:(id)arg1 didTapLikeButton:(id)arg2 {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
|
||||
// For some stupid reason they removed the "liketapped" methods on newer Instagram versions
|
||||
// Now we have to do a shitty workaround instead :(
|
||||
// Works 99% of the time, but sometimes clicks get through directly to the like button (somehow)
|
||||
- (void)layoutSubviews {
|
||||
%orig;
|
||||
|
||||
if (![SCIUtils getBoolPref:@"like_confirm"]) return;
|
||||
|
||||
UIButton *likeButton = [self valueForKey:@"likeButton"];
|
||||
if (!likeButton) return;
|
||||
|
||||
// 129115 = L(12) I(9) K(11) E(5)
|
||||
static NSInteger kOverlayTag = 129115;
|
||||
if ([likeButton viewWithTag:kOverlayTag]) return;
|
||||
|
||||
UIButton *overlay = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
overlay.tag = kOverlayTag;
|
||||
overlay.frame = likeButton.bounds;
|
||||
overlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[overlay addTarget:self action:@selector(overlayTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[likeButton addSubview:overlay];
|
||||
}
|
||||
|
||||
%new - (void)overlayTapped:(UIButton *)overlay {
|
||||
UIButton *likeButton = (UIButton *)overlay.superview;
|
||||
|
||||
[SCIUtils showConfirmation:^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[likeButton sendActionsForControlEvents:UIControlEventTouchUpInside];
|
||||
});
|
||||
}];
|
||||
}
|
||||
%end
|
||||
|
||||
// DM like button (seems to be hidden)
|
||||
%hook IGDirectThreadViewController
|
||||
- (void)_didTapLikeButton {
|
||||
CONFIRMPOSTLIKE(%orig);
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGCommentComposer.IGCommentComposerController
|
||||
- (void)onSendButtonTap {
|
||||
if ([SCIUtils getBoolPref:@"post_comment_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm post comment triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,33 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGDirectThreadViewController
|
||||
- (void)swipeableScrollManagerDidEndDraggingAboveSwipeThreshold:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"shh_mode_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm shh mode triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)shhModeTransitionButtonDidTap:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"shh_mode_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm shh mode triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)messageListViewControllerDidToggleShhMode:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"shh_mode_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm shh mode triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGStoryViewerTapTarget
|
||||
- (void)_didTap:(id)arg1 forEvent:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"sticker_interact_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm sticker interact triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
} else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,35 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
%hook IGStoryTextEntryControlsOverlayView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"enable_hidden_texteffectsstyles"]) {
|
||||
|
||||
// Clear previous option values
|
||||
[self.animationTypes removeAllObjects];
|
||||
[self.effectTypes removeAllObjects];
|
||||
|
||||
// Generate new animation values
|
||||
// * Animation effects <= 9 are invalid
|
||||
// * Animations past the maximum count (changing each app update) will crash the app when selected
|
||||
for (int i = 10; i <= 76; i++) {
|
||||
[self.animationTypes addObject:@(i)];
|
||||
}
|
||||
|
||||
// Generate new effect values
|
||||
// * Effects past the maximum count (changing each app update) will gracefully just do nothing
|
||||
for (int i = 0; i <= 84; i++) {
|
||||
[self.effectTypes addObject:@(i)];
|
||||
}
|
||||
|
||||
// Refresh option picker
|
||||
if ([self respondsToSelector:@selector(reloadData)]) {
|
||||
NSLog(@"[SCInsta] Enable all text effects: Reloading data...");
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,26 @@
|
||||
%hook IGSundialFeedViewController
|
||||
- (_Bool)_isHomecomingEnabled {
|
||||
return true;
|
||||
}
|
||||
- (_Bool)_isHomeComingHomeFeed {
|
||||
return true;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGSundialViewerManagedRequestItem
|
||||
- (id)initWithMedia:(id)media launcherSet:(id)set isHomecomingEnabled:(_Bool)enabled {
|
||||
return %orig(media, set, true);
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGTabBarViewControllerManager
|
||||
- (_Bool)_isHomecomingEnabled {
|
||||
return true;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGMainAppSurfaceIntent
|
||||
+ (id)resolvedHomeAppSurfaceIntentWithIsHomecomingEnabled:(_Bool)enabled {
|
||||
return %orig(true);
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,10 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
// Demangled name: IGFeedPlayback.IGFeedPlaybackStrategy
|
||||
%hook _TtC14IGFeedPlayback22IGFeedPlaybackStrategy
|
||||
- (id)initWithShouldDisableAutoplay:(_Bool)autoplay {
|
||||
if ([SCIUtils getBoolPref:@"disable_feed_autoplay"]) return %orig(true);
|
||||
|
||||
return %orig(autoplay);
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,334 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
static NSArray *removeItemsInList(NSArray *list, BOOL isFeed) {
|
||||
NSArray *originalObjs = list;
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
// Remove suggested posts
|
||||
if (isFeed && [SCIUtils getBoolPref:@"no_suggested_post"]) {
|
||||
|
||||
// Posts
|
||||
if (
|
||||
([obj isKindOfClass:%c(IGMedia)] && [((IGMedia *)obj).explorePostInFeed isEqual:@YES])
|
||||
|| ([obj isKindOfClass:%c(IGFeedGroupHeaderViewModel)] && [[obj title] isEqualToString:@"Suggested Posts"])
|
||||
) {
|
||||
NSLog(@"[SCInsta] Removing suggested posts");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Suggested stories (carousel)
|
||||
if ([obj isKindOfClass:%c(IGInFeedStoriesTrayModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested stories carousel");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Remove suggested reels (carousel)
|
||||
if (isFeed && [SCIUtils getBoolPref:@"no_suggested_reels"]) {
|
||||
if ([obj isKindOfClass:%c(IGFeedScrollableClipsModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested reels carousel");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove suggested for you (accounts)
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_account"]) {
|
||||
|
||||
// Feed
|
||||
if (isFeed && [obj isKindOfClass:%c(IGHScrollAYMFModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding accounts suggested for you (feed)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reels
|
||||
if ([obj isKindOfClass:%c(IGSuggestedUserInReelsModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding accounts suggested for you (reels)");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove suggested threads posts
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_threads"]) {
|
||||
|
||||
// Feed (carousel)
|
||||
if (isFeed) {
|
||||
if ([obj isKindOfClass:%c(IGBloksFeedUnitModel)] || [obj isKindOfClass:objc_getClass("IGThreadsInFeedModels.IGThreadsInFeedModel")]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested threads posts (carousel)");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Reels
|
||||
if ([obj isKindOfClass:%c(IGSundialNetegoItem)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested threads posts (reels)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Remove story tray
|
||||
if (isFeed && [SCIUtils getBoolPref:@"hide_stories_tray"]) {
|
||||
if ([obj isKindOfClass:%c(IGStoryDataController)]) {
|
||||
NSLog(@"[SCInsta] Hiding stories tray");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide entire feed
|
||||
if (isFeed && [SCIUtils getBoolPref:@"hide_entire_feed"]) {
|
||||
if ([obj isKindOfClass:%c(IGPostCreationManager)] || [obj isKindOfClass:%c(IGMedia)] || [obj isKindOfClass:%c(IGEndOfFeedDemarcatorModel)] || [obj isKindOfClass:%c(IGSpinnerLabelViewModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding entire feed");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ads
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
if (
|
||||
([obj isKindOfClass:%c(IGFeedItem)] && ([obj isSponsored] || [obj isSponsoredApp]))
|
||||
|| ([obj isKindOfClass:%c(IGDiscoveryGridItem)] && [[obj model] isKindOfClass:%c(IGAdItem)])
|
||||
|| [obj isKindOfClass:%c(IGAdItem)]
|
||||
) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
|
||||
// Suggested posts/reels
|
||||
%hook IGMainFeedListAdapterDataSource
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *filteredObjs = removeItemsInList(%orig, YES);
|
||||
|
||||
// Remove loading spinner at end of feed (if 5 or less items in feed)
|
||||
NSUInteger arrayLength = [filteredObjs count];
|
||||
|
||||
if (arrayLength <= 5) {
|
||||
filteredObjs = [filteredObjs filteredArrayUsingPredicate:
|
||||
[NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
|
||||
return ![obj isKindOfClass:[%c(IGSpinnerLabelViewModel) class]];
|
||||
}]
|
||||
];
|
||||
}
|
||||
|
||||
return filteredObjs;
|
||||
}
|
||||
%end
|
||||
%hook IGSundialFeedDataSource
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *filteredList = removeItemsInList(%orig, NO);
|
||||
|
||||
if ([SCIUtils getBoolPref:@"prevent_doom_scrolling"]) {
|
||||
double reelCount = [SCIUtils getDoublePref:@"doom_scrolling_reel_count"];
|
||||
return [filteredList subarrayWithRange:NSMakeRange(0, MIN((NSUInteger)reelCount, filteredList.count))];
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
%end
|
||||
%hook IGContextualFeedViewController
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return removeItemsInList(%orig, NO);
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGVideoFeedViewController
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return removeItemsInList(%orig, NO);
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGChainingFeedViewController
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return removeItemsInList(%orig, NO);
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGStoryAdPool
|
||||
- (id)initWithUserSession:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGStoryAdsManager
|
||||
- (id)initWithUserSession:(id)arg1 storyViewerLoggingContext:(id)arg2 storyFullscreenSectionLoggingContext:(id)arg3 viewController:(id)arg4 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGStoryAdsFetcher
|
||||
- (id)initWithUserSession:(id)arg1 delegate:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
// IG 148.0
|
||||
%hook IGStoryAdsResponseParser
|
||||
- (id)parsedObjectFromResponse:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
- (id)initWithReelStore:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGStoryAdsOptInTextView
|
||||
- (id)initWithBrandedContentStyledString:(id)arg1 sponsoredPostLabel:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGSundialAdsResponseParser
|
||||
- (id)parsedObjectFromResponse:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
- (id)initWithMediaStore:(id)arg1 userStore:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
NSLog(@"[SCInsta] Removing ads");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
// "Sponsored" posts on discover/search page
|
||||
%hook IGExploreListKitDataSource
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return removeItemsInList(%orig, NO);
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
// Demangled name: IGExploreViewControllerSwift.IGExploreListKitDataSource
|
||||
%hook _TtC28IGExploreViewControllerSwift26IGExploreListKitDataSource
|
||||
- (NSArray *)objectsForListAdapter:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return removeItemsInList(%orig, NO);
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Hide shopping carousel in reel comments
|
||||
// Demangled name: IGCommentThreadCommerceCarouselPill.IGCommentThreadCommerceCarousel
|
||||
%hook _TtC35IGCommentThreadCommerceCarouselPill31IGCommentThreadCommerceCarousel
|
||||
- (id)initWithFrame:(CGRect)frame pillText:(id)text pillStyle:(NSInteger)style {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig(frame, text, style);
|
||||
}
|
||||
%end
|
||||
|
||||
// Hide suggested search/shopping on reels
|
||||
|
||||
// Demangled name: IGShoppableEverythingCommon.IGRapEntrypointResolver
|
||||
%hook _TtC27IGShoppableEverythingCommon23IGRapEntrypointResolver
|
||||
- (id)initWithLauncherSet:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig(arg1);
|
||||
}
|
||||
%end
|
||||
// Demangled name: IGSundialOrganicCTAContainerView.IGSundialOrganicCTAContainerView
|
||||
%hook _TtC32IGSundialOrganicCTAContainerView32IGSundialOrganicCTAContainerView
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
|
||||
// Hide "suggested for you" text at end of feed
|
||||
%hook IGEndOfFeedDemarcatorCellTopOfFeed
|
||||
- (void)configureWithViewConfig:(id)arg1 {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_post"]) {
|
||||
NSLog(@"[SCInsta] Hiding end of feed message");
|
||||
|
||||
// Hide suggested for you text
|
||||
UILabel *_titleLabel = MSHookIvar<UILabel *>(self, "_titleLabel");
|
||||
|
||||
if (_titleLabel != nil) {
|
||||
[_titleLabel setText:@""];
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,15 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Disable story data source
|
||||
%hook IGMainStoryTrayDataSource
|
||||
- (id)initWithUserSession:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_stories_tray"]) {
|
||||
NSLog(@"[SCInsta] Hiding story tray");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,15 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Remove suggested threads posts (carousel, under suggested posts in feed)
|
||||
%hook BKBloksViewHelper
|
||||
- (id)initWithObjectSet:(id)arg1 bloksData:(id)arg2 delegate:(id)arg3 {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_threads"]) {
|
||||
NSLog(@"[SCInsta] Hiding threads posts");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,50 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../../modules/JGProgressHUD/JGProgressHUD.h"
|
||||
|
||||
%hook IGCoreTextView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"copy_description"]) {
|
||||
[self addHandleLongPress];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addHandleLongPress {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = 0.5;
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
// Remove hashtags at end of string
|
||||
NSRegularExpression *regex =
|
||||
[NSRegularExpression regularExpressionWithPattern:@"\\s*(?:#[^\\s]+\\s*)+$"
|
||||
options:0
|
||||
error:nil];
|
||||
|
||||
NSString *result = [[regex stringByReplacingMatchesInString:self.text
|
||||
options:0
|
||||
range:NSMakeRange(0, self.text.length)
|
||||
withTemplate:@""]
|
||||
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
NSLog(@"[SCInsta] Copying description");
|
||||
|
||||
// Copy text to system clipboard
|
||||
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
||||
pasteboard.string = result;
|
||||
|
||||
// Notify user
|
||||
JGProgressHUD *HUD = [[JGProgressHUD alloc] init];
|
||||
HUD.textLabel.text = @"Copied text to clipboard";
|
||||
HUD.indicatorView = [[JGProgressHUDSuccessIndicatorView alloc] init];
|
||||
|
||||
[HUD showInView:topMostController().view];
|
||||
[HUD dismissAfterDelay:2.0];
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,87 @@
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGStoryEyedropperToggleButton
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"detailed_color_picker"]) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
if ([self.gestureRecognizers count] == 0) {
|
||||
NSLog(@"[SCInsta] Adding color eyedroppper long press gesture recognizer");
|
||||
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = 0.25;
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
UIColorPickerViewController *colorPickerController = [[UIColorPickerViewController alloc] init];
|
||||
|
||||
colorPickerController.delegate = (id<UIColorPickerViewControllerDelegate>)self; // cast to suppress warnings
|
||||
colorPickerController.title = @"Select color";
|
||||
colorPickerController.modalPresentationStyle = UIModalPresentationPopover;
|
||||
colorPickerController.supportsAlpha = NO;
|
||||
colorPickerController.selectedColor = self.color;
|
||||
|
||||
UIViewController *presentingVC = [SCIUtils nearestViewControllerForView:self];
|
||||
|
||||
if (presentingVC != nil) {
|
||||
[presentingVC presentViewController:colorPickerController animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
// UIColorPickerViewControllerDelegate Protocol
|
||||
%new - (void)colorPickerViewController:(UIColorPickerViewController *)viewController
|
||||
didSelectColor:(UIColor *)color
|
||||
continuously:(BOOL)continuously
|
||||
{
|
||||
NSLog(@"[SCInsta] Selected text color: %@", color);
|
||||
|
||||
UIColor *opaque = [color colorWithAlphaComponent:1.0];
|
||||
self.color = opaque;
|
||||
|
||||
[self setPushedDown:YES];
|
||||
|
||||
// Trigger change for text color
|
||||
id presentingVC = [SCIUtils nearestViewControllerForView:self];
|
||||
|
||||
if ([presentingVC isKindOfClass:%c(IGStoryTextEntryViewController)]) {
|
||||
[presentingVC textViewControllerDidUpdateWithColor:color colorSource:0];
|
||||
}
|
||||
else if (
|
||||
[presentingVC isKindOfClass:%c(IGStoryCreationDrawingViewController)]
|
||||
|| [presentingVC isKindOfClass:%c(IGDirectThreadViewDrawingViewController)]
|
||||
) {
|
||||
[presentingVC drawingControls:nil didSelectColor:color];
|
||||
}
|
||||
|
||||
};
|
||||
%end
|
||||
|
||||
%hook IGStoryColorPaletteView
|
||||
- (CGFloat)collectionView:(id)view didSelectItemAtIndexPath:(id)index {
|
||||
UIView *colorPickingControls = [self superview];
|
||||
|
||||
if (
|
||||
[colorPickingControls isKindOfClass:%c(IGStoryColorPickingControls)]
|
||||
|| [colorPickingControls isKindOfClass:%c(IGDirectThreadColorPickingControls)]
|
||||
) {
|
||||
IGStoryEyedropperToggleButton *_eyedropperToggleButton = MSHookIvar<IGStoryEyedropperToggleButton *>(colorPickingControls, "_eyedropperToggleButton");
|
||||
|
||||
if (_eyedropperToggleButton != nil) {
|
||||
[_eyedropperToggleButton setPushedDown:NO];
|
||||
}
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,36 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
%hook IGUnifiedVideoCollectionView
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"disable_scrolling_reels"]) {
|
||||
NSLog(@"[SCInsta] Disabling scrolling reels");
|
||||
|
||||
self.scrollEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setScrollEnabled:(BOOL)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"disable_scrolling_reels"]) {
|
||||
NSLog(@"[SCInsta] Disabling scrolling reels");
|
||||
|
||||
return %orig(NO);
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Disable auto-scrolling reels
|
||||
%hook _TtC19IGSundialAutoScroll19IGSundialAutoScroll
|
||||
- (void)setIsEnabled:(BOOL)enabled {
|
||||
if ([SCIUtils getBoolPref:@"disable_scrolling_reels"]) {
|
||||
%orig(NO);
|
||||
}
|
||||
else {
|
||||
%orig(enabled);
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,31 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
%hook IGExploreGridViewController
|
||||
- (void)viewDidLoad {
|
||||
if ([SCIUtils getBoolPref:@"hide_explore_grid"]) {
|
||||
NSLog(@"[SCInsta] Hiding explore grid");
|
||||
|
||||
[[self view] removeFromSuperview];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGExploreViewController
|
||||
- (void)viewDidLoad {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_explore_grid"]) {
|
||||
NSLog(@"[SCInsta] Hiding explore grid");
|
||||
|
||||
IGShimmeringGridView *shimmeringGridView = MSHookIvar<IGShimmeringGridView *>(self, "_shimmeringGridView");
|
||||
if (shimmeringGridView != nil) {
|
||||
[shimmeringGridView removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,33 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGDirectNotesTrayRowCell
|
||||
- (id)listAdapterObjects {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_friends_map"]) {
|
||||
|
||||
if ([obj isKindOfClass:%c(IGDirectNotesTrayUserViewModel)]) {
|
||||
|
||||
if ([[obj valueForKey:@"notePk"] isEqualToString:@"friends_map"]) {
|
||||
NSLog(@"[SCInsta] Hiding friends map");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,572 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Direct
|
||||
|
||||
// Meta AI button functionality on direct search bar
|
||||
%hook IGDirectInboxViewController
|
||||
- (void)searchBarMetaAIButtonTappedOnSearchBar:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"])
|
||||
{
|
||||
NSLog(@"[SCInsta] Hiding meta ai: direct search bar functionality");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// AI agents in direct new message view
|
||||
%hook IGDirectRecipientGenAIBotsResult
|
||||
- (id)initWithGenAIBots:(id)arg1 lastFetchedTimestamp:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"])
|
||||
{
|
||||
NSLog(@"[SCInsta] Hiding meta ai: direct recipient ai agents");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Meta AI in message composer
|
||||
%hook IGDirectCommandSystemListViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
if ([obj isKindOfClass:%c(IGDirectCommandSystemViewModel)]) {
|
||||
IGDirectCommandSystemViewModel *typedObj = (IGDirectCommandSystemViewModel *)obj;
|
||||
IGDirectCommandSystemRow *cmdSystemRow = (IGDirectCommandSystemRow *)[typedObj row];
|
||||
|
||||
IGDirectCommandSystemResult *_commandResult_command = MSHookIvar<IGDirectCommandSystemResult *>(cmdSystemRow, "_commandResult_command");
|
||||
|
||||
if (_commandResult_command != nil) {
|
||||
|
||||
// Meta AI
|
||||
if ([[_commandResult_command title] isEqualToString:@"Meta AI"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: direct message composer suggestion");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
// Meta AI (Imagine)
|
||||
else if ([[_commandResult_command commandString] hasPrefix:@"/imagine"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: direct message composer /imagine suggestion");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Suggested AI chats in direct inbox header
|
||||
%hook IGDirectInboxNavigationHeaderView
|
||||
- (id)initWithFrame:(CGRect)arg1
|
||||
title:(id)arg2
|
||||
titleView:(id)arg3
|
||||
directInboxConfig:(IGDirectInboxConfig *)config
|
||||
userSession:(id)arg5
|
||||
loggingDelegate:(id)arg6
|
||||
{
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: suggested ai chats in direct inbox header");
|
||||
|
||||
@try {
|
||||
[config setValue:0 forKey:@"shouldShowAIChatsEntrypointButton"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
}
|
||||
|
||||
return %orig(arg1, arg2, arg3, [config copy], arg5, arg6);
|
||||
}
|
||||
%end
|
||||
|
||||
// Meta AI "imagine" in media picker
|
||||
%hook IGDirectMediaPickerViewController
|
||||
- (id)initWithUserSession:(id)arg1
|
||||
config:(IGDirectMediaPickerConfig *)config
|
||||
capabilities:(id)arg3
|
||||
threadMetadata:(id)arg4
|
||||
messageSender:(id)arg5
|
||||
threadAnalyticsLogger:(id)arg6
|
||||
multimodalPerfLogger:(id)arg7
|
||||
localSendSpeedLogger:(id)arg8
|
||||
sendAttributionFactory:(id)arg9
|
||||
{
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: imagine tile in media picker");
|
||||
|
||||
@try {
|
||||
IGDirectMediaPickerGalleryConfig *galleryConfig = [config valueForKey:@"galleryConfig"];
|
||||
|
||||
[galleryConfig setValue:0 forKey:@"isImagineEntryPointEnabled"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
}
|
||||
|
||||
return %orig(arg1, [config copy], arg3, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||
}
|
||||
%end
|
||||
|
||||
// Write with meta ai in message composer
|
||||
%hook IGDirectComposer
|
||||
- (id)initWithLayoutSpecProvider:(id)arg1
|
||||
userLauncherSetProviding:(id)arg2
|
||||
config:(IGDirectComposerConfig *)config
|
||||
style:(id)arg4
|
||||
text:(id)arg5
|
||||
{
|
||||
return %orig(arg1, arg2, [self patchConfig:config], arg4, arg5);
|
||||
}
|
||||
|
||||
- (id)initWithLayoutSpecProvider:(id)arg1
|
||||
userLauncherSetProviding:(id)arg2
|
||||
config:(IGDirectComposerConfig *)config
|
||||
style:(id)arg4
|
||||
text:(id)arg5
|
||||
shouldUpdateModeLater:(BOOL)arg6
|
||||
{
|
||||
return %orig(arg1, arg2, [self patchConfig:config], arg4, arg5, arg6);
|
||||
}
|
||||
|
||||
- (void)setConfig:(IGDirectComposerConfig *)config {
|
||||
%orig([self patchConfig:config]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
%new - (IGDirectComposerConfig *)patchConfig:(IGDirectComposerConfig *)config {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
NSLog(@"[SCInsta] Hiding meta ai: reconfiguring direct composer");
|
||||
|
||||
// writeWithAIEnabled
|
||||
@try {
|
||||
[config setValue:0 forKey:@"writeWithAIEnabled"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [config copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Direct sticker tray picker view
|
||||
%hook IGStickerTrayListAdapterDataSource
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
if ([obj isKindOfClass:%c(IGDirectUnifiedComposerAIStickerModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: AI stickers option in sticker view");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Long press menu on messages
|
||||
// Demangled name: IGDirectMessageMenuConfiguration.IGDirectMessageMenuConfiguration
|
||||
%hook _TtC32IGDirectMessageMenuConfiguration32IGDirectMessageMenuConfiguration
|
||||
+ (id)menuConfigurationWithEligibleOptions:(id)options
|
||||
messageViewModel:(id)arg2
|
||||
contentType:(id)arg3
|
||||
isSticker:(_Bool)arg4
|
||||
isMusicSticker:(_Bool)arg5
|
||||
directNuxManager:(id)arg6
|
||||
sessionUserDefaults:(id)arg7
|
||||
launcherSet:(id)arg8
|
||||
userSession:(id)arg9
|
||||
tapHandler:(id)arg10
|
||||
{
|
||||
// 31: Restyle
|
||||
// 41: Make AI image
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", @[ @(31), @(41) ]];
|
||||
NSArray *newOptions = [options filteredArrayUsingPredicate:predicate];
|
||||
|
||||
return %orig([newOptions copy], arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
|
||||
}
|
||||
%end
|
||||
|
||||
// Expanded in-chat photo UI
|
||||
// Demangled name: IGDirectAggregatedMediaViewerComponentsSwift.IGDirectAggregatedMediaViewerViewControllerTitleViewModelObject
|
||||
%hook _TtC44IGDirectAggregatedMediaViewerComponentsSwift63IGDirectAggregatedMediaViewerViewControllerTitleViewModelObject
|
||||
- (id)initWithAuthorProfileImage:(id)arg1
|
||||
authorUsername:(id)arg2
|
||||
canForward:(_Bool)arg3
|
||||
canSave:(_Bool)arg4
|
||||
canAddToStory:(_Bool)arg5
|
||||
canShowAIRestyle:(_Bool)arg6
|
||||
canUnsend:(_Bool)arg7
|
||||
canReport:(_Bool)arg8
|
||||
displayConfig:(id)arg9
|
||||
isPending:(_Bool)arg10
|
||||
isMoreMenuListStyle:(_Bool)arg11
|
||||
senderIsCurrentUser:(_Bool)arg12
|
||||
shouldHideInfoViews:(_Bool)arg13
|
||||
subtitle:(id)arg14
|
||||
entryPoint:(long long)arg15
|
||||
canTapAuthor:(_Bool)arg16
|
||||
{
|
||||
BOOL showAiRestyle = [SCIUtils getBoolPref:@"hide_meta_ai"] ? false : arg6;
|
||||
|
||||
return %orig(arg1, arg2, arg3, arg4, arg5, showAiRestyle, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16);
|
||||
}
|
||||
%end
|
||||
|
||||
// AI generated DM channel themes
|
||||
%hook IGDirectThreadThemePickerViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
if (
|
||||
[obj isKindOfClass:%c(IGDirectThreadThemePickerOption)]
|
||||
&& [[obj valueForKey:@"themeId"] isEqualToString:@"direct_ai_theme_creation"]
|
||||
) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: AI generated DM channel themes");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// "Click to summarize" pill under DM navigation bar
|
||||
%hook IGDirectThreadViewMetaAISummaryFeatureController
|
||||
- (id)initWithUserSession:(id)arg1 mutableStateProvider:(id)arg2 threadViewControllerFeatureDelegate:(id)arg3 presentingViewController:(id)arg4 {
|
||||
return nil;
|
||||
}
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Explore
|
||||
|
||||
// Meta AI explore search summary
|
||||
%hook IGDiscoveryListKitGQLDataSource
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Meta AI summary
|
||||
if ([obj isKindOfClass:%c(IGSearchMetaAIHCMModel)]) {
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding explore meta ai search summary");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Meta AI search bar ring button
|
||||
%hook IGSearchBarDonutButton
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Reels/Sundial
|
||||
|
||||
// Suggested AI searches in comment section
|
||||
%hook IGCommentThreadAICarousel
|
||||
- (id)initWithLauncherSet:(id)arg1 hasSearchPrefix:(BOOL)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: suggested ai searches comment carousel");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook _TtC34IGCommentThreadAICarouselPillSwift30IGCommentThreadAICarouselSwift
|
||||
- (id)initWithLauncherSet:(id)arg1 hasSearchPrefix:(BOOL)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: suggested ai searches comment carousel");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Story
|
||||
|
||||
// AI images "add to story" suggestion
|
||||
// Demangled name: IGGalleryDestinationToolbar.IGGalleryDestinationToolbarView
|
||||
%hook _TtC27IGGalleryDestinationToolbar31IGGalleryDestinationToolbarView
|
||||
- (void)setTools:(id)tools {
|
||||
NSArray *newTools = [tools copy];
|
||||
|
||||
NSLog(@"[SCInsta] Hiding meta ai: ai images add to story suggestion");
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", @[ @(10), @(11) ]];
|
||||
newTools = [tools filteredArrayUsingPredicate:predicate];
|
||||
}
|
||||
|
||||
%orig(newTools);
|
||||
|
||||
return;
|
||||
}
|
||||
%end
|
||||
|
||||
// AI generated fonts in text entry
|
||||
%hook IGCreationTextToolView
|
||||
- (id)initWithMenuConfiguration:(unsigned long long)configuration userSession:(id)session creationEntryPoint:(long long)point isAIFontsEnabled:(_Bool)enabled genAINuxManager:(id)manager showFontBadge:(_Bool)badge {
|
||||
return %orig(configuration, session, point, [SCIUtils getBoolPref:@"hide_meta_ai"] ? false : enabled, manager, badge);
|
||||
}
|
||||
%end
|
||||
|
||||
// Text rewrite in text entry
|
||||
%hook IGStoryTextMentionLocationPickerView
|
||||
- (id)initWithIsTextRewriteEnabled:(_Bool)arg1
|
||||
isImageRewriteEnabled:(_Bool)arg2
|
||||
isStackedToolSelectorEnabled:(_Bool)arg3
|
||||
isMentionLocationVisible:(_Bool)arg4
|
||||
isEnabledForFeedCaption:(_Bool)arg5
|
||||
isFeedEntryPoint:(_Bool)arg6
|
||||
{
|
||||
_Bool isTextRewriteEnabled = [SCIUtils getBoolPref:@"hide_meta_ai"] ? false : arg1;
|
||||
_Bool isImageRewriteEnabled = [SCIUtils getBoolPref:@"hide_meta_ai"] ? false : arg2;
|
||||
|
||||
return %orig(isTextRewriteEnabled, isImageRewriteEnabled, arg3, arg4, arg5, arg6);
|
||||
}
|
||||
%end
|
||||
|
||||
// "Imagine background" in story editor vertical action bar
|
||||
%hook _TtC17IGCreationOSSwift19IGCreationHeaderBar
|
||||
- (void)setButtons:(id)buttons maxItems:(NSInteger)max {
|
||||
NSArray *filteredObjs = buttons;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
filteredObjs = [filteredObjs filteredArrayUsingPredicate:
|
||||
[NSPredicate predicateWithBlock:^BOOL(IGCreationActionBarLabeledButton *obj, NSDictionary *bindings) {
|
||||
|
||||
return !(
|
||||
obj.button
|
||||
&& [((IGCreationActionBarButton *)obj.button).accessibilityIdentifier isEqualToString:@"contextual-background"]
|
||||
);
|
||||
|
||||
}]
|
||||
];
|
||||
}
|
||||
|
||||
%orig(filteredObjs, max);
|
||||
}
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Other
|
||||
|
||||
// Meta AI-branded search bars
|
||||
%hook IGSearchBar
|
||||
- (id)initWithConfig:(IGSearchBarConfig *)config {
|
||||
return %orig([self sanitizePlaceholderForConfig:config]);
|
||||
}
|
||||
|
||||
- (id)initWithConfig:(IGSearchBarConfig *)config userSession:(id)arg2 {
|
||||
return %orig([self sanitizePlaceholderForConfig:config], arg2);
|
||||
}
|
||||
|
||||
- (void)setConfig:(IGSearchBarConfig *)config {
|
||||
%orig([self sanitizePlaceholderForConfig:config]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
%new - (IGSearchBarConfig *)sanitizePlaceholderForConfig:(IGSearchBarConfig *)config {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
NSLog(@"[SCInsta] Hiding meta ai: reconfiguring search bar");
|
||||
|
||||
NSString *placeholder = [config valueForKey:@"placeholder"];
|
||||
|
||||
if ([placeholder containsString:@"Meta AI"]) {
|
||||
|
||||
// placeholder
|
||||
@try {
|
||||
[config setValue:@"Search" forKey:@"placeholder"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
|
||||
// shouldAnimatePlaceholder
|
||||
@try {
|
||||
[config setValue:0 forKey:@"shouldAnimatePlaceholder"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
|
||||
NSLog(@"[SCInsta] Changed search bar placeholder from: \"%@\" to \"%@\"", placeholder, [config valueForKey:@"placeholder"]);
|
||||
|
||||
// leftIconStyle
|
||||
@try {
|
||||
[config setValue:0 forKey:@"leftIconStyle"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
|
||||
// rightButtonStyle
|
||||
@try {
|
||||
[config setValue:0 forKey:@"rightButtonStyle"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [config copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Themed in-app buttons
|
||||
%hook IGTapButton
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
// Hide buttons that are associated with meta ai
|
||||
if ([self.accessibilityIdentifier containsString:@"meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: meta ai associated button");
|
||||
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Home feed meta ai button
|
||||
%hook IGFloatingActionButton.IGFloatingActionButton
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
[self removeFromSuperview];
|
||||
NSLog(@"[SCInsta] Hiding meta ai: home feed meta ai button");
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Share menu recipients
|
||||
%hook IGDirectRecipientListViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
if ([obj isKindOfClass:%c(IGDirectRecipientCellViewModel)]) {
|
||||
|
||||
// Meta AI (catch-all)
|
||||
if ([[[obj recipient] threadName] isEqualToString:@"Meta AI"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai suggested as recipient (share menu)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,16 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
%hook IGDSSegmentedPillBarView
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([[self delegate] isKindOfClass:%c(IGSearchTypeaheadNavigationHeaderView)]) {
|
||||
if ([SCIUtils getBoolPref:@"hide_trending_searches"]) {
|
||||
NSLog(@"[SCInsta] Hiding trending searches");
|
||||
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,100 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
BOOL isSurfaceShown(IGMainAppSurfaceIntent *surface) {
|
||||
BOOL isShown = YES;
|
||||
|
||||
// Feed
|
||||
if ([[surface tabStringFromSurfaceIntent] isEqualToString:@"FEED"] && [SCIUtils getBoolPref:@"hide_feed_tab"]) {
|
||||
isShown = NO;
|
||||
}
|
||||
|
||||
// Reels
|
||||
else if ([[surface tabStringFromSurfaceIntent] isEqualToString:@"CLIPS"] && [SCIUtils getBoolPref:@"hide_reels_tab"]) {
|
||||
isShown = NO;
|
||||
}
|
||||
|
||||
// Explore
|
||||
else if ([[surface tabStringFromSurfaceIntent] isEqualToString:@"SEARCH"] && [SCIUtils getBoolPref:@"hide_explore_tab"]) {
|
||||
isShown = NO;
|
||||
}
|
||||
|
||||
// Create
|
||||
else if ([(NSNumber *)[surface valueForKey:@"_subtype"] unsignedIntegerValue] == 3 && [SCIUtils getBoolPref:@"hide_create_tab"]) {
|
||||
isShown = NO;
|
||||
}
|
||||
|
||||
return isShown;
|
||||
}
|
||||
|
||||
NSArray *filterSurfacesArray(NSArray *surfaces) {
|
||||
NSMutableArray *filteredSurfaces = [NSMutableArray array];
|
||||
|
||||
for (IGMainAppSurfaceIntent *surface in surfaces) {
|
||||
if (![surface isKindOfClass:%c(IGMainAppSurfaceIntent)]) break;
|
||||
|
||||
if (isSurfaceShown(surface)) {
|
||||
[filteredSurfaces addObject:surface];
|
||||
}
|
||||
}
|
||||
|
||||
return filteredSurfaces;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
|
||||
%hook IGTabBarControllerSwipeCoordinator
|
||||
- (id)initWithSurfaces:(id)surfaces parentViewController:(id)controller enableHaptics:(_Bool)haptics launcherSet:(id)set {
|
||||
// Removes the surface from the main swipeable app collection view
|
||||
return %orig(filterSurfacesArray(surfaces), controller, haptics, set);
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGTabBarController
|
||||
- (void)_layoutTabBar {
|
||||
// Prevents the wrong icon from being shown as selected because of mismatched surface array indexes
|
||||
NSArray *_tabBarSurfaces = [SCIUtils getIvarForObj:self name:"_tabBarSurfaces"];
|
||||
|
||||
[SCIUtils setIvarForObj:self name:"_tabBarSurfaces" value:filterSurfacesArray(_tabBarSurfaces)];
|
||||
|
||||
%orig;
|
||||
}
|
||||
|
||||
- (id)_buttonForTabBarSurface:(id)surface {
|
||||
// Prevents the button from being added to the tab bar
|
||||
id button = %orig(surface);
|
||||
|
||||
if (!isSurfaceShown(surface)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
%end
|
||||
|
||||
// Demangled name: IGNavConfiguration.IGNavConfiguration
|
||||
%hook _TtC18IGNavConfiguration18IGNavConfiguration
|
||||
- (NSInteger)tabOrdering {
|
||||
|
||||
if ([[SCIUtils getStringPref:@"nav_icon_ordering"] isEqualToString:@"classic"]) return 0;
|
||||
else if ([[SCIUtils getStringPref:@"nav_icon_ordering"] isEqualToString:@"standard"]) return 1;
|
||||
else if ([[SCIUtils getStringPref:@"nav_icon_ordering"] isEqualToString:@"alternate"]) return 2;
|
||||
|
||||
return %orig;
|
||||
|
||||
}
|
||||
- (void)setTabOrdering:(NSInteger)arg1 {
|
||||
return;
|
||||
}
|
||||
|
||||
- (BOOL)isTabSwipingEnabled {
|
||||
|
||||
if ([[SCIUtils getStringPref:@"swipe_nav_tabs"] isEqualToString:@"enabled"]) return YES;
|
||||
else if ([[SCIUtils getStringPref:@"swipe_nav_tabs"] isEqualToString:@"disabled"]) return NO;
|
||||
|
||||
return %orig;
|
||||
|
||||
}
|
||||
- (void)setIsTabSwipingEnabled:(BOOL)arg1 {
|
||||
return;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,50 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Disable logging of searches at server-side
|
||||
%hook IGSearchEntityRouter
|
||||
- (id)initWithUserSession:(id)arg1 analyticsModule:(id)arg2 shouldAddToRecents:(BOOL)shouldAddToRecents {
|
||||
if ([SCIUtils getBoolPref:@"no_recent_searches"]) {
|
||||
NSLog(@"[SCInsta] Disabling recent searches");
|
||||
|
||||
shouldAddToRecents = false;
|
||||
}
|
||||
|
||||
return %orig(arg1, arg2, shouldAddToRecents);
|
||||
}
|
||||
%end
|
||||
|
||||
// Most in-app search bars
|
||||
%hook IGRecentSearchStore
|
||||
- (id)initWithDiskManager:(id)arg1 recentSearchStoreConfiguration:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"no_recent_searches"]) {
|
||||
NSLog(@"[SCInsta] Disabling recent searches");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
- (BOOL)addItem:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"no_recent_searches"]) {
|
||||
NSLog(@"[SCInsta] Disabling recent searches");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Recent dm message recipients search bar
|
||||
%hook IGDirectRecipientRecentSearchStorage
|
||||
- (id)initWithDiskManager:(id)arg1 directCache:(id)arg2 userStore:(id)arg3 currentUser:(id)arg4 featureSets:(id)arg5 {
|
||||
if ([SCIUtils getBoolPref:@"no_recent_searches"]) {
|
||||
NSLog(@"[SCInsta] Disabling recent searches");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,19 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Channels dms tab (header)
|
||||
%hook IGDirectInboxHeaderSectionController
|
||||
- (id)viewModel {
|
||||
if ([[%orig title] isEqualToString:@"Suggested"]) {
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_chats"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats (header: channels tab)");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,263 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// "Welcome to instagram" suggested users in feed
|
||||
%hook IGSuggestedUnitViewModel
|
||||
- (id)initWithAYMFModel:(id)arg1 headerViewModel:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: main feed welcome section");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
%hook IGSuggestionsUnitViewModel
|
||||
- (id)initWithAYMFModel:(id)arg1 headerViewModel:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: main feed welcome section");
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Suggested users in profile header
|
||||
%hook IGProfileHeaderView
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
if ([obj isKindOfClass:%c(IGProfileChainingModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: profile header");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Notifications/activity feed
|
||||
%hook IGActivityFeedViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Section header
|
||||
if ([obj isKindOfClass:%c(IGLabelItemViewModel)]) {
|
||||
// Suggested for you
|
||||
if ([[obj labelTitle] isEqualToString:@"Suggested for you"]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users (header: activity feed)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Suggested user
|
||||
else if ([obj isKindOfClass:%c(IGDiscoverPeopleItemConfiguration)]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: (user: activity feed)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// "See all" button
|
||||
else if ([obj isKindOfClass:%c(IGSeeAllItemConfiguration)]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: (see all: activity feed)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Profile "following" and "followers" tabs
|
||||
%hook IGFollowListViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig(arg1);
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (IGStoryTrayViewModel *obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
|
||||
// Suggested user
|
||||
if ([obj isKindOfClass:%c(IGDiscoverPeopleItemConfiguration)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: follow list suggested user");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
// Section header
|
||||
else if ([obj isKindOfClass:%c(IGLabelItemViewModel)]) {
|
||||
|
||||
// "Suggested for you" search results header
|
||||
if ([[obj valueForKey:@"labelTitle"] isEqualToString:@"Suggested for you"]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// See all suggested users
|
||||
else if ([obj isKindOfClass:%c(IGSeeAllItemConfiguration)] && ((IGSeeAllItemConfiguration *)obj).destination == 4) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: follow list suggested user");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGSegmentedTabControl
|
||||
- (void)setSegments:(id)segments {
|
||||
NSArray *originalObjs = segments;
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (IGStoryTrayViewModel *obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
if ([obj isKindOfClass:%c(IGFindUsersViewController)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: find users segmented tab");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return %orig([filteredObjs copy]);
|
||||
}
|
||||
%end
|
||||
|
||||
// Suggested subscriptions
|
||||
%hook IGFanClubSuggestedUsersDataSource
|
||||
- (id)initWithUserSession:(id)arg1 delegate:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig(arg1, arg2);
|
||||
}
|
||||
%end
|
||||
|
||||
// Follow request/discover section (accessed through notifications page)
|
||||
// Demangled name: IGFriendingCenter.IGFriendingCenterViewController
|
||||
%hook _TtC17IGFriendingCenter31IGFriendingCenterViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig(arg1);
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (IGStoryTrayViewModel *obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
|
||||
// Suggested user
|
||||
if ([obj isKindOfClass:%c(IGDiscoverPeopleItemConfiguration)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: follow list suggested user");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
// Section header
|
||||
else if ([obj isKindOfClass:%c(IGLabelItemViewModel)]) {
|
||||
|
||||
// "Suggested for you" search results header
|
||||
if ([[obj valueForKey:@"labelTitle"] isEqualToString:@"Suggested for you"]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGProfileActionBarViewModel
|
||||
- (id)initWithIdentifier:(id)arg1
|
||||
rows:(id)arg2
|
||||
allActionsToDisplay:(id)arg3
|
||||
overflowActions:(id)arg4
|
||||
actionToBadgeInfoMap:(id)arg5
|
||||
allBusinessActions:(id)arg6
|
||||
overflowBusinessActions:(id)arg7
|
||||
contactSheetActions:(id)arg8
|
||||
user:(id)arg9
|
||||
sponsoredInfoProvider:(id)arg10
|
||||
profileBackgroundColor:(id)arg11
|
||||
{
|
||||
NSArray *rows = arg2;
|
||||
NSOrderedSet *allActions = [arg3 copy];
|
||||
NSOrderedSet *overflowActions = [arg4 copy];
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", @[ @(3) ]];
|
||||
|
||||
// Actions sets
|
||||
allActions = [allActions filteredOrderedSetUsingPredicate:predicate];
|
||||
overflowActions = [overflowActions filteredOrderedSetUsingPredicate:predicate];
|
||||
|
||||
// Rows of actions sets
|
||||
NSMutableArray *filteredRows = [NSMutableArray new];
|
||||
for (NSOrderedSet *set in rows) {
|
||||
[filteredRows addObject:[set filteredOrderedSetUsingPredicate:predicate]];
|
||||
}
|
||||
rows = [filteredRows copy];
|
||||
}
|
||||
|
||||
return %orig(arg1, rows, allActions, overflowActions, arg5, arg6, arg7, arg8, arg9, arg10, arg11);
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,293 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
static char targetStaticRef[] = "target";
|
||||
|
||||
%hook IGDirectNotesCreationView
|
||||
- (id)initWithViewModel:(id)model
|
||||
featureSupport:(IGNotesCreationFeatureSupportModel *)support
|
||||
presentationAnimation:(id)animation
|
||||
composerUpdateListener:(id)listener
|
||||
delegate:(id)delegate
|
||||
layoutType:(long long)type
|
||||
userSession:(id)session
|
||||
{
|
||||
if ([SCIUtils getBoolPref:@"enable_notes_customization"]) {
|
||||
|
||||
// enableAnimatedEmojisInCreation
|
||||
@try {
|
||||
[support setValue:@(YES) forKey:@"enableAnimatedEmojisInCreation"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, support);
|
||||
}
|
||||
|
||||
// enableBubbleCustomization
|
||||
@try {
|
||||
[support setValue:@(YES) forKey:@"enableBubbleCustomization"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, support);
|
||||
}
|
||||
|
||||
// enableRandomThemeGenerator
|
||||
@try {
|
||||
[support setValue:@(YES) forKey:@"enableRandomThemeGenerator"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, support);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return %orig(model, support, animation, listener, delegate, type, session);
|
||||
}
|
||||
%end
|
||||
|
||||
// Demangled name: IGDirectNotesUISwift.IGDirectNotesBubbleEditorColorPaletteView
|
||||
%hook _TtC20IGDirectNotesUISwift41IGDirectNotesBubbleEditorColorPaletteView
|
||||
%property (nonatomic, copy) UIColor *backgroundColor;
|
||||
%property (nonatomic, copy) UIColor *textColor;
|
||||
%property (nonatomic, copy) NSString *emojiText;
|
||||
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if (![SCIUtils getBoolPref:@"custom_note_themes"]) return;
|
||||
|
||||
// Inject buttons once in view lifecycle
|
||||
static char didInjectButtons;
|
||||
if (objc_getAssociatedObject(self, &didInjectButtons)) {
|
||||
return;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) self = weakSelf;
|
||||
if (!self || !self.window) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIView *container = self.superview ?: self.window;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Button config
|
||||
UIButtonConfiguration *config = [UIButtonConfiguration tintedButtonConfiguration];
|
||||
config.background.cornerRadius = 12.0;
|
||||
config.cornerStyle = UIButtonConfigurationCornerStyleFixed;
|
||||
config.contentInsets = NSDirectionalEdgeInsetsMake(13.7, 10, 13.7, 10);
|
||||
|
||||
|
||||
// Left button
|
||||
UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
leftButton.configuration = config;
|
||||
leftButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
leftButton.tintColor = [SCIUtils SCIColor_Primary];
|
||||
|
||||
NSMutableAttributedString *attrTitleLeft = [[NSMutableAttributedString alloc] initWithString:@"Background"];
|
||||
[attrTitleLeft addAttribute:NSFontAttributeName
|
||||
value:[UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]
|
||||
range:NSMakeRange(0, attrTitleLeft.length)
|
||||
];
|
||||
[leftButton setAttributedTitle:attrTitleLeft forState:UIControlStateNormal];
|
||||
[leftButton sizeToFit];
|
||||
|
||||
[leftButton addAction:[UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) {
|
||||
[self presentColorPicker:@"Background"];
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
// Middle button
|
||||
UIButton *middleButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
middleButton.configuration = config;
|
||||
middleButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
middleButton.tintColor = [SCIUtils SCIColor_Primary];
|
||||
|
||||
NSMutableAttributedString *attrTitleMiddle = [[NSMutableAttributedString alloc] initWithString:@"Text"];
|
||||
[attrTitleMiddle addAttribute:NSFontAttributeName
|
||||
value:[UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]
|
||||
range:NSMakeRange(0, attrTitleMiddle.length)
|
||||
];
|
||||
[middleButton setAttributedTitle:attrTitleMiddle forState:UIControlStateNormal];
|
||||
[middleButton sizeToFit];
|
||||
|
||||
[middleButton addAction:[UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) {
|
||||
[self presentColorPicker:@"Text"];
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
// Right button
|
||||
UIButton *rightButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
rightButton.configuration = config;
|
||||
rightButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
rightButton.tintColor = [SCIUtils SCIColor_Primary];
|
||||
|
||||
NSMutableAttributedString *attrTitleRight = [[NSMutableAttributedString alloc] initWithString:@"Emoji"];
|
||||
[attrTitleRight addAttribute:NSFontAttributeName
|
||||
value:[UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]
|
||||
range:NSMakeRange(0, attrTitleRight.length)
|
||||
];
|
||||
[rightButton setAttributedTitle:attrTitleRight forState:UIControlStateNormal];
|
||||
[rightButton sizeToFit];
|
||||
|
||||
[rightButton addAction:[UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Enter Emoji Text"
|
||||
message:@"Click the Apply button after this to see the emoji"
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = @"Type emoji...";
|
||||
}];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
self.emojiText = alert.textFields[0].text;
|
||||
[self applySCICustomTheme:@"Emoji"];
|
||||
}]];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil]];
|
||||
|
||||
UIViewController *vc = [SCIUtils nearestViewControllerForView:self];
|
||||
[vc presentViewController:alert animated:YES completion:nil];
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
|
||||
// Create stack view
|
||||
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[leftButton, middleButton, rightButton]];
|
||||
stack.axis = UILayoutConstraintAxisHorizontal;
|
||||
stack.spacing = 15.0;
|
||||
stack.alignment = UIStackViewAlignmentCenter;
|
||||
stack.distribution = UIStackViewDistributionFillEqually;
|
||||
|
||||
// Find max height among arranged subviews
|
||||
CGFloat maxHeight = 0.0;
|
||||
for (UIView *subview in stack.arrangedSubviews) {
|
||||
maxHeight = MAX(maxHeight, subview.bounds.size.height);
|
||||
}
|
||||
|
||||
// Manual frame with side padding
|
||||
CGFloat bottomMargin = 15.0;
|
||||
|
||||
CGRect viewFrame = [self convertRect:self.bounds toView:container];
|
||||
CGFloat y = CGRectGetMinY(viewFrame) - maxHeight - bottomMargin;
|
||||
CGFloat width = container.bounds.size.width - stack.spacing * 2;
|
||||
|
||||
stack.frame = CGRectMake(stack.spacing, y, width, maxHeight);
|
||||
|
||||
[stack layoutIfNeeded];
|
||||
[container addSubview:stack];
|
||||
|
||||
objc_setAssociatedObject(
|
||||
self,
|
||||
&didInjectButtons,
|
||||
@YES,
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
%new - (void)presentColorPicker:(NSString *)target {
|
||||
UIColorPickerViewController *colorPickerController = [[UIColorPickerViewController alloc] init];
|
||||
|
||||
colorPickerController.delegate = (id<UIColorPickerViewControllerDelegate>)self; // cast to suppress warnings
|
||||
colorPickerController.title = [NSString stringWithFormat:@"%@ color", target];
|
||||
colorPickerController.modalPresentationStyle = UIModalPresentationPopover;
|
||||
colorPickerController.supportsAlpha = NO;
|
||||
|
||||
// Show last picked color for type
|
||||
if ([target isEqualToString:@"Background"]) {
|
||||
colorPickerController.selectedColor = self.backgroundColor;
|
||||
}
|
||||
else if ([target isEqualToString:@"Text"]) {
|
||||
colorPickerController.selectedColor = self.textColor;
|
||||
}
|
||||
|
||||
UIViewController *presentingVC = [SCIUtils nearestViewControllerForView:self];
|
||||
|
||||
if (presentingVC != nil) {
|
||||
[presentingVC presentViewController:colorPickerController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
// Save which color target to update
|
||||
objc_setAssociatedObject(
|
||||
presentingVC,
|
||||
&targetStaticRef,
|
||||
target,
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
||||
);
|
||||
}
|
||||
|
||||
// UIColorPickerViewControllerDelegate Protocol
|
||||
%new - (void)colorPickerViewController:(UIColorPickerViewController *)viewController
|
||||
didSelectColor:(UIColor *)color
|
||||
continuously:(BOOL)continuously
|
||||
{
|
||||
_TtC20IGDirectNotesUISwift41IGDirectNotesBubbleEditorColorPaletteView *bubbleEditorVC = [SCIUtils nearestViewControllerForView:self];
|
||||
|
||||
NSString *target = objc_getAssociatedObject(bubbleEditorVC, &targetStaticRef);
|
||||
if (!target) return;
|
||||
|
||||
// Update saved color target
|
||||
if ([target isEqualToString:@"Background"]) {
|
||||
self.backgroundColor = color;
|
||||
}
|
||||
else if ([target isEqualToString:@"Text"]) {
|
||||
self.textColor = color;
|
||||
}
|
||||
|
||||
[self applySCICustomTheme:target];
|
||||
};
|
||||
|
||||
%new - (void)applySCICustomTheme:(NSString *)target {
|
||||
// Get notes composer vc
|
||||
_TtC20IGDirectNotesUISwift39IGDirectNotesBubbleEditorViewController *parentVC = [SCIUtils nearestViewControllerForView:self];
|
||||
if (!parentVC) return;
|
||||
|
||||
IGDirectNotesComposerViewController *composerVC = parentVC.delegate;
|
||||
if (!composerVC) return;
|
||||
|
||||
// Get current theme model
|
||||
IGNotesCustomThemeCreationModel *model = [composerVC valueForKey:@"_selectedCustomThemeCreationModel"];
|
||||
if (!model) {
|
||||
// Create new note theme model
|
||||
model = [[%c(IGNotesCustomThemeCreationModel) alloc] init];
|
||||
if (!model) return;
|
||||
}
|
||||
|
||||
//SCILog(@"Current note theme model: %@", model);
|
||||
[model setValue:[composerVC valueForKey:@"_composerText"] forKey:@"customEmoji"];
|
||||
|
||||
// Update saved color target
|
||||
if ([target isEqualToString:@"Background"]) {
|
||||
[model setValue:self.backgroundColor forKey:@"backgroundColor"];
|
||||
}
|
||||
else if ([target isEqualToString:@"Text"]) {
|
||||
[model setValue:self.textColor forKey:@"textColor"];
|
||||
[model setValue:self.textColor forKey:@"secondaryTextColor"];
|
||||
}
|
||||
|
||||
// Always set emoji to prevent it being overwritten
|
||||
[model setValue:self.emojiText forKey:@"customEmoji"];
|
||||
|
||||
//SCILog(@"Updated note theme model: %@", model);
|
||||
|
||||
// Apply custom notes theme
|
||||
[composerVC notesBubbleEditorViewControllerDidUpdateWithCustomThemeCreationModel:model];
|
||||
|
||||
// Enable apply/cancel buttons
|
||||
UIView *parentVCView = [parentVC view];
|
||||
if (!parentVCView) return;
|
||||
|
||||
NSArray<UIView *> *parentVCSubviews = [parentVCView subviews];
|
||||
if (!parentVCSubviews) return;
|
||||
|
||||
[parentVCSubviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||
if ([obj isKindOfClass:%c(IGDSBottomButtonsView)]) {
|
||||
[obj setPrimaryButtonEnabled:YES];
|
||||
[obj setSecondaryButtonEnabled:YES];
|
||||
}
|
||||
}];
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,58 @@
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Settings/SCISettingsViewController.h"
|
||||
|
||||
// Show SCInsta tweak settings by holding on the settings/more icon under profile for ~1 second
|
||||
%hook IGBadgedNavigationButton
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([self.accessibilityIdentifier isEqualToString:@"profile-more-button"]) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
if ([self.gestureRecognizers count] == 0) {
|
||||
NSLog(@"[SCInsta] Adding tweak settings long press gesture recognizer");
|
||||
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
NSLog(@"[SCInsta] Tweak settings gesture activated");
|
||||
|
||||
[SCIUtils showSettingsVC:[self window]];
|
||||
}
|
||||
%end
|
||||
|
||||
// Quick access to tweak settings by holding on home tab button
|
||||
%hook IGTabBarButton
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
// Only work on home/feed tab
|
||||
if (![self.accessibilityIdentifier isEqualToString:@"mainfeed-tab"]) return;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"settings_shortcut"]) {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = 0.3;
|
||||
|
||||
// Take precidence over existing gesture recognizers
|
||||
for (UIGestureRecognizer *existing in self.gestureRecognizers) {
|
||||
[existing requireGestureRecognizerToFail:longPress];
|
||||
}
|
||||
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
[SCIUtils showSettingsVC:[self window]];
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,38 @@
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGImageWithAccessoryButton
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
BOOL hasLongPress = [self.gestureRecognizers filteredArrayUsingPredicate:
|
||||
[NSPredicate predicateWithBlock:^BOOL(NSObject *item, NSDictionary *_) {
|
||||
return [item isKindOfClass:[UILongPressGestureRecognizer class]];
|
||||
}]
|
||||
].count > 0;
|
||||
|
||||
if (!hasLongPress) {
|
||||
NSLog(@"[SCInsta] Adding teen app icons long press gesture recognizer");
|
||||
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"teen_app_icons"]) {
|
||||
IGHomeFeedHeaderViewController *homeFeedHeaderVC = [SCIUtils nearestViewControllerForView:self];
|
||||
|
||||
if (homeFeedHeaderVC != nil) {
|
||||
[homeFeedHeaderVC headerDidLongPressLogo:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%end
|
||||
@@ -0,0 +1,703 @@
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Utils.h"
|
||||
#import "../../Downloader/Download.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static SCIDownloadDelegate *imageDownloadDelegate;
|
||||
static SCIDownloadDelegate *videoDownloadDelegate;
|
||||
|
||||
static DownloadAction sciGetDownloadAction() {
|
||||
NSString *method = [SCIUtils getStringPref:@"dw_save_action"];
|
||||
if ([method isEqualToString:@"photos"]) return saveToPhotos;
|
||||
return share;
|
||||
}
|
||||
|
||||
static void initDownloaders () {
|
||||
// Re-init each time to pick up the current save action preference
|
||||
DownloadAction action = sciGetDownloadAction();
|
||||
DownloadAction imgAction = (action == saveToPhotos) ? saveToPhotos : quickLook;
|
||||
imageDownloadDelegate = [[SCIDownloadDelegate alloc] initWithAction:imgAction showProgress:NO];
|
||||
videoDownloadDelegate = [[SCIDownloadDelegate alloc] initWithAction:action showProgress:YES];
|
||||
}
|
||||
|
||||
// Helper: run a download block with optional confirmation dialog
|
||||
static void sciConfirmAndDownload(NSString *title, void(^downloadBlock)(void)) {
|
||||
if ([SCIUtils getBoolPref:@"dw_confirm"]) {
|
||||
[SCIUtils showConfirmation:downloadBlock title:title];
|
||||
} else {
|
||||
downloadBlock();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: recursively search within a view tree for downloadable media (bounded to one post)
|
||||
static BOOL sciFindAndDownloadMediaInView(UIView *root) {
|
||||
if (!root) return NO;
|
||||
|
||||
// Check for video media via mediaCellFeedItem
|
||||
if ([root respondsToSelector:@selector(mediaCellFeedItem)]) {
|
||||
IGMedia *media = [root performSelector:@selector(mediaCellFeedItem)];
|
||||
if (media) {
|
||||
NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:media];
|
||||
if (videoUrl) {
|
||||
initDownloaders();
|
||||
[videoDownloadDelegate downloadFileWithURL:videoUrl fileExtension:[[videoUrl lastPathComponent] pathExtension] hudLabel:nil];
|
||||
return YES;
|
||||
}
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrlForMedia:media];
|
||||
if (photoUrl) {
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl fileExtension:[[photoUrl lastPathComponent] pathExtension] hudLabel:nil];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for IGFeedPhotoView with delegate chain
|
||||
if ([root isKindOfClass:NSClassFromString(@"IGFeedPhotoView")] && [root respondsToSelector:@selector(delegate)]) {
|
||||
id delegate = [root performSelector:@selector(delegate)];
|
||||
if ([delegate isKindOfClass:NSClassFromString(@"IGFeedItemPhotoCell")]) {
|
||||
@try {
|
||||
Ivar cfgIvar = class_getInstanceVariable([delegate class], "_configuration");
|
||||
if (cfgIvar) {
|
||||
id cfg = object_getIvar(delegate, cfgIvar);
|
||||
if (cfg) {
|
||||
Ivar photoIvar = class_getInstanceVariable([cfg class], "_photo");
|
||||
if (photoIvar) {
|
||||
IGPhoto *photo = object_getIvar(cfg, photoIvar);
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrl:photo];
|
||||
if (photoUrl) {
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl fileExtension:[[photoUrl lastPathComponent] pathExtension] hudLabel:nil];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} @catch (NSException *e) {}
|
||||
}
|
||||
if ([delegate isKindOfClass:NSClassFromString(@"IGFeedItemPagePhotoCell")]) {
|
||||
@try {
|
||||
if ([delegate respondsToSelector:@selector(pagePhotoPost)]) {
|
||||
id pagePhotoPost = [delegate performSelector:@selector(pagePhotoPost)];
|
||||
if (pagePhotoPost && [pagePhotoPost respondsToSelector:@selector(photo)]) {
|
||||
IGPhoto *photo = [pagePhotoPost performSelector:@selector(photo)];
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrl:photo];
|
||||
if (photoUrl) {
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl fileExtension:[[photoUrl lastPathComponent] pathExtension] hudLabel:nil];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
} @catch (NSException *e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into subviews
|
||||
for (UIView *sub in root.subviews) {
|
||||
if (sciFindAndDownloadMediaInView(sub)) return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Helper: find IGMedia from a cell using runtime ivar scanning
|
||||
// Avoids property getters which can cause EXC_BAD_ACCESS on certain IG versions
|
||||
static IGMedia * _Nullable sciGetMediaFromView(UIView *view) {
|
||||
if (!view) return nil;
|
||||
|
||||
unsigned int ivarCount = 0;
|
||||
Ivar *ivars = class_copyIvarList([view class], &ivarCount);
|
||||
if (!ivars) return nil;
|
||||
|
||||
IGMedia *found = nil;
|
||||
Class mediaClass = NSClassFromString(@"IGMedia");
|
||||
|
||||
for (unsigned int i = 0; i < ivarCount; i++) {
|
||||
const char *name = ivar_getName(ivars[i]);
|
||||
if (!name) continue;
|
||||
|
||||
NSString *ivarName = [NSString stringWithUTF8String:name];
|
||||
NSString *lower = [ivarName lowercaseString];
|
||||
|
||||
if ([lower containsString:@"video"] || [lower containsString:@"media"] || [lower containsString:@"item"]) {
|
||||
id value = object_getIvar(view, ivars[i]);
|
||||
if (value && mediaClass && [value isKindOfClass:mediaClass]) {
|
||||
found = (IGMedia *)value;
|
||||
NSLog(@"[SCInsta] Found IGMedia in ivar '%@' of %@", ivarName, NSStringFromClass([view class]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(ivars);
|
||||
return found;
|
||||
}
|
||||
|
||||
// Helper: walk superview chain to find a view of a given class
|
||||
static UIView * _Nullable sciFindSuperviewOfClass(UIView *view, NSString *className) {
|
||||
Class cls = NSClassFromString(className);
|
||||
if (!cls) return nil;
|
||||
UIView *current = view.superview;
|
||||
int depth = 0;
|
||||
while (current && depth < 15) {
|
||||
if ([current isKindOfClass:cls]) return current;
|
||||
current = current.superview;
|
||||
depth++;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Helper: show debug ivar dump when media extraction fails (survives IG updates)
|
||||
static void sciShowDebugIvarDump(UIView *cell) {
|
||||
NSMutableString *debug = [NSMutableString stringWithFormat:@"No IGMedia found in %@\n\nIvars:\n", NSStringFromClass([cell class])];
|
||||
unsigned int count = 0;
|
||||
Ivar *ivars = class_copyIvarList([cell class], &count);
|
||||
for (unsigned int i = 0; i < count && i < 50; i++) {
|
||||
const char *name = ivar_getName(ivars[i]);
|
||||
const char *type = ivar_getTypeEncoding(ivars[i]);
|
||||
if (name) [debug appendFormat:@"%s (%s)\n", name, type ? type : "?"];
|
||||
}
|
||||
if (ivars) free(ivars);
|
||||
|
||||
NSLog(@"[SCInsta] Debug: %@", debug);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SCInsta Debug"
|
||||
message:debug
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Copy & Close" style:UIAlertActionStyleDefault handler:^(UIAlertAction *a) {
|
||||
[[UIPasteboard generalPasteboard] setString:debug];
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleCancel handler:nil]];
|
||||
UIViewController *topVC = topMostController();
|
||||
if (topVC) [topVC presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
// Whether download buttons (not long-press) are enabled
|
||||
static BOOL sciUseDownloadButtons() {
|
||||
return [[SCIUtils getStringPref:@"dw_method"] isEqualToString:@"button"];
|
||||
}
|
||||
|
||||
|
||||
/* * Feed * */
|
||||
|
||||
// Download feed images
|
||||
%hook IGFeedPhotoView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if (![SCIUtils getBoolPref:@"dw_feed_posts"]) return;
|
||||
|
||||
if (sciUseDownloadButtons()) {
|
||||
[self sciAddDownloadButton];
|
||||
} else {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
}
|
||||
%new - (void)sciAddDownloadButton {
|
||||
if ([self viewWithTag:1338]) return;
|
||||
|
||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
btn.tag = 1338;
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:20 weight:UIImageSymbolWeightMedium];
|
||||
[btn setImage:[UIImage systemImageNamed:@"arrow.down.to.line" withConfiguration:config] forState:UIControlStateNormal];
|
||||
btn.tintColor = [UIColor whiteColor];
|
||||
btn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||
btn.layer.cornerRadius = 16;
|
||||
btn.clipsToBounds = YES;
|
||||
btn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[btn addTarget:self action:@selector(sciDownloadBtnTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:btn];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[btn.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:10],
|
||||
[btn.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-10],
|
||||
[btn.widthAnchor constraintEqualToConstant:32],
|
||||
[btn.heightAnchor constraintEqualToConstant:32]
|
||||
]];
|
||||
}
|
||||
%new - (void)sciDownloadBtnTapped:(UIButton *)sender {
|
||||
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[haptic impactOccurred];
|
||||
[UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformMakeScale(0.75, 0.75); }
|
||||
completion:^(BOOL f) { [UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformIdentity; }]; }];
|
||||
|
||||
sciConfirmAndDownload(@"Download photo?", ^{
|
||||
[self handleLongPress:nil];
|
||||
});
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender && sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
IGPhoto *photo;
|
||||
|
||||
if ([self.delegate isKindOfClass:%c(IGFeedItemPhotoCell)]) {
|
||||
IGFeedItemPhotoCellConfiguration *_configuration = MSHookIvar<IGFeedItemPhotoCellConfiguration *>(self.delegate, "_configuration");
|
||||
if (!_configuration) return;
|
||||
photo = MSHookIvar<IGPhoto *>(_configuration, "_photo");
|
||||
}
|
||||
else if ([self.delegate isKindOfClass:%c(IGFeedItemPagePhotoCell)]) {
|
||||
IGFeedItemPagePhotoCell *pagePhotoCell = self.delegate;
|
||||
photo = pagePhotoCell.pagePhotoPost.photo;
|
||||
}
|
||||
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrl:photo];
|
||||
if (!photoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract photo url from post"];
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl
|
||||
fileExtension:[[photoUrl lastPathComponent]pathExtension]
|
||||
hudLabel:nil];
|
||||
}
|
||||
%end
|
||||
|
||||
// Download feed videos
|
||||
%hook IGModernFeedVideoCell.IGModernFeedVideoCell
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if (![SCIUtils getBoolPref:@"dw_feed_posts"]) return;
|
||||
|
||||
if (sciUseDownloadButtons()) {
|
||||
[self sciAddDownloadButton];
|
||||
} else {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
}
|
||||
%new - (void)sciAddDownloadButton {
|
||||
UIView *selfView = (UIView *)self;
|
||||
if ([selfView viewWithTag:1338]) return;
|
||||
|
||||
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
btn.tag = 1338;
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:20 weight:UIImageSymbolWeightMedium];
|
||||
[btn setImage:[UIImage systemImageNamed:@"arrow.down.to.line" withConfiguration:config] forState:UIControlStateNormal];
|
||||
btn.tintColor = [UIColor whiteColor];
|
||||
btn.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.4];
|
||||
btn.layer.cornerRadius = 16;
|
||||
btn.clipsToBounds = YES;
|
||||
btn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[btn addTarget:self action:@selector(sciDownloadBtnTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[selfView addSubview:btn];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[btn.leadingAnchor constraintEqualToAnchor:selfView.leadingAnchor constant:10],
|
||||
[btn.bottomAnchor constraintEqualToAnchor:selfView.bottomAnchor constant:-10],
|
||||
[btn.widthAnchor constraintEqualToConstant:32],
|
||||
[btn.heightAnchor constraintEqualToConstant:32]
|
||||
]];
|
||||
}
|
||||
%new - (void)sciDownloadBtnTapped:(UIButton *)sender {
|
||||
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[haptic impactOccurred];
|
||||
[UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformMakeScale(0.75, 0.75); }
|
||||
completion:^(BOOL f) { [UIView animateWithDuration:0.1 animations:^{ sender.transform = CGAffineTransformIdentity; }]; }];
|
||||
|
||||
sciConfirmAndDownload(@"Download video?", ^{
|
||||
[self handleLongPress:nil];
|
||||
});
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender && sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:[self mediaCellFeedItem]];
|
||||
if (!videoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract video url from post"];
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[videoDownloadDelegate downloadFileWithURL:videoUrl
|
||||
fileExtension:[[videoUrl lastPathComponent] pathExtension]
|
||||
hudLabel:nil];
|
||||
}
|
||||
%end
|
||||
|
||||
|
||||
|
||||
/* * Reels * */
|
||||
|
||||
// Download reels (photos) — long press only when gesture mode selected
|
||||
%hook IGSundialViewerPhotoView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"dw_reels"] && !sciUseDownloadButtons()) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
@try {
|
||||
IGPhoto *_photo = nil;
|
||||
@try {
|
||||
_photo = MSHookIvar<IGPhoto *>(self, "_photo");
|
||||
} @catch (NSException *e) {}
|
||||
|
||||
if (!_photo) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not access reel photo"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrl:_photo];
|
||||
if (!photoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract photo url from reel"];
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl
|
||||
fileExtension:[[photoUrl lastPathComponent]pathExtension]
|
||||
hudLabel:nil];
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] Reel photo download error: %@", exception);
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Reel photo download failed: %@", exception.reason]];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Download reels (videos) — long press only when gesture mode selected
|
||||
%hook IGSundialViewerVideoCell
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"dw_reels"] && !sciUseDownloadButtons()) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
@try {
|
||||
IGMedia *media = sciGetMediaFromView(self);
|
||||
if (!media) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not access reel media"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:media];
|
||||
if (!videoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract video url from reel"];
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[videoDownloadDelegate downloadFileWithURL:videoUrl
|
||||
fileExtension:[[videoUrl lastPathComponent] pathExtension]
|
||||
hudLabel:nil];
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"[SCInsta] Reel download error: %@", exception);
|
||||
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:@"Reel download failed: %@", exception.reason]];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Download button on reels vertical UFI (like/comment/share sidebar)
|
||||
%hook IGSundialViewerVerticalUFI
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if (![SCIUtils getBoolPref:@"dw_reels"]) return;
|
||||
if (!sciUseDownloadButtons()) return;
|
||||
if (!self.superview) return;
|
||||
|
||||
// Add to superview so we're not clipped by the narrow 29pt UFI
|
||||
UIView *parent = self.superview;
|
||||
if ([parent viewWithTag:1337]) return;
|
||||
|
||||
UIButton *downloadBtn = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
downloadBtn.tag = 1337;
|
||||
|
||||
// Match IG reel sidebar style: outline icon, semi-transparent white
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:24 weight:UIImageSymbolWeightSemibold];
|
||||
UIImage *icon = [UIImage systemImageNamed:@"arrow.down" withConfiguration:config];
|
||||
[downloadBtn setImage:icon forState:UIControlStateNormal];
|
||||
downloadBtn.tintColor = [UIColor colorWithWhite:1.0 alpha:0.9];
|
||||
|
||||
downloadBtn.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
downloadBtn.layer.shadowOffset = CGSizeMake(0, 1);
|
||||
downloadBtn.layer.shadowOpacity = 0.5;
|
||||
downloadBtn.layer.shadowRadius = 3;
|
||||
|
||||
downloadBtn.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[downloadBtn addTarget:self action:@selector(sciDownloadTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[parent addSubview:downloadBtn];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[downloadBtn.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
||||
[downloadBtn.bottomAnchor constraintEqualToAnchor:self.topAnchor constant:-10],
|
||||
[downloadBtn.widthAnchor constraintEqualToConstant:40],
|
||||
[downloadBtn.heightAnchor constraintEqualToConstant:40]
|
||||
]];
|
||||
}
|
||||
|
||||
%new - (void)sciDownloadTapped:(UIButton *)sender {
|
||||
NSLog(@"[SCInsta] Reel download button tapped");
|
||||
|
||||
// Haptic + visual feedback
|
||||
UIImpactFeedbackGenerator *haptic = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
|
||||
[haptic impactOccurred];
|
||||
[UIView animateWithDuration:0.1 animations:^{
|
||||
sender.transform = CGAffineTransformMakeScale(0.75, 0.75);
|
||||
} completion:^(BOOL finished) {
|
||||
[UIView animateWithDuration:0.1 animations:^{
|
||||
sender.transform = CGAffineTransformIdentity;
|
||||
}];
|
||||
}];
|
||||
|
||||
sciConfirmAndDownload(@"Download reel?", ^{
|
||||
// Find IGSundialViewerVideoCell in superview chain
|
||||
UIView *videoCell = sciFindSuperviewOfClass(self, @"IGSundialViewerVideoCell");
|
||||
|
||||
if (videoCell) {
|
||||
IGMedia *media = sciGetMediaFromView(videoCell);
|
||||
if (media) {
|
||||
NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:media];
|
||||
if (videoUrl) {
|
||||
initDownloaders();
|
||||
[videoDownloadDelegate downloadFileWithURL:videoUrl
|
||||
fileExtension:[[videoUrl lastPathComponent] pathExtension]
|
||||
hudLabel:nil];
|
||||
return;
|
||||
}
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract video URL from reel"];
|
||||
return;
|
||||
}
|
||||
sciShowDebugIvarDump(videoCell);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try photo reel
|
||||
UIView *photoView = sciFindSuperviewOfClass(self, @"IGSundialViewerPhotoView");
|
||||
if (photoView) {
|
||||
unsigned int count = 0;
|
||||
Ivar *ivars = class_copyIvarList([photoView class], &count);
|
||||
Class photoClass = NSClassFromString(@"IGPhoto");
|
||||
for (unsigned int i = 0; i < count; i++) {
|
||||
const char *name = ivar_getName(ivars[i]);
|
||||
if (!name) continue;
|
||||
NSString *ivarName = [NSString stringWithUTF8String:name];
|
||||
if ([[ivarName lowercaseString] containsString:@"photo"]) {
|
||||
id value = object_getIvar(photoView, ivars[i]);
|
||||
if (value && photoClass && [value isKindOfClass:photoClass]) {
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrl:(IGPhoto *)value];
|
||||
if (photoUrl) {
|
||||
free(ivars);
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl
|
||||
fileExtension:[[photoUrl lastPathComponent] pathExtension]
|
||||
hudLabel:nil];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ivars) free(ivars);
|
||||
sciShowDebugIvarDump(photoView);
|
||||
return;
|
||||
}
|
||||
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not find reel cell in view hierarchy"];
|
||||
});
|
||||
}
|
||||
%end
|
||||
|
||||
|
||||
/* * Stories * */
|
||||
|
||||
// Download story (images)
|
||||
%hook IGStoryPhotoView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"dw_story"]) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
NSURL *photoUrl = [SCIUtils getPhotoUrlForMedia:[self item]];
|
||||
if (!photoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract photo url from story"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:photoUrl
|
||||
fileExtension:[[photoUrl lastPathComponent]pathExtension]
|
||||
hudLabel:nil];
|
||||
}
|
||||
%end
|
||||
|
||||
// Download story (videos)
|
||||
%hook IGStoryModernVideoView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"dw_story"]) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
NSURL *videoUrl = [SCIUtils getVideoUrlForMedia:self.item];
|
||||
|
||||
if (!videoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract video url from story"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[videoDownloadDelegate downloadFileWithURL:videoUrl
|
||||
fileExtension:[[videoUrl lastPathComponent] pathExtension]
|
||||
hudLabel:nil];
|
||||
}
|
||||
%end
|
||||
|
||||
// Download story (videos, legacy)
|
||||
%hook IGStoryVideoView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"dw_story"]) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = [SCIUtils getDoublePref:@"dw_finger_duration"];
|
||||
longPress.numberOfTouchesRequired = [SCIUtils getDoublePref:@"dw_finger_count"];
|
||||
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
NSURL *videoUrl;
|
||||
|
||||
IGStoryFullscreenSectionController *captionDelegate = self.captionDelegate;
|
||||
if (captionDelegate) {
|
||||
videoUrl = [SCIUtils getVideoUrlForMedia:captionDelegate.currentStoryItem];
|
||||
}
|
||||
else {
|
||||
// Direct messages video player
|
||||
id parentVC = [SCIUtils nearestViewControllerForView:self];
|
||||
if (!parentVC || ![parentVC isKindOfClass:%c(IGDirectVisualMessageViewerController)]) return;
|
||||
|
||||
IGDirectVisualMessageViewerViewModeAwareDataSource *_dataSource = MSHookIvar<IGDirectVisualMessageViewerViewModeAwareDataSource *>(parentVC, "_dataSource");
|
||||
if (!_dataSource) return;
|
||||
|
||||
IGDirectVisualMessage *_currentMessage = MSHookIvar<IGDirectVisualMessage *>(_dataSource, "_currentMessage");
|
||||
if (!_currentMessage) return;
|
||||
|
||||
IGVideo *rawVideo = _currentMessage.rawVideo;
|
||||
if (!rawVideo) return;
|
||||
|
||||
videoUrl = [SCIUtils getVideoUrl:rawVideo];
|
||||
}
|
||||
|
||||
if (!videoUrl) {
|
||||
[SCIUtils showErrorHUDWithDescription:@"Could not extract video url from story"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
initDownloaders();
|
||||
[videoDownloadDelegate downloadFileWithURL:videoUrl
|
||||
fileExtension:[[videoUrl lastPathComponent] pathExtension]
|
||||
hudLabel:nil];
|
||||
}
|
||||
%end
|
||||
|
||||
|
||||
/* * Profile pictures * */
|
||||
|
||||
%hook IGProfilePictureImageView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"save_profile"]) {
|
||||
[self addLongPressGestureRecognizer];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
%new - (void)addLongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
IGImageView *_imageView = MSHookIvar<IGImageView *>(self, "_imageView");
|
||||
if (!_imageView) return;
|
||||
|
||||
IGImageSpecifier *imageSpecifier = _imageView.imageSpecifier;
|
||||
if (!imageSpecifier) return;
|
||||
|
||||
NSURL *imageUrl = imageSpecifier.url;
|
||||
if (!imageUrl) return;
|
||||
|
||||
initDownloaders();
|
||||
[imageDownloadDelegate downloadFileWithURL:imageUrl
|
||||
fileExtension:[[imageUrl lastPathComponent] pathExtension]
|
||||
hudLabel:@"Loading"];
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGSundialViewerNavigationBarOld
|
||||
- (void)didMoveToWindow {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_reels_header"]) {
|
||||
NSLog(@"[SCInsta] Hiding reels header");
|
||||
|
||||
[self removeFromSuperview];
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,72 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGSundialPlaybackControlsTestConfiguration
|
||||
- (id)initWithLauncherSet:(id)set
|
||||
tapToPauseEnabled:(_Bool)tapPauseEnabled
|
||||
combineSingleTapPlaybackControls:(_Bool)controls
|
||||
isVideoPreviewThumbnailEnabled:(_Bool)previewThumbEnabled
|
||||
minScrubberDurationSec:(long long)minSec
|
||||
seekResumeScrubberCooldownSec:(double)seekSec
|
||||
tapResumeScrubberCooldownSec:(double)tapSec
|
||||
persistentScrubberMinVideoDuration:(long long)duration
|
||||
isScrubberForShortVideoEnabled:(_Bool)shortScrubberEnabled
|
||||
{
|
||||
_Bool userTapPauseEnabled = tapPauseEnabled;
|
||||
if ([[SCIUtils getStringPref:@"reels_tap_control"] isEqualToString:@"pause"]) userTapPauseEnabled = true;
|
||||
else if ([[SCIUtils getStringPref:@"reels_tap_control"] isEqualToString:@"mute"]) userTapPauseEnabled = false;
|
||||
|
||||
long long userMinSec = minSec;
|
||||
long long userDuration = duration;
|
||||
_Bool userShortScrubberEnabled = shortScrubberEnabled;
|
||||
if ([SCIUtils getBoolPref:@"reels_show_scrubber"]) {
|
||||
userMinSec = 0;
|
||||
userDuration = 0;
|
||||
userShortScrubberEnabled = true;
|
||||
}
|
||||
|
||||
return %orig(set, userTapPauseEnabled, controls, previewThumbEnabled, userMinSec, seekSec, tapSec, userDuration, userShortScrubberEnabled);
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGSundialFeedViewController
|
||||
- (void)_refreshReelsWithParamsForNetworkRequest:(NSInteger)arg1 userDidPullToRefresh:(BOOL)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"prevent_doom_scrolling"]) {
|
||||
IGRefreshControl *_refreshControl = MSHookIvar<IGRefreshControl *>(self, "_refreshControl");
|
||||
[self refreshControlDidEndFinishLoadingAnimation:_refreshControl];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ([SCIUtils getBoolPref:@"refresh_reel_confirm"]) {
|
||||
NSLog(@"[SCInsta] Reel refresh triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig(arg1, arg2); }
|
||||
cancelHandler:^(void) {
|
||||
IGRefreshControl *_refreshControl = MSHookIvar<IGRefreshControl *>(self, "_refreshControl");
|
||||
[self refreshControlDidEndFinishLoadingAnimation:_refreshControl];
|
||||
}
|
||||
title:@"Refresh Reels"];
|
||||
} else {
|
||||
return %orig(arg1, arg2);
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// * Disable volume/mute button triggering unmutes
|
||||
%hook IGAudioStatusAnnouncer
|
||||
- (void)_muteSwitchStateChanged:(id)changed {
|
||||
if (![SCIUtils getBoolPref:@"disable_auto_unmuting_reels"]) {
|
||||
%orig(changed);
|
||||
}
|
||||
}
|
||||
- (void)_didPressVolumeButton:(id)button {
|
||||
if (![SCIUtils getBoolPref:@"disable_auto_unmuting_reels"]) {
|
||||
%orig(button);
|
||||
}
|
||||
}
|
||||
- (void)_didUnplugHeadphones:(id)headphones {
|
||||
if (![SCIUtils getBoolPref:@"disable_auto_unmuting_reels"]) {
|
||||
%orig(headphones);
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,58 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
#define QUICKSNAPENABLED(orig) return [SCIUtils getBoolPref:@"disable_instants_creation"] ? false : orig;
|
||||
|
||||
// Demangled name: IGQuickSnapExperimentation.IGQuickSnapExperimentationHelper
|
||||
%hook _TtC26IGQuickSnapExperimentation32IGQuickSnapExperimentationHelper
|
||||
+ (_Bool)isQuicksnapEnabled:(id)enabled {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapEnabledInFeed:(id)feed {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapEnabledInInbox:(id)inbox {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapEnabledInStories:(id)stories {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapEnabledInNotesTray:(id)tray {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapEnabledInNotesTrayWithPeek:(id)peek {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapEnabledInNotesTrayWithPog:(id)pog {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
+ (_Bool)isQuicksnapNotesTrayEmptyPogEnabled:(id)enabled {
|
||||
QUICKSNAPENABLED(%orig);
|
||||
}
|
||||
// + (_Bool)isStoriesSpringEnabled:(id)enabled {
|
||||
// return true;
|
||||
// }
|
||||
// + (_Bool)shouldEnableScreenshotBlocking:(id)blocking {
|
||||
// return false;
|
||||
// }
|
||||
// + (_Bool)areFiltersEnabled:(id)enabled {
|
||||
// return true;
|
||||
// }
|
||||
// + (_Bool)isBottomsheetCustomAudienceEnabled:(id)enabled {
|
||||
// return true;
|
||||
// }
|
||||
// + (_Bool)isVideoCaptureEnabled:(id)enabled {
|
||||
// return true;
|
||||
// }
|
||||
%end
|
||||
|
||||
// %hook IGDirectNotesTrayRowCell
|
||||
// - (_Bool)isQuicksnapPeekVisible {
|
||||
// return true;
|
||||
// }
|
||||
// %end
|
||||
|
||||
// %hook IGDirectNotesTrayRowSectionController
|
||||
// - (_Bool)isQuicksnapPeekVisible {
|
||||
// return true;
|
||||
// }
|
||||
// %end
|
||||
@@ -0,0 +1,245 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
// Bypass flag: when YES, all hooks let calls through (for manual mark as seen)
|
||||
static BOOL sciSeenBypassActive = NO;
|
||||
|
||||
static BOOL sciShouldBlockSeen() {
|
||||
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;
|
||||
}
|
||||
- (void)uploadSeenStateWithMedia:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)uploadSeenState {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)_uploadSeenState:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (void)sendSeenReceipt:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) return;
|
||||
%orig;
|
||||
}
|
||||
- (id)networker {
|
||||
if (sciShouldBlockSeen()) return nil;
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// Block seen tracking on fullscreen section controller
|
||||
%hook IGStoryFullscreenSectionController
|
||||
- (void)markItemAsSeen:(id)arg1 {
|
||||
if (sciShouldBlockSeen()) 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;
|
||||
%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;
|
||||
%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;
|
||||
}
|
||||
%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;
|
||||
}
|
||||
%end
|
||||
|
||||
// Manual "mark as seen" button on story overlay
|
||||
%hook IGStoryFullscreenOverlayView
|
||||
- (void)didMoveToSuperview {
|
||||
%orig;
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
// Re-enable blocking
|
||||
sciSeenBypassActive = NO;
|
||||
|
||||
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"];
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,9 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGDirectTypingStatusService
|
||||
- (void)updateOutgoingStatusIsActive:(_Bool)active threadKey:(id)key threadMetadata:(id)metadata typingStatusType:(long long)type {
|
||||
if ([SCIUtils getBoolPref:@"disable_typing_status"]) return;
|
||||
|
||||
return %orig(active, key, metadata, type);
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,22 @@
|
||||
#import "../../Utils.h"
|
||||
#import "../../InstagramHeaders.h"
|
||||
|
||||
%hook IGDirectRealtimeIrisThreadDelta
|
||||
+ (id)removeItemWithMessageId:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"keep_deleted_message"]) {
|
||||
arg1 = NULL;
|
||||
}
|
||||
|
||||
return %orig(arg1);
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGDirectMessageUpdate
|
||||
+ (id)removeMessageWithMessageId:(id)arg1{
|
||||
if ([SCIUtils getBoolPref:@"keep_deleted_message"]) {
|
||||
arg1 = NULL;
|
||||
}
|
||||
|
||||
return %orig(arg1);
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,96 @@
|
||||
#import "../../InstagramHeaders.h"
|
||||
#import "../../Tweak.h"
|
||||
#import "../../Utils.h"
|
||||
|
||||
// Seen buttons (in DMs)
|
||||
// - Enables no seen for messages
|
||||
// - Enables unlimited views of DM visual messages
|
||||
%hook IGTallNavigationBarView
|
||||
- (void)setRightBarButtonItems:(NSArray <UIBarButtonItem *> *)items {
|
||||
NSMutableArray *new_items = [[items filteredArrayUsingPredicate:
|
||||
[NSPredicate predicateWithBlock:^BOOL(UIView *value, NSDictionary *_) {
|
||||
if ([SCIUtils getBoolPref:@"hide_reels_blend"]) {
|
||||
return ![value.accessibilityIdentifier isEqualToString:@"blend-button"];
|
||||
}
|
||||
|
||||
return true;
|
||||
}]
|
||||
] mutableCopy];
|
||||
|
||||
// Messages seen
|
||||
if ([SCIUtils getBoolPref:@"remove_lastseen"]) {
|
||||
UIBarButtonItem *seenButton = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"checkmark.message"] style:UIBarButtonItemStylePlain target:self action:@selector(seenButtonHandler:)];
|
||||
[new_items addObject:seenButton];
|
||||
}
|
||||
|
||||
// DM visual messages viewed
|
||||
if ([SCIUtils getBoolPref:@"unlimited_replay"]) {
|
||||
UIBarButtonItem *dmVisualMsgsViewedButton = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"photo.badge.checkmark"] style:UIBarButtonItemStylePlain target:self action:@selector(dmVisualMsgsViewedButtonHandler:)];
|
||||
[new_items addObject:dmVisualMsgsViewedButton];
|
||||
|
||||
if (dmVisualMsgsViewedButtonEnabled) {
|
||||
[dmVisualMsgsViewedButton setTintColor:SCIUtils.SCIColor_Primary];
|
||||
} else {
|
||||
[dmVisualMsgsViewedButton setTintColor:UIColor.labelColor];
|
||||
}
|
||||
}
|
||||
|
||||
%orig([new_items copy]);
|
||||
}
|
||||
|
||||
// Messages seen button
|
||||
%new - (void)seenButtonHandler:(UIBarButtonItem *)sender {
|
||||
UIViewController *nearestVC = [SCIUtils nearestViewControllerForView:self];
|
||||
if ([nearestVC isKindOfClass:%c(IGDirectThreadViewController)]) {
|
||||
[(IGDirectThreadViewController *)nearestVC markLastMessageAsSeen];
|
||||
|
||||
[SCIUtils showToastForDuration:2.5 title:@"Marked messages as seen"];
|
||||
}
|
||||
}
|
||||
// DM visual messages viewed button
|
||||
%new - (void)dmVisualMsgsViewedButtonHandler:(UIBarButtonItem *)sender {
|
||||
if (dmVisualMsgsViewedButtonEnabled) {
|
||||
dmVisualMsgsViewedButtonEnabled = false;
|
||||
[sender setTintColor:UIColor.labelColor];
|
||||
|
||||
[SCIUtils showToastForDuration:4.5 title:@"Visual messages can be replayed without expiring"];
|
||||
}
|
||||
else {
|
||||
dmVisualMsgsViewedButtonEnabled = true;
|
||||
[sender setTintColor:SCIUtils.SCIColor_Primary];
|
||||
|
||||
[SCIUtils showToastForDuration:4.5 title:@"Visual messages will now expire after viewing"];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Messages seen logic
|
||||
%hook IGDirectThreadViewListAdapterDataSource
|
||||
- (BOOL)shouldUpdateLastSeenMessage {
|
||||
if ([SCIUtils getBoolPref:@"remove_lastseen"]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
// DM stories viewed logic
|
||||
%hook IGDirectVisualMessageViewerEventHandler
|
||||
- (void)visualMessageViewerController:(id)arg1 didBeginPlaybackForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 {
|
||||
if ([SCIUtils getBoolPref:@"unlimited_replay"]) {
|
||||
// Check if dm stories should be marked as viewed
|
||||
if (dmVisualMsgsViewedButtonEnabled) {
|
||||
%orig;
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)visualMessageViewerController:(id)arg1 didEndPlaybackForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 mediaCurrentTime:(CGFloat)arg4 forNavType:(NSInteger)arg5 {
|
||||
if ([SCIUtils getBoolPref:@"unlimited_replay"]) {
|
||||
// Check if dm stories should be marked as viewed
|
||||
if (dmVisualMsgsViewedButtonEnabled) {
|
||||
%orig;
|
||||
}
|
||||
}
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,21 @@
|
||||
#import "../../Utils.h"
|
||||
|
||||
%hook IGDirectVisualMessage
|
||||
- (NSInteger)viewMode {
|
||||
NSInteger mode = %orig;
|
||||
|
||||
// * Modes *
|
||||
// 0 - View Once
|
||||
// 1 - Replayable
|
||||
|
||||
if ([SCIUtils getBoolPref:@"disable_view_once_limitations"]) {
|
||||
if (mode == 0) {
|
||||
mode = 1;
|
||||
|
||||
NSLog(@"[SCInsta] Modifying visual message from read-once to replayable");
|
||||
}
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
%end
|
||||
@@ -0,0 +1,616 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <objc/NSObject.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "../modules/JGProgressHUD/JGProgressHUD.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define _Bool bool
|
||||
#endif
|
||||
|
||||
@interface NSURL ()
|
||||
- (id)normalizedURL; // method provided by Instagram app
|
||||
@end
|
||||
|
||||
@interface IGActionableConfirmationToastViewModel : NSObject {
|
||||
NSString *_text_annotatedTitleText;
|
||||
NSString *_text_annotatedSubtitleText;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface IGActionableConfirmationToastPresenter : NSObject
|
||||
- (void)showAlertWithViewModel:(id)model isAnimated:(_Bool)animated animationDuration:(double)duration presentationPriority:(long long)priority tapActionBlock:(id)tap presentedHandler:(id)presented dismissedHandler:(id)dismissed;
|
||||
- (void)hideAlert;
|
||||
@end
|
||||
|
||||
@interface IGRootViewController : UIViewController
|
||||
- (IGActionableConfirmationToastPresenter *)toastPresenter;
|
||||
|
||||
- (void)addHandleLongPress; // new
|
||||
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; // new
|
||||
@end
|
||||
|
||||
@interface IGViewController : UIViewController
|
||||
- (void)_superPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(id)completion;
|
||||
@end
|
||||
|
||||
@interface IGMainFeedAppHeaderController : UIViewController
|
||||
- (void)_superPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(id)completion; // new
|
||||
@end
|
||||
|
||||
@interface IGShimmeringGridView : UIView
|
||||
@end
|
||||
|
||||
@interface IGExploreGridViewController : IGViewController
|
||||
@end
|
||||
|
||||
@interface UIImage ()
|
||||
- (NSString *)ig_imageName;
|
||||
@end
|
||||
|
||||
@interface IGProfileMenuSheetViewController : IGViewController
|
||||
@end
|
||||
|
||||
@interface IGTabBar: UIView
|
||||
@end
|
||||
|
||||
@interface IGTabBarController : UIViewController
|
||||
@end
|
||||
|
||||
@interface IGTableViewCell: UITableViewCell
|
||||
- (id)initWithReuseIdentifier:(NSString *)identifier;
|
||||
@end
|
||||
|
||||
@interface IGProfileSheetTableViewCell : IGTableViewCell
|
||||
@end
|
||||
|
||||
@interface IGTallNavigationBarView : UIView
|
||||
@end
|
||||
|
||||
@interface UIView (RCTViewUnmounting)
|
||||
@property(retain, nonatomic) UIViewController *viewController;
|
||||
- (UIView *)_rootView;
|
||||
@end
|
||||
|
||||
@interface IGImageSpecifier : NSObject
|
||||
@property(readonly, nonatomic) NSURL *url;
|
||||
@end
|
||||
|
||||
@interface IGVideo : NSObject
|
||||
- (id)sortedVideoURLsBySize; // Before Instagram v398
|
||||
- (id)allVideoURLs; // After Instagram v398
|
||||
@end
|
||||
|
||||
@interface IGPhoto : NSObject
|
||||
- (id)imageURLForWidth:(CGFloat)width;
|
||||
@end
|
||||
|
||||
@interface IGBaseMedia : NSObject
|
||||
@property (retain, nonatomic) id explorePostInFeed;
|
||||
@end
|
||||
|
||||
@interface IGMedia : IGBaseMedia
|
||||
@property(readonly) IGVideo *video;
|
||||
@property(readonly) IGPhoto *photo;
|
||||
@end
|
||||
|
||||
@interface IGPostItem : NSObject
|
||||
@property(readonly) IGVideo *video;
|
||||
@property(readonly) IGPhoto *photo;
|
||||
@end
|
||||
|
||||
@interface IGPageMediaView : UIView
|
||||
@property(readonly) NSMutableArray <IGPostItem *> *items;
|
||||
- (IGPostItem *)currentMediaItem;
|
||||
@end
|
||||
|
||||
@interface IGFeedItem : NSObject
|
||||
@property long long likeCount;
|
||||
@property(readonly) IGVideo *video;
|
||||
- (BOOL)isSponsored;
|
||||
- (BOOL)isSponsoredApp;
|
||||
@end
|
||||
|
||||
@interface IGImageView : UIImageView
|
||||
@property(retain, nonatomic) IGImageSpecifier *imageSpecifier;
|
||||
@end
|
||||
|
||||
@interface IGFeedItemPagePhotoCell : UICollectionViewCell
|
||||
@property (nonatomic, strong) id post;
|
||||
@property (nonatomic, strong) IGPostItem *pagePhotoPost;
|
||||
@end
|
||||
|
||||
@interface IGProfilePicturePreviewViewController : UIViewController
|
||||
{
|
||||
IGImageView *_profilePictureView;
|
||||
}
|
||||
@property (nonatomic, strong) JGProgressHUD *hud;
|
||||
- (void)addHandleLongPress; // new
|
||||
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; // new
|
||||
@end
|
||||
|
||||
@interface IGFeedItemMediaCell : UICollectionViewCell
|
||||
@property(retain, nonatomic) IGMedia *post;
|
||||
- (UIImage *)mediaCellCurrentlyDisplayedImage;
|
||||
@end
|
||||
|
||||
@interface IGFeedItemPhotoCell : IGFeedItemMediaCell
|
||||
@end
|
||||
|
||||
@interface IGFeedItemPhotoCellConfiguration : NSObject
|
||||
@end
|
||||
|
||||
@interface IGFeedPhotoView : UIView
|
||||
@property (nonatomic, strong) id delegate;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
- (void)sciAddDownloadButton; // new
|
||||
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; // new
|
||||
@end
|
||||
|
||||
@interface IGModernFeedVideoCell : UIView
|
||||
- (id)mediaCellFeedItem;
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
- (void)sciAddDownloadButton; // new
|
||||
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; // new
|
||||
@end
|
||||
|
||||
@interface IGSundialViewerVideoCell : UIView
|
||||
@property(readonly, nonatomic) IGMedia *video;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGSundialViewerPhotoView : UIView
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGImageProgressView : UIView
|
||||
@property(retain, nonatomic) IGImageSpecifier *imageSpecifier;
|
||||
@end
|
||||
|
||||
@interface IGStatefulVideoPlayer : NSObject
|
||||
@end
|
||||
|
||||
@interface IGStoryPhotoView : UIView
|
||||
- (id)item;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGStoryFullscreenSectionController : NSObject
|
||||
@property (nonatomic, strong, readwrite) IGMedia *currentStoryItem;
|
||||
@end
|
||||
|
||||
@interface IGStoryVideoView : UIView
|
||||
@property (nonatomic, weak, readwrite) IGStoryFullscreenSectionController *captionDelegate;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGStoryModernVideoView : UIView
|
||||
@property (nonatomic, readonly) IGMedia *item;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGStoryFullscreenOverlayView : UIView
|
||||
@property (nonatomic, weak, readwrite) id gestureDelegate;
|
||||
- (id)gestureDelegate;
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGDirectVisualMessageViewerController : UIViewController
|
||||
@end
|
||||
|
||||
@interface IGDirectVisualMessageViewerViewModeAwareDataSource : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectVisualMessage : NSObject
|
||||
- (id)rawVideo;
|
||||
@end
|
||||
|
||||
@interface IGUser : NSObject
|
||||
@property NSInteger followStatus;
|
||||
@property(copy) NSString *username;
|
||||
@property BOOL followsCurrentUser;
|
||||
@end
|
||||
|
||||
@interface IGFollowController : NSObject
|
||||
@property IGUser *user;
|
||||
@end
|
||||
|
||||
@interface IGCoreTextView : UIView
|
||||
@property(nonatomic, strong) NSString *text;
|
||||
- (void)addHandleLongPress; // new
|
||||
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; // new
|
||||
@end
|
||||
|
||||
@interface IGUserSession : NSObject
|
||||
@property (readonly, nonatomic) IGUser *user;
|
||||
@end
|
||||
|
||||
@interface IGWindow : UIWindow
|
||||
@property (nonatomic) __weak IGUserSession *userSession;
|
||||
@end
|
||||
|
||||
@interface IGShakeWindow : UIWindow
|
||||
@property (nonatomic) __weak IGUserSession *userSession;
|
||||
@end
|
||||
|
||||
@interface IGStyledString : NSObject
|
||||
@property (retain, nonatomic) NSMutableAttributedString *attributedString;
|
||||
- (void)appendString:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGInstagramAppDelegate : NSObject <UIApplicationDelegate>
|
||||
@end
|
||||
|
||||
@interface IGDirectInboxSearchAIAgentsPillsContainerCell : UIView
|
||||
@end
|
||||
|
||||
@interface IGTapButton : UIButton
|
||||
@end
|
||||
|
||||
@interface IGLabel : UILabel
|
||||
@end
|
||||
|
||||
@interface IGLabelItemViewModel : NSObject
|
||||
- (id)labelTitle;
|
||||
- (id)uniqueIdentifier;
|
||||
@end
|
||||
|
||||
@interface IGDirectInboxSuggestedThreadCellViewModel : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectInboxHeaderCellViewModel : NSObject
|
||||
- (id)title;
|
||||
@end
|
||||
|
||||
@interface IGSearchResultViewModel : NSObject
|
||||
- (id)title;
|
||||
- (NSUInteger)itemType;
|
||||
@end
|
||||
|
||||
@interface IGDirectShareRecipient : NSObject
|
||||
- (NSString *)threadName;
|
||||
- (BOOL)isBroadcastChannel;
|
||||
@end
|
||||
|
||||
@interface IGDirectRecipientCellViewModel : NSObject
|
||||
- (id)recipient;
|
||||
- (NSInteger)sectionType;
|
||||
@end
|
||||
|
||||
@interface IGDirectInboxSearchAIAgentsSuggestedPromptRowCell : UIView
|
||||
@end
|
||||
|
||||
@interface IGDSSegmentedPillBarView : UIView
|
||||
- (id)delegate;
|
||||
@end
|
||||
|
||||
@interface IGImageWithAccessoryButton : IGTapButton
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
- (void)handleLongPress:(UILongPressGestureRecognizer *)gr; // new
|
||||
@end
|
||||
|
||||
@interface IGHomeFeedHeaderViewController
|
||||
- (void)headerDidLongPressLogo:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGSearchBarDonutButton : UIView
|
||||
@end
|
||||
|
||||
@interface IGAnimatablePlaceholderTextField : UITextField
|
||||
@end
|
||||
|
||||
@interface IGDirectCommandSystemViewModel : NSObject
|
||||
- (id)row;
|
||||
@end
|
||||
|
||||
@interface IGDirectCommandSystemRow : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectCommandSystemResult : NSObject
|
||||
- (id)title;
|
||||
- (id)commandString;
|
||||
@end
|
||||
|
||||
@interface IGGrowingTextView : UIView
|
||||
- (id)placeholderText;
|
||||
- (void)setPlaceholderText:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGUnifiedVideoCollectionView : UIScrollView
|
||||
@end
|
||||
|
||||
@interface IGBadgedNavigationButton : UIView
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGSearchBar : UIView
|
||||
- (NSObject *)sanitizePlaceholderForConfig:(NSObject *)config; // new
|
||||
@end
|
||||
|
||||
@interface IGSearchBarConfig : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectComposer : UIView
|
||||
- (NSObject *)patchConfig:(NSObject *)config; // new
|
||||
@end
|
||||
|
||||
@interface IGDirectComposerConfig : NSObject
|
||||
@end
|
||||
|
||||
@interface IGAnimatablePlaceholderTextFieldContainer : UIView
|
||||
@end
|
||||
|
||||
@interface IGDirectInboxConfig : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectMediaPickerConfig : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectMediaPickerGalleryConfig : NSObject
|
||||
@end
|
||||
|
||||
@interface IGStoryEyedropperToggleButton : UIControl
|
||||
@property (nonatomic, strong, readwrite) UIColor *color;
|
||||
|
||||
- (void)setPushedDown:(BOOL)pushedDown;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGStoryTextEntryViewController : UIViewController
|
||||
- (void)textViewControllerDidUpdateWithColor:(id)color colorSource:(NSInteger)source;
|
||||
@end
|
||||
|
||||
@interface IGStoryColorPaletteView : UIView
|
||||
@end
|
||||
|
||||
@interface IGProfilePictureImageView : UIView
|
||||
@property (nonatomic, readonly) IGUser *userGQL;
|
||||
|
||||
- (void)addLongPressGestureRecognizer; // new
|
||||
@end
|
||||
|
||||
@interface IGImageRequest : NSObject
|
||||
- (id)url;
|
||||
@end
|
||||
|
||||
@interface IGDiscoveryGridItem : NSObject
|
||||
- (id)model;
|
||||
@end
|
||||
|
||||
@interface IGStoryTextEntryControlsOverlayView : UIView
|
||||
|
||||
@property (readonly, nonatomic) NSMutableArray *animationTypes;
|
||||
@property (readonly, nonatomic) NSMutableArray *effectTypes;
|
||||
|
||||
- (void)reloadData;
|
||||
|
||||
@end
|
||||
|
||||
@interface _TtC27IGGalleryDestinationToolbar31IGGalleryDestinationToolbarView : UIView
|
||||
@property(nonatomic, copy, readwrite) NSArray *tools;
|
||||
@end
|
||||
|
||||
@interface IGSundialViewerVerticalUFI : UIView
|
||||
- (void)_didTapLikeButton:(id)arg1;
|
||||
- (void)_didTapRepostButton:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGMainAppSurfaceIntent : NSObject
|
||||
- (id)tabStringFromSurfaceIntent;
|
||||
@end
|
||||
|
||||
@interface IGSundialFeedViewController : UIViewController
|
||||
- (void)refreshControlDidEndFinishLoadingAnimation:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGRefreshControl : UIControl
|
||||
@end
|
||||
|
||||
@interface IGDirectThreadViewDrawingViewController : UIViewController
|
||||
- (void)drawingControls:controls didSelectColor:color;
|
||||
@end
|
||||
|
||||
@interface IGSundialViewerNavigationBarOld : UIView
|
||||
@end
|
||||
|
||||
@interface IGUFIInteractionCountsView : UIView
|
||||
@end
|
||||
|
||||
@interface IGFeedItemUFICell : UIView
|
||||
- (void)UFIButtonBarDidTapOnRepost:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGNotesCreationFeatureSupportModel : NSObject
|
||||
@end
|
||||
|
||||
@interface IGNotesCustomThemeCreationModel : NSObject
|
||||
+ (id)defaultModelForExpressiveEmojiType:(id)arg1;
|
||||
@end
|
||||
|
||||
@interface IGDirectNotesComposerViewController : UIViewController
|
||||
- (void)notesBubbleEditorViewControllerDidUpdateWithCustomThemeCreationModel:(id)model;
|
||||
@end
|
||||
|
||||
@interface _TtC20IGDirectNotesUISwift41IGDirectNotesBubbleEditorColorPaletteView : UIView
|
||||
@property (nonatomic, copy) UIColor *backgroundColor; // new
|
||||
@property (nonatomic, copy) UIColor *textColor; // new
|
||||
@property (nonatomic, copy) NSString *emojiText; // new
|
||||
|
||||
- (void)presentColorPicker:(NSString *)target; // new
|
||||
- (void)applySCICustomTheme:(NSString *)target; // new
|
||||
@end
|
||||
|
||||
@interface _TtC20IGDirectNotesUISwift39IGDirectNotesBubbleEditorViewController : UIViewController
|
||||
@property (nonatomic) IGDirectNotesComposerViewController *delegate;
|
||||
@end
|
||||
|
||||
@interface IGDSBottomButtonsView : UIView
|
||||
- (void)setPrimaryButtonEnabled:(BOOL)enabled;
|
||||
- (void)setSecondaryButtonEnabled:(BOOL)enabled;
|
||||
@end
|
||||
|
||||
@interface IGStoryTrayViewModel : NSObject
|
||||
@property (nonatomic, readonly) NSString *pk;
|
||||
@property (nonatomic, readonly) BOOL isUnseenNux;
|
||||
@end
|
||||
|
||||
@interface _TtC32IGSundialOrganicCTAContainerView32IGSundialOrganicCTAContainerView : UIView
|
||||
@end
|
||||
|
||||
@interface IGCommentThreadViewController : UIViewController
|
||||
@end
|
||||
|
||||
@interface IGSeeAllItemConfiguration : NSObject
|
||||
@property (readonly, nonatomic) long long destination;
|
||||
@end
|
||||
|
||||
@interface IGDSMenuItem : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectThreadViewController : UIViewController
|
||||
- (void)markLastMessageAsSeen;
|
||||
@end
|
||||
|
||||
@interface IGTabBarButton : UIButton
|
||||
- (void)addHandleLongPress; // new
|
||||
@end
|
||||
|
||||
@interface IGStoryFullscreenDefaultFooterView : NSObject
|
||||
@end
|
||||
|
||||
@interface IGDirectThreadThemePickerOption : NSObject
|
||||
@end
|
||||
|
||||
@interface IGCreationActionBarButton : UIButton
|
||||
@end
|
||||
|
||||
@interface IGCreationActionBarLabeledButton : NSObject
|
||||
@property (readonly, nonatomic) IGCreationActionBarButton *button;
|
||||
@end
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
static BOOL is_iPad() {
|
||||
if ([(NSString *)[UIDevice currentDevice].model hasPrefix:@"iPad"]) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
static UIViewController * _Nullable _topMostController(UIViewController * _Nonnull cont) {
|
||||
UIViewController *topController = cont;
|
||||
while (topController.presentedViewController) {
|
||||
topController = topController.presentedViewController;
|
||||
}
|
||||
if ([topController isKindOfClass:[UINavigationController class]]) {
|
||||
UIViewController *visible = ((UINavigationController *)topController).visibleViewController;
|
||||
if (visible) {
|
||||
topController = visible;
|
||||
}
|
||||
}
|
||||
return (topController != cont ? topController : nil);
|
||||
}
|
||||
static UIViewController * _Nonnull topMostController() {
|
||||
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
UIViewController *next = nil;
|
||||
while ((next = _topMostController(topController)) != nil) {
|
||||
topController = next;
|
||||
}
|
||||
return topController;
|
||||
}
|
||||
|
||||
@class FLEXAlert, FLEXAlertAction;
|
||||
|
||||
typedef void (^FLEXAlertReveal)(void);
|
||||
typedef void (^FLEXAlertBuilder)(FLEXAlert *make);
|
||||
typedef FLEXAlert * _Nonnull (^FLEXAlertStringProperty)(NSString * _Nullable);
|
||||
typedef FLEXAlert * _Nonnull (^FLEXAlertStringArg)(NSString * _Nullable);
|
||||
typedef FLEXAlert * _Nonnull (^FLEXAlertTextField)(void(^configurationHandler)(UITextField *textField));
|
||||
typedef FLEXAlertAction * _Nonnull (^FLEXAlertAddAction)(NSString *title);
|
||||
typedef FLEXAlertAction * _Nonnull (^FLEXAlertActionStringProperty)(NSString * _Nullable);
|
||||
typedef FLEXAlertAction * _Nonnull (^FLEXAlertActionProperty)(void);
|
||||
typedef FLEXAlertAction * _Nonnull (^FLEXAlertActionBOOLProperty)(BOOL);
|
||||
typedef FLEXAlertAction * _Nonnull (^FLEXAlertActionHandler)(void(^handler)(NSArray<NSString *> *strings));
|
||||
|
||||
@interface FLEXAlert : NSObject
|
||||
|
||||
// Shows a simple alert with one button which says "Dismiss"
|
||||
+ (void)showAlert:(NSString * _Nullable)title message:(NSString * _Nullable)message from:(UIViewController *)viewController;
|
||||
|
||||
// Shows a simple alert with no buttons and only a title, for half a second
|
||||
+ (void)showQuickAlert:(NSString *)title from:(UIViewController *)viewController;
|
||||
|
||||
// Construct and display an alert
|
||||
+ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController;
|
||||
// Construct and display an action sheet-style alert
|
||||
+ (void)makeSheet:(FLEXAlertBuilder)block
|
||||
showFrom:(UIViewController *)viewController
|
||||
source:(id)viewOrBarItem;
|
||||
|
||||
// Construct an alert
|
||||
+ (UIAlertController *)makeAlert:(FLEXAlertBuilder)block;
|
||||
// Construct an action sheet-style alert
|
||||
+ (UIAlertController *)makeSheet:(FLEXAlertBuilder)block;
|
||||
|
||||
// Set the alert's title.
|
||||
///
|
||||
// Call in succession to append strings to the title.
|
||||
@property (nonatomic, readonly) FLEXAlertStringProperty title;
|
||||
// Set the alert's message.
|
||||
///
|
||||
// Call in succession to append strings to the message.
|
||||
@property (nonatomic, readonly) FLEXAlertStringProperty message;
|
||||
// Add a button with a given title with the default style and no action.
|
||||
@property (nonatomic, readonly) FLEXAlertAddAction button;
|
||||
// Add a text field with the given (optional) placeholder text.
|
||||
@property (nonatomic, readonly) FLEXAlertStringArg textField;
|
||||
// Add and configure the given text field.
|
||||
///
|
||||
// Use this if you need to more than set the placeholder, such as
|
||||
// supply a delegate, make it secure entry, or change other attributes.
|
||||
@property (nonatomic, readonly) FLEXAlertTextField configuredTextField;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXAlertAction : NSObject
|
||||
|
||||
// Set the action's title.
|
||||
///
|
||||
// Call in succession to append strings to the title.
|
||||
@property (nonatomic, readonly) FLEXAlertActionStringProperty title;
|
||||
// Make the action destructive. It appears with red text.
|
||||
@property (nonatomic, readonly) FLEXAlertActionProperty destructiveStyle;
|
||||
// Make the action cancel-style. It appears with a bolder font.
|
||||
@property (nonatomic, readonly) FLEXAlertActionProperty cancelStyle;
|
||||
// Enable or disable the action. Enabled by default.
|
||||
@property (nonatomic, readonly) FLEXAlertActionBOOLProperty enabled;
|
||||
// Give the button an action. The action takes an array of text field strings.
|
||||
@property (nonatomic, readonly) FLEXAlertActionHandler handler;
|
||||
// Access the underlying UIAlertAction, should you need to change it while
|
||||
// the encompassing alert is being displayed. For example, you may want to
|
||||
// enable or disable a button based on the input of some text fields in the alert.
|
||||
// Do not call this more than once per instance.
|
||||
@property (nonatomic, readonly) UIAlertAction *action;
|
||||
|
||||
@end
|
||||
@interface FLEXManager : NSObject
|
||||
+ (instancetype)sharedManager;
|
||||
- (void)showExplorer;
|
||||
- (void)hideExplorer;
|
||||
- (void)toggleExplorer;
|
||||
@end
|
||||
@@ -0,0 +1,10 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <QuickLook/QuickLook.h>
|
||||
|
||||
@interface QuickLookDelegate : NSObject <QLPreviewControllerDataSource, QLPreviewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSURL *> *previewItemURLs;
|
||||
|
||||
- (instancetype)initWithPreviewItemURLs:(NSArray<NSURL *> *)urls;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
#import "QuickLook.h"
|
||||
|
||||
@implementation QuickLookDelegate
|
||||
|
||||
- (instancetype)initWithPreviewItemURLs:(NSArray<NSURL *> *)urls {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_previewItemURLs = [urls copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/* * QLPreviewControllerDataSource Protocol * */
|
||||
|
||||
- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller {
|
||||
return self.previewItemURLs.count;
|
||||
}
|
||||
|
||||
- (id<QLPreviewItem>)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index {
|
||||
return self.previewItemURLs[index];
|
||||
}
|
||||
|
||||
/* QLPreviewControllerDelegate Protocol */
|
||||
|
||||
// - (void)previewControllerWillDismiss:(QLPreviewController *)controller {}
|
||||
|
||||
// - (void)previewControllerDidDismiss:(QLPreviewController *)controller {}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,105 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "SCISymbol.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, SCITableCell) {
|
||||
SCITableCellStatic,
|
||||
SCITableCellLink,
|
||||
SCITableCellSwitch,
|
||||
SCITableCellStepper,
|
||||
SCITableCellButton,
|
||||
SCITableCellMenu,
|
||||
SCITableCellNavigation,
|
||||
};
|
||||
|
||||
///
|
||||
|
||||
@interface SCISetting : NSObject
|
||||
|
||||
@property (nonatomic, readonly) SCITableCell type;
|
||||
|
||||
@property (nonatomic, strong) NSString *title;
|
||||
@property (nonatomic, strong) NSString *subtitle;
|
||||
|
||||
@property (nonatomic, strong, nullable) SCISymbol *icon;
|
||||
@property (nonatomic, strong) NSString *defaultsKey;
|
||||
|
||||
@property (nonatomic, strong) NSURL *url;
|
||||
@property (nonatomic, strong) NSURL *imageUrl;
|
||||
|
||||
@property (nonatomic) BOOL requiresRestart;
|
||||
|
||||
@property (nonatomic) double min;
|
||||
@property (nonatomic) double max;
|
||||
@property (nonatomic) double step;
|
||||
@property (nonatomic, copy) NSString *label;
|
||||
@property (nonatomic, copy) NSString *singularLabel;
|
||||
|
||||
@property (nonatomic, copy) void (^action)(void);
|
||||
|
||||
@property (nonatomic, strong) UIMenu *baseMenu;
|
||||
|
||||
@property (nonatomic, strong) NSArray *navSections;
|
||||
@property (nonatomic, strong) UIViewController *navViewController;
|
||||
|
||||
+ (instancetype)staticCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon;
|
||||
|
||||
+ (instancetype)linkCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
url:(NSString *)url;
|
||||
|
||||
+ (instancetype)linkCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
imageUrl:(NSString *)imageUrl
|
||||
url:(NSString *)url;
|
||||
|
||||
+ (instancetype)switchCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
defaultsKey:(NSString *)defaultsKey;
|
||||
|
||||
+ (instancetype)switchCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
defaultsKey:(NSString *)defaultsKey
|
||||
requiresRestart:(BOOL)requiresRestart;
|
||||
|
||||
+ (instancetype)stepperCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
defaultsKey:(NSString *)defaultsKey
|
||||
min:(double)min
|
||||
max:(double)max
|
||||
step:(double)step
|
||||
label:(NSString *)label
|
||||
singularLabel:(NSString *)singularLabel;
|
||||
|
||||
+ (instancetype)buttonCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
action:(void (^)(void))action;
|
||||
|
||||
+ (instancetype)menuCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
menu:(UIMenu *)menu;
|
||||
|
||||
+ (instancetype)navigationCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
navSections:(NSArray *)navSections;
|
||||
|
||||
+ (instancetype)navigationCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
viewController:(UIViewController *)viewController;
|
||||
|
||||
|
||||
# pragma mark - Instance methods
|
||||
|
||||
- (UIMenu *)menuForButton:(UIButton *)button;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,245 @@
|
||||
#import "SCISetting.h"
|
||||
|
||||
@interface SCISetting ()
|
||||
|
||||
@property (nonatomic, readwrite) SCITableCell type;
|
||||
|
||||
- (instancetype)initWithType:(SCITableCell)type NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
///
|
||||
|
||||
@implementation SCISetting
|
||||
|
||||
// MARK: - - initWithType
|
||||
|
||||
- (instancetype)initWithType:(SCITableCell)type {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
self.type = type;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
// MARK: - + staticCellWithTitle
|
||||
|
||||
+ (instancetype)staticCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellStatic];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
setting.icon = icon;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
// MARK: - + linkCellWithTitle
|
||||
|
||||
+ (instancetype)linkCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
url:(NSString *)url
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellLink];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
setting.icon = icon;
|
||||
setting.url = [NSURL URLWithString:url];
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
+ (instancetype)linkCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
imageUrl:(NSString *)imageUrl
|
||||
url:(NSString *)url
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellLink];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
|
||||
setting.imageUrl = [NSURL URLWithString:imageUrl];
|
||||
setting.url = [NSURL URLWithString:url];
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
// MARK: - + switchCellWithTitle
|
||||
|
||||
+ (instancetype)switchCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
defaultsKey:(NSString *)defaultsKey
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellSwitch];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
setting.defaultsKey = defaultsKey;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
+ (instancetype)switchCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
defaultsKey:(NSString *)defaultsKey
|
||||
requiresRestart:(BOOL)requiresRestart
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellSwitch];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
setting.defaultsKey = defaultsKey;
|
||||
setting.requiresRestart = requiresRestart;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
// MARK: - + stepperCellWithTitle
|
||||
|
||||
+ (instancetype)stepperCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
defaultsKey:(NSString *)defaultsKey
|
||||
min:(double)min
|
||||
max:(double)max
|
||||
step:(double)step
|
||||
label:(NSString *)label
|
||||
singularLabel:(NSString *)singularLabel
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellStepper];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
setting.defaultsKey = defaultsKey;
|
||||
|
||||
setting.min = min;
|
||||
setting.max = max;
|
||||
setting.step = step;
|
||||
setting.label = label;
|
||||
setting.singularLabel = singularLabel;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
// MARK: - + buttonCellWithTitle
|
||||
|
||||
+ (instancetype)buttonCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
action:(void (^)(void))action
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellButton];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
|
||||
setting.icon = icon;
|
||||
setting.action = action;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
# pragma mark + menuCellWithTitle
|
||||
|
||||
+ (instancetype)menuCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
menu:(UIMenu *)menu
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellMenu];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
|
||||
setting.baseMenu = menu;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
// MARK: - + navigationCellWithTitle
|
||||
|
||||
+ (instancetype)navigationCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
navSections:(NSArray *)navSections
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellNavigation];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
|
||||
setting.icon = icon;
|
||||
setting.navSections = navSections;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
+ (instancetype)navigationCellWithTitle:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
icon:(nullable SCISymbol *)icon
|
||||
viewController:(UIViewController *)viewController
|
||||
{
|
||||
SCISetting *setting = [[self alloc] initWithType:SCITableCellNavigation];
|
||||
|
||||
setting.title = title;
|
||||
setting.subtitle = subtitle;
|
||||
|
||||
setting.icon = icon;
|
||||
setting.navViewController = viewController;
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Instance methods
|
||||
|
||||
- (UIMenu *)menuForButton:(UIButton *)button {
|
||||
return [self submenuForButton:button submenu:self.baseMenu];
|
||||
}
|
||||
|
||||
- (UIMenu *)submenuForButton:(UIButton *)button submenu:(UIMenu*)submenu {
|
||||
NSMutableArray<UIMenuElement *> *children = [NSMutableArray array];
|
||||
|
||||
for (id obj in submenu.children) {
|
||||
// Handle recursive submenus
|
||||
if ([obj isKindOfClass:[UIMenu class]]) {
|
||||
[children addObject:[self submenuForButton:button submenu:(UIMenu *)obj]];
|
||||
continue;
|
||||
}
|
||||
else if (![obj isKindOfClass:[UICommand class]]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UICommand *child = obj;
|
||||
|
||||
NSString *saved = [[NSUserDefaults standardUserDefaults] stringForKey:child.propertyList[@"defaultsKey"]];
|
||||
|
||||
UICommand *command = [UICommand commandWithTitle:child.title
|
||||
image:child.image
|
||||
action:child.action
|
||||
propertyList:child.propertyList];
|
||||
|
||||
if ([child.propertyList[@"value"] isEqualToString:saved]) {
|
||||
command.state = YES;
|
||||
|
||||
[button setTitle:command.title forState:UIControlStateNormal];
|
||||
}
|
||||
else {
|
||||
command.state = NO;
|
||||
}
|
||||
|
||||
[children addObject:command];
|
||||
}
|
||||
|
||||
return [UIMenu menuWithTitle:submenu.title image:nil identifier:nil options:submenu.options children:children];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
#import <objc/runtime.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TweakSettings.h"
|
||||
#import "SCISetting.h"
|
||||
#import "SCISymbol.h"
|
||||
#import "../Utils.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCISettingsViewController : UIViewController
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title sections:(NSArray *)sections reduceMargin:(BOOL)reduceMargin;
|
||||
- (instancetype)init;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,356 @@
|
||||
#import "SCISettingsViewController.h"
|
||||
|
||||
static char rowStaticRef[] = "row";
|
||||
|
||||
@interface SCISettingsViewController () <UITableViewDataSource, UITableViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) UITableView *tableView;
|
||||
@property (nonatomic, copy) NSArray *sections;
|
||||
@property (nonatomic) BOOL reduceMargin;
|
||||
|
||||
@end
|
||||
|
||||
///
|
||||
|
||||
@implementation SCISettingsViewController
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title sections:(NSArray *)sections reduceMargin:(BOOL)reduceMargin {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
self.title = title;
|
||||
self.reduceMargin = reduceMargin;
|
||||
|
||||
// Exclude development cells from release builds
|
||||
NSMutableArray *mutableSections = [sections mutableCopy];
|
||||
|
||||
[mutableSections enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSDictionary *section, NSUInteger index, BOOL *stop) {
|
||||
|
||||
if ([section[@"header"] hasPrefix:@"_"] && [section[@"footer"] hasPrefix:@"_"]) {
|
||||
if (![[SCIUtils IGVersionString] isEqualToString:@"0.0.0"]) {
|
||||
[mutableSections removeObjectAtIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
else if ([section[@"header"] isEqualToString:@"Experimental"]) {
|
||||
if (![[SCIUtils IGVersionString] hasSuffix:@"-dev"]) {
|
||||
[mutableSections removeObjectAtIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
self.sections = [mutableSections copy];
|
||||
}
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
return [self initWithTitle:[SCITweakSettings title] sections:[SCITweakSettings sections] reduceMargin:YES];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationController.navigationBar.prefersLargeTitles = NO;
|
||||
self.view.backgroundColor = UIColor.systemBackgroundColor;
|
||||
|
||||
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleInsetGrouped];
|
||||
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.contentInset = UIEdgeInsetsMake(self.reduceMargin ? -30 : -10, 0, 0, 0);
|
||||
self.tableView.delegate = self;
|
||||
|
||||
[self.view addSubview:self.tableView];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
if (![[[NSUserDefaults standardUserDefaults] objectForKey:@"SCInstaFirstRun"] isEqualToString:SCIVersionString]) {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SCInsta Settings Info"
|
||||
message:@"In the future: Hold down on the three lines at the top right of your profile page, to re-open SCInsta settings."
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"I understand!"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:nil]];
|
||||
|
||||
UIViewController *presenter = self.presentingViewController;
|
||||
[presenter presentViewController:alert animated:YES completion:nil];
|
||||
|
||||
// Done with first-time setup for this version
|
||||
[[NSUserDefaults standardUserDefaults] setValue:SCIVersionString forKey:@"SCInstaFirstRun"];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
SCISetting *row = self.sections[indexPath.section][@"rows"][indexPath.row];
|
||||
if (!row) return nil;
|
||||
|
||||
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
|
||||
UIListContentConfiguration *cellContentConfig = cell.defaultContentConfiguration;
|
||||
|
||||
cellContentConfig.text = row.title;
|
||||
|
||||
// Subtitle
|
||||
if (row.subtitle.length) {
|
||||
cellContentConfig.secondaryText = row.subtitle;
|
||||
cellContentConfig.textToSecondaryTextVerticalPadding = 4.5;
|
||||
}
|
||||
|
||||
// Icon
|
||||
if (row.icon != nil) {
|
||||
cellContentConfig.image = [row.icon image];
|
||||
cellContentConfig.imageProperties.tintColor = row.icon.color;
|
||||
}
|
||||
|
||||
// Image url
|
||||
if (row.imageUrl != nil) {
|
||||
[self loadImageFromURL:row.imageUrl atIndexPath:indexPath forTableView:tableView];
|
||||
|
||||
cellContentConfig.imageToTextPadding = 14;
|
||||
}
|
||||
|
||||
switch (row.type) {
|
||||
case SCITableCellStatic: {
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
break;
|
||||
}
|
||||
|
||||
case SCITableCellLink: {
|
||||
cellContentConfig.textProperties.color = [UIColor systemBlueColor];
|
||||
cellContentConfig.textProperties.font = [UIFont systemFontOfSize:[UIFont preferredFontForTextStyle:UIFontTextStyleBody].pointSize
|
||||
weight:UIFontWeightMedium];
|
||||
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"safari"]];
|
||||
imageView.tintColor = [UIColor systemGray3Color];
|
||||
cell.accessoryView = imageView;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SCITableCellSwitch: {
|
||||
UISwitch *toggle = [UISwitch new];
|
||||
toggle.on = [[NSUserDefaults standardUserDefaults] boolForKey:row.defaultsKey];
|
||||
toggle.onTintColor = [SCIUtils SCIColor_Primary];
|
||||
|
||||
objc_setAssociatedObject(toggle, rowStaticRef, row, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
|
||||
[toggle addTarget:self action:@selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
cell.accessoryView = toggle;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
break;
|
||||
}
|
||||
|
||||
case SCITableCellStepper: {
|
||||
UIStepper *stepper = [UIStepper new];
|
||||
stepper.minimumValue = row.min;
|
||||
stepper.maximumValue = row.max;
|
||||
stepper.stepValue = row.step;
|
||||
stepper.value = [[NSUserDefaults standardUserDefaults] doubleForKey:row.defaultsKey];
|
||||
|
||||
objc_setAssociatedObject(stepper, rowStaticRef, row, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
|
||||
[stepper addTarget:self
|
||||
action:@selector(stepperChanged:)
|
||||
forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
// Template subtitle
|
||||
if (row.subtitle.length) {
|
||||
cellContentConfig.secondaryText = [self formatString:row.subtitle withValue:stepper.value label:row.label singularLabel:row.singularLabel];
|
||||
}
|
||||
|
||||
cell.accessoryView = stepper;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
break;
|
||||
}
|
||||
|
||||
case SCITableCellButton: {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
break;
|
||||
}
|
||||
|
||||
case SCITableCellMenu: {
|
||||
UIButton *menuButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[menuButton setTitle:@"•••" forState:UIControlStateNormal];
|
||||
menuButton.menu = [row menuForButton:menuButton];
|
||||
menuButton.showsMenuAsPrimaryAction = YES;
|
||||
menuButton.titleLabel.font = [UIFont systemFontOfSize:[UIFont preferredFontForTextStyle:UIFontTextStyleBody].pointSize
|
||||
weight:UIFontWeightMedium];
|
||||
|
||||
UIButtonConfiguration *config = menuButton.configuration ?: [UIButtonConfiguration plainButtonConfiguration];
|
||||
menuButton.configuration.contentInsets = NSDirectionalEdgeInsetsMake(8, 8, 8, 8);
|
||||
menuButton.configuration = config;
|
||||
|
||||
[menuButton sizeToFit];
|
||||
|
||||
cell.accessoryView = menuButton;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
break;
|
||||
}
|
||||
|
||||
case SCITableCellNavigation: {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cell.contentConfiguration = cellContentConfig;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return [self.sections[section][@"rows"] count];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.sections[section][@"header"];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
|
||||
return self.sections[section][@"footer"];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.sections.count;
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
SCISetting *row = self.sections[indexPath.section][@"rows"][indexPath.row];
|
||||
if (!row) return;
|
||||
|
||||
if (row.type == SCITableCellLink) {
|
||||
[[UIApplication sharedApplication] openURL:row.url options:@{} completionHandler:nil];
|
||||
}
|
||||
else if (row.type == SCITableCellButton) {
|
||||
if (row.action != nil) {
|
||||
row.action();
|
||||
}
|
||||
}
|
||||
else if (row.type == SCITableCellNavigation) {
|
||||
if (row.navSections.count > 0) {
|
||||
UIViewController *vc = [[SCISettingsViewController alloc] initWithTitle:row.title sections:row.navSections reduceMargin:NO];
|
||||
vc.title = row.title;
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
else if (row.navViewController) {
|
||||
[self.navigationController pushViewController:row.navViewController animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
- (void)switchChanged:(UISwitch *)sender {
|
||||
SCISetting *row = objc_getAssociatedObject(sender, rowStaticRef);
|
||||
[[NSUserDefaults standardUserDefaults] setBool:sender.isOn forKey:row.defaultsKey];
|
||||
|
||||
NSLog(@"Switch changed: %@", sender.isOn ? @"ON" : @"OFF");
|
||||
|
||||
if (row.requiresRestart) {
|
||||
[SCIUtils showRestartConfirmation];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stepperChanged:(UIStepper *)sender {
|
||||
SCISetting *row = objc_getAssociatedObject(sender, rowStaticRef);
|
||||
[[NSUserDefaults standardUserDefaults] setDouble:sender.value forKey:row.defaultsKey];
|
||||
|
||||
NSLog(@"Stepper changed: %f", sender.value);
|
||||
|
||||
[self reloadCellForView:sender];
|
||||
}
|
||||
|
||||
- (void)menuChanged:(UICommand *)command {
|
||||
NSDictionary *properties = command.propertyList;
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setValue:properties[@"value"] forKey:properties[@"defaultsKey"]];
|
||||
|
||||
NSLog(@"Menu changed: %@", command.propertyList[@"value"]);
|
||||
|
||||
[self reloadCellForView:command.sender animated:YES];
|
||||
|
||||
if (properties[@"requiresRestart"]) {
|
||||
[SCIUtils showRestartConfirmation];
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
- (NSString *)formatString:(NSString *)template withValue:(double)value label:(NSString *)label singularLabel:(NSString *)singularLabel {
|
||||
// Singular or plural labels
|
||||
NSString *applicableLabel = fabs(value - 1.0) < 0.00001 ? singularLabel : label;
|
||||
|
||||
// Force value to 0 to prevent it being -0
|
||||
if (fabs(value) < 0.00001) {
|
||||
value = 0.0;
|
||||
}
|
||||
|
||||
// Get correct decimal value based on step value
|
||||
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
||||
formatter.numberStyle = NSNumberFormatterDecimalStyle;
|
||||
formatter.minimumFractionDigits = 0;
|
||||
formatter.maximumFractionDigits = [SCIUtils decimalPlacesInDouble:value];
|
||||
|
||||
NSString *stringValue = [formatter stringFromNumber:@(value)];
|
||||
|
||||
return [NSString stringWithFormat:template, stringValue, applicableLabel];
|
||||
}
|
||||
|
||||
- (void)reloadCellForView:(UIView *)view animated:(BOOL)animated {
|
||||
UITableViewCell *cell = (UITableViewCell *)view.superview;
|
||||
while (cell && ![cell isKindOfClass:[UITableViewCell class]]) {
|
||||
cell = (UITableViewCell *)cell.superview;
|
||||
}
|
||||
if (!cell) return;
|
||||
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
|
||||
if (!indexPath) return;
|
||||
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
|
||||
withRowAnimation:animated ? UITableViewRowAnimationAutomatic : UITableViewRowAnimationNone];
|
||||
}
|
||||
- (void)reloadCellForView:(UIView *)view {
|
||||
[self reloadCellForView:view animated:NO];
|
||||
}
|
||||
|
||||
- (void)loadImageFromURL:(NSURL *)url atIndexPath:(NSIndexPath *)indexPath forTableView:(UITableView *)tableView
|
||||
{
|
||||
if (!url) return;
|
||||
|
||||
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
|
||||
{
|
||||
if (!data || error) return;
|
||||
|
||||
UIImage *image = [UIImage imageWithData:data];
|
||||
if (!image) return;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
if (!cell) return;
|
||||
|
||||
UIListContentConfiguration *config = (UIListContentConfiguration *)cell.contentConfiguration;
|
||||
config.image = image;
|
||||
config.imageProperties.maximumSize = CGSizeMake(45, 45);
|
||||
cell.contentConfiguration = config;
|
||||
});
|
||||
}];
|
||||
|
||||
[task resume];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,21 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCISymbol : NSObject
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *name;
|
||||
@property (nonatomic, copy, readonly) UIColor *color;
|
||||
@property (nonatomic, readonly) CGFloat size;
|
||||
@property (nonatomic, readonly) UIImageSymbolWeight weight;
|
||||
|
||||
- (UIImage *)image;
|
||||
|
||||
+ (instancetype)symbolWithName:(NSString *)name;
|
||||
+ (instancetype)symbolWithName:(NSString *)name color:(UIColor *)color;
|
||||
+ (instancetype)symbolWithName:(NSString *)name color:(UIColor *)color size:(CGFloat)size;
|
||||
+ (instancetype)symbolWithName:(NSString *)name color:(UIColor *)color size:(CGFloat)size weight:(UIImageSymbolWeight)weight;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,87 @@
|
||||
#import "SCISymbol.h"
|
||||
|
||||
@interface SCISymbol ()
|
||||
|
||||
@property (nonatomic, copy, readwrite) NSString *name;
|
||||
@property (nonatomic, copy, readwrite) UIColor *color;
|
||||
@property (nonatomic, readwrite) CGFloat size;
|
||||
@property (nonatomic, readwrite) UIImageSymbolWeight weight;
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
@end
|
||||
|
||||
///
|
||||
|
||||
@implementation SCISymbol
|
||||
|
||||
// MARK: - Instance methods
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
self.name = @"";
|
||||
self.color = [UIColor labelColor];
|
||||
self.weight = UIImageSymbolWeightRegular;
|
||||
self.size = 15.0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UIImage *)image {
|
||||
UIImage *symbol = [UIImage systemImageNamed:self.name];
|
||||
|
||||
if (self.size || (self.size && self.weight)) {
|
||||
UIImageSymbolConfiguration *symbolConfig = [UIImageSymbolConfiguration configurationWithTextStyle:UIFontTextStyleTitle1];
|
||||
symbolConfig = [symbolConfig configurationByApplyingConfiguration:
|
||||
[UIImageSymbolConfiguration configurationWithPointSize:self.size weight:self.weight]];
|
||||
|
||||
return [symbol imageWithConfiguration:symbolConfig];
|
||||
}
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
// MARK: - Class methods
|
||||
|
||||
+ (instancetype)symbolWithName:(NSString *)name {
|
||||
SCISymbol *symbol = [[self alloc] init];
|
||||
|
||||
symbol.name = name;
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
+ (instancetype)symbolWithName:(NSString *)name color:(UIColor *)color {
|
||||
SCISymbol *symbol = [[self alloc] init];
|
||||
|
||||
symbol.name = name;
|
||||
symbol.color = color;
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
+ (instancetype)symbolWithName:(NSString *)name color:(UIColor *)color size:(CGFloat)size {
|
||||
SCISymbol *symbol = [[self alloc] init];
|
||||
|
||||
symbol.name = name;
|
||||
symbol.color = color;
|
||||
symbol.size = size;
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
+ (instancetype)symbolWithName:(NSString *)name color:(UIColor *)color size:(CGFloat)size weight:(UIImageSymbolWeight)weight {
|
||||
SCISymbol *symbol = [[self alloc] init];
|
||||
|
||||
symbol.name = name;
|
||||
symbol.color = color;
|
||||
symbol.size = size;
|
||||
symbol.weight = weight;
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "SCISetting.h"
|
||||
#import "SCISymbol.h"
|
||||
#import "../Utils.h"
|
||||
#import "../Tweak.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SCITweakSettings : NSObject
|
||||
|
||||
+ (NSArray *)sections;
|
||||
+ (NSString *)title;
|
||||
+ (NSDictionary *)menus;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,520 @@
|
||||
#import "TweakSettings.h"
|
||||
|
||||
@implementation SCITweakSettings
|
||||
|
||||
// MARK: - Sections
|
||||
|
||||
///
|
||||
/// This returns an array of sections, with each section consisting of a dictionary
|
||||
///
|
||||
/// `"title"`: The section title (leave blank for no title)
|
||||
///
|
||||
/// `"rows"`: An array of **SCISetting** classes, potentially containing a "navigationCellWithTitle" initializer to allow for nested setting pages.
|
||||
///
|
||||
/// `"footer`: The section footer (leave blank for no footer)
|
||||
|
||||
+ (NSArray *)sections {
|
||||
return @[
|
||||
@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting linkCellWithTitle:@"Donate" subtitle:@"Consider donating to support this tweak's development!" icon:[SCISymbol symbolWithName:@"heart.circle.fill" color:[UIColor systemPinkColor] size:20.0] url:@"https://ko-fi.com/SoCuul"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting navigationCellWithTitle:@"General"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"gear"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Hide ads" subtitle:@"Removes all ads from the Instagram app" defaultsKey:@"hide_ads"],
|
||||
[SCISetting switchCellWithTitle:@"Hide Meta AI" subtitle:@"Hides the meta ai buttons/functionality within the app" defaultsKey:@"hide_meta_ai"],
|
||||
[SCISetting switchCellWithTitle:@"Copy description" subtitle:@"Copy description text fields by long-pressing on them" defaultsKey:@"copy_description"],
|
||||
[SCISetting switchCellWithTitle:@"Do not save recent searches" subtitle:@"Search bars will no longer save your recent searches" defaultsKey:@"no_recent_searches"],
|
||||
[SCISetting switchCellWithTitle:@"Use detailed color picker" subtitle:@"Long press on the eyedropper tool in stories to customize the text color more precisely" defaultsKey:@"detailed_color_picker"],
|
||||
[SCISetting switchCellWithTitle:@"Enable liquid glass buttons" subtitle:@"Enables experimental liquid glass buttons within the app" defaultsKey:@"liquid_glass_buttons" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Enable liquid glass surfaces" subtitle:@"Enables liquid glass for other elements, such as menus" defaultsKey:@"liquid_glass_surfaces" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Enable teen app icons" subtitle:@"When enabled, hold down on the Instagram logo to change the app icon" defaultsKey:@"teen_app_icons" requiresRestart:YES]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Notes",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Hide notes tray" subtitle:@"Hides the notes tray in the dm inbox" defaultsKey:@"hide_notes_tray"],
|
||||
[SCISetting switchCellWithTitle:@"Hide friends map" subtitle:@"Hides the friends map icon in the notes tray" defaultsKey:@"hide_friends_map"],
|
||||
[SCISetting switchCellWithTitle:@"Enable note theming" subtitle:@"Enables the ability to use the notes theme picker" defaultsKey:@"enable_notes_customization"],
|
||||
[SCISetting switchCellWithTitle:@"Custom note themes" subtitle:@"Provides an option to set custom emojis and background/text colors" defaultsKey:@"custom_note_themes"],
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Focus/distractions",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"No suggested users" subtitle:@"Hides all suggested users for you to follow, outside your feed" defaultsKey:@"no_suggested_users"],
|
||||
[SCISetting switchCellWithTitle:@"No suggested chats" subtitle:@"Hides the suggested broadcast channels in direct messages" defaultsKey:@"no_suggested_chats"],
|
||||
[SCISetting switchCellWithTitle:@"Hide explore posts grid" subtitle:@"Hides the grid of suggested posts on the explore/search tab" defaultsKey:@"hide_explore_grid"],
|
||||
[SCISetting switchCellWithTitle:@"Hide trending searches" subtitle:@"Hides the trending searches under the explore search bar" defaultsKey:@"hide_trending_searches"],
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Feed"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"rectangle.stack"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Hide stories tray" subtitle:@"Hides the story tray at the top and within your feed" defaultsKey:@"hide_stories_tray"],
|
||||
[SCISetting switchCellWithTitle:@"Hide entire feed" subtitle:@"Removes all content from your home feed, including posts" defaultsKey:@"hide_entire_feed"],
|
||||
[SCISetting switchCellWithTitle:@"No suggested posts" subtitle:@"Removes suggested posts from your feed" defaultsKey:@"no_suggested_post"],
|
||||
[SCISetting switchCellWithTitle:@"No suggested for you" subtitle:@"Hides suggested accounts for you to follow" defaultsKey:@"no_suggested_account"],
|
||||
[SCISetting switchCellWithTitle:@"No suggested reels" subtitle:@"Hides suggested reels to watch" defaultsKey:@"no_suggested_reels"],
|
||||
[SCISetting switchCellWithTitle:@"No suggested threads posts" subtitle:@"Hides suggested threads posts" defaultsKey:@"no_suggested_threads"],
|
||||
[SCISetting switchCellWithTitle:@"Disable video autoplay" subtitle:@"Prevents videos on your feed from playing automatically" defaultsKey:@"disable_feed_autoplay"]
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Reels"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"film.stack"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting menuCellWithTitle:@"Tap Controls" subtitle:@"Change what happens when you tap on a reel" menu:[self menus][@"reels_tap_control"]],
|
||||
[SCISetting switchCellWithTitle:@"Always show progress scrubber" subtitle:@"Forces the progress bar to appear on every reel" defaultsKey:@"reels_show_scrubber"],
|
||||
[SCISetting switchCellWithTitle:@"Disable auto-unmuting reels" subtitle:@"Prevents reels from unmuting when the volume/silent button is pressed" defaultsKey:@"disable_auto_unmuting_reels" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Confirm reel refresh" subtitle:@"Shows an alert when you trigger a reels refresh" defaultsKey:@"refresh_reel_confirm"],
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Hiding",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Hide reels header" subtitle:@"Hides the top navigation bar when watching reels" defaultsKey:@"hide_reels_header"],
|
||||
[SCISetting switchCellWithTitle:@"Hide reels blend button" subtitle:@"Hides the button in DMs to open a reels blend" defaultsKey:@"hide_reels_blend"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Limits",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Disable scrolling reels" subtitle:@"Prevents reels from being scrolled to the next video" defaultsKey:@"disable_scrolling_reels" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Prevent doom scrolling" subtitle:@"Limits the amount of reels available to scroll at any given time, and prevents refreshing" defaultsKey:@"prevent_doom_scrolling"],
|
||||
[SCISetting stepperCellWithTitle:@"Doom scrolling limit" subtitle:@"Only loads %@ %@" defaultsKey:@"doom_scrolling_reel_count" min:1 max:100 step:1 label:@"reels" singularLabel:@"reel"]
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Saving"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"tray.and.arrow.down"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[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"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Download method",
|
||||
@"rows": @[
|
||||
[SCISetting menuCellWithTitle:@"Download method" subtitle:@"How to trigger downloads" menu:[self menus][@"dw_method"]],
|
||||
[SCISetting menuCellWithTitle:@"Save action" subtitle:@"What happens after downloading" menu:[self menus][@"dw_save_action"]],
|
||||
[SCISetting switchCellWithTitle:@"Confirm before download" subtitle:@"Show a confirmation dialog before starting a download" defaultsKey:@"dw_confirm"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Customize gestures",
|
||||
@"footer": @"Only applies when download method is set to \"Long-press gesture\"",
|
||||
@"rows": @[
|
||||
[SCISetting stepperCellWithTitle:@"Finger count for long-press" subtitle:@"Downloads with %@ %@" defaultsKey:@"dw_finger_count" min:1 max:5 step:1 label:@"fingers" singularLabel:@"finger"],
|
||||
[SCISetting stepperCellWithTitle:@"Long-press hold time" subtitle:@"Press finger(s) for %@ %@" defaultsKey:@"dw_finger_duration" min:0 max:10 step:0.25 label:@"sec" singularLabel:@"sec"]
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Stories and messages"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"rectangle.portrait.on.rectangle.portrait.angled"]
|
||||
navSections:@[@{
|
||||
@"header": @"Messages",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Keep deleted messages" subtitle:@"Saves deleted messages in chat conversations" defaultsKey:@"keep_deleted_message"],
|
||||
[SCISetting switchCellWithTitle:@"Manually mark messages as seen" subtitle:@"Adds a button to DM threads, which will mark messages as seen" defaultsKey:@"remove_lastseen"],
|
||||
[SCISetting switchCellWithTitle:@"Disable typing status" subtitle:@"Prevents the typing indicator from being shown to others when you're typing in DMs" defaultsKey:@"disable_typing_status"],
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Visual messages & stories",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Unlimited replay of visual messages" subtitle:@"Replays direct visual messages normal/once stories unlimited times (toggle with image check icon)" defaultsKey:@"unlimited_replay"],
|
||||
[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:@"Disable instants creation" subtitle:@"Hides the functionality to create/send instants" defaultsKey:@"disable_instants_creation" requiresRestart:YES]
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Navigation"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"hand.draw.fill"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting menuCellWithTitle:@"Icon order" subtitle:@"The order of the icons on the bottom navigation bar" menu:[self menus][@"nav_icon_ordering"]],
|
||||
[SCISetting menuCellWithTitle:@"Swipe between tabs" subtitle:@"Lets you swipe to switch between navigation bar tabs" menu:[self menus][@"swipe_nav_tabs"]],
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Hiding tabs",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Hide feed tab" subtitle:@"Hides the feed/home tab on the bottom navigation bar" defaultsKey:@"hide_feed_tab" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Hide explore tab" subtitle:@"Hides the explore/search tab on the bottom navigation bar" defaultsKey:@"hide_explore_tab" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Hide reels tab" subtitle:@"Hides the reels tab on the bottom navigation bar" defaultsKey:@"hide_reels_tab" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Hide create tab" subtitle:@"Hides the create tab on the bottom navigation bar" defaultsKey:@"hide_create_tab" requiresRestart:YES]
|
||||
]
|
||||
}]
|
||||
],
|
||||
[SCISetting navigationCellWithTitle:@"Confirm actions"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"checkmark"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Confirm like: Posts/Stories" subtitle:@"Shows an alert when you click the like button on posts or stories to confirm the like" defaultsKey:@"like_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm like: Reels" subtitle:@"Shows an alert when you click the like button on reels to confirm the like" defaultsKey:@"like_confirm_reels"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Confirm follow" subtitle:@"Shows an alert when you click the follow button to confirm the follow" defaultsKey:@"follow_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm repost" subtitle:@"Shows an alert when you click the repost button to confirm before resposting" defaultsKey:@"repost_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm call" subtitle:@"Shows an alert when you click the audio/video call button to confirm before calling" defaultsKey:@"call_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm voice messages" subtitle:@"Shows an alert to confirm before sending a voice message" defaultsKey:@"voice_message_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm follow requests" subtitle:@"Shows an alert when you accept/decline a follow request" defaultsKey:@"follow_request_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm shh mode" subtitle:@"Shows an alert to confirm before toggling disappearing messages" defaultsKey:@"shh_mode_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm posting comment" subtitle:@"Shows an alert when you click the post comment button to confirm" defaultsKey:@"post_comment_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm changing theme" subtitle:@"Shows an alert when you change a chat theme to confirm" defaultsKey:@"change_direct_theme_confirm"],
|
||||
[SCISetting switchCellWithTitle:@"Confirm sticker interaction" subtitle:@"Shows an alert when you click a sticker on someone's story to confirm the action" defaultsKey:@"sticker_interact_confirm"]
|
||||
]
|
||||
}]
|
||||
]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"",
|
||||
@"rows": @[
|
||||
// [SCISetting navigationCellWithTitle:@"Experimental"
|
||||
// subtitle:@""
|
||||
// icon:[SCISymbol symbolWithName:@"testtube.2"]
|
||||
// navSections:@[@{
|
||||
// @"header": @"Warning",
|
||||
// @"footer": @"These features are unstable and cause the Instagram app to crash unexpectedly.\n\nUse at your own risk!"
|
||||
// },
|
||||
// @{
|
||||
// @"header": @"",
|
||||
// @"rows": @[
|
||||
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// ],
|
||||
[SCISetting navigationCellWithTitle:@"Debug"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"ladybug"]
|
||||
navSections:@[@{
|
||||
@"header": @"FLEX",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Enable FLEX gesture" subtitle:@"Allows you to hold 5 fingers on the screen to open the FLEX explorer" defaultsKey:@"flex_instagram"],
|
||||
[SCISetting switchCellWithTitle:@"Open FLEX on app launch" subtitle:@"Automatically opens the FLEX explorer when the app launches" defaultsKey:@"flex_app_launch"],
|
||||
[SCISetting switchCellWithTitle:@"Open FLEX on app focus" subtitle:@"Automatically opens the FLEX explorer when the app is focused" defaultsKey:@"flex_app_start"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"SCInsta",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Enable tweak settings quick-access" subtitle:@"Allows you to hold on the home tab to open the SCInsta settings" defaultsKey:@"settings_shortcut" requiresRestart:YES],
|
||||
[SCISetting switchCellWithTitle:@"Show tweak settings on app launch" subtitle:@"Automatically opens the SCInsta settings when the app launches" defaultsKey:@"tweak_settings_app_launch"],
|
||||
[SCISetting buttonCellWithTitle:@"Reset onboarding completion state"
|
||||
subtitle:@""
|
||||
icon:nil
|
||||
action:^(void) { [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"SCInstaFirstRun"]; [SCIUtils showRestartConfirmation];}
|
||||
],
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Instagram",
|
||||
@"rows": @[
|
||||
[SCISetting switchCellWithTitle:@"Disable safe mode" subtitle:@"Makes Instagram not reset settings after subsequent crashes (at your own risk)" defaultsKey:@"disable_safe_mode"]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"_ Example",
|
||||
@"rows": @[
|
||||
[SCISetting staticCellWithTitle:@"Static Cell" subtitle:@"" icon:[SCISymbol symbolWithName:@"tablecells"]],
|
||||
[SCISetting switchCellWithTitle:@"Switch Cell" subtitle:@"Tap the switch" defaultsKey:@"test_switch_cell"],
|
||||
[SCISetting switchCellWithTitle:@"Switch Cell (Restart)" subtitle:@"Tap the switch" defaultsKey:@"test_switch_cell_restart" requiresRestart:YES],
|
||||
[SCISetting stepperCellWithTitle:@"Stepper cell" subtitle:@"I have %@%@" defaultsKey:@"test_stepper_cell" min:-10 max:1000 step:5.5 label:@"$" singularLabel:@"$"],
|
||||
[SCISetting linkCellWithTitle:@"Link Cell" subtitle:@"Using icon" icon:[SCISymbol symbolWithName:@"link" color:[UIColor systemTealColor] size:20.0] url:@"https://google.com"],
|
||||
[SCISetting linkCellWithTitle:@"Link Cell" subtitle:@"Using image" imageUrl:@"https://i.imgur.com/c9CbytZ.png" url:@"https://google.com"],
|
||||
[SCISetting buttonCellWithTitle:@"Button Cell"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"oval.inset.filled"]
|
||||
action:^(void) { [SCIUtils showConfirmation:^(void){}]; }
|
||||
],
|
||||
[SCISetting menuCellWithTitle:@"Menu Cell" subtitle:@"Change the value on the right" menu:[self menus][@"test"]],
|
||||
[SCISetting navigationCellWithTitle:@"Navigation Cell"
|
||||
subtitle:@""
|
||||
icon:[SCISymbol symbolWithName:@"rectangle.stack"]
|
||||
navSections:@[@{
|
||||
@"header": @"",
|
||||
@"rows": @[]
|
||||
}]
|
||||
]
|
||||
],
|
||||
@"footer": @"_ Example"
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
@{
|
||||
@"header": @"Credits",
|
||||
@"rows": @[
|
||||
[SCISetting linkCellWithTitle:@"Developer" subtitle:@"SoCuul" imageUrl:@"https://i.imgur.com/c9CbytZ.png" url:@"https://socuul.dev"],
|
||||
[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]]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Title
|
||||
|
||||
///
|
||||
/// This is the title displayed on the initial settings page view controller
|
||||
///
|
||||
|
||||
+ (NSString *)title {
|
||||
return @"SCInsta Settings";
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Menus
|
||||
|
||||
///
|
||||
/// This returns a dictionary where each key corresponds to a certain menu that can be displayed.
|
||||
/// Each "propertyList" item is an NSDictionary containing the following items:
|
||||
///
|
||||
/// `"defaultsKey"`: The key to save the selected value under in NSUserDefaults
|
||||
///
|
||||
/// `"value"`: A unique string corresponding to the menu item which is selected
|
||||
///
|
||||
/// `"requiresRestart"`: (optional) Causes a popup to appear detailing you have to restart to use these features
|
||||
///
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
|
||||
+ (NSDictionary *)menus {
|
||||
return @{
|
||||
@"dw_save_action": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:@"Share sheet"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"dw_save_action",
|
||||
@"value": @"share"
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"Save to Photos"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"dw_save_action",
|
||||
@"value": @"photos"
|
||||
}
|
||||
]
|
||||
]],
|
||||
|
||||
@"dw_method": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:@"Download button"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"dw_method",
|
||||
@"value": @"button",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"Long-press gesture"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"dw_method",
|
||||
@"value": @"gesture",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
]
|
||||
]],
|
||||
|
||||
@"reels_tap_control": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:@"Default"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"reels_tap_control",
|
||||
@"value": @"default",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UIMenu menuWithTitle:@""
|
||||
image:nil
|
||||
identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[
|
||||
[UICommand commandWithTitle:@"Pause/Play"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"reels_tap_control",
|
||||
@"value": @"pause",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"Mute/Unmute"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"reels_tap_control",
|
||||
@"value": @"mute",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
]],
|
||||
|
||||
@"nav_icon_ordering": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:@"Default"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"nav_icon_ordering",
|
||||
@"value": @"default",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UIMenu menuWithTitle:@""
|
||||
image:nil
|
||||
identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[
|
||||
[UICommand commandWithTitle:@"Classic"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"nav_icon_ordering",
|
||||
@"value": @"classic",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"Standard"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"nav_icon_ordering",
|
||||
@"value": @"standard",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"Alternate"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"nav_icon_ordering",
|
||||
@"value": @"alternate",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
]],
|
||||
@"swipe_nav_tabs": [UIMenu menuWithChildren:@[
|
||||
[UICommand commandWithTitle:@"Default"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"swipe_nav_tabs",
|
||||
@"value": @"default",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UIMenu menuWithTitle:@""
|
||||
image:nil
|
||||
identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[
|
||||
[UICommand commandWithTitle:@"Enabled"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"swipe_nav_tabs",
|
||||
@"value": @"enabled",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"Disabled"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"swipe_nav_tabs",
|
||||
@"value": @"disabled",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
]],
|
||||
|
||||
@"test": [UIMenu menuWithChildren:@[
|
||||
[UIMenu menuWithTitle:@""
|
||||
image:nil
|
||||
identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[
|
||||
[UICommand commandWithTitle:@"ABC"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"test_menu_cell",
|
||||
@"value": @"abc"
|
||||
}
|
||||
],
|
||||
[UICommand commandWithTitle:@"123"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"test_menu_cell",
|
||||
@"value": @"123"
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
[UICommand commandWithTitle:@"Requires restart"
|
||||
image:nil
|
||||
action:@selector(menuChanged:)
|
||||
propertyList:@{
|
||||
@"defaultsKey": @"test_menu_cell",
|
||||
@"value": @"requires_restart",
|
||||
@"requiresRestart": @YES
|
||||
}
|
||||
],
|
||||
]]
|
||||
};
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// * Tweak version *
|
||||
extern NSString *SCIVersionString;
|
||||
|
||||
// Variables that work across features
|
||||
extern BOOL dmVisualMsgsViewedButtonEnabled; // Whether story dm unlimited views button is enabled
|
||||
+718
@@ -0,0 +1,718 @@
|
||||
#import <substrate.h>
|
||||
#import "InstagramHeaders.h"
|
||||
#import "Tweak.h"
|
||||
#import "Utils.h"
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// Screenshot handlers
|
||||
|
||||
#define VOID_HANDLESCREENSHOT(orig) [SCIUtils getBoolPref:@"remove_screenshot_alert"] ? nil : orig;
|
||||
#define NONVOID_HANDLESCREENSHOT(orig) return VOID_HANDLESCREENSHOT(orig)
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// * Tweak version *
|
||||
NSString *SCIVersionString = @"v1.1.2";
|
||||
|
||||
// Variables that work across features
|
||||
BOOL dmVisualMsgsViewedButtonEnabled = false;
|
||||
|
||||
// Tweak first-time setup
|
||||
%hook IGInstagramAppDelegate
|
||||
- (_Bool)application:(UIApplication *)application willFinishLaunchingWithOptions:(id)arg2 {
|
||||
// Default SCInsta config
|
||||
NSDictionary *sciDefaults = @{
|
||||
@"hide_ads": @(YES),
|
||||
@"copy_description": @(YES),
|
||||
@"detailed_color_picker": @(YES),
|
||||
@"remove_screenshot_alert": @(YES),
|
||||
@"call_confirm": @(YES),
|
||||
@"keep_deleted_message": @(YES),
|
||||
@"dw_feed_posts": @(YES),
|
||||
@"dw_reels": @(YES),
|
||||
@"dw_story": @(YES),
|
||||
@"save_profile": @(YES),
|
||||
@"dw_method": @"button",
|
||||
@"dw_confirm": @(YES),
|
||||
@"dw_save_action": @"share",
|
||||
@"dw_finger_count": @(3),
|
||||
@"dw_finger_duration": @(0.5),
|
||||
@"reels_tap_control": @"default",
|
||||
@"nav_icon_ordering": @"default",
|
||||
@"swipe_nav_tabs": @"default",
|
||||
@"enable_notes_customization": @(YES),
|
||||
@"custom_note_themes": @(YES),
|
||||
@"disable_auto_unmuting_reels": @(YES),
|
||||
@"doom_scrolling_reel_count": @(1)
|
||||
};
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:sciDefaults];
|
||||
|
||||
// Override instagram defaults
|
||||
if ([SCIUtils getBoolPref:@"liquid_glass_buttons"]) {
|
||||
[[NSUserDefaults standardUserDefaults] setValue:@(YES) forKey:@"instagram.override.project.lucent.navigation"];
|
||||
}
|
||||
else {
|
||||
[[NSUserDefaults standardUserDefaults] setValue:@(NO) forKey:@"instagram.override.project.lucent.navigation"];
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
- (_Bool)application:(UIApplication *)application didFinishLaunchingWithOptions:(id)arg2 {
|
||||
%orig;
|
||||
|
||||
// Open settings for first-time users
|
||||
double openDelay = [SCIUtils getBoolPref:@"tweak_settings_app_launch"] ? 0.0 : 5.0;
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(openDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (
|
||||
![[[NSUserDefaults standardUserDefaults] objectForKey:@"SCInstaFirstRun"] isEqualToString:SCIVersionString]
|
||||
|| [SCIUtils getBoolPref:@"tweak_settings_app_launch"]
|
||||
) {
|
||||
NSLog(@"[SCInsta] First run, initializing");
|
||||
|
||||
// Display settings modal on screen
|
||||
NSLog(@"[SCInsta] Displaying SCInsta first-time settings modal");
|
||||
[SCIUtils showSettingsVC:[self window]];
|
||||
}
|
||||
});
|
||||
|
||||
NSLog(@"[SCInsta] Cleaning cache...");
|
||||
[SCIUtils cleanCache];
|
||||
|
||||
if ([SCIUtils getBoolPref:@"flex_app_launch"]) {
|
||||
[[objc_getClass("FLEXManager") sharedManager] showExplorer];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(id)arg1 {
|
||||
%orig;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"flex_app_start"]) {
|
||||
[[objc_getClass("FLEXManager") sharedManager] showExplorer];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGDSLauncherConfig
|
||||
- (_Bool)isLiquidGlassInAppNotificationEnabled {
|
||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||
}
|
||||
- (_Bool)isLiquidGlassContextMenuEnabled{
|
||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||
}
|
||||
- (_Bool)isLiquidGlassToastEnabled {
|
||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||
}
|
||||
- (_Bool)isLiquidGlassToastPeekEnabled {
|
||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||
}
|
||||
- (_Bool)isLiquidGlassAlertDialogEnabled {
|
||||
return [SCIUtils liquidGlassEnabledBool:%orig];
|
||||
}
|
||||
%end
|
||||
|
||||
// Disable sending modded insta bug reports
|
||||
%hook IGWindow
|
||||
- (void)showDebugMenu {
|
||||
return;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGBugReportUploader
|
||||
- (id)initWithNetworker:(id)arg1
|
||||
pandoGraphQLService:(id)arg2
|
||||
analyticsLogger:(id)arg3
|
||||
userDefaults:(id)arg4
|
||||
launcherSetProvider:(id)arg5
|
||||
shouldPersistLastBugReportId:(id)arg6
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
%end
|
||||
|
||||
// Disable anti-screenshot feature on visual messages
|
||||
%hook IGStoryViewerContainerView
|
||||
- (void)setShouldBlockScreenshot:(BOOL)arg1 viewModel:(id)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
// Disable screenshot logging/detection
|
||||
%hook IGDirectVisualMessageViewerSession
|
||||
- (id)visualMessageViewerController:(id)arg1 didDetectScreenshotForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 { NONVOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGDirectVisualMessageReplayService
|
||||
- (id)visualMessageViewerController:(id)arg1 didDetectScreenshotForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 { NONVOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGDirectVisualMessageReportService
|
||||
- (id)visualMessageViewerController:(id)arg1 didDetectScreenshotForVisualMessage:(id)arg2 atIndex:(NSInteger)arg3 { NONVOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGDirectVisualMessageScreenshotSafetyLogger
|
||||
- (id)initWithUserSession:(id)arg1 entryPoint:(NSInteger)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"remove_screenshot_alert"]) {
|
||||
NSLog(@"[SCInsta] Disable visual message screenshot safety logger");
|
||||
return nil;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGScreenshotObserver
|
||||
- (id)initForController:(id)arg1 { NONVOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGScreenshotObserverDelegate
|
||||
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGDirectMediaViewerViewController
|
||||
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGStoryViewerViewController
|
||||
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGSundialFeedViewController
|
||||
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
%hook IGDirectVisualMessageViewerController
|
||||
- (void)screenshotObserverDidSeeScreenshotTaken:(id)arg1 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
- (void)screenshotObserverDidSeeActiveScreenCapture:(id)arg1 event:(NSInteger)arg2 { VOID_HANDLESCREENSHOT(%orig); }
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Hide items
|
||||
|
||||
// Direct suggested chats (in search bar)
|
||||
%hook IGDirectInboxSearchListAdapterDataSource
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Section header
|
||||
if ([obj isKindOfClass:%c(IGLabelItemViewModel)]) {
|
||||
|
||||
// Broadcast channels
|
||||
if ([[obj valueForKey:@"uniqueIdentifier"] isEqualToString:@"channels"]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_chats"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats (header)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Ask Meta AI
|
||||
else if ([[obj valueForKey:@"labelTitle"] isEqualToString:@"Ask Meta AI"]) {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai suggested chats (header)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// AI
|
||||
else if ([[obj valueForKey:@"labelTitle"] isEqualToString:@"AI"]) {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding ai suggested chats (header)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// AI agents section
|
||||
else if (
|
||||
[obj isKindOfClass:%c(IGDirectInboxSearchAIAgentsPillsSectionViewModel)]
|
||||
|| [obj isKindOfClass:%c(IGDirectInboxSearchAIAgentsSuggestedPromptViewModel)]
|
||||
|| [obj isKindOfClass:%c(IGDirectInboxSearchAIAgentsSuggestedPromptLoggingViewModel)]
|
||||
) {
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats (ai agents)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Recipients list
|
||||
else if ([obj isKindOfClass:%c(IGDirectRecipientCellViewModel)]) {
|
||||
|
||||
// Broadcast channels
|
||||
if ([[obj recipient] isBroadcastChannel]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_chats"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats (broadcast channels recipient)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Meta AI (special section types)
|
||||
else if (([obj sectionType] == 20) || [obj sectionType] == 18) {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai suggested chats (meta ai recipient)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Meta AI (catch-all)
|
||||
else if ([[[obj recipient] threadName] isEqualToString:@"Meta AI"]) {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai suggested chats (meta ai recipient)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Direct suggested chats (thread creation view)
|
||||
%hook IGDirectThreadCreationViewController
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Meta AI suggested user in direct new message view
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
if ([obj isKindOfClass:%c(IGDirectCreateChatCellViewModel)]) {
|
||||
|
||||
// "AI Chats"
|
||||
if ([[obj valueForKey:@"title"] isEqualToString:@"AI chats"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: direct thread creation ai chats section");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
else if ([obj isKindOfClass:%c(IGDirectRecipientCellViewModel)]) {
|
||||
|
||||
// Meta AI suggested user
|
||||
if ([[[obj recipient] threadName] isEqualToString:@"Meta AI"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai: direct thread creation ai suggestion");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Invite friends to insta contacts upsell
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
if ([obj isKindOfClass:%c(IGContactInvitesSearchUpsellViewModel)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: invite contacts upsell");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Direct suggested chats (inbox view)
|
||||
%hook IGDirectInboxListAdapterDataSource
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Section header
|
||||
if ([obj isKindOfClass:%c(IGDirectInboxHeaderCellViewModel)]) {
|
||||
|
||||
// "Suggestions" header
|
||||
if ([[obj title] isEqualToString:@"Suggestions"]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats (header: messages tab)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// "Accounts to follow/message" header
|
||||
else if ([[obj title] hasPrefix:@"Accounts to"]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: (header: inbox view)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Suggested recipients
|
||||
else if ([obj isKindOfClass:%c(IGDirectInboxSuggestedThreadCellViewModel)]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats (recipients: channels tab)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// "Accounts to follow" recipients
|
||||
else if ([obj isKindOfClass:%c(IGDiscoverPeopleItemConfiguration)] || [obj isKindOfClass:%c(IGDiscoverPeopleConnectionItemConfiguration)]) {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested chats: (recipients: inbox view)");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide notes tray
|
||||
else if ([obj isKindOfClass:%c(IGDirectNotesTrayRowViewModel)]) {
|
||||
if ([SCIUtils getBoolPref:@"hide_notes_tray"]) {
|
||||
NSLog(@"[SCInsta] Hiding notes tray");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Explore page results
|
||||
%hook IGSearchListKitDataSource
|
||||
- (id)objectsForListAdapter:(id)arg1 {
|
||||
NSArray *originalObjs = %orig();
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Meta AI
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
|
||||
// Section header
|
||||
if ([obj isKindOfClass:%c(IGLabelItemViewModel)]) {
|
||||
|
||||
// "Ask Meta AI" search results header
|
||||
if ([[obj valueForKey:@"labelTitle"] isEqualToString:@"Ask Meta AI"]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Empty search bar upsell view
|
||||
else if ([obj isKindOfClass:%c(IGSearchNullStateUpsellViewModel)]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
// Meta AI search suggestions
|
||||
else if ([obj isKindOfClass:%c(IGSearchResultNestedGroupViewModel)]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
// Meta AI suggested search results
|
||||
else if ([obj isKindOfClass:%c(IGSearchResultViewModel)]) {
|
||||
|
||||
// itemType 6 is meta ai suggestions
|
||||
if ([obj itemType] == 6) {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Meta AI user account in search results
|
||||
else if ([[[obj title] string] isEqualToString:@"meta.ai"]) {
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// No suggested users
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
|
||||
// Section header
|
||||
if ([obj isKindOfClass:%c(IGLabelItemViewModel)]) {
|
||||
|
||||
// "Suggested for you" search results header
|
||||
if ([[obj valueForKey:@"labelTitle"] isEqualToString:@"Suggested for you"]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Instagram users
|
||||
else if ([obj isKindOfClass:%c(IGDiscoverPeopleItemConfiguration)]) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
// See all suggested users
|
||||
else if ([obj isKindOfClass:%c(IGSeeAllItemConfiguration)] && ((IGSeeAllItemConfiguration *)obj).destination == 4) {
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Story tray
|
||||
%hook IGMainStoryTrayDataSource
|
||||
- (id)allItemsForTrayUsingCachedValue:(BOOL)cached {
|
||||
NSArray *originalObjs = %orig(cached);
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (IGStoryTrayViewModel *obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) {
|
||||
if ([obj isKindOfClass:%c(IGStoryTrayViewModel)]) {
|
||||
NSNumber *type = [((IGStoryTrayViewModel *)obj) valueForKey:@"type"];
|
||||
|
||||
// 8/9 looks to be the types for recommended stories
|
||||
if ([type isEqual:@(8)] || [type isEqual:@(9)]) {
|
||||
NSLog(@"[SCInsta] Hiding suggested users: story tray");
|
||||
|
||||
shouldHide = YES;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_ads"]) {
|
||||
// "New!" account id is 3538572169
|
||||
if ([obj isKindOfClass:%c(IGStoryTrayViewModel)] && (obj.isUnseenNux == YES || [obj.pk isEqualToString:@"3538572169"])) {
|
||||
NSLog(@"[SCInsta] Removing ads: story tray");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
}
|
||||
|
||||
return [filteredObjs copy];
|
||||
}
|
||||
%end
|
||||
|
||||
// Story tray expanded footer (Suggested accounts to follow)
|
||||
%hook IGStoryTraySectionController
|
||||
- (void)storyTrayControllerShowSUPOGEducationBump {
|
||||
if ([SCIUtils getBoolPref:@"no_suggested_users"]) return;
|
||||
|
||||
return %orig();
|
||||
}
|
||||
%end
|
||||
|
||||
// Modern IGDS app menus
|
||||
%hook IGDSMenu
|
||||
- (id)initWithMenuItems:(NSArray<IGDSMenuItem *> *)originalObjs edr:(BOOL)edr headerLabelText:(id)headerLabelText {
|
||||
NSMutableArray *filteredObjs = [NSMutableArray arrayWithCapacity:[originalObjs count]];
|
||||
|
||||
for (id obj in originalObjs) {
|
||||
BOOL shouldHide = NO;
|
||||
|
||||
// Meta AI
|
||||
if (
|
||||
[[obj valueForKey:@"title"] isEqualToString:@"AI images"]
|
||||
|| [[obj valueForKey:@"title"] isEqualToString:@"Meta AI"]
|
||||
) {
|
||||
|
||||
if ([SCIUtils getBoolPref:@"hide_meta_ai"]) {
|
||||
NSLog(@"[SCInsta] Hiding meta ai from IGDS menu");
|
||||
|
||||
shouldHide = YES;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Populate new objs array
|
||||
if (!shouldHide) {
|
||||
[filteredObjs addObject:obj];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return %orig([filteredObjs copy], edr, headerLabelText);
|
||||
}
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Confirm buttons
|
||||
|
||||
%hook IGFeedItemUFICell
|
||||
- (void)UFIButtonBarDidTapOnLike:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"like_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm post like triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)UFIButtonBarDidTapOnRepost:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm repost triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)UFIButtonBarDidLongPressOnRepost:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm repost triggered (long press ignored)");
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
- (void)UFIButtonBarDidLongPressOnRepost:(id)arg1 withGestureRecognizer:(id)arg2 {
|
||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm repost triggered (long press ignored)");
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
%hook IGSundialViewerVerticalUFI
|
||||
- (void)_didTapLikeButton:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) {
|
||||
NSLog(@"[SCInsta] Confirm reels like triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_didLongPressLikeButton:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) {
|
||||
NSLog(@"[SCInsta] Confirm repost triggered (long press ignored)");
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_didTapRepostButton:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm repost triggered");
|
||||
|
||||
[SCIUtils showConfirmation:^(void) { %orig; }];
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_didLongPressRepostButton:(id)arg1 {
|
||||
if ([SCIUtils getBoolPref:@"repost_confirm"]) {
|
||||
NSLog(@"[SCInsta] Confirm repost triggered (long press ignored)");
|
||||
}
|
||||
else {
|
||||
return %orig;
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// FLEX explorer gesture handler
|
||||
%hook IGRootViewController
|
||||
- (void)viewDidLoad {
|
||||
%orig;
|
||||
|
||||
// Recognize 5-finger long press
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPress.minimumPressDuration = 1;
|
||||
longPress.numberOfTouchesRequired = 5;
|
||||
[self.view addGestureRecognizer:longPress];
|
||||
}
|
||||
%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state != UIGestureRecognizerStateBegan) return;
|
||||
|
||||
if ([SCIUtils getBoolPref:@"flex_instagram"]) {
|
||||
[[objc_getClass("FLEXManager") sharedManager] showExplorer];
|
||||
}
|
||||
}
|
||||
%end
|
||||
|
||||
// Disable safe mode (defaults reset upon subsequent crashes)
|
||||
%hook IGSafeModeChecker
|
||||
- (id)initWithInstacrashCounterProvider:(void *)provider crashThreshold:(unsigned long long)threshold {
|
||||
if ([SCIUtils getBoolPref:@"disable_safe_mode"]) return nil;
|
||||
|
||||
return %orig(provider, threshold);
|
||||
}
|
||||
- (unsigned long long)crashCount {
|
||||
if ([SCIUtils getBoolPref:@"disable_safe_mode"]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return %orig;
|
||||
}
|
||||
%end
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <QuickLook/QuickLook.h>
|
||||
#import <os/log.h>
|
||||
#import <objc/message.h>
|
||||
|
||||
#import "../modules/JGProgressHUD/JGProgressHUD.h"
|
||||
|
||||
#import "InstagramHeaders.h"
|
||||
#import "QuickLook.h"
|
||||
|
||||
#import "Settings/SCISettingsViewController.h"
|
||||
|
||||
#define SCILog(fmt, ...) \
|
||||
do { \
|
||||
NSString *tmpStr = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \
|
||||
os_log(OS_LOG_DEFAULT, "[SCInsta Test] %{public}s", tmpStr.UTF8String); \
|
||||
} while(0)
|
||||
|
||||
#define SCILogId(prefix, obj) os_log(OS_LOG_DEFAULT, "[SCInsta Test] %{public}@: %{public}@", prefix, obj);
|
||||
|
||||
@interface SCIUtils : NSObject
|
||||
|
||||
+ (BOOL)getBoolPref:(NSString *)key;
|
||||
+ (double)getDoublePref:(NSString *)key;
|
||||
+ (NSString *)getStringPref:(NSString *)key;
|
||||
|
||||
+ (_Bool)liquidGlassEnabledBool:(_Bool)fallback;
|
||||
|
||||
+ (void)cleanCache;
|
||||
|
||||
// Displaying View Controllers
|
||||
+ (void)showQuickLookVC:(NSArray<id> *)items;
|
||||
+ (void)showShareVC:(id)item;
|
||||
+ (void)showSettingsVC:(UIWindow *)window;
|
||||
|
||||
// Colours
|
||||
+ (UIColor *)SCIColor_Primary;
|
||||
|
||||
// Errors
|
||||
+ (NSError *)errorWithDescription:(NSString *)errorDesc;
|
||||
+ (NSError *)errorWithDescription:(NSString *)errorDesc code:(NSInteger)errorCode;
|
||||
|
||||
+ (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc;
|
||||
+ (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc dismissAfterDelay:(CGFloat)dismissDelay;
|
||||
|
||||
// Media
|
||||
+ (NSURL *)getPhotoUrl:(IGPhoto *)photo;
|
||||
+ (NSURL *)getPhotoUrlForMedia:(IGMedia *)media;
|
||||
|
||||
+ (NSURL *)getVideoUrl:(IGVideo *)video;
|
||||
+ (NSURL *)getVideoUrlForMedia:(IGMedia *)media;
|
||||
|
||||
// View Controllers
|
||||
+ (UIViewController *)viewControllerForView:(UIView *)view;
|
||||
+ (UIViewController *)viewControllerForAncestralView:(UIView *)view;
|
||||
+ (UIViewController *)nearestViewControllerForView:(UIView *)view;
|
||||
|
||||
// Functions
|
||||
+ (NSString *)IGVersionString;
|
||||
+ (BOOL)isNotch;
|
||||
|
||||
+ (BOOL)existingLongPressGestureRecognizerForView:(UIView *)view;
|
||||
|
||||
// Alerts
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler title:(NSString *)title;
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler title:(NSString *)title;
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler;
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler;
|
||||
+ (void)showRestartConfirmation;
|
||||
|
||||
// Toasts
|
||||
+ (void)showToastForDuration:(double)duration title:(NSString *)title;
|
||||
+ (void)showToastForDuration:(double)duration title:(NSString *)title subtitle:(NSString *)subtitle;
|
||||
|
||||
// Math
|
||||
+ (NSUInteger)decimalPlacesInDouble:(double)value;
|
||||
|
||||
// Ivars
|
||||
+ (id)getIvarForObj:(id)obj name:(const char *)name;
|
||||
+ (void)setIvarForObj:(id)obj name:(const char *)name value:(id)value;
|
||||
|
||||
@end
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
#import "Utils.h"
|
||||
|
||||
@implementation SCIUtils
|
||||
|
||||
+ (BOOL)getBoolPref:(NSString *)key {
|
||||
if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return false;
|
||||
|
||||
return [[NSUserDefaults standardUserDefaults] boolForKey:key];
|
||||
}
|
||||
+ (double)getDoublePref:(NSString *)key {
|
||||
if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return 0;
|
||||
|
||||
return [[NSUserDefaults standardUserDefaults] doubleForKey:key];
|
||||
}
|
||||
+ (NSString *)getStringPref:(NSString *)key {
|
||||
if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return @"";
|
||||
|
||||
return [[NSUserDefaults standardUserDefaults] stringForKey:key];
|
||||
}
|
||||
|
||||
+ (_Bool)liquidGlassEnabledBool:(_Bool)fallback {
|
||||
BOOL setting = [SCIUtils getBoolPref:@"liquid_glass_surfaces"];
|
||||
return setting ? true : fallback;
|
||||
}
|
||||
|
||||
+ (void)cleanCache {
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSMutableArray<NSError *> *deletionErrors = [NSMutableArray array];
|
||||
|
||||
// Temp folder
|
||||
// * disabled bc app crashed trying to delete certain files inside it
|
||||
// todo: remove the above disclaimer if this new code doesn't cause crashing
|
||||
NSArray *tempFolderContents = [fileManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:NSTemporaryDirectory()] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
|
||||
|
||||
for (NSURL *fileURL in tempFolderContents) {
|
||||
NSError *cacheItemDeletionError;
|
||||
[fileManager removeItemAtURL:fileURL error:&cacheItemDeletionError];
|
||||
|
||||
if (cacheItemDeletionError) [deletionErrors addObject:cacheItemDeletionError];
|
||||
}
|
||||
|
||||
// Analytics folder
|
||||
NSString *analyticsFolder = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Application Support/com.burbn.instagram/analytics"];
|
||||
NSArray *analyticsFolderContents = [fileManager contentsOfDirectoryAtURL:[[NSURL alloc] initFileURLWithPath:analyticsFolder] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
|
||||
|
||||
for (NSURL *fileURL in analyticsFolderContents) {
|
||||
NSError *cacheItemDeletionError;
|
||||
[fileManager removeItemAtURL:fileURL error:&cacheItemDeletionError];
|
||||
|
||||
if (cacheItemDeletionError) [deletionErrors addObject:cacheItemDeletionError];
|
||||
}
|
||||
|
||||
// Caches folder
|
||||
NSString *cachesFolder = [[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Caches"];
|
||||
NSArray *cachesFolderContents = [fileManager contentsOfDirectoryAtURL:[[NSURL alloc] initFileURLWithPath:cachesFolder] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
|
||||
|
||||
for (NSURL *fileURL in cachesFolderContents) {
|
||||
NSError *cacheItemDeletionError;
|
||||
[fileManager removeItemAtURL:fileURL error:&cacheItemDeletionError];
|
||||
|
||||
if (cacheItemDeletionError) [deletionErrors addObject:cacheItemDeletionError];
|
||||
}
|
||||
|
||||
// Log errors
|
||||
if (deletionErrors.count > 1) {
|
||||
|
||||
for (NSError *error in deletionErrors) {
|
||||
NSLog(@"[SCInsta] File Deletion Error: %@", error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Displaying View Controllers
|
||||
+ (void)showQuickLookVC:(NSArray<id> *)items {
|
||||
UIViewController *topVC = topMostController();
|
||||
if (!topVC) {
|
||||
NSLog(@"[SCInsta] No view controller available to present QuickLook");
|
||||
return;
|
||||
}
|
||||
|
||||
QLPreviewController *previewController = [[QLPreviewController alloc] init];
|
||||
QuickLookDelegate *quickLookDelegate = [[QuickLookDelegate alloc] initWithPreviewItemURLs:items];
|
||||
|
||||
previewController.dataSource = quickLookDelegate;
|
||||
|
||||
[topVC presentViewController:previewController animated:true completion:nil];
|
||||
}
|
||||
+ (void)showShareVC:(id)item {
|
||||
UIViewController *topVC = topMostController();
|
||||
if (!topVC) {
|
||||
NSLog(@"[SCInsta] No view controller available to present share sheet");
|
||||
return;
|
||||
}
|
||||
|
||||
UIActivityViewController *acVC = [[UIActivityViewController alloc] initWithActivityItems:@[item] applicationActivities:nil];
|
||||
if (is_iPad()) {
|
||||
acVC.popoverPresentationController.sourceView = topVC.view;
|
||||
acVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width / 2.0, topVC.view.bounds.size.height / 2.0, 1.0, 1.0);
|
||||
}
|
||||
[topVC presentViewController:acVC animated:true completion:nil];
|
||||
}
|
||||
+ (void)showSettingsVC:(UIWindow *)window {
|
||||
UIViewController *rootController = [window rootViewController];
|
||||
SCISettingsViewController *settingsViewController = [SCISettingsViewController new];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:settingsViewController];
|
||||
|
||||
[rootController presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
// Colours
|
||||
+ (UIColor *)SCIColor_Primary {
|
||||
return [UIColor colorWithRed:0/255.0 green:152/255.0 blue:254/255.0 alpha:1];
|
||||
};
|
||||
|
||||
// Errors
|
||||
+ (NSError *)errorWithDescription:(NSString *)errorDesc {
|
||||
return [self errorWithDescription:errorDesc code:1];
|
||||
}
|
||||
+ (NSError *)errorWithDescription:(NSString *)errorDesc code:(NSInteger)errorCode {
|
||||
NSError *error = [ NSError errorWithDomain:@"com.socuul.scinsta" code:errorCode userInfo:@{ NSLocalizedDescriptionKey: errorDesc } ];
|
||||
return error;
|
||||
}
|
||||
|
||||
+ (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc {
|
||||
return [self showErrorHUDWithDescription:errorDesc dismissAfterDelay:4.0];
|
||||
}
|
||||
+ (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc dismissAfterDelay:(CGFloat)dismissDelay {
|
||||
JGProgressHUD *hud = [[JGProgressHUD alloc] init];
|
||||
hud.textLabel.text = errorDesc;
|
||||
hud.indicatorView = [[JGProgressHUDErrorIndicatorView alloc] init];
|
||||
|
||||
UIView *hudView = topMostController().view;
|
||||
if (!hudView) hudView = [UIApplication sharedApplication].keyWindow;
|
||||
if (hudView) {
|
||||
[hud showInView:hudView];
|
||||
[hud dismissAfterDelay:dismissDelay];
|
||||
} else {
|
||||
NSLog(@"[SCInsta] No valid view for error HUD: %@", errorDesc);
|
||||
}
|
||||
|
||||
return hud;
|
||||
}
|
||||
|
||||
// Media
|
||||
+ (NSURL *)getPhotoUrl:(IGPhoto *)photo {
|
||||
if (!photo) return nil;
|
||||
|
||||
// Get highest quality photo link
|
||||
NSURL *photoUrl = [photo imageURLForWidth:100000.00];
|
||||
|
||||
return photoUrl;
|
||||
}
|
||||
+ (NSURL *)getPhotoUrlForMedia:(IGMedia *)media {
|
||||
if (!media) return nil;
|
||||
|
||||
IGPhoto *photo = media.photo;
|
||||
|
||||
return [SCIUtils getPhotoUrl:photo];
|
||||
}
|
||||
+ (NSURL *)getVideoUrl:(IGVideo *)video {
|
||||
if (!video) return nil;
|
||||
|
||||
// The past (pre v398)
|
||||
if ([video respondsToSelector:@selector(sortedVideoURLsBySize)]) {
|
||||
NSArray<NSDictionary *> *sorted = [video sortedVideoURLsBySize];
|
||||
NSString *urlString = sorted.firstObject[@"url"];
|
||||
return urlString.length ? [NSURL URLWithString:urlString] : nil;
|
||||
}
|
||||
|
||||
// The present (post v398)
|
||||
if ([video respondsToSelector:@selector(allVideoURLs)]) {
|
||||
return [[video allVideoURLs] anyObject];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
+ (NSURL *)getVideoUrlForMedia:(IGMedia *)media {
|
||||
if (!media) return nil;
|
||||
|
||||
IGVideo *video = media.video;
|
||||
if (!video) return nil;
|
||||
|
||||
return [SCIUtils getVideoUrl:video];
|
||||
}
|
||||
|
||||
// View Controllers
|
||||
+ (UIViewController *)viewControllerForView:(UIView *)view {
|
||||
NSString *viewDelegate = @"viewDelegate";
|
||||
if ([view respondsToSelector:NSSelectorFromString(viewDelegate)]) {
|
||||
return [view valueForKey:viewDelegate];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (UIViewController *)viewControllerForAncestralView:(UIView *)view {
|
||||
NSString *_viewControllerForAncestor = @"_viewControllerForAncestor";
|
||||
if ([view respondsToSelector:NSSelectorFromString(_viewControllerForAncestor)]) {
|
||||
return [view valueForKey:_viewControllerForAncestor];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (UIViewController *)nearestViewControllerForView:(UIView *)view {
|
||||
return [self viewControllerForView:view] ?: [self viewControllerForAncestralView:view];
|
||||
}
|
||||
|
||||
// Functions
|
||||
+ (NSString *)IGVersionString {
|
||||
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
||||
};
|
||||
+ (BOOL)isNotch {
|
||||
return [[[UIApplication sharedApplication] keyWindow] safeAreaInsets].bottom > 0;
|
||||
};
|
||||
|
||||
+ (BOOL)existingLongPressGestureRecognizerForView:(UIView *)view {
|
||||
NSArray *allRecognizers = view.gestureRecognizers;
|
||||
|
||||
for (UIGestureRecognizer *recognizer in allRecognizers) {
|
||||
if ([[recognizer class] isSubclassOfClass:[UILongPressGestureRecognizer class]]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Alerts
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler title:(NSString *)title {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:@"Are you sure?" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Yes" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||||
okHandler();
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"No!" style:UIAlertActionStyleCancel handler:nil]];
|
||||
|
||||
[topMostController() presentViewController:alert animated:YES completion:nil];
|
||||
|
||||
return nil;
|
||||
};
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler title:(NSString *)title {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:@"Are you sure?" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Yes" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||||
okHandler();
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"No!" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
|
||||
if (cancelHandler != nil) {
|
||||
cancelHandler();
|
||||
}
|
||||
}]];
|
||||
|
||||
[topMostController() presentViewController:alert animated:YES completion:nil];
|
||||
|
||||
return nil;
|
||||
};
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler {
|
||||
return [self showConfirmation:okHandler title:nil];
|
||||
};
|
||||
+ (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler {
|
||||
return [self showConfirmation:okHandler cancelHandler:cancelHandler title:nil];
|
||||
}
|
||||
+ (void)showRestartConfirmation {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Restart required" message:@"You must restart the app to apply this change" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Restart" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||||
exit(0);
|
||||
}]];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Later" style:UIAlertActionStyleCancel handler:nil]];
|
||||
|
||||
[topMostController() presentViewController:alert animated:YES completion:nil];
|
||||
};
|
||||
|
||||
// Toasts
|
||||
+ (void)showToastForDuration:(double)duration title:(NSString *)title {
|
||||
[SCIUtils showToastForDuration:duration title:title subtitle:nil];
|
||||
}
|
||||
+ (void)showToastForDuration:(double)duration title:(NSString *)title subtitle:(NSString *)subtitle {
|
||||
// Root VC
|
||||
Class rootVCClass = NSClassFromString(@"IGRootViewController");
|
||||
|
||||
UIViewController *topMostVC = topMostController();
|
||||
if (![topMostVC isKindOfClass:rootVCClass]) return;
|
||||
|
||||
IGRootViewController *rootVC = (IGRootViewController *)topMostVC;
|
||||
|
||||
// Presenter
|
||||
IGActionableConfirmationToastPresenter *toastPresenter = [rootVC toastPresenter];
|
||||
if (toastPresenter == nil) return;
|
||||
|
||||
// View Model
|
||||
Class modelClass = NSClassFromString(@"IGActionableConfirmationToastViewModel");
|
||||
IGActionableConfirmationToastViewModel *model = [modelClass new];
|
||||
|
||||
[model setValue:title forKey:@"text_annotatedTitleText"];
|
||||
[model setValue:subtitle forKey:@"text_annotatedSubtitleText"];
|
||||
|
||||
// Show new toast, after clearing existing one
|
||||
[toastPresenter hideAlert];
|
||||
[toastPresenter showAlertWithViewModel:model isAnimated:true animationDuration:duration presentationPriority:0 tapActionBlock:nil presentedHandler:nil dismissedHandler:nil];
|
||||
}
|
||||
|
||||
// Math
|
||||
+ (NSUInteger)decimalPlacesInDouble:(double)value {
|
||||
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
||||
[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
|
||||
[formatter setMaximumFractionDigits:15]; // Allow enough digits for double precision
|
||||
[formatter setMinimumFractionDigits:0];
|
||||
[formatter setDecimalSeparator:@"."]; // Force dot for internal logic, then respect locale for final display if needed
|
||||
|
||||
NSString *stringValue = [formatter stringFromNumber:@(value)];
|
||||
|
||||
// Find decimal separator
|
||||
NSRange decimalRange = [stringValue rangeOfString:formatter.decimalSeparator];
|
||||
|
||||
if (decimalRange.location == NSNotFound) {
|
||||
return 0;
|
||||
} else {
|
||||
return stringValue.length - (decimalRange.location + decimalRange.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Ivars
|
||||
+ (id)getIvarForObj:(id)obj name:(const char *)name {
|
||||
Ivar ivar = class_getInstanceVariable(object_getClass(obj), name);
|
||||
if (!ivar) return nil;
|
||||
|
||||
return object_getIvar(obj, ivar);
|
||||
}
|
||||
+ (void)setIvarForObj:(id)obj name:(const char *)name value:(id)value {
|
||||
Ivar ivar = class_getInstanceVariable(object_getClass(obj), name);
|
||||
if (!ivar) return;
|
||||
|
||||
object_setIvarWithStrongDefault(obj, ivar, value);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user