mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-31 04:44:14 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		
							
								
								
									
										833
									
								
								Tweaks/Return-YouTube-Dislikes/Tweak.xm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										833
									
								
								Tweaks/Return-YouTube-Dislikes/Tweak.xm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,833 @@ | ||||
| #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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn