mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 12:33:57 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			833 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			833 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| #import <CommonCrypto/CommonDigest.h>
 | |
| #import <Foundation/Foundation.h>
 | |
| #import <UIKit/UIKit.h>
 | |
| #import <HBLog.h>
 | |
| #import <rootless.h>
 | |
| #import "unicode/unum.h"
 | |
| #import "Tweak.h"
 | |
| 
 | |
| #define TWEAK_NAME @"Return YouTube Dislike"
 | |
| #define maxRetryCount 3
 | |
| #define apiUrl @"https://returnyoutubedislikeapi.com"
 | |
| #define UserIDKey @"RYD-USER-ID"
 | |
| #define RegistrationConfirmedKey @"RYD-USER-REGISTERED"
 | |
| #define EnabledKey @"RYD-ENABLED"
 | |
| #define EnableVoteSubmissionKey @"RYD-VOTE-SUBMISSION"
 | |
| #define ExactLikeKey @"RYD-EXACT-LIKE-NUMBER"
 | |
| #define ExactDislikeKey @"RYD-EXACT-NUMBER"
 | |
| #define DidShowEnableVoteSubmissionAlertKey @"RYD-DID-SHOW-VOTE-SUBMISSION-ALERT"
 | |
| #define FETCHING @"⌛"
 | |
| #define FAILED @"❌"
 | |
| 
 | |
| #define _LOC(b, x) [b localizedStringForKey:x value:nil table:nil]
 | |
| #define LOC(x) _LOC(tweakBundle, x)
 | |
| 
 | |
| static const NSInteger RYDSection = 1080;
 | |
| 
 | |
| @interface YTSettingsSectionItemManager (RYD)
 | |
| - (void)updateRYDSectionWithEntry:(id)entry;
 | |
| @end
 | |
| 
 | |
| static NSCache <NSString *, NSDictionary *> *cache;
 | |
| 
 | |
| void (*ASNodeContextPush)(ASNodeContext *);
 | |
| void (*ASNodeContextPop)(void);
 | |
| 
 | |
| NSBundle *RYDBundle() {
 | |
|     static NSBundle *bundle = nil;
 | |
|     static dispatch_once_t onceToken;
 | |
| 	dispatch_once(&onceToken, ^{
 | |
|         NSString *tweakBundlePath = [[NSBundle mainBundle] pathForResource:@"RYD" ofType:@"bundle"];
 | |
|         if (tweakBundlePath)
 | |
|             bundle = [NSBundle bundleWithPath:tweakBundlePath];
 | |
|         else
 | |
|             bundle = [NSBundle bundleWithPath:ROOT_PATH_NS(@"/Library/Application Support/RYD.bundle")];
 | |
|     });
 | |
|     return bundle;
 | |
| }
 | |
| 
 | |
| static int toRYDLikeStatus(YTLikeStatus likeStatus) {
 | |
|     switch (likeStatus) {
 | |
|         case YTLikeStatusLike:
 | |
|             return 1;
 | |
|         case YTLikeStatusDislike:
 | |
|             return -1;
 | |
|         default:
 | |
|             return 0;
 | |
|     }
 | |
| }
 | |
| 
 | |
| static NSString *getUserID() {
 | |
|     return [[NSUserDefaults standardUserDefaults] stringForKey:UserIDKey];
 | |
| }
 | |
| 
 | |
| static BOOL isRegistered() {
 | |
|     return [[NSUserDefaults standardUserDefaults] boolForKey:RegistrationConfirmedKey];
 | |
| }
 | |
| 
 | |
| static BOOL TweakEnabled() {
 | |
|     return [[NSUserDefaults standardUserDefaults] boolForKey:EnabledKey];
 | |
| }
 | |
| 
 | |
| static BOOL VoteSubmissionEnabled() {
 | |
|     return [[NSUserDefaults standardUserDefaults] boolForKey:EnableVoteSubmissionKey];
 | |
| }
 | |
| 
 | |
| static BOOL ExactLikeNumber() {
 | |
|     return [[NSUserDefaults standardUserDefaults] boolForKey:ExactLikeKey];
 | |
| }
 | |
| 
 | |
| static BOOL ExactDislikeNumber() {
 | |
|     return [[NSUserDefaults standardUserDefaults] boolForKey:ExactDislikeKey];
 | |
| }
 | |
| 
 | |
| static const char *charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 | |
| 
 | |
| // Ported to objc from RYD browser extension
 | |
| static NSString *generateUserID() {
 | |
|     NSString *existingID = getUserID();
 | |
|     if (existingID) return existingID;
 | |
|     HBLogDebug(@"generateUserID()");
 | |
|     char userID[36 + 1];
 | |
|     for (int i = 0; i < 36; ++i)
 | |
|         userID[i] = charset[arc4random_uniform(64)];
 | |
|     userID[36] = '\0';
 | |
|     NSString *result = [NSString stringWithUTF8String:userID];
 | |
|     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
 | |
|     [defaults setObject:result forKey:UserIDKey];
 | |
|     [defaults synchronize];
 | |
|     return result;
 | |
| }
 | |
| 
 | |
| // Ported to objc from RYD browser extension
 | |
