mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-08-22 02:38:45 -04:00
443 lines
20 KiB
Plaintext
443 lines
20 KiB
Plaintext
#import <rootless.h>
|
|
#import "../YouTubeHeader/YTAlertView.h"
|
|
#import "../YouTubeHeader/YTAppDelegate.h"
|
|
#import "../YouTubeHeader/YTCommonUtils.h"
|
|
#import "../YouTubeHeader/YTUIUtils.h"
|
|
#import "../YouTubeHeader/YTVersionUtils.h"
|
|
#import "../YouTubeHeader/YTGlobalConfig.h"
|
|
#import "../YouTubeHeader/YTColdConfig.h"
|
|
#import "../YouTubeHeader/YTHotConfig.h"
|
|
#import "../YouTubeHeader/YTSettingsSectionItem.h"
|
|
#import "../YouTubeHeader/YTSettingsSectionItemManager.h"
|
|
#import "../YouTubeHeader/YTSettingsPickerViewController.h"
|
|
#import "../YouTubeHeader/YTSettingsViewController.h"
|
|
#import "../YouTubeHeader/YTSearchableSettingsViewController.h"
|
|
#import "../YouTubeHeader/YTToastResponderEvent.h"
|
|
|
|
#define Prefix @"YTABC"
|
|
#define EnabledKey @"EnabledYTABC"
|
|
#define GroupedKey @"GroupedYTABC"
|
|
#define INCLUDED_CLASSES @"Included classes: YTGlobalConfig, YTColdConfig, YTHotConfig"
|
|
#define EXCLUDED_METHODS @"Excluded settings: android*, amsterdam*, musicClient* and unplugged*"
|
|
|
|
#define _LOC(b, x) [b localizedStringForKey:x value:nil table:nil]
|
|
#define LOC(x) _LOC(tweakBundle, x)
|
|
|
|
static const NSInteger YTABCSection = 404;
|
|
|
|
@interface YTSettingsSectionItemManager (YTABConfig)
|
|
- (void)updateYTABCSectionWithEntry:(id)entry;
|
|
@end
|
|
|
|
NSMutableDictionary <NSString *, NSMutableDictionary <NSString *, NSNumber *> *> *cache;
|
|
NSUserDefaults *defaults;
|
|
NSArray <NSString *> *allKeys;
|
|
|
|
static BOOL tweakEnabled() {
|
|
return [defaults boolForKey:EnabledKey];
|
|
}
|
|
|
|
static BOOL groupedSettings() {
|
|
return [defaults boolForKey:GroupedKey];
|
|
}
|
|
|
|
static NSString *getKey(NSString *method, NSString *classKey) {
|
|
return [NSString stringWithFormat:@"%@.%@.%@", Prefix, classKey, method];
|
|
}
|
|
|
|
static NSString *getCacheKey(NSString *method, NSString *classKey) {
|
|
return [NSString stringWithFormat:@"%@.%@", classKey, method];
|
|
}
|
|
|
|
static BOOL getValue(NSString *methodKey) {
|
|
if (![allKeys containsObject:methodKey])
|
|
return [[cache valueForKeyPath:[methodKey substringFromIndex:Prefix.length + 1]] boolValue];
|
|
return [defaults boolForKey:methodKey];
|
|
}
|
|
|
|
static void setValue(NSString *method, NSString *classKey, BOOL value) {
|
|
[cache setValue:@(value) forKeyPath:getCacheKey(method, classKey)];
|
|
[defaults setBool:value forKey:getKey(method, classKey)];
|
|
}
|
|
|
|
static void updateAllKeys() {
|
|
allKeys = [defaults dictionaryRepresentation].allKeys;
|
|
}
|
|
|
|
static BOOL returnFunction(id const self, SEL _cmd) {
|
|
NSString *method = NSStringFromSelector(_cmd);
|
|
NSString *methodKey = getKey(method, NSStringFromClass([self class]));
|
|
return getValue(methodKey);
|
|
}
|
|
|
|
static BOOL getValueFromInvocation(id target, SEL selector) {
|
|
NSInvocationOperation *i = [[NSInvocationOperation alloc] initWithTarget:target selector:selector object:nil];
|
|
[i start];
|
|
BOOL result = NO;
|
|
[i.result getValue:&result];
|
|
return result;
|
|
}
|
|
|
|
NSBundle *YTABCBundle() {
|
|
static NSBundle *bundle = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
NSString *tweakBundlePath = [[NSBundle mainBundle] pathForResource:@"YTABC" ofType:@"bundle"];
|
|
if (tweakBundlePath)
|
|
bundle = [NSBundle bundleWithPath:tweakBundlePath];
|
|
else
|
|
bundle = [NSBundle bundleWithPath:ROOT_PATH_NS(@"/Library/Application Support/YTABC.bundle")];
|
|
});
|
|
return bundle;
|
|
}
|
|
|
|
%group Search
|
|
|
|
%hook YTSettingsViewController
|
|
|
|
- (void)loadWithModel:(id)model fromView:(UIView *)view {
|
|
%orig;
|
|
if ([[self valueForKey:@"_detailsCategoryID"] integerValue] == YTABCSection)
|
|
MSHookIvar<BOOL>(self, "_shouldShowSearchBar") = YES;
|
|
}
|
|
|
|
- (void)setSectionControllers {
|
|
%orig;
|
|
if (MSHookIvar<BOOL>(self, "_shouldShowSearchBar")) {
|
|
YTSettingsSectionController *settingsSectionController = [self settingsSectionControllers][[self valueForKey:@"_detailsCategoryID"]];
|
|
if (settingsSectionController) {
|
|
YTSearchableSettingsViewController *searchableVC = [self valueForKey:@"_searchableSettingsViewController"];
|
|
[searchableVC storeCollectionViewSections:@[settingsSectionController]];
|
|
}
|
|
}
|
|
}
|
|
|
|
%end
|
|
|
|
%end
|
|
|
|
%hook YTSettingsSectionController
|
|
|
|
- (void)setSelectedItem:(NSUInteger)selectedItem {
|
|
if (selectedItem != NSNotFound) %orig;
|
|
}
|
|
|
|
%end
|
|
|
|
%hook YTAppSettingsPresentationData
|
|
|
|
+ (NSArray *)settingsCategoryOrder {
|
|
NSArray *order = %orig;
|
|
NSMutableArray *mutableOrder = [order mutableCopy];
|
|
[mutableOrder insertObject:@(YTABCSection) atIndex:0];
|
|
return mutableOrder;
|
|
}
|
|
|
|
%end
|
|
|
|
static NSString *getCategory(char c, NSString *method) {
|
|
if (c == 'e') {
|
|
if ([method hasPrefix:@"elements"]) return @"elements";
|
|
if ([method hasPrefix:@"enable"]) return @"enable";
|
|
}
|
|
if (c == 'i') {
|
|
if ([method hasPrefix:@"ios"]) return @"ios";
|
|
if ([method hasPrefix:@"is"]) return @"is";
|
|
}
|
|
if (c == 's') {
|
|
if ([method hasPrefix:@"shorts"]) return @"shorts";
|
|
if ([method hasPrefix:@"should"]) return @"should";
|
|
}
|
|
unichar uc = (unichar)c;
|
|
return [NSString stringWithCharacters:&uc length:1];;
|
|
}
|
|
|
|
%hook YTSettingsSectionItemManager
|
|
|
|
%new(v@:@)
|
|
- (void)updateYTABCSectionWithEntry:(id)entry {
|
|
NSMutableArray *sectionItems = [NSMutableArray array];
|
|
int totalSettings = 0;
|
|
NSBundle *tweakBundle = YTABCBundle();
|
|
BOOL isPhone = ![%c(YTCommonUtils) isIPad];
|
|
NSString *yesText = _LOC([NSBundle mainBundle], @"settings.yes");
|
|
NSString *cancelText = _LOC([NSBundle mainBundle], @"confirm.cancel");
|
|
NSString *deleteText = _LOC([NSBundle mainBundle], @"search.action.delete");
|
|
Class YTSettingsSectionItemClass = %c(YTSettingsSectionItem);
|
|
Class YTAlertViewClass = %c(YTAlertView);
|
|
if (tweakEnabled()) {
|
|
NSMutableDictionary <NSString *, NSMutableArray <YTSettingsSectionItem *> *> *properties = [NSMutableDictionary dictionary];
|
|
for (NSString *classKey in cache) {
|
|
for (NSString *method in cache[classKey]) {
|
|
char c = tolower([method characterAtIndex:0]);
|
|
NSString *category = getCategory(c, method);
|
|
if (![properties objectForKey:category]) properties[category] = [NSMutableArray array];
|
|
updateAllKeys();
|
|
BOOL modified = [allKeys containsObject:getKey(method, classKey)];
|
|
NSString *modifiedTitle = modified ? [NSString stringWithFormat:@"%@ *", method] : method;
|
|
YTSettingsSectionItem *methodSwitch = [YTSettingsSectionItemClass switchItemWithTitle:modifiedTitle
|
|
titleDescription:isPhone && method.length > 26 ? modifiedTitle : nil
|
|
accessibilityIdentifier:nil
|
|
switchOn:getValue(getKey(method, classKey))
|
|
switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
|
|
setValue(method, classKey, enabled);
|
|
return YES;
|
|
}
|
|
selectBlock:^BOOL (YTSettingsCell *cell, NSUInteger arg1) {
|
|
NSString *content = [NSString stringWithFormat:@"%@.%@", classKey, method];
|
|
YTAlertView *alertView = [YTAlertViewClass confirmationDialog];
|
|
alertView.title = method;
|
|
alertView.subtitle = content;
|
|
[alertView addTitle:LOC(@"COPY_TO_CLIPBOARD") withAction:^{
|
|
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
|
pasteboard.string = content;
|
|
[[%c(YTToastResponderEvent) eventWithMessage:LOC(@"COPIED_TO_CLIPBOARD") firstResponder:[self parentResponder]] send];
|
|
}];
|
|
updateAllKeys();
|
|
NSString *key = getKey(method, classKey);
|
|
if ([allKeys containsObject:key]) {
|
|
[alertView addTitle:deleteText withAction:^{
|
|
[defaults removeObjectForKey:key];
|
|
updateAllKeys();
|
|
}];
|
|
}
|
|
[alertView addCancelButton:NULL];
|
|
[alertView show];
|
|
return NO;
|
|
}
|
|
settingItemId:0];
|
|
[properties[category] addObject:methodSwitch];
|
|
}
|
|
}
|
|
YTSettingsViewController *settingsViewController = [self valueForKey:@"_settingsViewControllerDelegate"];
|
|
BOOL grouped = groupedSettings();
|
|
for (NSString *category in properties) {
|
|
NSMutableArray <YTSettingsSectionItem *> *rows = properties[category];
|
|
totalSettings += rows.count;
|
|
if (grouped) {
|
|
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
|
|
[rows sortUsingDescriptors:@[sort]];
|
|
NSString *shortTitle = [NSString stringWithFormat:@"\"%@\" (%ld)", category, rows.count];
|
|
NSString *title = [NSString stringWithFormat:@"%@ %@", LOC(@"SETTINGS_START_WITH"), shortTitle];
|
|
YTSettingsSectionItem *sectionItem = [YTSettingsSectionItemClass itemWithTitle:title accessibilityIdentifier:nil detailTextBlock:nil selectBlock:^BOOL (YTSettingsCell *cell, NSUInteger arg1) {
|
|
YTSettingsPickerViewController *picker = [[%c(YTSettingsPickerViewController) alloc] initWithNavTitle:shortTitle pickerSectionTitle:nil rows:rows selectedItemIndex:NSNotFound parentResponder:[self parentResponder]];
|
|
[settingsViewController pushViewController:picker];
|
|
return YES;
|
|
}];
|
|
[sectionItems addObject:sectionItem];
|
|
} else {
|
|
[sectionItems addObjectsFromArray:rows];
|
|
}
|
|
}
|
|
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES];
|
|
[sectionItems sortUsingDescriptors:@[sort]];
|
|
YTSettingsSectionItem *copyAll = [YTSettingsSectionItemClass itemWithTitle:LOC(@"COPY_CURRENT_SETTINGS")
|
|
titleDescription:LOC(@"COPY_CURRENT_SETTINGS_DESC")
|
|
accessibilityIdentifier:nil
|
|
detailTextBlock:nil
|
|
selectBlock:^BOOL (YTSettingsCell *cell, NSUInteger arg1) {
|
|
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
|
NSMutableArray *content = [NSMutableArray array];
|
|
for (NSString *classKey in cache) {
|
|
[cache[classKey] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *value, BOOL* stop) {
|
|
[content addObject:[NSString stringWithFormat:@"%@: %d", key, [value boolValue]]];
|
|
}];
|
|
}
|
|
[content sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
|
|
[content insertObject:[NSString stringWithFormat:@"Device model: %@", [%c(YTCommonUtils) hardwareModel]] atIndex:0];
|
|
[content insertObject:[NSString stringWithFormat:@"App version: %@", [%c(YTVersionUtils) appVersion]] atIndex:0];
|
|
[content insertObject:EXCLUDED_METHODS atIndex:0];
|
|
[content insertObject:INCLUDED_CLASSES atIndex:0];
|
|
[content insertObject:[NSString stringWithFormat:@"YTABConfig version: %@", @(OS_STRINGIFY(TWEAK_VERSION))] atIndex:0];
|
|
pasteboard.string = [content componentsJoinedByString:@"\n"];
|
|
[[%c(YTToastResponderEvent) eventWithMessage:LOC(@"COPIED_TO_CLIPBOARD") firstResponder:[self parentResponder]] send];
|
|
return YES;
|
|
}];
|
|
[sectionItems insertObject:copyAll atIndex:0];
|
|
YTSettingsSectionItem *modified = [YTSettingsSectionItemClass itemWithTitle:LOC(@"VIEW_MODIFIED_SETTINGS")
|
|
titleDescription:LOC(@"VIEW_MODIFIED_SETTINGS_DESC")
|
|
accessibilityIdentifier:nil
|
|
detailTextBlock:nil
|
|
selectBlock:^BOOL (YTSettingsCell *cell, NSUInteger arg1) {
|
|
NSMutableArray *features = [NSMutableArray array];
|
|
updateAllKeys();
|
|
for (NSString *key in allKeys) {
|
|
if ([key hasPrefix:Prefix]) {
|
|
NSString *displayKey = [key substringFromIndex:Prefix.length + 1];
|
|
[features addObject:[NSString stringWithFormat:@"%@: %d", displayKey, [defaults boolForKey:key]]];
|
|
}
|
|
}
|
|
[features sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
|
|
[features insertObject:[NSString stringWithFormat:LOC(@"TOTAL_MODIFIED_SETTINGS"), features.count] atIndex:0];
|
|
NSString *content = [features componentsJoinedByString:@"\n"];
|
|
YTAlertView *alertView = [YTAlertViewClass confirmationDialogWithAction:^{
|
|
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
|
pasteboard.string = content;
|
|
[[%c(YTToastResponderEvent) eventWithMessage:LOC(@"COPIED_TO_CLIPBOARD") firstResponder:[self parentResponder]] send];
|
|
} actionTitle:LOC(@"COPY_TO_CLIPBOARD")];
|
|
alertView.title = LOC(@"MODIFIED_SETTINGS_TITLE");
|
|
alertView.subtitle = content;
|
|
[alertView show];
|
|
return YES;
|
|
}];
|
|
[sectionItems insertObject:modified atIndex:0];
|
|
YTSettingsSectionItem *reset = [YTSettingsSectionItemClass itemWithTitle:LOC(@"RESET_KILL")
|
|
titleDescription:LOC(@"RESET_KILL_DESC")
|
|
accessibilityIdentifier:nil
|
|
detailTextBlock:nil
|
|
selectBlock:^BOOL (YTSettingsCell *cell, NSUInteger arg1) {
|
|
YTAlertView *alertView = [YTAlertViewClass confirmationDialogWithAction:^{
|
|
updateAllKeys();
|
|
for (NSString *key in allKeys) {
|
|
if ([key hasPrefix:Prefix])
|
|
[defaults removeObjectForKey:key];
|
|
}
|
|
exit(0);
|
|
} actionTitle:yesText];
|
|
alertView.title = LOC(@"WARNING");
|
|
alertView.subtitle = LOC(@"APPLY_DESC");
|
|
[alertView show];
|
|
return YES;
|
|
}];
|
|
[sectionItems insertObject:reset atIndex:0];
|
|
YTSettingsSectionItem *group = [YTSettingsSectionItemClass switchItemWithTitle:LOC(@"GROUPED")
|
|
titleDescription:nil
|
|
accessibilityIdentifier:nil
|
|
switchOn:groupedSettings()
|
|
switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
|
|
YTAlertView *alertView = [YTAlertViewClass confirmationDialogWithAction:^{
|
|
[defaults setBool:enabled forKey:GroupedKey];
|
|
exit(0);
|
|
}
|
|
actionTitle:yesText
|
|
cancelAction:^{
|
|
[cell setSwitchOn:!enabled animated:YES];
|
|
}
|
|
cancelTitle:cancelText];
|
|
alertView.title = LOC(@"WARNING");
|
|
alertView.subtitle = LOC(@"APPLY_DESC");
|
|
[alertView show];
|
|
return YES;
|
|
}
|
|
settingItemId:0];
|
|
[sectionItems insertObject:group atIndex:0];
|
|
}
|
|
YTSettingsSectionItem *thread = [YTSettingsSectionItemClass itemWithTitle:LOC(@"OPEN_MEGATHREAD")
|
|
titleDescription:LOC(@"OPEN_MEGATHREAD_DESC")
|
|
accessibilityIdentifier:nil
|
|
detailTextBlock:nil
|
|
selectBlock:^BOOL (YTSettingsCell *cell, NSUInteger arg1) {
|
|
return [%c(YTUIUtils) openURL:[NSURL URLWithString:@"https://github.com/PoomSmart/YTABConfig/discussions"]];
|
|
}];
|
|
[sectionItems insertObject:thread atIndex:0];
|
|
YTSettingsSectionItem *master = [YTSettingsSectionItemClass switchItemWithTitle:LOC(@"ENABLED")
|
|
titleDescription:LOC(@"ENABLED_DESC")
|
|
accessibilityIdentifier:nil
|
|
switchOn:tweakEnabled()
|
|
switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
|
|
[defaults setBool:enabled forKey:EnabledKey];
|
|
YTAlertView *alertView = [YTAlertViewClass confirmationDialogWithAction:^{ exit(0); }
|
|
actionTitle:yesText
|
|
cancelAction:^{
|
|
[cell setSwitchOn:!enabled animated:YES];
|
|
}
|
|
cancelTitle:cancelText];
|
|
alertView.title = LOC(@"WARNING");
|
|
alertView.subtitle = LOC(@"APPLY_DESC");
|
|
[alertView show];
|
|
return YES;
|
|
}
|
|
settingItemId:0];
|
|
[sectionItems insertObject:master atIndex:0];
|
|
YTSettingsViewController *delegate = [self valueForKey:@"_dataDelegate"];
|
|
[delegate setSectionItems:sectionItems
|
|
forCategory:YTABCSection
|
|
title:@"A/B"
|
|
titleDescription:tweakEnabled() ? [NSString stringWithFormat:@"YTABConfig %@, %d feature flags.", @(OS_STRINGIFY(TWEAK_VERSION)), totalSettings] : nil
|
|
headerHidden:NO];
|
|
}
|
|
|
|
- (void)updateSectionForCategory:(NSUInteger)category withEntry:(id)entry {
|
|
if (category == YTABCSection) {
|
|
[self updateYTABCSectionWithEntry:entry];
|
|
return;
|
|
}
|
|
%orig;
|
|
}
|
|
|
|
%end
|
|
|
|
static NSMutableArray <NSString *> *getBooleanMethods(Class clz) {
|
|
NSMutableArray *allMethods = [NSMutableArray array];
|
|
unsigned int methodCount = 0;
|
|
Method *methods = class_copyMethodList(clz, &methodCount);
|
|
for (unsigned int i = 0; i < methodCount; ++i) {
|
|
Method method = methods[i];
|
|
const char *name = sel_getName(method_getName(method));
|
|
if (strstr(name, "ndroid") || strstr(name, "musicClient") || strstr(name, "amsterdam") || strstr(name, "unplugged")) continue;
|
|
const char *encoding = method_getTypeEncoding(method);
|
|
if (strcmp(encoding, "B16@0:8")) continue;
|
|
NSString *selector = [NSString stringWithUTF8String:name];
|
|
if (![allMethods containsObject:selector])
|
|
[allMethods addObject:selector];
|
|
}
|
|
free(methods);
|
|
return allMethods;
|
|
}
|
|
|
|
static void hookClass(NSObject *instance) {
|
|
if (!instance) [NSException raise:@"hookClass Invalid argument exception" format:@"Hooking the class of a non-existing instance"];
|
|
Class instanceClass = [instance class];
|
|
NSMutableArray <NSString *> *methods = getBooleanMethods(instanceClass);
|
|
NSString *classKey = NSStringFromClass(instanceClass);
|
|
NSMutableDictionary *classCache = cache[classKey] = [NSMutableDictionary new];
|
|
for (NSString *method in methods) {
|
|
SEL selector = NSSelectorFromString(method);
|
|
BOOL result = getValueFromInvocation(instance, selector);
|
|
classCache[method] = @(result);
|
|
MSHookMessageEx(instanceClass, selector, (IMP)returnFunction, NULL);
|
|
}
|
|
}
|
|
|
|
%hook YTAppDelegate
|
|
|
|
- (BOOL)application:(id)arg1 didFinishLaunchingWithOptions:(id)arg2 {
|
|
defaults = [NSUserDefaults standardUserDefaults];
|
|
if (tweakEnabled()) {
|
|
updateAllKeys();
|
|
YTGlobalConfig *globalConfig;
|
|
YTColdConfig *coldConfig;
|
|
YTHotConfig *hotConfig;
|
|
@try {
|
|
globalConfig = [self valueForKey:@"_globalConfig"];
|
|
coldConfig = [self valueForKey:@"_coldConfig"];
|
|
hotConfig = [self valueForKey:@"_hotConfig"];
|
|
} @catch (id ex) {
|
|
id settings = [self valueForKey:@"_settings"];
|
|
globalConfig = [settings valueForKey:@"_globalConfig"];
|
|
coldConfig = [settings valueForKey:@"_coldConfig"];
|
|
hotConfig = [settings valueForKey:@"_hotConfig"];
|
|
}
|
|
hookClass(globalConfig);
|
|
hookClass(coldConfig);
|
|
hookClass(hotConfig);
|
|
if (!groupedSettings()) {
|
|
%init(Search);
|
|
}
|
|
}
|
|
return %orig;
|
|
}
|
|
|
|
%end
|
|
|
|
%ctor {
|
|
NSBundle *bundle = [NSBundle bundleWithPath:[NSString stringWithFormat:@"%@/Frameworks/Module_Framework.framework", [[NSBundle mainBundle] bundlePath]]];
|
|
if (!bundle.loaded) [bundle load];
|
|
cache = [NSMutableDictionary new];
|
|
%init;
|
|
}
|
|
|
|
%dtor {
|
|
[cache removeAllObjects];
|
|
}
|