diff --git a/Makefile b/Makefile index 8ec96c0..1a45c0a 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ TARGET := iphone:clang:latest:14.0 INSTALL_TARGET_PROCESSES = BeReal -ARCHS = arm64 arm64e +ARCHS = arm64 FINALPACKAGE = 1 -PACKAGE_VERSION = 1.2.2 +PACKAGE_VERSION = 1.3 + +THEOS_PACKAGE_SCHEME = rootless export SYSROOT = $(THEOS)/sdks/iPhoneOS15.5.sdk @@ -10,16 +12,16 @@ include $(THEOS)/makefiles/common.mk TWEAK_NAME = Bea -Bea_FILES = Tweak/Tweak.x -Bea_CFLAGS = -fobjc-arc -Bea_FRAMEWORKS = UIKit MapKit +$(TWEAK_NAME)_FILES = Tweak/Tweak.x +$(TWEAK_NAME)_CFLAGS = -fobjc-arc +$(TWEAK_NAME)_FRAMEWORKS = UIKit MapKit -ifeq ($(JAILED), 1) -Bea_CFLAGS += -D JAILED=1 +ifeq ($(LEGACY_SUPPORT), 1) +$(TWEAK_NAME)_CFLAGS += -D LEGACY_SUPPORT=1 endif -ifeq ($(LEGACY_SUPPORT), 1) -Bea_CFLAGS += -D LEGACY_SUPPORT=1 +ifeq ($(JAILED), 1) +Bea_CFLAGS += -D JAILED=1 endif include $(THEOS_MAKE_PATH)/tweak.mk diff --git a/README.md b/README.md index 8e7da9b..88c3aae 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Compatible with all iOS devices running iOS 14 or later. - Post on time, even if you're posting late - Set a custom location - Set a custom retake count + - Add what you're currently playing or add a song - Download BeReals - Bypass screenshot detection @@ -34,6 +35,9 @@ Compatible with all iOS devices running iOS 14 or later. 1. Add `https://havoc.app` to your package manager or download the latest `.deb` from the [Releases](https://github.com/yandevelop/Bea/releases) tab. 2. Install Bea using your package manager of choice. +## Donations +If you would like to support the development of this project, you can donate to me [here](https://ko-fi.com/yandevelop) + ## License You may not copy, modify, sublicense or distribute the source code or any packages from it. diff --git a/Tweak/Tweak.h b/Tweak/Tweak.h index 1cfc239..0a0a93d 100644 --- a/Tweak/Tweak.h +++ b/Tweak/Tweak.h @@ -1,8 +1,8 @@ #import #import #import "../Utilities/BeaUtilities.m" -#import "../Utilities/UploadViewController/BeaUploadViewController.m" -#import +#import "../Utilities/Managers/TokenManager/BeaTokenManager.m" +#import "../Utilities/ViewControllers/UploadViewController/BeaUploadViewController.m" BOOL isUnblurred = NO; NSString *authorizationKey = nil; @@ -10,4 +10,20 @@ Class photoView; @interface CAFilter : NSObject @property (copy) NSString * name; +@end + +@interface DoublePhotoView : UIView +@property (nonatomic, strong) BeaButton *downloadButton; +@end + +@interface HomeViewController : UIViewController +@property (nonatomic, retain) UIImageView *ibNavBarLogoImageView; +- (void)openDebugMenu; +@end + +@interface SettingsViewController : UIViewController +@property (nonatomic, retain) UITableView *tableView; +@end + +@interface UIHostingView : UIView @end \ No newline at end of file diff --git a/Tweak/Tweak.x b/Tweak/Tweak.x index 8703a96..7eb3514 100644 --- a/Tweak/Tweak.x +++ b/Tweak/Tweak.x @@ -1,48 +1,51 @@ #import "Tweak.h" %hook DoublePhotoView -- (void)layoutSubviews { +%property (nonatomic, strong) BeaButton *downloadButton; + +- (void)drawRect:(CGRect)rect { %orig; - UIView *doublePhotoView = (UIView *)self; + UIResponder *responder = self; + while (responder && ![responder isKindOfClass:[UIViewController class]]) { + responder = [responder nextResponder]; + } + UIViewController *vc = (UIViewController *)responder; + + if ((![vc isKindOfClass:NSClassFromString(@"BeReal.SUIFeedViewController")] && ![vc isKindOfClass:NSClassFromString(@"BeReal.FeedViewController")] && ![vc isKindOfClass:NSClassFromString(@"BeReal.MemoryDetailsViewController")]) || CGRectGetWidth([self frame]) < 180) return; + + if ([self downloadButton]) return; - if ([doublePhotoView.subviews.lastObject isKindOfClass:[BeaButton class]] || doublePhotoView.frame.size.width < 180) return; - // make the view accept touches (dragging photos etc) - doublePhotoView.superview.userInteractionEnabled = YES; - doublePhotoView.superview.superview.userInteractionEnabled = YES; + [[self superview] setUserInteractionEnabled:YES]; + [[[self superview] superview] setUserInteractionEnabled:YES]; - UIResponder *responder = self; - while (responder && ![responder isKindOfClass:[UIViewController class]]) { - responder = [responder nextResponder]; - } - UIViewController *vc = (UIViewController *)responder; - - if (![vc isKindOfClass:objc_getClass("BeReal.SUIFeedViewController")] && ![vc isKindOfClass:objc_getClass("BeReal.FeedViewController")]) return; + BeaButton *downloadButton = [BeaButton downloadButton]; + downloadButton.layer.zPosition = 3; - BeaButton *downloadButton = [BeaButton downloadButton]; - [doublePhotoView addSubview:downloadButton]; + [self setDownloadButton:downloadButton]; + [self addSubview:downloadButton]; [NSLayoutConstraint activateConstraints:@[ - [downloadButton.trailingAnchor constraintEqualToAnchor:doublePhotoView.trailingAnchor constant:-11.6], - [downloadButton.bottomAnchor constraintEqualToAnchor:doublePhotoView.topAnchor constant:47.333] + [[[self downloadButton] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor] constant:-11.6], + [[[self downloadButton] bottomAnchor] constraintEqualToAnchor:[self topAnchor] constant:47.333] ]]; } + - (void)onMainImagePressed:(UILongPressGestureRecognizer *)gestureRecognizer { %orig; - [BeaButton toggleDownloadButtonVisibility:self gestureRecognizer:gestureRecognizer]; + [[self downloadButton] toggleVisibilityWithGestureRecognizer:gestureRecognizer]; } - - (void)handleMainPanned:(UIPinchGestureRecognizer *)gestureRecognizer { %orig; - [BeaButton toggleDownloadButtonVisibility:self gestureRecognizer:gestureRecognizer]; + [[self downloadButton] toggleVisibilityWithGestureRecognizer:gestureRecognizer]; } - (void)handleMainPinched:(UIPinchGestureRecognizer *)gestureRecognizer { %orig; - [BeaButton toggleDownloadButtonVisibility:self gestureRecognizer:gestureRecognizer]; + [[self downloadButton] toggleVisibilityWithGestureRecognizer:gestureRecognizer]; } %end @@ -52,15 +55,16 @@ %orig; if (isUnblurred) return; - UIAlertController *alertController = (UIAlertController *)self; - if ([alertController.actions[2].title isEqual:@"👀 Unblur"]) { + if ([self.actions[2].title isEqual:@"👀 Unblur"]) { // Set the whole view to hidden self.view.superview.hidden = YES; - UIAlertAction *thirdAction = alertController.actions[2]; + UIAlertAction *thirdAction = self.actions[2]; id block = [thirdAction valueForKey:@"_handler"]; if (block) { - void (^handler)(UIAlertAction *) = block; - handler(thirdAction); + dispatch_async(dispatch_get_main_queue(), ^{ + void (^handler)(UIAlertAction *) = block; + handler(thirdAction); + }); } isUnblurred = YES; // Dismiss the UIAlertController automatically @@ -69,28 +73,22 @@ } %end - %hook HomeViewController - (void)viewDidLoad { %orig; - UIViewController *homeViewController = (UIViewController *)self; - - if (!isUnblurred && [homeViewController respondsToSelector:@selector(openDebugMenu)]) { + if (!isUnblurred && [self respondsToSelector:@selector(openDebugMenu)]) { //[homeViewController performSelector:@selector(openDebugMenu)]; #ifndef LEGACY_SUPPORT NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; NSComparisonResult result = [version compare:@"1.1.2" options:NSNumericSearch]; if (result == NSOrderedAscending) { BeaAlertView *alertView = [[BeaAlertView alloc] init]; - [homeViewController.view addSubview:alertView]; + [[self view] addSubview:alertView]; } #endif } - UIImageView *beRealLogoView = [self valueForKey:@"ibNavBarLogoImageView"]; - beRealLogoView.userInteractionEnabled = YES; - #ifdef JAILED NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"Bea" ofType:@"bundle"]; NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; @@ -100,29 +98,28 @@ UIImage *beFakeLogo = [UIImage imageNamed:@"BeFake.png" inBundle:bundle compatibleWithTraitCollection:nil]; #endif - CGSize targetSize = beRealLogoView.image.size; + CGSize targetSize = [[[self ibNavBarLogoImageView] image] size]; UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0); [beFakeLogo drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)]; UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - beRealLogoView.image = resizedImage; - UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; - [beRealLogoView addGestureRecognizer:tapGestureRecognizer]; + + [[self ibNavBarLogoImageView] addGestureRecognizer:tapGestureRecognizer]; + [[self ibNavBarLogoImageView] setImage:resizedImage]; + [[self ibNavBarLogoImageView] setUserInteractionEnabled:YES]; } %new - (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer { - - UIViewController *vc = (UIViewController *)self; // display the error view here if (!authorizationKey) return; - BeaUploadViewController *beaUploadViewController = [[BeaUploadViewController alloc] initWithAuthorization:authorizationKey]; + BeaUploadViewController *beaUploadViewController = [[BeaUploadViewController alloc] init]; beaUploadViewController.modalPresentationStyle = UIModalPresentationFullScreen; - [vc presentViewController:beaUploadViewController animated:YES completion:nil]; + [self presentViewController:beaUploadViewController animated:YES completion:nil]; } %end @@ -138,21 +135,18 @@ } %end - %hook SettingsViewController - (void)viewDidLoad { %orig; - UIViewController *vc = (UIViewController *)self; - UITableView *labelView = vc.view.subviews.firstObject; - - UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, labelView.frame.size.width, 50)]; + UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, [[self tableView] frame].size.width, 50)]; NSString *headerText = [NSString stringWithFormat:@"Bea %@\nmade with ❤️ by yan", TWEAK_VERSION]; headerLabel.text = headerText; headerLabel.numberOfLines = 0; headerLabel.font = [UIFont fontWithName:@"Inter" size:10]; headerLabel.textAlignment = NSTextAlignmentCenter; - labelView.tableHeaderView = headerLabel; + + [[self tableView] setTableHeaderView:headerLabel]; } %end @@ -163,16 +157,6 @@ } %end - -%hook NSNotificationCenter -- (void)addObserver:(id)arg0 selector:(SEL)arg1 name:(NSNotificationName)arg2 object:(id)arg3 { - if (arg2 == UIApplicationUserDidTakeScreenshotNotification) { - return; - } - %orig; -} -%end - // return a nil string so the BeReal photo view is clear :) %hook NSBundle - (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName { @@ -193,18 +177,18 @@ %orig; if ([[arg1 allKeys] containsObject:@"Authorization"] && !authorizationKey) { authorizationKey = arg1[@"Authorization"]; + [[BeaTokenManager sharedInstance] setBRAccessToken:authorizationKey]; } } %end - %hook UIHostingView - (void)layoutSubviews { %orig; - UIView *s = (UIView *)self; - for (UIView *v in s.superview.subviews) { - if ((v.frame.size.width <= 48 && v.frame.size.width > 32) || (([v isKindOfClass:objc_getClass("SwiftUI._UIGraphicsView")] || [v isKindOfClass:[UIView class]]) && v.frame.size.width > 350 && v.subviews.count == 0)) { - v.hidden = YES; + for (UIView *v in [[self superview] subviews]) { + CGFloat width = v.frame.size.width; + if ((width <= 48 && width > 32) || ([v isKindOfClass:[UIView class]] && width > 350 && width < 1400 && v.subviews.count == 0)) { + [v setHidden:YES]; } } } @@ -215,7 +199,8 @@ - (void)viewWillAppear:(id)arg1 { %orig; if ([self.viewControllers.firstObject isKindOfClass:objc_getClass("BeReal.SUIFeedViewController")] && [self.parentViewController isKindOfClass:objc_getClass("BeReal.HomeViewController")] && !isUnblurred) { - [self.parentViewController performSelector:@selector(openDebugMenu)]; + HomeViewController *controller = (HomeViewController *)self.parentViewController; + [controller openDebugMenu]; } } %end @@ -227,8 +212,8 @@ photoView = objc_getClass("RealComponents.DoublePhotoView"); #endif - %init(HomeViewController = objc_getClass("_TtC6BeReal18HomeViewController"), + %init(HomeViewController = objc_getClass("BeReal.HomeViewController"), DoublePhotoView = photoView, - SettingsViewController = objc_getClass("_TtC6BeReal22SettingsViewController"), + SettingsViewController = objc_getClass("BeReal.SettingsViewController"), UIHostingView = objc_getClass("_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697116_UIInheritedView")); } \ No newline at end of file diff --git a/Utilities/BeaUtilities.h b/Utilities/BeaUtilities.h index 564704c..54817e6 100644 --- a/Utilities/BeaUtilities.h +++ b/Utilities/BeaUtilities.h @@ -1,6 +1,6 @@ @interface BeaButton : UIButton + (instancetype)downloadButton; -+ (void)toggleDownloadButtonVisibility:(UIView *)view gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer; +- (void)toggleVisibilityWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer; @end @interface BeaDownloader : NSObject diff --git a/Utilities/BeaUtilities.m b/Utilities/BeaUtilities.m index 3850da5..c89c86b 100644 --- a/Utilities/BeaUtilities.m +++ b/Utilities/BeaUtilities.m @@ -1,4 +1,5 @@ #import "BeaUtilities.h" +#import @implementation BeaDownloader + (void)downloadImage:(id)sender { @@ -26,7 +27,7 @@ + (void)downloadImage:(id)sender { + (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { if (error) { - NSLog(@"Error saving image: %@", error.localizedDescription); + NSLog(@"[Bea]Error saving image: %@", error.localizedDescription); } else { UIButton *button = (__bridge UIButton *)contextInfo; UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:19]; @@ -72,19 +73,15 @@ + (instancetype)downloadButton { return downloadButton; } -+ (void)toggleDownloadButtonVisibility:(UIView *)view gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { - if (view.frame.size.width < 180 || ![view.subviews.lastObject isKindOfClass:[BeaButton class]]) return; - - BeaButton *downloadButton = view.subviews.lastObject; - +- (void)toggleVisibilityWithGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { if ((gestureRecognizer.numberOfTouches < 2 && [gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) || gestureRecognizer.state == 3) { if (gestureRecognizer.state == 2) return; [UIView animateWithDuration:0.2 animations:^{ - downloadButton.alpha = 1; + self.alpha = 1; }]; } else if ((gestureRecognizer.state == 1 || gestureRecognizer.state == 2)) { [UIView animateWithDuration:0.2 animations:^{ - downloadButton.alpha = 0; + self.alpha = 0; }]; } } diff --git a/Utilities/Managers/TokenManager/BeaTokenManager.h b/Utilities/Managers/TokenManager/BeaTokenManager.h new file mode 100644 index 0000000..28f1c4e --- /dev/null +++ b/Utilities/Managers/TokenManager/BeaTokenManager.h @@ -0,0 +1,8 @@ +@interface BeaTokenManager : NSObject ++ (instancetype)sharedInstance; +@property (nonatomic, strong) NSString *BRAccessToken; +@property (nonatomic, strong) NSString *spotifyAccessToken; +@property (nonatomic, strong) NSString *spotifyRefreshToken; +@property (nonatomic, strong) NSNumber *expiryValue; +- (void)writeToKeychainWithDictionary:(NSDictionary *)response; +@end \ No newline at end of file diff --git a/Utilities/Managers/TokenManager/BeaTokenManager.m b/Utilities/Managers/TokenManager/BeaTokenManager.m new file mode 100644 index 0000000..fb11a32 --- /dev/null +++ b/Utilities/Managers/TokenManager/BeaTokenManager.m @@ -0,0 +1,93 @@ +#import "BeaTokenManager.h" + +@implementation BeaTokenManager ++ (instancetype)sharedInstance { + static BeaTokenManager *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (void)retrieveCredentials { + NSString *serviceName = @"com.bereal.BRMusic"; + NSString *accountName = @"Spotify.AuthStore.credentials"; + NSString *groupName = @"group.BeReal"; + + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService: serviceName, + (__bridge id)kSecAttrAccount: accountName, + (__bridge id)kSecAttrAccessGroup: groupName, + (__bridge id)kSecReturnData: @YES + }; + + CFTypeRef result = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); + + if (status == errSecSuccess) { + NSData *credentialData = (__bridge_transfer NSData *)result; + NSDictionary *credentials = [NSJSONSerialization JSONObjectWithData:credentialData options:0 error:nil]; + + self.spotifyAccessToken = credentials[@"accessToken"]; + self.spotifyRefreshToken = credentials[@"refreshToken"]; + self.expiryValue = credentials[@"expiry"]; + + } else if (status == errSecItemNotFound) { + NSLog(@"[Bea] Keychain item not found for the specified service and account."); + } else { + NSLog(@"[Bea] Keychain error: %d", (int)status); + } +} + +// update the keychain with the new access token and a new expiry value +- (void)writeToKeychainWithDictionary:(NSDictionary *)response { + NSString *serviceName = @"com.bereal.BRMusic"; + NSString *accountName = @"Spotify.AuthStore.credentials"; + NSString *groupName = @"group.BeReal"; + + NSDate *now = [NSDate date]; + NSTimeInterval oneHour = 3600; + NSDate *expireDate = [now dateByAddingTimeInterval:oneHour]; + NSTimeInterval expiryInterval = [expireDate timeIntervalSinceReferenceDate]; + NSNumber *expiryValue = [NSNumber numberWithDouble:expiryInterval]; + + NSString *accessToken = response[@"accessToken"]; + NSString *refreshToken = self.spotifyRefreshToken; + self.spotifyAccessToken = accessToken; + + NSDictionary *credentials = @{ + @"accessToken": accessToken, + @"refreshToken": refreshToken, + @"expiry" : expiryValue + }; + + NSData *credentialData = [NSJSONSerialization dataWithJSONObject:credentials options:0 error:nil]; + + NSDictionary *searchQuery = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService: serviceName, + (__bridge id)kSecAttrAccount: accountName, + (__bridge id)kSecAttrAccessGroup: groupName + }; + + NSDictionary *updateQuery = @{ + (__bridge id)kSecValueData: credentialData + }; + + OSStatus searchStatus = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, NULL); + + if (searchStatus == errSecSuccess) { + OSStatus updateStatus = SecItemUpdate((__bridge CFDictionaryRef)searchQuery, (__bridge CFDictionaryRef)updateQuery); + + if (updateStatus == errSecSuccess) { + NSLog(@"[Bea] Data successfully updated in the keychain."); + } else { + NSLog(@"[Bea] Failed to update data in the keychain. Error: %d", (int)updateStatus); + } + } else { + NSLog(@"[Bea] Keychain item not found for the specified service, account, and group."); + } +} +@end \ No newline at end of file diff --git a/Utilities/Music Components/Managers/APIHandler/BeaSpotifyAPIHandler.h b/Utilities/Music Components/Managers/APIHandler/BeaSpotifyAPIHandler.h new file mode 100644 index 0000000..b2caac1 --- /dev/null +++ b/Utilities/Music Components/Managers/APIHandler/BeaSpotifyAPIHandler.h @@ -0,0 +1,11 @@ +@protocol BeaSpotifyAPIHandlerDelegate +- (void)managerDidValidateAccessToken; +@end + +@interface BeaSpotifyAPIHandler : NSObject +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) NSString *accessToken; +@property (nonatomic, strong) NSString *refreshToken; +@property (nonatomic, strong) NSNumber *expiryValue; +- (void)retrieveCurrentlyPlayingSong; +@end \ No newline at end of file diff --git a/Utilities/Music Components/Managers/APIHandler/BeaSpotifyAPIHandler.m b/Utilities/Music Components/Managers/APIHandler/BeaSpotifyAPIHandler.m new file mode 100644 index 0000000..9365107 --- /dev/null +++ b/Utilities/Music Components/Managers/APIHandler/BeaSpotifyAPIHandler.m @@ -0,0 +1,180 @@ +#import "BeaSpotifyAPIHandler.h" + +@implementation BeaSpotifyAPIHandler +- (instancetype)init { + self = [super init]; + if (self) { + [self validateAccessToken]; + } + return self; +} + + +- (void)validateAccessToken { + [[BeaTokenManager sharedInstance] retrieveCredentials]; + + self.expiryValue = [[BeaTokenManager sharedInstance] expiryValue]; + + NSDate *now = [NSDate date]; + NSTimeInterval nowTimestamp = [now timeIntervalSinceReferenceDate]; + + NSTimeInterval expireTimestamp = [self.expiryValue doubleValue]; + + // check if the access token already expired + if (nowTimestamp > expireTimestamp) { + [self refreshSpotifyAccessToken]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate managerDidValidateAccessToken]; + }); + } +} + +- (void)refreshSpotifyAccessToken { + // to refresh the access token, a call to bereals api endpoint has to be made + // this will return a new access token that we can use to fetch the currently playing song + + self.refreshToken = [[BeaTokenManager sharedInstance] spotifyRefreshToken]; + NSString *BRAccessToken = [[BeaTokenManager sharedInstance] BRAccessToken]; + + if (!self.refreshToken) return; + + NSString *baseRefreshURL = @"https://mobile.bereal.com/api/music/spotify/refresh_token"; + NSString *refreshURLString = [NSString stringWithFormat:@"%@?refresh_token=%@", baseRefreshURL, self.refreshToken]; + NSURL *refreshURL = [NSURL URLWithString:refreshURLString]; + + NSMutableURLRequest *refreshTokenRequest = [NSMutableURLRequest requestWithURL:refreshURL]; + [refreshTokenRequest setHTTPMethod:@"GET"]; + + NSDictionary *headers = @{ + @"authorization": BRAccessToken, + @"accept": @"*/*", + @"bereal-platform": @"iOS", + @"bereal-os-version": @"14.7.1", + @"accept-Language": @"en-US;q=1.0", + @"user-Agent": @"BeReal/1.7.0 (AlexisBarreyat.BeReal; build:11001; iOS 14.7.1) 1.0.0/BRApiKit", + @"bereal-app-language": @"en-US", + @"bereal-device-language": @"en", + @"bereal-app-version" : @"1.7.0-(11001)" + }; + + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + [refreshTokenRequest setValue:value forHTTPHeaderField:field]; + }]; + + NSURLSession *session = [NSURLSession sharedSession]; + NSURLSessionDataTask *task = [session dataTaskWithRequest:refreshTokenRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (!error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 200) { + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]; + self.accessToken = jsonResponse[@"accessToken"]; + + // update the access tokens in the keychain to avoid future unneccesary api calls + [[BeaTokenManager sharedInstance] writeToKeychainWithDictionary:jsonResponse]; + + // notify the delegate that the manager now validated the access token + [self.delegate managerDidValidateAccessToken]; + } else { + NSLog(@"[Bea] Error! %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + } + } + }]; + [task resume]; +} + +- (void)retrieveCurrentlyPlayingSong { + // since the delegate is called from the main thread and thus this function also + // enter the background thread again + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // check if the access token is nil since it's possible that it hasn't been set before + if (!self.accessToken) { + self.accessToken = [[BeaTokenManager sharedInstance] spotifyAccessToken]; + } + + NSString *currentlyPlayingURLString = @"https://api.spotify.com/v1/me/player/currently-playing?additional_types=episode"; + NSURL *currentlyPlayingURL = [NSURL URLWithString:currentlyPlayingURLString]; + + NSMutableURLRequest *currentlyPlayingRequest = [NSMutableURLRequest requestWithURL:currentlyPlayingURL]; + [currentlyPlayingRequest setValue:[NSString stringWithFormat:@"Bearer %@", self.accessToken] forHTTPHeaderField:@"Authorization"]; + + NSURLSessionDataTask *currentlyPlayingTask = [[NSURLSession sharedSession] dataTaskWithRequest:currentlyPlayingRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 204 || data.length == 0) { + NSDictionary *musicDict = @{ + @"music" : @{ + @"artist" : @"", + @"track" : @"No music playing" + } + }; + + [[BeaMusicManager sharedInstance] updateCurrentlyPlaying:musicDict]; + return; + } + + if (error) { + NSLog(@"[Bea] Error retrieving currently playing song: %@", error.localizedDescription); + return; + } + + if (httpResponse.statusCode == 401) { + NSDictionary *musicDict = @{ + @"music" : @{ + @"artist" : @"", + @"track" : @"Access token expired" + } + }; + + [[BeaMusicManager sharedInstance] updateCurrentlyPlaying:musicDict]; + [self refreshSpotifyAccessToken]; + NSLog(@"[Bea] Error: Access token expired %ld", (long)httpResponse.statusCode); + return; + } + + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]; + + NSString *audioType = jsonResponse[@"currently_playing_type"]; + + NSString *artist; + NSString *artwork; + NSString *isrc; + + if ([audioType isEqual:@"episode"]) { + artist = jsonResponse[@"item"][@"show"][@"publisher"]; + artwork = jsonResponse[@"item"][@"images"][0][@"url"]; + isrc = @""; + } else { + artist = jsonResponse[@"item"][@"artists"][0][@"name"]; + artwork = jsonResponse[@"item"][@"album"][@"images"][0][@"url"]; + isrc = jsonResponse[@"item"][@"external_ids"][@"isrc"]; + } + + NSString *openUrl = jsonResponse[@"item"][@"external_urls"][@"spotify"]; + NSString *provider = @"spotify"; + + NSString *providerId = jsonResponse[@"item"][@"id"]; + NSString *track = jsonResponse[@"item"][@"name"]; + NSString *visibility = @"public"; + + NSDictionary *musicDict = @{ + @"music" : @{ + @"artist" : artist, + @"artwork" : artwork, + @"audioType" : audioType, + @"isrc" : isrc, + @"openUrl" : openUrl, + @"provider" : provider, + @"providerId" : providerId, + @"track" : track, + @"visibility" : visibility + } + }; + + [[BeaMusicManager sharedInstance] updateCurrentlyPlaying:musicDict]; + }]; + + [currentlyPlayingTask resume]; + }); +} +@end diff --git a/Utilities/Music Components/Managers/MusicManager/BeaMusicManager.h b/Utilities/Music Components/Managers/MusicManager/BeaMusicManager.h new file mode 100644 index 0000000..124a59a --- /dev/null +++ b/Utilities/Music Components/Managers/MusicManager/BeaMusicManager.h @@ -0,0 +1,9 @@ +@interface BeaMusicManager : NSObject +@property (nonatomic, strong) NSMutableDictionary *musicDict; +@property (nonatomic, strong) NSString *artist; +@property (nonatomic, strong) NSString *track; +@property (nonatomic, assign) NSInteger playingStatus; ++ (instancetype)sharedInstance; +- (void)updateCurrentlyPlaying:(NSDictionary *)musicDict; +- (void)setMusicVisibility:(NSString *)visibility; +@end \ No newline at end of file diff --git a/Utilities/Music Components/Managers/MusicManager/BeaMusicManager.m b/Utilities/Music Components/Managers/MusicManager/BeaMusicManager.m new file mode 100644 index 0000000..72241be --- /dev/null +++ b/Utilities/Music Components/Managers/MusicManager/BeaMusicManager.m @@ -0,0 +1,45 @@ +#import "BeaMusicManager.h" + +@implementation BeaMusicManager ++ (instancetype)sharedInstance { + static BeaMusicManager *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (void)updateCurrentlyPlaying:(NSDictionary *)musicDict { + if ([self.musicDict isEqual:musicDict]) return; + + self.musicDict = [musicDict mutableCopy]; + + if ([musicDict[@"music"][@"artist"] isEqual:@""] || [musicDict[@"music"][@"track"] isEqual:@""]) { + [self setPlayingStatus:0]; + } else { + [self setPlayingStatus:1]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:@"MusicUpdated" object:nil]; +} + + + +- (void)setMusicVisibility:(NSString *)visibility { + if ([visibility isEqual:@"none"]) { + self.musicDict = nil; + return; + } + + NSMutableDictionary *mutableMusicDict = [self.musicDict[@"music"] mutableCopy]; + [mutableMusicDict setValue:visibility forKey:@"visibility"]; + [self.musicDict setObject:mutableMusicDict forKey:@"music"]; +} + +- (void)resetData { + self.musicDict = nil; + self.artist = nil; + self.track = nil; +} +@end \ No newline at end of file diff --git a/Utilities/Music Components/SpotifyImports.h b/Utilities/Music Components/SpotifyImports.h new file mode 100644 index 0000000..cc39471 --- /dev/null +++ b/Utilities/Music Components/SpotifyImports.h @@ -0,0 +1,4 @@ +#import "Managers/MusicManager/BeaMusicManager.m" +#import "Managers/APIHandler/BeaSpotifyAPIHandler.m" +#import "ViewControllers/SongSearchViewController/BeaSongSearchViewController.m" +#import "ViewControllers/SpotifyViewController/BeaSpotifyViewController.m" \ No newline at end of file diff --git a/Utilities/Music Components/ViewControllers/SongSearchViewController/BeaSongSearchViewController.h b/Utilities/Music Components/ViewControllers/SongSearchViewController/BeaSongSearchViewController.h new file mode 100644 index 0000000..27ad7ee --- /dev/null +++ b/Utilities/Music Components/ViewControllers/SongSearchViewController/BeaSongSearchViewController.h @@ -0,0 +1,7 @@ +@interface BeaSongSearchViewController : UIViewController +@property (nonatomic, strong) UISearchBar *searchBar; +@property (nonatomic, strong) UITableView *tableView; +@property (nonatomic, strong) NSArray *searchResults; +@property (nonatomic, strong) UIView *contentContainer; +@property (nonatomic, strong) NSCache *imageCache; +@end \ No newline at end of file diff --git a/Utilities/Music Components/ViewControllers/SongSearchViewController/BeaSongSearchViewController.m b/Utilities/Music Components/ViewControllers/SongSearchViewController/BeaSongSearchViewController.m new file mode 100644 index 0000000..b8f761a --- /dev/null +++ b/Utilities/Music Components/ViewControllers/SongSearchViewController/BeaSongSearchViewController.m @@ -0,0 +1,229 @@ +#import "BeaSongSearchViewController.h" + +@implementation BeaSongSearchViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + self.imageCache = [[NSCache alloc] init]; + self.imageCache.totalCostLimit = 10; + + self.contentContainer = [[UIView alloc] initWithFrame:CGRectZero]; + self.contentContainer.layer.cornerRadius = 8.0; + self.contentContainer.clipsToBounds = YES; + self.contentContainer.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.contentContainer]; + + self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectZero]; + self.searchBar.delegate = self; + self.searchBar.barTintColor = [UIColor colorWithRed:0.06 green:0.06 blue:0.06 alpha:1.00]; + self.searchBar.placeholder = @"Search song, artist, album..."; + self.searchBar.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.searchBar]; + + self.tableView = [[UITableView alloc] initWithFrame:CGRectZero]; + self.tableView.dataSource = self; + self.tableView.delegate = self; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.tableView.backgroundColor = [UIColor colorWithRed:0.06 green:0.06 blue:0.06 alpha:1.00]; + self.tableView.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.tableView]; + + [NSLayoutConstraint activateConstraints:@[ + [self.contentContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.contentContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.contentContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [self.contentContainer.heightAnchor constraintEqualToAnchor:self.view.heightAnchor multiplier:0.75], + + [self.searchBar.topAnchor constraintEqualToAnchor:self.contentContainer.topAnchor constant:4], + [self.searchBar.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor], + [self.searchBar.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor], + + [self.tableView.topAnchor constraintEqualToAnchor:self.searchBar.bottomAnchor], + [self.tableView.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor], + [self.tableView.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor], + [self.tableView.bottomAnchor constraintEqualToAnchor:self.contentContainer.bottomAnchor] + ]]; +} + +- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { + [self performSearchWithKeyword:searchBar.text]; + [searchBar resignFirstResponder]; +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { + searchBar.text = @""; + [searchBar resignFirstResponder]; + self.searchResults = nil; + [self.tableView reloadData]; +} + +- (void)performSearchWithKeyword:(NSString *)keyword { + NSString *accessToken = [[BeaTokenManager sharedInstance] spotifyAccessToken]; + NSString *query = [keyword stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSString *apiUrl = [NSString stringWithFormat:@"https://api.spotify.com/v1/search?q=%@&type=track&limit=10", query]; + NSURL *url = [NSURL URLWithString:apiUrl]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setValue:[NSString stringWithFormat:@"Bearer %@", accessToken] forHTTPHeaderField:@"Authorization"]; + + NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + NSLog(@"[Bea] Error performing search: %@", error); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode != 200) { + NSLog(@"[Bea] Search request failed with status code %ld", (long)httpResponse.statusCode); + return; + } + + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + + NSArray *tracks = json[@"tracks"][@"items"]; + NSMutableArray *results = [NSMutableArray array]; + + for (NSDictionary *track in tracks) { + NSString *artist = track[@"album"][@"artists"][0][@"name"]; + NSString *artwork = track[@"album"][@"images"][0][@"url"]; + NSString *isrc = track[@"external_ids"][@"isrc"]; + NSString *audioType = track[@"type"]; + NSString *openUrl = track[@"external_urls"][@"spotify"]; + NSString *provider = @"spotify"; + NSString *providerId = track[@"id"]; + NSString *trackName = track[@"name"]; + NSString *visibility = @"public"; + + NSDictionary *dict = @{ + @"music" : @{ + @"artist" : artist, + @"artwork" : artwork, + @"audioType" : audioType, + @"isrc" : isrc, + @"openUrl" : openUrl, + @"provider" : provider, + @"providerId" : providerId, + @"track" : trackName, + @"visibility" : visibility + } + }; + + [results addObject:dict]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + self.searchResults = results; + [self.tableView reloadData]; + }); + }]; + + [dataTask resume]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.searchResults.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SearchResultCell"]; + + UIImageView *artworkImageView; + UILabel *trackLabel; + UILabel *artistLabel; + + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"SearchResultCell"]; + + cell.contentView.backgroundColor = [UIColor colorWithRed:0.06 green:0.06 blue:0.06 alpha:1.00]; + + artworkImageView = [[UIImageView alloc] init]; + artworkImageView.translatesAutoresizingMaskIntoConstraints = NO; + artworkImageView.layer.cornerRadius = 4.0; + artworkImageView.clipsToBounds = YES; + artworkImageView.tag = 1; + [cell.contentView addSubview:artworkImageView]; + + trackLabel = [[UILabel alloc] init]; + trackLabel.translatesAutoresizingMaskIntoConstraints = NO; + trackLabel.font = [UIFont fontWithName:@"Inter" size:17]; + trackLabel.tag = 2; + [cell.contentView addSubview:trackLabel]; + + artistLabel = [[UILabel alloc] init]; + artistLabel.translatesAutoresizingMaskIntoConstraints = NO; + artistLabel.font = [UIFont systemFontOfSize:13]; + artistLabel.tag = 3; + [cell.contentView addSubview:artistLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [artworkImageView.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:10], + [artworkImageView.topAnchor constraintEqualToAnchor:cell.contentView.topAnchor constant:10], + [artworkImageView.bottomAnchor constraintEqualToAnchor:cell.contentView.bottomAnchor constant:-10], + [artworkImageView.widthAnchor constraintEqualToConstant:58], + [artworkImageView.heightAnchor constraintEqualToConstant:58], + + [trackLabel.leadingAnchor constraintEqualToAnchor:artworkImageView.trailingAnchor constant:10], + [trackLabel.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-10], + [trackLabel.topAnchor constraintEqualToAnchor:cell.contentView.topAnchor constant:20], + + [artistLabel.leadingAnchor constraintEqualToAnchor:artworkImageView.trailingAnchor constant:10], + [artistLabel.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-10], + [artistLabel.topAnchor constraintEqualToAnchor:trackLabel.bottomAnchor constant:5], + [artistLabel.bottomAnchor constraintEqualToAnchor:cell.contentView.bottomAnchor constant:-20], + ]]; + } else { + artworkImageView = (UIImageView *)[cell.contentView viewWithTag:1]; + trackLabel = (UILabel *)[cell.contentView viewWithTag:2]; + artistLabel = (UILabel *)[cell.contentView viewWithTag:3]; + } + + NSDictionary *searchResult = self.searchResults[indexPath.row][@"music"]; + + NSString *artist = searchResult[@"artist"]; + NSString *track = searchResult[@"track"]; + + trackLabel.text = track; + artistLabel.text = artist; + + NSString *imageUrlString = searchResult[@"artwork"]; + + // check if the image is already cached + UIImage *cachedImage = [self.imageCache objectForKey:imageUrlString]; + if (cachedImage) { + artworkImageView.image = cachedImage; + } else { + artworkImageView.image = nil; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *artworkImageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrlString]]; + UIImage *artworkImage = [UIImage imageWithData:artworkImageData]; + + // cache the downloaded image + if (artworkImage) { + [self.imageCache setObject:artworkImage forKey:imageUrlString]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + // ensure that the cell is visible and only then update it + if ([tableView.indexPathsForVisibleRows containsObject:indexPath]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView transitionWithView:artworkImageView duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + artworkImageView.image = artworkImage; + } completion:nil]; + }); + } + }); + }); + } + + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + NSDictionary *results = self.searchResults[indexPath.row]; + [[BeaMusicManager sharedInstance] updateCurrentlyPlaying:results]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"StopUpdatingCurrentlyPlaying" object:nil]; + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + [self dismissViewControllerAnimated:YES completion:nil]; +} +@end \ No newline at end of file diff --git a/Utilities/Music Components/ViewControllers/SpotifyViewController/BeaSpotifyViewController.h b/Utilities/Music Components/ViewControllers/SpotifyViewController/BeaSpotifyViewController.h new file mode 100644 index 0000000..44104ea --- /dev/null +++ b/Utilities/Music Components/ViewControllers/SpotifyViewController/BeaSpotifyViewController.h @@ -0,0 +1,22 @@ +@interface BeaSpotifyViewController : UIViewController +@property (nonatomic, strong) UIView *contentContainer; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) NSDictionary *musicDict; +@property (nonatomic, strong) UIImageView *artworkImageView; +@property (nonatomic, strong) UILabel *trackLabel; +@property (nonatomic, strong) UILabel *artistLabel; +@property (nonatomic, strong) NSArray *visibilityData; +@property (nonatomic, strong) UIView *visibilityView; +@property (nonatomic, strong) UIImageView *visibilityImageView; +@property (nonatomic, strong) UILabel *visibilityLabel; +@property (nonatomic, strong) UILabel *visibilitySubtitle; +@property (nonatomic, strong) UIImpactFeedbackGenerator *generator; + +@property (nonatomic, strong) UIView *searchActionView; +@property (nonatomic, strong) UIImageView *searchIconImageView; +@property (nonatomic, strong) UILabel *searchLabel; +@property (nonatomic, strong) UILabel *searchSubtitle; + +@property (nonatomic, strong) BeaSongSearchViewController *songSearchViewController; +- (void)updateArtworkView; +@end \ No newline at end of file diff --git a/Utilities/Music Components/ViewControllers/SpotifyViewController/BeaSpotifyViewController.m b/Utilities/Music Components/ViewControllers/SpotifyViewController/BeaSpotifyViewController.m new file mode 100644 index 0000000..dc60e64 --- /dev/null +++ b/Utilities/Music Components/ViewControllers/SpotifyViewController/BeaSpotifyViewController.m @@ -0,0 +1,254 @@ +#import "BeaSpotifyViewController.h" + +@implementation BeaSpotifyViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + self.visibilityData = @[ + @{@"label": @"Shared", @"subtitle": @"Visible to your friends", @"image": @"person.2.fill", @"value" : @"public"}, + @{@"label": @"Private", @"subtitle": @"Only visible to you", @"image": @"lock.fill", @"value" : @"private"}, + @{@"label": @"Disabled", @"subtitle": @"Don't add what you're listening to", @"image": @"play.slash", @"value" : @"none"} + ]; + + self.songSearchViewController = [[BeaSongSearchViewController alloc] init]; + self.generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + + [self setupArtworkView]; + [self setupVisibilityView]; + [self setupSearchActionView]; + + [NSLayoutConstraint activateConstraints:@[ + [self.contentContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.contentContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.contentContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [self.contentContainer.heightAnchor constraintEqualToAnchor:self.view.heightAnchor multiplier:0.7], + + [self.titleLabel.topAnchor constraintEqualToAnchor:self.contentContainer.topAnchor constant:22], + [self.titleLabel.centerXAnchor constraintEqualToAnchor:self.contentContainer.centerXAnchor], + + [self.artworkImageView.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:18], + [self.artworkImageView.centerXAnchor constraintEqualToAnchor:self.contentContainer.centerXAnchor], + [self.artworkImageView.widthAnchor constraintEqualToConstant:134], + [self.artworkImageView.heightAnchor constraintEqualToConstant:134], + + [self.trackLabel.topAnchor constraintEqualToAnchor:self.artworkImageView.bottomAnchor constant:18], + [self.trackLabel.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.contentContainer.leadingAnchor constant:22], + [self.trackLabel.trailingAnchor constraintLessThanOrEqualToAnchor:self.contentContainer.trailingAnchor constant:-22], + [self.trackLabel.centerXAnchor constraintEqualToAnchor:self.contentContainer.centerXAnchor], + [self.trackLabel.widthAnchor constraintLessThanOrEqualToAnchor:self.contentContainer.widthAnchor constant:-44], + + [self.artistLabel.topAnchor constraintEqualToAnchor:self.trackLabel.bottomAnchor constant:2], + [self.artistLabel.centerXAnchor constraintEqualToAnchor:self.contentContainer.centerXAnchor], + + [self.visibilityView.topAnchor constraintEqualToAnchor:self.artistLabel.bottomAnchor constant:18], + [self.visibilityView.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor constant:18], + [self.visibilityView.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor constant:-18], + [self.visibilityView.widthAnchor constraintEqualToAnchor:self.contentContainer.widthAnchor constant:-36], + [self.visibilityView.heightAnchor constraintEqualToConstant:55], + + [self.visibilityImageView.centerYAnchor constraintEqualToAnchor:self.visibilityView.centerYAnchor], + [self.visibilityImageView.leadingAnchor constraintEqualToAnchor:self.visibilityView.leadingAnchor constant:14], + [self.visibilityImageView.widthAnchor constraintEqualToConstant:24], + [self.visibilityImageView.heightAnchor constraintEqualToConstant:24], + + [self.visibilityLabel.topAnchor constraintEqualToAnchor:self.visibilityView.topAnchor constant:10], + [self.visibilityLabel.leadingAnchor constraintEqualToAnchor:self.visibilityImageView.trailingAnchor constant:9], + + [self.visibilitySubtitle.topAnchor constraintEqualToAnchor:self.visibilityLabel.bottomAnchor], + [self.visibilitySubtitle.leadingAnchor constraintEqualToAnchor:self.visibilityLabel.leadingAnchor], + + [self.searchActionView.topAnchor constraintEqualToAnchor:self.visibilityView.bottomAnchor constant:12], + [self.searchActionView.leadingAnchor constraintEqualToAnchor:self.contentContainer.leadingAnchor constant:18], + [self.searchActionView.trailingAnchor constraintEqualToAnchor:self.contentContainer.trailingAnchor constant:-18], + [self.searchActionView.widthAnchor constraintEqualToAnchor:self.contentContainer.widthAnchor constant:-36], + [self.searchActionView.heightAnchor constraintEqualToAnchor:self.visibilityView.heightAnchor], + + [self.searchIconImageView.centerYAnchor constraintEqualToAnchor:self.searchActionView.centerYAnchor], + [self.searchIconImageView.leadingAnchor constraintEqualToAnchor:self.searchActionView.leadingAnchor constant:14], + [self.searchIconImageView.widthAnchor constraintEqualToConstant:24], + [self.searchIconImageView.heightAnchor constraintEqualToConstant:24], + + [self.searchLabel.topAnchor constraintEqualToAnchor:self.searchActionView.topAnchor constant:10], + [self.searchLabel.leadingAnchor constraintEqualToAnchor:self.visibilityLabel.leadingAnchor], + + [self.searchSubtitle.topAnchor constraintEqualToAnchor:self.searchLabel.bottomAnchor], + [self.searchSubtitle.leadingAnchor constraintEqualToAnchor:self.searchLabel.leadingAnchor] + ]]; + + [self updateMusicData]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateMusicData) name:@"MusicUpdated" object:nil]; +} + +- (void)updateMusicData { + self.musicDict = [[BeaMusicManager sharedInstance] musicDict]; + [self updateArtworkView]; +} + +- (void)setupArtworkView { + self.contentContainer = [[UIView alloc] initWithFrame:CGRectZero]; + self.contentContainer.layer.cornerRadius = 8.0; + self.contentContainer.clipsToBounds = YES; + self.contentContainer.backgroundColor = [UIColor colorWithRed:0.06 green:0.06 blue:0.06 alpha:0.97]; + self.contentContainer.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.contentContainer]; + + self.titleLabel = [[UILabel alloc] init]; + self.titleLabel.text = @"Currently playing"; + self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.titleLabel.font = [UIFont fontWithName:@"Inter" size:22]; + [self.contentContainer addSubview:self.titleLabel]; + + self.artworkImageView = [[UIImageView alloc] init]; + self.artworkImageView.layer.cornerRadius = 4.0; + self.artworkImageView.clipsToBounds = YES; + self.artworkImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.artworkImageView]; + + self.trackLabel = [[UILabel alloc] init]; + self.trackLabel.font = [UIFont fontWithName:@"Inter" size:20]; + self.trackLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.trackLabel]; + + self.artistLabel = [[UILabel alloc] init]; + self.artistLabel.font = [UIFont fontWithName:@"Inter" size:12]; + self.artistLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.artistLabel]; +} + +- (void)setupSearchActionView { + self.searchActionView = [[UIView alloc] init]; + self.searchActionView.backgroundColor = [UIColor whiteColor]; + self.searchActionView.layer.cornerRadius = 12.0; + self.searchActionView.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.searchActionView]; + + self.searchIconImageView = [[UIImageView alloc] init]; + self.searchIconImageView.contentMode = UIViewContentModeScaleAspectFit; + self.searchIconImageView.clipsToBounds = YES; + self.searchIconImageView.image = [[UIImage systemImageNamed:@"magnifyingglass"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.searchIconImageView.tintColor = [UIColor blackColor]; + self.searchIconImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.searchActionView addSubview:self.searchIconImageView]; + + self.searchLabel = [[UILabel alloc] init]; + self.searchLabel.text = @"Search"; + self.searchLabel.font = [UIFont fontWithName:@"Inter" size:16]; + self.searchLabel.textColor = [UIColor blackColor]; + self.searchLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.searchActionView addSubview:self.searchLabel]; + + self.searchSubtitle = [[UILabel alloc] init]; + self.searchSubtitle.text = @"Search for another song"; + self.searchSubtitle.font = [UIFont systemFontOfSize:12]; + self.searchSubtitle.textColor = [UIColor blackColor]; + self.searchSubtitle.translatesAutoresizingMaskIntoConstraints = NO; + [self.searchActionView addSubview:self.searchSubtitle]; + + UITapGestureRecognizer *searchGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(searchViewTapped)]; + [self.searchActionView addGestureRecognizer:searchGestureRecognizer]; +} + +- (void)setupVisibilityView { + self.visibilityView = [[UIView alloc] init]; + self.visibilityView.backgroundColor = [UIColor whiteColor]; + self.visibilityView.layer.cornerRadius = 12.0; + self.visibilityView.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentContainer addSubview:self.visibilityView]; + + self.visibilityImageView = [[UIImageView alloc] init]; + self.visibilityImageView.contentMode = UIViewContentModeScaleAspectFit; + self.visibilityImageView.clipsToBounds = YES; + + UIImage *visibilityImage = [[UIImage systemImageNamed:self.visibilityData[0][@"image"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.visibilityImageView.image = visibilityImage; + + self.visibilityImageView.tintColor = [UIColor blackColor]; + self.visibilityImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.visibilityView addSubview:self.visibilityImageView]; + + self.visibilityLabel = [[UILabel alloc] init]; + self.visibilityLabel.text = self.visibilityData[0][@"label"]; + self.visibilityLabel.font = [UIFont fontWithName:@"Inter" size:16]; + self.visibilityLabel.textColor = [UIColor blackColor]; + self.visibilityLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.visibilityView addSubview:self.visibilityLabel]; + + self.visibilitySubtitle = [[UILabel alloc] init]; + self.visibilitySubtitle.text = self.visibilityData[0][@"subtitle"]; + self.visibilitySubtitle.font = [UIFont systemFontOfSize:12]; + self.visibilitySubtitle.textColor = [UIColor blackColor]; + self.visibilitySubtitle.translatesAutoresizingMaskIntoConstraints = NO; + [self.visibilityView addSubview:self.visibilitySubtitle]; + + UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(visibilityViewTapped)]; + [self.visibilityView addGestureRecognizer:tapRecognizer]; +} + +- (void)searchViewTapped { + [UIView animateWithDuration:0.15 animations:^{ + self.searchActionView.transform = CGAffineTransformMakeScale(0.98, 0.98); + } completion:^(BOOL finished) { + [UIView animateWithDuration:0.15 animations:^{ + self.searchActionView.transform = CGAffineTransformIdentity; + }]; + }]; + + [self.generator prepare]; + [self.generator impactOccurred]; + + [self presentViewController:self.songSearchViewController animated:YES completion:nil]; +} + +- (void)updateArtworkView { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *artworkImageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.musicDict[@"music"][@"artwork"]]]; + UIImage *artworkImage = [UIImage imageWithData:artworkImageData]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView transitionWithView:self.artworkImageView duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + self.artworkImageView.image = artworkImage; + } completion:nil]; + }); + }); + + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView transitionWithView:self.artistLabel duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + self.artistLabel.text = self.musicDict[@"music"][@"artist"]; + } completion:nil]; + + [UIView transitionWithView:self.trackLabel duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + self.trackLabel.text = self.musicDict[@"music"][@"track"]; + } completion:nil]; + }); +} + +- (void)visibilityViewTapped { + static NSInteger currentIndex = 1; + NSDictionary *currentItem = self.visibilityData[currentIndex]; + + [UIView animateWithDuration:0.15 animations:^{ + self.visibilityView.transform = CGAffineTransformMakeScale(0.98, 0.98); + } completion:^(BOOL finished) { + [UIView animateWithDuration:0.15 animations:^{ + self.visibilityView.transform = CGAffineTransformIdentity; + }]; + }]; + + [self.generator prepare]; + [self.generator impactOccurred]; + + self.visibilityLabel.text = currentItem[@"label"]; + self.visibilitySubtitle.text = currentItem[@"subtitle"]; + self.visibilityImageView.image = [UIImage systemImageNamed:currentItem[@"image"]]; + + // update the object that holds the current playing info + [[BeaMusicManager sharedInstance] setMusicVisibility:currentItem[@"value"]]; + + currentIndex = (currentIndex + 1) % self.visibilityData.count; +} + +- (void)viewWillDisappear:(BOOL)animated { + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"MusicUpdated" object:nil]; +} +@end \ No newline at end of file diff --git a/Utilities/UploadTask/BeaUploadTask.h b/Utilities/UploadTask/BeaUploadTask.h index eb1ccc9..b336a13 100644 --- a/Utilities/UploadTask/BeaUploadTask.h +++ b/Utilities/UploadTask/BeaUploadTask.h @@ -1,3 +1,6 @@ +#import +#import + @interface BeaUploadTask : NSObject - (instancetype)initWithData:(NSDictionary *)data frontImage:(UIImage *)frontImage backImage:(UIImage *)backImage; @property (nonatomic, strong) NSString *authorizationKey; @@ -7,6 +10,7 @@ @property (nonatomic, strong) NSString *takenAt; @property (nonatomic, strong) NSString *lastMoment; @property (nonatomic, strong) NSString *region; +@property (nonatomic, strong) NSDictionary *headers; - (void)uploadBeRealWithCompletion:(void (^)(BOOL success, NSError *error))completion; - (void)makePUTRequestWithData:(NSDictionary *)data completion:(void (^)(BOOL success, NSError *error))completion; - (void)putPhotoWithURL:(NSURL *)url headers:(NSDictionary *)headers imageData:(NSData *)imageData completion:(void (^)(BOOL success))completion; diff --git a/Utilities/UploadTask/BeaUploadTask.m b/Utilities/UploadTask/BeaUploadTask.m index a02b193..c78d506 100644 --- a/Utilities/UploadTask/BeaUploadTask.m +++ b/Utilities/UploadTask/BeaUploadTask.m @@ -38,7 +38,19 @@ - (instancetype)initWithData:(NSDictionary *)data frontImage:(UIImage *)frontIma self = [super init]; if (self) { self.userDictionary = data; - self.authorizationKey = data[@"authorization"]; + self.authorizationKey = [[BeaTokenManager sharedInstance] BRAccessToken]; + + self.headers = @{ + @"authorization": self.authorizationKey, + @"accept": @"*/*", + @"bereal-platform": @"iOS", + @"bereal-os-version": @"14.7.1", + @"accept-Language": @"en-US;q=1.0", + @"user-Agent": @"BeReal/1.7.0 (AlexisBarreyat.BeReal; build:11001; iOS 14.7.1) 1.0.0/BRApiKit", + @"bereal-app-language": @"en-US", + @"bereal-device-language": @"en", + @"bereal-app-version" : @"1.7.0-(11001)" + }; UIImage *resizedFrontImage = [self resizeImage:frontImage toSize:CGSizeMake(1500, 2000)]; UIImage *resizedBackImage = [self resizeImage:backImage toSize:CGSizeMake(1500, 2000)]; @@ -62,14 +74,10 @@ - (void)uploadBeRealWithCompletion:(void (^)(BOOL success, NSError *error))compl NSURL *uploadRequestURL = [NSURL URLWithString:@"https://mobile.bereal.com/api/content/posts/upload-url?mimeType=image/webp"]; NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:uploadRequestURL]; [uploadRequest setHTTPMethod:@"GET"]; - [uploadRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; - [uploadRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; - [uploadRequest setValue:@"iOS" forHTTPHeaderField:@"bereal-platform"]; - [uploadRequest setValue:@"14.7.1" forHTTPHeaderField:@"bereal-os-version"]; - [uploadRequest setValue:@"en-US;q=1.0" forHTTPHeaderField:@"Accept-Language"]; - [uploadRequest setValue:@"BeReal/0.28.2 (AlexisBarreyat.BeReal; build:8425; iOS 14.7.1) 1.0.0/BRApiKit" forHTTPHeaderField:@"User-Agent"]; - [uploadRequest setValue:@"en-US" forHTTPHeaderField:@"bereal-app-language"]; - [uploadRequest setValue:@"en" forHTTPHeaderField:@"bereal-device-language"]; + + [self.headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + [uploadRequest setValue:value forHTTPHeaderField:field]; + }]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *uploadRequestTask = [session dataTaskWithRequest:uploadRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *getError) { @@ -121,7 +129,6 @@ - (void)makePUTRequestWithData:(NSDictionary *)response completion:(void (^)(BOO } dispatch_group_leave(group); }]; - dispatch_group_notify(group, dispatch_get_main_queue(), ^{ [self postBeRealWithFrontPath:frontImageUploadPath backPath:backImageUploadPath frontBucket:frontImageBucket backBucket:backImageBucket completion:completion]; @@ -166,7 +173,7 @@ - (void)postBeRealWithFrontPath:(NSString *)frontPath backPath:(NSString *)backP NSDate *currentDate = [NSDate date]; self.takenAt = [dateFormatter stringFromDate:currentDate]; } - + NSMutableDictionary *payload = [NSMutableDictionary dictionaryWithDictionary:@{ @"visibility": @[@"friends"], @"isLate": @([self.userDictionary[@"isLate"] boolValue]), @@ -186,6 +193,10 @@ - (void)postBeRealWithFrontPath:(NSString *)frontPath backPath:(NSString *)backP } }]; + if (self.userDictionary[@"music"]) { + [payload setObject:self.userDictionary[@"music"] forKey:@"music"]; + } + if (self.userDictionary[@"longitude"] && self.userDictionary[@"latitude"]) { NSDictionary *locationDict = @{ @"latitude": self.userDictionary[@"latitude"], @@ -204,16 +215,18 @@ - (void)postBeRealWithFrontPath:(NSString *)frontPath backPath:(NSString *)backP NSMutableURLRequest *postBeRealRequest = [NSMutableURLRequest requestWithURL:postBeRealURL]; [postBeRealRequest setHTTPMethod:@"POST"]; + [postBeRealRequest setValue:@"application/json" forHTTPHeaderField:@"content-type"]; - [postBeRealRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; - [postBeRealRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; - [postBeRealRequest setValue:@"en-US" forHTTPHeaderField:@"bereal-app-language"]; + [self.headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + [postBeRealRequest setValue:value forHTTPHeaderField:field]; + }]; NSURLSessionUploadTask *uploadTask = [[NSURLSession sharedSession] uploadTaskWithRequest:postBeRealRequest fromData:payloadJSON completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (error || httpResponse.statusCode > 299) { NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *message = [NSString stringWithFormat:@"1 - Uploading failed: %@: %@", responseDictionary[@"statusCode"], responseDictionary[@"errorKey"]]; + //NSString *message = [NSString stringWithFormat:@"1 - Uploading failed: %@: %@", responseDictionary[@"statusCode"], responseDictionary[@"errorKey"]]; + NSString *message = [NSString stringWithFormat:@"%@, %@, %@", responseDictionary[@"error"], responseDictionary[@"message"], responseDictionary[@"errorKey"]]; [self handleErrorWithTitle:@"API Error" message:message completion:completion]; return; } @@ -231,11 +244,10 @@ - (void)getRegion { NSURL *meURL = [NSURL URLWithString:@"https://mobile.bereal.com/api/person/me"]; NSMutableURLRequest *regionRequest = [NSMutableURLRequest requestWithURL:meURL]; - - [regionRequest setHTTPMethod:@"GET"]; - [regionRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; - [regionRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; - [regionRequest setValue:@"BeReal/0.28.2 (AlexisBarreyat.BeReal; build:8425; iOS 14.7.1) 1.0.0/BRApiKit" forHTTPHeaderField:@"User-Agent"]; + + [self.headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + [regionRequest setValue:value forHTTPHeaderField:field]; + }]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *regionRequestTask = [session dataTaskWithRequest:regionRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { @@ -258,9 +270,10 @@ - (void)getLastMoment { NSMutableURLRequest *lastMomentRequest = [NSMutableURLRequest requestWithURL:lastMomentURL]; [lastMomentRequest setHTTPMethod:@"GET"]; - [lastMomentRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; - [lastMomentRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; - [lastMomentRequest setValue:@"BeReal/0.28.2 (AlexisBarreyat.BeReal; build:8425; iOS 14.7.1) 1.0.0/BRApiKit" forHTTPHeaderField:@"User-Agent"]; + + [self.headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + [lastMomentRequest setValue:value forHTTPHeaderField:field]; + }]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *lastMomentRequestTask = [session dataTaskWithRequest:lastMomentRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { diff --git a/Utilities/InfoViewController/BeaInfoViewController.h b/Utilities/ViewControllers/InfoViewController/BeaInfoViewController.h similarity index 91% rename from Utilities/InfoViewController/BeaInfoViewController.h rename to Utilities/ViewControllers/InfoViewController/BeaInfoViewController.h index 3ccf4c5..63db400 100644 --- a/Utilities/InfoViewController/BeaInfoViewController.h +++ b/Utilities/ViewControllers/InfoViewController/BeaInfoViewController.h @@ -6,4 +6,4 @@ @property (nonatomic, strong) UILabel *versionLabel; @end -#define TWEAK_VERSION @"1.2.2" \ No newline at end of file +#define TWEAK_VERSION @"1.3" \ No newline at end of file diff --git a/Utilities/InfoViewController/BeaInfoViewController.m b/Utilities/ViewControllers/InfoViewController/BeaInfoViewController.m similarity index 100% rename from Utilities/InfoViewController/BeaInfoViewController.m rename to Utilities/ViewControllers/InfoViewController/BeaInfoViewController.m diff --git a/Utilities/LocationViewController/BeaLocationViewController.h b/Utilities/ViewControllers/LocationViewController/BeaLocationViewController.h similarity index 100% rename from Utilities/LocationViewController/BeaLocationViewController.h rename to Utilities/ViewControllers/LocationViewController/BeaLocationViewController.h diff --git a/Utilities/LocationViewController/BeaLocationViewController.m b/Utilities/ViewControllers/LocationViewController/BeaLocationViewController.m similarity index 95% rename from Utilities/LocationViewController/BeaLocationViewController.m rename to Utilities/ViewControllers/LocationViewController/BeaLocationViewController.m index be1b57a..f901cf0 100644 --- a/Utilities/LocationViewController/BeaLocationViewController.m +++ b/Utilities/ViewControllers/LocationViewController/BeaLocationViewController.m @@ -6,10 +6,13 @@ - (void)viewDidLoad { self.mapView = [[MKMapView alloc] initWithFrame:self.view.frame]; self.mapView.delegate = self; + + UIEdgeInsets mapInsets = self.mapView.layoutMargins; + mapInsets.bottom = 30; + self.mapView.layoutMargins = mapInsets; [self.view addSubview:self.mapView]; self.locationManager = [CLLocationManager performSelector:@selector(sharedManager)]; - //self.locationManager = [[CLLocationManager alloc] init]; self.locationManager.delegate = self; self.doneButton = [UIButton buttonWithType:UIButtonTypeSystem]; @@ -40,7 +43,7 @@ - (void)viewDidLoad { [self.doneButton.heightAnchor constraintEqualToConstant:44.0], [self.userLocationButton.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:20.0], - [self.userLocationButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0], + [self.userLocationButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-8.0], [self.userLocationButton.widthAnchor constraintEqualToConstant:44.0], [self.userLocationButton.heightAnchor constraintEqualToConstant:44.0] ]]; diff --git a/Utilities/UploadViewController/BeaUploadViewController.h b/Utilities/ViewControllers/UploadViewController/BeaUploadViewController.h similarity index 75% rename from Utilities/UploadViewController/BeaUploadViewController.h rename to Utilities/ViewControllers/UploadViewController/BeaUploadViewController.h index d58fa2b..2956bfb 100644 --- a/Utilities/UploadViewController/BeaUploadViewController.h +++ b/Utilities/ViewControllers/UploadViewController/BeaUploadViewController.h @@ -1,13 +1,14 @@ #import #import -#import "StatusView/BeaStatusView.m" -#import "../InfoViewController/BeaInfoViewController.m" -#import "../UploadTask/BeaUploadTask.m" +#import "../../Music Components/SpotifyImports.h" #import "../LocationViewController/BeaLocationViewController.m" +#import "../InfoViewController/BeaInfoViewController.m" +#import "../../UploadTask/BeaUploadTask.m" +#import "../../Views/StatusView/BeaStatusView.m" +#import "../../Views/SpotifyMusicView/BeaSpotifyMusicView.m" @interface BeaUploadViewController : UIViewController @property (nonatomic, strong) BeaLocationViewController *locationVC; -@property (nonatomic, strong) NSString *authorizationKey; @property (nonatomic, strong) UIImageView *frontImageView; @property (nonatomic, strong) UIImageView *backImageView; @property (nonatomic, strong) UILabel *frontTextLabel; @@ -30,6 +31,9 @@ @property (nonatomic, strong) UISwitch *isLateSwitch; @property (nonatomic, strong) UILabel *isLateLabel; @property (nonatomic, assign) BOOL isLate; -@property (nonatomic, strong) UIButton *infoButton; -@property (nonatomic, strong) UIImageView *infoButtonImageView; +@property (nonatomic, strong) UIButton *dropdownButton; +@property (nonatomic, strong) UIImageView *dropdownImageView; +@property (nonatomic, strong) NSDictionary *musicDict; +@property (nonatomic, strong) BeaSpotifyViewController *spotifyViewController; +@property (nonatomic, strong) BeaSpotifyMusicView *spotifyMusicView; @end \ No newline at end of file diff --git a/Utilities/UploadViewController/BeaUploadViewController.m b/Utilities/ViewControllers/UploadViewController/BeaUploadViewController.m similarity index 83% rename from Utilities/UploadViewController/BeaUploadViewController.m rename to Utilities/ViewControllers/UploadViewController/BeaUploadViewController.m index 1dc98d0..99af35f 100644 --- a/Utilities/UploadViewController/BeaUploadViewController.m +++ b/Utilities/ViewControllers/UploadViewController/BeaUploadViewController.m @@ -1,12 +1,11 @@ #import "BeaUploadViewController.h" +#import @implementation BeaUploadViewController -- (instancetype)initWithAuthorization:(NSString *)authKey { +- (instancetype)init { self = [super init]; if (self) { - if (!authKey) return nil; - self.authorizationKey = authKey; self.locationVC = [[BeaLocationViewController alloc] init]; self.locationVC.delegate = self; } @@ -25,7 +24,12 @@ - (void)viewDidLoad { UIImage *beFakeLogo = [UIImage imageNamed:@"BeFake.png" inBundle:bundle compatibleWithTraitCollection:nil]; #endif + self.view.backgroundColor = [UIColor blackColor]; + self.spotifyViewController = [[BeaSpotifyViewController alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(musicManagerDidUpdateMusic) name:@"MusicUpdated" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showMusicViewController) name:@"openSpotifyViewController" object:nil]; self.titleImageView = [[UIImageView alloc] initWithImage:beFakeLogo]; self.titleImageView.contentMode = UIViewContentModeScaleAspectFit; @@ -43,22 +47,11 @@ - (void)viewDidLoad { self.backButtonImageView.translatesAutoresizingMaskIntoConstraints = NO; [self.backButton addSubview:self.backButtonImageView]; - self.infoButton = [UIButton buttonWithType:UIButtonTypeCustom]; - self.infoButton.translatesAutoresizingMaskIntoConstraints = NO; - [self.infoButton addTarget:self action:@selector(infoButtonTapped) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:self.infoButton]; - - UIImage *infoButtonImage = [[UIImage systemImageNamed:@"info.circle.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - self.infoButtonImageView = [[UIImageView alloc] initWithImage:infoButtonImage]; - self.infoButtonImageView.tintColor = [UIColor whiteColor]; - self.infoButtonImageView.translatesAutoresizingMaskIntoConstraints = NO; - [self.infoButton addSubview:self.infoButtonImageView]; - self.statusView = [[BeaStatusView alloc] initWithFrame:CGRectZero]; [self.view addSubview:self.statusView]; self.statusView.translatesAutoresizingMaskIntoConstraints = NO; - self.frontImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + self.frontImageView = [[UIImageView alloc] init]; self.frontImageView.backgroundColor = [UIColor blackColor]; self.frontImageView.layer.borderWidth = 1.8; self.frontImageView.layer.cornerRadius = 8.0; @@ -73,14 +66,14 @@ - (void)viewDidLoad { [self.view addSubview:self.frontImageView]; - self.frontTextLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.frontTextLabel = [[UILabel alloc] init]; self.frontTextLabel.font = [UIFont fontWithName:@"Inter" size:14]; self.frontTextLabel.text = @"Front image"; self.frontTextLabel.textAlignment = NSTextAlignmentCenter; self.frontTextLabel.translatesAutoresizingMaskIntoConstraints = NO; [self.frontImageView addSubview:self.frontTextLabel]; - self.backImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + self.backImageView = [[UIImageView alloc] init]; self.backImageView.backgroundColor = [UIColor blackColor]; self.backImageView.layer.borderWidth = 1.8; self.backImageView.layer.cornerRadius = 8.0; @@ -144,7 +137,7 @@ - (void)viewDidLoad { self.actionButton.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:self.actionButton]; - self.locationLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.locationLabel = [[UILabel alloc] init]; self.locationLabel.font = [UIFont fontWithName:@"Inter" size:22]; self.locationLabel.text = @"Location"; self.locationLabel.translatesAutoresizingMaskIntoConstraints = NO; @@ -168,22 +161,61 @@ - (void)viewDidLoad { [self.isLateSwitch setOn:NO animated:NO]; [self.view addSubview:self.isLateSwitch]; - self.isLateLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.isLateLabel = [[UILabel alloc] init]; self.isLateLabel.font = [UIFont fontWithName:@"Inter" size:22]; self.isLateLabel.text = @"Post late"; self.isLateLabel.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:self.isLateLabel]; + self.spotifyMusicView = [[BeaSpotifyMusicView alloc] init]; + [self.view addSubview:self.spotifyMusicView]; + + self.dropdownButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.dropdownButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.dropdownButton]; + + UIImage *dotImage = [[UIImage systemImageNamed:@"ellipsis"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + + self.dropdownImageView = [[UIImageView alloc] initWithImage:dotImage]; + self.dropdownImageView.translatesAutoresizingMaskIntoConstraints = NO; + self.dropdownImageView.contentMode = UIViewContentModeScaleAspectFit; + [self.dropdownImageView setTintColor:[UIColor whiteColor]]; + [self.dropdownButton addSubview:self.dropdownImageView]; + + NSMutableArray *actions = [[NSMutableArray alloc] init]; + [actions addObject:[UIAction actionWithTitle:@"Show Information" image:[UIImage systemImageNamed:@"info.circle.fill"] identifier:nil handler:^(UIAction * action) { + BeaInfoViewController *infoViewController = [[BeaInfoViewController alloc] init]; + [self presentViewController:infoViewController animated:YES completion:nil]; + }]]; + + NSString *donationImage; + if (@available(iOS 16.0, *)) { + donationImage = @"mug.fill"; + } else { + donationImage = @"dollarsign.circle.fill"; + } + + [actions addObject:[UIAction actionWithTitle:@"Buy me a ☕" image:[UIImage systemImageNamed:donationImage] identifier:nil handler:^(UIAction * action) { + NSURL *kofiURL = [NSURL URLWithString:@"https://ko-fi.com/yandevelop"]; + if ([[UIApplication sharedApplication] canOpenURL:kofiURL]) { + [[UIApplication sharedApplication] openURL:kofiURL options:@{} completionHandler:nil]; + } + }]]; + + UIMenu *menu = [UIMenu menuWithChildren:actions]; + [self.dropdownButton setShowsMenuAsPrimaryAction:true]; + [self.dropdownButton setMenu:menu]; + [NSLayoutConstraint activateConstraints:@[ [self.frontImageView.centerXAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:2 + self.view.frame.size.width / 4], - [self.frontImageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:120], + [self.frontImageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:110], [self.frontImageView.widthAnchor constraintEqualToConstant:150], [self.frontImageView.heightAnchor constraintEqualToConstant:200], [self.frontTextLabel.centerXAnchor constraintEqualToAnchor:self.frontImageView.centerXAnchor], [self.frontTextLabel.centerYAnchor constraintEqualToAnchor:self.frontImageView.centerYAnchor], [self.backImageView.centerXAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-2 - self.view.frame.size.width / 4], - [self.backImageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:120], + [self.backImageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:110], [self.backImageView.widthAnchor constraintEqualToConstant:150], [self.backImageView.heightAnchor constraintEqualToConstant:200], [self.backTextLabel.centerXAnchor constraintEqualToAnchor:self.backImageView.centerXAnchor], @@ -237,20 +269,40 @@ - (void)viewDidLoad { [self.backButtonImageView.widthAnchor constraintEqualToConstant:20], [self.backButtonImageView.heightAnchor constraintEqualToConstant:20], - [self.infoButton.centerYAnchor constraintEqualToAnchor:self.titleImageView.centerYAnchor], - [self.infoButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], - [self.infoButton.widthAnchor constraintEqualToConstant:40], - [self.infoButton.heightAnchor constraintEqualToConstant:40], - - [self.infoButtonImageView.centerYAnchor constraintEqualToAnchor:self.infoButton.centerYAnchor], - [self.infoButtonImageView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], - [self.infoButtonImageView.widthAnchor constraintEqualToConstant:20], - [self.infoButtonImageView.heightAnchor constraintEqualToConstant:20] + [self.dropdownButton.centerYAnchor constraintEqualToAnchor:self.titleImageView.centerYAnchor], + [self.dropdownButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], + [self.dropdownButton.widthAnchor constraintEqualToConstant:40], + [self.dropdownButton.heightAnchor constraintEqualToConstant:40], + + [self.dropdownImageView.trailingAnchor constraintEqualToAnchor:self.dropdownButton.trailingAnchor], + [self.dropdownImageView.centerYAnchor constraintEqualToAnchor:self.dropdownButton.centerYAnchor], + [self.dropdownImageView.widthAnchor constraintEqualToAnchor:self.dropdownButton.widthAnchor multiplier:0.57], + [self.dropdownImageView.heightAnchor constraintEqualToAnchor:self.dropdownButton.heightAnchor multiplier:0.57], + + [self.spotifyMusicView.topAnchor constraintEqualToAnchor:self.locationButton.bottomAnchor constant:14], + [self.spotifyMusicView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20], + [self.spotifyMusicView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], + [self.spotifyMusicView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], + [self.spotifyMusicView.widthAnchor constraintLessThanOrEqualToAnchor:self.view.widthAnchor constant:-44], + [self.spotifyMusicView.heightAnchor constraintEqualToConstant:46], ]]; [self checkForLatestVersion]; } +- (void)showMusicViewController { + [self presentViewController:self.spotifyViewController animated:YES completion:nil]; +} + +- (void)musicManagerDidUpdateMusic { + if ([BeaMusicManager sharedInstance].playingStatus == 0) { + self.musicDict = nil; + return; + } + + self.musicDict = [[BeaMusicManager sharedInstance] musicDict]; +} + - (void)checkForLatestVersion { NSString *currentVersion = TWEAK_VERSION; NSURL *url = [NSURL URLWithString:@"https://api.github.com/repos/yandevelop/Bea/releases/latest"]; @@ -278,11 +330,6 @@ - (void)checkForLatestVersion { [dataTask resume]; } -- (void)infoButtonTapped { - BeaInfoViewController *infoViewController = [[BeaInfoViewController alloc] init]; - [self presentViewController:infoViewController animated:YES completion:nil]; -} - - (void)isLateStateChanged:(UISwitch *)sender { if (sender.isOn) { self.isLate = YES; @@ -465,20 +512,29 @@ - (void)sendBeReal { [self showErrorWithTitle:@"Missing images" message:@"Select all required images."]; return; } + self.actionButton.enabled = NO; [self.actionButton setTitle:@"" forState:UIControlStateNormal]; + // stop the api calls being made + [self.spotifyMusicView stopTimer]; + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; spinner.center = self.actionButton.center; [self.view addSubview:spinner]; [spinner startAnimating]; [UIView animateWithDuration:0.3 animations:^{ - self.actionButton.alpha = 0.4; + self.actionButton.alpha = 0.5; }]; - NSDictionary *userData = [self createDataDictionary]; + // if the access token is not available, return + if (![[BeaTokenManager sharedInstance] BRAccessToken]) { + [self showErrorWithTitle:@"Something went wrong" message:@"2 - Please restart the app and try again."]; + return; + } + NSDictionary *userData = [self createDataDictionary]; // because of processing the images the spinner lags a bit BeaUploadTask *task = [[BeaUploadTask alloc] initWithData:userData frontImage:self.frontImage backImage:self.backImage]; @@ -522,7 +578,7 @@ - (void)uploadDidSucceed { }]; }); - // reset our view and properties to initial state + // reset the view and properties to initial state self.frontImageView.image = nil; self.backImageView.image = nil; self.frontImage = nil; @@ -538,12 +594,6 @@ - (void)uploadDidSucceed { - (NSDictionary *)createDataDictionary { NSMutableDictionary *data = [NSMutableDictionary dictionary]; - if (!self.authorizationKey) { - [self showErrorWithTitle:@"Something went wrong" message:@"2 - Please restart the app and try again."]; - return nil; - } - - [data setObject:self.authorizationKey forKey:@"authorization"]; [data setValue:@(self.isLate) forKey:@"isLate"]; if (self.caption) { @@ -560,6 +610,17 @@ - (NSDictionary *)createDataDictionary { [data setObject:latitudeNumber forKey:@"latitude"]; } + if (self.musicDict && [BeaMusicManager sharedInstance].playingStatus == 0) { + [data addEntriesFromDictionary:self.musicDict]; + } + return [data copy]; } + +- (void)viewWillDisappear:(BOOL)animated { + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"MusicUpdated" object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"openSpotifyViewController" object:nil]; + [self.spotifyMusicView stopTimer]; + [[BeaMusicManager sharedInstance] resetData]; +} @end \ No newline at end of file diff --git a/Utilities/Views/SpotifyMusicView/BeaSpotifyMusicView.h b/Utilities/Views/SpotifyMusicView/BeaSpotifyMusicView.h new file mode 100644 index 0000000..f3b4dca --- /dev/null +++ b/Utilities/Views/SpotifyMusicView/BeaSpotifyMusicView.h @@ -0,0 +1,10 @@ +@interface BeaSpotifyMusicView : UIView +@property (nonatomic, strong) UIImageView *artworkImageView; +@property (nonatomic, strong) UILabel *trackLabel; +@property (nonatomic, strong) UILabel *artistLabel; +@property (nonatomic, strong) NSDictionary *musicDict; +@property (nonatomic, strong) UITapGestureRecognizer *tapRecognizer; +@property (nonatomic, strong) NSTimer *timer; +@property (nonatomic, strong) BeaSpotifyAPIHandler *handler; +- (void)refreshMusicView; +@end \ No newline at end of file diff --git a/Utilities/Views/SpotifyMusicView/BeaSpotifyMusicView.m b/Utilities/Views/SpotifyMusicView/BeaSpotifyMusicView.m new file mode 100644 index 0000000..e49040c --- /dev/null +++ b/Utilities/Views/SpotifyMusicView/BeaSpotifyMusicView.m @@ -0,0 +1,99 @@ +#import "BeaSpotifyMusicView.h" + +@implementation BeaSpotifyMusicView +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshMusicView) name:@"MusicUpdated" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(stopTimer) name:@"StopUpdatingCurrentlyPlaying" object:nil]; + + self.translatesAutoresizingMaskIntoConstraints = NO; + + self.artworkImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + self.artworkImageView.translatesAutoresizingMaskIntoConstraints = NO; + self.artworkImageView.layer.cornerRadius = 1.0; + self.artworkImageView.clipsToBounds = YES; + [self addSubview:self.artworkImageView]; + + // set up the properties for the artist and track label + self.trackLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.trackLabel.font = [UIFont fontWithName:@"Inter" size:17]; + self.trackLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:self.trackLabel]; + + self.artistLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.artistLabel.font = [UIFont systemFontOfSize:12]; + self.artistLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:self.artistLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [self.artworkImageView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor], + [self.artworkImageView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor], + [self.artworkImageView.widthAnchor constraintEqualToConstant:36], + [self.artworkImageView.heightAnchor constraintEqualToConstant:36], + + [self.trackLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:2], + [self.trackLabel.topAnchor constraintEqualToAnchor:self.topAnchor constant:4], + [self.trackLabel.trailingAnchor constraintEqualToAnchor:self.artworkImageView.leadingAnchor constant:-12], + + [self.artistLabel.topAnchor constraintEqualToAnchor:self.trackLabel.bottomAnchor constant:2], + [self.artistLabel.leadingAnchor constraintEqualToAnchor:self.trackLabel.leadingAnchor], + [self.artistLabel.widthAnchor constraintLessThanOrEqualToConstant:125], + ]]; + + self.handler = [[BeaSpotifyAPIHandler alloc] init]; + self.handler.delegate = self; + } + + return self; +} + +- (void)managerDidValidateAccessToken { + [self startFetchingSongs]; +} + +- (void)startFetchingSongs { + // add a gesture recognizer to the view that opens the spotify modal + self.tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openSpotifyViewController)]; + [self addGestureRecognizer:self.tapRecognizer]; + [self.handler retrieveCurrentlyPlayingSong]; + [self startTimer]; +} + +- (void)startTimer { + [self.timer invalidate]; + self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self.handler selector:@selector(retrieveCurrentlyPlayingSong) userInfo:nil repeats:YES]; +} + +- (void)stopTimer { + [self.timer invalidate]; + self.timer = nil; +} + +- (void)openSpotifyViewController { + [[NSNotificationCenter defaultCenter] postNotificationName:@"openSpotifyViewController" object:nil]; +} + +- (void)refreshMusicView { + self.musicDict = [[BeaMusicManager sharedInstance] musicDict]; + + NSData *artworkImageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.musicDict[@"music"][@"artwork"]]]; + UIImage *artworkImage = [UIImage imageWithData:artworkImageData]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView transitionWithView:self.trackLabel duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + self.trackLabel.text = self.musicDict[@"music"][@"track"]; + } completion:nil]; + + [UIView transitionWithView:self.artistLabel duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + self.artistLabel.text = self.musicDict[@"music"][@"artist"]; + } completion:nil]; + + [UIView transitionWithView:self.artworkImageView duration:0.3 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ + [self.artworkImageView setImage:artworkImage]; + } + completion:nil]; + }); +} +@end \ No newline at end of file diff --git a/Utilities/UploadViewController/StatusView/BeaStatusView.h b/Utilities/Views/StatusView/BeaStatusView.h similarity index 100% rename from Utilities/UploadViewController/StatusView/BeaStatusView.h rename to Utilities/Views/StatusView/BeaStatusView.h diff --git a/Utilities/UploadViewController/StatusView/BeaStatusView.m b/Utilities/Views/StatusView/BeaStatusView.m similarity index 99% rename from Utilities/UploadViewController/StatusView/BeaStatusView.m rename to Utilities/Views/StatusView/BeaStatusView.m index 53341a0..b6aa856 100644 --- a/Utilities/UploadViewController/StatusView/BeaStatusView.m +++ b/Utilities/Views/StatusView/BeaStatusView.m @@ -12,14 +12,13 @@ - (instancetype)initWithFrame:(CGRect)frame { self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; self.titleLabel.font = [UIFont fontWithName:@"Inter" size:20.0]; self.titleLabel.textColor = [UIColor whiteColor]; + [self addSubview:self.titleLabel]; self.messageLabel = [[UILabel alloc] initWithFrame:CGRectZero]; self.messageLabel.font = [UIFont fontWithName:@"Inter" size:11.0]; self.messageLabel.numberOfLines = 2; self.messageLabel.lineBreakMode = NSLineBreakByWordWrapping; self.messageLabel.textColor = [UIColor whiteColor]; - - [self addSubview:self.titleLabel]; [self addSubview:self.messageLabel]; self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; diff --git a/control b/control index fcffc39..5da547e 100644 --- a/control +++ b/control @@ -1,7 +1,7 @@ Package: com.yan.bea Name: Bea -Version: 1.2.2 -Architecture: iphoneos-arm +Version: 1.3 +Architecture: iphoneos-arm64 Description: Lightweight BeReal. enhancement tweak. Maintainer: yan Author: yan