| static int countLeadingZeroes(uint8_t *hash) {
 | |
|     int zeroes = 0;
 | |
|     int value = 0;
 | |
|     for (int i = 0; i < CC_SHA512_DIGEST_LENGTH; i++) {
 | |
|         value = hash[i];
 | |
|         if (value == 0)
 | |
|             zeroes += 8;
 | |
|         else {
 | |
|             int count = 1;
 | |
|             if (value >> 4 == 0) {
 | |
|                 count += 4;
 | |
|                 value <<= 4;
 | |
|             }
 | |
|             if (value >> 6 == 0) {
 | |
|                 count += 2;
 | |
|                 value <<= 2;
 | |
|             }
 | |
|             zeroes += count - (value >> 7);
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
|     return zeroes;
 | |
| }
 | |
| 
 | |
| // Ported to objc from RYD browser extension
 | |
| static NSString *btoa(NSString *input) {
 | |
|     NSMutableString *output = [NSMutableString string];
 | |
|     for (int i = 0; i < input.length; i += 3) {
 | |
|         int groupsOfSix[4] = { -1, -1, -1, -1 };
 | |
|         unichar ci = [input characterAtIndex:i];
 | |
|         groupsOfSix[0] = ci >> 2;
 | |
|         groupsOfSix[1] = (ci & 0x03) << 4;
 | |
|         if (input.length > i + 1) {
 | |
|             unichar ci1 = [input characterAtIndex:i + 1];
 | |
|             groupsOfSix[1] |= ci1 >> 4;
 | |
|             groupsOfSix[2] = (ci1 & 0x0f) << 2;
 | |
|         }
 | |
|         if (input.length > i + 2) {
 | |
|             unichar ci2 = [input characterAtIndex:i + 2];
 | |
|             groupsOfSix[2] |= ci2 >> 6;
 | |
|             groupsOfSix[3] = ci2 & 0x3f;
 | |
|         }
 | |
|         for (int j = 0; j < 4; ++j) {
 | |
|             if (groupsOfSix[j] == -1)
 | |
|                 [output appendString:@"="];
 | |
|             else
 | |
|                 [output appendFormat:@"%c", charset[groupsOfSix[j]]];
 | |
|         }
 | |
|     }
 | |
|     return output;
 | |
| }
 | |
| 
 | |
| static void fetch(
 | |
|     NSString *endpoint,
 | |
|     NSString *method,
 | |
|     NSDictionary *body,
 | |
|     void (^dataHandler)(NSDictionary *data),
 | |
|     BOOL (^responseCodeHandler)(NSUInteger responseCode),
 | |
|     void (^networkErrorHandler)(void),
 | |
|     void (^dataErrorHandler)(void)
 | |
| ) {
 | |
|     NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", apiUrl, endpoint]];
 | |
|     NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
 | |
|     NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
 | |
|     urlRequest.HTTPMethod = method;
 | |
|     if (body) {
 | |
|         [urlRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
 | |
|         NSError *error = nil;
 | |
|         NSData *data = [NSJSONSerialization dataWithJSONObject:body options:NSJSONWritingPrettyPrinted error:&error];
 | |
|         if (error) {
 | |
|             if (dataErrorHandler) {
 | |
|                 dispatch_async(dispatch_get_main_queue(), ^{
 | |
|                     dataErrorHandler();
 | |
|                 });
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
|         HBLogDebug(@"fetch() POST body: %@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
 | |
|         urlRequest.HTTPBody = data;
 | |
|     } else
 | |
|         [urlRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
 | |
|     [[session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
 | |
|         NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
 | |
|         NSUInteger responseCode = [httpResponse statusCode];
 | |
|         if (responseCodeHandler) {
 | |
|             if (!responseCodeHandler(responseCode))
 | |
|                 return;
 | |
|         }
 | |
|         if (error || responseCode != 200) {
 | |
|             HBLogDebug(@"fetch() error requesting: %@ (%lu)", error, responseCode);
 | |
|             if (networkErrorHandler) {
 | |
|                 dispatch_async(dispatch_get_main_queue(), ^{
 | |
|                     networkErrorHandler();
 | |
|                 });
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
|         NSError *jsonError;
 | |
|         NSDictionary *myData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingFragmentsAllowed error:&jsonError];
 | |
|         if (jsonError) {
 | |
|             HBLogDebug(@"fetch() error decoding response: %@", jsonError);
 | |
|             if (dataErrorHandler) {
 | |
|                 dispatch_async(dispatch_get_main_queue(), ^{
 | |
|                     dataErrorHandler();
 | |
|                 });
 | |
|             }
 | |
|             return;
 | |
|         }
 | |
|         dispatch_async(dispatch_get_main_queue(), ^{
 | |
|             dataHandler(myData);
 | |
|         });
 | |
|     }] resume];
 | |
| }
 | |
| 
 | |
| // Ported to objc from RYD browser extension
 | |
| static NSString *solvePuzzle(NSDictionary *data) {
 | |
|     NSString *solution = nil;
 | |
|     NSString *challenge = data[@"challenge"];
 | |
|     int difficulty = [data[@"difficulty"] intValue];
 | |
|     NSData *cd = [[NSData alloc] initWithBase64EncodedString:challenge options:0];
 | |
|     NSString *decoded = [[NSString alloc] initWithData:cd encoding:NSASCIIStringEncoding];
 | |
|     uint8_t c[decoded.length];
 | |
|     char *buffer = (char *)calloc(20, sizeof(char));
 | |
|     uint32_t *uInt32View = (uint32_t *)buffer;
 | |
|     for (int i = 0; i < decoded.length; ++i)
 | |
|         c[i] = [decoded characterAtIndex:i];
 | |
|     int maxCount = (1 << difficulty) * 3;
 | |
|     for (int i = 4; i < 20; ++i)
 | |
|         buffer[i] = c[i - 4];
 | |
|     for (int i = 0; i < maxCount; ++i) {
 | |
|         uInt32View[0] = i;
 | |
|         uint8_t hash[CC_SHA512_DIGEST_LENGTH] = {0};
 | |
|         CC_SHA512(buffer, 20, hash);
 | |
|         if (countLeadingZeroes(hash) >= difficulty) {
 | |
|             char chars[4] = { buffer[0], buffer[1], buffer[2], buffer[3] };
 | |
|             NSString *s = [[NSString alloc] initWithBytes:chars length:4 encoding:NSASCIIStringEncoding];
 | |
|             solution = btoa(s);
 | |
|             HBLogDebug(@"solvePuzzle() success (%@)", solution);
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
|     free(buffer);
 | |
|     if (!solution)
 | |
|         HBLogDebug(@"solvePuzzle() failed");
 | |
|     return solution;
 | |
| }
 | |
| 
 | |
| // Ported to objc from RYD browser extension
 | |
| static void registerUser() {
 | |
|     NSString *userId = generateUserID();
 | |
|     HBLogDebug(@"registerUser() (%@)", userId);
 | |
|     NSString *puzzleEndpoint = [NSString stringWithFormat:@"/puzzle/registration?userId=%@", userId];
 | |
|     fetch(
 | |
|         puzzleEndpoint,
 | |
|         @"GET",
 | |
|         nil,
 | |
|         ^(NSDictionary *data) {
 | |
|             NSString *solution = solvePuzzle(data);
 | |
|             if (!solution) {
 | |
|                 HBLogDebug(@"registerUser() skipped");
 | |
|                 return;
 | |
|             }
 | |
|             fetch(
 | |
|                 puzzleEndpoint,
 | |
|                 @"POST",
 | |
|                 @{ @"solution": solution },
 | |
|                 ^(NSDictionary *data) {
 | |
|                     if ([data isKindOfClass:[NSNumber class]] && ![(NSNumber *)data boolValue]) {
 | |
|                         HBLogInfo(@"registerUser() failed");
 | |
|                         return;
 | |
|                     }
 | |
|                     if (!isRegistered()) {
 | |
|                         [[NSUserDefaults standardUserDefaults] setBool:YES forKey:RegistrationConfirmedKey];
 | |
|                         [[NSUserDefaults standardUserDefaults] synchronize];
 | |
|                     }
 | |
|                     HBLogDebug(@"registerUser() success or already registered");
 | |
|                 },
 | |
|                 NULL,
 | |
|                 ^() {
 | |
|                     HBLogDebug(@"registerUser() puzzle failed (network)");
 | |
|                 },
 | |
|                 ^() {
 | |
|                     HBLogDebug(@"registerUser() puzzle failed (data)");
 | |
|                 }
 | |
|             );
 | |
|         },
 | |
|         NULL,
 | |
|         ^() {
 | |
|             HBLogDebug(@"registerUser() failed (network)");
 | |
|         },
 | |
|         ^() {
 | |
|             HBLogDebug(@"registerUser() failed (data)");
 | |
|         }
 | |
|     );
 | |
| }
 | |
| 
 | |
| // Ported to objc from RYD browser extension
 | |
| static void sendVote(NSString *videoId, YTLikeStatus s) {
 | |
|     NSString *userId = getUserID();
 | |
|     if (!userId || !isRegistered()) {
 | |
|         registerUser();
 | |
|         return;
 | |
|     }
 | |
|     int likeStatus = toRYDLikeStatus(s);
 | |
|     HBLogDebug(@"sendVote(%@, %d)", videoId, likeStatus);
 | |
|     fetch(
 | |
|         @"/interact/vote",
 | |
|         @"POST",
 | |
|         @{
 | |
|             @"userId": userId,
 | |
|             @"videoId": videoId,
 | |
|             @"value": @(likeStatus)
 | |
|         },
 | |
|         ^(NSDictionary *data) {
 | |
|             NSString *solution = solvePuzzle(data);
 | |
|             if (!solution) {
 | |
|                 HBLogDebug(@"sendVote() skipped");
 | |
|                 return;
 | |
|             }
 | |
|             fetch(
 | |
|                 @"/interact/confirmVote",
 | |
|                 @"POST",
 | |
|                 @{
 | |
|                     @"userId": userId,
 | |
|                     @"videoId": videoId,
 | |
|                     @"solution": solution
 | |
|                 },
 | |
|                 ^(NSDictionary *data) {
 | |
|                     HBLogDebug(@"sendVote() success");
 | |
|                 },
 | |
|                 NULL,
 | |
|                 ^() {
 | |
|                     HBLogDebug(@"sendVote() confirm failed (network)");
 | |
|                 },
 | |
|                 ^() {
 | |
|                     HBLogDebug(@"sendVote() confirm failed (data)");
 | |
|                 }
 | |
|             );
 | |
|         },
 | |
|         ^BOOL(NSUInteger responseCode) {
 | |
|             if (responseCode == 401) {
 | |
|                 HBLogDebug(@"sendVote() error 401, trying again");
 | |
|                 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 | |
|                     registerUser();
 | |
|                     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
 | |
|                         sendVote(videoId, s);
 | |
|                     });
 | |
|                 });
 | |
|                 return NO;
 | |
|             }
 | |
|             return YES;
 | |
|         },
 | |
|         ^() {
 | |
|             HBLogDebug(@"sendVote() failed (network)");
 | |
|         },
 | |
|         ^() {
 | |
|             HBLogDebug(@"sendVote() failed (data)");
 | |
|         }
 | |
|     );
 | |
| }
 | |
| 
 | |
| static NSString *formattedLongNumber(NSNumber *number, NSString *error) {
 | |
|     return error ?: [NSNumberFormatter localizedStringFromNumber:number numberStyle:NSNumberFormatterDecimalStyle];
 | |
| }
 | |
| 
 | |
| static NSString *getXPointYFormat(NSString *count, char c) {
 | |
|     char firstInt = [count characterAtIndex:0];
 | |
|     char secondInt = [count characterAtIndex:1];
 | |
|     if (secondInt == '0')
 | |
|         return [NSString stringWithFormat:@"%c%c", firstInt, c];
 | |
|     return [NSString stringWithFormat:@"%c.%c%c", firstInt, secondInt, c];
 | |
| }
 | |
| 
 | |
| // https://gist.github.com/danpashin/5951706a6aa25748a7faa1acd5c1db8b
 | |
| API_AVAILABLE(ios(13))
 | |
| static NSString *formattedShortNumber(int64_t number) {
 | |
|     UErrorCode status;
 | |
|     status = U_ZERO_ERROR;
 | |
|     NSString *currentLocale = [[[NSLocale preferredLanguages] firstObject] stringByReplacingOccurrencesOfString:@"-" withString:@"_"];
 | |
|     UNumberFormat *formatter = unum_open(UNUM_DECIMAL_COMPACT_SHORT, NULL, 0, [currentLocale UTF8String], NULL, &status);
 | |
|     assert(!U_FAILURE(status));
 | |
|     status = U_ZERO_ERROR;
 | |
|     int32_t used = unum_formatInt64(formatter, number, NULL, 0, NULL, &status);
 | |
|     NSString *resultString = nil;
 | |
|     if (status == U_BUFFER_OVERFLOW_ERROR) {
 | |
|         NSUInteger length = sizeof(UChar) * (NSUInteger)used;
 | |
|         UChar *ustr = (UChar *)CFAllocatorAllocate(kCFAllocatorSystemDefault, (CFIndex)length + 1, 0);
 | |
|         status = U_ZERO_ERROR;
 | |
|         unum_formatInt64(formatter, number, ustr, used, NULL, &status);
 | |
|         resultString = [[NSString alloc] initWithBytesNoCopy:ustr length:length encoding:NSUTF16LittleEndianStringEncoding freeWhenDone:YES];
 | |
|     }
 | |
|     unum_close(formatter);
 | |
|     formatter = NULL;
 | |
|     return resultString;
 | |
| }
 | |
| 
 | |
| static NSString *getNormalizedDislikes(NSNumber *dislikeNumber, NSString *error) {
 | |
|     if (!dislikeNumber) return FAILED;
 | |
|     if (error) return error;
 | |
|     if (ExactDislikeNumber())
 | |
|         return formattedLongNumber(dislikeNumber, nil);
 | |
|     NSString *dislikeCount = [dislikeNumber stringValue];
 | |
|     NSUInteger digits = dislikeCount.length;
 | |
|     if (digits <= 3) // 0 - 999
 | |
|         return dislikeCount;
 | |
|     if (@available(iOS 13.0, *))
 | |
|         return formattedShortNumber([dislikeNumber unsignedIntegerValue]);
 | |
|     if (digits == 4) // 1000 - 9999
 | |
|         return getXPointYFormat(dislikeCount, 'K');
 | |
|     if (digits <= 6) // 10_000 - 999_999
 | |
|         return [NSString stringWithFormat:@"%@K", [dislikeCount substringToIndex:digits - 3]];
 | |
|     if (digits <= 9) // 1_000_000 - 999_999_999
 | |
|         return [NSString stringWithFormat:@"%@M", [dislikeCount substringToIndex:digits - 6]];
 | |
|     return [NSString stringWithFormat:@"%@B", [dislikeCount substringToIndex:digits - 9]]; // 1_000_000_000+
 | |
| }
 | |
| 
 | |
| static void getVoteFromVideoWithHandler(NSString *videoId, int retryCount, void (^handler)(NSDictionary *d, NSString *error)) {
 | |
|     if (retryCount <= 0) return;
 | |
|     NSDictionary *data = [cache objectForKey:videoId];
 | |
|     if (data) {
 | |
|         handler(data, nil);
 | |
|         return;
 | |
|     }
 | |
|     fetch(
 | |
|         [NSString stringWithFormat:@"/votes?videoId=%@", videoId],
 | |
|         @"GET",
 | |
|         nil,
 | |
|         ^(NSDictionary *data) {
 | |
|             [cache setObject:data forKey:videoId];
 | |
|             handler(data, nil);
 | |
|         },
 | |
|         ^BOOL(NSUInteger responseCode) {
 | |
|             if (responseCode == 502 || responseCode == 503) {
 | |
|                 handler(nil, @"CON"); // connection error
 | |
|                 return NO;
 | |
|             }
 | |
|             if (responseCode == 401 || responseCode == 403 || responseCode == 407) {
 | |
|                 handler(nil, @"AUTH"); // unauthorized
 | |
|                 return NO;
 | |
|             }
 | |
|             if (responseCode == 429) {
 | |
|                 handler(nil, @"RL"); // rate limit
 | |
|                 return NO;
 | |
|             }
 | |
|             if (responseCode == 404) {
 | |
|                 handler(nil, @"NULL"); // non-existing video
 | |
|                 return NO;
 | |
|             }
 | |
|             if (responseCode == 400) {
 | |
|                 handler(nil, @"INV"); // malformed video
 | |
|                 return NO;
 | |
|             }
 | |
|             return YES;
 | |
|         },
 | |
|         ^() {
 | |
|             handler(nil, FAILED);
 | |
|             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 | |
|                 getVoteFromVideoWithHandler(videoId, retryCount - 1, handler);
 | |
|             });
 | |
|         },
 | |
|         ^() {
 | |
|             handler(nil, FAILED);
 | |
|         }
 | |
|     );
 | |
| }
 | |
| 
 | |
| %hook YTSlimVideoDetailsActionView
 | |
| 
 | |
| + (YTSlimVideoDetailsActionView *)actionViewWithSlimMetadataButtonSupportedRenderer:(YTISlimMetadataButtonSupportedRenderers *)renderer withElementsContextBlock:(id)block {
 | |
|     if ([renderer rendererOneOfCase] == 153515154 && TweakEnabled())
 | |
|         return [[%c(YTSlimVideoDetailsActionView) alloc] initWithSlimMetadataButtonSupportedRenderer:renderer];
 | |
|     return %orig;
 | |
| }
 | |
| 
 | |
| - (id)initWithSlimMetadataButtonSupportedRenderer:(id)arg1 {
 | |
|     self = %orig;
 | |
|     if (self && TweakEnabled()) {
 | |
|         YTISlimMetadataButtonSupportedRenderers *renderer = [self valueForKey:@"_supportedRenderer"];
 | |
|         if ((ExactLikeNumber() && [renderer slimButton_isLikeButton]) || [renderer slimButton_isDislikeButton]) {
 | |
|             YTISlimMetadataToggleButtonRenderer *meta = renderer.slimMetadataToggleButtonRenderer;
 | |
|             getVoteFromVideoWithHandler(meta.target.videoId, maxRetryCount, ^(NSDictionary *data, NSString *error) {
 | |
|                 dispatch_async(dispatch_get_main_queue(), ^{
 | |
|                     if ([renderer slimButton_isDislikeButton])
 | |
|                         [self.label setFormattedString:[%c(YTIFormattedString) formattedStringWithString:getNormalizedDislikes(data[@"dislikes"], error)]];
 | |
|                     else if ([renderer slimButton_isLikeButton] && error == nil)
 | |
|                         [self.label setFormattedString:[%c(YTIFormattedString) formattedStringWithString:formattedLongNumber(data[@"likes"], nil)]];
 | |
|                     [self setNeedsLayout];
 | |
|                 });
 | |
|             });
 | |
|         }
 | |
|     }
 | |
|     return self;
 | |
| }
 | |
| 
 | |
| - (void)setToggled:(BOOL)toggled {
 | |
|     if (!TweakEnabled()) {
 | |
|         %orig;
 | |
|         return;
 | |
|     }
 | |
|     YTISlimMetadataButtonSupportedRenderers *renderer = [self valueForKey:@"_supportedRenderer"];
 | |
|     BOOL isLikeButton = ExactLikeNumber() && [renderer slimButton_isLikeButton];
 | |
|     BOOL isDislikeButton = [renderer slimButton_isDislikeButton];
 | |
|     YTISlimMetadataToggleButtonRenderer *meta = renderer.slimMetadataToggleButtonRenderer;
 | |
|     YTIToggleButtonRenderer *buttonRenderer = meta.button.toggleButtonRenderer;
 | |
|     BOOL changed = NO;
 | |
|     if (isLikeButton || isDislikeButton) {
 | |
|         changed = self.toggled != toggled;
 | |
|         YTIFormattedString *formattedText = [%c(YTIFormattedString) formattedStringWithString:FETCHING];
 | |
|         buttonRenderer.defaultText = formattedText;
 | |
|         buttonRenderer.toggledText = formattedText;
 | |
|     }
 | |
|     %orig;
 | |
|     if (changed && (isLikeButton || isDislikeButton)) {
 | |
|         getVoteFromVideoWithHandler(meta.target.videoId, maxRetryCount, ^(NSDictionary *data, NSString *error) {
 | |
|             NSString *defaultText = isDislikeButton ? getNormalizedDislikes(data[@"dislikes"], error) : formattedLongNumber(data[@"likes"], error);
 | |
|             NSString *toggledText = isDislikeButton ? getNormalizedDislikes(@([data[@"dislikes"] unsignedIntegerValue] + 1), error) : formattedLongNumber(@([data[@"likes"] unsignedIntegerValue] + 1), error);
 | |
|             YTIFormattedString *formattedDefaultText = [%c(YTIFormattedString) formattedStringWithString:defaultText];
 | |
|             YTIFormattedString *formattedToggledText = [%c(YTIFormattedString) formattedStringWithString:toggledText];
 | |
|             buttonRenderer.defaultText = formattedDefaultText;
 | |
|             buttonRenderer.toggledText = formattedToggledText;
 | |
|             if ([self isKindOfClass:%c(YTSlimVideoDetailsActionView)]) {
 | |
|                 [self.label setFormattedString:toggled ? formattedToggledText : formattedDefaultText];
 | |
|                 [self setNeedsLayout];
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %hook YTFullscreenEngagementActionBarButtonView
 | |
| 
 | |
| - (void)updateButtonAndLabelForToggled:(BOOL)toggled {
 | |
|     if (!TweakEnabled()) {
 | |
|         %orig;
 | |
|         return;
 | |
|     }
 | |
|     YTFullscreenEngagementActionBarButtonRenderer *renderer = [self valueForKey:@"_buttonRenderer"];
 | |
|     BOOL isLikeButton = ExactLikeNumber() && [renderer isLikeButton];
 | |
|     BOOL isDislikeButton = [renderer isDislikeButton];
 | |
|     YTISlimMetadataToggleButtonRenderer *meta = [renderer valueForKey:@"_toggleButtonRenderer"];
 | |
|     YTIToggleButtonRenderer *buttonRenderer = meta.button.toggleButtonRenderer;
 | |
|     if (isLikeButton || isDislikeButton) {
 | |
|         YTIFormattedString *formattedText = [%c(YTIFormattedString) formattedStringWithString:FETCHING];
 | |
|         buttonRenderer.defaultText = formattedText;
 | |
|         buttonRenderer.toggledText = formattedText;
 | |
|     }
 | |
|     %orig;
 | |
|     if (isLikeButton || isDislikeButton) {
 | |
|         getVoteFromVideoWithHandler(meta.target.videoId, maxRetryCount, ^(NSDictionary *data, NSString *error) {
 | |
|             NSString *defaultText = isDislikeButton ? getNormalizedDislikes(data[@"dislikes"], error) : formattedLongNumber(data[@"likes"], error);
 | |
|             NSString *toggledText = isDislikeButton ? getNormalizedDislikes(@([data[@"dislikes"] unsignedIntegerValue] + 1), error) : formattedLongNumber(@([data[@"likes"] unsignedIntegerValue] + 1), error);
 | |
|             YTIFormattedString *formattedDefaultText = [%c(YTIFormattedString) formattedStringWithString:defaultText];
 | |
|             YTIFormattedString *formattedToggledText = [%c(YTIFormattedString) formattedStringWithString:toggledText];
 | |
|             buttonRenderer.defaultText = formattedDefaultText;
 | |
|             buttonRenderer.toggledText = formattedToggledText;
 | |
|             if ([self isKindOfClass:%c(YTFullscreenEngagementActionBarButtonView)]) {
 | |
|                 [self.label setFormattedString:toggled ? formattedToggledText : formattedDefaultText];
 | |
|                 [self setNeedsLayout];
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %hook YTReelWatchLikesController
 | |
| 
 | |
| - (void)updateLikeButtonWithRenderer:(YTILikeButtonRenderer *)renderer {
 | |
|     %orig;
 | |
|     if (!TweakEnabled()) return;
 | |
|     YTQTMButton *dislikeButton = self.dislikeButton;
 | |
|     [dislikeButton setTitle:FETCHING forState:UIControlStateNormal];
 | |
|     [dislikeButton setTitle:FETCHING forState:UIControlStateSelected];
 | |
|     YTLikeStatus likeStatus = renderer.likeStatus;
 | |
|     getVoteFromVideoWithHandler(renderer.target.videoId, maxRetryCount, ^(NSDictionary *data, NSString *error) {
 | |
|         NSString *formattedDislikeCount = getNormalizedDislikes(data[@"dislikes"], error);
 | |
|         NSString *formattedToggledDislikeCount = getNormalizedDislikes(@([data[@"dislikes"] unsignedIntegerValue] + 1), error);
 | |
|         YTIFormattedString *formattedText = [%c(YTIFormattedString) formattedStringWithString:formattedDislikeCount];
 | |
|         YTIFormattedString *formattedToggledText = [%c(YTIFormattedString) formattedStringWithString:formattedToggledDislikeCount];
 | |
|         if (renderer.hasDislikeCountText)
 | |
|             renderer.dislikeCountText = formattedText;
 | |
|         if (renderer.hasDislikeCountWithDislikeText)
 | |
|             renderer.dislikeCountWithDislikeText = formattedToggledText;
 | |
|         if (renderer.hasDislikeCountWithUndislikeText)
 | |
|             renderer.dislikeCountWithUndislikeText = formattedText;
 | |
|         dispatch_async(dispatch_get_main_queue(), ^{
 | |
|             if (likeStatus == YTLikeStatusDislike) {
 | |
|                 [dislikeButton setTitle:[renderer.dislikeCountWithUndislikeText stringWithFormattingRemoved] forState:UIControlStateNormal];
 | |
|                 [dislikeButton setTitle:[renderer.dislikeCountText stringWithFormattingRemoved] forState:UIControlStateSelected];
 | |
|             } else {
 | |
|                 [dislikeButton setTitle:[renderer.dislikeCountText stringWithFormattingRemoved] forState:UIControlStateNormal];
 | |
|                 [dislikeButton setTitle:[renderer.dislikeCountWithDislikeText stringWithFormattingRemoved] forState:UIControlStateSelected];
 | |
|             }
 | |
|         });
 | |
|         if (ExactLikeNumber() && error == nil) {
 | |
|             YTQTMButton *likeButton = self.likeButton;
 | |
|             NSString *formattedLikeCount = formattedLongNumber(data[@"likes"], nil);
 | |
|             NSString *formattedToggledLikeCount = getNormalizedDislikes(@([data[@"likes"] unsignedIntegerValue] + 1), nil);
 | |
|             YTIFormattedString *formattedText = [%c(YTIFormattedString) formattedStringWithString:formattedLikeCount];
 | |
|             YTIFormattedString *formattedToggledText = [%c(YTIFormattedString) formattedStringWithString:formattedToggledLikeCount];
 | |
|             if (renderer.hasLikeCountText)
 | |
|                 renderer.likeCountText = formattedText;
 | |
|             if (renderer.hasLikeCountWithLikeText)
 | |
|                 renderer.likeCountWithLikeText = formattedToggledText;
 | |
|             if (renderer.hasLikeCountWithUnlikeText)
 | |
|                 renderer.likeCountWithUnlikeText = formattedText;
 | |
|             dispatch_async(dispatch_get_main_queue(), ^{
 | |
|                 if (likeStatus == YTLikeStatusLike) {
 | |
|                     [likeButton setTitle:[renderer.likeCountWithUnlikeText stringWithFormattingRemoved] forState:UIControlStateNormal];
 | |
|                     [likeButton setTitle:[renderer.likeCountText stringWithFormattingRemoved] forState:UIControlStateSelected];
 | |
|                 } else {
 | |
|                     [likeButton setTitle:[renderer.likeCountText stringWithFormattingRemoved] forState:UIControlStateNormal];
 | |
|                     [likeButton setTitle:[renderer.likeCountWithLikeText stringWithFormattingRemoved] forState:UIControlStateSelected];
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %hook YTLikeService
 | |
| 
 | |
| - (void)makeRequestWithStatus:(YTLikeStatus)likeStatus target:(YTILikeTarget *)target clickTrackingParams:(id)arg3 requestParams:(id)arg4 responseBlock:(id)arg5 errorBlock:(id)arg6 {
 | |
|     if (TweakEnabled() && VoteSubmissionEnabled())
 | |
|         sendVote(target.videoId, likeStatus);
 | |
|     %orig;
 | |
| }
 | |
| 
 | |
| - (void)makeRequestWithStatus:(YTLikeStatus)likeStatus target:(YTILikeTarget *)target clickTrackingParams:(id)arg3 queueContextParams:(id)arg4 requestParams:(id)arg5 responseBlock:(id)arg6 errorBlock:(id)arg7 {
 | |
|     if (TweakEnabled() && VoteSubmissionEnabled())
 | |
|         sendVote(target.videoId, likeStatus);
 | |
|     %orig;
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %hook ELMTextNode
 | |
| 
 | |
| %property (assign) BOOL blockUpdate;
 | |
| 
 | |
| - (void)updatedAttributedText {
 | |
|     if (self.blockUpdate) return;
 | |
|     %orig;
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %hook _ASDisplayView
 | |
| 
 | |
| - (void)didMoveToSuperview {
 | |
|     %orig;
 | |
|     if (!TweakEnabled()) return;
 | |
|     int mode = 0;
 | |
|     BOOL pair = NO;
 | |
|     BOOL exactLikeNumber = ExactLikeNumber();
 | |
|     ELMContainerNode *node = (ELMContainerNode *)self.keepalive_node;
 | |
|     if (![node.accessibilityIdentifier isEqualToString:@"id.video.dislike.button"]) {
 | |
|         if (![node.accessibilityIdentifier isEqualToString:@"id.video.like.button"])
 | |
|             return;
 | |
|         mode = 1;
 | |
|     }
 | |
|     UIViewController *vc = [node closestViewController];
 | |
|     if (![vc isKindOfClass:%c(YTWatchNextResultsViewController)]) return;
 | |
|     if (node.yogaChildren.count < 1) return;
 | |
|     ELMTextNode *likeTextNode = nil;
 | |
|     ELMTextNode *dislikeTextNode = nil;
 | |
|     NSMutableAttributedString *mutableDislikeText = nil;
 | |
|     if (mode == 0) {
 | |
|         _ASDisplayView *superview = (_ASDisplayView *)self.superview;
 | |
|         ELMContainerNode *snode = (ELMContainerNode *)superview.keepalive_node;
 | |
|         ELMContainerNode *likeNode = snode.yogaChildren[0];
 | |
|         if ([likeNode.accessibilityIdentifier isEqualToString:@"id.video.like.button"] && likeNode.yogaChildren.count == 2) {
 | |
|             likeTextNode = likeNode.yogaChildren[1];
 | |
|             if (![likeTextNode isKindOfClass:%c(ELMTextNode)]) return;
 | |
|             ASNodeContext *context = [(ASNodeContext *)[%c(ASNodeContext) alloc] initWithOptions:1];
 | |
|             ASNodeContextPush(context);
 | |
|             dislikeTextNode = [[%c(ELMTextNode) alloc] initWithElement:likeTextNode.element context:[likeTextNode valueForKey:@"_context"]];
 | |
|             ASNodeContextPop();
 | |
|             mutableDislikeText = [[NSMutableAttributedString alloc] initWithAttributedString:likeTextNode.attributedText];
 | |
|             dislikeTextNode.attributedText = mutableDislikeText;
 | |
|             [node addYogaChild:dislikeTextNode];
 | |
|             dislikeTextNode.blockUpdate = YES;
 | |
|             [self addSubview:dislikeTextNode.view];
 | |
|             pair = YES;
 | |
|         } else {
 | |
|             dislikeTextNode = node.yogaChildren[1];
 | |
|             if (![dislikeTextNode isKindOfClass:%c(ELMTextNode)]) return;
 | |
|             mutableDislikeText = [[NSMutableAttributedString alloc] initWithAttributedString:dislikeTextNode.attributedText];
 | |
|         }
 | |
|     } else {
 | |
|         likeTextNode = node.yogaChildren[1];
 | |
|         if (![likeTextNode isKindOfClass:%c(ELMTextNode)]) return;
 | |
|     }
 | |
|     NSObject *wc = [vc valueForKey:@"_metadataPanelStateProvider"];
 | |
|     YTPlayerViewController *pvc;
 | |
|     @try {
 | |
|         YTWatchPlaybackController *wpc = ((YTWatchController *)wc).watchPlaybackController;
 | |
|         pvc = [wpc valueForKey:@"_playerViewController"];
 | |
|     } @catch (id ex) {
 | |
|         pvc = [wc valueForKey:@"_playerViewController"];
 | |
|     }
 | |
|     NSString *videoId = [pvc currentVideoID];
 | |
|     if (mode == 0) {
 | |
|         mutableDislikeText.mutableString.string = FETCHING;
 | |
|         dislikeTextNode.attributedText = mutableDislikeText;
 | |
|     }
 | |
|     getVoteFromVideoWithHandler(videoId, maxRetryCount, ^(NSDictionary *data, NSString *error) {
 | |
|         dispatch_async(dispatch_get_main_queue(), ^{
 | |
|             if (exactLikeNumber && error == nil) {
 | |
|                 NSString *likeCount = formattedLongNumber(data[@"likes"], nil);
 | |
|                 if (likeCount) {
 | |
|                     NSMutableAttributedString *mutableLikeText = [[NSMutableAttributedString alloc] initWithAttributedString:likeTextNode.attributedText];
 | |
|                     mutableLikeText.mutableString.string = likeCount;
 | |
|                     likeTextNode.attributedText = mutableLikeText;
 | |
|                     likeTextNode.accessibilityLabel = likeCount; 
 | |
|                 }
 | |
|             }
 | |
|             if (mode == 0) {
 | |
|                 NSString *dislikeCount = getNormalizedDislikes(data[@"dislikes"], error);
 | |
|                 mutableDislikeText.mutableString.string = pair ? [NSString stringWithFormat:@"  %@ ", dislikeCount] : dislikeCount;
 | |
|                 dislikeTextNode.attributedText = mutableDislikeText;
 | |
|                 dislikeTextNode.accessibilityLabel = dislikeCount;
 | |
|             }
 | |
|         });
 | |
|     });
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| static void enableVoteSubmission(BOOL enabled) {
 | |
|     [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:EnableVoteSubmissionKey];
 | |
| }
 | |
| 
 | |
| %hook YTAppSettingsPresentationData
 | |
| 
 | |
| + (NSArray *)settingsCategoryOrder {
 | |
|     NSArray *order = %orig;
 | |
|     NSMutableArray *mutableOrder = [order mutableCopy];
 | |
|     NSUInteger insertIndex = [order indexOfObject:@(1)];
 | |
|     if (insertIndex != NSNotFound)
 | |
|         [mutableOrder insertObject:@(RYDSection) atIndex:insertIndex + 1];
 | |
|     return mutableOrder;
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %hook YTSettingsSectionItemManager
 | |
| 
 | |
| %new(v@:@)
 | |
| - (void)updateRYDSectionWithEntry:(id)entry {
 | |
|     NSMutableArray *sectionItems = [NSMutableArray array];
 | |
|     NSBundle *tweakBundle = RYDBundle();
 | |
|     YTSettingsViewController *delegate = [self valueForKey:@"_dataDelegate"];
 | |
|     YTSettingsSectionItem *enabled = [%c(YTSettingsSectionItem) switchItemWithTitle:LOC(@"ENABLED")
 | |
|         titleDescription:nil
 | |
|         accessibilityIdentifier:nil
 | |
|         switchOn:TweakEnabled()
 | |
|         switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
 | |
|             [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:EnabledKey];
 | |
|             return YES;
 | |
|         }
 | |
|         settingItemId:0];
 | |
|     [sectionItems addObject:enabled];
 | |
|     YTSettingsSectionItem *vote = [%c(YTSettingsSectionItem) switchItemWithTitle:LOC(@"ENABLE_VOTE_SUBMIT")
 | |
|         titleDescription:[NSString stringWithFormat:LOC(@"ENABLE_VOTE_SUBMIT_DESC"), apiUrl]
 | |
|         accessibilityIdentifier:nil
 | |
|         switchOn:VoteSubmissionEnabled()
 | |
|         switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
 | |
|             enableVoteSubmission(enabled);
 | |
|             return YES;
 | |
|         }
 | |
|         settingItemId:0];
 | |
|     [sectionItems addObject:vote];
 | |
|     YTSettingsSectionItem *exactDislike = [%c(YTSettingsSectionItem) switchItemWithTitle:LOC(@"EXACT_DISLIKE_NUMBER")
 | |
|         titleDescription:[NSString stringWithFormat:LOC(@"EXACT_DISLIKE_NUMBER_DESC"), @"12345", [NSNumberFormatter localizedStringFromNumber:@(12345) numberStyle:NSNumberFormatterDecimalStyle]]
 | |
|         accessibilityIdentifier:nil
 | |
|         switchOn:ExactDislikeNumber()
 | |
|         switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
 | |
|             [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:ExactDislikeKey];
 | |
|             return YES;
 | |
|         }
 | |
|         settingItemId:0];
 | |
|     [sectionItems addObject:exactDislike];
 | |
|     YTSettingsSectionItem *exactLike = [%c(YTSettingsSectionItem) switchItemWithTitle:LOC(@"EXACT_LIKE_NUMBER")
 | |
|         titleDescription:nil
 | |
|         accessibilityIdentifier:nil
 | |
|         switchOn:ExactLikeNumber()
 | |
|         switchBlock:^BOOL (YTSettingsCell *cell, BOOL enabled) {
 | |
|             [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:ExactLikeKey];
 | |
|             return YES;
 | |
|         }
 | |
|         settingItemId:0];
 | |
|     [sectionItems addObject:exactLike];
 | |
|     [delegate setSectionItems:sectionItems forCategory:RYDSection title:TWEAK_NAME titleDescription:nil headerHidden:NO];
 | |
| }
 | |
| 
 | |
| - (void)updateSectionForCategory:(NSUInteger)category withEntry:(id)entry {
 | |
|     if (category == RYDSection) {
 | |
|         [self updateRYDSectionWithEntry:entry];
 | |
|         return;
 | |
|     }
 | |
|     %orig;
 | |
| }
 | |
| 
 | |
| %end
 | |
| 
 | |
| %ctor {
 | |
|     cache = [NSCache new];
 | |
|     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
 | |
|     if (![defaults boolForKey:DidShowEnableVoteSubmissionAlertKey] && !VoteSubmissionEnabled()) {
 | |
|         [defaults setBool:YES forKey:DidShowEnableVoteSubmissionAlertKey];
 | |
|         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
 | |
|             NSBundle *tweakBundle = RYDBundle();
 | |
|             YTAlertView *alertView = [%c(YTAlertView) confirmationDialogWithAction:^{
 | |
|                 enableVoteSubmission(YES);
 | |
|             } actionTitle:_LOC([NSBundle mainBundle], @"settings.yes")];
 | |
|             alertView.title = TWEAK_NAME;
 | |
|             alertView.subtitle = [NSString stringWithFormat:LOC(@"WANT_TO_ENABLE"), apiUrl, TWEAK_NAME, LOC(@"ENABLE_VOTE_SUBMIT")];
 | |
|             [alertView show];
 | |
|         });
 | |
|     }
 | |
|     NSString *frameworkPath = [NSString stringWithFormat:@"%@/Frameworks/Module_Framework.framework/Module_Framework", NSBundle.mainBundle.bundlePath];
 | |
|     NSBundle *bundle = [NSBundle bundleWithPath:frameworkPath];
 | |
|     if (!bundle.loaded) [bundle load];
 | |
|     MSImageRef ref = MSGetImageByName([frameworkPath UTF8String]);
 | |
|     ASNodeContextPush = (void (*)(ASNodeContext *))MSFindSymbol(ref, "_ASNodeContextPush");
 | |
|     ASNodeContextPop = (void (*)(void))MSFindSymbol(ref, "_ASNodeContextPop");
 | |
|     %init;
 | |
| } | 
