├── .clang-format ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .vscode ├── AirDrop.shortcut ├── extensions.json ├── logos-format.py ├── settings.json └── tasks.json ├── ChannelManager.h ├── ChannelManager.m ├── Gonerino.plist ├── Makefile ├── README.md ├── Settings.h ├── Settings.x ├── Sideloading.x ├── Tweak.h ├── Tweak.x ├── Util.h ├── Util.m ├── VideoManager.h ├── VideoManager.m ├── WordManager.h ├── WordManager.m └── control /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 4 3 | ObjCBlockIndentWidth: 4 4 | UseTab: Never 5 | ColumnLimit: 120 6 | AccessModifierOffset: -4 7 | AllowShortBlocksOnASingleLine: Empty 8 | AllowShortFunctionsOnASingleLine: Empty 9 | AllowShortCaseLabelsOnASingleLine: false 10 | AllowShortEnumsOnASingleLine: false 11 | AllowShortIfStatementsOnASingleLine: Never 12 | AllowShortLambdasOnASingleLine: Empty 13 | AllowShortLoopsOnASingleLine: false 14 | AlignConsecutiveAssignments: Consecutive 15 | MaxEmptyLinesToKeep: 1 16 | ContinuationIndentWidth: 4 17 | IndentCaseLabels: true 18 | AlignTrailingComments: true 19 | BreakBeforeBraces: Attach 20 | ObjCSpaceAfterProperty: false 21 | ObjCSpaceBeforeProtocolList: false 22 | SpacesInContainerLiterals: false 23 | SpaceBeforeAssignmentOperators: true 24 | SpaceBeforeParens: ControlStatements 25 | SpaceInEmptyParentheses: false 26 | SpacesInSquareBrackets: false 27 | SpacesInParentheses: false 28 | SpaceAfterCStyleCast: false 29 | PointerAlignment: Right 30 | KeepEmptyLinesAtTheStartOfBlocks: false -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | GH_TOKEN: ${{ github.token }} 8 | 9 | jobs: 10 | build-tweak-and-release: 11 | runs-on: macos-14 12 | permissions: 13 | contents: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Prepare Theos 20 | uses: Randomblock1/theos-action@v1 21 | 22 | - name: Clone YouTubeHeader 23 | run: | 24 | git clone https://github.com/PoomSmart/YouTubeHeader $THEOS/include/YouTubeHeader 25 | 26 | - name: Build packages 27 | run: make package FINALPACKAGE=1 28 | 29 | - name: Extract Values 30 | run: | 31 | NAME=$(grep '^Name:' control | cut -d ' ' -f 2) 32 | echo "NAME=$NAME" >> $GITHUB_ENV 33 | PACKAGE=$(grep '^Package:' control | cut -d ' ' -f 2) 34 | VERSION=$(grep '^Version:' control | cut -d ' ' -f 2) 35 | echo "VERSION=$VERSION" >> $GITHUB_ENV 36 | ROOTLESS_DEB_FILE_NAME="${PACKAGE}_${VERSION}_iphoneos-arm64.deb" 37 | echo "ROOTLESS_DEB_FILE_NAME=$ROOTLESS_DEB_FILE_NAME" >> $GITHUB_ENV 38 | 39 | - name: Create GitHub Release 40 | id: create_release 41 | uses: softprops/action-gh-release@v2 42 | with: 43 | tag_name: v${{ env.VERSION }} 44 | files: | 45 | .theos/obj/${{ env.NAME }}.dylib 46 | packages/${{ env.ROOTLESS_DEB_FILE_NAME }} 47 | generate_release_notes: true 48 | fail_on_unmatched_files: true 49 | token: ${{ env.GITHUB_TOKEN }} 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Update Repository 54 | uses: actions/github-script@v7 55 | with: 56 | github-token: ${{ secrets.REPO_ACCESS_TOKEN }} 57 | script: | 58 | const assets = JSON.parse('${{ steps.create_release.outputs.assets }}'); 59 | const debAsset = assets.find(asset => asset.name === '${{ env.ROOTLESS_DEB_FILE_NAME }}'); 60 | 61 | if (!debAsset) { 62 | core.setFailed('Could not find DEB asset in release'); 63 | return; 64 | } 65 | 66 | await github.rest.repos.createDispatchEvent({ 67 | owner: 'castdrian', 68 | repo: 'apt-repo', 69 | event_type: 'package-update', 70 | client_payload: { 71 | package_url: debAsset.browser_download_url, 72 | package_name: '${{ env.ROOTLESS_DEB_FILE_NAME }}' 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .theos/ 2 | packages/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.vscode/AirDrop.shortcut: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/castdrian/Gonerino/84dde148be716fc47b1b8144f863ad4455b147ef/.vscode/AirDrop.shortcut -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "tale.logos-vscode", 4 | "spencerwmiles.vscode-task-buttons", 5 | "redhat.vscode-yaml", 6 | "davidanson.vscode-markdownlint", 7 | "SteefH.external-formatters" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/logos-format.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | from subprocess import Popen, PIPE, STDOUT 4 | 5 | # block level 6 | # hook -> replace with @logosformathook with ; at the end 7 | # end -> replace with @logosformatend with ; at the end 8 | # property -> replace with @logosformatproperty with NO ; at the end. Special case for block level 9 | # new -> replce with @logosformatnew with ; at the end 10 | # group -> replace with @logosformatgroup with ; at the end 11 | # subclass -> replace with @logosformatsubclass with ; at the end 12 | # top level 13 | # config -> replace with @logosformatconfig 14 | # hookf -> replace with @logosformathookf 15 | # ctor -> replace with @logosformatctor 16 | # dtor -> replace with @logosformatdtor 17 | 18 | # function level 19 | # init -> replace with @logosformatinit 20 | # c -> replace with @logosformatc 21 | # orig -> replace with @logosformatorig 22 | # log -> replace with @logosformatlog 23 | 24 | specialFilterList = ["%hook", "%end", "%new", "%group", "%subclass"] 25 | filterList = [ 26 | "%property", 27 | "%config", 28 | "%hookf", 29 | "%ctor", 30 | "%dtor", 31 | "%init", 32 | "%c", 33 | "%orig", 34 | "%log", 35 | ] 36 | 37 | 38 | fileContentsList = sys.stdin.read().splitlines() 39 | newList = [] 40 | 41 | for line in fileContentsList: 42 | for token in filterList: 43 | if token in line: 44 | line = re.sub(rf"%({token[1:]})\b", r"@logosformat\1", line) 45 | for token in specialFilterList: 46 | if token in line: 47 | line = re.sub(rf"%({token[1:]})\b", r"@logosformat\1", line) + ";" 48 | newList.append(line) 49 | 50 | command = ["clang-format"] + sys.argv[1:] 51 | process = Popen(command, stdout=PIPE, stderr=None, stdin=PIPE) 52 | stdoutData = process.communicate(input="\n".join(newList).encode())[0] 53 | refinedList = stdoutData.decode().splitlines() 54 | 55 | 56 | for line in refinedList: 57 | if "@logosformat" in line: 58 | fix = line.replace("@logosformat", "%") 59 | if any(token in fix for token in specialFilterList): 60 | print(fix.replace(";","")) 61 | else: 62 | print(fix) 63 | else: 64 | print(line) 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD029": false, 4 | "MD033": false, 5 | "MD045": false, 6 | }, 7 | "VsCodeTaskButtons.tasks": [ 8 | { 9 | "label": "$(tools) Build Tweak", 10 | "task": "Build Tweak", 11 | "tooltip": "Build Tweak" 12 | }, 13 | { 14 | "label": "$(cloud-upload) AirDrop Tweak", 15 | "task": "AirDrop Tweak", 16 | "tooltip": "AirDrop Tweak" 17 | } 18 | ], 19 | "externalFormatters.languages": { 20 | "logos": { 21 | "command": "python3", 22 | "arguments": [ 23 | ".vscode/logos-format.py", 24 | "--assume-filename", 25 | "objc" 26 | ] 27 | } 28 | }, 29 | "yaml.schemas": { 30 | "https://json.schemastore.org/github-workflow.json": [ 31 | "*.github/workflows/*.yaml", 32 | "*.github/workflows/*.yml", 33 | ], 34 | "https://json.schemastore.org/github-action.json": [ 35 | "action.yaml", 36 | "action.yml", 37 | ] 38 | }, 39 | "editor.formatOnSave": true 40 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "label": "Build Tweak", 7 | "command": "zsh", 8 | "args": [ 9 | "-c", 10 | "rm -rf packages && make clean && make package" 11 | ], 12 | "problemMatcher": { 13 | "owner": "cpp", 14 | "pattern": { 15 | "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", 16 | "file": 1, 17 | "line": 2, 18 | "column": 3, 19 | "severity": 4, 20 | "message": 5 21 | } 22 | }, 23 | "group": { 24 | "kind": "build", 25 | "isDefault": true 26 | }, 27 | "presentation": { 28 | "panel": "shared", 29 | "showReuseMessage": false, 30 | "clear": true, 31 | "close": true 32 | } 33 | }, 34 | { 35 | "type": "shell", 36 | "label": "AirDrop Tweak", 37 | "command": "zsh", 38 | "args": [ 39 | "-c", 40 | "shortcuts run 'AirDrop' -i ./packages/*arm64.deb" 41 | ], 42 | "presentation": { 43 | "panel": "shared", 44 | "showReuseMessage": false, 45 | "clear": true, 46 | "close": true 47 | }, 48 | "dependsOn": [ 49 | "Build Tweak" 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /ChannelManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface ChannelManager : NSObject 4 | 5 | + (instancetype)sharedInstance; 6 | - (NSArray *)blockedChannels; 7 | - (void)addBlockedChannel:(NSString *)channelName; 8 | - (void)removeBlockedChannel:(NSString *)channelName; 9 | - (BOOL)isChannelBlocked:(NSString *)channelName; 10 | - (void)setBlockedChannels:(NSArray *)channels; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ChannelManager.m: -------------------------------------------------------------------------------- 1 | #import "ChannelManager.h" 2 | 3 | @interface ChannelManager () 4 | @property(nonatomic, strong) NSMutableSet *blockedChannelSet; 5 | @end 6 | 7 | @implementation ChannelManager 8 | 9 | + (instancetype)sharedInstance { 10 | static ChannelManager *instance = nil; 11 | static dispatch_once_t onceToken; 12 | dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); 13 | return instance; 14 | } 15 | 16 | - (instancetype)init { 17 | self = [super init]; 18 | if (self) { 19 | _blockedChannelSet = 20 | [[[NSUserDefaults standardUserDefaults] arrayForKey:@"GonerinoBlockedChannels"] mutableCopy] 21 | ?: [NSMutableSet set]; 22 | } 23 | return self; 24 | } 25 | 26 | - (NSArray *)blockedChannels { 27 | return [self.blockedChannelSet allObjects]; 28 | } 29 | 30 | - (void)addBlockedChannel:(NSString *)channelName { 31 | if (channelName.length > 0) { 32 | [self.blockedChannelSet addObject:channelName]; 33 | [self saveBlockedChannels]; 34 | } 35 | } 36 | 37 | - (void)removeBlockedChannel:(NSString *)channelName { 38 | if (channelName) { 39 | [self.blockedChannelSet removeObject:channelName]; 40 | [self saveBlockedChannels]; 41 | } 42 | } 43 | 44 | - (BOOL)isChannelBlocked:(NSString *)channelName { 45 | return [self.blockedChannelSet containsObject:channelName]; 46 | } 47 | 48 | - (void)saveBlockedChannels { 49 | [[NSUserDefaults standardUserDefaults] setObject:[self.blockedChannelSet allObjects] 50 | forKey:@"GonerinoBlockedChannels"]; 51 | [[NSUserDefaults standardUserDefaults] synchronize]; 52 | } 53 | 54 | - (void)setBlockedChannels:(NSArray *)channels { 55 | self.blockedChannelSet = [NSMutableSet setWithArray:channels]; 56 | [self saveBlockedChannels]; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Gonerino.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.google.ios.youtube" ); }; } 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:latest:14.0 2 | ARCHS = arm64 3 | INSTALL_TARGET_PROCESSES = YouTube 4 | THEOS_PACKAGE_SCHEME = rootless 5 | FINALPACKAGE = 1 6 | 7 | include $(THEOS)/makefiles/common.mk 8 | 9 | TWEAK_NAME = Gonerino 10 | 11 | Gonerino_FILES = $(wildcard *.x) $(wildcard *.m) 12 | Gonerino_FRAMEWORKS = UIKit Foundation UniformTypeIdentifiers MobileCoreServices 13 | Gonerino_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(shell grep '^Version:' control | cut -d' ' -f2)"' 14 | 15 | include $(THEOS_MAKE_PATH)/tweak.mk 16 | 17 | before-stage:: 18 | $(ECHO_NOTHING)find . -name ".DS_Store" -type f -delete$(ECHO_END) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gonerino 2 | 3 | A YouTube tweak that allows you to block specific channels and automatically remove their videos from your feed. 4 | 5 | ## Download 6 | 7 | - [apt repo](https://repo.adriancastro.dev) 8 | - [github release](https://github.com/castdrian/Gonerino/releases/latest) 9 | 10 | ## Features 11 | 12 | - Block channels directly from video context menu 13 | - Block specific videos without blocking the entire channel 14 | - Block videos via keywords 15 | - Automatically removes blocked videos from the home and search feeds 16 | - Block "People also watched this video" and "You might also like this" suggestions 17 | 18 | ## Compatibility 19 | 20 | | Category | Version/Method | Status | 21 | |----------|---------------|---------| 22 | | **iOS Version** | 15.8.3+ | ✅ | 23 | | **YouTube Version** | 19.42.1+ | ✅ | 24 | | **Injection Methods** | ElleKit (palera1n) (Dopamine) | ✅ | 25 | | | TrollFools (TrollStore) | ⚠️ | 26 | | | Sideloaded IPA (MobileSubstrate) | ✅ | 27 | | | LiveContainer (TweakLoader) | ⚠️ | 28 | 29 | ### Status Legend 30 | ✅ - Fully Working\ 31 | ⚠️ - Partially Working/Known Issues\ 32 | ❌ - Not Working 33 | 34 | ## Contributors 35 | 36 | [![Contributors](https://contrib.rocks/image?repo=castdrian/Gonerino)](https://github.com/castdrian/Gonerino/graphs/contributors) 37 | -------------------------------------------------------------------------------- /Settings.h: -------------------------------------------------------------------------------- 1 | #import "ChannelManager.h" 2 | #import "VideoManager.h" 3 | #import "WordManager.h" 4 | 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | #import 18 | #import 19 | 20 | NS_ASSUME_NONNULL_BEGIN 21 | 22 | #define SECTION_HEADER(s) \ 23 | [sectionItems addObject:[objc_getClass("YTSettingsSectionItem") \ 24 | itemWithTitle:@"\t" \ 25 | titleDescription:s \ 26 | accessibilityIdentifier:nil \ 27 | detailTextBlock:nil \ 28 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger sectionItemIndex) { \ 29 | return NO; \ 30 | }]] 31 | 32 | static const NSInteger GonerinoSection = 2002; 33 | 34 | #define TWEAK_VERSION PACKAGE_VERSION 35 | 36 | static BOOL isImportOperation = NO; 37 | 38 | @interface YTToastResponderEvent : NSObject 39 | + (instancetype)eventWithMessage:(NSString *)message firstResponder:(id)responder; 40 | - (void)send; 41 | @end 42 | 43 | @interface YTNavigationController : UINavigationController 44 | @end 45 | 46 | @interface YTSettingsViewController () 47 | @property(nonatomic, strong, readonly, nullable) YTNavigationController *navigationController; 48 | @end 49 | 50 | @interface YTSettingsViewController (Gonerino) 51 | - (void)setSectionItems:(nullable NSArray *)items 52 | forCategory:(NSInteger)category 53 | title:(nullable NSString *)title 54 | titleDescription:(nullable NSString *)titleDescription 55 | headerHidden:(BOOL)headerHidden; 56 | @end 57 | 58 | @interface YTSettingsSectionItemManager (Gonerino) 59 | - (void)updateGonerinoSectionWithEntry:(nullable id)entry; 60 | - (void)updateChannelManagementSection:(nonnull YTSettingsViewController *)viewController; 61 | - (nullable UITableView *)findTableViewInView:(nonnull UIView *)view; 62 | - (void)reloadGonerinoSection; 63 | @end 64 | 65 | NS_ASSUME_NONNULL_END -------------------------------------------------------------------------------- /Settings.x: -------------------------------------------------------------------------------- 1 | #import "Settings.h" 2 | 3 | %hook YTSettingsGroupData 4 | 5 | - (NSArray *)orderedCategories { 6 | if (self.type != 1) 7 | return %orig; 8 | NSMutableArray *mutableCategories = %orig.mutableCopy; 9 | [mutableCategories insertObject:@(GonerinoSection) atIndex:0]; 10 | return mutableCategories.copy; 11 | } 12 | 13 | %end 14 | 15 | %hook YTAppSettingsPresentationData 16 | 17 | + (NSArray *)settingsCategoryOrder { 18 | NSArray *order = %orig; 19 | NSMutableArray *mutableOrder = [order mutableCopy]; 20 | NSUInteger insertIndex = [order indexOfObject:@(1)]; 21 | if (insertIndex != NSNotFound) { 22 | [mutableOrder insertObject:@(GonerinoSection) atIndex:insertIndex + 1]; 23 | } 24 | return mutableOrder; 25 | } 26 | 27 | %end 28 | 29 | %hook YTSettingsSectionItemManager 30 | 31 | %new 32 | - (void)updateGonerinoSectionWithEntry:(id)entry { 33 | YTSettingsViewController *delegate = [self valueForKey:@"_settingsViewControllerDelegate"]; 34 | NSMutableArray *sectionItems = [NSMutableArray array]; 35 | 36 | SECTION_HEADER(@"Gonerino Settings"); 37 | 38 | YTSettingsSectionItem *shakeToggle = [%c(YTSettingsSectionItem) 39 | switchItemWithTitle:@"Enable Shake Gesture" 40 | titleDescription:@"Allow toggling Gonerino with shake gesture" 41 | accessibilityIdentifier:nil 42 | switchOn:[[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoShakeEnabled"] == nil 43 | ? NO 44 | : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoShakeEnabled"] 45 | switchBlock:^BOOL(YTSettingsCell *cell, BOOL enabled) { 46 | [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"GonerinoShakeEnabled"]; 47 | [[NSUserDefaults standardUserDefaults] synchronize]; 48 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 49 | [[%c(YTToastResponderEvent) 50 | eventWithMessage:[NSString stringWithFormat:@"Shake gesture %@", 51 | enabled ? @"enabled" : @"disabled"] 52 | firstResponder:settingsVC] send]; 53 | return YES; 54 | } 55 | settingItemId:0]; 56 | [sectionItems addObject:shakeToggle]; 57 | 58 | NSUInteger channelCount = [[ChannelManager sharedInstance] blockedChannels].count; 59 | YTSettingsSectionItem *manageChannels = [%c(YTSettingsSectionItem) 60 | itemWithTitle:@"Manage Channels" 61 | titleDescription:[NSString stringWithFormat:@"%lu blocked channel%@", (unsigned long)channelCount, 62 | channelCount == 1 ? @"" : @"s"] 63 | accessibilityIdentifier:nil 64 | detailTextBlock:nil 65 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 66 | NSMutableArray *rows = [NSMutableArray array]; 67 | 68 | [rows 69 | addObject: 70 | [%c(YTSettingsSectionItem) 71 | itemWithTitle:@"Add Channel" 72 | titleDescription:@"Block a new channel" 73 | accessibilityIdentifier:nil 74 | detailTextBlock:nil 75 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 76 | YTSettingsViewController *settingsVC = 77 | [self valueForKey:@"_settingsViewControllerDelegate"]; 78 | UIAlertController *alertController = [UIAlertController 79 | alertControllerWithTitle:@"Add Channel" 80 | message:@"Enter the " 81 | @"channel name to " 82 | @"block" 83 | preferredStyle:UIAlertControllerStyleAlert]; 84 | 85 | [alertController 86 | addTextFieldWithConfigurationHandler:^(UITextField *textField) { 87 | textField.placeholder = @"Channel Name"; 88 | }]; 89 | 90 | [alertController 91 | addAction: 92 | [UIAlertAction 93 | actionWithTitle:@"Add" 94 | style:UIAlertActionStyleDefault 95 | handler:^(UIAlertAction *action) { 96 | NSString *channelName = 97 | alertController.textFields.firstObject 98 | .text; 99 | if (channelName.length > 0) { 100 | [[ChannelManager sharedInstance] 101 | addBlockedChannel:channelName]; 102 | [self reloadGonerinoSection]; 103 | 104 | UIImpactFeedbackGenerator *generator = 105 | [[UIImpactFeedbackGenerator alloc] 106 | initWithStyle: 107 | UIImpactFeedbackStyleMedium]; 108 | [generator prepare]; 109 | [generator impactOccurred]; 110 | 111 | [[%c(YTToastResponderEvent) 112 | eventWithMessage: 113 | [NSString stringWithFormat: 114 | @"A" 115 | @"d" 116 | @"d" 117 | @"e" 118 | @"d" 119 | @" " 120 | @"%" 121 | @"@", 122 | channelName] 123 | firstResponder:settingsVC] send]; 124 | } 125 | }]]; 126 | 127 | [alertController 128 | addAction:[UIAlertAction 129 | actionWithTitle:@"Cancel" 130 | style:UIAlertActionStyleCancel 131 | handler:nil]]; 132 | 133 | [settingsVC presentViewController:alertController 134 | animated:YES 135 | completion:nil]; 136 | return YES; 137 | }]]; 138 | 139 | for (NSString *channelName in [[ChannelManager sharedInstance] blockedChannels]) { 140 | [rows 141 | addObject: 142 | [%c(YTSettingsSectionItem) 143 | itemWithTitle:channelName 144 | titleDescription:nil 145 | accessibilityIdentifier:nil 146 | detailTextBlock:nil 147 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 148 | YTSettingsViewController *settingsVC = 149 | [self valueForKey:@"_settingsViewControllerDelegate"]; 150 | UIAlertController *alertController = [UIAlertController 151 | alertControllerWithTitle:@"Delete Channel" 152 | message:[NSString 153 | stringWithFormat:@"Are you " 154 | @"sure " 155 | @"you " 156 | @"want to " 157 | @"delete " 158 | @"'%@'?", 159 | channelName] 160 | preferredStyle:UIAlertControllerStyleAlert]; 161 | 162 | [alertController 163 | addAction: 164 | [UIAlertAction 165 | actionWithTitle:@"Delete" 166 | style:UIAlertActionStyleDestructive 167 | handler:^(UIAlertAction *action) { 168 | [[ChannelManager sharedInstance] 169 | removeBlockedChannel:channelName]; 170 | [self reloadGonerinoSection]; 171 | 172 | UIImpactFeedbackGenerator *generator = 173 | [[UIImpactFeedbackGenerator alloc] 174 | initWithStyle: 175 | UIImpactFeedbackStyleMedium]; 176 | [generator prepare]; 177 | [generator impactOccurred]; 178 | 179 | [[%c(YTToastResponderEvent) 180 | eventWithMessage: 181 | [NSString stringWithFormat: 182 | @"D" 183 | @"e" 184 | @"l" 185 | @"e" 186 | @"t" 187 | @"e" 188 | @"d" 189 | @" " 190 | @"%" 191 | @"@", 192 | channelName] 193 | firstResponder:settingsVC] send]; 194 | }]]; 195 | 196 | [alertController 197 | addAction:[UIAlertAction 198 | actionWithTitle:@"Cancel" 199 | style:UIAlertActionStyleCancel 200 | handler:nil]]; 201 | 202 | [settingsVC presentViewController:alertController 203 | animated:YES 204 | completion:nil]; 205 | return YES; 206 | }]]; 207 | } 208 | 209 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 210 | YTSettingsPickerViewController *picker = [[%c(YTSettingsPickerViewController) alloc] 211 | initWithNavTitle:@"Manage Channels" 212 | pickerSectionTitle:nil 213 | rows:rows 214 | selectedItemIndex:NSNotFound 215 | parentResponder:[self parentResponder]]; 216 | 217 | if ([settingsVC respondsToSelector:@selector(navigationController)]) { 218 | UINavigationController *nav = settingsVC.navigationController; 219 | [nav pushViewController:picker animated:YES]; 220 | } 221 | return YES; 222 | }]; 223 | [sectionItems addObject:manageChannels]; 224 | 225 | NSUInteger videoCount = [[VideoManager sharedInstance] blockedVideos].count; 226 | YTSettingsSectionItem *manageVideos = [%c(YTSettingsSectionItem) 227 | itemWithTitle:@"Manage Videos" 228 | titleDescription:[NSString stringWithFormat:@"%lu blocked video%@", (unsigned long)videoCount, 229 | videoCount == 1 ? @"" : @"s"] 230 | accessibilityIdentifier:nil 231 | detailTextBlock:nil 232 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 233 | NSArray *blockedVideos = [[VideoManager sharedInstance] blockedVideos]; 234 | if (blockedVideos.count == 0) { 235 | YTSettingsViewController *settingsVC = 236 | [self valueForKey:@"_settingsViewControllerDelegate"]; 237 | [[%c(YTToastResponderEvent) eventWithMessage:@"No blocked videos" 238 | firstResponder:settingsVC] send]; 239 | return YES; 240 | } 241 | 242 | NSMutableArray *rows = [NSMutableArray array]; 243 | 244 | [rows addObject:[%c(YTSettingsSectionItem) 245 | itemWithTitle:@"\t" 246 | titleDescription:@"Blocked videos" 247 | accessibilityIdentifier:nil 248 | detailTextBlock:nil 249 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 250 | return NO; 251 | }]]; 252 | 253 | for (NSDictionary *videoInfo in blockedVideos) { 254 | [rows 255 | addObject: 256 | [%c(YTSettingsSectionItem) 257 | itemWithTitle:videoInfo[@"channel"] ?: @"Unknown Channel" 258 | titleDescription:videoInfo[@"title"] ?: @"Unknown Title" 259 | accessibilityIdentifier:nil 260 | detailTextBlock:nil 261 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 262 | YTSettingsViewController *settingsVC = 263 | [self valueForKey:@"_settingsViewControllerDelegate"]; 264 | UIAlertController *alertController = [UIAlertController 265 | alertControllerWithTitle:@"Delete Video" 266 | message:[NSString 267 | stringWithFormat: 268 | @"Are you sure you want " 269 | @"to delete '%@'?", 270 | videoInfo[@"title"]] 271 | preferredStyle:UIAlertControllerStyleAlert]; 272 | 273 | [alertController 274 | addAction: 275 | [UIAlertAction 276 | actionWithTitle:@"Delete" 277 | style:UIAlertActionStyleDestructive 278 | handler:^(UIAlertAction *action) { 279 | [[VideoManager sharedInstance] 280 | removeBlockedVideo:videoInfo 281 | [@"id"]]; 282 | [self reloadGonerinoSection]; 283 | 284 | UIImpactFeedbackGenerator *generator = 285 | [[UIImpactFeedbackGenerator alloc] 286 | initWithStyle: 287 | UIImpactFeedbackStyleMedium]; 288 | [generator prepare]; 289 | [generator impactOccurred]; 290 | 291 | [[%c(YTToastResponderEvent) 292 | eventWithMessage: 293 | [NSString 294 | stringWithFormat: 295 | @"Deleted %@", 296 | videoInfo[@"title"]] 297 | firstResponder:settingsVC] send]; 298 | }]]; 299 | 300 | [alertController 301 | addAction:[UIAlertAction 302 | actionWithTitle:@"Cancel" 303 | style:UIAlertActionStyleCancel 304 | handler:nil]]; 305 | 306 | [settingsVC presentViewController:alertController 307 | animated:YES 308 | completion:nil]; 309 | return YES; 310 | }]]; 311 | } 312 | 313 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 314 | YTSettingsPickerViewController *picker = [[%c(YTSettingsPickerViewController) alloc] 315 | initWithNavTitle:@"Manage Videos" 316 | pickerSectionTitle:nil 317 | rows:rows 318 | selectedItemIndex:NSNotFound 319 | parentResponder:[self parentResponder]]; 320 | 321 | if ([settingsVC respondsToSelector:@selector(navigationController)]) { 322 | UINavigationController *nav = settingsVC.navigationController; 323 | [nav pushViewController:picker animated:YES]; 324 | } 325 | return YES; 326 | }]; 327 | [sectionItems addObject:manageVideos]; 328 | 329 | NSUInteger wordCount = [[WordManager sharedInstance] blockedWords].count; 330 | YTSettingsSectionItem *manageWords = [%c(YTSettingsSectionItem) 331 | itemWithTitle:@"Manage Words" 332 | titleDescription:[NSString stringWithFormat:@"%lu blocked word%@", (unsigned long)wordCount, 333 | wordCount == 1 ? @"" : @"s"] 334 | accessibilityIdentifier:nil 335 | detailTextBlock:nil 336 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 337 | NSMutableArray *rows = [NSMutableArray array]; 338 | 339 | [rows 340 | addObject: 341 | [%c(YTSettingsSectionItem) 342 | itemWithTitle:@"Add Word" 343 | titleDescription:@"Block a new word or phrase" 344 | accessibilityIdentifier:nil 345 | detailTextBlock:nil 346 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 347 | YTSettingsViewController *settingsVC = 348 | [self valueForKey:@"_settingsViewControllerDelegate"]; 349 | UIAlertController *alertController = [UIAlertController 350 | alertControllerWithTitle:@"Add Word" 351 | message:@"Enter a word or phrase to block" 352 | preferredStyle:UIAlertControllerStyleAlert]; 353 | 354 | [alertController 355 | addTextFieldWithConfigurationHandler:^(UITextField *textField) { 356 | textField.placeholder = @"Word or phrase"; 357 | }]; 358 | 359 | [alertController 360 | addAction: 361 | [UIAlertAction 362 | actionWithTitle:@"Add" 363 | style:UIAlertActionStyleDefault 364 | handler:^(UIAlertAction *action) { 365 | NSString *word = alertController.textFields 366 | .firstObject.text; 367 | if (word.length > 0) { 368 | [[WordManager sharedInstance] 369 | addBlockedWord:word]; 370 | [self reloadGonerinoSection]; 371 | 372 | UIImpactFeedbackGenerator *generator = 373 | [[UIImpactFeedbackGenerator alloc] 374 | initWithStyle: 375 | UIImpactFeedbackStyleMedium]; 376 | [generator prepare]; 377 | [generator impactOccurred]; 378 | 379 | [[%c(YTToastResponderEvent) 380 | eventWithMessage: 381 | [NSString stringWithFormat: 382 | @"Added %@", word] 383 | firstResponder:settingsVC] send]; 384 | } 385 | }]]; 386 | 387 | [alertController 388 | addAction:[UIAlertAction 389 | actionWithTitle:@"Cancel" 390 | style:UIAlertActionStyleCancel 391 | handler:nil]]; 392 | 393 | [settingsVC presentViewController:alertController 394 | animated:YES 395 | completion:nil]; 396 | return YES; 397 | }]]; 398 | 399 | for (NSString *word in [[WordManager sharedInstance] blockedWords]) { 400 | [rows 401 | addObject: 402 | [%c(YTSettingsSectionItem) 403 | itemWithTitle:word 404 | titleDescription:nil 405 | accessibilityIdentifier:nil 406 | detailTextBlock:nil 407 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 408 | YTSettingsViewController *settingsVC = 409 | [self valueForKey:@"_settingsViewControllerDelegate"]; 410 | UIAlertController *alertController = [UIAlertController 411 | alertControllerWithTitle:@"Delete Word" 412 | message:[NSString 413 | stringWithFormat: 414 | @"Are you sure you want " 415 | @"to delete '%@'?", 416 | word] 417 | preferredStyle:UIAlertControllerStyleAlert]; 418 | 419 | [alertController 420 | addAction: 421 | [UIAlertAction 422 | actionWithTitle:@"Delete" 423 | style:UIAlertActionStyleDestructive 424 | handler:^(UIAlertAction *action) { 425 | [[WordManager sharedInstance] 426 | removeBlockedWord:word]; 427 | [self reloadGonerinoSection]; 428 | 429 | UIImpactFeedbackGenerator *generator = 430 | [[UIImpactFeedbackGenerator alloc] 431 | initWithStyle: 432 | UIImpactFeedbackStyleMedium]; 433 | [generator prepare]; 434 | [generator impactOccurred]; 435 | 436 | [[%c(YTToastResponderEvent) 437 | eventWithMessage: 438 | [NSString 439 | stringWithFormat: 440 | @"Deleted %@", word] 441 | firstResponder:settingsVC] send]; 442 | }]]; 443 | 444 | [alertController 445 | addAction:[UIAlertAction 446 | actionWithTitle:@"Cancel" 447 | style:UIAlertActionStyleCancel 448 | handler:nil]]; 449 | 450 | [settingsVC presentViewController:alertController 451 | animated:YES 452 | completion:nil]; 453 | return YES; 454 | }]]; 455 | } 456 | 457 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 458 | YTSettingsPickerViewController *picker = [[%c(YTSettingsPickerViewController) alloc] 459 | initWithNavTitle:@"Manage Words" 460 | pickerSectionTitle:nil 461 | rows:rows 462 | selectedItemIndex:NSNotFound 463 | parentResponder:[self parentResponder]]; 464 | 465 | if ([settingsVC respondsToSelector:@selector(navigationController)]) { 466 | UINavigationController *nav = settingsVC.navigationController; 467 | [nav pushViewController:picker animated:YES]; 468 | } 469 | return YES; 470 | }]; 471 | [sectionItems addObject:manageWords]; 472 | 473 | YTSettingsSectionItem *blockPeopleWatched = [%c(YTSettingsSectionItem) 474 | switchItemWithTitle:@"Block 'People also watched this video'" 475 | titleDescription:@"Remove 'People also watched this video' suggestions" 476 | accessibilityIdentifier:nil 477 | switchOn:[[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoPeopleWatched"] 478 | switchBlock:^BOOL(YTSettingsCell *cell, BOOL enabled) { 479 | [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"GonerinoPeopleWatched"]; 480 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 481 | [[%c(YTToastResponderEvent) 482 | eventWithMessage:[NSString stringWithFormat:@"'People also watched' %@", 483 | enabled ? @"blocked" : @"unblocked"] 484 | firstResponder:settingsVC] send]; 485 | return YES; 486 | } 487 | settingItemId:0]; 488 | [sectionItems addObject:blockPeopleWatched]; 489 | 490 | YTSettingsSectionItem *blockMightLike = [%c(YTSettingsSectionItem) 491 | switchItemWithTitle:@"Block 'You might also like this'" 492 | titleDescription:@"Remove 'You might also like this' suggestions" 493 | accessibilityIdentifier:nil 494 | switchOn:[[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoMightLike"] 495 | switchBlock:^BOOL(YTSettingsCell *cell, BOOL enabled) { 496 | [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:@"GonerinoMightLike"]; 497 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 498 | [[%c(YTToastResponderEvent) 499 | eventWithMessage:[NSString stringWithFormat:@"'You might also like' %@", 500 | enabled ? @"blocked" : @"unblocked"] 501 | firstResponder:settingsVC] send]; 502 | return YES; 503 | } 504 | settingItemId:0]; 505 | [sectionItems addObject:blockMightLike]; 506 | 507 | SECTION_HEADER(@"Manage Settings"); 508 | 509 | YTSettingsSectionItem *exportSettings = [%c(YTSettingsSectionItem) 510 | itemWithTitle:@"Export Settings" 511 | titleDescription:@"Export settings to a plist file" 512 | accessibilityIdentifier:nil 513 | detailTextBlock:nil 514 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 515 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 516 | 517 | NSMutableDictionary *settings = [NSMutableDictionary dictionary]; 518 | settings[@"blockedChannels"] = [[ChannelManager sharedInstance] blockedChannels]; 519 | settings[@"blockedVideos"] = [[VideoManager sharedInstance] blockedVideos]; 520 | settings[@"blockedWords"] = [[WordManager sharedInstance] blockedWords]; 521 | settings[@"gonerinoEnabled"] = 522 | @([[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoEnabled"] == nil 523 | ? YES 524 | : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoEnabled"]); 525 | settings[@"blockPeopleWatched"] = 526 | @([[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoPeopleWatched"]); 527 | settings[@"blockMightLike"] = 528 | @([[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoMightLike"]); 529 | 530 | NSURL *tempFileURL = 531 | [NSURL fileURLWithPath:[NSTemporaryDirectory() 532 | stringByAppendingPathComponent:@"gonerino_settings.plist"]]; 533 | [settings writeToURL:tempFileURL atomically:YES]; 534 | 535 | isImportOperation = NO; 536 | 537 | UIDocumentPickerViewController *picker = 538 | [[UIDocumentPickerViewController alloc] initForExportingURLs:@[tempFileURL]]; 539 | picker.delegate = (id)self; 540 | [settingsVC presentViewController:picker animated:YES completion:nil]; 541 | return YES; 542 | }]; 543 | [sectionItems addObject:exportSettings]; 544 | 545 | YTSettingsSectionItem *importSettings = [%c(YTSettingsSectionItem) 546 | itemWithTitle:@"Import Settings" 547 | titleDescription:@"Import settings from a plist file" 548 | accessibilityIdentifier:nil 549 | detailTextBlock:nil 550 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 551 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 552 | 553 | isImportOperation = YES; 554 | 555 | UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] 556 | initForOpeningContentTypes:@[[UTType typeWithIdentifier:@"com.apple.property-list"]]]; 557 | picker.delegate = (id)self; 558 | [settingsVC presentViewController:picker animated:YES completion:nil]; 559 | return YES; 560 | }]; 561 | [sectionItems addObject:importSettings]; 562 | 563 | SECTION_HEADER(@"About"); 564 | 565 | [sectionItems 566 | addObject:[%c(YTSettingsSectionItem) itemWithTitle:@"GitHub" 567 | titleDescription:@"View source code and report issues" 568 | accessibilityIdentifier:nil 569 | detailTextBlock:nil 570 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 571 | return [%c(YTUIUtils) 572 | openURL:[NSURL URLWithString:@"https://github.com/" 573 | @"castdrian/Gonerino"]]; 574 | }]]; 575 | 576 | [sectionItems 577 | addObject:[%c(YTSettingsSectionItem) itemWithTitle:@"Version" 578 | titleDescription:nil 579 | accessibilityIdentifier:nil 580 | detailTextBlock:^NSString *() { return [NSString stringWithFormat:@"v%@", TWEAK_VERSION]; } 581 | selectBlock:^BOOL(YTSettingsCell *cell, NSUInteger arg1) { 582 | return [%c(YTUIUtils) 583 | openURL:[NSURL URLWithString:@"https://github.com/castdrian/Gonerino/releases"]]; 584 | }]]; 585 | 586 | if ([delegate respondsToSelector:@selector(setSectionItems: 587 | forCategory:title:icon:titleDescription:headerHidden:)]) { 588 | YTIIcon *icon = [%c(YTIIcon) new]; 589 | icon.iconType = YT_FILTER; 590 | [delegate setSectionItems:sectionItems 591 | forCategory:GonerinoSection 592 | title:@"Gonerino" 593 | icon:icon 594 | titleDescription:nil 595 | headerHidden:NO]; 596 | } else { 597 | [delegate setSectionItems:sectionItems 598 | forCategory:GonerinoSection 599 | title:@"Gonerino" 600 | titleDescription:nil 601 | headerHidden:NO]; 602 | } 603 | } 604 | 605 | - (void)updateSectionForCategory:(NSUInteger)category withEntry:(id)entry { 606 | if (category == GonerinoSection) { 607 | [self updateGonerinoSectionWithEntry:entry]; 608 | return; 609 | } 610 | %orig; 611 | } 612 | 613 | %new 614 | - (UITableView *)findTableViewInView:(UIView *)view { 615 | if ([view isKindOfClass:[UITableView class]]) { 616 | return (UITableView *)view; 617 | } 618 | for (UIView *subview in view.subviews) { 619 | UITableView *tableView = [self findTableViewInView:subview]; 620 | if (tableView) { 621 | return tableView; 622 | } 623 | } 624 | return nil; 625 | } 626 | 627 | %new 628 | - (void)reloadGonerinoSection { 629 | dispatch_async(dispatch_get_main_queue(), ^{ 630 | YTSettingsViewController *delegate = [self valueForKey:@"_settingsViewControllerDelegate"]; 631 | if ([delegate isKindOfClass:%c(YTSettingsViewController)]) { 632 | [self updateGonerinoSectionWithEntry:nil]; 633 | UITableView *tableView = [self findTableViewInView:delegate.view]; 634 | if (tableView) { 635 | [tableView beginUpdates]; 636 | NSIndexSet *sectionSet = [NSIndexSet indexSetWithIndex:GonerinoSection]; 637 | [tableView reloadSections:sectionSet withRowAnimation:UITableViewRowAnimationAutomatic]; 638 | [tableView endUpdates]; 639 | } 640 | } 641 | }); 642 | } 643 | 644 | %new 645 | - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { 646 | if (urls.count == 0) 647 | return; 648 | 649 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 650 | NSURL *url = urls.firstObject; 651 | 652 | if (isImportOperation) { 653 | [url startAccessingSecurityScopedResource]; 654 | 655 | NSError *error = nil; 656 | NSData *data = [NSData dataWithContentsOfURL:url options:0 error:&error]; 657 | 658 | [url stopAccessingSecurityScopedResource]; 659 | 660 | if (!data || error) { 661 | [[%c(YTToastResponderEvent) eventWithMessage:@"Failed to read settings file" 662 | firstResponder:settingsVC] send]; 663 | return; 664 | } 665 | 666 | NSDictionary *settings = [NSPropertyListSerialization propertyListWithData:data 667 | options:NSPropertyListImmutable 668 | format:NULL 669 | error:&error]; 670 | 671 | if (!settings || error) { 672 | [[%c(YTToastResponderEvent) eventWithMessage:@"Invalid settings file format" 673 | firstResponder:settingsVC] send]; 674 | return; 675 | } 676 | 677 | void (^continueImport)(void) = ^{ 678 | NSArray *words = settings[@"blockedWords"]; 679 | if (words) { 680 | [[WordManager sharedInstance] setBlockedWords:words]; 681 | } 682 | 683 | NSNumber *peopleWatched = settings[@"blockPeopleWatched"]; 684 | if (peopleWatched) { 685 | [[NSUserDefaults standardUserDefaults] setBool:[peopleWatched boolValue] 686 | forKey:@"GonerinoPeopleWatched"]; 687 | } 688 | 689 | NSNumber *mightLike = settings[@"blockMightLike"]; 690 | if (mightLike) { 691 | [[NSUserDefaults standardUserDefaults] setBool:[mightLike boolValue] forKey:@"GonerinoMightLike"]; 692 | } 693 | 694 | NSNumber *gonerinoEnabled = settings[@"gonerinoEnabled"]; 695 | if (gonerinoEnabled) { 696 | [[NSUserDefaults standardUserDefaults] setBool:[gonerinoEnabled boolValue] forKey:@"GonerinoEnabled"]; 697 | } 698 | 699 | [[NSUserDefaults standardUserDefaults] synchronize]; 700 | [self reloadGonerinoSection]; 701 | [[%c(YTToastResponderEvent) eventWithMessage:@"Settings imported successfully" 702 | firstResponder:settingsVC] send]; 703 | }; 704 | 705 | NSArray *channels = settings[@"blockedChannels"]; 706 | if (channels) { 707 | [[ChannelManager sharedInstance] setBlockedChannels:[NSMutableArray arrayWithArray:channels]]; 708 | } 709 | 710 | NSArray *videos = settings[@"blockedVideos"]; 711 | if (videos) { 712 | if ([videos isKindOfClass:[NSArray class]]) { 713 | BOOL isValidFormat = YES; 714 | for (id videoEntry in videos) { 715 | if (![videoEntry isKindOfClass:[NSDictionary class]] || 716 | ![videoEntry[@"id"] isKindOfClass:[NSString class]] || 717 | ![videoEntry[@"title"] isKindOfClass:[NSString class]] || 718 | ![videoEntry[@"channel"] isKindOfClass:[NSString class]] || [videoEntry count] != 3) { 719 | isValidFormat = NO; 720 | break; 721 | } 722 | } 723 | 724 | if (isValidFormat) { 725 | [[VideoManager sharedInstance] setBlockedVideos:videos]; 726 | continueImport(); 727 | } else { 728 | [[%c(YTToastResponderEvent) 729 | eventWithMessage:@"Format outdated, blocked videos will not be imported" 730 | firstResponder:settingsVC] send]; 731 | 732 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), 733 | dispatch_get_main_queue(), ^{ continueImport(); }); 734 | } 735 | } else { 736 | [[%c(YTToastResponderEvent) 737 | eventWithMessage:@"Format outdated, blocked videos will not be imported" 738 | firstResponder:settingsVC] send]; 739 | 740 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), 741 | dispatch_get_main_queue(), ^{ continueImport(); }); 742 | } 743 | } else { 744 | continueImport(); 745 | } 746 | } else { 747 | NSMutableDictionary *settings = [NSMutableDictionary dictionary]; 748 | settings[@"blockedChannels"] = [[ChannelManager sharedInstance] blockedChannels]; 749 | settings[@"blockedVideos"] = [[VideoManager sharedInstance] blockedVideos]; 750 | settings[@"blockedWords"] = [[WordManager sharedInstance] blockedWords]; 751 | settings[@"gonerinoEnabled"] = @([[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoEnabled"] == nil 752 | ? YES 753 | : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoEnabled"]); 754 | settings[@"blockPeopleWatched"] = 755 | @([[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoPeopleWatched"]); 756 | settings[@"blockMightLike"] = @([[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoMightLike"]); 757 | 758 | [settings writeToURL:url atomically:YES]; 759 | [[%c(YTToastResponderEvent) eventWithMessage:@"Settings exported successfully" 760 | firstResponder:settingsVC] send]; 761 | } 762 | } 763 | 764 | %new 765 | - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { 766 | YTSettingsViewController *settingsVC = [self valueForKey:@"_settingsViewControllerDelegate"]; 767 | NSString *message = isImportOperation ? @"Import cancelled" : @"Export cancelled"; 768 | [[%c(YTToastResponderEvent) eventWithMessage:message firstResponder:settingsVC] send]; 769 | } 770 | 771 | %end 772 | 773 | %hook YTSettingsViewController 774 | 775 | - (void)loadWithModel:(id)model { 776 | %orig; 777 | if ([self respondsToSelector:@selector(updateSectionForCategory:withEntry:)]) { 778 | [(YTSettingsSectionItemManager *)[self valueForKey:@"_sectionItemManager"] updateGonerinoSectionWithEntry:nil]; 779 | } 780 | } 781 | 782 | %end 783 | 784 | %ctor { 785 | %init; 786 | } 787 | -------------------------------------------------------------------------------- /Sideloading.x: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | %group Sideloading 5 | 6 | // https://github.com/khanhduytran0/LiveContainer/blob/main/TweakLoader/DocumentPicker.m 7 | %hook UIDocumentPickerViewController 8 | 9 | - (instancetype)initForOpeningContentTypes:(NSArray *)contentTypes asCopy:(BOOL)asCopy { 10 | BOOL shouldMultiselect = NO; 11 | if ([contentTypes count] == 1 && contentTypes[0] == UTTypeFolder) { 12 | shouldMultiselect = YES; 13 | } 14 | 15 | NSArray *contentTypesNew = @[UTTypeItem, UTTypeFolder]; 16 | 17 | UIDocumentPickerViewController *ans = %orig(contentTypesNew, YES); 18 | if (shouldMultiselect) { 19 | [ans setAllowsMultipleSelection:YES]; 20 | } 21 | return ans; 22 | } 23 | 24 | - (instancetype)initWithDocumentTypes:(NSArray *)contentTypes inMode:(NSUInteger)mode { 25 | return [self initForOpeningContentTypes:contentTypes asCopy:(mode == 1 ? NO : YES)]; 26 | } 27 | 28 | - (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { 29 | if ([self allowsMultipleSelection]) { 30 | return; 31 | } 32 | %orig(YES); 33 | } 34 | 35 | %end 36 | 37 | %hook UIDocumentBrowserViewController 38 | 39 | - (instancetype)initForOpeningContentTypes:(NSArray *)contentTypes { 40 | NSArray *contentTypesNew = @[UTTypeItem, UTTypeFolder]; 41 | return %orig(contentTypesNew); 42 | } 43 | 44 | %end 45 | 46 | %hook NSURL 47 | 48 | - (BOOL)startAccessingSecurityScopedResource { 49 | %orig; 50 | return YES; 51 | } 52 | 53 | %end 54 | 55 | %end 56 | 57 | %ctor { 58 | BOOL isAppStoreApp = 59 | [[NSFileManager defaultManager] fileExistsAtPath:[[NSBundle mainBundle] appStoreReceiptURL].path]; 60 | if (!isAppStoreApp) { 61 | %init(Sideloading); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tweak.h: -------------------------------------------------------------------------------- 1 | #import "Util.h" 2 | 3 | #import "ChannelManager.h" 4 | #import "VideoManager.h" 5 | 6 | #import 7 | #import 8 | #import 9 | 10 | @class YTAsyncCollectionView; 11 | @class _ASCollectionViewCell; 12 | @class ASDisplayNode; 13 | @class ASTextNode; 14 | @class YTWatchController; 15 | @class YTSingleVideoController; 16 | @class YTDefaultSheetController; 17 | @class YTActionSheetAction; 18 | @class YTToastResponderEvent; 19 | @class YTSettingsCell; 20 | 21 | NS_ASSUME_NONNULL_BEGIN 22 | 23 | @interface YTAsyncCollectionView : UICollectionView 24 | 25 | - (void)layoutSubviews; 26 | 27 | - (void)performBatchUpdates:(void(NS_NOESCAPE ^ _Nullable)(void))updates 28 | completion:(void (^_Nullable)(BOOL finished))completion; 29 | 30 | - (NSArray *)visibleCells; 31 | 32 | - (nullable NSIndexPath *)indexPathForCell:(UICollectionViewCell *)cell; 33 | 34 | - (void)removeOffendingCells; 35 | 36 | @end 37 | 38 | @interface _ASCollectionViewCell : UICollectionViewCell 39 | 40 | - (nullable ASDisplayNode *)node; 41 | 42 | @end 43 | 44 | @interface ASDisplayNode : NSObject 45 | 46 | @property(nonatomic, copy, nullable) NSString *accessibilityLabel; 47 | @property(nonatomic, copy, nullable) NSString *accessibilityIdentifier; 48 | 49 | - (nullable NSArray *)subnodes; 50 | 51 | @end 52 | 53 | @interface ASTextNode : ASDisplayNode 54 | 55 | @property(nonatomic, copy, nullable) NSAttributedString *attributedText; 56 | 57 | @end 58 | 59 | @interface NSObject (ChannelName) 60 | 61 | - (nullable NSString *)channelName; 62 | - (nullable NSString *)ownerName; 63 | 64 | @end 65 | 66 | @interface YTWatchController : NSObject 67 | @property(nonatomic, strong, readonly) YTSingleVideoController *singleVideoController; 68 | - (YTSingleVideoController *)valueForKey:(NSString *)key; 69 | @end 70 | 71 | @interface YTSingleVideoController : NSObject 72 | @property(nonatomic, copy, readonly) NSString *channelName; 73 | - (NSString *)valueForKey:(NSString *)key; 74 | @end 75 | 76 | @interface YTDefaultSheetController : NSObject 77 | - (void)addAction:(YTActionSheetAction *)action; 78 | - (void)dismiss; 79 | - (id)valueForKey:(NSString *)key; 80 | - (UIImage *)createBlockIconWithOriginalAction:(nullable YTActionSheetAction *)originalAction; 81 | - (UIViewController *)findViewControllerForView:(UIView *)view; 82 | - (void)extractChannelNameFromNode:(id)node completion:(void (^)(NSString *channelName))completion; 83 | - (nullable NSString *)extractVideoTitleFromNode:(id)node; 84 | - (NSArray *)actions; // Added this line 85 | @end 86 | 87 | @interface YTActionSheetAction : NSObject 88 | @property(nonatomic, copy) NSString *title; 89 | @property(nonatomic, copy) void (^handler)(id); 90 | @property(nonatomic, strong) UIImage *iconImage; 91 | @property(nonatomic) BOOL shouldDismissOnAction; 92 | 93 | + (instancetype)actionWithTitle:(NSString *)title 94 | iconImage:(UIImage *)iconImage 95 | style:(NSInteger)style 96 | handler:(void (^)(id))handler; 97 | 98 | + (instancetype)actionWithTitle:(NSString *)title iconImage:(UIImage *)iconImage handler:(void (^)(id))handler; 99 | @end 100 | 101 | @interface YTActionSheetController : UIViewController 102 | - (void)presentFromView:(UIView *)view; 103 | - (NSArray *)actions; 104 | - (void)addAction:(YTActionSheetAction *)action; 105 | - (void)dismiss; 106 | - (UIViewController *)findViewControllerForView:(UIView *)view; 107 | @end 108 | @interface YTToastResponderEvent : NSObject 109 | + (instancetype)eventWithMessage:(NSString *)message firstResponder:(UIViewController *)responder; 110 | - (void)send; 111 | @end 112 | 113 | @interface YTSettingsSectionItem : NSObject 114 | + (instancetype)itemWithTitle:(NSString *)title 115 | titleDescription:(nullable NSString *)titleDescription 116 | accessibilityIdentifier:(nullable NSString *)accessibilityIdentifier 117 | detailTextBlock:(nullable NSString * (^)(void))detailTextBlock 118 | selectBlock:(BOOL (^)(YTSettingsCell *, NSUInteger))selectBlock 119 | settingItemId:(NSUInteger)settingItemId; 120 | @end 121 | 122 | @interface YTICommand : NSObject 123 | @property(copy, nonatomic) NSString *description; 124 | @end 125 | 126 | @interface YTInlinePlaybackPlayerDescriptor : NSObject 127 | @property(retain, nonatomic) id navigationEndpoint; 128 | @end 129 | 130 | @interface YTASDPlayableEntry : NSObject 131 | @property(retain, nonatomic) YTICommand *navigationEndpoint; 132 | @property(nonatomic) BOOL hasNavigationEndpoint; 133 | @property(copy, nonatomic) NSString *description; 134 | @end 135 | 136 | @interface YTElementsInlineMutedPlaybackView : NSObject 137 | @property(retain, nonatomic) YTASDPlayableEntry *asdPlayableEntry; 138 | @end 139 | 140 | @interface ELMContext : NSObject 141 | - (id)elementForKey:(NSString *)key; 142 | @end 143 | 144 | @interface ELMElement : NSObject 145 | @property(retain, nonatomic) id properties; 146 | @property(retain, nonatomic) ELMContext *context; 147 | - (id)propertyForKey:(NSString *)key; 148 | - (NSDictionary *)allProperties; 149 | - (id)valueForKey:(NSString *)key; 150 | @end 151 | 152 | @interface YTInlinePlaybackPlayerNode : ASDisplayNode 153 | @property(nonatomic, readonly) id playbackView; 154 | @property(nonatomic, readonly) ELMElement *element; 155 | @property(nonatomic, readonly) ELMContext *context; 156 | - (id)playbackView; 157 | @end 158 | 159 | NS_ASSUME_NONNULL_END 160 | -------------------------------------------------------------------------------- /Tweak.x: -------------------------------------------------------------------------------- 1 | #import "Tweak.h" 2 | 3 | static BOOL isShaking = NO; 4 | static NSTimeInterval shakeStartTime = 0; 5 | static UIImpactFeedbackGenerator *feedbackGenerator = nil; 6 | static UILabel *statusOverlayLabel = nil; 7 | 8 | static void updateStatusOverlay() { 9 | BOOL isEnabled = [[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoEnabled"] == nil 10 | ? YES 11 | : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoEnabled"]; 12 | 13 | dispatch_async(dispatch_get_main_queue(), ^{ 14 | if (!statusOverlayLabel) { 15 | statusOverlayLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 150, 30)]; 16 | statusOverlayLabel.textColor = [UIColor redColor]; 17 | statusOverlayLabel.backgroundColor = [UIColor colorWithWhite:0 alpha:0.7]; 18 | statusOverlayLabel.textAlignment = NSTextAlignmentCenter; 19 | statusOverlayLabel.layer.cornerRadius = 10; 20 | statusOverlayLabel.layer.masksToBounds = YES; 21 | statusOverlayLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightBold]; 22 | statusOverlayLabel.alpha = 0.0; 23 | 24 | UIWindow *keyWindow = nil; 25 | for (UIWindow *window in [UIApplication sharedApplication].windows) { 26 | if (window.isKeyWindow) { 27 | keyWindow = window; 28 | break; 29 | } 30 | } 31 | 32 | if (keyWindow) { 33 | statusOverlayLabel.frame = 34 | CGRectMake((keyWindow.bounds.size.width - 150) / 2, keyWindow.safeAreaInsets.top + 5, 150, 30); 35 | [keyWindow addSubview:statusOverlayLabel]; 36 | } 37 | } 38 | 39 | statusOverlayLabel.text = @"GONERINO DISABLED"; 40 | 41 | [UIView animateWithDuration:0.3 42 | animations:^{ statusOverlayLabel.alpha = isEnabled ? 0.0 : 1.0; } 43 | completion:nil]; 44 | }); 45 | } 46 | 47 | static void triggerHapticFeedback(void) { 48 | if (!feedbackGenerator) { 49 | feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; 50 | } 51 | [feedbackGenerator prepare]; 52 | [feedbackGenerator impactOccurred]; 53 | } 54 | 55 | static void toggleGonerinoStatus() { 56 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 57 | BOOL isEnabled = [defaults objectForKey:@"GonerinoEnabled"] == nil ? YES : [defaults boolForKey:@"GonerinoEnabled"]; 58 | BOOL newState = !isEnabled; 59 | [defaults setBool:newState forKey:@"GonerinoEnabled"]; 60 | [defaults synchronize]; 61 | 62 | updateStatusOverlay(); 63 | 64 | UIViewController *topVC = nil; 65 | 66 | NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; 67 | for (UIScene *scene in connectedScenes) { 68 | if ([scene isKindOfClass:[UIWindowScene class]]) { 69 | UIWindowScene *windowScene = (UIWindowScene *)scene; 70 | for (UIWindow *window in windowScene.windows) { 71 | if (window.rootViewController) { 72 | topVC = window.rootViewController; 73 | while (topVC.presentedViewController) { 74 | topVC = topVC.presentedViewController; 75 | } 76 | break; 77 | } 78 | } 79 | if (topVC) 80 | break; 81 | } 82 | } 83 | 84 | if (topVC) { 85 | dispatch_async(dispatch_get_main_queue(), ^{ 86 | [[%c(YTToastResponderEvent) 87 | eventWithMessage:[NSString stringWithFormat:@"Gonerino %@", !isEnabled ? @"activated" : @"deactivated"] 88 | firstResponder:topVC] send]; 89 | }); 90 | } 91 | } 92 | 93 | %hook UIWindow 94 | 95 | - (void)becomeKeyWindow { 96 | %orig; 97 | } 98 | 99 | - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { 100 | BOOL isShakeEnabled = [[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoShakeEnabled"] == nil 101 | ? NO 102 | : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoShakeEnabled"]; 103 | 104 | if (motion == UIEventSubtypeMotionShake && isShakeEnabled) { 105 | isShaking = YES; 106 | shakeStartTime = [[NSDate date] timeIntervalSince1970]; 107 | } 108 | %orig; 109 | } 110 | 111 | - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { 112 | BOOL isShakeEnabled = [[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoShakeEnabled"] == nil 113 | ? NO 114 | : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoShakeEnabled"]; 115 | 116 | if (motion == UIEventSubtypeMotionShake && isShaking && isShakeEnabled) { 117 | NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; 118 | NSTimeInterval shakeDuration = currentTime - shakeStartTime; 119 | 120 | if (shakeDuration >= 0.5 && shakeDuration <= 2.0) { 121 | triggerHapticFeedback(); 122 | dispatch_async(dispatch_get_main_queue(), ^{ toggleGonerinoStatus(); }); 123 | } 124 | isShaking = NO; 125 | } 126 | %orig; 127 | } 128 | 129 | %end 130 | 131 | %hook YTAsyncCollectionView 132 | 133 | - (void)layoutSubviews { 134 | %orig; 135 | [self removeOffendingCells]; 136 | } 137 | 138 | %new 139 | - (void)removeOffendingCells { 140 | __weak typeof(self) weakSelf = self; 141 | 142 | dispatch_async(dispatch_get_main_queue(), ^{ 143 | __strong typeof(weakSelf) strongSelf = weakSelf; 144 | if (!strongSelf) 145 | return; 146 | 147 | @try { 148 | NSArray *visibleCells = [strongSelf visibleCells]; 149 | NSMutableArray *indexPathsToRemove = [NSMutableArray array]; 150 | 151 | for (UICollectionViewCell *cell in visibleCells) { 152 | if (![cell isKindOfClass:NSClassFromString(@"_ASCollectionViewCell")]) { 153 | continue; 154 | } 155 | 156 | _ASCollectionViewCell *asCell = (_ASCollectionViewCell *)cell; 157 | if (![asCell respondsToSelector:@selector(node)]) { 158 | continue; 159 | } 160 | 161 | id node = [asCell node]; 162 | if (![node isKindOfClass:NSClassFromString(@"YTVideoWithContextNode")]) { 163 | continue; 164 | } 165 | 166 | if ([Util nodeContainsBlockedVideo:node]) { 167 | NSIndexPath *indexPath = [strongSelf indexPathForCell:cell]; 168 | if (indexPath) { 169 | [indexPathsToRemove addObject:indexPath]; 170 | } 171 | } 172 | } 173 | 174 | if (indexPathsToRemove.count > 0) { 175 | [strongSelf 176 | performBatchUpdates:^{ [strongSelf deleteItemsAtIndexPaths:indexPathsToRemove]; } 177 | completion:nil]; 178 | } 179 | } @catch (NSException *exception) { 180 | NSLog(@"[Gonerino] Exception in removeOffendingCells: %@", exception); 181 | } 182 | }); 183 | } 184 | 185 | %end 186 | 187 | %hook YTDefaultSheetController 188 | 189 | - (void)addAction:(YTActionSheetAction *)action { 190 | %orig; 191 | 192 | static void *blockActionKey = &blockActionKey; 193 | if (objc_getAssociatedObject(self, blockActionKey)) { 194 | return; 195 | } 196 | 197 | UIView *sourceView = [self valueForKey:@"sourceView"]; 198 | id node = [sourceView valueForKey:@"asyncdisplaykit_node"]; 199 | 200 | if (!node || ![node debugDescription] || ![[node debugDescription] containsString:@"YTVideoWithContextNode"]) { 201 | return; 202 | } 203 | 204 | NSInteger currentActionsCount = 3; 205 | if ([self respondsToSelector:@selector(actions)]) { 206 | currentActionsCount = [[self actions] count]; 207 | } 208 | 209 | if (currentActionsCount < 3) { 210 | return; 211 | } 212 | 213 | __weak typeof(self) weakSelf = self; 214 | CGSize iconSize = CGSizeMake(24, 24); 215 | if (action) { 216 | UIImage *originalIcon = [action valueForKey:@"_iconImage"]; 217 | if (originalIcon) { 218 | iconSize = originalIcon.size; 219 | } 220 | } 221 | 222 | YTActionSheetAction *blockChannelAction = [%c(YTActionSheetAction) 223 | actionWithTitle:@"Block channel" 224 | iconImage:[Util createBlockChannelIconWithSize:iconSize] 225 | style:0 226 | handler:^(YTActionSheetAction *action) { 227 | __strong typeof(self) strongSelf = weakSelf; 228 | @try { 229 | UIView *sourceView = [strongSelf valueForKey:@"sourceView"]; 230 | id node = [sourceView valueForKey:@"asyncdisplaykit_node"]; 231 | 232 | if ([node respondsToSelector:@selector(subnodes)]) { 233 | for (id subnode in [node subnodes]) { 234 | if ([subnode isKindOfClass:NSClassFromString(@"YTInlinePlaybackPlayerNode")]) { 235 | [Util extractVideoInfoFromNode:subnode 236 | completion:^(NSString *videoId, NSString *videoTitle, 237 | NSString *ownerName) { 238 | if (ownerName) { 239 | [[ChannelManager sharedInstance] 240 | addBlockedChannel:ownerName]; 241 | UIViewController *viewController = 242 | (UIViewController *)strongSelf; 243 | [[%c(YTToastResponderEvent) 244 | eventWithMessage:[NSString 245 | stringWithFormat:@"Blocked %@", 246 | ownerName] 247 | firstResponder:viewController] send]; 248 | } 249 | }]; 250 | break; 251 | } 252 | } 253 | } 254 | } @catch (NSException *e) { 255 | NSLog(@"[Gonerino] Exception in block action: %@", e); 256 | } 257 | }]; 258 | 259 | YTActionSheetAction *blockVideoAction = [%c(YTActionSheetAction) 260 | actionWithTitle:@"Block video" 261 | iconImage:[Util createBlockVideoIconWithSize:iconSize] 262 | style:0 263 | handler:^(YTActionSheetAction *action) { 264 | __strong typeof(self) strongSelf = weakSelf; 265 | @try { 266 | UIView *sourceView = [strongSelf valueForKey:@"sourceView"]; 267 | id node = [sourceView valueForKey:@"asyncdisplaykit_node"]; 268 | 269 | if ([node respondsToSelector:@selector(subnodes)]) { 270 | for (id subnode in [node subnodes]) { 271 | if ([subnode isKindOfClass:NSClassFromString(@"YTInlinePlaybackPlayerNode")]) { 272 | [Util 273 | extractVideoInfoFromNode:subnode 274 | completion:^(NSString *videoId, NSString *videoTitle, 275 | NSString *ownerName) { 276 | if (videoId) { 277 | [[VideoManager sharedInstance] addBlockedVideo:videoId 278 | title:videoTitle 279 | channel:ownerName]; 280 | UIViewController *viewController = 281 | (UIViewController *)strongSelf; 282 | [[%c(YTToastResponderEvent) 283 | eventWithMessage: 284 | [NSString stringWithFormat:@"Blocked video: %@", 285 | videoTitle ?: videoId] 286 | firstResponder:viewController] send]; 287 | if ([strongSelf respondsToSelector:@selector(dismiss)]) { 288 | [strongSelf dismiss]; 289 | } 290 | } 291 | }]; 292 | break; 293 | } 294 | } 295 | } 296 | } @catch (NSException *e) { 297 | NSLog(@"[Gonerino] Exception in block action: %@", e); 298 | } 299 | }]; 300 | 301 | objc_setAssociatedObject(self, blockActionKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 302 | [self addAction:blockChannelAction]; 303 | [self addAction:blockVideoAction]; 304 | } 305 | 306 | %new 307 | - (UIViewController *)findViewControllerForView:(UIView *)view { 308 | UIResponder *responder = view; 309 | while (responder) { 310 | if ([responder isKindOfClass:[UIViewController class]]) { 311 | return (UIViewController *)responder; 312 | } 313 | responder = [responder nextResponder]; 314 | } 315 | return nil; 316 | } 317 | 318 | %end 319 | 320 | %hook UIApplication 321 | 322 | - (void)applicationDidBecomeActive:(id)arg1 { 323 | %orig; 324 | updateStatusOverlay(); 325 | } 326 | 327 | %end 328 | 329 | %ctor { 330 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), 331 | ^{ updateStatusOverlay(); }); 332 | } 333 | -------------------------------------------------------------------------------- /Util.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import "Tweak.h" 6 | #import "WordManager.h" 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | @interface Util : NSObject 11 | 12 | + (void)extractVideoInfoFromNode:(id)node 13 | completion:(void (^)(NSString *videoId, NSString *videoTitle, NSString *ownerName))completion; 14 | 15 | + (BOOL)nodeContainsBlockedVideo:(id)node; 16 | 17 | + (UIImage *)createBlockChannelIconWithSize:(CGSize)size; 18 | + (UIImage *)createBlockVideoIconWithSize:(CGSize)size; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /Util.m: -------------------------------------------------------------------------------- 1 | #import "Util.h" 2 | 3 | @implementation Util 4 | 5 | + (void)extractVideoInfoFromNode:(id)node 6 | completion:(void (^)(NSString *videoId, NSString *videoTitle, NSString *ownerName))completion { 7 | if (!completion) 8 | return; 9 | 10 | if (![node isKindOfClass:NSClassFromString(@"YTInlinePlaybackPlayerNode")]) { 11 | NSLog(@"[Gonerino] Error: extractVideoInfoFromNode received incorrect node type: %@", 12 | NSStringFromClass([node class])); 13 | return; 14 | } 15 | 16 | @try { 17 | UIView *view = [node view]; 18 | for (UIView *subview in view.subviews) { 19 | if ([subview isKindOfClass:NSClassFromString(@"YTElementsInlineMutedPlaybackView")]) { 20 | YTElementsInlineMutedPlaybackView *playbackView = (YTElementsInlineMutedPlaybackView *)subview; 21 | YTASDPlayableEntry *playableEntry = playbackView.asdPlayableEntry; 22 | 23 | if (playableEntry && playableEntry.hasNavigationEndpoint) { 24 | NSString *description = [playableEntry.navigationEndpoint description]; 25 | 26 | if (!description) 27 | return; 28 | 29 | NSError *error = nil; 30 | NSString *videoId = nil; 31 | NSString *videoTitle = nil; 32 | NSString *ownerName = nil; 33 | 34 | NSArray *patterns = @[ 35 | @"video_id: \"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"", 36 | @"video_title: \"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"", 37 | @"owner_display_name: \"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"" 38 | ]; 39 | 40 | for (NSString *pattern in patterns) { 41 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern 42 | options:0 43 | error:&error]; 44 | if (error) { 45 | NSLog(@"[Gonerino] Regex error for pattern %@: %@", pattern, error); 46 | continue; 47 | } 48 | 49 | NSTextCheckingResult *match = [regex firstMatchInString:description 50 | options:0 51 | range:NSMakeRange(0, description.length)]; 52 | 53 | if (match && match.numberOfRanges > 1) { 54 | NSString *value = [description substringWithRange:[match rangeAtIndex:1]]; 55 | 56 | value = [value stringByReplacingOccurrencesOfString:@"\\\"" withString:@"\""]; 57 | value = [value stringByReplacingOccurrencesOfString:@"\\'" withString:@"'"]; 58 | 59 | if ([pattern hasPrefix:@"video_id:"]) { 60 | videoId = value; 61 | } else if ([pattern hasPrefix:@"video_title:"]) { 62 | videoTitle = value; 63 | } else if ([pattern hasPrefix:@"owner_display_name:"]) { 64 | ownerName = value; 65 | } 66 | } 67 | } 68 | 69 | if (videoId || videoTitle || ownerName) { 70 | completion(videoId, videoTitle, ownerName); 71 | } 72 | return; 73 | } 74 | } 75 | } 76 | } @catch (NSException *exception) { 77 | NSLog(@"[Gonerino] Exception in extractVideoInfoFromNode: %@", exception); 78 | } 79 | } 80 | 81 | + (BOOL)nodeContainsBlockedVideo:(id)node { 82 | BOOL isEnabled = [[NSUserDefaults standardUserDefaults] objectForKey:@"GonerinoEnabled"] == nil ? 83 | YES : [[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoEnabled"]; 84 | 85 | if (!isEnabled) { 86 | return NO; 87 | } 88 | 89 | if ([node respondsToSelector:@selector(accessibilityLabel)]) { 90 | NSString *accessibilityLabel = [node accessibilityLabel]; 91 | if (accessibilityLabel) { 92 | if ([[WordManager sharedInstance] isWordBlocked:accessibilityLabel]) { 93 | NSLog(@"[Gonerino] Blocking video because of blocked word: %@", accessibilityLabel); 94 | return YES; 95 | } 96 | } 97 | } 98 | 99 | if ([node isKindOfClass:NSClassFromString(@"ASTextNode")]) { 100 | NSAttributedString *attributedText = [(ASTextNode *)node attributedText]; 101 | NSString *text = [attributedText string]; 102 | 103 | if ([[WordManager sharedInstance] isWordBlocked:text]) { 104 | NSLog(@"[Gonerino] Blocking content with blocked word: %@", text); 105 | return YES; 106 | } 107 | 108 | if ([text containsString:@" · "]) { 109 | NSArray *components = [text componentsSeparatedByString:@" · "]; 110 | if (components.count >= 1) { 111 | NSString *potentialChannelName = components[0]; 112 | if ([[ChannelManager sharedInstance] isChannelBlocked:potentialChannelName]) { 113 | NSLog(@"[Gonerino] Blocking content from blocked channel: %@", potentialChannelName); 114 | return YES; 115 | } 116 | } 117 | } 118 | } 119 | 120 | if ([node respondsToSelector:@selector(channelName)]) { 121 | NSString *nodeChannelName = [node channelName]; 122 | if ([[ChannelManager sharedInstance] isChannelBlocked:nodeChannelName]) { 123 | NSLog(@"[Gonerino] Blocking content from blocked channel: %@", nodeChannelName); 124 | return YES; 125 | } 126 | } 127 | 128 | if ([node respondsToSelector:@selector(ownerName)]) { 129 | NSString *nodeOwnerName = [node ownerName]; 130 | if ([[ChannelManager sharedInstance] isChannelBlocked:nodeOwnerName]) { 131 | NSLog(@"[Gonerino] Blocking content from blocked channel: %@", nodeOwnerName); 132 | return YES; 133 | } 134 | } 135 | 136 | __block BOOL isBlocked = NO; 137 | 138 | if ([node isKindOfClass:NSClassFromString(@"ASTextNode")]) { 139 | NSAttributedString *attributedText = [(ASTextNode *)node attributedText]; 140 | NSString *text = [attributedText string]; 141 | 142 | if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoPeopleWatched"] && 143 | [text isEqualToString:@"People also watched this video"]) { 144 | NSLog(@"[Gonerino] Blocking 'People also watched' section"); 145 | return YES; 146 | } 147 | 148 | if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GonerinoMightLike"] && 149 | [text isEqualToString:@"You might also like this"]) { 150 | NSLog(@"[Gonerino] Blocking 'You might also like' section"); 151 | return YES; 152 | } 153 | } 154 | 155 | if ([node isKindOfClass:NSClassFromString(@"YTInlinePlaybackPlayerNode")]) { 156 | [self 157 | extractVideoInfoFromNode:node 158 | completion:^(NSString *videoId, NSString *videoTitle, NSString *ownerName) { 159 | if ([[VideoManager sharedInstance] isVideoBlocked:videoId]) { 160 | isBlocked = YES; 161 | NSLog(@"[Gonerino] Blocking video with id: %@", videoId); 162 | } 163 | if ([[ChannelManager sharedInstance] isChannelBlocked:ownerName]) { 164 | isBlocked = YES; 165 | NSLog(@"[Gonerino] Blocking video with id %@: Channel %@ is blocked", videoId, 166 | ownerName); 167 | } 168 | if ([[WordManager sharedInstance] isWordBlocked:videoTitle]) { 169 | isBlocked = YES; 170 | NSLog(@"[Gonerino] Blocking video with id %@: title contains blocked word", videoId); 171 | } 172 | if ([[WordManager sharedInstance] isWordBlocked:ownerName]) { 173 | isBlocked = YES; 174 | NSLog(@"[Gonerino] Blocking video with id %@: channel name contains blocked word", 175 | videoId); 176 | } 177 | }]; 178 | return isBlocked; 179 | } 180 | 181 | if ([node respondsToSelector:@selector(subnodes)]) { 182 | NSArray *subnodes = [node subnodes]; 183 | for (id subnode in subnodes) { 184 | if ([self nodeContainsBlockedVideo:subnode]) { 185 | return YES; 186 | } 187 | } 188 | } 189 | 190 | return NO; 191 | } 192 | 193 | + (UIImage *)createBlockChannelIconWithSize:(CGSize)size { 194 | @try { 195 | UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale); 196 | CGContextRef context = UIGraphicsGetCurrentContext(); 197 | if (!context) { 198 | NSLog(@"[Gonerino] Failed to create graphics context"); 199 | return nil; 200 | } 201 | 202 | CGContextSetShouldAntialias(context, YES); 203 | CGContextSetAllowsAntialiasing(context, YES); 204 | CGContextSetShouldSmoothFonts(context, NO); 205 | 206 | [[UIColor whiteColor] setStroke]; 207 | 208 | CGFloat noSymbolRadius = size.width * 0.45; 209 | CGPoint center = CGPointMake(size.width / 2, size.height / 2); 210 | UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center 211 | radius:noSymbolRadius 212 | startAngle:0 213 | endAngle:2 * M_PI 214 | clockwise:YES]; 215 | 216 | CGFloat bodyRadius = size.width * 0.3; 217 | CGPoint bodyCenter = CGPointMake(size.width / 2, size.height * 0.85); 218 | UIBezierPath *bodyPath = [UIBezierPath bezierPathWithArcCenter:bodyCenter 219 | radius:bodyRadius 220 | startAngle:M_PI 221 | endAngle:2 * M_PI 222 | clockwise:YES]; 223 | 224 | CGFloat headRadius = size.width * 0.15; 225 | CGPoint headCenter = CGPointMake(size.width / 2, size.height * 0.35); 226 | UIBezierPath *headPath = [UIBezierPath bezierPathWithArcCenter:headCenter 227 | radius:headRadius 228 | startAngle:0 229 | endAngle:2 * M_PI 230 | clockwise:YES]; 231 | 232 | UIBezierPath *linePath = [UIBezierPath bezierPath]; 233 | CGFloat offset = noSymbolRadius * 0.7071; 234 | [linePath moveToPoint:CGPointMake(center.x - offset, center.y - offset)]; 235 | [linePath addLineToPoint:CGPointMake(center.x + offset, center.y + offset)]; 236 | 237 | CGFloat lineWidth = 1.5; 238 | circlePath.lineWidth = lineWidth; 239 | headPath.lineWidth = lineWidth; 240 | bodyPath.lineWidth = lineWidth; 241 | linePath.lineWidth = lineWidth; 242 | 243 | [circlePath stroke]; 244 | [bodyPath stroke]; 245 | [headPath stroke]; 246 | [linePath stroke]; 247 | 248 | UIImage *icon = UIGraphicsGetImageFromCurrentImageContext(); 249 | UIGraphicsEndImageContext(); 250 | 251 | return [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 252 | } @catch (NSException *exception) { 253 | NSLog(@"[Gonerino] Exception in createBlockChannelIcon: %@", exception); 254 | return nil; 255 | } 256 | } 257 | 258 | + (UIImage *)createBlockVideoIconWithSize:(CGSize)size { 259 | @try { 260 | UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale); 261 | CGContextRef context = UIGraphicsGetCurrentContext(); 262 | if (!context) { 263 | NSLog(@"[Gonerino] Failed to create graphics context"); 264 | return nil; 265 | } 266 | 267 | CGContextSetShouldAntialias(context, YES); 268 | CGContextSetAllowsAntialiasing(context, YES); 269 | CGContextSetShouldSmoothFonts(context, NO); 270 | 271 | [[UIColor whiteColor] setStroke]; 272 | [[UIColor whiteColor] setFill]; 273 | 274 | CGPoint center = CGPointMake(size.width / 2, size.height / 2); 275 | 276 | UIBezierPath *rectPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(size.width * 0.2, size.height * 0.3, 277 | size.width * 0.6, size.height * 0.4) 278 | cornerRadius:3.0]; 279 | 280 | UIBezierPath *trianglePath = [UIBezierPath bezierPath]; 281 | CGFloat triangleSize = size.width * 0.2; 282 | CGPoint triangleCenter = center; 283 | 284 | [trianglePath 285 | moveToPoint:CGPointMake(triangleCenter.x - triangleSize / 2, triangleCenter.y - triangleSize / 2)]; 286 | [trianglePath addLineToPoint:CGPointMake(triangleCenter.x + triangleSize / 2, triangleCenter.y)]; 287 | [trianglePath 288 | addLineToPoint:CGPointMake(triangleCenter.x - triangleSize / 2, triangleCenter.y + triangleSize / 2)]; 289 | [trianglePath closePath]; 290 | 291 | CGFloat noSymbolRadius = size.width * 0.45; 292 | UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center 293 | radius:noSymbolRadius 294 | startAngle:0 295 | endAngle:2 * M_PI 296 | clockwise:YES]; 297 | 298 | UIBezierPath *linePath = [UIBezierPath bezierPath]; 299 | CGFloat offset = noSymbolRadius * 0.7071; 300 | [linePath moveToPoint:CGPointMake(center.x - offset, center.y - offset)]; 301 | [linePath addLineToPoint:CGPointMake(center.x + offset, center.y + offset)]; 302 | 303 | CGFloat lineWidth = 1.5; 304 | rectPath.lineWidth = lineWidth; 305 | trianglePath.lineWidth = lineWidth; 306 | circlePath.lineWidth = lineWidth; 307 | linePath.lineWidth = lineWidth; 308 | 309 | [rectPath stroke]; 310 | [trianglePath fill]; 311 | [circlePath stroke]; 312 | [linePath stroke]; 313 | 314 | UIImage *icon = UIGraphicsGetImageFromCurrentImageContext(); 315 | UIGraphicsEndImageContext(); 316 | 317 | return [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 318 | } @catch (NSException *exception) { 319 | NSLog(@"[Gonerino] Exception in createBlockVideoIcon: %@", exception); 320 | return nil; 321 | } 322 | } 323 | 324 | @end 325 | -------------------------------------------------------------------------------- /VideoManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface VideoManager : NSObject 4 | 5 | @property(nonatomic, readonly) NSArray *blockedVideos; 6 | 7 | + (instancetype)sharedInstance; 8 | - (void)addBlockedVideo:(NSString *)videoId title:(NSString *)title channel:(NSString *)channel; 9 | - (void)removeBlockedVideo:(NSString *)videoId; 10 | - (BOOL)isVideoBlocked:(NSString *)videoId; 11 | - (void)setBlockedVideos:(NSArray *)videos; 12 | 13 | @end -------------------------------------------------------------------------------- /VideoManager.m: -------------------------------------------------------------------------------- 1 | #import "VideoManager.h" 2 | 3 | @interface VideoManager () 4 | @property(nonatomic, strong) NSMutableArray *blockedVideoArray; 5 | @end 6 | 7 | @implementation VideoManager 8 | 9 | + (instancetype)sharedInstance { 10 | static VideoManager *instance = nil; 11 | static dispatch_once_t onceToken; 12 | dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); 13 | return instance; 14 | } 15 | 16 | - (instancetype)init { 17 | self = [super init]; 18 | if (self) { 19 | _blockedVideoArray = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"GonerinoBlockedVideos"] mutableCopy] 20 | ?: [NSMutableArray array]; 21 | } 22 | return self; 23 | } 24 | 25 | - (NSArray *)blockedVideos { 26 | return [self.blockedVideoArray copy]; 27 | } 28 | 29 | - (void)addBlockedVideo:(NSString *)videoId title:(NSString *)title channel:(NSString *)channel { // Fixed: Method name 30 | if (!videoId.length) 31 | return; 32 | 33 | NSDictionary *videoInfo = @{@"id": videoId, @"title": title ?: @"", @"channel": channel ?: @""}; 34 | 35 | NSInteger existingIndex = 36 | [self.blockedVideoArray indexOfObjectPassingTest:^BOOL(NSDictionary *obj, NSUInteger idx, BOOL *stop) { 37 | return [obj[@"id"] isEqualToString:videoId]; 38 | }]; 39 | 40 | if (existingIndex == NSNotFound) { 41 | [self.blockedVideoArray addObject:videoInfo]; 42 | [self saveBlockedVideos]; 43 | } 44 | } 45 | 46 | - (void)removeBlockedVideo:(NSString *)videoId { 47 | NSIndexSet *indexes = 48 | [self.blockedVideoArray indexesOfObjectsPassingTest:^BOOL(NSDictionary *obj, NSUInteger idx, BOOL *stop) { 49 | return [obj[@"id"] isEqualToString:videoId]; 50 | }]; 51 | 52 | if (indexes.count > 0) { 53 | [self.blockedVideoArray removeObjectsAtIndexes:indexes]; 54 | [self saveBlockedVideos]; 55 | } 56 | } 57 | 58 | - (BOOL)isVideoBlocked:(NSString *)videoId { 59 | if (!videoId) 60 | return NO; 61 | 62 | return [self.blockedVideoArray indexOfObjectPassingTest:^BOOL(NSDictionary *obj, NSUInteger idx, BOOL *stop) { 63 | return [obj[@"id"] isEqualToString:videoId]; 64 | }] != NSNotFound; 65 | } 66 | 67 | - (void)saveBlockedVideos { 68 | [[NSUserDefaults standardUserDefaults] setObject:self.blockedVideoArray forKey:@"GonerinoBlockedVideos"]; 69 | [[NSUserDefaults standardUserDefaults] synchronize]; 70 | } 71 | 72 | - (void)setBlockedVideos:(NSArray *)videos { 73 | NSArray *validVideos = [videos 74 | filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSDictionary *dict, NSDictionary *bindings) { 75 | return [dict isKindOfClass:[NSDictionary class]] && dict[@"id"] && 76 | [dict[@"id"] isKindOfClass:[NSString class]] && [dict[@"id"] length] > 0; 77 | }]]; 78 | 79 | self.blockedVideoArray = [validVideos mutableCopy]; 80 | [self saveBlockedVideos]; 81 | } 82 | 83 | @end -------------------------------------------------------------------------------- /WordManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface WordManager : NSObject 4 | 5 | + (instancetype)sharedInstance; 6 | - (NSArray *)blockedWords; 7 | - (void)addBlockedWord:(NSString *)word; 8 | - (void)removeBlockedWord:(NSString *)word; 9 | - (BOOL)isWordBlocked:(NSString *)text; 10 | - (void)setBlockedWords:(NSArray *)words; 11 | 12 | @end -------------------------------------------------------------------------------- /WordManager.m: -------------------------------------------------------------------------------- 1 | #import "WordManager.h" 2 | 3 | @interface WordManager () 4 | @property(nonatomic, strong) NSMutableSet *blockedWordSet; 5 | @end 6 | 7 | @implementation WordManager 8 | 9 | + (instancetype)sharedInstance { 10 | static WordManager *instance = nil; 11 | static dispatch_once_t onceToken; 12 | dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); 13 | return instance; 14 | } 15 | 16 | - (instancetype)init { 17 | self = [super init]; 18 | if (self) { 19 | _blockedWordSet = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"GonerinoBlockedWords"] mutableCopy] 20 | ?: [NSMutableSet set]; 21 | } 22 | return self; 23 | } 24 | 25 | - (NSArray *)blockedWords { 26 | return [self.blockedWordSet allObjects]; 27 | } 28 | 29 | - (void)addBlockedWord:(NSString *)word { 30 | if (word.length > 0) { 31 | [self.blockedWordSet addObject:word]; 32 | [self saveBlockedWords]; 33 | } 34 | } 35 | 36 | - (void)removeBlockedWord:(NSString *)word { 37 | if (word) { 38 | [self.blockedWordSet removeObject:word]; 39 | [self saveBlockedWords]; 40 | } 41 | } 42 | 43 | - (BOOL)isWordBlocked:(NSString *)text { 44 | for (NSString *word in self.blockedWordSet) { 45 | if ([text.lowercaseString containsString:word.lowercaseString]) { 46 | return YES; 47 | } 48 | } 49 | return NO; 50 | } 51 | 52 | - (void)saveBlockedWords { 53 | [[NSUserDefaults standardUserDefaults] setObject:[self.blockedWordSet allObjects] forKey:@"GonerinoBlockedWords"]; 54 | [[NSUserDefaults standardUserDefaults] synchronize]; 55 | } 56 | 57 | - (void)setBlockedWords:(NSArray *)words { 58 | self.blockedWordSet = [NSMutableSet setWithArray:words]; 59 | [self saveBlockedWords]; 60 | } 61 | 62 | @end -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: dev.adrian.gonerino 2 | Name: Gonerino 3 | Version: 1.2.2 4 | Architecture: iphoneos-arm 5 | Description: Remove videos uploaded by specific channels 6 | Maintainer: Adrian Castro 7 | Author: Adrian Castro 8 | Section: Tweaks 9 | Depends: mobilesubstrate (>= 0.9.5000) 10 | Depiction: https://repo.adriancastro.dev/depictions/web/?p=dev.adrian.gonerino 11 | SileoDepiction: https://repo.adriancastro.dev/depictions/native/dev.adrian.gonerino/depiction.json 12 | --------------------------------------------------------------------------------