mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-11-03 04:49:28 -05: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