From 267f07d11400db30f4542ef42074a005200c612b Mon Sep 17 00:00:00 2001 From: BandarHelal <31299470+BandarHL@users.noreply.github.com> Date: Wed, 12 Jul 2023 04:17:19 +0300 Subject: [PATCH] project --- .gitignore | 3 + BHDownload.h | 35 + BHDownload.m | 45 + BHIManager.h | 32 + BHIManager.m | 149 ++ BHMultipleDownload.h | 14 + BHMultipleDownload.m | 65 + BHTikTok.plist | 1 + JGProgressHUD/JGProgressHUD-Defines.h | 78 + JGProgressHUD/JGProgressHUD.h | 310 ++++ JGProgressHUD/JGProgressHUD.m | 1234 ++++++++++++++++ JGProgressHUD/JGProgressHUDAnimation.h | 43 + JGProgressHUD/JGProgressHUDAnimation.m | 48 + .../JGProgressHUDErrorIndicatorView.h | 24 + .../JGProgressHUDErrorIndicatorView.m | 57 + JGProgressHUD/JGProgressHUDFadeAnimation.h | 33 + JGProgressHUD/JGProgressHUDFadeAnimation.m | 56 + .../JGProgressHUDFadeZoomAnimation.h | 40 + .../JGProgressHUDFadeZoomAnimation.m | 77 + .../JGProgressHUDImageIndicatorView.h | 28 + .../JGProgressHUDImageIndicatorView.m | 42 + .../JGProgressHUDIndeterminateIndicatorView.h | 25 + .../JGProgressHUDIndeterminateIndicatorView.m | 63 + JGProgressHUD/JGProgressHUDIndicatorView.h | 61 + JGProgressHUD/JGProgressHUDIndicatorView.m | 103 ++ JGProgressHUD/JGProgressHUDPieIndicatorView.h | 35 + JGProgressHUD/JGProgressHUDPieIndicatorView.m | 171 +++ .../JGProgressHUDRingIndicatorView.h | 50 + .../JGProgressHUDRingIndicatorView.m | 194 +++ JGProgressHUD/JGProgressHUDShadow.h | 37 + JGProgressHUD/JGProgressHUDShadow.m | 30 + .../JGProgressHUDSuccessIndicatorView.h | 24 + .../JGProgressHUDSuccessIndicatorView.m | 56 + Makefile | 15 + README.md | 28 + SecurityViewController.h | 5 + SecurityViewController.m | 47 + SettingsViewController.h | 44 + SettingsViewController.m | 643 +++++++++ TikTokHeaders.h | 289 ++++ Tweak.x | 1249 +++++++++++++++++ control | 9 + 42 files changed, 5592 insertions(+) create mode 100644 .gitignore create mode 100644 BHDownload.h create mode 100644 BHDownload.m create mode 100644 BHIManager.h create mode 100644 BHIManager.m create mode 100644 BHMultipleDownload.h create mode 100644 BHMultipleDownload.m create mode 100644 BHTikTok.plist create mode 100644 JGProgressHUD/JGProgressHUD-Defines.h create mode 100644 JGProgressHUD/JGProgressHUD.h create mode 100755 JGProgressHUD/JGProgressHUD.m create mode 100644 JGProgressHUD/JGProgressHUDAnimation.h create mode 100755 JGProgressHUD/JGProgressHUDAnimation.m create mode 100644 JGProgressHUD/JGProgressHUDErrorIndicatorView.h create mode 100644 JGProgressHUD/JGProgressHUDErrorIndicatorView.m create mode 100644 JGProgressHUD/JGProgressHUDFadeAnimation.h create mode 100755 JGProgressHUD/JGProgressHUDFadeAnimation.m create mode 100644 JGProgressHUD/JGProgressHUDFadeZoomAnimation.h create mode 100755 JGProgressHUD/JGProgressHUDFadeZoomAnimation.m create mode 100644 JGProgressHUD/JGProgressHUDImageIndicatorView.h create mode 100644 JGProgressHUD/JGProgressHUDImageIndicatorView.m create mode 100644 JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.h create mode 100644 JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.m create mode 100644 JGProgressHUD/JGProgressHUDIndicatorView.h create mode 100755 JGProgressHUD/JGProgressHUDIndicatorView.m create mode 100644 JGProgressHUD/JGProgressHUDPieIndicatorView.h create mode 100755 JGProgressHUD/JGProgressHUDPieIndicatorView.m create mode 100644 JGProgressHUD/JGProgressHUDRingIndicatorView.h create mode 100755 JGProgressHUD/JGProgressHUDRingIndicatorView.m create mode 100644 JGProgressHUD/JGProgressHUDShadow.h create mode 100644 JGProgressHUD/JGProgressHUDShadow.m create mode 100644 JGProgressHUD/JGProgressHUDSuccessIndicatorView.h create mode 100644 JGProgressHUD/JGProgressHUDSuccessIndicatorView.m create mode 100644 Makefile create mode 100644 SecurityViewController.h create mode 100644 SecurityViewController.m create mode 100755 SettingsViewController.h create mode 100755 SettingsViewController.m create mode 100644 TikTokHeaders.h create mode 100644 Tweak.x create mode 100644 control diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faf8687 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.theos/ +packages/ +.DS_Store diff --git a/BHDownload.h b/BHDownload.h new file mode 100644 index 0000000..b62002b --- /dev/null +++ b/BHDownload.h @@ -0,0 +1,35 @@ +// +// BHDownload.h +// DIYTableView +// +// Created by BandarHelal on 12/01/1442 AH. +// Copyright © 1442 BandarHelal. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol BHDownloadDelegate +@optional +- (void)downloadProgress:(float)progress; +- (void)downloadDidFinish:(NSURL *)filePath Filename:(NSString *)fileName; +- (void)downloadDidFailureWithError:(NSError *)error; +@end + +@interface BHDownload : NSObject +{ + id delegate; +} +- (void)setDelegate:(id)newDelegate; +- (instancetype)init; +- (void)downloadFileWithURL:(NSURL *)url; +@property (nonatomic, strong) NSString *fileName; +@end + +@interface BHDownload () +@property (nonatomic, strong) NSURLSession *Session; +@end + +NS_ASSUME_NONNULL_END diff --git a/BHDownload.m b/BHDownload.m new file mode 100644 index 0000000..59c691b --- /dev/null +++ b/BHDownload.m @@ -0,0 +1,45 @@ +// +// BHDownload.m +// DIYTableView +// +// Created by BandarHelal on 12/01/1442 AH. +// Copyright © 1442 BandarHelal. All rights reserved. +// + +#import "BHDownload.h" + +@implementation BHDownload +- (instancetype)init { + self = [super init]; + if (self) { + self.Session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; + } + return self; +} + +- (void)downloadFileWithURL:(NSURL *)url { + if (url) { + self.fileName = url.absoluteString.lastPathComponent; + NSURLSessionDownloadTask *downloadTask = [self.Session downloadTaskWithURL:url]; + [downloadTask resume]; + } +} +- (void)setDelegate:(id)newDelegate { + if (newDelegate) { + delegate = newDelegate; + } +} +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { + float prog = (float)totalBytesWritten / (float)totalBytesExpectedToWrite; + [delegate downloadProgress:prog]; +} +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location { + [delegate downloadDidFinish:location Filename:self.fileName]; +} +- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error { + [delegate downloadDidFailureWithError:error]; +} +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { + [delegate downloadDidFailureWithError:error]; +} +@end diff --git a/BHIManager.h b/BHIManager.h new file mode 100644 index 0000000..a34056d --- /dev/null +++ b/BHIManager.h @@ -0,0 +1,32 @@ +#import +#import + +@interface BHIManager: NSObject ++ (BOOL)hideAds; ++ (BOOL)downloadVideos; ++ (BOOL)downloadMusics; ++ (BOOL)hideElementButton; ++ (BOOL)copyVideoDecription; ++ (BOOL)copyMusicLink; ++ (BOOL)copyVideoLink; ++ (BOOL)autoPlay; ++ (BOOL)progressBar; ++ (BOOL)likeConfirmation; ++ (BOOL)likeCommentConfirmation; ++ (BOOL)dislikeCommentConfirmation; ++ (BOOL)followConfirmation; ++ (BOOL)profileSave; ++ (BOOL)profileCopy; ++ (BOOL)alwaysOpenSafari; ++ (BOOL)regionChangingEnabled; ++ (NSDictionary *)selectedRegion; ++ (BOOL)fakeChangesEnabled; ++ (BOOL)fakeVerified; ++ (BOOL)extendedBio; ++ (BOOL)extendedComment; ++ (BOOL)appLock; ++ (void)showSaveVC:(id)item; ++ (void)cleanCache; ++ (BOOL)isEmpty:(NSURL *)url; ++ (NSString *)getDownloadingPersent:(float)per; +@end \ No newline at end of file diff --git a/BHIManager.m b/BHIManager.m new file mode 100644 index 0000000..74cb03d --- /dev/null +++ b/BHIManager.m @@ -0,0 +1,149 @@ +#import "BHIManager.h" +#import "TikTokHeaders.h" + +@implementation BHIManager ++ (BOOL)hideAds { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"hide_ads"]; +} ++ (BOOL)downloadVideos { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"dw_videos"]; +} ++ (BOOL)downloadMusics { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"dw_musics"]; +} ++ (BOOL)hideElementButton { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"remove_elements_button"]; +} ++ (BOOL)copyVideoDecription { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"copy_decription"]; +} ++ (BOOL)copyMusicLink { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"copy_music_link"]; +} ++ (BOOL)copyVideoLink { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"copy_video_link"]; +} ++ (BOOL)autoPlay { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"auto_play"]; +} ++ (BOOL)progressBar { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"show_porgress_bar"]; +} ++ (BOOL)likeConfirmation { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"like_confirm"]; +} ++ (BOOL)likeCommentConfirmation { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"like_comment_confirm"]; +} ++ (BOOL)dislikeCommentConfirmation { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"dislike_comment_confirm"]; +} ++ (BOOL)followConfirmation { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"follow_confirm"]; +} ++ (BOOL)profileSave { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"save_profile"]; +} ++ (BOOL)profileCopy { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"copy_profile_information"]; +} ++ (BOOL)alwaysOpenSafari { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"openInBrowser"]; +} ++ (BOOL)regionChangingEnabled { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"en_region"]; +} ++ (NSDictionary *)selectedRegion { + return [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"region"]; +} ++ (BOOL)fakeChangesEnabled { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"en_fake"]; +} ++ (BOOL)fakeVerified { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"fake_verify"]; +} ++ (BOOL)extendedBio { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"extended_bio"]; +} ++ (BOOL)extendedComment { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"extendedComment"]; +} ++ (BOOL)appLock { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"padlock"]; +} ++ (void)cleanCache { + NSArray *DocumentFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject] includingPropertiesForKeys:@[] options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; + + for (NSURL *file in DocumentFiles) { + if ([file.pathExtension.lowercaseString isEqualToString:@"mp4"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"png"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"jpeg"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"mp3"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"m4a"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + } + + NSArray *TempFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:NSTemporaryDirectory()] includingPropertiesForKeys:@[] options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; + + for (NSURL *file in TempFiles) { + if ([file.pathExtension.lowercaseString isEqualToString:@"mp4"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"mov"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"tmp"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"png"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"jpeg"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"mp3"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file.pathExtension.lowercaseString isEqualToString:@"m4a"]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + if ([file hasDirectoryPath]) { + if ([BHIManager isEmpty:file]) { + [[NSFileManager defaultManager] removeItemAtURL:file error:nil]; + } + } + } +} ++ (BOOL)isEmpty:(NSURL *)url { + NSArray *FolderFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:url includingPropertiesForKeys:@[] options:NSDirectoryEnumerationSkipsHiddenFiles error:nil]; + if (FolderFiles.count == 0) { + return true; + } else { + return false; + } +} ++ (void)showSaveVC:(id)item { + UIActivityViewController *acVC = [[UIActivityViewController alloc] initWithActivityItems:item applicationActivities:nil]; + if (is_iPad()) { + acVC.popoverPresentationController.sourceView = topMostController().view; + acVC.popoverPresentationController.sourceRect = CGRectMake(topMostController().view.bounds.size.width / 2.0, topMostController().view.bounds.size.height / 2.0, 1.0, 1.0); + } + [topMostController() presentViewController:acVC animated:true completion:nil]; +} + ++ (NSString *)getDownloadingPersent:(float)per { + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setNumberStyle:NSNumberFormatterPercentStyle]; + NSNumber *number = [NSNumber numberWithFloat:per]; + return [numberFormatter stringFromNumber:number]; +} +@end \ No newline at end of file diff --git a/BHMultipleDownload.h b/BHMultipleDownload.h new file mode 100644 index 0000000..e1e30df --- /dev/null +++ b/BHMultipleDownload.h @@ -0,0 +1,14 @@ +#import + +@protocol BHMultipleDownloadDelegate +- (void)downloaderProgress:(float)progress; +- (void)downloaderDidFinishDownloadingAllFiles:(NSMutableArray *)downloadedFilePaths; +- (void)downloaderDidFailureWithError:(NSError *)error; +@end + +@interface BHMultipleDownload : NSObject +@property (nonatomic, strong) NSURLSession *session; +@property (nonatomic, weak) id delegate; +- (void)downloadFiles:(NSArray *)fileURLs; + +@end \ No newline at end of file diff --git a/BHMultipleDownload.m b/BHMultipleDownload.m new file mode 100644 index 0000000..19891b9 --- /dev/null +++ b/BHMultipleDownload.m @@ -0,0 +1,65 @@ +#import "BHMultipleDownload.h" + +@implementation BHMultipleDownload { + NSMutableArray *_downloadedFilePaths; + NSInteger _completedDownloads; + NSInteger _totalDownloads; +} + +- (instancetype)init { + self = [super init]; + if (self) { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]]; + } + return self; +} + +- (void)downloadFiles:(NSArray *)fileURLs { + _downloadedFilePaths = [NSMutableArray array]; + _completedDownloads = 0; + _totalDownloads = fileURLs.count; + + for (NSURL *fileURL in fileURLs) { + NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:fileURL]; + [downloadTask resume]; + } + + if (fileURLs.count == 0) { + [self URLSession:self.session didBecomeInvalidWithError:[NSError errorWithDomain:NSURLErrorDomain code:99 userInfo:nil]]; + } +} + +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { + float prog = (float)totalBytesWritten / (float)totalBytesExpectedToWrite; + if ([self.delegate respondsToSelector:@selector(downloaderProgress:)]) { + [self.delegate downloaderProgress:prog]; + } +} +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask + didFinishDownloadingToURL:(NSURL *)location { + NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + NSString *destinationPath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@", NSUUID.UUID.UUIDString, downloadTask.response.suggestedFilename]]; + + NSError *error; + [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:destinationPath] error:&error]; + + if (error == nil) { + [_downloadedFilePaths addObject:[NSURL fileURLWithPath:destinationPath]]; + } + + _completedDownloads++; + + if (_completedDownloads == _totalDownloads) { + if ([self.delegate respondsToSelector:@selector(downloaderDidFinishDownloadingAllFiles:)]) { + [self.delegate downloaderDidFinishDownloadingAllFiles:_downloadedFilePaths]; + } + } +} + +- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error { + if ([self.delegate respondsToSelector:@selector(downloaderDidFailureWithError:)]) { + [self.delegate downloaderDidFailureWithError:error]; + } +} +@end \ No newline at end of file diff --git a/BHTikTok.plist b/BHTikTok.plist new file mode 100644 index 0000000..870cfdd --- /dev/null +++ b/BHTikTok.plist @@ -0,0 +1 @@ +{ Filter = { Bundles = ( "com.zhiliaoapp.musically" ); }; } diff --git a/JGProgressHUD/JGProgressHUD-Defines.h b/JGProgressHUD/JGProgressHUD-Defines.h new file mode 100644 index 0000000..d777bf6 --- /dev/null +++ b/JGProgressHUD/JGProgressHUD-Defines.h @@ -0,0 +1,78 @@ +// +// JGProgressHUD-Defines.h +// JGProgressHUD +// +// Created by Jonas Gessner on 28.04.15. +// Copyright (c) 2015 Jonas Gessner. All rights reserved. +// + +#import + +/** + Positions of the HUD. + */ +typedef NS_ENUM(NSUInteger, JGProgressHUDPosition) { + /** Center position. */ + JGProgressHUDPositionCenter = 0, + /** Top left position. */ + JGProgressHUDPositionTopLeft, + /** Top center position. */ + JGProgressHUDPositionTopCenter, + /** Top right position. */ + JGProgressHUDPositionTopRight, + /** Center left position. */ + JGProgressHUDPositionCenterLeft, + /** Center right position. */ + JGProgressHUDPositionCenterRight, + /** Bottom left position. */ + JGProgressHUDPositionBottomLeft, + /** Bottom center position. */ + JGProgressHUDPositionBottomCenter, + /** Bottom right position. */ + JGProgressHUDPositionBottomRight +}; + +/** + Appearance styles of the HUD. + */ +typedef NS_ENUM(NSUInteger, JGProgressHUDStyle) { + /** Extra light HUD with dark elements. */ + JGProgressHUDStyleExtraLight = 0, + /** Light HUD with dark elemets. */ + JGProgressHUDStyleLight, + /** Dark HUD with light elements. */ + JGProgressHUDStyleDark, +}; + +#if TARGET_OS_IOS +/** + Interaction types. + */ +typedef NS_ENUM(NSUInteger, JGProgressHUDInteractionType) { + /** Block all touches. No interaction behin the HUD is possible. */ + JGProgressHUDInteractionTypeBlockAllTouches = 0, + /** Block touches on the HUD view. */ + JGProgressHUDInteractionTypeBlockTouchesOnHUDView, + /** Block no touches. */ + JGProgressHUDInteractionTypeBlockNoTouches +}; +#endif + +/** + Parallax Modes. + */ +typedef NS_ENUM(NSUInteger, JGProgressHUDParallaxMode) { + /** Follows the device setting for parallax. If "Reduce Motion" is enabled, no parallax effect is added to the HUD, if "Reduce Motion" is disabled the HUD will have a parallax effect. This behaviour is only supported on iOS 8 and higher. */ + JGProgressHUDParallaxModeDevice = 0, + /** Always adds a parallax effect to the HUD. Parallax is only supported on iOS 7 and higher. */ + JGProgressHUDParallaxModeAlwaysOn, + /** Never adds a parallax effect to the HUD. */ + JGProgressHUDParallaxModeAlwaysOff +}; + +#ifndef fequal +/** + Macro for safe floating point comparison (for internal use in JGProgressHUD). + */ +#define fequal(a,b) (fabs((a) - (b)) < FLT_EPSILON) +#endif diff --git a/JGProgressHUD/JGProgressHUD.h b/JGProgressHUD/JGProgressHUD.h new file mode 100644 index 0000000..3169c8d --- /dev/null +++ b/JGProgressHUD/JGProgressHUD.h @@ -0,0 +1,310 @@ +// +// JGProgressHUD.h +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUD-Defines.h" +#import "JGProgressHUDShadow.h" +#import "JGProgressHUDAnimation.h" +#import "JGProgressHUDFadeAnimation.h" +#import "JGProgressHUDFadeZoomAnimation.h" +#import "JGProgressHUDIndicatorView.h" +#import "JGProgressHUDErrorIndicatorView.h" +#import "JGProgressHUDSuccessIndicatorView.h" +#import "JGProgressHUDRingIndicatorView.h" +#import "JGProgressHUDPieIndicatorView.h" +#import "JGProgressHUDIndeterminateIndicatorView.h" +#pragma clang diagnostic pop + +@protocol JGProgressHUDDelegate; + +/** + A HUD to indicate progress, success, error, warnings or other notifications to the user. + @discussion @c JGProgressHUD respects its @c layoutMargins when positioning the HUD view. Additionally, on iOS 11 if @c insetsLayoutMarginsFromSafeArea is set to @c YES (default) the @c layoutMargins additionally contain the @c safeAreaInsets. + @note Remember to call every method from the main thread! UIKit => main thread! + @attention You may not add JGProgressHUD to a view which has an alpha value < 1.0 or to a view which is a subview of a view with an alpha value < 1.0. + */ +@interface JGProgressHUD : UIView + +/** + Designated initializer. + @param style The appearance style of the HUD. + */ +- (instancetype __nonnull)initWithStyle:(JGProgressHUDStyle)style; + +/** + Convenience initializer. + @param style The appearance style of the HUD. + */ ++ (instancetype __nonnull)progressHUDWithStyle:(JGProgressHUDStyle)style; + +/** + Convenience initializer. The HUD will dynamically change its style based on whether dark mode is enabled or not. When dark mode is on, the style will be JGProgressHUDStyleDark, when dark mode is off the style will be JGProgressHUDStyleExtraLight. + */ +- (instancetype __nonnull)initWithAutomaticStyle; + +/** + Convenience initializer. The HUD will dynamically change its style based on whether dark mode is enabled or not. When dark mode is on, the style will be JGProgressHUDStyleDark, when dark mode is off the style will be JGProgressHUDStyleExtraLight. + */ ++ (instancetype __nonnull)progressHUDWithAutomaticStyle; + +/** + The appearance style of the HUD. + @b Default: JGProgressHUDStyleExtraLight. + */ +@property (nonatomic, assign) JGProgressHUDStyle style; + +/** The view in which the HUD is presented. */ +@property (nonatomic, weak, readonly, nullable) UIView *targetView; + +/** + The delegate of the HUD. + @sa JGProgressHUDDelegate. + */ +@property (nonatomic, weak, nullable) id delegate; + +/** The actual HUD view visible on screen. You may add animations to this view. */ +@property (nonatomic, strong, readonly, nonnull) UIView *HUDView; + +/** + The content view inside the @c HUDView. If you want to add additional views to the HUD you should add them as subview to the @c contentView. + */ +@property (nonatomic, strong, readonly, nonnull) UIView *contentView; + +/** + The label used to present text on the HUD. Set the @c text or @c attributedText property of this label to change the displayed text. You may not change the label's @c frame or @c bounds. + */ +@property (nonatomic, strong, readonly, nonnull) UILabel *textLabel; + +/** + The label used to present detail text on the HUD. Set the @c text or @c attributedText property of this label to change the displayed text. You may not change the label's @c frame or @c bounds. + */ +@property (nonatomic, strong, readonly, nonnull) UILabel *detailTextLabel; + +/** + The indicator view. You can assign a custom subclass of @c JGProgressHUDIndicatorView to this property or one of the default indicator views (if you do so, you should assign it before showing the HUD). This value is optional. + @b Default: JGProgressHUDIndeterminateIndicatorView. + */ +@property (nonatomic, strong, nullable) JGProgressHUDIndicatorView *indicatorView; + +/** + The shadow cast by the @c HUDView. This value is optional. Setting this to @c nil means no shadow is cast by the HUD. + @b Default: nil. + */ +@property (nonatomic, strong, nullable) JGProgressHUDShadow *shadow; + +/** + The position of the HUD inside the hosting view's frame, or inside the specified frame. + @b Default: JGProgressHUDPositionCenter + */ +@property (nonatomic, assign) JGProgressHUDPosition position; + +/** + The animation used for showing and dismissing the HUD. + @b Default: JGProgressHUDFadeAnimation. + */ +@property (nonatomic, strong, nonnull) JGProgressHUDAnimation *animation; + +#if TARGET_OS_IOS +/** + Interaction type of the HUD. Determines whether touches should be let through to the views behind the HUD. + @sa JGProgressHUDInteractionType. + @b Default: JGProgressHUDInteractionTypeBlockAllTouches. + */ +@property (nonatomic, assign) JGProgressHUDInteractionType interactionType; +#endif + +/** + Parallax mode for the HUD. This setting determines whether the HUD should have a parallax (@c UIDeviceMotion) effect. This effect is controlled by device motion on iOS and remote touchpad panning gestures on tvOS. + @sa JGProgressHUDParallaxMode. + @b Default: JGProgressHUDParallaxModeDevice. + */ +@property (nonatomic, assign) JGProgressHUDParallaxMode parallaxMode; + +#if TARGET_OS_TV +/** + When this property is set to @c YES the HUD will try to become focused, which prevents interactions with the @c targetView. If set to @c NO the HUD will not become focused and interactions with @c targetView remain possible. Default: @c YES. + */ +@property (nonatomic, assign) BOOL wantsFocus; +#endif + +/** + If the HUD should always have the same width and height. + @b Default: NO. + */ +@property (nonatomic, assign) BOOL square; + +/** + Internally @c JGProgressHUD uses an @c UIVisualEffectView with a @c UIBlurEffect. A second @c UIVisualEffectView can be added on top of that with a @c UIVibrancyEffect which amplifies and adjusts the color of content layered behind the view, allowing content placed inside the contentView to become more vivid. This flag sets whether the @c UIVibrancyEffect should be used. Using the vibrancy effect can sometimes, depending on the contents of the display, result in a weird look (especially on iOS < 9.3). + @b Default: NO. + */ +@property (nonatomic, assign) BOOL vibrancyEnabled; + +/** + The radius used for rounding the four corners of the HUD view. + @b Default: 10.0. + */ +@property (nonatomic, assign) CGFloat cornerRadius; + +/** + Insets the contents of the HUD. + @b Default: (20, 20, 20, 20). + */ +@property (nonatomic, assign) UIEdgeInsets contentInsets; + +/** + @return Whether the HUD is visible on screen. + */ +@property (nonatomic, assign, readonly, getter = isVisible) BOOL visible; + +/** + The progress to display using the @c progressIndicatorView. A change of this property is not animated. Use the @c setProgress:animated: method for an animated progress change. + @b Default: 0.0. + */ +@property (nonatomic, assign) float progress; + +/** + Adjusts the current progress shown by the receiver, optionally animating the change. + + The current progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the completion of the task. The default value is 0.0. Values less than 0.0 and greater than 1.0 are pinned to those limits. + @param progress The new progress value. + @param animated YES if the change should be animated, NO if the change should happen immediately. + */ +- (void)setProgress:(float)progress animated:(BOOL)animated; + +/** + Specifies a minimum time that the HUD will be on-screen. Useful to prevent the HUD from flashing quickly on the screen when indeterminate tasks complete more quickly than expected. + @b Default: 0.0. + */ +@property (nonatomic, assign) NSTimeInterval minimumDisplayTime; + +/** + Determines whether Voice Over announcements should be made upon displaying the HUD (if Voice Over is active). + @b Default: YES + */ +@property (nonatomic, assign) BOOL voiceOverEnabled; + +#if TARGET_OS_IOS +/** + A block to be invoked when the HUD view is tapped. + @note The interaction type of the HUD must be @c JGProgressHUDInteractionTypeBlockTouchesOnHUDView or @c JGProgressHUDInteractionTypeBlockAllTouches, otherwise this block won't be fired. + */ +@property (nonatomic, copy, nullable) void (^tapOnHUDViewBlock)(JGProgressHUD *__nonnull HUD); + +/** + A block to be invoked when the area outside of the HUD view is tapped. + @note The interaction type of the HUD must be @c JGProgressHUDInteractionTypeBlockAllTouches, otherwise this block won't be fired. + */ +@property (nonatomic, copy, nullable) void (^tapOutsideBlock)(JGProgressHUD *__nonnull HUD); +#endif + +/** + Shows the HUD animated. You should preferably show the HUD in a UIViewController's view. The HUD will be repositioned in response to rotation and keyboard show/hide notifications. + @param view The view to show the HUD in. The frame of the @c view will be used to calculate the position of the HUD. + */ +- (void)showInView:(UIView *__nonnull)view; + +/** + Shows the HUD. You should preferably show the HUD in a UIViewController's view. The HUD will be repositioned in response to rotation and keyboard show/hide notifications. + @param view The view to show the HUD in. The frame of the @c view will be used to calculate the position of the HUD. + @param animated If the HUD should show with an animation. + */ +- (void)showInView:(UIView *__nonnull)view animated:(BOOL)animated; + +/** + Shows the HUD after a delay. You should preferably show the HUD in a UIViewController's view. The HUD will be repositioned in response to rotation and keyboard show/hide notifications. + You may call @c dismiss to stop the HUD from appearing before the delay has passed. + @param view The view to show the HUD in. The frame of the @c view will be used to calculate the position of the HUD. + @param animated If the HUD should show with an animation. + @param delay The delay until the HUD will be shown. + */ +- (void)showInView:(UIView *__nonnull)view animated:(BOOL)animated afterDelay:(NSTimeInterval)delay; + +/** Dismisses the HUD animated. If the HUD is currently not visible this method does nothing. */ +- (void)dismiss; + +/** + Dismisses the HUD. If the HUD is currently not visible this method does nothing. + @param animated If the HUD should dismiss with an animation. + */ +- (void)dismissAnimated:(BOOL)animated; + +/** + Dismisses the HUD animated after a delay. If the HUD is currently not visible this method does nothing. + @param delay The delay until the HUD will be dismissed. + */ +- (void)dismissAfterDelay:(NSTimeInterval)delay; + +/** + Dismisses the HUD after a delay. If the HUD is currently not visible this method does nothing. + @param delay The delay until the HUD will be dismissed. + @param animated If the HUD should dismiss with an animation. + */ +- (void)dismissAfterDelay:(NSTimeInterval)delay animated:(BOOL)animated; + +/** + Dismisses the HUD after a delay and runs a block upon completion. If the HUD is currently not visible this method does nothing. + @param delay The delay until the HUD will be dismissed. + @param animated If the HUD should dismiss with an animation. + @param dismissCompletion The block to execute after the HUD was dismissed. + */ +- (void)dismissAfterDelay:(NSTimeInterval)delay animated:(BOOL)animated completion:(void (^_Nullable)(void))dismissCompletion; + +/** + Schedules the given block to be executed when this HUD disapears. If the HUD is currently not visible this method does nothing. + @param dismissCompletion The block to execute after the HUD was dismissed. Multiple calls to this method cause the different blocks to be executed in FIFO order. + */ +- (void)performAfterDismiss:(void (^_Nonnull)(void))dismissCompletion; + +@end + +@interface JGProgressHUD (HUDManagement) + +/** + @param view The view to return all visible progress HUDs for. + @return All visible progress HUDs in the view. + */ ++ (NSArray *__nonnull)allProgressHUDsInView:(UIView *__nonnull)view; + +/** + @param view The view to return all visible progress HUDs for. + @return All visible progress HUDs in the view and its subviews. + */ ++ (NSArray *__nonnull)allProgressHUDsInViewHierarchy:(UIView *__nonnull)view; + +@end + +@protocol JGProgressHUDDelegate + +@optional + +/** + Called before the HUD will appear. + @param view The view in which the HUD is presented. + */ +- (void)progressHUD:(JGProgressHUD *__nonnull)progressHUD willPresentInView:(UIView *__nonnull)view; + +/** + Called after the HUD appeared. + @param view The view in which the HUD is presented. + */ +- (void)progressHUD:(JGProgressHUD *__nonnull)progressHUD didPresentInView:(UIView *__nonnull)view; + +/** + Called before the HUD will disappear. + @param view The view in which the HUD is presented and will be dismissed from. + */ +- (void)progressHUD:(JGProgressHUD *__nonnull)progressHUD willDismissFromView:(UIView *__nonnull)view; + +/** + Called after the HUD has disappeared. + @param view The view in which the HUD was presented and was be dismissed from. + */ +- (void)progressHUD:(JGProgressHUD *__nonnull)progressHUD didDismissFromView:(UIView *__nonnull)view; + +@end diff --git a/JGProgressHUD/JGProgressHUD.m b/JGProgressHUD/JGProgressHUD.m new file mode 100755 index 0000000..d397ee0 --- /dev/null +++ b/JGProgressHUD/JGProgressHUD.m @@ -0,0 +1,1234 @@ +// +// JGProgressHUD.m +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUD.h" +#import +#import "JGProgressHUDFadeAnimation.h" +#import "JGProgressHUDIndeterminateIndicatorView.h" + +#if !__has_feature(objc_arc) +#error "JGProgressHUD requires ARC!" +#endif + +static inline CGRect JGProgressHUD_CGRectIntegral(CGRect rect) { + CGFloat scale = [[UIScreen mainScreen] scale]; + + return (CGRect){{((CGFloat)floor(rect.origin.x*scale))/scale, ((CGFloat)floor(rect.origin.y*scale))/scale}, {((CGFloat)ceil(rect.size.width*scale))/scale, ((CGFloat)ceil(rect.size.height*scale))/scale}}; +} + +API_AVAILABLE(ios(12.0), tvos(10.0)) +static inline JGProgressHUDStyle JGProgressHUDStyleFromUIUserInterfaceStyle(UIUserInterfaceStyle uiStyle) { + if (uiStyle == UIUserInterfaceStyleDark) { + return JGProgressHUDStyleDark; + } + else { + return JGProgressHUDStyleExtraLight; + } +} + +@interface JGProgressHUD () { + BOOL _transitioning; + BOOL _updateAfterAppear; + + BOOL _dismissAfterTransitionFinished; + BOOL _dismissAfterTransitionFinishedWithAnimation; + + CFAbsoluteTime _displayTimestamp; + dispatch_block_t __nullable _displayBlock; + + BOOL _effectiveIndicatorViewNeedsUpdate; + + UIView *__nonnull _blurViewContainer; + UIView *__nonnull _shadowView; + CAShapeLayer *__nonnull _shadowMaskLayer; + + NSMutableArray *_dismissActions; + + BOOL _automaticStyle; +} + +// In 'beta' +/** + Setting this to @c YES makes the text and indicator views bigger. + @b Default: NO. + */ +@property (nonatomic, assign) BOOL thick; + +@property (nonatomic, strong, readonly, nonnull) UIVisualEffectView *blurView; +@property (nonatomic, strong, readonly, nonnull) UIVisualEffectView *vibrancyView; + +@property (nonatomic, strong, readonly, nonnull) JGProgressHUDIndicatorView *effectiveIndicatorView; + +@end + +@interface JGProgressHUDAnimation (Private) + +@property (nonatomic, weak, nullable) JGProgressHUD *progressHUD; + +@end + +@implementation JGProgressHUD + +@synthesize HUDView = _HUDView; +@synthesize blurView = _blurView; +@synthesize vibrancyView = _vibrancyView; +@synthesize textLabel = _textLabel; +@synthesize detailTextLabel = _detailTextLabel; +@synthesize indicatorView = _indicatorView; +@synthesize animation = _animation; +@synthesize contentView = _contentView; + +@dynamic visible; + +#pragma mark - Keyboard + +static CGRect keyboardFrame = (CGRect){{0.0, 0.0}, {0.0, 0.0}}; + +#if TARGET_OS_IOS ++ (void)keyboardFrameWillChange:(NSNotification *)notification { + keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + if (CGRectIsEmpty(keyboardFrame)) { + keyboardFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + } +} + ++ (void)keyboardFrameDidChange:(NSNotification *)notification { + keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; +} + ++ (void)keyboardDidHide { + keyboardFrame = CGRectZero; +} + ++ (void)load { + @autoreleasepool { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardFrameWillChange:) name:UIKeyboardWillChangeFrameNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardFrameDidChange:) name:UIKeyboardDidChangeFrameNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide) name:UIKeyboardDidHideNotification object:nil]; + } +} +#endif + ++ (CGRect)currentKeyboardFrame { + return keyboardFrame; +} + +#pragma mark - Initializers + +- (instancetype)init { + return [self initWithAutomaticStyle]; +} + +- (instancetype)initWithFrame:(CGRect __unused)frame { + return [self initWithAutomaticStyle]; +} + +/* + Basic architecture: + + * self covers the entire target view. + * self.HUDView is a subview of self and has the size and position of the visible HUD. The layer has rounded corners set with self.cornerRadius. The layer does not clip to bounds. + * _shadowView is a subview of self.HUDView and always covers the entire HUDView. The corners are also rounded in the same way as self.HUDView. It draws its shadow only on the outside by using a masking layer, so that the shadow does not interfere with the blur view. + * _blurViewContainer is a subview of self.HUDView and always covers the entire self.HUDView. The corners are also rounded in the same way as self.HUDView but it additionally clips to bounds. + * self.blurView is a subview of _blurViewContainer and provides the blur effect. The corners are not rounded and the view does not clip to bounds. + * self.vibrancyView is a subview of self.blurView.contentView and provides the vibrancy effect. The corners are not rounded and the view does not clip to bounds. + * self.contentView is a subview of self.vibrancyView.contentView. It does not always have the same frame as it's superview (during transitions). + * self.contentView contains the labels and the indicator view. + + */ +- (instancetype)initWithStyle:(JGProgressHUDStyle)style { + self = [super initWithFrame:CGRectZero]; + + if (self) { + _style = style; + _voiceOverEnabled = YES; + _automaticStyle = NO; + + _HUDView = [[UIView alloc] init]; + self.HUDView.backgroundColor = [UIColor clearColor]; + [self addSubview:self.HUDView]; + + _blurViewContainer = [[UIView alloc] init]; + _blurViewContainer.backgroundColor = [UIColor clearColor]; + _blurViewContainer.clipsToBounds = YES; + [self.HUDView addSubview:_blurViewContainer]; + + _shadowView = [[UIView alloc] init]; + _shadowView.backgroundColor = [UIColor blackColor]; + _shadowView.userInteractionEnabled = NO; + _shadowView.layer.shadowOpacity = 1.0; + _shadowView.alpha = 0.0; + + _shadowMaskLayer = [CAShapeLayer layer]; + _shadowMaskLayer.fillRule = kCAFillRuleEvenOdd; + _shadowMaskLayer.fillColor = [UIColor blackColor].CGColor; + _shadowMaskLayer.opacity = 1.0; + + _shadowView.layer.mask = _shadowMaskLayer; + + [self.HUDView addSubview:_shadowView]; + + _indicatorView = [[JGProgressHUDIndeterminateIndicatorView alloc] init]; + _effectiveIndicatorView = _indicatorView; + [self.effectiveIndicatorView setUpForHUDStyle:self.style vibrancyEnabled:self.vibrancyEnabled]; + + self.hidden = YES; + self.backgroundColor = [UIColor clearColor]; + + self.contentInsets = UIEdgeInsetsMake(20.0, 20.0, 20.0, 20.0); + self.layoutMargins = UIEdgeInsetsMake(20.0, 20.0, 20.0, 20.0); + + self.cornerRadius = 10.0; + + _dismissActions = [[NSMutableArray alloc] init]; + +#if TARGET_OS_IOS + [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]]; +#elif TARGET_OS_TV + _wantsFocus = YES; +#endif + } + + return self; +} + ++ (instancetype)progressHUDWithStyle:(JGProgressHUDStyle)style { + return [(JGProgressHUD *)[self alloc] initWithStyle:style]; +} + +- (instancetype)initWithAutomaticStyle { + if (@available(iOS 12.0, tvOS 10.0, *)) { + JGProgressHUDStyle initialStyle; + + if (@available(iOS 13.0, tvOS 13.0, *)) { + initialStyle = JGProgressHUDStyleFromUIUserInterfaceStyle([[UITraitCollection currentTraitCollection] userInterfaceStyle]); + } + else { + initialStyle = JGProgressHUDStyleExtraLight; + } + + self = [self initWithStyle:initialStyle]; + + if (self != nil) { + _automaticStyle = YES; + } + } + else { + self = [self initWithStyle:JGProgressHUDStyleExtraLight]; + } + + return self; +} + ++ (instancetype)progressHUDWithAutomaticStyle { + return [[self alloc] initWithAutomaticStyle]; +} + +#pragma mark - Layout + +- (void)setHUDViewFrameCenterWithSize:(CGSize)size insetViewFrame:(CGRect)viewFrame { + CGRect frame = (CGRect){CGPointZero, size}; + + switch (self.position) { + case JGProgressHUDPositionTopLeft: + frame.origin.x = CGRectGetMinX(viewFrame); + frame.origin.y = CGRectGetMinY(viewFrame); + break; + + case JGProgressHUDPositionTopCenter: + frame.origin.x = CGRectGetMidX(viewFrame) - size.width/2.0; + frame.origin.y = CGRectGetMinY(viewFrame); + break; + + case JGProgressHUDPositionTopRight: + frame.origin.x = CGRectGetMaxX(viewFrame) - size.width; + frame.origin.y = CGRectGetMinY(viewFrame); + break; + + case JGProgressHUDPositionCenterLeft: + frame.origin.x = CGRectGetMinX(viewFrame); + frame.origin.y = CGRectGetMidY(viewFrame) - size.height/2.0; + break; + + case JGProgressHUDPositionCenter: + frame.origin.x = CGRectGetMidX(viewFrame) - size.width/2.0; + frame.origin.y = CGRectGetMidY(viewFrame) - size.height/2.0; + break; + + case JGProgressHUDPositionCenterRight: + frame.origin.x = CGRectGetMaxX(viewFrame) - frame.size.width; + frame.origin.y = CGRectGetMidY(viewFrame) - size.height/2.0; + break; + + case JGProgressHUDPositionBottomLeft: + frame.origin.x = CGRectGetMinX(viewFrame); + frame.origin.y = CGRectGetMaxY(viewFrame) - size.height; + break; + + case JGProgressHUDPositionBottomCenter: + frame.origin.x = CGRectGetMidX(viewFrame) - size.width/2.0; + frame.origin.y = CGRectGetMaxY(viewFrame) - frame.size.height; + break; + + case JGProgressHUDPositionBottomRight: + frame.origin.x = CGRectGetMaxX(viewFrame) - size.width; + frame.origin.y = CGRectGetMaxY(viewFrame) - size.height; + break; + } + + CGRect oldHUDFrame = self.HUDView.frame; + CGRect updatedHUDFrame = JGProgressHUD_CGRectIntegral(frame); + + self.HUDView.frame = updatedHUDFrame; + _shadowView.frame = self.HUDView.bounds; + [self updateShadowViewMask]; + + _blurViewContainer.frame = self.HUDView.bounds; + self.blurView.frame = self.HUDView.bounds; + self.vibrancyView.frame = self.HUDView.bounds; + + [UIView performWithoutAnimation:^{ + self.contentView.frame = (CGRect){{(oldHUDFrame.size.width - updatedHUDFrame.size.width)/2.0, (oldHUDFrame.size.height - updatedHUDFrame.size.height)/2.0}, updatedHUDFrame.size}; + }]; + + self.contentView.frame = self.HUDView.bounds; +} + +- (void)updateShadowViewMask { + if (CGRectIsEmpty(_shadowView.layer.bounds)) { + return; + } + + CGRect layerBounds = CGRectMake(0.0, 0.0, _shadowView.layer.bounds.size.width + self.shadow.radius*4.0, _shadowView.layer.bounds.size.height + self.shadow.radius*4.0); + + UIBezierPath *path = [UIBezierPath bezierPathWithRect:layerBounds]; + + CGRect maskRect = CGRectInset(layerBounds, self.shadow.radius*2.0, self.shadow.radius*2.0); + + UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:maskRect cornerRadius:self.cornerRadius]; + + [path appendPath:roundedPath]; + + _shadowMaskLayer.frame = CGRectInset(_shadowView.layer.bounds, -self.shadow.radius*2.0, -self.shadow.radius*2.0); + + CAAnimation *currentAnimation = [self.HUDView.layer animationForKey:@"position"]; + if (currentAnimation != nil) { + [CATransaction begin]; + + [CATransaction setAnimationDuration:currentAnimation.duration]; + [CATransaction setAnimationTimingFunction:currentAnimation.timingFunction]; + + CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; + [_shadowMaskLayer addAnimation:pathAnimation forKey:@"path"]; + + _shadowMaskLayer.path = path.CGPath; + + [CATransaction commit]; + } + else { + _shadowMaskLayer.path = path.CGPath; + // Remove implicit CALayer animations: + [_shadowMaskLayer removeAllAnimations]; + } +} + +- (void)layoutHUD { + if (_transitioning) { + _updateAfterAppear = YES; + return; + } + + if (_targetView == nil) { + return; + } + + CGRect indicatorFrame = self.effectiveIndicatorView.frame; + indicatorFrame.origin.y = self.contentInsets.top; + + CGRect insetFrame = [self insetFrameForView:self]; + + CGFloat maxContentWidth = insetFrame.size.width - self.contentInsets.left - self.contentInsets.right; + CGFloat maxContentHeight = insetFrame.size.height - self.contentInsets.top - self.contentInsets.bottom; + + CGRect labelFrame = CGRectZero; + CGRect detailFrame = CGRectZero; + + CGFloat currentY = CGRectGetMaxY(indicatorFrame); + if (!CGRectIsEmpty(indicatorFrame)) { + currentY += 10.0; + } + + if (_textLabel.text.length > 0) { + _textLabel.preferredMaxLayoutWidth = maxContentWidth; + + CGSize neededSize = _textLabel.intrinsicContentSize; + neededSize.height = MIN(neededSize.height, maxContentHeight); + + labelFrame.size = neededSize; + labelFrame.origin.y = currentY; + currentY = CGRectGetMaxY(labelFrame) + 6.0; + } + + if (_detailTextLabel.text.length > 0) { + _detailTextLabel.preferredMaxLayoutWidth = maxContentWidth; + + CGSize neededSize = _detailTextLabel.intrinsicContentSize; + neededSize.height = MIN(neededSize.height, maxContentHeight); + + detailFrame.size = neededSize; + detailFrame.origin.y = currentY; + } + + CGSize size = CGSizeZero; + + CGFloat width = MIN(self.contentInsets.left + MAX(indicatorFrame.size.width, MAX(labelFrame.size.width, detailFrame.size.width)) + self.contentInsets.right, insetFrame.size.width); + + CGFloat height = MAX(CGRectGetMaxY(labelFrame), MAX(CGRectGetMaxY(detailFrame), CGRectGetMaxY(indicatorFrame))) + self.contentInsets.bottom; + + if (self.square) { + CGFloat uniSize = MAX(width, height); + + size.width = uniSize; + size.height = uniSize; + + CGFloat heightDelta = (uniSize-height)/2.0; + + labelFrame.origin.y += heightDelta; + detailFrame.origin.y += heightDelta; + indicatorFrame.origin.y += heightDelta; + } + else { + size.width = width; + size.height = height; + } + + CGPoint center = CGPointMake(size.width/2.0, size.height/2.0); + + indicatorFrame.origin.x = center.x - indicatorFrame.size.width/2.0; + labelFrame.origin.x = center.x - labelFrame.size.width/2.0; + detailFrame.origin.x = center.x - detailFrame.size.width/2.0; + + [UIView performWithoutAnimation:^{ + self.effectiveIndicatorView.frame = indicatorFrame; + self->_textLabel.frame = JGProgressHUD_CGRectIntegral(labelFrame); + self->_detailTextLabel.frame = JGProgressHUD_CGRectIntegral(detailFrame); + }]; + + [self setHUDViewFrameCenterWithSize:size insetViewFrame:insetFrame]; +} + +- (CGRect)insetFrameForView:(UIView *)view { + CGRect localKeyboardFrame = [view convertRect:[[self class] currentKeyboardFrame] fromView:nil]; + CGRect frame = view.bounds; + + if (!CGRectIsEmpty(localKeyboardFrame) && CGRectIntersectsRect(frame, localKeyboardFrame)) { + CGFloat keyboardMinY = CGRectGetMinY(localKeyboardFrame); + + if (@available(iOS 11, tvOS 11, *)) { + if (self.insetsLayoutMarginsFromSafeArea) { + // This makes sure that the bottom safe area inset is only respected when that area is not covered by the keyboard. When the keyboard covers the bottom area outside of the safe area it is not necessary to keep the bottom safe area insets part of the insets for the HUD. + keyboardMinY += self.safeAreaInsets.bottom; + } + } + + frame.size.height = MAX(MIN(frame.size.height, keyboardMinY), 0.0); + } + + return UIEdgeInsetsInsetRect(frame, view.layoutMargins); +} + +- (void)applyCornerRadius { + self.HUDView.layer.cornerRadius = self.cornerRadius; + _blurViewContainer.layer.cornerRadius = self.cornerRadius; + _shadowView.layer.cornerRadius = self.cornerRadius; + + [self updateShadowViewMask]; +} + +#pragma mark - Showing + +- (void)cleanUpAfterPresentation { +#if TARGET_OS_TV + if (self.wantsFocus) { + [self.targetView setNeedsFocusUpdate]; + } +#endif + + self.hidden = NO; + + _transitioning = NO; + // Correct timestamp to the current time for animated presentations: + _displayTimestamp = CFAbsoluteTimeGetCurrent(); + + if (_effectiveIndicatorViewNeedsUpdate) { + self.effectiveIndicatorView = self.indicatorView; + _effectiveIndicatorViewNeedsUpdate = NO; + _updateAfterAppear = NO; + } + else if (_updateAfterAppear) { + [self layoutHUD]; + _updateAfterAppear = NO; + } + + if ([self.delegate respondsToSelector:@selector(progressHUD:didPresentInView:)]){ + [self.delegate progressHUD:self didPresentInView:self.targetView]; + } + + if (_dismissAfterTransitionFinished) { + [self dismissAnimated:_dismissAfterTransitionFinishedWithAnimation]; + _dismissAfterTransitionFinished = NO; + _dismissAfterTransitionFinishedWithAnimation = NO; + } + else if (self.voiceOverEnabled && UIAccessibilityIsVoiceOverRunning()) { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self); + } +} + +- (void)showInView:(UIView *)view { + [self showInView:view animated:YES]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [self layoutHUD]; +} + +- (void)showInView:(UIView *)view animated:(BOOL)animated { + if (_transitioning) { + return; + } + else if (self.targetView != nil) { +#if DEBUG + NSLog(@"[Warning] The HUD is already visible! Ignoring."); +#endif + return; + } + + if ([self.delegate respondsToSelector:@selector(progressHUD:willPresentInView:)]) { + [self.delegate progressHUD:self willPresentInView:view]; + } + + _targetView = view; + + self.frame = _targetView.bounds; + + [_targetView addSubview:self]; + + self.translatesAutoresizingMaskIntoConstraints = NO; + + [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:_targetView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0].active = YES; + [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:_targetView attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0.0].active = YES; + [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:_targetView attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0].active = YES; + [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_targetView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0].active = YES; + + [self setNeedsLayout]; + [self layoutIfNeeded]; + + _transitioning = YES; + + _displayTimestamp = CFAbsoluteTimeGetCurrent(); + + if (animated && self.animation != nil) { + [self.animation show]; + } + else { + [self cleanUpAfterPresentation]; + } + +#if TARGET_OS_IOS + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardFrameChanged:) name:UIKeyboardWillChangeFrameNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardFrameChanged:) name:UIKeyboardDidChangeFrameNotification object:nil]; +#endif +} + +- (void)showInView:(UIView *__nonnull)view animated:(BOOL)animated afterDelay:(NSTimeInterval)delay { + __weak __typeof(self) weakSelf = self; + + if (_displayBlock != NULL) { + dispatch_cancel(_displayBlock); + } + + _displayBlock = dispatch_block_create(0, ^{ + if (weakSelf) { + __strong __typeof(weakSelf) strongSelf = weakSelf; + strongSelf->_displayBlock = NULL; + + if (!strongSelf.visible) { + [strongSelf showInView:view animated:animated]; + } + } + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), _displayBlock); +} + +#pragma mark - Dismissing + +- (void)cleanUpAfterDismissal { + self.hidden = YES; + [self removeFromSuperview]; + + [self removeObservers]; + + _transitioning = NO; + _dismissAfterTransitionFinished = NO; + _dismissAfterTransitionFinishedWithAnimation = NO; + + __typeof(self.targetView) targetView = self.targetView; + _targetView = nil; + + for (void (^action)(void) in _dismissActions) { + action(); + } + [_dismissActions removeAllObjects]; + + if ([self.delegate respondsToSelector:@selector(progressHUD:didDismissFromView:)]) { + [self.delegate progressHUD:self didDismissFromView:targetView]; + } +} + +- (void)dismiss { + [self dismissAnimated:YES]; +} + +- (void)dismissAnimated:(BOOL)animated { + if (_displayBlock != NULL) { + dispatch_cancel(_displayBlock); + _displayBlock = NULL; + } + + if (_transitioning) { + _dismissAfterTransitionFinished = YES; + _dismissAfterTransitionFinishedWithAnimation = animated; + return; + } + + if (self.targetView == nil) { + return; + } + + if (self.minimumDisplayTime > 0.0 && _displayTimestamp > 0.0) { + CFAbsoluteTime displayedTime = CFAbsoluteTimeGetCurrent()-_displayTimestamp; + + if (displayedTime < self.minimumDisplayTime) { + NSTimeInterval delta = self.minimumDisplayTime-displayedTime; + + [self dismissAfterDelay:delta animated:animated]; + + return; + } + } + + if ([self.delegate respondsToSelector:@selector(progressHUD:willDismissFromView:)]) { + [self.delegate progressHUD:self willDismissFromView:self.targetView]; + } + + _transitioning = YES; + + if (animated && self.animation) { + [self.animation hide]; + } + else { + [self cleanUpAfterDismissal]; + } +} + +- (void)dismissAfterDelay:(NSTimeInterval)delay { + [self dismissAfterDelay:delay animated:YES]; +} + +- (void)dismissAfterDelay:(NSTimeInterval)delay animated:(BOOL)animated { + [self dismissAfterDelay:delay animated:animated completion:nil]; +} + +- (void)dismissAfterDelay:(NSTimeInterval)delay animated:(BOOL)animated completion:(void (^_Nullable)(void))dismissCompletion { + __weak __typeof(self) weakSelf = self; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (weakSelf) { + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf.visible) { + if (dismissCompletion != nil) { + [self performAfterDismiss:dismissCompletion]; + } + [strongSelf dismissAnimated:animated]; + } + } + }); +} + +- (void)performAfterDismiss:(void (^_Nonnull)(void))dismissCompletion { + if (self.visible) { + [_dismissActions addObject:dismissCompletion]; + } +} + +#pragma mark - Callbacks + +#if TARGET_OS_IOS +- (void)tapped:(UITapGestureRecognizer *)t { + if (CGRectContainsPoint(self.contentView.bounds, [t locationInView:self.contentView])) { + if (self.tapOnHUDViewBlock != nil) { + self.tapOnHUDViewBlock(self); + } + } + else if (self.tapOutsideBlock != nil) { + self.tapOutsideBlock(self); + } +} + +static inline UIViewAnimationOptions UIViewAnimationOptionsFromUIViewAnimationCurve(UIViewAnimationCurve curve) { + UIViewAnimationOptions testOptions = UIViewAnimationCurveLinear << 16; + + if (testOptions != UIViewAnimationOptionCurveLinear) { + NSLog(@"Unexpected implementation of UIViewAnimationOptionCurveLinear"); + } + + return (UIViewAnimationOptions)(curve << 16); +} + +- (void)keyboardFrameChanged:(NSNotification *)notification { + NSTimeInterval duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + UIViewAnimationCurve curve = (UIViewAnimationCurve)[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; + + [UIView animateWithDuration:duration delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionsFromUIViewAnimationCurve(curve) animations:^{ + [self layoutHUD]; + } completion:nil]; +} +#endif + +- (void)updateMotionOnHUDView { + BOOL reduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled(); + + BOOL wantsParallax = ((self.parallaxMode == JGProgressHUDParallaxModeDevice && !reduceMotionEnabled) || self.parallaxMode == JGProgressHUDParallaxModeAlwaysOn); + BOOL hasParallax = (self.HUDView.motionEffects.count > 0); + + if (wantsParallax == hasParallax) { + return; + } + + if (!wantsParallax) { + self.HUDView.motionEffects = @[]; + } + else { + UIInterpolatingMotionEffect *x = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; + + CGFloat maxMovement = 20.0; + + x.minimumRelativeValue = @(-maxMovement); + x.maximumRelativeValue = @(maxMovement); + + UIInterpolatingMotionEffect *y = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; + + y.minimumRelativeValue = @(-maxMovement); + y.maximumRelativeValue = @(maxMovement); + + self.HUDView.motionEffects = @[x, y]; + } +} + +- (void)animationDidFinish:(BOOL)presenting { + if (presenting) { + [self cleanUpAfterPresentation]; + } + else { + [self cleanUpAfterDismissal]; + } +} + +#pragma mark - Getters + +- (BOOL)isVisible { + return (self.superview != nil); +} + +- (UIVisualEffectView *)blurView { + if (!_blurView) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateMotionOnHUDView) name:UIAccessibilityReduceMotionStatusDidChangeNotification object:nil]; + + UIBlurEffectStyle effect; + + if (self.style == JGProgressHUDStyleDark) { + effect = UIBlurEffectStyleDark; + } + else if (self.style == JGProgressHUDStyleLight) { + effect = UIBlurEffectStyleLight; + } + else { + effect = UIBlurEffectStyleExtraLight; + } + + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:effect]; + + _blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + + [self updateMotionOnHUDView]; + + [_blurViewContainer addSubview:_blurView]; + +#if TARGET_OS_IOS + [self.contentView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)]]; +#endif + } + + return _blurView; +} + +- (UIVisualEffectView *)vibrancyView { + if (!_vibrancyView) { + UIVibrancyEffect *vibrancyEffect = (self.vibrancyEnabled ? [UIVibrancyEffect effectForBlurEffect:(UIBlurEffect *)self.blurView.effect] : nil); + + _vibrancyView = [[UIVisualEffectView alloc] initWithEffect:vibrancyEffect]; + + [self.blurView.contentView addSubview:_vibrancyView]; + } + + return _vibrancyView; +} + +- (UIView *)contentView { + if (_contentView == nil) { + _contentView = [[UIView alloc] init]; + [self.vibrancyView.contentView addSubview:_contentView]; + + if (self.effectiveIndicatorView != nil) { + [self.contentView addSubview:self.effectiveIndicatorView]; + } + } + + return _contentView; +} + +- (UILabel *)textLabel { + if (!_textLabel) { + _textLabel = [[UILabel alloc] init]; + _textLabel.backgroundColor = [UIColor clearColor]; + _textLabel.textColor = (self.style == JGProgressHUDStyleDark ? [UIColor whiteColor] : [UIColor blackColor]); + _textLabel.textAlignment = NSTextAlignmentCenter; +#if TARGET_OS_TV + CGFloat fontSize = 20.0; +#else + CGFloat fontSize = 17.0; +#endif + if (self.thick) { + fontSize *= 1.3; + } + + _textLabel.font = [UIFont boldSystemFontOfSize:fontSize]; + _textLabel.numberOfLines = 0; + [_textLabel addObserver:self forKeyPath:@"attributedText" options:(NSKeyValueObservingOptions)kNilOptions context:NULL]; + [_textLabel addObserver:self forKeyPath:@"text" options:(NSKeyValueObservingOptions)kNilOptions context:NULL]; + [_textLabel addObserver:self forKeyPath:@"font" options:(NSKeyValueObservingOptions)kNilOptions context:NULL]; + _textLabel.isAccessibilityElement = YES; + + [self.contentView addSubview:_textLabel]; + } + + return _textLabel; +} + +- (UILabel *)detailTextLabel { + if (!_detailTextLabel) { + _detailTextLabel = [[UILabel alloc] init]; + _detailTextLabel.backgroundColor = [UIColor clearColor]; + _detailTextLabel.textColor = (self.style == JGProgressHUDStyleDark ? [UIColor whiteColor] : [UIColor blackColor]); + _detailTextLabel.textAlignment = NSTextAlignmentCenter; +#if TARGET_OS_TV + CGFloat fontSize = 17.0; +#else + CGFloat fontSize = 15.0; +#endif + if (self.thick) { + fontSize *= 1.3; + } + + _detailTextLabel.font = [UIFont systemFontOfSize:fontSize]; + _detailTextLabel.numberOfLines = 0; + [_detailTextLabel addObserver:self forKeyPath:@"attributedText" options:(NSKeyValueObservingOptions)kNilOptions context:NULL]; + [_detailTextLabel addObserver:self forKeyPath:@"text" options:(NSKeyValueObservingOptions)kNilOptions context:NULL]; + [_detailTextLabel addObserver:self forKeyPath:@"font" options:(NSKeyValueObservingOptions)kNilOptions context:NULL]; + _detailTextLabel.isAccessibilityElement = YES; + + [self.contentView addSubview:_detailTextLabel]; + } + + return _detailTextLabel; +} + +- (JGProgressHUDAnimation *)animation { + if (!_animation) { + self.animation = [JGProgressHUDFadeAnimation animation]; + } + + return _animation; +} + +#pragma mark - Setters + +- (void)setStyle:(JGProgressHUDStyle)style { + if (self.style == style) { + return; + } + + _style = style; + + // Update indicator + [self.effectiveIndicatorView setUpForHUDStyle:self.style vibrancyEnabled:self.vibrancyEnabled]; + + // Update labels + self.textLabel.textColor = (self.style == JGProgressHUDStyleDark ? [UIColor whiteColor] : [UIColor blackColor]); + self.detailTextLabel.textColor = (self.style == JGProgressHUDStyleDark ? [UIColor whiteColor] : [UIColor blackColor]); + + // Update blur effect + UIBlurEffectStyle effect; + + if (self.style == JGProgressHUDStyleDark) { + effect = UIBlurEffectStyleDark; + } + else if (self.style == JGProgressHUDStyleLight) { + effect = UIBlurEffectStyleLight; + } + else { + effect = UIBlurEffectStyleExtraLight; + } + + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:effect]; + self.blurView.effect = blurEffect; + + // Update vibrancy effect + UIVibrancyEffect *vibrancyEffect = (self.vibrancyEnabled ? [UIVibrancyEffect effectForBlurEffect:(UIBlurEffect *)self.blurView.effect] : nil); + self.vibrancyView.effect = vibrancyEffect; +} + +#if TARGET_OS_TV +- (void)setWantsFocus:(BOOL)wantsFocus { + if (self.wantsFocus == wantsFocus) { + return; + } + + _wantsFocus = wantsFocus; + + self.userInteractionEnabled = self.wantsFocus; + + [self.targetView setNeedsFocusUpdate]; +} +#endif + +- (void)setCornerRadius:(CGFloat)cornerRadius { + if (fequal(self.cornerRadius, cornerRadius)) { + return; + } + + _cornerRadius = cornerRadius; + + [self applyCornerRadius]; +} + +- (void)setShadow:(JGProgressHUDShadow *)shadow { + if (shadow == self.shadow) { + return; + } + + _shadow = shadow; + + [self updateShadowViewMask]; + + if (_shadow != nil) { + _shadowView.layer.shadowColor = _shadow.color.CGColor; + _shadowView.layer.shadowOffset = _shadow.offset; + _shadowView.layer.shadowRadius = _shadow.radius; + + _shadowView.alpha = _shadow.opacity; + } + else { + _shadowView.layer.shadowOffset = CGSizeZero; + _shadowView.layer.shadowRadius = 0.0; + + _shadowView.alpha = 0.0; + } +} + +- (void)setAnimation:(JGProgressHUDAnimation *)animation { + if (_animation == animation) { + return; + } + + _animation.progressHUD = nil; + + _animation = animation; + + _animation.progressHUD = self; +} + +- (void)setParallaxMode:(JGProgressHUDParallaxMode)parallaxMode { + if (self.parallaxMode == parallaxMode) { + return; + } + + _parallaxMode = parallaxMode; + + [self updateMotionOnHUDView]; +} + +- (void)setPosition:(JGProgressHUDPosition)position { + if (self.position == position) { + return; + } + + _position = position; + [self layoutHUD]; +} + +- (void)setSquare:(BOOL)square { + if (self.square == square) { + return; + } + + _square = square; + + [self layoutHUD]; +} + +- (void)setThick:(BOOL)thick { + if (self.thick == thick) { + return; + } + + _thick = thick; + + if (self.thick) { + self.indicatorView.transform = CGAffineTransformMakeScale(1.5, 1.5); + } + else { + self.indicatorView.transform = CGAffineTransformIdentity; + } + +#if TARGET_OS_TV + CGFloat fontSize = 20.0; +#else + CGFloat fontSize = 17.0; +#endif + if (self.thick) { + fontSize *= 1.3; + } + + _textLabel.font = [UIFont boldSystemFontOfSize:fontSize]; +#if TARGET_OS_TV + fontSize = 17.0; +#else + fontSize = 15.0; +#endif + if (self.thick) { + fontSize *= 1.3; + } + + _detailTextLabel.font = [UIFont systemFontOfSize:fontSize]; + + [self layoutHUD]; +} + +- (void)setVibrancyEnabled:(BOOL)vibrancyEnabled { + if (vibrancyEnabled == self.vibrancyEnabled) { + return; + } + + _vibrancyEnabled = vibrancyEnabled; + + UIVibrancyEffect *vibrancyEffect = (self.vibrancyEnabled ? [UIVibrancyEffect effectForBlurEffect:(UIBlurEffect *)self.blurView.effect] : nil); + + self.vibrancyView.effect = vibrancyEffect; + + [self.effectiveIndicatorView setUpForHUDStyle:self.style vibrancyEnabled:self.vibrancyEnabled]; +} + +- (void)setIndicatorView:(JGProgressHUDIndicatorView *)indicatorView { + if (self.indicatorView == indicatorView) { + return; + } + + _indicatorView = indicatorView; + + if (_transitioning) { + _effectiveIndicatorViewNeedsUpdate = YES; + return; + } + else { + self.effectiveIndicatorView = self.indicatorView; + } +} + +- (void)setEffectiveIndicatorView:(JGProgressHUDIndicatorView * _Nonnull)effectiveIndicatorView { + if (self.effectiveIndicatorView == effectiveIndicatorView) { + return; + } + + [UIView performWithoutAnimation:^{ + [self->_effectiveIndicatorView removeFromSuperview]; + self->_effectiveIndicatorView = effectiveIndicatorView; + + if (self.indicatorView != nil) { + [self.effectiveIndicatorView setUpForHUDStyle:self.style vibrancyEnabled:self.vibrancyEnabled]; + + if (self.thick) { + self.effectiveIndicatorView.transform = CGAffineTransformMakeScale(1.5, 1.5); + } + else { + self.effectiveIndicatorView.transform = CGAffineTransformIdentity; + } + + [self.contentView addSubview:self.effectiveIndicatorView]; + } + }]; + + [self layoutHUD]; +} + +- (void)layoutMarginsDidChange { + [super layoutMarginsDidChange]; + + [self layoutHUD]; +} + +- (void)setContentInsets:(UIEdgeInsets)contentInsets { + if (UIEdgeInsetsEqualToEdgeInsets(self.contentInsets, contentInsets)) { + return; + } + + _contentInsets = contentInsets; + + [self layoutHUD]; +} + +- (void)setProgress:(float)progress { + [self setProgress:progress animated:NO]; +} + +- (void)setProgress:(float)progress animated:(BOOL)animated { + if (fequal(self.progress, progress)) { + return; + } + + _progress = progress; + + [self.effectiveIndicatorView setProgress:progress animated:animated]; +} + +#pragma mark - Overrides + +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { + [super traitCollectionDidChange:previousTraitCollection]; + + if (@available(iOS 12.0, tvOS 10.0, *)) { + if (_automaticStyle) { + self.style = JGProgressHUDStyleFromUIUserInterfaceStyle(self.traitCollection.userInterfaceStyle); + } + } +} + +#if TARGET_OS_IOS +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + if (self.interactionType == JGProgressHUDInteractionTypeBlockNoTouches) { + return nil; + } + else { + UIView *view = [super hitTest:point withEvent:event]; + + if (self.interactionType == JGProgressHUDInteractionTypeBlockAllTouches) { + return view; + } + else if (self.interactionType == JGProgressHUDInteractionTypeBlockTouchesOnHUDView && view != self) { + return view; + } + + return nil; + } +} +#elif TARGET_OS_TV +- (NSArray> *)preferredFocusEnvironments { + return @[self]; +} + +- (UIView *)preferredFocusedView { + return nil; +} + +- (BOOL)canBecomeFocused { + return self.wantsFocus; +} +#endif + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == _textLabel || object == _detailTextLabel) { + [self layoutHUD]; + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)dealloc { + [self removeObservers]; + + [_textLabel removeObserver:self forKeyPath:@"attributedText"]; + [_textLabel removeObserver:self forKeyPath:@"text"]; + [_textLabel removeObserver:self forKeyPath:@"font"]; + + [_detailTextLabel removeObserver:self forKeyPath:@"attributedText"]; + [_detailTextLabel removeObserver:self forKeyPath:@"text"]; + [_detailTextLabel removeObserver:self forKeyPath:@"font"]; +} + +- (void)removeObservers { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityReduceMotionStatusDidChangeNotification object:nil]; + +#if TARGET_OS_IOS + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidChangeFrameNotification object:nil]; +#endif +} + +@end + +@implementation JGProgressHUD (HUDManagement) + ++ (NSArray *)allProgressHUDsInView:(UIView *)view { + NSMutableArray *HUDs = [NSMutableArray array]; + + for (UIView *v in view.subviews) { + if ([v isKindOfClass:[JGProgressHUD class]]) { + [HUDs addObject:v]; + } + } + + return HUDs.copy; +} + ++ (NSMutableArray *)_allProgressHUDsInViewHierarchy:(UIView *)view { + NSMutableArray *HUDs = [NSMutableArray array]; + + for (UIView *v in view.subviews) { + if ([v isKindOfClass:[JGProgressHUD class]]) { + [HUDs addObject:v]; + } + else { + [HUDs addObjectsFromArray:[self _allProgressHUDsInViewHierarchy:v]]; + } + } + + return HUDs; +} + ++ (NSArray *)allProgressHUDsInViewHierarchy:(UIView *)view { + return [self _allProgressHUDsInViewHierarchy:view].copy; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDAnimation.h b/JGProgressHUD/JGProgressHUDAnimation.h new file mode 100644 index 0000000..67679b6 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDAnimation.h @@ -0,0 +1,43 @@ +// +// JGProgressHUDAnimation.h +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import +#import + +@class JGProgressHUD; + +/** + You may subclass this class to create a custom progress indicator view. + */ +@interface JGProgressHUDAnimation : NSObject + +/** Convenience initializer. */ ++ (instancetype __nonnull)animation; + +/** The HUD using this animation. */ +@property (nonatomic, weak, readonly, nullable) JGProgressHUD *progressHUD; + +/** + The @c progressHUD is hidden from screen with @c alpha = 1 and @c hidden = @c YES. Ideally, you should prepare the HUD for presentation, then set @c hidden to @c NO on the @c progressHUD and then perform the animation. + @post Call @c animationFinished. + */ +- (void)show NS_REQUIRES_SUPER; + +/** + The @c progressHUD wis visible on screen with @c alpha = 1 and @c hidden = @c NO. You should only perform the animation in this method, the @c progressHUD itself will take care of hiding itself and removing itself from superview. + @post Call @c animationFinished. + */ +- (void)hide NS_REQUIRES_SUPER; + +/** + @pre This method should only be called at the end of a @c show or @c hide animaiton. + @attention ALWAYS call this method after completing a @c show or @c hide animation. + */ +- (void)animationFinished NS_REQUIRES_SUPER; + +@end diff --git a/JGProgressHUD/JGProgressHUDAnimation.m b/JGProgressHUD/JGProgressHUDAnimation.m new file mode 100755 index 0000000..fb2628f --- /dev/null +++ b/JGProgressHUD/JGProgressHUDAnimation.m @@ -0,0 +1,48 @@ +// +// JGProgressHUDAnimation.m +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDAnimation.h" +#import "JGProgressHUD.h" + +@interface JGProgressHUD (Private) + +- (void)animationDidFinish:(BOOL)presenting; + +@end + +@interface JGProgressHUDAnimation () { + BOOL _presenting; +} + +@property (nonatomic, weak) JGProgressHUD *progressHUD; + +@end + +@implementation JGProgressHUDAnimation + +#pragma mark - Initializers + ++ (instancetype)animation { + return [[self alloc] init]; +} + +#pragma mark - Public methods + +- (void)show { + _presenting = YES; +} + +- (void)hide { + _presenting = NO; +} + +- (void)animationFinished { + [self.progressHUD animationDidFinish:_presenting]; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDErrorIndicatorView.h b/JGProgressHUD/JGProgressHUDErrorIndicatorView.h new file mode 100644 index 0000000..96f7731 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDErrorIndicatorView.h @@ -0,0 +1,24 @@ +// +// JGProgressHUDErrorIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 19.08.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDImageIndicatorView.h" +#pragma clang diagnostic pop + +/** + An image indicator showing a cross, representing a failed operation. + */ +@interface JGProgressHUDErrorIndicatorView : JGProgressHUDImageIndicatorView + +/** + Default initializer for this class. + */ +- (instancetype __nonnull)init; + +@end diff --git a/JGProgressHUD/JGProgressHUDErrorIndicatorView.m b/JGProgressHUD/JGProgressHUDErrorIndicatorView.m new file mode 100644 index 0000000..d9a7d31 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDErrorIndicatorView.m @@ -0,0 +1,57 @@ +// +// JGProgressHUDErrorIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 19.08.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDErrorIndicatorView.h" +#import "JGProgressHUD.h" + +static UIBezierPath *errorBezierPath() { + static UIBezierPath *path; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointMake(3, 3)]; + [path addLineToPoint:CGPointMake(30, 30)]; + [path moveToPoint:CGPointMake(30, 3)]; + [path addLineToPoint:CGPointMake(3, 30)]; + + [path setLineWidth:3]; + [path setLineJoinStyle:kCGLineJoinRound]; + [path setLineCapStyle:kCGLineCapRound]; + }); + + return path; +} + +@implementation JGProgressHUDErrorIndicatorView + +- (instancetype)initWithContentView:(UIView *__unused)contentView { + UIBezierPath *path = errorBezierPath(); + + UIGraphicsBeginImageContextWithOptions(CGSizeMake(33, 33), NO, 0.0); + [[UIColor blackColor] setStroke]; + [path stroke]; + + UIImage *img = [UIGraphicsGetImageFromCurrentImageContext() imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + + UIGraphicsEndImageContext(); + + self = [super initWithImage:img]; + + return self; +} + +- (instancetype)init { + return [self initWithContentView:nil]; +} + +- (void)updateAccessibility { + self.accessibilityLabel = NSLocalizedString(@"Error",); +} + +@end diff --git a/JGProgressHUD/JGProgressHUDFadeAnimation.h b/JGProgressHUD/JGProgressHUDFadeAnimation.h new file mode 100644 index 0000000..6460265 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDFadeAnimation.h @@ -0,0 +1,33 @@ +// +// JGProgressHUDFadeAnimation.h +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDAnimation.h" +#pragma clang diagnostic pop + +/** + A simple fade animation that fades the HUD from alpha @c 0.0 to alpha @c 1.0. + */ +@interface JGProgressHUDFadeAnimation : JGProgressHUDAnimation + +/** + Duration of the animation. + + @b Default: 0.4. + */ +@property (nonatomic, assign) NSTimeInterval duration; + +/** + Animation options + + @b Default: UIViewAnimationOptionCurveEaseInOut. + */ +@property (nonatomic, assign) UIViewAnimationOptions animationOptions; + +@end diff --git a/JGProgressHUD/JGProgressHUDFadeAnimation.m b/JGProgressHUD/JGProgressHUDFadeAnimation.m new file mode 100755 index 0000000..bd86290 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDFadeAnimation.m @@ -0,0 +1,56 @@ +// +// JGProgressHUDFadeAnimation.m +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDFadeAnimation.h" +#import "JGProgressHUD.h" + +@implementation JGProgressHUDFadeAnimation + +#pragma mark - Initializers + +- (instancetype)init { + self = [super init]; + if (self) { + self.duration = 0.4; + self.animationOptions = UIViewAnimationOptionCurveEaseInOut; + } + return self; +} + +- (void)setAnimationOptions:(UIViewAnimationOptions)animationOptions { + _animationOptions = (animationOptions | UIViewAnimationOptionBeginFromCurrentState); +} + +#pragma mark - Showing + +- (void)show { + [super show]; + + self.progressHUD.alpha = 0.0; + self.progressHUD.hidden = NO; + + [UIView animateWithDuration:self.duration delay:0.0 options:self.animationOptions animations:^{ + self.progressHUD.alpha = 1.0; + } completion:^(BOOL __unused finished) { + [self animationFinished]; + }]; +} + +#pragma mark - Hiding + +- (void)hide { + [super hide]; + + [UIView animateWithDuration:self.duration delay:0.0 options:self.animationOptions animations:^{ + self.progressHUD.alpha = 0.0; + } completion:^(BOOL __unused finished) { + [self animationFinished]; + }]; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDFadeZoomAnimation.h b/JGProgressHUD/JGProgressHUDFadeZoomAnimation.h new file mode 100644 index 0000000..0746f0b --- /dev/null +++ b/JGProgressHUD/JGProgressHUDFadeZoomAnimation.h @@ -0,0 +1,40 @@ +// +// JGProgressHUDFadeZoomAnimation.h +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDAnimation.h" +#pragma clang diagnostic pop + +/** + An animation that fades in the HUD and expands the HUD from scale @c (0, 0) to a customizable scale, and finally to scale @c (1, 1), creating a bouncing effect. + */ +@interface JGProgressHUDFadeZoomAnimation : JGProgressHUDAnimation + +/** + Duration of the animation from or to the shrinked state. + + @b Default: 0.2. + */ +@property (nonatomic, assign) NSTimeInterval shrinkAnimationDuaration; + +/** + Duration of the animation from or to the expanded state. + + @b Default: 0.1. + */ +@property (nonatomic, assign) NSTimeInterval expandAnimationDuaration; + +/** + The scale to apply to the HUD when expanding. + + @b Default: (1.1, 1.1). + */ +@property (nonatomic, assign) CGSize expandScale; + +@end diff --git a/JGProgressHUD/JGProgressHUDFadeZoomAnimation.m b/JGProgressHUD/JGProgressHUDFadeZoomAnimation.m new file mode 100755 index 0000000..f95af60 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDFadeZoomAnimation.m @@ -0,0 +1,77 @@ +// +// JGProgressHUDFadeZoomAnimation.m +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDFadeZoomAnimation.h" +#import "JGProgressHUD.h" + +@implementation JGProgressHUDFadeZoomAnimation + +#pragma mark - Initializers + +- (instancetype)init { + self = [super init]; + if (self) { + self.shrinkAnimationDuaration = 0.2; + self.expandAnimationDuaration = 0.1; + self.expandScale = CGSizeMake(1.1, 1.1); + } + return self; +} + +#pragma mark - Showing + +- (void)show { + [super show]; + + self.progressHUD.alpha = 0.0; + self.progressHUD.HUDView.transform = CGAffineTransformMakeScale(0.1, 0.1); + + NSTimeInterval totalDuration = self.expandAnimationDuaration+self.shrinkAnimationDuaration; + + self.progressHUD.hidden = NO; + + [UIView animateWithDuration:totalDuration delay:0.0 options:(UIViewAnimationOptions)(UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut) animations:^{ + self.progressHUD.alpha = 1.0; + } completion:nil]; + + [UIView animateWithDuration:self.shrinkAnimationDuaration delay:0.0 options:(UIViewAnimationOptions)(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) animations:^{ + self.progressHUD.HUDView.transform = CGAffineTransformMakeScale(self.expandScale.width, self.expandScale.height); + } completion:^(BOOL __unused _finished) { + [UIView animateWithDuration:self.expandAnimationDuaration delay:0.0 options:(UIViewAnimationOptions)(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState) animations:^{ + self.progressHUD.HUDView.transform = CGAffineTransformIdentity; + } completion:^(BOOL __unused __finished) { + [self animationFinished]; + }]; + }]; +} + +#pragma mark - Hiding + +- (void)hide { + [super hide]; + + NSTimeInterval totalDuration = self.expandAnimationDuaration+self.shrinkAnimationDuaration; + + [UIView animateWithDuration:totalDuration delay:0.0 options:(UIViewAnimationOptions)(UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut) animations:^{ + self.progressHUD.alpha = 0.0; + } completion:nil]; + + [UIView animateWithDuration:self.expandAnimationDuaration delay:0.0 options:(UIViewAnimationOptions)(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) animations:^{ + self.progressHUD.HUDView.transform = CGAffineTransformMakeScale(self.expandScale.width, self.expandScale.height); + } completion:^(BOOL __unused _finished) { + [UIView animateWithDuration:self.shrinkAnimationDuaration delay:0.0 options:(UIViewAnimationOptions)(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState) animations:^{ + self.progressHUD.HUDView.transform = CGAffineTransformMakeScale(0.1, 0.1); + } completion:^(BOOL __unused __finished) { + self.progressHUD.HUDView.transform = CGAffineTransformIdentity; + + [self animationFinished]; + }]; + }]; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDImageIndicatorView.h b/JGProgressHUD/JGProgressHUDImageIndicatorView.h new file mode 100644 index 0000000..4922238 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDImageIndicatorView.h @@ -0,0 +1,28 @@ +// +// JGProgressHUDImageIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 05.08.15. +// Copyright (c) 2015 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDIndicatorView.h" +#pragma clang diagnostic pop + +/** + An indicator for displaying custom images. Supports animated images. + + You may subclass this class to create a custom image indicator view. + */ +@interface JGProgressHUDImageIndicatorView : JGProgressHUDIndicatorView + +/** + Initializes the indicator view with an UIImageView showing the @c image. + + @param image The image to show in the indicator view. + */ +- (instancetype __nonnull)initWithImage:(UIImage *__nonnull)image; + +@end diff --git a/JGProgressHUD/JGProgressHUDImageIndicatorView.m b/JGProgressHUD/JGProgressHUDImageIndicatorView.m new file mode 100644 index 0000000..0a38d1f --- /dev/null +++ b/JGProgressHUD/JGProgressHUDImageIndicatorView.m @@ -0,0 +1,42 @@ +// +// JGProgressHUDImageIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 05.08.15. +// Copyright (c) 2015 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDImageIndicatorView.h" + +@implementation JGProgressHUDImageIndicatorView { + BOOL _customizedTintColor; +} + +- (instancetype)initWithImage:(UIImage *)image { + UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; + + self = [super initWithContentView:imageView]; + + return self; +} + +- (void)setUpForHUDStyle:(JGProgressHUDStyle)style vibrancyEnabled:(BOOL)vibrancyEnabled { + [super setUpForHUDStyle:style vibrancyEnabled:vibrancyEnabled]; + + if (!_customizedTintColor) { + if (style == JGProgressHUDStyleDark) { + self.tintColor = [UIColor whiteColor]; + } + else { + self.tintColor = [UIColor blackColor]; + } + _customizedTintColor = NO; + } +} + +- (void)setTintColor:(UIColor *)tintColor { + [super setTintColor:tintColor]; + _customizedTintColor = YES; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.h b/JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.h new file mode 100644 index 0000000..74594be --- /dev/null +++ b/JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.h @@ -0,0 +1,25 @@ +// +// JGProgressHUDIndeterminateIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 19.07.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDIndicatorView.h" +#pragma clang diagnostic pop + +/** + An indeterminate progress indicator showing a @c UIActivityIndicatorView. + */ +@interface JGProgressHUDIndeterminateIndicatorView : JGProgressHUDIndicatorView + +/** + Set the color of the activity indicator view. + @param color The color to apply to the activity indicator view. + */ +- (void)setColor:(UIColor *__nonnull)color; + +@end diff --git a/JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.m b/JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.m new file mode 100644 index 0000000..c9880d4 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDIndeterminateIndicatorView.m @@ -0,0 +1,63 @@ +// +// JGProgressHUDIndeterminateIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 19.07.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDIndeterminateIndicatorView.h" + +#ifndef __IPHONE_13_0 +#define __IPHONE_13_0 130000 +#endif + +@implementation JGProgressHUDIndeterminateIndicatorView + +- (instancetype)init { + UIActivityIndicatorView *activityIndicatorView; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 + if (@available(iOS 13, tvOS 13, *)) { + activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge]; + } + else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; +#pragma clang diagnostic pop + } +#else + activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; +#endif + + [activityIndicatorView startAnimating]; + + self = [super initWithContentView:activityIndicatorView]; + return self; +} + +- (instancetype)initWithHUDStyle:(JGProgressHUDStyle)style { + return [self init]; +} + +- (void)setUpForHUDStyle:(JGProgressHUDStyle)style vibrancyEnabled:(BOOL)vibrancyEnabled { + [super setUpForHUDStyle:style vibrancyEnabled:vibrancyEnabled]; + + if (style != JGProgressHUDStyleDark) { + self.color = [UIColor blackColor]; + } + else { + self.color = [UIColor whiteColor]; + } +} + +- (void)setColor:(UIColor *)color { + [(UIActivityIndicatorView *)self.contentView setColor:color]; +} + +- (void)updateAccessibility { + self.accessibilityLabel = NSLocalizedString(@"Indeterminate progress",); +} + +@end diff --git a/JGProgressHUD/JGProgressHUDIndicatorView.h b/JGProgressHUD/JGProgressHUDIndicatorView.h new file mode 100644 index 0000000..147616e --- /dev/null +++ b/JGProgressHUD/JGProgressHUDIndicatorView.h @@ -0,0 +1,61 @@ +// +// JGProgressHUDIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUD-Defines.h" +#pragma clang diagnostic pop + +/** You may subclass this class to create a custom progress indicator view. */ +@interface JGProgressHUDIndicatorView : UIView + +/** + Designated initializer for this class. + + @param contentView The content view to place on the container view (the container is the JGProgressHUDIndicatorView). + */ +- (instancetype __nonnull)initWithContentView:(UIView *__nullable)contentView; + +/** Use this method to set up the indicator view to fit the HUD style and vibrancy setting. This method is called by @c JGProgressHUD when the indicator view is added to the HUD and when the HUD's @c vibrancyEnabled property changes. This method may be called multiple times with different values. The default implementation does nothing. */ +- (void)setUpForHUDStyle:(JGProgressHUDStyle)style vibrancyEnabled:(BOOL)vibrancyEnabled; + +/** Ranges from 0.0 to 1.0. */ +@property (nonatomic, assign) float progress; + +/** + Adjusts the current progress shown by the receiver, optionally animating the change. + + The current progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the completion of the task. The default value is 0.0. Values less than 0.0 and greater than 1.0 are pinned to those limits. + + @param progress The new progress value. + @param animated YES if the change should be animated, NO if the change should happen immediately. + */ +- (void)setProgress:(float)progress animated:(BOOL)animated; + +/** + The content view which displays the progress. + */ +@property (nonatomic, strong, readonly, nullable) UIView *contentView; + +/** Schedules an accessibility update on the next run loop. */ +- (void)setNeedsAccessibilityUpdate; + +/** + Runs @c updateAccessibility immediately if an accessibility update has been scheduled (through @c setNeedsAccessibilityUpdate) but has not executed yet. + */ +- (void)updateAccessibilityIfNeeded; + +/** + Override to set custom accessibility properties. This method gets called once when initializing the view and after calling @c setNeedsAccessibilityUpdate. + */ +- (void)updateAccessibility; + +@end diff --git a/JGProgressHUD/JGProgressHUDIndicatorView.m b/JGProgressHUD/JGProgressHUDIndicatorView.m new file mode 100755 index 0000000..1a69631 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDIndicatorView.m @@ -0,0 +1,103 @@ +// +// JGProgressHUDIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDIndicatorView.h" +#import "JGProgressHUD.h" + +@interface JGProgressHUDIndicatorView () { + BOOL _accessibilityUpdateScheduled; +} + ++ (void)runBlock:(void (^)(void))block; + +@end + +static void runOnNextRunLoop(void (^block)(void)) { + [[NSRunLoop currentRunLoop] performSelector:@selector(runBlock:) target:[JGProgressHUDIndicatorView class] argument:(id)block order:0 modes:@[NSRunLoopCommonModes]]; +} + +@implementation JGProgressHUDIndicatorView + +#pragma mark - Initializers + +- (instancetype)initWithFrame:(CGRect __unused)frame { + return [self init]; +} + +- (instancetype)init { + return [self initWithContentView:nil]; +} + +- (instancetype)initWithContentView:(UIView *)contentView { + self = [super initWithFrame:(contentView ? contentView.frame : CGRectMake(0.0, 0.0, 50.0, 50.0))]; + if (self) { + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + + self.isAccessibilityElement = YES; + [self setNeedsAccessibilityUpdate]; + + if (contentView) { + _contentView = contentView; + + [self addSubview:self.contentView]; + } + } + return self; +} + +#pragma mark - Setup + +- (void)setUpForHUDStyle:(JGProgressHUDStyle)style vibrancyEnabled:(BOOL)vibrancyEnabled {} + +#pragma mark - Accessibility + ++ (void)runBlock:(void (^)(void))block { + if (block != nil) { + block(); + } +} + +- (void)setNeedsAccessibilityUpdate { + if (!_accessibilityUpdateScheduled) { + _accessibilityUpdateScheduled = YES; + + runOnNextRunLoop(^{ + [self updateAccessibilityIfNeeded]; + }); + } +} + +- (void)updateAccessibilityIfNeeded { + if (_accessibilityUpdateScheduled) { + [self updateAccessibility]; + _accessibilityUpdateScheduled = NO; + } +} + +- (void)updateAccessibility { + self.accessibilityLabel = [NSLocalizedString(@"Loading",) stringByAppendingFormat:@" %.f %%", self.progress]; +} + +#pragma mark - Getters & Setters + +- (void)setProgress:(float)progress { + [self setProgress:progress animated:NO]; +} + +- (void)setProgress:(float)progress animated:(__unused BOOL)animated { + if (fequal(self.progress, progress)) { + return; + } + + _progress = progress; + + [self setNeedsAccessibilityUpdate]; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDPieIndicatorView.h b/JGProgressHUD/JGProgressHUDPieIndicatorView.h new file mode 100644 index 0000000..94201cf --- /dev/null +++ b/JGProgressHUD/JGProgressHUDPieIndicatorView.h @@ -0,0 +1,35 @@ +// +// JGProgressHUDPieIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 19.07.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDIndicatorView.h" +#pragma clang diagnostic pop + +/** + A pie shaped determinate progress indicator. + */ +@interface JGProgressHUDPieIndicatorView : JGProgressHUDIndicatorView + +/** + Tint color of the Pie. + @attention Custom values need to be set after assigning the indicator view to @c JGProgressHUD's @c indicatorView property. + + @b Default: White for JGProgressHUDStyleDark, otherwise black. + */ +@property (nonatomic, strong, nonnull) UIColor *color; + +/** + The background fill color inside the pie. + @attention Custom values need to be set after assigning the indicator view to @c JGProgressHUD's @c indicatorView property. + + @b Default: Dark gray for JGProgressHUDStyleDark, otherwise light gray. + */ +@property (nonatomic, strong, nonnull) UIColor *fillColor; + +@end diff --git a/JGProgressHUD/JGProgressHUDPieIndicatorView.m b/JGProgressHUD/JGProgressHUDPieIndicatorView.m new file mode 100755 index 0000000..15a65d2 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDPieIndicatorView.m @@ -0,0 +1,171 @@ +// +// JGProgressHUDPieIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 19.07.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDPieIndicatorView.h" + +@interface JGProgressHUDPieIndicatorLayer : CALayer + +@property (nonatomic, assign) float progress; + +@property (nonatomic, weak) UIColor *color; + +@property (nonatomic, weak) UIColor *fillColor; + +@end + +@implementation JGProgressHUDPieIndicatorLayer + +@dynamic progress, color, fillColor; + ++ (BOOL)needsDisplayForKey:(NSString *)key { + return ([key isEqualToString:@"progress"] || [key isEqualToString:@"color"] || [key isEqualToString:@"fillColor"] || [super needsDisplayForKey:key]); +} + +- (id )actionForKey:(NSString *)key { + if ([key isEqualToString:@"progress"]) { + CABasicAnimation *progressAnimation = [CABasicAnimation animation]; + progressAnimation.fromValue = [self.presentationLayer valueForKey:key]; + + progressAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + + return progressAnimation; + } + + return [super actionForKey:key]; +} + +- (void)drawInContext:(CGContextRef)ctx { + UIGraphicsPushContext(ctx); + + CGRect rect = self.bounds; + + CGPoint center = CGPointMake(rect.origin.x + (CGFloat)floor(rect.size.width/2.0), rect.origin.y + (CGFloat)floor(rect.size.height/2.0)); + CGFloat lineWidth = 2.0; + CGFloat radius = (CGFloat)floor(MIN(rect.size.width, rect.size.height)/2.0)-lineWidth; + + UIBezierPath *borderPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0.0 endAngle:2.0*(CGFloat)M_PI clockwise:NO]; + + [borderPath setLineWidth:lineWidth]; + + if (self.fillColor) { + [self.fillColor setFill]; + + [borderPath fill]; + } + + [self.color set]; + + [borderPath stroke]; + + if (self.progress > 0.0) { + UIBezierPath *processPath = [UIBezierPath bezierPath]; + + [processPath setLineWidth:radius]; + + CGFloat startAngle = -((CGFloat)M_PI/2.0); + CGFloat endAngle = startAngle + 2.0 * (CGFloat)M_PI * self.progress; + + [processPath addArcWithCenter:center radius:radius/2.0 startAngle:startAngle endAngle:endAngle clockwise:YES]; + + [processPath stroke]; + } + + UIGraphicsPopContext(); +} + +@end + + +@implementation JGProgressHUDPieIndicatorView + +#pragma mark - Initializers + +- (instancetype)init { + self = [super initWithContentView:nil]; + + if (self) { + self.layer.contentsScale = [UIScreen mainScreen].scale; + [self.layer setNeedsDisplay]; + + self.color = [UIColor clearColor]; + self.fillColor = [UIColor clearColor]; + } + + return self; +} + +- (instancetype)initWithHUDStyle:(JGProgressHUDStyle)style { + return [self init]; +} + +- (instancetype)initWithContentView:(UIView *)contentView { + return [self init]; +} + +#pragma mark - Getters & Setters + +- (void)setColor:(UIColor *)tintColor { + if ([tintColor isEqual:self.color]) { + return; + } + + _color = tintColor; + + [(JGProgressHUDPieIndicatorLayer *)self.layer setColor:self.color]; +} + +- (void)setFillColor:(UIColor *)fillColor { + if ([fillColor isEqual:self.fillColor]) { + return; + } + + _fillColor = fillColor; + + [(JGProgressHUDPieIndicatorLayer *)self.layer setFillColor:self.fillColor]; +} + +- (void)setProgress:(float)progress animated:(BOOL)animated { + if (fequal(self.progress, progress)) { + return; + } + + [super setProgress:progress animated:animated]; + + [CATransaction begin]; + [CATransaction setAnimationDuration:(animated ? 0.3 : 0.0)]; + + [(JGProgressHUDPieIndicatorLayer *)self.layer setProgress:progress]; + + [CATransaction commit]; +} + +#pragma mark - Overrides + +- (void)setUpForHUDStyle:(JGProgressHUDStyle)style vibrancyEnabled:(BOOL)vibrancyEnabled { + [super setUpForHUDStyle:style vibrancyEnabled:vibrancyEnabled]; + + if (style == JGProgressHUDStyleDark) { + self.color = [UIColor colorWithWhite:1.0 alpha:1.0]; + self.fillColor = [UIColor colorWithWhite:0.2 alpha:1.0]; + } + else { + self.color = [UIColor blackColor]; + if (style == JGProgressHUDStyleLight) { + self.fillColor = [UIColor colorWithWhite:0.85 alpha:1.0]; + } + else { + self.fillColor = [UIColor colorWithWhite:0.9 alpha:1.0]; + } + } +} + ++ (Class)layerClass { + return [JGProgressHUDPieIndicatorLayer class]; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDRingIndicatorView.h b/JGProgressHUD/JGProgressHUDRingIndicatorView.h new file mode 100644 index 0000000..3306708 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDRingIndicatorView.h @@ -0,0 +1,50 @@ +// +// JGProgressHUDRingIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDIndicatorView.h" +#pragma clang diagnostic pop + +/** + A ring shaped determinate progress indicator. + */ +@interface JGProgressHUDRingIndicatorView : JGProgressHUDIndicatorView + +/** + Background color of the ring. + @attention Custom values need to be set after assigning the indicator view to @c JGProgressHUD's @c indicatorView property. + + @b Default: Black for JGProgressHUDStyleDark, light gray otherwise. + */ +@property (nonatomic, strong, nonnull) UIColor *ringBackgroundColor; + +/** + Progress color of the progress ring. + @attention Custom values need to be set after assigning the indicator view to @c JGProgressHUD's @c indicatorView property. + + @b Default: White for JGProgressHUDStyleDark, otherwise black. + */ +@property (nonatomic, strong, nonnull) UIColor *ringColor; + +/** + Sets if the progress ring should have a rounded line cap. + + @b Default: NO. + */ +@property (nonatomic, assign) BOOL roundProgressLine; + +/** + Width of the ring. + + @b Default: 3.0. + */ +@property (nonatomic, assign) CGFloat ringWidth; + +@end + diff --git a/JGProgressHUD/JGProgressHUDRingIndicatorView.m b/JGProgressHUD/JGProgressHUDRingIndicatorView.m new file mode 100755 index 0000000..d19d3e8 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDRingIndicatorView.m @@ -0,0 +1,194 @@ +// +// JGProgressHUDRingIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 20.7.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDRingIndicatorView.h" + + +@interface JGProgressHUDRingIndicatorLayer : CALayer + +@property (nonatomic, assign) float progress; + +@property (nonatomic, weak) UIColor *ringColor; +@property (nonatomic, weak) UIColor *ringBackgroundColor; + +@property (nonatomic, assign) BOOL roundProgressLine; + +@property (nonatomic, assign) CGFloat ringWidth; + +@end + +@implementation JGProgressHUDRingIndicatorLayer + +@dynamic progress, ringBackgroundColor, ringColor, ringWidth, roundProgressLine; + ++ (BOOL)needsDisplayForKey:(NSString *)key { + return ([key isEqualToString:@"progress"] || [key isEqualToString:@"ringColor"] || [key isEqualToString:@"ringBackgroundColor"] || [key isEqualToString:@"roundProgressLine"] || [key isEqualToString:@"ringWidth"] || [super needsDisplayForKey:key]); +} + +- (id )actionForKey:(NSString *)key { + if ([key isEqualToString:@"progress"]) { + CABasicAnimation *progressAnimation = [CABasicAnimation animation]; + progressAnimation.fromValue = [self.presentationLayer valueForKey:key]; + + progressAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + + return progressAnimation; + } + + return [super actionForKey:key]; +} + +- (void)drawInContext:(CGContextRef)ctx { + UIGraphicsPushContext(ctx); + + CGRect rect = self.bounds; + + CGPoint center = CGPointMake(rect.origin.x + (CGFloat)floor(rect.size.width/2.0), rect.origin.y + (CGFloat)floor(rect.size.height/2.0)); + CGFloat lineWidth = self.ringWidth; + CGFloat radius = (CGFloat)floor(MIN(rect.size.width, rect.size.height)/2.0) - lineWidth; + + //Background + [self.ringBackgroundColor setStroke]; + + UIBezierPath *borderPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0.0 endAngle:2.0*(CGFloat)M_PI clockwise:NO]; + + [borderPath setLineWidth:lineWidth]; + [borderPath stroke]; + + //Progress + [self.ringColor setStroke]; + + if (self.progress > 0.0) { + UIBezierPath *processPath = [UIBezierPath bezierPath]; + + [processPath setLineWidth:lineWidth]; + [processPath setLineCapStyle:(self.roundProgressLine ? kCGLineCapRound : kCGLineCapSquare)]; + + CGFloat startAngle = -((CGFloat)M_PI / 2.0); + CGFloat endAngle = startAngle + 2.0 * (CGFloat)M_PI * self.progress; + + [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES]; + + [processPath stroke]; + } + + UIGraphicsPopContext(); +} + +@end + + +@implementation JGProgressHUDRingIndicatorView + +#pragma mark - Initializers + +- (instancetype)init { + self = [super initWithContentView:nil];; + + if (self) { + self.layer.contentsScale = [UIScreen mainScreen].scale; + [self.layer setNeedsDisplay]; + + self.ringWidth = 3.0; + self.ringColor = [UIColor clearColor]; + self.ringBackgroundColor = [UIColor clearColor]; + } + + return self; +} + +- (instancetype)initWithHUDStyle:(JGProgressHUDStyle)style { + return [self init]; +} + +- (instancetype)initWithContentView:(UIView *)contentView { + return [self init]; +} + +#pragma mark - Getters & Setters + +- (void)setRoundProgressLine:(BOOL)roundProgressLine { + if (roundProgressLine == self.roundProgressLine) { + return; + } + + _roundProgressLine = roundProgressLine; + + [(JGProgressHUDRingIndicatorLayer *)self.layer setRoundProgressLine:self.roundProgressLine]; +} + +- (void)setRingColor:(UIColor *)tintColor { + if ([tintColor isEqual:self.ringColor]) { + return; + } + + _ringColor = tintColor; + + [(JGProgressHUDRingIndicatorLayer *)self.layer setRingColor:self.ringColor]; +} + +- (void)setRingBackgroundColor:(UIColor *)backgroundTintColor { + if ([backgroundTintColor isEqual:self.ringBackgroundColor]) { + return; + } + + _ringBackgroundColor = backgroundTintColor; + + [(JGProgressHUDRingIndicatorLayer *)self.layer setRingBackgroundColor:self.ringBackgroundColor]; +} + +- (void)setRingWidth:(CGFloat)ringWidth { + if (fequal(ringWidth, self.ringWidth)) { + return; + } + + _ringWidth = ringWidth; + + [(JGProgressHUDRingIndicatorLayer *)self.layer setRingWidth:self.ringWidth]; +} + +- (void)setProgress:(float)progress animated:(BOOL)animated { + if (fequal(self.progress, progress)) { + return; + } + + [super setProgress:progress animated:animated]; + + [CATransaction begin]; + [CATransaction setAnimationDuration:(animated ? 0.3 : 0.0)]; + + [(JGProgressHUDRingIndicatorLayer *)self.layer setProgress:self.progress]; + + [CATransaction commit]; +} + +#pragma mark - Overrides + +- (void)setUpForHUDStyle:(JGProgressHUDStyle)style vibrancyEnabled:(BOOL)vibrancyEnabled { + [super setUpForHUDStyle:style vibrancyEnabled:vibrancyEnabled]; + + if (style == JGProgressHUDStyleDark) { + self.ringColor = [UIColor colorWithWhite:1.0 alpha:1.0]; + self.ringBackgroundColor = [UIColor colorWithWhite:0.0 alpha:1.0]; + } + else { + self.ringColor = [UIColor blackColor]; + if (style == JGProgressHUDStyleLight) { + self.ringBackgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0]; + } + else { + self.ringBackgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; + } + } +} + ++ (Class)layerClass { + return [JGProgressHUDRingIndicatorLayer class]; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDShadow.h b/JGProgressHUD/JGProgressHUDShadow.h new file mode 100644 index 0000000..105b1fa --- /dev/null +++ b/JGProgressHUD/JGProgressHUDShadow.h @@ -0,0 +1,37 @@ +// +// JGProgressHUDShadow.h +// JGProgressHUD +// +// Created by Jonas Gessner on 25.09.17. +// Copyright © 2017 Jonas Gessner. All rights reserved. +// + +#import + +/** + A wrapper representing properties of a shadow. + */ +@interface JGProgressHUDShadow : NSObject + +- (instancetype __nonnull)initWithColor:(UIColor *__nonnull)color offset:(CGSize)offset radius:(CGFloat)radius opacity:(float)opacity; + +/** Convenience initializer. */ ++ (instancetype __nonnull)shadowWithColor:(UIColor *__nonnull)color offset:(CGSize)offset radius:(CGFloat)radius opacity:(float)opacity; + +/** + The color of the shadow. Colors created from patterns are currently NOT supported. + */ +@property (nonatomic, strong, readonly, nonnull) UIColor *color; + +/** The shadow offset. */ +@property (nonatomic, assign, readonly) CGSize offset; + +/** The blur radius used to create the shadow. */ +@property (nonatomic, assign, readonly) CGFloat radius; + +/** + The opacity of the shadow. Specifying a value outside the [0,1] range will give undefined results. + */ +@property (nonatomic, assign, readonly) float opacity; + +@end diff --git a/JGProgressHUD/JGProgressHUDShadow.m b/JGProgressHUD/JGProgressHUDShadow.m new file mode 100644 index 0000000..3c67bcd --- /dev/null +++ b/JGProgressHUD/JGProgressHUDShadow.m @@ -0,0 +1,30 @@ +// +// JGProgressHUDShadow.m +// JGProgressHUD +// +// Created by Jonas Gessner on 25.09.17. +// Copyright © 2017 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDShadow.h" + +@implementation JGProgressHUDShadow + ++ (instancetype)shadowWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius opacity:(float)opacity { + return [[self alloc] initWithColor:color offset:offset radius:radius opacity:opacity]; +} + +- (instancetype)initWithColor:(UIColor *)color offset:(CGSize)offset radius:(CGFloat)radius opacity:(float)opacity { + self = [super init]; + + if (self) { + _color = color; + _offset = offset; + _radius = radius; + _opacity = opacity; + } + + return self; +} + +@end diff --git a/JGProgressHUD/JGProgressHUDSuccessIndicatorView.h b/JGProgressHUD/JGProgressHUDSuccessIndicatorView.h new file mode 100644 index 0000000..c1e7fc8 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDSuccessIndicatorView.h @@ -0,0 +1,24 @@ +// +// JGProgressHUDSuccessIndicatorView.h +// JGProgressHUD +// +// Created by Jonas Gessner on 19.08.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" +#import "JGProgressHUDImageIndicatorView.h" +#pragma clang diagnostic pop + +/** + An image indicator showing a checkmark, representing a failed operation. + */ +@interface JGProgressHUDSuccessIndicatorView : JGProgressHUDImageIndicatorView + +/** + Default initializer for this class. + */ +- (instancetype __nonnull)init; + +@end diff --git a/JGProgressHUD/JGProgressHUDSuccessIndicatorView.m b/JGProgressHUD/JGProgressHUDSuccessIndicatorView.m new file mode 100644 index 0000000..1fcbae2 --- /dev/null +++ b/JGProgressHUD/JGProgressHUDSuccessIndicatorView.m @@ -0,0 +1,56 @@ +// +// JGProgressHUDSuccessIndicatorView.m +// JGProgressHUD +// +// Created by Jonas Gessner on 19.08.14. +// Copyright (c) 2014 Jonas Gessner. All rights reserved. +// + +#import "JGProgressHUDSuccessIndicatorView.h" +#import "JGProgressHUD.h" + +static UIBezierPath *successBezierPath() { + static UIBezierPath *path; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointMake(1.5, 18)]; + [path addLineToPoint:CGPointMake(11, 28)]; + [path addLineToPoint:CGPointMake(31.5, 5.5)]; + + [path setLineWidth:3]; + [path setLineJoinStyle:kCGLineJoinRound]; + [path setLineCapStyle:kCGLineCapRound]; + }); + + return path; +} + +@implementation JGProgressHUDSuccessIndicatorView + +- (instancetype)initWithContentView:(UIView *__unused)contentView { + UIBezierPath *path = successBezierPath(); + + UIGraphicsBeginImageContextWithOptions(CGSizeMake(33, 33), NO, 0.0); + [[UIColor blackColor] setStroke]; + [path stroke]; + + UIImage *img = [UIGraphicsGetImageFromCurrentImageContext() imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + + UIGraphicsEndImageContext(); + + self = [super initWithImage:img]; + + return self; +} + +- (instancetype)init { + return [self initWithContentView:nil]; +} + +- (void)updateAccessibility { + self.accessibilityLabel = NSLocalizedString(@"Success",); +} + +@end diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..723ed93 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +TARGET := iphone:clang:latest:13.0 +INSTALL_TARGET_PROCESSES = SpringBoard + + +include $(THEOS)/makefiles/common.mk + +TWEAK_NAME = BHTikTok + +BHTikTok_FILES = Tweak.x $(wildcard *.m JGProgressHUD/*.m) +BHTikTok_FRAMEWORKS = UIKit Foundation CoreGraphics Photos CoreServices SystemConfiguration SafariServices Security QuartzCore +BHTikTok_PRIVATE_FRAMEWORKS = Preferences +BHTikTok_EXTRA_FRAMEWORKS = Cephei CepheiPrefs CepheiUI +BHTikTok_CFLAGS = -fobjc-arc -Wno-unused-variable -Wno-unused-value -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-unused-function -Wno-incompatible-pointer-types + +include $(THEOS_MAKE_PATH)/tweak.mk diff --git a/README.md b/README.md index 5f00747..f252e74 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # BHTikTok An awesome tweak for TikTok! + +# Features +- No Ads +- Download Videos +- Download Musics +- Show/Hide UI button +- Copy video decription +- Copy video link +- Copy Music link +- Auto Play Next Video +- Show progress bar +- Confirm like +- Confirm comment like +- Confirm comment dislike +- Confirm follow +- Save profile image +- Copy profile information +- Extend bio +- Extend comment +- Always open in Safari +- Changing region +- Fake verify blue mark +- Fake Follower count +- Fake Following count +- Padlock + +# TODO +- [ ] Add Localization for the tweak. \ No newline at end of file diff --git a/SecurityViewController.h b/SecurityViewController.h new file mode 100644 index 0000000..d671e3d --- /dev/null +++ b/SecurityViewController.h @@ -0,0 +1,5 @@ +#import +#import + +@interface SecurityViewController : UIViewController +@end \ No newline at end of file diff --git a/SecurityViewController.m b/SecurityViewController.m new file mode 100644 index 0000000..6f2066b --- /dev/null +++ b/SecurityViewController.m @@ -0,0 +1,47 @@ +#import "SecurityViewController.h" + +@implementation SecurityViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; + UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + blurView.frame = self.view.bounds; + [self.view addSubview:blurView]; + + UIButton *authenticateButton = [[UIButton alloc] initWithFrame:CGRectMake(20, 20, 200, 60)]; + [authenticateButton setTitle:@"Authenticate" forState:UIControlStateNormal]; + authenticateButton.center = self.view.center; + [authenticateButton addTarget:self action:@selector(authenticateButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:authenticateButton]; + + [self authenticate]; +} + +- (void)authenticateButtonTapped:(id)sender { + [self authenticate]; +} + +- (void)authenticate { + LAContext *context = [[LAContext alloc] init]; + NSError *error = nil; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error]) { + NSString *reason = @"Identify yourself!"; + + [context evaluatePolicy:LAPolicyDeviceOwnerAuthentication localizedReason:reason reply:^(BOOL success, NSError *authenticationError) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (success) { + [self dismissViewControllerAnimated:YES completion:nil]; + } else { + // error + } + }); + }]; + } else { + // no biometry + } +} + +@end diff --git a/SettingsViewController.h b/SettingsViewController.h new file mode 100755 index 0000000..600e9b6 --- /dev/null +++ b/SettingsViewController.h @@ -0,0 +1,44 @@ +// +// SettingsViewController.h +// FlexCrack +// +// Created by BandarHelal on 25/11/2021. +// + +#import +#import +#import +#import +#import +#import +#import +#import + +typedef NS_ENUM(NSInteger, DynamicSpecifierOperatorType) { + EqualToOperatorType, + NotEqualToOperatorType, + GreaterThanOperatorType, + LessThanOperatorType, +}; + +@interface SettingsViewController : HBListController +- (instancetype)init; +- (PSSpecifier *)newSectionWithTitle:(NSString *)header footer:(NSString *)footer; +- (PSSpecifier *)newSwitchCellWithTitle:(NSString *)titleText detailTitle:(NSString *)detailText key:(NSString *)keyText defaultValue:(BOOL)defValue changeAction:(SEL)changeAction; +- (PSSpecifier *)newButtonCellWithTitle:(NSString *)titleText detailTitle:(NSString *)detailText dynamicRule:(NSString *)rule action:(SEL)action; +- (PSSpecifier *)newHBLinkCellWithTitle:(NSString *)titleText detailTitle:(NSString *)detailText url:(NSString *)url; +- (PSSpecifier *)newHBTwitterCellWithTitle:(NSString *)titleText twitterUsername:(NSString *)user customAvatarURL:(NSString *)avatarURL; +- (void)reloadSpecifiers; +- (void)collectDynamicSpecifiersFromArray:(NSArray *)array; +- (BOOL)shouldHideSpecifier:(PSSpecifier *)specifier; +- (DynamicSpecifierOperatorType)operatorTypeForString:(NSString *)string; + +- (id)readPreferenceValue:(PSSpecifier *)specifier; +- (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier; +@end + +@interface BHButtonTableViewCell : HBTintedTableCell +@end + +@interface BHSwitchTableCell : PSSwitchTableCell +@end diff --git a/SettingsViewController.m b/SettingsViewController.m new file mode 100755 index 0000000..40d1b5c --- /dev/null +++ b/SettingsViewController.m @@ -0,0 +1,643 @@ +// +// SettingsViewController.m +// BHTwitter +// +// Created by BandarHelal +// + +#import "SettingsViewController.h" + +@interface SettingsViewController () +@property (nonatomic, assign) BOOL hasDynamicSpecifiers; +@property (nonatomic, retain) NSMutableDictionary *dynamicSpecifiers; +@end + +@implementation SettingsViewController +- (instancetype)init { + self = [super init]; + if (self) { + [self setupAppearance]; + } + return self; +} +- (void)setupAppearance { + self.title = @"BHTikTok"; + + if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + HBAppearanceSettings *appearanceSettings = [[HBAppearanceSettings alloc] init]; + appearanceSettings.tableViewBackgroundColor = [UIColor blackColor]; + appearanceSettings.tableViewCellBackgroundColor = [UIColor colorWithRed: 25.0/255.0 green: 25.0/255.0 blue: 25.0/255.0 alpha: 1.0]; + self.hb_appearanceSettings = appearanceSettings; + } else { + HBAppearanceSettings *appearanceSettings = [[HBAppearanceSettings alloc] init]; + appearanceSettings.tableViewCellBackgroundColor = [UIColor whiteColor]; + self.hb_appearanceSettings = appearanceSettings; + } +} + +- (UITableViewStyle)tableViewStyle { + return UITableViewStyleInsetGrouped; +} +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { + [super traitCollectionDidChange:previousTraitCollection]; + if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) { + [self updateColorsForCurrentTraitCollection]; + } +} + +- (void)updateColorsForCurrentTraitCollection { + if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + HBAppearanceSettings *appearanceSettings = [[HBAppearanceSettings alloc] init]; + appearanceSettings.tableViewBackgroundColor = [UIColor blackColor]; + appearanceSettings.tableViewCellBackgroundColor = [UIColor colorWithRed: 25.0/255.0 green: 25.0/255.0 blue: 25.0/255.0 alpha: 1.0]; + self.hb_appearanceSettings = appearanceSettings; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.tableView reloadData]; + }); + } else { + HBAppearanceSettings *appearanceSettings = [[HBAppearanceSettings alloc] init]; + appearanceSettings.tableViewBackgroundColor = [UIColor systemBackgroundColor]; + appearanceSettings.tableViewCellBackgroundColor = [UIColor whiteColor]; + self.hb_appearanceSettings = appearanceSettings; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.tableView reloadData]; + }); + } +} + + +- (PSSpecifier *)newSectionWithTitle:(NSString *)header footer:(NSString *)footer { + PSSpecifier *section = [PSSpecifier preferenceSpecifierNamed:header target:self set:nil get:nil detail:nil cell:PSGroupCell edit:nil]; + if (footer != nil) { + [section setProperty:footer forKey:@"footerText"]; + } + return section; +} +- (PSSpecifier *)newLinkListCellWithTitle:(NSString *)titleText key:(NSString *)keyText defaultValue:(NSNumber *)defValue dynamicRule:(NSString *)rule validTitles:(NSMutableArray *)validTitles validValues:(NSMutableArray *)validValues { + PSSpecifier *linkListCell = [PSSpecifier preferenceSpecifierNamed:titleText target:self set:@selector(setPreferenceValue:specifier:) get:@selector(readPreferenceValue:) detail:PSListItemsController.class cell:PSLinkListCell edit:nil]; + + [linkListCell setProperty:keyText forKey:@"key"]; + [linkListCell setProperty:defValue forKey:@"default"]; + [linkListCell setValues:validValues titles:validTitles]; + + if (rule != nil) { + [linkListCell setProperty:rule forKey:@"dynamicRule"]; + } + + return linkListCell; +} +- (PSSpecifier *)newEditTextCellWithLabel:(NSString *)labeltext placeholder:(NSString *)placeholder keyboardType:(NSString *)keyboardType dynamicRule:(NSString *)rule key:(NSString *)keyText { + PSSpecifier *editTextCell = [PSSpecifier preferenceSpecifierNamed:labeltext target:self set:@selector(setPreferenceValue:specifier:) get:@selector(readPreferenceValue:) detail:nil cell:PSEditTextCell edit:nil]; + + [editTextCell setProperty:keyText forKey:@"key"]; + [editTextCell setProperty:keyText forKey:@"id"]; + [editTextCell setProperty:labeltext forKey:@"label"]; + [editTextCell setProperty:placeholder forKey:@"placeholder"]; + [editTextCell setProperty:keyboardType forKey:@"keyboardType"]; + [editTextCell setProperty:NSBundle.mainBundle.bundleIdentifier forKey:@"defaults"]; + + if (rule != nil) { + [editTextCell setProperty:rule forKey:@"dynamicRule"]; + } + + return editTextCell; +} +- (PSSpecifier *)newSwitchCellWithTitle:(NSString *)titleText detailTitle:(NSString *)detailText key:(NSString *)keyText defaultValue:(BOOL)defValue changeAction:(SEL)changeAction { + PSSpecifier *switchCell = [PSSpecifier preferenceSpecifierNamed:titleText target:self set:@selector(setPreferenceValue:specifier:) get:@selector(readPreferenceValue:) detail:nil cell:PSSwitchCell edit:nil]; + + [switchCell setProperty:keyText forKey:@"key"]; + [switchCell setProperty:keyText forKey:@"id"]; + [switchCell setProperty:@YES forKey:@"big"]; + [switchCell setProperty:BHSwitchTableCell.class forKey:@"cellClass"]; + [switchCell setProperty:NSBundle.mainBundle.bundleIdentifier forKey:@"defaults"]; + [switchCell setProperty:@(defValue) forKey:@"default"]; + [switchCell setProperty:NSStringFromSelector(changeAction) forKey:@"switchAction"]; + if (detailText != nil) { + [switchCell setProperty:detailText forKey:@"subtitle"]; + } + return switchCell; +} +- (PSSpecifier *)newButtonCellWithTitle:(NSString *)titleText detailTitle:(NSString *)detailText dynamicRule:(NSString *)rule action:(SEL)action { + PSSpecifier *buttonCell = [PSSpecifier preferenceSpecifierNamed:titleText target:self set:@selector(setPreferenceValue:specifier:) get:@selector(readPreferenceValue:) detail:nil cell:PSButtonCell edit:nil]; + + [buttonCell setButtonAction:action]; + [buttonCell setProperty:@YES forKey:@"big"]; + [buttonCell setProperty:BHButtonTableViewCell.class forKey:@"cellClass"]; + if (detailText != nil ){ + [buttonCell setProperty:detailText forKey:@"subtitle"]; + } + if (rule != nil) { + [buttonCell setProperty:@44 forKey:@"height"]; + [buttonCell setProperty:rule forKey:@"dynamicRule"]; + } + return buttonCell; +} +- (PSSpecifier *)newHBLinkCellWithTitle:(NSString *)titleText detailTitle:(NSString *)detailText url:(NSString *)url { + PSSpecifier *HBLinkCell = [PSSpecifier preferenceSpecifierNamed:titleText target:self set:@selector(setPreferenceValue:specifier:) get:@selector(readPreferenceValue:) detail:nil cell:PSButtonCell edit:nil]; + + [HBLinkCell setButtonAction:@selector(hb_openURL:)]; + [HBLinkCell setProperty:HBLinkTableCell.class forKey:@"cellClass"]; + [HBLinkCell setProperty:url forKey:@"url"]; + if (detailText != nil) { + [HBLinkCell setProperty:detailText forKey:@"subtitle"]; + } + return HBLinkCell; +} +- (PSSpecifier *)newHBTwitterCellWithTitle:(NSString *)titleText twitterUsername:(NSString *)user customAvatarURL:(NSString *)avatarURL { + PSSpecifier *TwitterCell = [PSSpecifier preferenceSpecifierNamed:titleText target:self set:@selector(setPreferenceValue:specifier:) get:@selector(readPreferenceValue:) detail:nil cell:1 edit:nil]; + + [TwitterCell setButtonAction:@selector(hb_openURL:)]; + [TwitterCell setProperty:HBTwitterCell.class forKey:@"cellClass"]; + [TwitterCell setProperty:user forKey:@"user"]; + [TwitterCell setProperty:@YES forKey:@"big"]; + [TwitterCell setProperty:@56 forKey:@"height"]; + [TwitterCell setProperty:avatarURL forKey:@"iconURL"]; + return TwitterCell; +} +- (NSArray *)specifiers { + if (!_specifiers) { + NSArray *regionTitles = @[@"Saudi Arabia 🇸🇦", @"Taiwan 🇹🇼", @"Hong Kong 🇭🇰", @"Macao 🇲🇴", @"Japan 🇯🇵", @"South Korea 🇰🇷", @"United Kingdom 🇬🇧", @"United States 🇺🇸", @"Australia 🇦🇺", @"Canada 🇨🇦", @"Argentina 🇦🇷", @"Philippines 🇵🇭", @"Laos 🇱🇦", @"Malaysia 🇲🇾", @"Thailand 🇹🇭", @"Singapore 🇸🇬", @"Indonesia 🇮🇩", @"Vietnam 🇻🇳", @"Anguilla 🇦🇮", @"Panama 🇵🇦", @"Germany 🇩🇪", @"Russia 🇷🇺", @"France 🇫🇷", @"Finland 🇫🇮", @"Italy 🇮🇹", @"Pakistan 🇵🇰", @"Denmark 🇩🇰", @"Norway 🇳🇴", @"Sudan 🇸🇩", @"Romania 🇷🇴", @"United Arab Emirates 🇦🇪", @"Egypt 🇪🇬", @"Lebanon 🇱🇧", @"Mexico 🇲🇽", @"Brazil 🇧🇷", @"Turkey 🇹🇷", @"Kuwait 🇰🇼", @"Algeria 🇩🇿"]; + NSArray *regionCodes = @[ + @{ + @"area": @"Saudi Arabia 🇸🇦", + @"code": @"SA", + @"mcc": @"420", + @"mnc": @"01" + }, + @{ + @"area": @"Taiwan 🇹🇼", + @"code": @"TW", + @"mcc": @"466", + @"mnc": @"01" + }, + @{ + @"area": @"Hong Kong 🇭🇰", + @"code": @"HK", + @"mcc": @"454", + @"mnc": @"00" + }, + @{ + @"area": @"Macao 🇲🇴", + @"code": @"MO", + @"mcc": @"455", + @"mnc": @"00" + }, + @{ + @"area": @"Japan 🇯🇵", + @"code": @"JP", + @"mcc": @"440", + @"mnc": @"00" + }, + @{ + @"area": @"South Korea 🇰🇷", + @"code": @"KR", + @"mcc": @"450", + @"mnc": @"05" + }, + @{ + @"area": @"United Kingdom 🇬🇧", + @"code": @"GB", + @"mcc": @"234", + @"mnc": @"30" + }, + @{ + @"area": @"United States 🇺🇸", + @"code": @"US", + @"mcc": @"310", + @"mnc": @"00" + }, + @{ + @"area": @"Australia 🇦🇺", + @"code": @"AU", + @"mcc": @"505", + @"mnc": @"02" + }, + @{ + @"area": @"Canada 🇨🇦", + @"code": @"CA", + @"mcc": @"302", + @"mnc": @"720" + }, + @{ + @"area": @"Argentina 🇦🇷", + @"code": @"AR", + @"mcc": @"722", + @"mnc": @"07" + }, + @{ + @"area": @"Philippines 🇵🇭", + @"code": @"PH", + @"mcc": @"515", + @"mnc": @"02" + }, + @{ + @"area": @"Laos 🇱🇦", + @"code": @"LA", + @"mcc": @"457", + @"mnc": @"01" + }, + @{ + @"area": @"Malaysia 🇲🇾", + @"code": @"MY", + @"mcc": @"502", + @"mnc": @"13" + }, + @{ + @"area": @"Thailand 🇹🇭", + @"code": @"TH", + @"mcc": @"520", + @"mnc": @"18" + }, + @{ + @"area": @"Singapore 🇸🇬", + @"code": @"SG", + @"mcc": @"525", + @"mnc": @"01" + }, + @{ + @"area": @"Indonesia 🇮🇩", + @"code": @"ID", + @"mcc": @"510", + @"mnc": @"01" + }, + @{ + @"area": @"Vietnam 🇻🇳", + @"code": @"VN", + @"mcc": @"452", + @"mnc": @"01" + }, + @{ + @"area": @"Anguilla 🇦🇮", + @"code": @"AI", + @"mcc": @"365", + @"mnc": @"840" + }, + @{ + @"area": @"Panama 🇵🇦", + @"code": @"PA", + @"mcc": @"714", + @"mnc": @"04" + }, + @{ + @"area": @"Germany 🇩🇪", + @"code": @"DE", + @"mcc": @"262", + @"mnc": @"01" + }, + @{ + @"area": @"Russia 🇷🇺", + @"code": @"RU", + @"mcc": @"250", + @"mnc": @"01" + }, + @{ + @"area": @"France 🇫🇷", + @"code": @"FR", + @"mcc": @"208", + @"mnc": @"10" + }, + @{ + @"area": @"Finland 🇫🇮", + @"code": @"FI", + @"mcc": @"244", + @"mnc": @"91" + }, + @{ + @"area": @"Italy 🇮🇹", + @"code": @"IT", + @"mcc": @"222", + @"mnc": @"10" + }, + @{ + @"area": @"Pakistan 🇵🇰", + @"code": @"PK", + @"mcc": @"410", + @"mnc": @"01" + }, + @{ + @"area": @"Denmark 🇩🇰", + @"code": @"DK", + @"mcc": @"238", + @"mnc": @"01" + }, + @{ + @"area": @"Norway 🇳🇴", + @"code": @"NO", + @"mcc": @"242", + @"mnc": @"01" + }, + @{ + @"area": @"Sudan 🇸🇩", + @"code": @"SD", + @"mcc": @"634", + @"mnc": @"01" + }, + @{ + @"area": @"Romania 🇷🇴", + @"code": @"RO", + @"mcc": @"226", + @"mnc": @"01" + }, + @{ + @"area": @"United Arab Emirates 🇦🇪", + @"code": @"AE", + @"mcc": @"424", + @"mnc": @"02" + }, + @{ + @"area": @"Egypt 🇪🇬", + @"code": @"EG", + @"mcc": @"602", + @"mnc": @"01" + }, + @{ + @"area": @"Lebanon 🇱🇧", + @"code": @"LB", + @"mcc": @"415", + @"mnc": @"01" + }, + @{ + @"area": @"Mexico 🇲🇽", + @"code": @"MX", + @"mcc": @"334", + @"mnc": @"030" + }, + @{ + @"area": @"Brazil 🇧🇷", + @"code": @"BR", + @"mcc": @"724", + @"mnc": @"06" + }, + @{ + @"area": @"Turkey 🇹🇷", + @"code": @"TR", + @"mcc": @"286", + @"mnc": @"01" + }, + @{ + @"area": @"Kuwait 🇰🇼", + @"code": @"KW", + @"mcc": @"419", + @"mnc": @"02" + }, + @{ + @"area": @"Algeria 🇩🇿", + @"code": @"DZ", + @"mcc": @"603", + @"mnc": @"01" + } + ]; + + PSSpecifier *feedSection = [self newSectionWithTitle:@"Feed" footer:nil]; + PSSpecifier *profileSection = [self newSectionWithTitle:@"Profile" footer:nil]; + PSSpecifier *countrySection = [self newSectionWithTitle:@"Country" footer:nil]; + PSSpecifier *confirmationSection = [self newSectionWithTitle:@"Confirmation" footer:nil]; + PSSpecifier *fakeSection = [self newSectionWithTitle:@"Fake" footer:@"if you want the default value leave it blank."]; + PSSpecifier *securitySection = [self newSectionWithTitle:@"Security" footer:nil]; + PSSpecifier *developer = [self newSectionWithTitle:@"Developer" footer:nil]; + + PSSpecifier *hideAds = [self newSwitchCellWithTitle:@"No Ads" detailTitle:@"Remove all Ads from TikTok app" key:@"hide_ads" defaultValue:true changeAction:nil]; + PSSpecifier *downloadVid = [self newSwitchCellWithTitle:@"Download Videos" detailTitle:@"Download Videos by log press in any video you want." key:@"dw_videos" defaultValue:true changeAction:nil]; + PSSpecifier *downloadMis = [self newSwitchCellWithTitle:@"Download Musics" detailTitle:@"Download Musics by log press in any video you want." key:@"dw_musics" defaultValue:true changeAction:nil]; + PSSpecifier *hideElementButton = [self newSwitchCellWithTitle:@"Show/Hide UI button" detailTitle:@"A button on the main page to remove UI elements" key:@"remove_elements_button" defaultValue:true changeAction:nil]; + PSSpecifier *copyVideoDecription = [self newSwitchCellWithTitle:@"Copy video decription" detailTitle:@"Show new option in long press to copy the video decription" key:@"copy_decription" defaultValue:true changeAction:nil]; + PSSpecifier *copyVideoLink = [self newSwitchCellWithTitle:@"Copy video link" detailTitle:@"Show new option in long press to copy the video link" key:@"copy_video_link" defaultValue:true changeAction:nil]; + PSSpecifier *copyMusicLink = [self newSwitchCellWithTitle:@"Copy Music link" detailTitle:@"Show new option in long press to copy the video link" key:@"copy_music_link" defaultValue:true changeAction:nil]; + PSSpecifier *autoPlay = [self newSwitchCellWithTitle:@"Auto Play Next Video" detailTitle:@"Play next video automatcilly when the post is finished" key:@"auto_play" defaultValue:false changeAction:nil]; + PSSpecifier *progressBar = [self newSwitchCellWithTitle:@"Show progress bar" detailTitle:nil key:@"show_porgress_bar" defaultValue:true changeAction:nil]; + PSSpecifier *likeConfirmation = [self newSwitchCellWithTitle:@"Confirm like" detailTitle:@"Show alert when you click the like button to confirm the like" key:@"like_confirm" defaultValue:false changeAction:nil]; + PSSpecifier *likeCommentConfirmation = [self newSwitchCellWithTitle:@"Confirm comment like" detailTitle:@"Show alert when you click the like button in comment to confirm the like" key:@"like_comment_confirm" defaultValue:false changeAction:nil]; + PSSpecifier *dislikeCommentConfirmation = [self newSwitchCellWithTitle:@"Confirm comment dislike" detailTitle:@"Show alert when you click the dislike button in comment to confirm the like" key:@"dislike_comment_confirm" defaultValue:false changeAction:nil]; + PSSpecifier *followConfirmation = [self newSwitchCellWithTitle:@"Confirm follow" detailTitle:@"Show alert when you click the follow button to confirm the like" key:@"follow_confirm" defaultValue:false changeAction:nil]; + + PSSpecifier *profileSave = [self newSwitchCellWithTitle:@"Save profile image" detailTitle:@"Save profile image by long press." key:@"save_profile" defaultValue:true changeAction:nil]; + PSSpecifier *profileCopy = [self newSwitchCellWithTitle:@"Copy profile information" detailTitle:@"Copy profile information by long press." key:@"copy_profile_information" defaultValue:true changeAction:nil]; + PSSpecifier *extendedBio = [self newSwitchCellWithTitle:@"Extend bio" detailTitle:@"Extend the bio letters by 222 characters" key:@"extended_bio" defaultValue:true changeAction:nil]; + PSSpecifier *extendedComment = [self newSwitchCellWithTitle:@"Extend comment" detailTitle:@"Extend the comment letters by 240 characters" key:@"extended_comment" defaultValue:true changeAction:nil]; + PSSpecifier *alwaysOpenSafari = [self newSwitchCellWithTitle:@"Always open in Safari" detailTitle:@"Force twitter to open URLs in Safari or your default browser." key:@"openInBrowser" defaultValue:false changeAction:nil]; + + PSSpecifier *regionSwitch = [self newSwitchCellWithTitle:@"Enable changing region" detailTitle:nil key:@"en_region" defaultValue:false changeAction:nil]; + PSSpecifier *regions = [self newLinkListCellWithTitle:@"Regions" key:@"region" defaultValue:@0 dynamicRule:@"en_region, ==, 0" validTitles:regionTitles validValues:regionCodes]; + + PSSpecifier *fakeVerified = [self newSwitchCellWithTitle:@"Fake verify blue mark" detailTitle:nil key:@"fake_verify" defaultValue:false changeAction:nil]; + PSSpecifier *fakeChangesEnabled = [self newSwitchCellWithTitle:@"Enable fake options" detailTitle:nil key:@"en_fake" defaultValue:false changeAction:nil]; + PSSpecifier *followerCount = [self newEditTextCellWithLabel:@"Follower count" placeholder:nil keyboardType:@"decimalPad" dynamicRule:@"en_fake, ==, 0" key:@"follower_count"]; + PSSpecifier *followingCount = [self newEditTextCellWithLabel:@"Following count" placeholder:nil keyboardType:@"decimalPad" dynamicRule:@"en_fake, ==, 0" key:@"following_count"]; + + PSSpecifier *appLock = [self newSwitchCellWithTitle:@"Padlock" detailTitle:@"Lock TikTok with passcode" key:@"padlock" defaultValue:false changeAction:nil]; + + // dvelopers section + PSSpecifier *bandarHL = [self newHBTwitterCellWithTitle:@"BandarHelal" twitterUsername:@"BandarHL" customAvatarURL:@"https://unavatar.io/twitter/BandarHL"]; + PSSpecifier *tipJar = [self newHBLinkCellWithTitle:@"Tip Jar" detailTitle:@"Donate Via Paypal" url:@"https://www.paypal.me/BandarHL"]; + + _specifiers = [NSMutableArray arrayWithArray:@[ + + feedSection, // 1 + hideAds, + downloadVid, + downloadMis, + hideElementButton, + copyVideoDecription, + copyVideoLink, + copyMusicLink, + autoPlay, + progressBar, + + profileSection, // 2 + profileSave, + profileCopy, + extendedBio, + extendedComment, + + confirmationSection, // 3 + likeConfirmation, + likeCommentConfirmation, + dislikeCommentConfirmation, + followConfirmation, + + countrySection, // 4 + regionSwitch, + regions, + + fakeSection, // 5 + fakeVerified, + fakeChangesEnabled, + followerCount, + followingCount, + + securitySection, // 6 + alwaysOpenSafari, + appLock, + + developer, // 7 + bandarHL, + tipJar, + ]]; + + [self collectDynamicSpecifiersFromArray:_specifiers]; + } + + return _specifiers; +} +- (void)reloadSpecifiers { + [super reloadSpecifiers]; + + [self collectDynamicSpecifiersFromArray:self.specifiers]; +} +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (self.hasDynamicSpecifiers) { + PSSpecifier *dynamicSpecifier = [self specifierAtIndexPath:indexPath]; + BOOL __block shouldHide = false; + + [self.dynamicSpecifiers enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + NSMutableArray *specifiers = obj; + if ([specifiers containsObject:dynamicSpecifier]) { + shouldHide = [self shouldHideSpecifier:dynamicSpecifier]; + + UITableViewCell *specifierCell = [dynamicSpecifier propertyForKey:PSTableCellKey]; + specifierCell.clipsToBounds = shouldHide; + } + }]; + if (shouldHide) { + return 0; + } + } + + return UITableViewAutomaticDimension; +} + +- (void)collectDynamicSpecifiersFromArray:(NSArray *)array { + if (!self.dynamicSpecifiers) { + self.dynamicSpecifiers = [NSMutableDictionary new]; + + } else { + [self.dynamicSpecifiers removeAllObjects]; + } + + for (PSSpecifier *specifier in array) { + NSString *dynamicSpecifierRule = [specifier propertyForKey:@"dynamicRule"]; + + if (dynamicSpecifierRule.length > 0) { + NSArray *ruleComponents = [dynamicSpecifierRule componentsSeparatedByString:@", "]; + + if (ruleComponents.count == 3) { + NSString *opposingSpecifierID = [ruleComponents objectAtIndex:0]; + if ([self.dynamicSpecifiers objectForKey:opposingSpecifierID]) { + NSMutableArray *specifiers = [[self.dynamicSpecifiers objectForKey:opposingSpecifierID] mutableCopy]; + [specifiers addObject:specifier]; + + + [self.dynamicSpecifiers removeObjectForKey:opposingSpecifierID]; + [self.dynamicSpecifiers setObject:specifiers forKey:opposingSpecifierID]; + } else { + [self.dynamicSpecifiers setObject:[NSMutableArray arrayWithArray:@[specifier]] forKey:opposingSpecifierID]; + } + + } else { + [NSException raise:NSInternalInconsistencyException format:@"dynamicRule key requires three components (Specifier ID, Comparator, Value To Compare To). You have %ld of 3 (%@) for specifier '%@'.", ruleComponents.count, dynamicSpecifierRule, [specifier propertyForKey:PSTitleKey]]; + } + } + } + + self.hasDynamicSpecifiers = (self.dynamicSpecifiers.count > 0); +} +- (DynamicSpecifierOperatorType)operatorTypeForString:(NSString *)string { + NSDictionary *operatorValues = @{ @"==" : @(EqualToOperatorType), @"!=" : @(NotEqualToOperatorType), @">" : @(GreaterThanOperatorType), @"<" : @(LessThanOperatorType) }; + return [operatorValues[string] intValue]; +} +- (BOOL)shouldHideSpecifier:(PSSpecifier *)specifier { + if (specifier) { + NSString *dynamicSpecifierRule = [specifier propertyForKey:@"dynamicRule"]; + NSArray *ruleComponents = [dynamicSpecifierRule componentsSeparatedByString:@", "]; + + PSSpecifier *opposingSpecifier = [self specifierForID:[ruleComponents objectAtIndex:0]]; + id opposingValue = [self readPreferenceValue:opposingSpecifier]; + id requiredValue = [ruleComponents objectAtIndex:2]; + + if ([opposingValue isKindOfClass:NSNumber.class]) { + DynamicSpecifierOperatorType operatorType = [self operatorTypeForString:[ruleComponents objectAtIndex:1]]; + + switch (operatorType) { + case EqualToOperatorType: + return ([opposingValue intValue] == [requiredValue intValue]); + break; + + case NotEqualToOperatorType: + return ([opposingValue intValue] != [requiredValue intValue]); + break; + + case GreaterThanOperatorType: + return ([opposingValue intValue] > [requiredValue intValue]); + break; + + case LessThanOperatorType: + return ([opposingValue intValue] < [requiredValue intValue]); + break; + } + } + + if ([opposingValue isKindOfClass:NSString.class]) { + return [opposingValue isEqualToString:requiredValue]; + } + + if ([opposingValue isKindOfClass:NSArray.class]) { + return [opposingValue containsObject:requiredValue]; + } + } + + return NO; +} + +- (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { + NSUserDefaults *Prefs = [NSUserDefaults standardUserDefaults]; + [Prefs setValue:value forKey:[specifier identifier]]; + + if (self.hasDynamicSpecifiers) { + NSString *specifierID = [specifier propertyForKey:PSIDKey]; + PSSpecifier *dynamicSpecifier = [self.dynamicSpecifiers objectForKey:specifierID]; + + if (dynamicSpecifier) { + [self.table beginUpdates]; + [self.table endUpdates]; + } + } +} +- (id)readPreferenceValue:(PSSpecifier *)specifier { + NSUserDefaults *Prefs = [NSUserDefaults standardUserDefaults]; + return [Prefs valueForKey:[specifier identifier]]?:[specifier properties][@"default"]; +} +@end + +@implementation BHButtonTableViewCell +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { + self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier specifier:specifier]; + if (self) { + NSString *subTitle = [specifier.properties[@"subtitle"] copy]; + BOOL isBig = specifier.properties[@"big"] ? ((NSNumber *)specifier.properties[@"big"]).boolValue : NO; + self.detailTextLabel.text = subTitle; + self.detailTextLabel.numberOfLines = isBig ? 0 : 1; + self.detailTextLabel.textColor = [UIColor secondaryLabelColor]; + } + return self; +} + +@end + +@implementation BHSwitchTableCell +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { + if ((self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier specifier:specifier])) { + NSString *subTitle = [specifier.properties[@"subtitle"] copy]; + BOOL isBig = specifier.properties[@"big"] ? ((NSNumber *)specifier.properties[@"big"]).boolValue : NO; + self.detailTextLabel.text = subTitle; + self.detailTextLabel.numberOfLines = isBig ? 0 : 1; + self.detailTextLabel.textColor = [UIColor secondaryLabelColor]; + + if (specifier.properties[@"switchAction"]) { + UISwitch *targetSwitch = ((UISwitch *)[self control]); + NSString *strAction = [specifier.properties[@"switchAction"] copy]; + [targetSwitch addTarget:[self cellTarget] action:NSSelectorFromString(strAction) forControlEvents:UIControlEventValueChanged]; + } + } + return self; +} +@end diff --git a/TikTokHeaders.h b/TikTokHeaders.h new file mode 100644 index 0000000..dff518a --- /dev/null +++ b/TikTokHeaders.h @@ -0,0 +1,289 @@ +#import +#import +#import +#import "BHIManager.h" +#import "SettingsViewController.h" +#import "SecurityViewController.h" +#import "BHDownload.h" +#import "BHMultipleDownload.h" +#import "JGProgressHUD/JGProgressHUD.h" + +@interface AppDelegate : NSObject +@end + +@interface SparkViewController: UIViewController +@property(nonatomic, strong, readwrite) NSURL *originURL; +- (void)didTapCloseButton; +@end + +@interface UIView (RCTViewUnmounting) +@property(retain, nonatomic) UIViewController *yy_viewController; +@end + +@interface TikTokFeedTabControl: UIView +@end + +@interface AWEFeedVideoButton: UIButton +@property(copy, nonatomic, readwrite) NSString *imageNameString; +@end + +@interface AWEURLModel : NSObject +@property(retain, nonatomic) NSArray* originURLList; +- (NSURL *)recommendUrl; +- (NSURL *)bestURLtoDownload; +- (NSString *)bestURLtoDownloadFormat; +@end + +@interface AWEVideoModel : NSObject +@property(readonly, nonatomic) AWEURLModel *playURL; +@property(readonly, nonatomic) AWEURLModel *downloadURL; +@property(readonly, nonatomic) NSNumber *duration; +@end + +@interface AWEMusicModel : NSObject +@property(readonly, nonatomic) AWEURLModel *playURL; +@end + +@interface AWEPhotoAlbumPhoto: NSObject +@property(readonly, nonatomic) AWEURLModel *originPhotoURL; +@end + +@interface AWEPhotoAlbumModel: NSObject +@property(readonly, nonatomic) NSArray *photos; +@end + +@interface AWEAwemeModel : NSObject +@property(nonatomic) BOOL isAds; +@property(retain, nonatomic) AWEVideoModel *video; +@property(retain, nonatomic) id music; +@property(retain, nonatomic) AWEPhotoAlbumModel *photoAlbum; +@property(nonatomic) NSString *music_songName; +@property(nonatomic) NSString *music_artistName; +@property(nonatomic, strong, readwrite) AWEAwemeModel *currentPlayingStory; +@end + +@interface AWEUserModel: NSObject +@property(nonatomic, copy, readwrite) NSString *bioUrl; +@property(nonatomic, copy, readwrite) NSString *nickname; +@property(nonatomic, copy, readwrite) NSString *signature; +@property(nonatomic, copy, readwrite) NSString *socialName; +@end + +@interface AWESettingItemModel: NSObject +@property(nonatomic, copy, readwrite) NSString *identifier; +@property(nonatomic, copy, readwrite) NSString *title; +@property(nonatomic, copy, readwrite) NSString *detail; +@property(nonatomic, strong, readwrite) UIImage *iconImage; +@property(nonatomic, assign, readwrite) NSUInteger type; +- (instancetype)initWithIdentifier:(NSString *)identifier; +@end + +@interface TTKSettingsBaseCellPlugin: NSObject +@property(nonatomic, weak, readwrite) id context; +@property(nonatomic, strong, readwrite) AWESettingItemModel *itemModel; +- (instancetype)initWithPluginContext:(id)context; +@end + +@interface AWEBaseListSectionViewModel: NSObject +@property(nonatomic, copy, readwrite) NSArray *modelsArray; +- (void)insertModel:(id)model atIndex:(NSInteger)index animated:(bool)animated; +@end + +@interface AWESettingsNormalSectionViewModel: AWEBaseListSectionViewModel +@property(nonatomic, weak, readwrite) id context; +@property(nonatomic, copy, readwrite) NSString *sectionHeaderTitle; +@property(nonatomic, copy, readwrite) NSString *sectionIdentifier; +@end + +@interface AWEBaseListViewModel: NSObject +- (NSArray *)sectionViewModelsArray; +@end + +@interface AWEPluginBaseViewModel: AWEBaseListViewModel +@end + +@interface AWESettingsBaseViewModel: AWEPluginBaseViewModel +@end + +@interface TTKSettingsViewModel: AWESettingsBaseViewModel +@end + +@interface TIKTOKProfileHeaderViewController: UIViewController +@property(nonatomic, strong) AWEUserModel *user; +@end + +@interface TIKTOKProfileHeaderView: UIView +- (void)addHandleLongPress; +- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; +@end + +@interface AWEProfileImagePreviewView: UIView +@property(strong, nonatomic, readwrite) UIImageView *avatar; +- (void)addHandleLongPress; +- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; +@end + +@interface AWEAwemeBaseViewController: UIViewController +@property(nonatomic, strong, readwrite) AWEAwemeModel *model; +@property (nonatomic, copy, readwrite) NSString *referString; +@property (nonatomic) id interactionController; +@end + + +@interface TTKFeedInteractionContainerElement: NSObject +- (void)hideAllElements:(BOOL)hide exceptArray:(id)array; +@end + +@interface TTKFeedInteractionMainContainerElement: TTKFeedInteractionContainerElement +@end + +@interface TTKFeedInteractionLegacyMainContainerElement: TTKFeedInteractionMainContainerElement +@end + +@interface AWEPlayPhotoAlbumViewController: UIViewController +@property(nonatomic, strong, readwrite) AWEAwemeModel *model; +- (NSIndexPath *)currentIndexPath; +@end + +@interface TTKPhotoAlbumFeedCellController: AWEAwemeBaseViewController +{ + AWEPlayPhotoAlbumViewController *_photoAlbumController; +} +@end + +@interface TTKPhotoAlbumDetailCellController: AWEAwemeBaseViewController +{ + AWEPlayPhotoAlbumViewController *_photoAlbumController; +} +@end + +@interface AWEFeedCellViewController: AWEAwemeBaseViewController +@end + +@interface AWEAwemeDetailCellViewController: AWEAwemeBaseViewController +@end + +@interface TTKStoryContainerViewController: UIViewController +@property(nonatomic, strong, readwrite) AWEAwemeModel *model; +@property (nonatomic) id interactionController; +@end +@interface TTKStoryDetailContainerViewController: TTKStoryContainerViewController +@end + + +@interface AWEFeedViewTemplateCell: UITableViewCell +@property(nonatomic, strong, readwrite) UIViewController *viewController; +@property(nonatomic, strong, readwrite) UIViewController *parentVC; +@property(nonatomic, assign) BOOL elementsHidden; +@property (nonatomic, strong) JGProgressHUD *hud; +@property (nonatomic, retain) NSString *fileextension; +- (void)addHandleLongPress; +- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; +- (void)addHideElementButton; +- (void)hideElementButtonHandler:(UIButton *)sender; +@end +@interface AWEFeedViewTemplateCell () +@end + +@interface AWEFeedViewCell: AWEFeedViewTemplateCell +@end + +@interface AWEAwemeDetailTableViewCell: UITableViewCell +@property(nonatomic, strong, readwrite) UIViewController *viewController; +@property(nonatomic, strong, readwrite) UIViewController *parentVC; +@property(nonatomic, assign) BOOL elementsHidden; +@property (nonatomic, strong) JGProgressHUD *hud; +@property (nonatomic, retain) NSString *fileextension; +- (void)addHandleLongPress; +- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; +- (void)addHideElementButton; +- (void)hideElementButtonHandler:(UIButton *)sender; +@end +@interface AWEAwemeDetailTableViewCell () +@end + +@interface TTKStoryDetailTableViewCell: UITableViewCell +@property(nonatomic, strong, readwrite) UIViewController *viewController; +@property(nonatomic, strong, readwrite) UIViewController *parentVC; +@property(nonatomic, assign) BOOL elementsHidden; +@property (nonatomic, strong) JGProgressHUD *hud; +@property (nonatomic, retain) NSString *fileextension; +- (void)addHandleLongPress; +- (void)handleLongPress:(UILongPressGestureRecognizer *)sender; +- (void)addHideElementButton; +- (void)hideElementButtonHandler:(UIButton *)sender; +@end +@interface TTKStoryDetailTableViewCell () +@end + +@interface TTKFeedPassthroughStackView: UIStackView +@end + +@interface TUXActionSheetAction: NSObject +@property(nonatomic) NSString *title; +@property(nonatomic) NSString *subtitle; +@property(nonatomic) NSString *imageLabel; +@property(nonatomic) UIImage *image; +- (instancetype)initWithStyle:(NSUInteger)style title:(NSString *)title subtitle:(NSString *)subtitle image:(UIImage *)image imageLabel:(NSString *)imageLabel handler:(void (^ __nullable)(TUXActionSheetAction *action))handler; +@end + +@interface TUXActionSheetController: UIViewController +@property(nonatomic, assign, readwrite) BOOL dismissOnDraggingDown; +@property(nonatomic, strong, readwrite) UITableView *tableView; +- (instancetype)initWithTitle:(NSString *)title; +- (void)addAction:(TUXActionSheetAction *)action; +@end + +@interface AWEUIAlertView: UIView ++ (void)showAlertWithTitle:(NSString *)title description:(NSString *)description image:(UIImage *)image actionButtonTitle:(NSString *)actionButtonTitle cancelButtonTitle:(NSString *)cancelButtonTitle actionBlock:(void (^)(void))actionBlock cancelBlock:(void (^)(void))cancelBlock; +@end + +@interface AWEToast: NSObject ++ (void)showSuccess:(NSString *)title; +@end + +@interface AWEPlayVideoPlayerController : NSObject +@property(nonatomic) AWEAwemeBaseViewController *container; +- (void)setPlayerSeekTime:(double)arg1 completion:(id)arg2; +@end + +@interface TTKSearchEntranceButton: UIButton +@end + +@interface AWEFeedContainerViewController: UIViewController +@property(nonatomic, strong, readwrite) TTKSearchEntranceButton *searchEntranceView; +@end + +@interface AWENewFeedTableViewController : UIViewController +@property(nonatomic, weak, readwrite) UIViewController *tabContainerController; +- (void)scrollToNextVideo; +@end + +static BOOL is_iPad() { + if ([(NSString *)[UIDevice currentDevice].model hasPrefix:@"iPad"]) { + return YES; + } + return NO; +} + +static UIViewController * _Nullable _topMostController(UIViewController * _Nonnull cont) { + UIViewController *topController = cont; + while (topController.presentedViewController) { + topController = topController.presentedViewController; + } + if ([topController isKindOfClass:[UINavigationController class]]) { + UIViewController *visible = ((UINavigationController *)topController).visibleViewController; + if (visible) { + topController = visible; + } + } + return (topController != cont ? topController : nil); +} +static UIViewController * _Nonnull topMostController() { + UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController; + UIViewController *next = nil; + while ((next = _topMostController(topController)) != nil) { + topController = next; + } + return topController; +} \ No newline at end of file diff --git a/Tweak.x b/Tweak.x new file mode 100644 index 0000000..47db9d7 --- /dev/null +++ b/Tweak.x @@ -0,0 +1,1249 @@ +#import "TikTokHeaders.h" + +NSArray *jailbreakPaths; + +static void showConfirmation(void (^okHandler)(void)) { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"Are you sure?" image:nil actionButtonTitle:@"Yes" cancelButtonTitle:@"No" actionBlock:^{ + okHandler(); + } cancelBlock:nil]; +} + +%hook AppDelegate +- (_Bool)application:(UIApplication *)application didFinishLaunchingWithOptions:(id)arg2 { + %orig; + if (![[NSUserDefaults standardUserDefaults] objectForKey:@"BHTikTokFirstRun"]) { + [[NSUserDefaults standardUserDefaults] setValue:@"BHTikTokFirstRun" forKey:@"BHTikTokFirstRun"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"hide_ads"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"dw_videos"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"dw_musics"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"remove_elements_button"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"copy_decription"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"copy_video_link"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"copy_music_link"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"show_porgress_bar"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"save_profile"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"copy_profile_information"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"extended_bio"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"extendedComment"]; + } + [BHIManager cleanCache]; + return true; +} + +static BOOL isAuthenticationShowed = FALSE; +- (void)applicationDidBecomeActive:(id)arg1 { // app lock + %orig; + + if ([BHIManager appLock] && !isAuthenticationShowed) { + UIViewController *rootController = [[self window] rootViewController]; + SecurityViewController *securityViewController = [SecurityViewController new]; + securityViewController.modalPresentationStyle = UIModalPresentationOverFullScreen; + [rootController presentViewController:securityViewController animated:YES completion:nil]; + isAuthenticationShowed = TRUE; + } +} + +- (void)applicationWillEnterForeground:(id)arg1 { + %orig; + isAuthenticationShowed = FALSE; +} +%end + +%hook TTKSettingsBaseCellPlugin +- (void)didSelectItemAtIndex:(NSInteger)index { + if ([self.itemModel.identifier isEqualToString:@"bhtiktok_settings"]) { + UINavigationController *BHTikTokSettings = [[UINavigationController alloc] initWithRootViewController:[[SettingsViewController alloc] init]]; + [topMostController() presentViewController:BHTikTokSettings animated:true completion:nil]; + } else { + return %orig; + } +} +%end + +%hook AWESettingsNormalSectionViewModel +- (void)viewDidLoad { + %orig; + if ([self.sectionIdentifier isEqualToString:@"account"]) { + TTKSettingsBaseCellPlugin *BHTikTokSettingsPluginCell = [[%c(TTKSettingsBaseCellPlugin) alloc] initWithPluginContext:self.context]; + + AWESettingItemModel *BHTikTokSettingsItemModel = [[%c(AWESettingItemModel) alloc] initWithIdentifier:@"bhtiktok_settings"]; + [BHTikTokSettingsItemModel setTitle:@"BHTikTok settings"]; + [BHTikTokSettingsItemModel setDetail:@"BHTikTok settings"]; + [BHTikTokSettingsItemModel setIconImage:[UIImage systemImageNamed:@"gear"]]; + [BHTikTokSettingsItemModel setType:99]; + + [BHTikTokSettingsPluginCell setItemModel:BHTikTokSettingsItemModel]; + + [self insertModel:BHTikTokSettingsPluginCell atIndex:0 animated:true]; + } +} +%end + +%hook SparkViewController // alwaysOpenSafari +- (void)viewWillAppear:(BOOL)animated { + if (![BHIManager alwaysOpenSafari]) { + return %orig; + } + + // NSURL *url = self.originURL; + NSURLComponents *components = [NSURLComponents componentsWithURL:self.originURL resolvingAgainstBaseURL:NO]; + NSString *searchParameter = @"url"; + NSString *searchValue = nil; + + for (NSURLQueryItem *queryItem in components.queryItems) { + if ([queryItem.name isEqualToString:searchParameter]) { + searchValue = queryItem.value; + break; + } + } + + // In-app browser is used for two-factor authentication with security key, + // login will not complete successfully if it's redirected to Safari + // if ([urlStr containsString:@"twitter.com/account/"] || [urlStr containsString:@"twitter.com/i/flow/"]) { + // return %orig; + // } + + if (searchValue) { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchValue] options:@{} completionHandler:nil]; + [self didTapCloseButton]; + } else { + return %orig; + } +} +%end + +%hook CTCarrier // changes country +- (NSString *)mobileCountryCode { + if ([BHIManager regionChangingEnabled]) { + if ([BHIManager selectedRegion]) { + NSDictionary *selectedRegion = [BHIManager selectedRegion]; + return selectedRegion[@"mcc"]; + } + return %orig; + } + return %orig; +} + +- (void)setIsoCountryCode:(NSString *)arg1 { + if ([BHIManager regionChangingEnabled]) { + if ([BHIManager selectedRegion]) { + NSDictionary *selectedRegion = [BHIManager selectedRegion]; + return %orig(selectedRegion[@"code"]); + } + return %orig(arg1); + } + return %orig(arg1); +} + +- (NSString *)isoCountryCode { + if ([BHIManager regionChangingEnabled]) { + if ([BHIManager selectedRegion]) { + NSDictionary *selectedRegion = [BHIManager selectedRegion]; + return selectedRegion[@"code"]; + } + return %orig; + } + return %orig; +} + +- (NSString *)mobileNetworkCode { + if ([BHIManager regionChangingEnabled]) { + if ([BHIManager selectedRegion]) { + NSDictionary *selectedRegion = [BHIManager selectedRegion]; + return selectedRegion[@"mnc"]; + } + return %orig; + } + return %orig; +} +%end + +%hook AWEAwemeModel // no ads, show porgress bar +- (id)initWithDictionary:(id)arg1 error:(id *)arg2 { + id orig = %orig; + return [BHIManager hideAds] && self.isAds ? nil : orig; +} +- (id)init { + id orig = %orig; + return [BHIManager hideAds] && self.isAds ? nil : orig; +} + +- (BOOL)progressBarDraggable { + return [BHIManager progressBar] || %orig; +} +- (BOOL)progressBarVisible { + return [BHIManager progressBar] || %orig; +} +%end + +%hook AWEPlayVideoPlayerController // auto play next video and stop looping video +- (void)playerWillLoopPlaying:(id)arg1 { + if ([BHIManager autoPlay]) { + if ([self.container.parentViewController isKindOfClass:%c(AWENewFeedTableViewController)]) { + [((AWENewFeedTableViewController *)self.container.parentViewController) scrollToNextVideo]; + return; + } + } + %orig; +} +%end + +%hook TIKTOKProfileHeaderExtraViewController // follow confirmation +- (void)relationBtnClicked:(id)sender { + if ([BHIManager followConfirmation]) { + showConfirmation(^(void) { %orig; }); + } else { + return %orig; + } +} +%end +%hook AWEPlayInteractionUserAvatarElement +- (void)onFollowViewClicked:(id)sender { + if ([BHIManager followConfirmation]) { + showConfirmation(^(void) { %orig; }); + } else { + return %orig; + } +} +%end +%hook AWEFeedVideoButton // like feed confirmation +- (void)_onTouchUpInside { + if ([BHIManager likeConfirmation] && [self.imageNameString isEqualToString:@"icon_home_like_before"]) { + showConfirmation(^(void) { %orig; }); + } else { + %orig; + } +} +%end +%hook AWECommentPanelCell // like/dislike comment confirmation +- (void)likeButtonTapped { + if ([BHIManager likeCommentConfirmation]) { + showConfirmation(^(void) { %orig; }); + } else { + return %orig; + } +} +- (void)dislikeButtonTapped { + if ([BHIManager dislikeCommentConfirmation]) { + showConfirmation(^(void) { %orig; }); + } else { + return %orig; + } +} +%end + +%hook AWEUserModel // follower, following Count fake +- (NSNumber *)followerCount { + if ([BHIManager fakeChangesEnabled]) { + NSString *fakeCountString = [[NSUserDefaults standardUserDefaults] stringForKey:@"follower_count"]; + if (!(fakeCountString.length == 0)) { + NSInteger fakeCount = [fakeCountString integerValue]; + return [NSNumber numberWithInt:fakeCount]; + } + + return %orig; + } + + return %orig; +} +- (NSNumber *)followingCount { + if ([BHIManager fakeChangesEnabled]) { + NSString *fakeCountString = [[NSUserDefaults standardUserDefaults] stringForKey:@"following_count"]; + if (!(fakeCountString.length == 0)) { + NSInteger fakeCount = [fakeCountString integerValue]; + return [NSNumber numberWithInt:fakeCount]; + } + + return %orig; + } + + return %orig; +} +- (BOOL)isVerifiedUser { + if ([BHIManager fakeVerified]) { + return true; + } + return %orig; +} +%end + +%hook AWETextInputController +- (NSUInteger)maxLength { + if ([BHIManager extendedComment]) { + return 240; + } + + return %orig; +} +%end +%hook AWEProfileEditTextViewController +- (NSInteger)maxTextLength { + if ([BHIManager extendedBio]) { + return 222; + } + + return %orig; +} +%end + +%hook TIKTOKProfileHeaderView // copy profile information +- (id)initWithFrame:(CGRect)arg1 { + self = %orig; + if ([BHIManager profileCopy]) { + [self addHandleLongPress]; + } + return self; +} +%new - (void)addHandleLongPress { + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + longPress.minimumPressDuration = 0.3; + [self addGestureRecognizer:longPress]; +} +%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender { + if (sender.state == UIGestureRecognizerStateBegan) { + if ([self.yy_viewController isKindOfClass:%c(TIKTOKProfileHeaderViewController)]) { + TIKTOKProfileHeaderViewController *rootVC = self.yy_viewController; + TUXActionSheetController *alert = [[%c(TUXActionSheetController) alloc] initWithTitle:@"Select option to copy."]; + if (rootVC.user.socialName) { + NSString *accountName = rootVC.user.socialName; + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy soical name" subtitle:accountName image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = accountName; + [%c(AWEToast) showSuccess:@"Copied"]; + }]]; + } + if (rootVC.user.nickname) { + NSString *nickName = rootVC.user.nickname; + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy nick name" subtitle:nickName image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = nickName; + [%c(AWEToast) showSuccess:@"Copied"]; + }]]; + } + if (rootVC.user.signature) { + NSString *bio = rootVC.user.signature; + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy bio" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = bio; + [%c(AWEToast) showSuccess:@"Copied"]; + }]]; + } + if (rootVC.user.bioUrl) { + NSString *bioURL = rootVC.user.bioUrl; + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy URL inbio" subtitle:bioURL image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = bioURL; + [%c(AWEToast) showSuccess:@"Copied"]; + }]]; + } + + [alert setTitle:@"Select option to copy."]; + [alert setDismissOnDraggingDown:true]; + [self.yy_viewController presentViewController:alert animated:YES completion:nil]; + } + } +} +%end + +%hook AWEProfileImagePreviewView // save profile image +- (id)initWithFrame:(CGRect)arg1 image:(id)arg2 imageURL:(id)arg3 backgroundColor:(id)arg4 userID:(id)arg5 type:(NSUInteger)arg6 { + self = %orig; + if ([BHIManager profileSave]) { + [self addHandleLongPress]; + } + return self; +} +%new - (void)addHandleLongPress { + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + longPress.minimumPressDuration = 0.3; + [self addGestureRecognizer:longPress]; +} +%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender { + if (sender.state == UIGestureRecognizerStateBegan) { + [BHIManager showSaveVC:@[self.avatar.image]]; + } +} +%end + +%hook AWEFeedViewTemplateCell +%property (nonatomic, strong) JGProgressHUD *hud; +%property(nonatomic, assign) BOOL elementsHidden; +%property (nonatomic, retain) NSString *fileextension; +- (void)configWithModel:(id)model { + %orig; + [self addHandleLongPress]; + self.elementsHidden = false; + + if ([BHIManager hideElementButton]) { + [self addHideElementButton]; + } +} +- (void)configureWithModel:(id)model { + %orig; + [self addHandleLongPress]; + self.elementsHidden = false; + + if ([BHIManager hideElementButton]) { + [self addHideElementButton]; + } +} +%new - (void)addHandleLongPress { + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + longPress.minimumPressDuration = 0.3; + [self addGestureRecognizer:longPress]; +} +%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender { + if (sender.state == UIGestureRecognizerStateBegan) { + NSString *video_description; + TUXActionSheetController *alert = [[%c(TUXActionSheetController) alloc] initWithTitle:video_description]; + + if ([self.viewController isKindOfClass:%c(AWEFeedCellViewController)]) { + AWEFeedCellViewController *rootVC = self.viewController; + video_description = rootVC.model.music_songName; + + if ([BHIManager downloadVideos]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download video" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + self.fileextension = [rootVC.model.video.playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } + }]]; + } + + if ([BHIManager downloadMusics]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download music" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + self.fileextension = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyMusicLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable music link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyVideoLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable video link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } + }]]; + } + + } else if ([self.viewController isKindOfClass:%c(TTKPhotoAlbumFeedCellController)]) { + TTKPhotoAlbumFeedCellController *rootVC = self.viewController; + video_description = rootVC.model.music_songName; + AWEPlayPhotoAlbumViewController *photoAlbumController = [rootVC valueForKey:@"_photoAlbumController"]; + + if ([BHIManager downloadVideos]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download current photo" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSIndexPath *currentPhotoIndexPath = [photoAlbumController currentIndexPath]; + NSArray *photos = rootVC.model.photoAlbum.photos; + AWEPhotoAlbumPhoto *currentPhoto = photos[currentPhotoIndexPath.item]; + NSURL *downloadableURL = [currentPhoto.originPhotoURL bestURLtoDownload]; + self.fileextension = [currentPhoto.originPhotoURL bestURLtoDownloadFormat]; + + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } + }]]; + + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download all photos" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSArray *photos = rootVC.model.photoAlbum.photos; + NSMutableArray *fileURLs = [NSMutableArray array]; + + for (AWEPhotoAlbumPhoto *currentPhoto in photos) { + NSURL *downloadableURL = [currentPhoto.originPhotoURL bestURLtoDownload]; + self.fileextension = [currentPhoto.originPhotoURL bestURLtoDownloadFormat]; + if (downloadableURL) { + [fileURLs addObject:downloadableURL]; + } + } + + BHMultipleDownload *dwManager = [[BHMultipleDownload alloc] init]; + [dwManager setDelegate:self]; + [dwManager downloadFiles:fileURLs]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + + }]]; + } + + if ([BHIManager downloadMusics]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download music" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + self.fileextension = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + } else if ([self.viewController isKindOfClass:%c(TTKPhotoAlbumDetailCellController)]) { + TTKPhotoAlbumDetailCellController *rootVC = self.viewController; + video_description = rootVC.model.music_songName; + AWEPlayPhotoAlbumViewController *photoAlbumController = [rootVC valueForKey:@"_photoAlbumController"]; + + if ([BHIManager downloadVideos]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download current photo" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSIndexPath *currentPhotoIndexPath = [photoAlbumController currentIndexPath]; + NSArray *photos = rootVC.model.photoAlbum.photos; + AWEPhotoAlbumPhoto *currentPhoto = photos[currentPhotoIndexPath.item]; + NSURL *downloadableURL = [currentPhoto.originPhotoURL bestURLtoDownload]; + self.fileextension = [currentPhoto.originPhotoURL bestURLtoDownloadFormat]; + + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } + }]]; + + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download all photos" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSArray *photos = rootVC.model.photoAlbum.photos; + NSMutableArray *fileURLs = [NSMutableArray array]; + + for (AWEPhotoAlbumPhoto *currentPhoto in photos) { + NSURL *downloadableURL = [currentPhoto.originPhotoURL bestURLtoDownload]; + self.fileextension = [currentPhoto.originPhotoURL bestURLtoDownloadFormat]; + if (downloadableURL) { + [fileURLs addObject:downloadableURL]; + } + } + + BHMultipleDownload *dwManager = [[BHMultipleDownload alloc] init]; + [dwManager setDelegate:self]; + [dwManager downloadFiles:fileURLs]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + + }]]; + } + + if ([BHIManager downloadMusics]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download music" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + self.fileextension = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + } else if ([self.viewController isKindOfClass:%c(TTKStoryContainerViewController)]) { + TTKStoryContainerViewController *rootVC = self.viewController; + video_description = rootVC.model.music_songName; + + if ([BHIManager downloadVideos]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download video" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + self.fileextension = [rootVC.model.video.playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } + }]]; + } + + if ([BHIManager downloadMusics]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download music" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + self.fileextension = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyMusicLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable music link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:nil cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyVideoLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable video link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } + }]]; + } + } + + if ([BHIManager copyVideoDecription]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy description" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = video_description; + }]]; + } + + [alert setTitle:video_description]; + [alert setDismissOnDraggingDown:true]; + [self.yy_viewController presentViewController:alert animated:YES completion:nil]; + } +} + +%new - (void)addHideElementButton { + UIButton *hideElementButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [hideElementButton setTag:999]; + [hideElementButton setTranslatesAutoresizingMaskIntoConstraints:false]; + [hideElementButton addTarget:self action:@selector(hideElementButtonHandler:) forControlEvents:UIControlEventTouchUpInside]; + if (self.elementsHidden) { + [hideElementButton setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateNormal]; + } else { + [hideElementButton setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal]; + } + + if (![self viewWithTag:999]) { + [hideElementButton setTintColor:[UIColor whiteColor]]; + [self addSubview:hideElementButton]; + + [NSLayoutConstraint activateConstraints:@[ + [hideElementButton.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor constant:50], + [hideElementButton.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor constant:-10], + [hideElementButton.widthAnchor constraintEqualToConstant:30], + [hideElementButton.heightAnchor constraintEqualToConstant:30], + ]]; + } +} +%new - (void)hideElementButtonHandler:(UIButton *)sender { + AWEAwemeBaseViewController *rootVC = self.viewController; + if ([rootVC.interactionController isKindOfClass:%c(TTKFeedInteractionLegacyMainContainerElement)]) { + TTKFeedInteractionLegacyMainContainerElement *interactionController = rootVC.interactionController; + if (self.elementsHidden) { + self.elementsHidden = false; + [interactionController hideAllElements:false exceptArray:nil]; + [sender setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal]; + } else { + self.elementsHidden = true; + [interactionController hideAllElements:true exceptArray:nil]; + [sender setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateNormal]; + } + } +} + +%new - (void)downloaderProgress:(float)progress { + self.hud.detailTextLabel.text = [BHIManager getDownloadingPersent:progress]; +} +%new - (void)downloaderDidFinishDownloadingAllFiles:(NSMutableArray *)downloadedFilePaths { + [self.hud dismiss]; + [BHIManager showSaveVC:downloadedFilePaths]; +} +%new - (void)downloaderDidFailureWithError:(NSError *)error { + if (error) { + [self.hud dismiss]; + } +} + +%new - (void)downloadProgress:(float)progress { + self.hud.detailTextLabel.text = [BHIManager getDownloadingPersent:progress]; +} +%new - (void)downloadDidFinish:(NSURL *)filePath Filename:(NSString *)fileName { + NSString *DocPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *newFilePath = [[NSURL fileURLWithPath:DocPath] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", NSUUID.UUID.UUIDString, self.fileextension]]; + [manager moveItemAtURL:filePath toURL:newFilePath error:nil]; + + [self.hud dismiss]; + [BHIManager showSaveVC:@[newFilePath]]; +} +%new - (void)downloadDidFailureWithError:(NSError *)error { + if (error) { + [self.hud dismiss]; + } +} +%end + +%hook AWEAwemeDetailTableViewCell +%property (nonatomic, strong) JGProgressHUD *hud; +%property(nonatomic, assign) BOOL elementsHidden; +%property (nonatomic, retain) NSString *fileextension; +- (void)configWithModel:(id)model { + %orig; + [self addHandleLongPress]; + self.elementsHidden = false; + + if ([BHIManager hideElementButton]) { + [self addHideElementButton]; + } +} +- (void)configureWithModel:(id)model { + %orig; + [self addHandleLongPress]; + self.elementsHidden = false; + + if ([BHIManager hideElementButton]) { + [self addHideElementButton]; + } +} +%new - (void)addHandleLongPress { + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + longPress.minimumPressDuration = 0.3; + [self addGestureRecognizer:longPress]; +} +%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender { + if (sender.state == UIGestureRecognizerStateBegan) { + NSString *video_description; + TUXActionSheetController *alert = [[%c(TUXActionSheetController) alloc] initWithTitle:video_description]; + + if ([self.viewController isKindOfClass:%c(AWEAwemeDetailCellViewController)]) { + AWEAwemeDetailCellViewController *rootVC = self.viewController; + video_description = rootVC.model.music_songName; + + if ([BHIManager downloadVideos]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download video" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + self.fileextension = [rootVC.model.video.playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } + }]]; + } + + if ([BHIManager downloadMusics]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download music" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + self.fileextension = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyMusicLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable music link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.music).playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyVideoLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable video link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } + }]]; + } + + } + + if ([BHIManager copyVideoDecription]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy description" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = video_description; + }]]; + } + + [alert setTitle:video_description]; + [alert setDismissOnDraggingDown:true]; + [self.yy_viewController presentViewController:alert animated:YES completion:nil]; + } +} + +%new - (void)addHideElementButton { + UIButton *hideElementButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [hideElementButton setTag:999]; + [hideElementButton setTranslatesAutoresizingMaskIntoConstraints:false]; + [hideElementButton addTarget:self action:@selector(hideElementButtonHandler:) forControlEvents:UIControlEventTouchUpInside]; + if (self.elementsHidden) { + [hideElementButton setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateNormal]; + } else { + [hideElementButton setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal]; + } + + if (![self viewWithTag:999]) { + [hideElementButton setTintColor:[UIColor whiteColor]]; + [self addSubview:hideElementButton]; + + [NSLayoutConstraint activateConstraints:@[ + [hideElementButton.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor constant:50], + [hideElementButton.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor constant:-10], + [hideElementButton.widthAnchor constraintEqualToConstant:30], + [hideElementButton.heightAnchor constraintEqualToConstant:30], + ]]; + } +} +%new - (void)hideElementButtonHandler:(UIButton *)sender { + AWEAwemeBaseViewController *rootVC = self.viewController; + if ([rootVC.interactionController isKindOfClass:%c(TTKFeedInteractionLegacyMainContainerElement)]) { + TTKFeedInteractionLegacyMainContainerElement *interactionController = rootVC.interactionController; + if (self.elementsHidden) { + self.elementsHidden = false; + [interactionController hideAllElements:false exceptArray:nil]; + [sender setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal]; + } else { + self.elementsHidden = true; + [interactionController hideAllElements:true exceptArray:nil]; + [sender setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateNormal]; + } + } +} + +%new - (void)downloadProgress:(float)progress { + self.hud.detailTextLabel.text = [BHIManager getDownloadingPersent:progress]; +} +%new - (void)downloadDidFinish:(NSURL *)filePath Filename:(NSString *)fileName { + NSString *DocPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *newFilePath = [[NSURL fileURLWithPath:DocPath] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", NSUUID.UUID.UUIDString, self.fileextension]]; + [manager moveItemAtURL:filePath toURL:newFilePath error:nil]; + + [self.hud dismiss]; + [BHIManager showSaveVC:@[newFilePath]]; +} +%new - (void)downloadDidFailureWithError:(NSError *)error { + if (error) { + [self.hud dismiss]; + } +} +%end + +%hook TTKStoryDetailTableViewCell +%property (nonatomic, strong) JGProgressHUD *hud; +%property(nonatomic, assign) BOOL elementsHidden; +%property (nonatomic, retain) NSString *fileextension; +- (void)configWithModel:(id)model { + %orig; + [self addHandleLongPress]; + self.elementsHidden = false; + + if ([BHIManager hideElementButton]) { + [self addHideElementButton]; + } +} +- (void)configureWithModel:(id)model { + %orig; + [self addHandleLongPress]; + self.elementsHidden = false; + + if ([BHIManager hideElementButton]) { + [self addHideElementButton]; + } +} +%new - (void)addHandleLongPress { + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + longPress.minimumPressDuration = 0.3; + [self addGestureRecognizer:longPress]; +} +%new - (void)handleLongPress:(UILongPressGestureRecognizer *)sender { + if (sender.state == UIGestureRecognizerStateBegan) { + NSString *video_description; + TUXActionSheetController *alert = [[%c(TUXActionSheetController) alloc] initWithTitle:video_description]; + + if ([self.viewController isKindOfClass:%c(TTKStoryDetailContainerViewController)]) { + TTKStoryDetailContainerViewController *rootVC = self.viewController; + video_description = rootVC.model.currentPlayingStory.music_songName; + + if ([BHIManager downloadVideos]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download video" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.currentPlayingStory.video.playURL bestURLtoDownload]; + self.fileextension = [rootVC.model.video.playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } + }]]; + } + + if ([BHIManager downloadMusics]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Download music" subtitle:nil image:[UIImage systemImageNamed:@"arrow.down"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.currentPlayingStory.music).playURL bestURLtoDownload]; + self.fileextension = [((AWEMusicModel *)rootVC.model.currentPlayingStory.music).playURL bestURLtoDownloadFormat]; + if (downloadableURL) { + BHDownload *dwManager = [[BHDownload alloc] init]; + [dwManager downloadFileWithURL:downloadableURL]; + [dwManager setDelegate:self]; + self.hud = [JGProgressHUD progressHUDWithStyle:JGProgressHUDStyleDark]; + self.hud.textLabel.text = @"Downloading"; + [self.hud showInView:topMostController().view]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyMusicLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable music link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [((AWEMusicModel *)rootVC.model.currentPlayingStory.music).playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } else { + [%c(AWEUIAlertView) showAlertWithTitle:@"BHTikTok, Hi" description:@"The video dosen't have music to download." image:nil actionButtonTitle:@"OK" cancelButtonTitle:nil actionBlock:nil cancelBlock:nil]; + } + }]]; + } + + if ([BHIManager copyVideoLink]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy downloadable video link" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + NSURL *downloadableURL = [rootVC.model.video.playURL bestURLtoDownload]; + if (downloadableURL) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = [downloadableURL absoluteString]; + } + }]]; + } + + } + + if ([BHIManager copyVideoDecription]) { + [alert addAction:[[%c(TUXActionSheetAction) alloc] initWithStyle:0 title:@"Copy description" subtitle:nil image:[UIImage systemImageNamed:@"clipboard"] imageLabel:nil handler:^(TUXActionSheetAction * _Nonnull action) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = video_description; + }]]; + } + + [alert setTitle:video_description]; + [alert setDismissOnDraggingDown:true]; + [self.yy_viewController presentViewController:alert animated:YES completion:nil]; + } +} + +%new - (void)addHideElementButton { + UIButton *hideElementButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [hideElementButton setTag:999]; + [hideElementButton setTranslatesAutoresizingMaskIntoConstraints:false]; + [hideElementButton addTarget:self action:@selector(hideElementButtonHandler:) forControlEvents:UIControlEventTouchUpInside]; + if (self.elementsHidden) { + [hideElementButton setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateNormal]; + } else { + [hideElementButton setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal]; + } + + if (![self viewWithTag:999]) { + [hideElementButton setTintColor:[UIColor whiteColor]]; + [self addSubview:hideElementButton]; + + [NSLayoutConstraint activateConstraints:@[ + [hideElementButton.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor constant:50], + [hideElementButton.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor constant:-10], + [hideElementButton.widthAnchor constraintEqualToConstant:30], + [hideElementButton.heightAnchor constraintEqualToConstant:30], + ]]; + } +} +%new - (void)hideElementButtonHandler:(UIButton *)sender { + TTKStoryDetailContainerViewController *rootVC = self.viewController; + if ([rootVC.interactionController isKindOfClass:%c(TTKFeedInteractionLegacyMainContainerElement)]) { + TTKFeedInteractionLegacyMainContainerElement *interactionController = rootVC.interactionController; + if (self.elementsHidden) { + self.elementsHidden = false; + [interactionController hideAllElements:false exceptArray:nil]; + [sender setImage:[UIImage systemImageNamed:@"eye.slash.fill"] forState:UIControlStateNormal]; + } else { + self.elementsHidden = true; + [interactionController hideAllElements:true exceptArray:nil]; + [sender setImage:[UIImage systemImageNamed:@"eye.fill"] forState:UIControlStateNormal]; + } + } +} + +%new - (void)downloadProgress:(float)progress { + self.hud.detailTextLabel.text = [BHIManager getDownloadingPersent:progress]; +} +%new - (void)downloadDidFinish:(NSURL *)filePath Filename:(NSString *)fileName { + NSString *DocPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *newFilePath = [[NSURL fileURLWithPath:DocPath] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", NSUUID.UUID.UUIDString, self.fileextension]]; + [manager moveItemAtURL:filePath toURL:newFilePath error:nil]; + + [self.hud dismiss]; + [BHIManager showSaveVC:@[newFilePath]]; +} +%new - (void)downloadDidFailureWithError:(NSError *)error { + if (error) { + [self.hud dismiss]; + } +} +%end + +%hook AWEURLModel +%new - (NSString *)bestURLtoDownloadFormat { + NSURL *bestURLFormat; + for (NSString *url in self.originURLList) { + if ([url containsString:@"video_mp4"]) { + bestURLFormat = @"mp4"; + } else if ([url containsString:@".jpeg"]) { + bestURLFormat = @"jpeg"; + } else if ([url containsString:@".png"]) { + bestURLFormat = @"png"; + } else if ([url containsString:@".mp3"]) { + bestURLFormat = @"mp3"; + } else if ([url containsString:@".m4a"]) { + bestURLFormat = @"m4a"; + } + } + if (bestURLFormat == nil) { + bestURLFormat = @"m4a"; + } + + return bestURLFormat; +} +%new - (NSURL *)bestURLtoDownload { + NSURL *bestURL; + for (NSString *url in self.originURLList) { + if ([url containsString:@"video_mp4"] || [url containsString:@".jpeg"] || [url containsString:@".mp3"]) { + bestURL = [NSURL URLWithString:url]; + } + } + + if (bestURL == nil) { + bestURL = [NSURL URLWithString:[self.originURLList firstObject]]; + } + + return bestURL; +} +%end + +%hook NSFileManager +-(BOOL)fileExistsAtPath:(id)arg1 { + for (NSString *file in jailbreakPaths) { + if ([arg1 isEqualToString:file]) { + return NO; + } + } + return %orig; +} +-(BOOL)fileExistsAtPath:(id)arg1 isDirectory:(BOOL*)arg2 { + for (NSString *file in jailbreakPaths) { + if ([arg1 isEqualToString:file]) { + return NO; + } + } + return %orig; +} +%end +%hook BDADeviceHelper ++(bool)isJailBroken { + return NO; +} +%end + +%hook UIDevice ++(bool)btd_isJailBroken { + return NO; +} +%end + +%hook TTInstallUtil ++(bool)isJailBroken { + return NO; +} +%end + +%hook AppsFlyerUtils ++(bool)isJailbrokenWithSkipAdvancedJailbreakValidation:(bool)arg2 { + return NO; +} +%end + +%hook PIPOIAPStoreManager +-(bool)_pipo_isJailBrokenDeviceWithProductID:(id)arg2 orderID:(id)arg3 { + return NO; +} +%end + +%hook IESLiveDeviceInfo ++(bool)isJailBroken { + return NO; +} +%end + +%hook PIPOStoreKitHelper +-(bool)isJailBroken { + return NO; +} +%end + +%hook BDInstallNetworkUtility ++(bool)isJailBroken { + return NO; +} +%end + +%hook TTAdSplashDeviceHelper ++(bool)isJailBroken { + return NO; +} +%end + +%hook GULAppEnvironmentUtil ++(bool)isFromAppStore { + return YES; +} ++(bool)isAppStoreReceiptSandbox { + return NO; +} ++(bool)isAppExtension { + return YES; +} +%end + +%hook FBSDKAppEventsUtility ++(bool)isDebugBuild { + return NO; +} +%end + +%hook AWEAPMManager ++(id)signInfo { + return @"AppStore"; +} +%end + +%hook NSBundle +-(id)pathForResource:(id)arg1 ofType:(id)arg2 { + if ([arg2 isEqualToString:@"mobileprovision"]) { + return nil; + } + return %orig; +} +%end +%hook AWESecurity +- (void)resetCollectMode { + return; +} +%end +%hook MSManagerOV +- (id)setMode { + return (id (^)(id)) ^{ + }; +} +%end +%hook MSConfigOV +- (id)setMode { + return (id (^)(id)) ^{ + }; +} +%end + +%hook HBForceCepheiPrefs ++ (BOOL)forceCepheiPrefsWhichIReallyNeedToAccessAndIKnowWhatImDoingISwear { + return YES; +} +%end + +%ctor { + jailbreakPaths = @[ + @"/Applications/Cydia.app", @"/Applications/blackra1n.app", + @"/Applications/FakeCarrier.app", @"/Applications/Icy.app", + @"/Applications/IntelliScreen.app", @"/Applications/MxTube.app", + @"/Applications/RockApp.app", @"/Applications/SBSettings.app", @"/Applications/WinterBoard.app", + @"/.cydia_no_stash", @"/.installed_unc0ver", @"/.bootstrapped_electra", + @"/usr/libexec/cydia/firmware.sh", @"/usr/libexec/ssh-keysign", @"/usr/libexec/sftp-server", + @"/usr/bin/ssh", @"/usr/bin/sshd", @"/usr/sbin/sshd", + @"/var/lib/cydia", @"/var/lib/dpkg/info/mobilesubstrate.md5sums", + @"/var/log/apt", @"/usr/share/jailbreak/injectme.plist", @"/usr/sbin/frida-server", + @"/Library/MobileSubstrate/CydiaSubstrate.dylib", @"/Library/TweakInject", + @"/Library/MobileSubstrate/MobileSubstrate.dylib", @"Library/MobileSubstrate/MobileSubstrate.dylib", + @"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist", @"/Library/MobileSubstrate/DynamicLibraries/Veency.plist", + @"/System/Library/LaunchDaemons/com.ikey.bbot.plist", @"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist", @"/System/Library/CoreServices/SystemVersion.plist", + @"/private/var/mobile/Library/SBSettings/Themes", @"/private/var/lib/cydia", + @"/private/var/tmp/cydia.log", @"/private/var/log/syslog", + @"/private/var/cache/apt/", @"/private/var/lib/apt", + @"/private/var/Users/", @"/private/var/stash", + @"/usr/lib/libjailbreak.dylib", @"/usr/lib/libz.dylib", + @"/usr/lib/system/introspectionNSZombieEnabled", + @"/usr/lib/dyld", + @"/jb/amfid_payload.dylib", @"/jb/libjailbreak.dylib", + @"/jb/jailbreakd.plist", @"/jb/offsets.plist", + @"/jb/lzma", + @"/hmd_tmp_file", + @"/etc/ssh/sshd_config", @"/etc/apt/undecimus/undecimus.list", + @"/etc/apt/sources.list.d/sileo.sources", @"/etc/apt/sources.list.d/electra.list", + @"/etc/apt", @"/etc/ssl/certs", @"/etc/ssl/cert.pem", + @"/bin/sh", @"/bin/bash", + ]; + %init; +} \ No newline at end of file diff --git a/control b/control new file mode 100644 index 0000000..f8b507f --- /dev/null +++ b/control @@ -0,0 +1,9 @@ +Package: com.bandarhl.bhtiktok +Name: BHTikTok +Version: 0.0.1 +Architecture: iphoneos-arm +Description: An awesome tweak for TikTok! +Maintainer: Bandar Alruwaili +Author: Bandar Alruwaili +Section: Tweaks +Depends: mobilesubstrate (>= 0.9.5000)