-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathTweak.x
More file actions
729 lines (636 loc) · 27.3 KB
/
Tweak.x
File metadata and controls
729 lines (636 loc) · 27.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
#import <substrate.h>
#import <UIKit/UIKit.h>
#import <YouTubeHeader/YTAppDelegate.h>
#import <YouTubeHeader/YTGlobalConfig.h>
#import <YouTubeHeader/YTColdConfig.h>
#import <YouTubeHeader/YTHotConfig.h>
#import <YouTubeHeader/YTIElementRenderer.h>
// Forward declarations
@class YTWatchViewController;
@class YTMainAppVideoPlayerOverlayView;
@class YTAsyncCollectionView;
@class _ASCollectionViewCell;
// Storage for original method implementations
NSMutableDictionary <NSString *, NSMutableDictionary <NSString *, NSNumber *> *> *abConfigCache;
// Night mode overlay
static UIView *nightModeOverlay = nil;
static void ensureOverlayOnMainWindow(void) {
UIWindow *mainWindow = [[[UIApplication sharedApplication] delegate] window];
if (!mainWindow) return;
if (!nightModeOverlay) {
nightModeOverlay = [[UIView alloc] initWithFrame:mainWindow.bounds];
nightModeOverlay.backgroundColor = [UIColor blackColor];
nightModeOverlay.alpha = 0.0;
nightModeOverlay.userInteractionEnabled = NO;
nightModeOverlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
nightModeOverlay.layer.zPosition = CGFLOAT_MAX;
}
if (nightModeOverlay.window != mainWindow) {
[nightModeOverlay removeFromSuperview];
[mainWindow addSubview:nightModeOverlay];
}
}
// nightMode_level: 0 = Off, 1 = Low, 2 = Medium, 3 = High, 4 = Maximum
static void updateNightModeOverlay(void) {
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{ updateNightModeOverlay(); });
return;
}
NSInteger level = [[NSUserDefaults standardUserDefaults] integerForKey:@"nightMode_level"];
CGFloat opacityValues[] = {0.0, 0.3, 0.5, 0.7, 0.9};
CGFloat opacity = (level >= 0 && level <= 4) ? opacityValues[level] : 0.0;
if (level > 0) {
ensureOverlayOnMainWindow();
if (nightModeOverlay) {
nightModeOverlay.hidden = NO;
[UIView animateWithDuration:0.3 animations:^{ nightModeOverlay.alpha = opacity; }];
}
} else if (nightModeOverlay) {
[UIView animateWithDuration:0.3 animations:^{
nightModeOverlay.alpha = 0.0;
} completion:^(BOOL finished) {
if (finished) nightModeOverlay.hidden = YES;
}];
}
}
// Forward declaration for recursive summary finder
static BOOL findSummaryInNodeController(id nodeController, NSArray <NSString *> *identifiers);
// Helper function to get original value from config instance
static BOOL getValueFromInvocation(id target, SEL selector) {
IMP imp = [target methodForSelector:selector];
BOOL (*func)(id, SEL) = (BOOL (*)(id, SEL))imp;
return func(target, selector);
}
// Replacement function for A/B config boolean methods
static BOOL returnFunction(id const self, SEL _cmd) {
NSString *method = NSStringFromSelector(_cmd);
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// Check if user has overridden this setting
if ([defaults objectForKey:method]) {
return [defaults boolForKey:method];
}
// Return cached original value if not overridden
NSString *classKey = NSStringFromClass([self class]);
NSNumber *cachedValue = abConfigCache[classKey][method];
return cachedValue ? [cachedValue boolValue] : NO;
}
// Get all boolean methods from a config class
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 *encoding = method_getTypeEncoding(method);
// Only hook boolean return methods: B16@0:8
if (strcmp(encoding, "B16@0:8")) continue;
NSString *selector = [NSString stringWithUTF8String:sel_getName(method_getName(method))];
// Exclude Android and other irrelevant methods
if ([selector hasPrefix:@"android"] ||
[selector hasPrefix:@"amsterdam"] ||
[selector hasPrefix:@"kidsClient"] ||
[selector hasPrefix:@"musicClient"] ||
[selector hasPrefix:@"musicOfflineClient"] ||
[selector hasPrefix:@"unplugged"] ||
[selector rangeOfString:@"Android"].location != NSNotFound) {
continue;
}
if (![allMethods containsObject:selector])
[allMethods addObject:selector];
}
free(methods);
return allMethods;
}
// Hook all boolean methods in a config class
static void hookClass(NSObject *instance) {
if (!instance) return;
Class instanceClass = [instance class];
NSMutableArray <NSString *> *methods = getBooleanMethods(instanceClass);
NSString *classKey = NSStringFromClass(instanceClass);
// Initialize cache for this class
NSMutableDictionary *classCache = abConfigCache[classKey] = [NSMutableDictionary new];
// Hook each boolean method
for (NSString *method in methods) {
SEL selector = NSSelectorFromString(method);
// Cache the original value
BOOL result = getValueFromInvocation(instance, selector);
classCache[method] = @(result);
// Replace with our function that checks NSUserDefaults
MSHookMessageEx(instanceClass, selector, (IMP)returnFunction, NULL);
}
}
// Hook YTAppDelegate to intercept A/B config classes on app launch
%hook YTAppDelegate
- (BOOL)application:(id)arg1 didFinishLaunchingWithOptions:(id)arg2 {
BOOL result = %orig;
// Hook YouTube's A/B config classes
YTGlobalConfig *globalConfig = nil;
YTColdConfig *coldConfig = nil;
YTHotConfig *hotConfig = nil;
@try {
// Try to get config instances from app delegate
globalConfig = [self valueForKey:@"_globalConfig"];
coldConfig = [self valueForKey:@"_coldConfig"];
hotConfig = [self valueForKey:@"_hotConfig"];
} @catch (id ex) {
// Fallback: try getting from _settings
@try {
id settings = [self valueForKey:@"_settings"];
globalConfig = [settings valueForKey:@"_globalConfig"];
coldConfig = [settings valueForKey:@"_coldConfig"];
hotConfig = [settings valueForKey:@"_hotConfig"];
} @catch (id ex) {}
}
// Hook each config class
hookClass(globalConfig);
hookClass(coldConfig);
hookClass(hotConfig);
[[NSNotificationCenter defaultCenter] addObserverForName:@"YTWKSNightModeChanged"
object:nil queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) { updateNightModeOverlay(); }];
// 3 second delay before applying setting on fresh launch
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{ updateNightModeOverlay(); });
return result;
}
%end
// Fullscreen Mode (iPhone-Exclusive) - @arichornlover & @bhackel
// WARNING: Please turn off any "Portrait Fullscreen" or "iPad Layout" Options while Fullscreen Mode is enabled.
// fullscreen_mode: 0 = Off, 1 = Left, 2 = Right
%hook YTWatchViewController
- (UIInterfaceOrientationMask)allowedFullScreenOrientations {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSInteger fullscreenMode = [defaults integerForKey:@"fullscreen_mode"];
if (fullscreenMode == 1) {
return UIInterfaceOrientationMaskLandscapeLeft;
}
if (fullscreenMode == 2) {
return UIInterfaceOrientationMaskLandscapeRight;
}
return %orig;
}
%end
// Virtual bezel in landscape mode to prevent accidental video scrubbing
%hook YTMainAppVideoPlayerOverlayView
- (void)layoutSubviews {
%orig;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults boolForKey:@"virtualBezel_enabled"]) {
// Remove blocking views if feature is disabled
UIView *view = (UIView *)self;
UIView *leftBlocker = [view viewWithTag:999998];
UIView *rightBlocker = [view viewWithTag:999999];
if (leftBlocker) [leftBlocker removeFromSuperview];
if (rightBlocker) [rightBlocker removeFromSuperview];
return;
}
// Check if we're in landscape orientation
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation);
if (!isLandscape) {
// Remove blocking views if they exist when not in landscape
UIView *view = (UIView *)self;
UIView *leftBlocker = [view viewWithTag:999998];
UIView *rightBlocker = [view viewWithTag:999999];
if (leftBlocker) [leftBlocker removeFromSuperview];
if (rightBlocker) [rightBlocker removeFromSuperview];
return;
}
// Get screen dimensions
UIView *view = (UIView *)self;
CGRect screenBounds = view.bounds;
CGFloat screenWidth = screenBounds.size.width;
CGFloat screenHeight = screenBounds.size.height;
// Calculate 2177:1179 aspect ratio for clickable area (touchable region)
// This ratio extends the clickable area just enough to reach the settings button
CGFloat clickableAspectRatio = 2177.0 / 1179.0;
CGFloat clickableWidth, clickableHeight;
// Calculate clickable area based on aspect ratio
// In landscape, we want the clickable area to match the aspect ratio
if (screenWidth / screenHeight > clickableAspectRatio) {
// Screen is wider than 2177:1179, so clickable height matches screen height
clickableHeight = screenHeight;
clickableWidth = clickableHeight * clickableAspectRatio;
} else {
// Screen is taller than 2177:1179, so clickable width matches screen width
clickableWidth = screenWidth;
clickableHeight = clickableWidth / clickableAspectRatio;
}
// Center the clickable region
CGFloat clickableX = (screenWidth - clickableWidth) / 2.0;
// Only create blocking views if there's space on the sides
if (clickableX > 0) {
// Create or update left blocking view
UIView *leftBlocker = [view viewWithTag:999998];
if (!leftBlocker) {
leftBlocker = [[UIView alloc] init];
leftBlocker.tag = 999998;
leftBlocker.backgroundColor = [UIColor clearColor];
leftBlocker.userInteractionEnabled = YES;
leftBlocker.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleHeight;
[view addSubview:leftBlocker];
}
leftBlocker.frame = CGRectMake(0, 0, clickableX, screenHeight);
} else {
// Remove left blocker if no space
UIView *leftBlocker = [view viewWithTag:999998];
if (leftBlocker) [leftBlocker removeFromSuperview];
}
CGFloat rightBlockerX = clickableX + clickableWidth;
CGFloat rightBlockerWidth = screenWidth - rightBlockerX;
if (rightBlockerWidth > 0) {
// Create or update right blocking view
UIView *rightBlocker = [view viewWithTag:999999];
if (!rightBlocker) {
rightBlocker = [[UIView alloc] init];
rightBlocker.tag = 999999;
rightBlocker.backgroundColor = [UIColor clearColor];
rightBlocker.userInteractionEnabled = YES;
rightBlocker.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
[view addSubview:rightBlocker];
}
rightBlocker.frame = CGRectMake(rightBlockerX, 0, rightBlockerWidth, screenHeight);
} else {
// Remove right blocker if no space
UIView *rightBlocker = [view viewWithTag:999999];
if (rightBlocker) [rightBlocker removeFromSuperview];
}
}
%end
// Hide AI Summaries - Filter at element data level and remove cells
%hook YTIElementRenderer
- (NSData *)elementData {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults boolForKey:@"hideAISummaries_enabled"]) {
return %orig;
}
NSString *description = [self description];
// Check for AI summary related strings in element description
// Expanded patterns based on common YouTube element naming
NSArray *summaryPatterns = @[
@"summary.eml",
@"ai_summary",
@"video_summary",
@"gemini",
@"summary_button",
@"summary_card",
@"summary_shelf",
@"video_summary_card",
@"gemini_summary",
@"summary_card.eml",
@"video_summary_button",
@"ai_summary_card",
@"gemini_button"
];
for (NSString *pattern in summaryPatterns) {
if ([description containsString:pattern]) {
// Return empty data to prevent rendering
return [NSData data];
}
}
return %orig;
}
%end
// Recursive function to find summary elements in node hierarchy
static BOOL findSummaryInNodeController(id nodeController, NSArray <NSString *> *identifiers) {
if (!nodeController) return NO;
// Check if nodeController has children method
if (![nodeController respondsToSelector:@selector(children)]) {
return NO;
}
NSArray *children = [nodeController performSelector:@selector(children)];
if (!children) return NO;
for (id child in children) {
// Check ELMNodeController children
Class ELMNodeControllerClass = objc_lookUpClass("ELMNodeController");
if (ELMNodeControllerClass && [child isKindOfClass:ELMNodeControllerClass]) {
if ([child respondsToSelector:@selector(children)]) {
NSArray *elmChildren = [child performSelector:@selector(children)];
if (elmChildren) {
Class ELMComponentClass = objc_lookUpClass("ELMComponent");
for (id elmChild in elmChildren) {
if (ELMComponentClass && [elmChild isKindOfClass:ELMComponentClass]) {
NSString *desc = [elmChild description];
for (NSString *identifier in identifiers) {
if (desc && [desc containsString:identifier]) {
return YES;
}
}
}
}
}
}
}
// Check ASNodeController children
Class ASNodeControllerClass = objc_lookUpClass("ASNodeController");
if (ASNodeControllerClass && [child isKindOfClass:ASNodeControllerClass]) {
// Check yogaChildren for accessibility identifiers
if ([child respondsToSelector:@selector(node)]) {
id node = [child performSelector:@selector(node)];
Class ASDisplayNodeClass = objc_lookUpClass("ASDisplayNode");
if (ASDisplayNodeClass && [node isKindOfClass:ASDisplayNodeClass]) {
if ([node respondsToSelector:@selector(yogaChildren)]) {
NSArray *yogaChildren = [node performSelector:@selector(yogaChildren)];
if (yogaChildren) {
for (id displayNode in yogaChildren) {
if ([displayNode respondsToSelector:@selector(accessibilityIdentifier)]) {
NSString *accId = [displayNode accessibilityIdentifier];
for (NSString *identifier in identifiers) {
if (accId && [accId containsString:identifier]) {
return YES;
}
}
}
}
}
}
}
}
// Recursively search child
if (findSummaryInNodeController(child, identifiers)) {
return YES;
}
}
}
return NO;
}
// Hide AI Summaries using ASCollectionView sizeForElement hook
%hook ASCollectionView
- (CGSize)sizeForElement:(id)element {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults boolForKey:@"hideAISummaries_enabled"]) {
return %orig;
}
// Get node and controller from element
if (!element || ![element respondsToSelector:@selector(node)]) {
return %orig;
}
id node = [element performSelector:@selector(node)];
if (!node || ![node respondsToSelector:@selector(controller)]) {
return %orig;
}
id nodeController = [node performSelector:@selector(controller)];
if (!nodeController) {
return %orig;
}
// Search for summary identifiers in node hierarchy
NSArray *summaryIdentifiers = @[
@"summary",
@"ai_summary",
@"video_summary",
@"gemini",
@"summary_button",
@"summary_card",
@"summary.eml",
@"video_summary_card",
@"gemini_summary"
];
if (findSummaryInNodeController(nodeController, summaryIdentifiers)) {
// Return zero size to prevent rendering
return CGSizeZero;
}
return %orig;
}
%end
// Remove summary cells from collection view
%hook YTAsyncCollectionView
- (id)cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = %orig;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults boolForKey:@"hideAISummaries_enabled"]) {
return cell;
}
// Check _ASCollectionViewCell for summary accessibility identifiers
Class ASCollectionViewCellClass = objc_lookUpClass("_ASCollectionViewCell");
if (ASCollectionViewCellClass && [cell isKindOfClass:ASCollectionViewCellClass]) {
if ([cell respondsToSelector:@selector(node)]) {
id node = [cell performSelector:@selector(node)];
// Check top-level accessibility identifier
if (node && [node respondsToSelector:@selector(accessibilityIdentifier)]) {
NSString *identifier = [node accessibilityIdentifier];
// Check for summary-related identifiers
NSArray *summaryIdentifiers = @[
@"summary",
@"ai_summary",
@"video_summary",
@"gemini",
@"summary_button",
@"summary_card",
@"summary_shelf",
@"video_summary_card",
@"gemini_summary"
];
for (NSString *summaryId in summaryIdentifiers) {
if (identifier && [identifier containsString:summaryId]) {
id selfId = (id)self;
[selfId performSelector:@selector(removeCellsAtIndexPath:) withObject:indexPath];
return cell;
}
}
}
// Search deeper in node hierarchy
if (node && [node respondsToSelector:@selector(controller)]) {
id nodeController = [node performSelector:@selector(controller)];
NSArray *summaryIdentifiers = @[
@"summary",
@"ai_summary",
@"video_summary",
@"gemini",
@"summary_button",
@"summary_card",
@"summary.eml",
@"video_summary_card",
@"gemini_summary"
];
if (findSummaryInNodeController(nodeController, summaryIdentifiers)) {
id selfId = (id)self;
[selfId performSelector:@selector(removeCellsAtIndexPath:) withObject:indexPath];
return cell;
}
}
}
}
return cell;
}
%new(v)
- (void)removeCellsAtIndexPath:(NSIndexPath *)indexPath {
id selfId = (id)self;
[selfId performSelector:@selector(deleteItemsAtIndexPaths:) withObject:@[indexPath]];
}
%end
// Hide AI Summaries (Fallback) - Look for sparkle/star icon (✨) and Summary text in views
static BOOL containsSparkleOrSummary(UIView *view) {
// Check if view contains sparkle character or "Summary" text
if ([view isKindOfClass:[UILabel class]]) {
UILabel *label = (UILabel *)view;
if (label.text) {
// Check for sparkle character (✨) or "Summary" text
NSString *text = label.text;
if ([text containsString:@"✨"] ||
[text containsString:@"✧"] ||
[text containsString:@"✦"] ||
[text containsString:@" sparkle"] ||
[text rangeOfString:@"Summary" options:NSCaseInsensitiveSearch].location != NSNotFound) {
return YES;
}
}
}
// Check UIImageView for sparkle icon
if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imageView = (UIImageView *)view;
// Check image description
NSString *imageDesc = [imageView.image description];
if (imageDesc && ([imageDesc containsString:@"sparkle"] ||
[imageDesc containsString:@"star"] ||
[imageDesc containsString:@"magic"] ||
[imageDesc containsString:@"wand"])) {
return YES;
}
// Also check if it's a small icon (sparkle icons are typically small)
if (imageView.frame.size.width < 30 && imageView.frame.size.height < 30 && imageView.image) {
// Could be a sparkle icon - check if nearby views have "Summary"
UIView *parent = imageView.superview;
if (parent) {
for (UIView *sibling in parent.subviews) {
if ([sibling isKindOfClass:[UILabel class]]) {
UILabel *siblingLabel = (UILabel *)sibling;
if (siblingLabel.text && [siblingLabel.text rangeOfString:@"Summary" options:NSCaseInsensitiveSearch].location != NSNotFound) {
return YES;
}
}
}
}
}
}
// Check accessibility label
if (view.accessibilityLabel) {
NSString *accLabel = view.accessibilityLabel;
if ([accLabel containsString:@"✨"] ||
[accLabel containsString:@"✧"] ||
[accLabel containsString:@"✦"] ||
[accLabel rangeOfString:@"Summary" options:NSCaseInsensitiveSearch].location != NSNotFound ||
[accLabel containsString:@"sparkle"] ||
[accLabel containsString:@"gemini"]) {
return YES;
}
}
// Check accessibility identifier
if (view.accessibilityIdentifier) {
NSString *accId = view.accessibilityIdentifier;
if ([accId rangeOfString:@"summary" options:NSCaseInsensitiveSearch].location != NSNotFound ||
[accId containsString:@"gemini"] ||
[accId containsString:@"ai_summary"]) {
return YES;
}
}
return NO;
}
static void hideSummaryViewsInView(UIView *view) {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults boolForKey:@"hideAISummaries_enabled"]) {
return;
}
// Search for views containing sparkle icon or "Summary" text
for (UIView *subview in view.subviews) {
if (containsSparkleOrSummary(subview)) {
// Found sparkle icon or Summary text - hide the parent container
UIView *container = subview.superview;
int levelsUp = 0;
// Go up to find the actual button/card container
while (container && container != view && levelsUp < 6) {
// Check if this looks like a button or card container
if ([container isKindOfClass:[UIButton class]] ||
container.backgroundColor ||
container.layer.cornerRadius > 0 ||
container.frame.size.height > 25) { // Likely the summary card/button
container.hidden = YES;
// Collapse the view by setting height to 0
CGRect frame = container.frame;
frame.size.height = 0;
container.frame = frame;
break;
}
container = container.superview;
levelsUp++;
}
// Fallback: hide immediate parent if we didn't find a specific container
if (levelsUp >= 6 && subview.superview) {
subview.superview.hidden = YES;
CGRect frame = subview.superview.frame;
frame.size.height = 0;
subview.superview.frame = frame;
}
}
// Also check if this view itself should be hidden
if (containsSparkleOrSummary(subview)) {
// Check siblings for "Summary" text if we found a sparkle icon
UIView *parent = subview.superview;
if (parent) {
for (UIView *sibling in parent.subviews) {
if (sibling != subview && containsSparkleOrSummary(sibling)) {
// Found both sparkle and summary in same container - hide it
parent.hidden = YES;
CGRect frame = parent.frame;
frame.size.height = 0;
parent.frame = frame;
break;
}
}
}
}
// Recursively check subviews (limit depth to avoid performance issues)
if (subview.subviews.count > 0 && subview.subviews.count < 50) {
hideSummaryViewsInView(subview);
}
}
}
// Hook collection view cells to hide summary views after layout
%hook UICollectionViewCell
- (void)layoutSubviews {
%orig;
hideSummaryViewsInView(self);
}
%end
// Fix Casting: https://github.com/arichornlover/uYouEnhanced/issues/606#issuecomment-2098289942
%hook YTColdConfig
- (BOOL)cxClientEnableIosLocalNetworkPermissionReliabilityFixes {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:@"fixCasting_enabled"]) {
return YES;
}
return %orig;
}
- (BOOL)cxClientEnableIosLocalNetworkPermissionUsingSockets {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:@"fixCasting_enabled"]) {
return NO;
}
return %orig;
}
- (BOOL)cxClientEnableIosLocalNetworkPermissionWifiFixes {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:@"fixCasting_enabled"]) {
return YES;
}
return %orig;
}
%end
%hook YTHotConfig
- (BOOL)isPromptForLocalNetworkPermissionsEnabled {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:@"fixCasting_enabled"]) {
return YES;
}
return %orig;
}
%end
%ctor {
[[NSBundle bundleWithPath:[NSString stringWithFormat:@"%@/Frameworks/Module_Framework.framework", [[NSBundle mainBundle] bundlePath]]] load];
// Initialize A/B config cache
abConfigCache = [NSMutableDictionary new];
%init;
}
%dtor {
// Clean up cache on unload
[abConfigCache removeAllObjects];
}