diff --git a/LCAppListViewController.m b/LCAppListViewController.m index c5fd4e1..2167a73 100644 --- a/LCAppListViewController.m +++ b/LCAppListViewController.m @@ -12,14 +12,6 @@ #import "UIViewController+LCAlert.h" #import "unarchive.h" -/* -#include -#include -#include -#include -#include -*/ - @implementation NSURL(hack) - (BOOL)safari_isHTTPFamilyURL { // Screw it, Apple @@ -294,31 +286,36 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa return 80.0f; } -- (void)deleteAppAtIndexPath:(NSIndexPath *)indexPath { - LCAppInfo* appInfo = [[LCAppInfo alloc] initWithBundlePath: [NSString stringWithFormat:@"%@/%@", self.bundlePath, self.objects[indexPath.row]]]; - UIAlertController* uninstallAlert = [UIAlertController alertControllerWithTitle:@"Confirm Uninstallation" message:[NSString stringWithFormat:@"Are you sure you want to uninstall %@?", [appInfo displayName]] preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction* uninstallApp = [UIAlertAction actionWithTitle:@"Uninstall" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { - NSError *error = nil; - [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@/%@", self.bundlePath, self.objects[indexPath.row]] error:&error]; - if (error) { - [self showDialogTitle:@"Error" message:error.localizedDescription]; - return; +- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath completionHandler:(void(^)(BOOL actionPerformed))handler { + NSString *path = [self.bundlePath stringByAppendingPathComponent:self.objects[indexPath.row]]; + LCAppInfo* appInfo = [[LCAppInfo alloc] initWithBundlePath:path]; + [self showConfirmationDialogTitle:@"Confirm Uninstallation" + message:[NSString stringWithFormat:@"Are you sure you want to uninstall %@?", appInfo.displayName] + destructive:YES + confirmButtonTitle:@"Uninstall" + handler:^(UIAlertAction * action) { + if (action.style != UIAlertActionStyleCancel) { + NSError *error = nil; + [NSFileManager.defaultManager removeItemAtPath:path error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + } else { + [self.objects removeObjectAtIndex:indexPath.row]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } } - [self.objects removeObjectAtIndex:indexPath.row]; - [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic]; + handler(YES); }]; - [uninstallAlert addAction:uninstallApp]; - UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; - [uninstallAlert addAction:cancelAction]; - [self presentViewController:uninstallAlert animated:YES completion:nil]; -} -- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { - [self deleteAppAtIndexPath:indexPath]; } -- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return self.objects[indexPath.row].length==0 ? UITableViewCellEditingStyleNone : UITableViewCellEditingStyleDelete; +- (UISwipeActionsConfiguration *) tableView:(UITableView *)tableView +trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { + return [UISwipeActionsConfiguration configurationWithActions:@[ + [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive + title:@"Delete" handler:^(UIContextualAction *action, __kindof UIView *sourceView, void (^completionHandler)(BOOL actionPerformed)) { + [self deleteItemAtIndexPath:indexPath completionHandler:completionHandler]; + }] + ]]; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { @@ -409,7 +406,6 @@ - (void)patchExecAndSignIfNeed:(NSIndexPath *)indexPath shouldSort:(BOOL)sortNam [info removeObjectForKey:@"LCBundleIdentifier"]; __block NSProgress *progress = [LCUtils signAppBundle:appPathURL - completionHandler:^(BOOL success, NSError *_Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { diff --git a/LCTabBarController.m b/LCTabBarController.m index 78f5d21..dee0375 100644 --- a/LCTabBarController.m +++ b/LCTabBarController.m @@ -1,5 +1,6 @@ #import "LCAppListViewController.h" #import "LCSettingsListController.h" +#import "LCTweakListViewController.h" #import "LCTabBarController.h" @implementation LCTabBarController @@ -10,16 +11,21 @@ - (void)loadView { LCAppListViewController* appTableVC = [LCAppListViewController new]; appTableVC.title = @"Apps"; + LCTweakListViewController* tweakTableVC = [LCTweakListViewController new]; + tweakTableVC.title = @"Tweaks"; + LCSettingsListController* settingsListVC = [LCSettingsListController new]; settingsListVC.title = @"Settings"; UINavigationController* appNavigationController = [[UINavigationController alloc] initWithRootViewController:appTableVC]; + UINavigationController* tweakNavigationController = [[UINavigationController alloc] initWithRootViewController:tweakTableVC]; UINavigationController* settingsNavigationController = [[UINavigationController alloc] initWithRootViewController:settingsListVC]; - + appNavigationController.tabBarItem.image = [UIImage systemImageNamed:@"square.stack.3d.up.fill"]; + tweakNavigationController.tabBarItem.image = [UIImage systemImageNamed:@"wrench.and.screwdriver"]; settingsNavigationController.tabBarItem.image = [UIImage systemImageNamed:@"gear"]; - self.viewControllers = @[appNavigationController, settingsNavigationController]; + self.viewControllers = @[appNavigationController, tweakNavigationController, settingsNavigationController]; } @end diff --git a/LCTweakListViewController (1).m b/LCTweakListViewController (1).m new file mode 100644 index 0000000..8e079c9 --- /dev/null +++ b/LCTweakListViewController (1).m @@ -0,0 +1,255 @@ +@import UniformTypeIdentifiers; + +#import "LCTweakListViewController.h" +#import "UIKitPrivate.h" +#import "UIViewController+LCAlert.h" + +@interface LCTweakListViewController() +@property(nonatomic) NSString *path; +@property(nonatomic) NSMutableArray *objects; +@end + +@implementation LCTweakListViewController + +- (void)loadView { + [super loadView]; + + if (!self.path) { + NSString *docPath = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject.path; + self.path = [docPath stringByAppendingPathComponent:@"Tweaks"]; + } + [self loadPath]; + + UIMenu *addMenu = [UIMenu menuWithTitle:@"" image:nil identifier:nil + options:UIMenuOptionsDisplayInline + children:@[ + [UIAction + actionWithTitle:@"Tweak" + image:[UIImage systemImageNamed:@"doc"] + identifier:nil handler:^(UIAction *action) { + [self addDylibButtonTapped]; + }], + [UIAction + actionWithTitle:@"Folder" + image:[UIImage systemImageNamed:@"folder"] + identifier:nil handler:^(UIAction *action) { + [self addDirectoryButtonTapped]; + }] + ]]; + self.navigationItem.rightBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd menu:addMenu]; +} + +- (void)loadPath { + BOOL reload = self.objects != nil; + + NSMutableArray *directories = [NSMutableArray new]; + NSArray *files = [[NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:nil] filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL(NSString *name, NSDictionary *bindings) { + BOOL isDir; + NSString *path = [self.path stringByAppendingPathComponent:name]; + [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir]; + if (isDir) { + [directories addObject:name]; + } + return !isDir && [name hasSuffix:@".dylib"]; +return YES; + }]]; + + self.objects = [NSMutableArray new]; + [self.objects addObjectsFromArray:[directories sortedArrayUsingSelector:@selector(compare:)]]; + [self.objects addObjectsFromArray:[files sortedArrayUsingSelector:@selector(compare:)]]; + + if (reload) { + [self.tableView reloadData]; + } +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +/* +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + return @"N items"; +} +*/ + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.objects.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"Cell"]; + } + + UIListContentConfiguration *config = cell.defaultContentConfiguration; + config.text = self.objects[indexPath.row]; + + BOOL isDir; + NSString *path = [self.path stringByAppendingPathComponent:self.objects[indexPath.row]]; + [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir]; + config.image = [UIImage systemImageNamed:(isDir ? @"folder.fill" : @"doc")]; + + if (isDir) { + config.secondaryText = @"folder"; + } else { + NSDictionary *attrs = [NSFileManager.defaultManager attributesOfItemAtPath:path error:nil]; + NSNumber *size = attrs[NSFileSize]; + config.secondaryText = [NSByteCountFormatter stringFromByteCount:size.unsignedLongLongValue + countStyle:NSByteCountFormatterCountStyleFile]; + } + + cell.contentConfiguration = config; + cell.selectionStyle = isDir ? + UITableViewCellSelectionStyleDefault : + UITableViewCellSelectionStyleNone; + return cell; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + if (cell.selectionStyle == UITableViewCellSelectionStyleNone) { + return nil; + } else { + return indexPath; + } +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + NSString *name = self.objects[indexPath.row]; + LCTweakListViewController *childVC = [LCTweakListViewController new]; + childVC.path = [self.path stringByAppendingPathComponent:name]; + childVC.title = name; + [self.navigationController pushViewController:childVC animated:YES]; +} + +- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath completionHandler:(void(^)(BOOL actionPerformed))handler { + NSString *name = self.objects[indexPath.row]; + NSString *path = [self.path stringByAppendingPathComponent:name]; + [self showConfirmationDialogTitle:@"Confirm" + message:[NSString stringWithFormat:@"Are you sure you want to delete %@?", name] + confirmButtonTitle:@"Delete" + handler:^(UIAlertAction * action) { + if (action.style == UIAlertActionStyleCancel) return; + NSError *error = nil; + [NSFileManager.defaultManager removeItemAtPath:path error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + } else { + [self.objects removeObjectAtIndex:indexPath.row]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } + handler(YES); + }]; +} + +- (UISwipeActionsConfiguration *) tableView:(UITableView *)tableView +trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { + return [UISwipeActionsConfiguration configurationWithActions:@[ + [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive + title:@"Delete" handler:^(UIContextualAction *action, __kindof UIView *sourceView, void (^completionHandler)(BOOL actionPerformed)) { + [self deleteItemAtIndexPath:indexPath completionHandler:completionHandler]; + }] + ]]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:@"fractionCompleted"]) { + NSProgress *progress = (NSProgress *)object; + dispatch_async(dispatch_get_main_queue(), ^{ + self.progressView.progress = progress.fractionCompleted; + }); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { + NSError *error; + NSString *path = self.path; + if (LCUtils.certificatePassword) { + // Move them to a tmp folder to sign them + path = [self.path stringByAppendingPathComponent:@".tmp"]; + [NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:NO attributes:@{} error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + return; + } + } + + for (NSURL *url in urls) { + NSString *filePath = [path stringByAppendingPathComponent:url.path.lastPathComponent]; + [NSFileManager.defaultManager moveItemAtPath:url.path toPath:filePath error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + return; + } + } + + if (!LCUtils.certificatePassword) { + // JIT stop here + return; + } + + // Setup a fake app bundle for signing + NSString *tmpExecPath = [path stringByAppendingPathComponent:@"LiveContainer.tmp"]; + NSString *tmpInfoPath = [path stringByAppendingPathComponent:@"Info.plist"]; + [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; + NSMutableDictionary *info = NSBundle.mainBundle.infoDictionary.mutableCopy; + info[@"CFBundleExecutable"] = @"LiveContainer.tmp"; + [info writeToFile:tmpInfoPath atomically:YES]; + + dispatch_block_t handler = ^{ + if (error) { + [self showDialogTitle:@"Error while signing tweaks" message:error.localizedDescription]; + } else { + // Move tweaks back + for (NSURL *url in urls) { + NSString *fromPath = [path stringByAppendingPathComponent:url.path.lastPathComponent]; + NSString *toPath = [self.path stringByAppendingPathComponent:url.path.lastPathComponent]; + [NSFileManager.defaultManager moveItemAtPath:fromPath toPath:toPath error:&error]; + } + } + + // Remove tmp folder + [NSFileManager.defaultManager removeItemAtPath:path error:nil]; + [progress removeObserver:self forKeyPath:@"fractionCompleted"]; + [self.progressView removeFromSuperview]; + [self loadPath]; + }; + + NSProgress *progress = [LCUtils signAppBundle:appPathURL + completionHandler:^(BOOL success, NSError *_Nullable signError) { + error = signError; + dispatch_async(dispatch_get_main_queue(), handler); + }]; + + if (progress) { + [progress addObserver:self forKeyPath:@"fractionCompleted" options:NSKeyValueObservingOptionNew context:nil]; + } +} + +- (void)addDirectoryButtonTapped { + [self showInputDialogTitle:@"Add folder" message:@"Enter name" placeholder:@"Name" callback:^(NSString *name) { + NSError *error; + NSString *path = [self.path stringByAppendingPathComponent:name]; + [NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:NO attributes:@{} error:&error]; + [self loadPath]; + return error.localizedDescription; + }]; +} + +- (void)addDylibButtonTapped { + UIDocumentPickerViewController *documentPickerVC = [[UIDocumentPickerViewController alloc] + initForOpeningContentTypes:@[[UTType typeWithFilenameExtension:@"dylib" conformingToType:UTTypeData]] + asCopy:YES]; + documentPickerVC.allowsMultipleSelection = YES; + documentPickerVC.delegate = self; + [self presentViewController:documentPickerVC animated:YES completion:nil]; +} + +@end diff --git a/LCTweakListViewController.h b/LCTweakListViewController.h new file mode 100644 index 0000000..ccd8167 --- /dev/null +++ b/LCTweakListViewController.h @@ -0,0 +1,4 @@ +@import UIKit; + +@interface LCTweakListViewController : UITableViewController +@end diff --git a/LCTweakListViewController.m b/LCTweakListViewController.m new file mode 100644 index 0000000..75cee58 --- /dev/null +++ b/LCTweakListViewController.m @@ -0,0 +1,350 @@ +@import UniformTypeIdentifiers; + +#import "LCTweakListViewController.h" +#import "LCUtils.h" +#import "MBRoundProgressView.h" +#import "UIKitPrivate.h" +#import "UIViewController+LCAlert.h" + +@interface LCTweakListViewController() +@property(nonatomic) NSString *path; +@property(nonatomic) NSMutableArray *objects; + +@property(nonatomic) UIButton *signButton; +@property(nonatomic) MBRoundProgressView *progressView; +@end + +@implementation LCTweakListViewController + +- (void)loadView { + [super loadView]; + + if (!self.path) { + NSString *docPath = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject.path; + self.path = [docPath stringByAppendingPathComponent:@"Tweaks"]; + } + [self loadPath]; + + UIMenu *addMenu = [UIMenu menuWithTitle:@"" image:nil identifier:nil + options:UIMenuOptionsDisplayInline + children:@[ + [UIAction + actionWithTitle:@"Tweak" + image:[UIImage systemImageNamed:@"doc"] + identifier:nil handler:^(UIAction *action) { + [self addDylibButtonTapped]; + }], + [UIAction + actionWithTitle:@"Folder" + image:[UIImage systemImageNamed:@"folder"] + identifier:nil handler:^(UIAction *action) { + [self addFolderButtonTapped]; + }] + ]]; + + self.signButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.signButton.frame = CGRectMake(0, 0, 40, 40); + [self.signButton setImage:[UIImage systemImageNamed:@"signature"] forState:UIControlStateNormal]; + [self.signButton addTarget:self action:@selector(signTweaksButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + self.progressView = [MBRoundProgressView new]; + self.progressView.hidden = YES; + [self.signButton addSubview:self.progressView]; + self.navigationItem.rightBarButtonItems = @[ + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd menu:addMenu], + [[UIBarButtonItem alloc] initWithCustomView:self.signButton] + ]; + + self.refreshControl = [UIRefreshControl new]; + [self.refreshControl addTarget:self action:@selector(loadPath) forControlEvents:UIControlEventValueChanged]; +} + +- (void)loadPath { + self.title = self.path.lastPathComponent; + BOOL reload = self.objects != nil; + + NSMutableArray *directories = [NSMutableArray new]; + NSArray *files = [[NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:nil] filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL(NSString *name, NSDictionary *bindings) { + BOOL isDir; + NSString *path = [self.path stringByAppendingPathComponent:name]; + [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir]; + if (isDir) { + [directories addObject:name]; + } + return !isDir && [name hasSuffix:@".dylib"]; + }]]; + + self.objects = [NSMutableArray new]; + [self.objects addObjectsFromArray:[directories sortedArrayUsingSelector:@selector(compare:)]]; + [self.objects addObjectsFromArray:[files sortedArrayUsingSelector:@selector(compare:)]]; + + if (reload) { + [self.tableView reloadData]; + [self.refreshControl endRefreshing]; + } +} + +- (void)addDylibButtonTapped { + UIDocumentPickerViewController *documentPickerVC = [[UIDocumentPickerViewController alloc] + initForOpeningContentTypes:@[[UTType typeWithFilenameExtension:@"dylib" conformingToType:UTTypeData]] + asCopy:YES]; + documentPickerVC.allowsMultipleSelection = YES; + documentPickerVC.delegate = self; + [self presentViewController:documentPickerVC animated:YES completion:nil]; +} + +- (void)addFolderButtonTapped { + [self showInputDialogTitle:@"Add folder" message:@"Enter name" placeholder:@"Name" callback:^(NSString *name) { + NSError *error; + NSString *path = [self.path stringByAppendingPathComponent:name]; + [NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:NO attributes:@{} error:&error]; + [self loadPath]; + return error.localizedDescription; + }]; +} + +- (void)signTweaksButtonTapped { + [self showConfirmationDialogTitle:@"Re-sign tweaks" + message:@"Continue will re-sign all files in this folder." + destructive:NO + confirmButtonTitle:@"OK" + handler:^(UIAlertAction *action) { + if (action.style == UIAlertActionStyleCancel) return; + [self signFilesInFolder:self.path completionHandler:nil]; + }]; +} + +- (void)signFilesInFolder:(NSString *)path completionHandler:(void(^)(BOOL success))handler { + NSString *codesignPath = [path stringByAppendingPathComponent:@"_CodeSignature"]; + NSString *tmpExecPath = [path stringByAppendingPathComponent:@"LiveContainer.tmp"]; + NSString *tmpInfoPath = [path stringByAppendingPathComponent:@"Info.plist"]; + [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; + NSMutableDictionary *info = NSBundle.mainBundle.infoDictionary.mutableCopy; + info[@"CFBundleExecutable"] = @"LiveContainer.tmp"; + [info writeToFile:tmpInfoPath atomically:YES]; + + __block NSProgress *progress = [LCUtils signAppBundle:[NSURL fileURLWithPath:path] + completionHandler:^(BOOL success, NSError *_Nullable signError) { + dispatch_async(dispatch_get_main_queue(), ^{ + // Cleanup + self.progressView.progress = 0; + [NSFileManager.defaultManager removeItemAtPath:codesignPath error:nil]; + [NSFileManager.defaultManager removeItemAtPath:tmpExecPath error:nil]; + [NSFileManager.defaultManager removeItemAtPath:tmpInfoPath error:nil]; + + if (handler) { + handler(signError == nil); + } + if (signError) { + [self showDialogTitle:@"Error while signing tweaks" message:signError.localizedDescription]; + } + + [progress removeObserver:self forKeyPath:@"fractionCompleted"]; + self.progressView.hidden = YES; + self.signButton.enabled = YES; + [self loadPath]; + }); + }]; + + if (progress) { + self.progressView.hidden = NO; + self.signButton.enabled = NO; + [progress addObserver:self forKeyPath:@"fractionCompleted" options:NSKeyValueObservingOptionNew context:nil]; + } +} + +- (UIAction *)destructiveActionWithTitle:(NSString *)title image:(UIImage *)image handler:(id)handler { + UIAction *action = [UIAction + actionWithTitle:title + image:image + identifier:nil + handler:handler]; + action.attributes = UIMenuElementAttributesDestructive; + return action; +} + +#pragma mark UITableViewDelegate +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +/* +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + return @"N items"; +} +*/ + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.objects.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"Cell"]; + } + + UIListContentConfiguration *config = cell.defaultContentConfiguration; + config.text = self.objects[indexPath.row]; + + BOOL isDir; + NSString *path = [self.path stringByAppendingPathComponent:self.objects[indexPath.row]]; + [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir]; + config.image = [UIImage systemImageNamed:(isDir ? @"folder.fill" : @"doc")]; + + if (isDir) { + config.secondaryText = @"folder"; + } else { + NSDictionary *attrs = [NSFileManager.defaultManager attributesOfItemAtPath:path error:nil]; + NSNumber *size = attrs[NSFileSize]; + config.secondaryText = [NSByteCountFormatter stringFromByteCount:size.unsignedLongLongValue + countStyle:NSByteCountFormatterCountStyleFile]; + } + + cell.contentConfiguration = config; + cell.selectionStyle = isDir ? + UITableViewCellSelectionStyleDefault : + UITableViewCellSelectionStyleNone; + return cell; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + if (cell.selectionStyle == UITableViewCellSelectionStyleNone) { + return nil; + } else { + return indexPath; + } +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + NSString *name = self.objects[indexPath.row]; + LCTweakListViewController *childVC = [LCTweakListViewController new]; + childVC.path = [self.path stringByAppendingPathComponent:name]; + [self.navigationController pushViewController:childVC animated:YES]; +} + +- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point { + NSArray *menuItems = @[ + [UIAction + actionWithTitle:@"Rename" + image:[UIImage systemImageNamed:@"pencil"] + identifier:nil + handler:^(UIAction *action) { + [self renameItemAtIndexPath:indexPath]; + }], + [self + destructiveActionWithTitle:@"Delete" + image:[UIImage systemImageNamed:@"trash"] + handler:^(UIAction *action) { + [self deleteItemAtIndexPath:indexPath completionHandler:nil]; + }] + ]; + + return [UIContextMenuConfiguration + configurationWithIdentifier:nil + previewProvider:nil + actionProvider:^UIMenu *(NSArray *suggestedActions) { + return [UIMenu menuWithTitle:self.objects[indexPath.row] children:menuItems]; + }]; +} + +- (void)deleteItemAtIndexPath:(NSIndexPath *)indexPath completionHandler:(void(^)(BOOL actionPerformed))handler { + NSString *name = self.objects[indexPath.row]; + NSString *path = [self.path stringByAppendingPathComponent:name]; + [self showConfirmationDialogTitle:@"Confirm" + message:[NSString stringWithFormat:@"Are you sure you want to delete %@?", name] + destructive:YES + confirmButtonTitle:@"Delete" + handler:^(UIAlertAction * action) { + if (action.style != UIAlertActionStyleCancel) { + NSError *error = nil; + [NSFileManager.defaultManager removeItemAtPath:path error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + } else { + [self.objects removeObjectAtIndex:indexPath.row]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } + } + if (handler) handler(YES); + }]; +} + +- (void)renameItemAtIndexPath:(NSIndexPath *)indexPath { + [self showInputDialogTitle:@"Rename" message:@"Enter name" placeholder:self.objects[indexPath.row] callback:^(NSString *name) { + NSError *error; + NSString *fromPath = [self.path stringByAppendingPathComponent:self.objects[indexPath.row]]; + NSString *toPath = [self.path stringByAppendingPathComponent:name]; + [NSFileManager.defaultManager moveItemAtPath:fromPath toPath:toPath error:&error]; + [self loadPath]; + return error.localizedDescription; + }]; +} + +- (UISwipeActionsConfiguration *) tableView:(UITableView *)tableView +trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { + return [UISwipeActionsConfiguration configurationWithActions:@[ + [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive + title:@"Delete" handler:^(UIContextualAction *action, __kindof UIView *sourceView, void (^completionHandler)(BOOL actionPerformed)) { + [self deleteItemAtIndexPath:indexPath completionHandler:completionHandler]; + }] + ]]; +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { + NSError *error; + NSString *path = self.path; + if (LCUtils.certificatePassword) { + // Move them to a tmp folder to sign them + path = [self.path stringByAppendingPathComponent:@".tmp"]; + [NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:NO attributes:@{} error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + return; + } + } + + for (NSURL *url in urls) { + NSString *filePath = [path stringByAppendingPathComponent:url.path.lastPathComponent]; + [NSFileManager.defaultManager moveItemAtPath:url.path toPath:filePath error:&error]; + if (error) { + [self showDialogTitle:@"Error" message:error.localizedDescription]; + return; + } + } + + if (!LCUtils.certificatePassword) { + // JIT stop here + return; + } + + // Setup a fake app bundle for signing + [self signFilesInFolder:path completionHandler:^(BOOL success){ + if (success) { + // Move tweaks back + for (NSURL *url in urls) { + NSString *fromPath = [path stringByAppendingPathComponent:url.path.lastPathComponent]; + NSString *toPath = [self.path stringByAppendingPathComponent:url.path.lastPathComponent]; + [NSFileManager.defaultManager moveItemAtPath:fromPath toPath:toPath error:nil]; + } + } + + // Remove tmp folder + [NSFileManager.defaultManager removeItemAtPath:path error:nil]; + }]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:@"fractionCompleted"]) { + NSProgress *progress = (NSProgress *)object; + dispatch_async(dispatch_get_main_queue(), ^{ + self.progressView.progress = progress.fractionCompleted; + }); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +@end diff --git a/LCUtils.m b/LCUtils.m index 13dadab..2b67737 100644 --- a/LCUtils.m +++ b/LCUtils.m @@ -36,6 +36,9 @@ + (void)setCertificateData:(NSData *)certData { } + (NSData *)certificateDataFile { + if ([NSUserDefaults.standardUserDefaults boolForKey:@"LCIgnoreALTCertificate"]) { + return nil; + } NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:self.appGroupID]; NSURL *url = [appGroupPath URLByAppendingPathComponent:@"Apps/com.SideStore.SideStore/App.app/ALTCertificate.p12"]; return [NSData dataWithContentsOfURL:url]; diff --git a/Makefile b/Makefile index b2ac59b..cc45890 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ CONFIG_BRANCH = $(shell git branch --show-current) CONFIG_COMMIT = $(shell git log --oneline | sed '2,10000000d' | cut -b 1-7) # Build the UI library -LiveContainerUI_FILES = LCAppDelegate.m LCJITLessSetupViewController.m LCMachOUtils.m LCAppListViewController.m LCSettingsListController.m LCTabBarController.m LCUtils.m MBRoundProgressView.m UIViewController+LCAlert.m unarchive.m LCAppInfo.m +LiveContainerUI_FILES = LCAppDelegate.m LCJITLessSetupViewController.m LCMachOUtils.m LCAppListViewController.m LCSettingsListController.m LCTabBarController.m LCTweakListViewController.m LCUtils.m MBRoundProgressView.m UIViewController+LCAlert.m unarchive.m LCAppInfo.m LiveContainerUI_CFLAGS = \ -fobjc-arc \ -DCONFIG_TYPE=\"$(CONFIG_TYPE)\" \ diff --git a/Resources/Root.plist b/Resources/Root.plist index 11e1f70..4e628da 100644 --- a/Resources/Root.plist +++ b/Resources/Root.plist @@ -22,6 +22,24 @@ label Setup JIT-less + + cell + PSGroupCell + footerText + If you see frequent re-sign, enable this option. + + + cell + PSSwitchCell + default + + defaults + com.kdt.livecontainer + key + LCIgnoreALTCertificate + label + Ignore ALTCertificate.p12 + cell PSGroupCell diff --git a/TweakLoader.m b/TweakLoader.m index 93ad330..1e83a8a 100644 --- a/TweakLoader.m +++ b/TweakLoader.m @@ -10,12 +10,15 @@ static void TweakLoaderConstructor() { NSString *tweakFolder = @(tweakFolderC); unsetenv("LC_TWEAK_FOLDER"); - NSArray *tweaks = [[NSFileManager.defaultManager contentsOfDirectoryAtPath:tweakFolder error:nil] - filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) { - return [object hasSuffix:@".dylib"]; - }]]; - for(NSString *tweak in tweaks) { - NSString *tweakPath = [tweakFolder stringByAppendingPathComponent:tweak]; + NSURL *tweakFolderURL = [NSURL fileURLWithPath:tweakFolder]; + NSDirectoryEnumerator *directoryEnumerator = [NSFileManager.defaultManager enumeratorAtURL:tweakFolderURL includingPropertiesForKeys:@[] options:0 errorHandler:^BOOL(NSURL *url, NSError *error) { + NSLog(@"Error while enumerating tweak directory: %@", error); + return YES; + }]; + for (NSURL *fileURL in directoryEnumerator) { + NSString *tweakPath = fileURL.path; + NSString *tweak = tweak.lastPathComponent; + if (![tweakPath hasSuffix:@".dylib"]) continue; void *handle = dlopen(tweakPath.UTF8String, RTLD_LAZY | RTLD_GLOBAL); const char *error = dlerror(); if(handle) { diff --git a/UIViewController+LCAlert.h b/UIViewController+LCAlert.h index 357684f..952d65b 100644 --- a/UIViewController+LCAlert.h +++ b/UIViewController+LCAlert.h @@ -4,6 +4,7 @@ - (void)showDialogTitle:(NSString *)title message:(NSString *)message; - (void)showDialogTitle:(NSString *)title message:(NSString *)message handler:(void(^)(UIAlertAction *))handler; +- (void)showConfirmationDialogTitle:(NSString *)title message:(NSString *)message destructive:(BOOL)destructive confirmButtonTitle:(NSString *)confirmBtnTitle handler:(void(^)(UIAlertAction *))handler; - (void)showInputDialogTitle:(NSString *)title message:(NSString *)message placeholder:(NSString *)placeholder callback:(NSString *(^)(NSString *inputText))callback; @end diff --git a/UIViewController+LCAlert.m b/UIViewController+LCAlert.m index 2088dd0..6fcbd89 100644 --- a/UIViewController+LCAlert.m +++ b/UIViewController+LCAlert.m @@ -22,6 +22,15 @@ - (void)showDialogTitle:(NSString *)title message:(NSString *)message handler:(v [self presentViewController:alert animated:YES completion:nil]; } +- (void)showConfirmationDialogTitle:(NSString *)title message:(NSString *)message destructive:(BOOL)destructive confirmButtonTitle:(NSString *)confirmBtnTitle handler:(void(^)(UIAlertAction *))handler { + UIAlertController* alert = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:confirmBtnTitle style:(destructive ? UIAlertActionStyleDestructive : UIAlertActionStyleDefault) handler:handler]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:handler]]; + [self presentViewController:alert animated:YES completion:nil]; +} + - (void)showInputDialogTitle:(NSString *)title message:(NSString *)message placeholder:(NSString *)placeholder callback:(NSString *(^)(NSString *inputText))callback { UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { diff --git a/control b/control index 7eeb81e..3737c4d 100644 --- a/control +++ b/control @@ -1,6 +1,6 @@ Package: com.kdt.livecontainer Name: livecontainer -Version: 2.0 +Version: 2.1 Architecture: iphoneos-arm Description: Run iOS app without actually installing it! Maintainer: khanhduytran0