git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

commit 3cb04e88d1e3b77a48427ea8bc1224e90634f4fe
parent 917b62785ca8aa7cf55f6a49868229fc2a3bd391
Author: Lior Halphon <LIJI32@gmail.com>
Date:   Sat, 15 Mar 2025 13:22:55 +0200

Remove HH integration

Diffstat:
DiOS/GBHub.h41-----------------------------------------
DiOS/GBHub.m344-------------------------------------------------------------------------------
DiOS/GBHubCell.h8--------
DiOS/GBHubCell.m4----
DiOS/GBHubGameViewController.h7-------
DiOS/GBHubGameViewController.m314-------------------------------------------------------------------------------
DiOS/GBHubViewController.h6------
DiOS/GBHubViewController.m402-------------------------------------------------------------------------------
MiOS/GBLibraryViewController.m37+++----------------------------------
DiOS/GlobeTemplate@2x.png0
DiOS/GlobeTemplate@3x.png0
11 files changed, 3 insertions(+), 1160 deletions(-)

diff --git a/iOS/GBHub.h b/iOS/GBHub.h @@ -1,41 +0,0 @@ -#import <Foundation/Foundation.h> - -typedef enum { - GBHubStatusNotReady, - GBHubStatusInProgress, - GBHubStatusReady, - GBHubStatusError, -} GBHubStatus; - -static inline NSString *GBSearchCanonicalString(NSString *string) -{ - return [[string.lowercaseString stringByApplyingTransform:NSStringTransformStripDiacritics reverse:false] stringByApplyingTransform:NSStringTransformStripCombiningMarks reverse:false]; -} - -@interface GBHubGame : NSObject -@property (readonly) NSString *title; -@property (readonly) NSString *developer; -@property (readonly) NSString *type; -@property (readonly) NSString *license; -@property (readonly) NSDate *publicationDate; -@property (readonly) NSArray <NSString *> *tags; -@property (readonly) NSURL *repository; -@property (readonly) NSURL *website; -@property (readonly) NSArray <NSURL *> *screenshots; -@property (readonly) NSURL *file; -@property (readonly) NSString *slug; -@property (readonly) NSString *entryDescription; -@property (readonly) NSString *keywords; -@end - -extern NSString *const GBHubStatusChangedNotificationName; - -@interface GBHub : NSObject -+ (instancetype)sharedHub; -- (void)refresh; -- (unsigned)countForTag:(NSString *)tag; -@property (readonly) GBHubStatus status; -@property (readonly) NSDictionary<NSString *, GBHubGame *> *allGames; -@property (readonly) NSArray<NSString *> *sortedTags; -@property (readonly) NSArray<GBHubGame *> *showcaseGames; -@end diff --git a/iOS/GBHub.m b/iOS/GBHub.m @@ -1,344 +0,0 @@ -#import "GBHub.h" -#pragma clang diagnostic ignored "-Warc-retain-cycles" - -NSString *const GBHubStatusChangedNotificationName = @"GBHubStatusChangedNotification"; - -static NSURL *StringToWebURL(NSString *string) -{ - if (![string isKindOfClass:[NSString class]]) return nil; - NSURL *url = [NSURL URLWithString:string]; - if (![url.scheme isEqual:@"http"] && [url.scheme isEqual:@"https"]) { - return nil; - } - - return url; -} - -@implementation GBHubGame - -- (instancetype)initWithJSON:(NSDictionary *)json -{ - self = [super init]; - - // Skip NSFW titles - if ([json[@"nsfw"] boolValue]) return nil; - - if (json[@"tags"] && ![json[@"tags"] isKindOfClass:[NSArray class]]) return nil; - _tags = [NSMutableArray array]; - - for (__strong NSString *tag in json[@"tags"]) { - if (![tag isKindOfClass:[NSString class]]) { - return nil; - } - if ([tag isEqual:@"hw:gbprinter"]) { - continue; - } - - if ([tag hasPrefix:@"event:"]) { - tag = [tag substringFromIndex:strlen("event:")]; - } - if ([tag hasPrefix:@"gb-showdown-"]) { - tag = [NSString stringWithFormat:@"Game Boy Showdown %@", [tag substringFromIndex:strlen("gb-showdown-")]]; - } - if ([tag hasPrefix:@"gbcompo"]) { - tag = [NSString stringWithFormat:@"GBCompo%@", [[tag substringFromIndex:strlen("gbcompo")].capitalizedString stringByReplacingOccurrencesOfString:@"-" withString:@" "]]; - } - if ([tag isEqual:tag.lowercaseString]) { - tag = [tag stringByReplacingOccurrencesOfString:@"-" withString:@" "].capitalizedString; - } - [(NSMutableArray *)_tags addObject:tag]; - } - - NSMutableSet *licenses = [NSMutableSet set]; - - if (json[@"license"]) { - [licenses addObject:json[@"license"]]; - } - if (json[@"gameLicense"]) { - [licenses addObject:json[@"gameLicense"]]; - } - if (json[@"assetsLicense"]) { - [licenses addObject:json[@"assetsLicense"]]; - } - - - if (licenses.count == 1) { - _license = licenses.anyObject; - if (![_license isKindOfClass:[NSString class]]) { - return nil; - } - if (!_license.length) _license = nil; - } - else if (licenses.count > 1) { - if (json[@"license"]) { - return nil; - } - _license = [NSString stringWithFormat:@"%@ (Assets: %@)", json[@"gameLicense"], json[@"assetsLicense"]]; - } - - if (_license && ![_tags containsObject:@"Open Source"]) { - // License is guaranteed to be Open Source by spec - [(NSMutableArray *)_tags addObject:@"Open Source"]; - } - - _title = json[@"title"]; - if (![_title isKindOfClass:[NSString class]]) { - return nil; - } - - _entryDescription = json[@"description"]; - if (_entryDescription && ![_entryDescription isKindOfClass:[NSString class]]) { - return nil; - } - - _developer = json[@"developer"]; - if (![_developer isKindOfClass:[NSString class]]) { - if ([_developer isKindOfClass:[NSArray class]] && ((NSArray *)_developer).count) { - if ([((NSArray *)_developer)[0] isKindOfClass:[NSString class]]) { - _developer = [(NSArray *)_developer componentsJoinedByString:@", "]; - } - else if ([((NSArray *)_developer)[0] isKindOfClass:[NSDictionary class]]) { - NSMutableArray *developers = [NSMutableArray array]; - for (NSDictionary *developer in (NSArray *)_developer) { - if (![developer isKindOfClass:[NSDictionary class]]) return nil; - NSString *name = developer[@"name"]; - if (!name) return nil; - [developers addObject:name]; - } - _developer = [developers componentsJoinedByString:@", "]; - } - else { - return nil; - } - } - else if ([_developer isKindOfClass:[NSDictionary class]]) { - _developer = ((NSDictionary *)_developer)[@"name"]; - } - else { - return nil; - } - } - - - _slug = json[@"slug"]; - if (![_slug isKindOfClass:[NSString class]]) { - return nil; - } - - _type = json[@"typetag"]; - if (![_type isKindOfClass:[NSString class]]) { - return nil; - } - - NSString *dateString = json[@"date"]; - if ([dateString isKindOfClass:[NSString class]]) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd"]; - _publicationDate = [dateFormatter dateFromString:dateString]; - } - - _repository = StringToWebURL(json[@"repository"]); - _website = StringToWebURL(json[@"website"]); - - NSURL *base = [NSURL URLWithString:[NSString stringWithFormat:@"https://hh3.gbdev.io/static/database-gb/entries/%@", _slug]]; - - NSMutableArray *screenshots = [NSMutableArray array]; - for (NSString *screenshot in json[@"screenshots"]) { - [screenshots addObject:[base URLByAppendingPathComponent:screenshot]]; - } - - - _screenshots = screenshots; - - for (NSDictionary *file in json[@"files"]) { - NSString *extension = [file[@"filename"] pathExtension].lowercaseString; - if (![extension isEqual:@"gb"] && ![extension isEqual:@"gbc"]) { - // Not a DMG/CGB game - continue; - } - if ([file[@"default"] boolValue] || !_file) { - _file = [base URLByAppendingPathComponent:file[@"filename"]]; - } - } - - if (!_file) { - return nil; - } - - _keywords = [NSString stringWithFormat:@"%@ %@ %@ %@", - GBSearchCanonicalString(_title), - GBSearchCanonicalString(_developer), - GBSearchCanonicalString(_entryDescription) ?: @"", - GBSearchCanonicalString([_tags componentsJoinedByString:@" "]) - ]; - - return self; -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"<%@ %p: %@>", self.class, self, self.title]; -} - -@end - -@implementation GBHub -{ - NSMutableDictionary<NSString *, GBHubGame *> *_allGames; - NSMutableDictionary<NSString *, NSNumber *> *_tags; - NSMutableArray<GBHubGame *> *_showcaseGames; - NSSet<NSString *> *_showcaseExtras; -} - -+ (instancetype)sharedHub -{ - static GBHub *hub = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - hub = [[self alloc] init]; - }); - return hub; -} - -- (void)setStatus:(GBHubStatus)status -{ - if (_status != status) { - _status = status; - if ([NSThread isMainThread]) { - [[NSNotificationCenter defaultCenter] postNotificationName:GBHubStatusChangedNotificationName - object:self]; - } - else { - dispatch_sync(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:GBHubStatusChangedNotificationName - object:self]; - }); - } - } -} - -- (void)handleAPIData:(NSData *)data forBaseURL:(NSString *)base completion:(void(^)(GBHubStatus))completion -{ - @try { - if (!data) { - completion(GBHubStatusError); - return; - } - NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - if (!json) { - completion(GBHubStatusError); - return; - } - - if (!json[@"page_current"] || !json[@"page_total"]) { - completion(GBHubStatusError); - return; - } - - for (NSDictionary *entry in json[@"entries"]) { - @try { - @autoreleasepool { - GBHubGame *game = [[GBHubGame alloc] initWithJSON:entry]; - if (game && !_allGames[game.slug]) { - _allGames[game.slug] = game; - bool showcase = [_showcaseExtras containsObject:game.slug]; - if (!showcase) { - for (NSString *tag in game.tags) { - _tags[tag] = @(_tags[tag].unsignedIntValue + 1); - if ([tag containsString:@"Shortlist"]) { - showcase = true; - break; - } - } - } - if (showcase) { - [_showcaseGames addObject:game]; - } - } - } - } - @catch (NSException *exception) { - // Just in case I missed some JSON edge cases, let's not abort the entire response over one bad game - } - } - - if ([json[@"page_current"] unsignedIntValue] == [json[@"page_total"] unsignedIntValue]) { - completion(GBHubStatusReady); - return; - } - - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@&page=%u", - base, - [json[@"page_current"] unsignedIntValue] + 1]] - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - [self handleAPIData:data forBaseURL:base completion:completion]; - }] resume]; - } - @catch (NSException *exception) { - self.status = GBHubStatusError; - } -} - -- (void)addGamesForURL:(NSString *)url completion:(void(^)(GBHubStatus))completion -{ - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:url] - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - completion(GBHubStatusError); - return; - } - [self handleAPIData:data forBaseURL:url completion:completion]; - }] resume]; -} - -- (unsigned)countForTag:(NSString *)tag -{ - return _tags[tag].unsignedIntValue; -} - -- (void)refresh -{ - if (_status == GBHubStatusInProgress) { - return; - } - self.status = GBHubStatusInProgress; - _allGames = [NSMutableDictionary dictionary]; - _tags = [NSMutableDictionary dictionary]; - _showcaseGames = [NSMutableArray array]; - - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://sameboy.github.io/ios-showcase"] - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (data) { - _showcaseExtras = [NSSet setWithArray:[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] componentsSeparatedByString:@"\n"]]; - } - [self addGamesForURL:@"https://hh3.gbdev.io/api/search?tags=Open+Source&results=1000" - completion:^(GBHubStatus ret) { - if (ret != GBHubStatusReady) { - self.status = ret; - return; - } - [self addGamesForURL:@"https://hh3.gbdev.io/api/search?thirdparty=sameboy&results=1000" - completion:^(GBHubStatus ret) { - if (ret == GBHubStatusReady) { - _sortedTags = [_tags.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) { - return [obj1 compare:obj2]; - }]; - } - unsigned day = time(NULL) / 60 / 60 / 24; - if (_showcaseGames.count > 5) { - typeof(_showcaseGames) temp = [NSMutableArray array]; - for (unsigned i = 5; i--;) { - unsigned index = day % _showcaseGames.count; - GBHubGame *game = _showcaseGames[index]; - [_showcaseGames removeObjectAtIndex:index]; - [temp addObject:game]; - } - _showcaseGames = temp; - } - self.status = ret; - }]; - }]; - }] resume]; -} - -@end diff --git a/iOS/GBHubCell.h b/iOS/GBHubCell.h @@ -1,8 +0,0 @@ -#import <UIKit/UIKit.h> -#import "GBHub.h" - -@interface GBHubCell : UITableViewCell - -@property GBHubGame *game; - -@end diff --git a/iOS/GBHubCell.m b/iOS/GBHubCell.m @@ -1,4 +0,0 @@ -#import "GBHubCell.h" - -@implementation GBHubCell -@end diff --git a/iOS/GBHubGameViewController.h b/iOS/GBHubGameViewController.h @@ -1,7 +0,0 @@ -#import <UIKit/UIKit.h> -#import "GBHub.h" - -@interface GBHubGameViewController : UIViewController -- (instancetype)initWithGame:(GBHubGame *)game; -@end - diff --git a/iOS/GBHubGameViewController.m b/iOS/GBHubGameViewController.m @@ -1,314 +0,0 @@ -#import "GBHubGameViewController.h" -#import "GBROMManager.h" -#import "UILabel+TapLocation.h" - -@implementation NSMutableAttributedString (append) - -- (void)appendWithAttributes:(NSDictionary *)attributes format:(NSString *)format, ... -{ - va_list args; - va_start(args, format); - NSString *string = [[NSString alloc] initWithFormat:format arguments:args]; - va_end(args); - [self appendAttributedString:[[NSAttributedString alloc] initWithString:string attributes:attributes]]; -} - -@end - -@implementation GBHubGameViewController -{ - GBHubGame *_game; - UIScrollView *_scrollView; - UIScrollView *_screenshotsScrollView; - UILabel *_titleLabel; - UILabel *_descriptionLabel; -} - -- (instancetype)initWithGame:(GBHubGame *)game -{ - self = [super init]; - _game = game; - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - UIColor *labelColor, *linkColor, *secondaryLabelColor; - if (@available(iOS 13.0, *)) { - self.view.backgroundColor = [UIColor systemBackgroundColor]; - labelColor = UIColor.labelColor; - linkColor = UIColor.linkColor; - secondaryLabelColor = UIColor.secondaryLabelColor; - - } - else { - self.view.backgroundColor = [UIColor whiteColor]; - labelColor = UIColor.blackColor; - linkColor = UIColor.blueColor; - secondaryLabelColor = [UIColor colorWithWhite:0.55 alpha:1.0]; - } - _scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; - _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _scrollView.scrollEnabled = true; - _scrollView.pagingEnabled = false; - _scrollView.showsVerticalScrollIndicator = true; - _scrollView.showsHorizontalScrollIndicator = false; - [self.view addSubview:_scrollView]; - - _scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, self.view.bounds.size.height * 2); - - _titleLabel = [[UILabel alloc] initWithFrame:(CGRectMake(0, 8, - self.view.bounds.size.width - 16, - 56))]; - NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; - style.paragraphSpacing = 4; - - NSMutableAttributedString *titleText = [[NSMutableAttributedString alloc] init]; - [titleText appendWithAttributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:34 weight:UIFontWeightBold], - NSForegroundColorAttributeName: labelColor, - NSParagraphStyleAttributeName: style, - } format:@"%@", _game.title]; - - [titleText appendWithAttributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:20], - NSForegroundColorAttributeName: secondaryLabelColor, - NSParagraphStyleAttributeName: style, - } format:@"\n by %@", _game.developer]; - - _titleLabel.attributedText = titleText; - _titleLabel.lineBreakMode = NSLineBreakByWordWrapping; - _titleLabel.numberOfLines = 0; - [_scrollView addSubview:_titleLabel]; - - - NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init]; - NSDictionary *labelAttributes = @{ - NSFontAttributeName: [UIFont boldSystemFontOfSize:UIFont.labelFontSize], - NSForegroundColorAttributeName: labelColor, - }; - NSDictionary *valueAttributes = @{ - NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], - NSForegroundColorAttributeName: labelColor, - }; - - if (_game.entryDescription) { - [text appendWithAttributes:valueAttributes format:@"%@\n\n", _game.entryDescription]; - } - if (_game.publicationDate) { - [text appendWithAttributes:labelAttributes format:@"Published: "]; - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - formatter.dateStyle = NSDateFormatterMediumStyle; - formatter.timeStyle = NSDateFormatterNoStyle; - formatter.locale = [NSLocale currentLocale]; - [text appendWithAttributes:valueAttributes format:@"%@\n", [formatter stringFromDate:_game.publicationDate]]; - } - if (_game.website) { - [text appendWithAttributes:labelAttributes format:@"Website: "]; - [text appendWithAttributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], - @"GBLinkAttribute": _game.website, - NSForegroundColorAttributeName: linkColor, - } format:@"%@\n", _game.website]; - } - if (_game.repository) { - [text appendWithAttributes:labelAttributes format:@"Repository: "]; - [text appendWithAttributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], - @"GBLinkAttribute": _game.repository, - NSForegroundColorAttributeName: linkColor, - } format:@"%@\n", _game.repository]; - } - if (_game.license) { - [text appendWithAttributes:labelAttributes format:@"License: "]; - [text appendWithAttributes:valueAttributes format:@"%@\n", _game.license]; - } - if (_game.tags.count) { - [text appendWithAttributes:labelAttributes format:@"Categories: "]; - bool first = true; - for (NSString *tag in _game.tags) { - if (!first) { - [text appendWithAttributes:valueAttributes format:@", ", _game.license]; - } - first = false; - [text appendWithAttributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], - @"GBHubTag": tag, - NSForegroundColorAttributeName: linkColor, - } format:@"%@", tag]; - } - [text appendWithAttributes:valueAttributes format:@"\n"]; - } - - _descriptionLabel = [[UILabel alloc] init]; - _descriptionLabel.numberOfLines = 0; - _descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; - if (@available(iOS 14.0, *)) { - _descriptionLabel.lineBreakStrategy = NSLineBreakStrategyNone; - } - _descriptionLabel.attributedText = text; - [_scrollView addSubview:_descriptionLabel]; - - unsigned screenshotWidth = (unsigned)(MIN(self.view.bounds.size.width, self.view.bounds.size.height) - 16) / 160 * 160; - unsigned screenshotHeight = screenshotWidth / 160 * 144; - if (_game.screenshots.count) { - _screenshotsScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, - self.view.bounds.size.width, - screenshotHeight + 8)]; - _screenshotsScrollView.scrollEnabled = true; - _screenshotsScrollView.pagingEnabled = false; - _screenshotsScrollView.showsVerticalScrollIndicator = false; - _screenshotsScrollView.showsHorizontalScrollIndicator = true; - - unsigned x = 0; - for (NSURL *url in _game.screenshots) { - x += 8; - UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(x, 0, - screenshotWidth, - screenshotHeight)]; - [imageView.layer setMinificationFilter:kCAFilterLinear]; - [imageView.layer setMagnificationFilter:kCAFilterNearest]; - imageView.layer.cornerRadius = 4; - imageView.layer.borderWidth = 1; - imageView.layer.masksToBounds = true; - - if (@available(iOS 13.0, *)) { - imageView.layer.borderColor = [UIColor tertiaryLabelColor].CGColor; - } - else { - imageView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.5].CGColor; - } - - [_screenshotsScrollView addSubview:imageView]; - [[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - if (location) { - UIImage *image = [UIImage imageWithContentsOfFile:location.path]; - dispatch_async(dispatch_get_main_queue(), ^{ - CGRect frame = imageView.frame; - imageView.image = image; - imageView.frame = frame; - imageView.contentMode = UIViewContentModeScaleAspectFit; - }); - } - }] resume]; - x += screenshotWidth + 8; - } - _screenshotsScrollView.contentSize = CGSizeMake(x, screenshotHeight); - _screenshotsScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; - - [_scrollView addSubview:_screenshotsScrollView]; - } - [self viewDidLayoutSubviews]; - _descriptionLabel.userInteractionEnabled = true; - [_descriptionLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(tappedLabel:)]]; - - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Download" - style:UIBarButtonItemStylePlain - target:self - action:@selector(rightButtonPressed)]; - if ([GBROMManager.sharedManager romFileForROM:_game.title]) { - self.navigationItem.rightBarButtonItem.title = @"Open"; - } -} - -- (void)rightButtonPressed -{ - if ([GBROMManager.sharedManager romFileForROM:_game.title]) { - [GBROMManager sharedManager].currentROM = _game.title; - [self.navigationController dismissViewControllerAnimated:true completion:nil]; - } - else { - UIActivityIndicatorViewStyle style = UIActivityIndicatorViewStyleWhite; - if (@available(iOS 13.0, *)) { - style = UIActivityIndicatorViewStyleMedium; - } - UIActivityIndicatorView *view = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style]; - CGRect frame = view.frame; - frame.size.width += 16; - view.frame = frame; - [view startAnimating]; - self.navigationItem.rightBarButtonItem.customView = view; - [[[NSURLSession sharedSession] downloadTaskWithURL:_game.file completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - if (!location) { - dispatch_async(dispatch_get_main_queue(), ^{ - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not download ROM" - message:@"Could not download this ROM from Homebrew Hub. Please try again later." - preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"Close" - style:UIAlertActionStyleCancel - handler:nil]]; - [self presentViewController:alert animated:true completion:nil]; - self.navigationItem.rightBarButtonItem.customView = nil; - }); - return; - } - NSString *newTempName = [[location.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:_game.file.lastPathComponent]; - [[NSFileManager defaultManager] moveItemAtPath:location.path toPath:newTempName error:nil]; - [[GBROMManager sharedManager] importROM:newTempName withName:_game.title keepOriginal:false]; - dispatch_async(dispatch_get_main_queue(), ^{ - self.navigationItem.rightBarButtonItem.title = @"Open"; - self.navigationItem.rightBarButtonItem.customView = nil; - }); - }] resume]; - } -} - -- (void)tappedLabel:(UITapGestureRecognizer *)tap -{ - unsigned characterIndex = [(UILabel *)tap.view characterAtTap:tap]; - - NSURL *url = [((UILabel *)tap.view).attributedText attribute:@"GBLinkAttribute" atIndex:characterIndex effectiveRange:NULL]; - - if (url) { - [[UIApplication sharedApplication] openURL:url options:nil completionHandler:nil]; - return; - } - - NSString *tag = [((UILabel *)tap.view).attributedText attribute:@"GBHubTag" atIndex:characterIndex effectiveRange:NULL]; - - if (tag) { - UINavigationItem *parent = self.navigationController.navigationBar.items[self.navigationController.navigationBar.items.count - 2]; - [self.navigationController popViewControllerAnimated:true]; - if (@available(iOS 13.0, *)) { - parent.searchController.searchBar.searchTextField.text = @""; - parent.searchController.searchBar.searchTextField.tokens = nil; - UISearchToken *token = [UISearchToken tokenWithIcon:nil text:tag]; - token.representedObject = tag; - [parent.searchController.searchBar.searchTextField insertToken:token atIndex:0]; - } - else { - parent.searchController.searchBar.text = tag; - } - parent.searchController.active = true; - return; - } - -} - -- (void)viewDidLayoutSubviews -{ - unsigned y = 12; - CGSize size = [_titleLabel sizeThatFits:(CGSize){_scrollView.bounds.size.width - 32, INFINITY}];; - _titleLabel.frame = (CGRect){{16, y}, {_scrollView.bounds.size.width - 32, size.height}}; - y += size.height + 24; - - if (_screenshotsScrollView) { - _screenshotsScrollView.frame = CGRectMake(0, y, _scrollView.bounds.size.width, _screenshotsScrollView.frame.size.height); - y += _screenshotsScrollView.frame.size.height + 8; - if (_game.screenshots.count == 1) { - CGRect frame = _screenshotsScrollView.frame; - frame.origin.x = (_scrollView.bounds.size.width - _screenshotsScrollView.contentSize.width) / 2; - _screenshotsScrollView.frame = frame; - } - } - - size = [_descriptionLabel sizeThatFits:(CGSize){_scrollView.bounds.size.width - 32, INFINITY}];; - _descriptionLabel.frame = (CGRect){{16, y}, {_scrollView.bounds.size.width - 32, size.height}}; - y += size.height; - - _scrollView.contentSize = CGSizeMake(_scrollView.bounds.size.width, y); -} - -@end diff --git a/iOS/GBHubViewController.h b/iOS/GBHubViewController.h @@ -1,6 +0,0 @@ -#import <UIKit/UIKit.h> - -@interface GBHubViewController : UITableViewController - -@end - diff --git a/iOS/GBHubViewController.m b/iOS/GBHubViewController.m @@ -1,402 +0,0 @@ -#import "GBHubViewController.h" -#import "GBHub.h" -#import "GBHubGameViewController.h" -#import "GBHubCell.h" -#import "UILabel+TapLocation.h" - -@interface GBHubViewController() <UISearchResultsUpdating> -@end - -@implementation GBHubViewController -{ - UISearchController *_searchController; - NSMutableDictionary<NSURL *, UIImage *> *_imageCache; - NSArray<GBHubGame *> *_results; - NSString *_resultsTitle; - bool _showingAllGames; - bool _appear; -} - -- (instancetype)init -{ - self = [self initWithStyle:UITableViewStyleGrouped]; - [[NSNotificationCenter defaultCenter] addObserver:self.tableView - selector:@selector(reloadData) - name:GBHubStatusChangedNotificationName - object:nil]; - _imageCache = [NSMutableDictionary dictionary]; - self.tableView.rowHeight = UITableViewAutomaticDimension; - return self; -} - -- (void)viewDidAppear:(BOOL)animated -{ - if (!_appear) { - _appear = true; - [GBHub.sharedHub refresh]; - } - [super viewDidAppear:animated]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - self.navigationItem.searchController = - _searchController = [[UISearchController alloc] initWithSearchResultsController:nil]; - _searchController.searchResultsUpdater = self; - self.tableView.scrollsToTop = true; - self.navigationItem.hidesSearchBarWhenScrolling = false; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - UIColor *labelColor; - UIColor *secondaryLabelColor; - if (@available(iOS 13.0, *)) { - labelColor = UIColor.labelColor; - secondaryLabelColor = UIColor.secondaryLabelColor; - } - else { - labelColor = UIColor.blackColor; - secondaryLabelColor = [UIColor colorWithWhite:0.55 alpha:1.0]; - } - switch (GBHub.sharedHub.status) { - case GBHubStatusNotReady: return nil; - case GBHubStatusInProgress: { - UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:0]; - UIActivityIndicatorViewStyle style = UIActivityIndicatorViewStyleWhite; - if (@available(iOS 13.0, *)) { - style = UIActivityIndicatorViewStyleMedium; - } - - UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style]; - cell.bounds = spinner.bounds; - [cell addSubview:spinner]; - [spinner startAnimating]; - spinner.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - return cell; - } - case GBHubStatusReady: { - if (indexPath.section == 0) { - GBHubCell *cell = [[GBHubCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:0]; - cell.game = _results? _results[indexPath.item] : GBHub.sharedHub.showcaseGames[indexPath.item]; - NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:cell.game.title - attributes:@{ - NSFontAttributeName: [UIFont boldSystemFontOfSize:UIFont.labelFontSize], - NSForegroundColorAttributeName: labelColor - }]; - [text appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" by %@\n", - cell.game.developer] - attributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], - NSForegroundColorAttributeName: labelColor - }]]; - [text appendAttributedString:[[NSAttributedString alloc] initWithString:cell.game.entryDescription ?: [cell.game.tags componentsJoinedByString:@", "] - attributes:@{ - NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize], - NSForegroundColorAttributeName: secondaryLabelColor - }]]; - cell.textLabel.attributedText = text; - cell.textLabel.numberOfLines = 2; - cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; - - static UIImage *emptyImage = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, tableView.window.screen.scale); - UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 3, 60, 54) cornerRadius:4]; - [mask addClip]; - [[UIColor whiteColor] set]; - [mask fill]; - if (@available(iOS 13.0, *)) { - [[UIColor tertiaryLabelColor] set]; - } - else { - [[UIColor colorWithWhite:0 alpha:0.5] set]; - } - [mask stroke]; - emptyImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - }); - cell.imageView.image = emptyImage; - return cell; - } - UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:0]; - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - NSString *tag = GBHub.sharedHub.sortedTags[indexPath.item]; - cell.textLabel.text = tag; - unsigned count = [GBHub.sharedHub countForTag:tag]; - if (count == 1) { - cell.detailTextLabel.text = @"1 Game"; - } - else { - cell.detailTextLabel.text = [NSString stringWithFormat:@"%u Games", count]; - } - cell.textLabel.numberOfLines = 2; - cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; - return cell; - } - case GBHubStatusError: { - UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:0]; - cell.textLabel.text = @"Could not connect to Homebrew Hub"; - return cell; - } - } - return nil; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - switch (GBHub.sharedHub.status) { - case GBHubStatusNotReady: return 0; - case GBHubStatusInProgress: return 1; - case GBHubStatusReady: { - if (_results) return _results.count; - if (section == 0) return GBHub.sharedHub.showcaseGames.count; - return GBHub.sharedHub.sortedTags.count; - } - case GBHubStatusError: return 1; - } - return 0; -} - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - switch (GBHub.sharedHub.status) { - case GBHubStatusNotReady: return 0; - case GBHubStatusInProgress: return 1; - case GBHubStatusReady: return _results? 1 : 2; - case GBHubStatusError: return 1; - } - return 0; -} - -- (NSString *)title -{ - return @"Homebrew Hub"; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - if (GBHub.sharedHub.status != GBHubStatusReady) return nil; - if (section == 0) return _results? _resultsTitle : @"Homebrew Showcase"; - return @"Categories"; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (GBHub.sharedHub.status == GBHubStatusReady && indexPath.section == 0) { - return 60; - } - return 45; -} - -- (void)tableView:(UITableView *)tableView willDisplayCell:(GBHubCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (![cell isKindOfClass:[GBHubCell class]]) return; - if (!cell.game.screenshots.count) return; - - NSURL *url = cell.game.screenshots[0]; - UIImage *image = _imageCache[url]; - if ([image isKindOfClass:[UIImage class]]) { - cell.imageView.image = image; - return; - } - if (!image) { - _imageCache[url] = (id)[NSNull null]; - [[[NSURLSession sharedSession] downloadTaskWithURL:url - completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - if (!location) return; - dispatch_sync(dispatch_get_main_queue(), ^{ - UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, tableView.window.screen.scale); - UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 3, 60, 54) cornerRadius:4]; - [mask addClip]; - UIImage *image = [UIImage imageWithContentsOfFile:location.path]; - [image drawInRect:mask.bounds]; - if (@available(iOS 13.0, *)) { - [[UIColor tertiaryLabelColor] set]; - } - else { - [[UIColor colorWithWhite:0 alpha:0.5] set]; - } - [mask stroke]; - _imageCache[url] = cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; - }); - }] resume]; - } -} - -- (void)updateSearchResultsForSearchController:(UISearchController *)searchController -{ - static unsigned cookie = 0; - NSArray<NSString *> *keywords = [GBSearchCanonicalString([searchController.searchBar.text - stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]) - componentsSeparatedByString:@" "]; - if (keywords.count == 1 && keywords[0].length == 0) { - keywords = @[]; - } - NSArray *tokens = nil; - if (@available(iOS 13.0, *)) { - tokens = searchController.searchBar.searchTextField.tokens; - } - - if (!searchController.isActive && tokens.count == 0 && !keywords.count) { - cookie++; - _results = nil; - _showingAllGames = false; - [self.tableView reloadData]; - [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0,0} length:2] - atScrollPosition:UITableViewScrollPositionTop - animated:false]; - return; - } - if (tokens.count || keywords.count) { - _showingAllGames = false; - cookie++; - unsigned myCookie = cookie; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSMutableArray<GBHubGame *> *results = [NSMutableArray array]; - for (GBHubGame *game in GBHub.sharedHub.allGames.allValues) { - bool matches = true; - if (@available(iOS 13.0, *)) { - for (UISearchToken *token in tokens) { - if (![game.tags containsObject:token.representedObject]) { - matches = false; - break; - } - } - if (!matches) continue; - } - for (NSString *keyword in keywords) { - if (keyword.length == 0) continue; - if (![game.keywords containsString:keyword]) { - matches = false; - break; - } - } - if (matches) { - [results addObject:game]; - } - } - dispatch_async(dispatch_get_main_queue(), ^{ - if (myCookie != cookie) return; - if (tokens.count) { - if (searchController.searchBar.text.length) { - _resultsTitle = [NSString stringWithFormat:@"Showing %@ games matching “%@”", [tokens[0] representedObject], searchController.searchBar.text]; - } - else { - _resultsTitle = [NSString stringWithFormat:@"Showing %@ games", [tokens[0] representedObject]]; - } - } - else { - _resultsTitle = [NSString stringWithFormat:@"Showing results for “%@”", searchController.searchBar.text]; - } - _results = results; - _results = [results sortedArrayUsingComparator:^NSComparisonResult(GBHubGame *obj1, GBHubGame *obj2) { - return [obj1.title.lowercaseString compare:obj2.title.lowercaseString]; - }]; - [self.tableView reloadData]; - if (_results.count) { - [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0,0} length:2] - atScrollPosition:UITableViewScrollPositionTop - animated:false]; - } - }); - }); - } - else { - if (_showingAllGames) return; - cookie++; - _showingAllGames = true; - _resultsTitle = @"Showing all games"; - _results = [GBHub.sharedHub.allGames.allValues sortedArrayUsingComparator:^NSComparisonResult(GBHubGame *obj1, GBHubGame *obj2) { - return [obj1.title.lowercaseString compare:obj2.title.lowercaseString]; - }]; - [self.tableView reloadData]; - if (_results.count) { - [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0,0} length:2] - atScrollPosition:UITableViewScrollPositionTop - animated:false]; - } - return; - } -} - -- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (GBHub.sharedHub.status == GBHubStatusReady) return indexPath; - return nil; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (GBHub.sharedHub.status != GBHubStatusReady) return; - if (indexPath.section == 1) { - NSString *tag = GBHub.sharedHub.sortedTags[indexPath.item]; - if (@available(iOS 13.0, *)) { - UISearchToken *token = [UISearchToken tokenWithIcon:nil - text:tag]; - token.representedObject = tag; - [_searchController.searchBar.searchTextField insertToken:token - atIndex:0]; - } - else { - _searchController.searchBar.text = tag; - } - [_searchController setActive:true]; - return; - } - - GBHubCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - if ([cell isKindOfClass:[GBHubCell class]]) { - GBHubGameViewController *controller = [[GBHubGameViewController alloc] initWithGame:cell.game]; - [self.navigationController pushViewController:controller animated:true]; - } -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - UIColor *linkColor; - if (@available(iOS 13.0, *)) { - linkColor = UIColor.linkColor; - - } - else { - linkColor = UIColor.blueColor; - } - - if (section != [self numberOfSectionsInTableView:nil] - 1) return; - UITableViewHeaderFooterView *footer = (UITableViewHeaderFooterView *)view; - NSMutableAttributedString *string = footer.textLabel.attributedText.mutableCopy; - - [string addAttributes:@{ - @"GBLinkAttribute": [NSURL URLWithString:@"https://hh.gbdev.io"], - NSForegroundColorAttributeName: linkColor, - } range:[string.string rangeOfString:@"Homebrew Hub"]]; - - footer.textLabel.attributedText = string; - footer.textLabel.userInteractionEnabled = true; - [footer.textLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(tappedFooterLabel:)]]; -} - -- (void)tappedFooterLabel:(UITapGestureRecognizer *)tap -{ - unsigned characterIndex = [(UILabel *)tap.view characterAtTap:tap]; - - NSURL *url = [((UILabel *)tap.view).attributedText attribute:@"GBLinkAttribute" atIndex:characterIndex effectiveRange:NULL]; - - if (url) { - [[UIApplication sharedApplication] openURL:url options:nil completionHandler:nil]; - } -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - if (section != [self numberOfSectionsInTableView:tableView] - 1) return nil; - return @"Powered by Homebrew Hub"; -} -@end diff --git a/iOS/GBLibraryViewController.m b/iOS/GBLibraryViewController.m @@ -1,6 +1,5 @@ #import "GBLibraryViewController.h" #import "GBROMViewController.h" -#import "GBHubViewController.h" #import "GBViewController.h" #import "GBROMManager.h" @@ -18,40 +17,10 @@ return ret; } -- (void)viewDidLoad + +- (instancetype)init { - [super viewDidLoad]; - - self.viewControllers = @[ - [self.class wrapViewController:[[GBROMViewController alloc] init]], - [self.class wrapViewController:[[GBHubViewController alloc] init]], - ]; - if (@available(iOS 13.0, *)) { - UIEdgeInsets insets = [UIApplication sharedApplication].keyWindow.safeAreaInsets; - bool hasHomeButton = insets.bottom == 0; - bool isPad = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; - NSString *symbol = isPad? @"ipad" : @"iphone"; - if (hasHomeButton) { - symbol = [symbol stringByAppendingString:@".homebutton"]; - } - else if (!isPad) { - if (@available(iOS 16.1, *)) { - if (MAX(insets.left, MAX(insets.right, MAX(insets.top, insets.bottom))) > 51) { - symbol = @"iphone.gen3"; - } - else { - symbol = @"iphone.gen2"; - } - } - } - self.viewControllers[0].tabBarItem.image = [UIImage systemImageNamed:symbol] ?: [UIImage systemImageNamed:@"folder.fill"]; - self.viewControllers.lastObject.tabBarItem.image = [UIImage systemImageNamed:@"globe"]; - } - else { - self.viewControllers[0].tabBarItem.image = [UIImage imageNamed:@"FolderTemplate"]; - self.viewControllers[1].tabBarItem.image = [UIImage imageNamed:@"GlobeTemplate"]; - } + return (GBLibraryViewController *)[self.class wrapViewController:[[GBROMViewController alloc] init]]; } - @end diff --git a/iOS/GlobeTemplate@2x.png b/iOS/GlobeTemplate@2x.png Binary files differ. diff --git a/iOS/GlobeTemplate@3x.png b/iOS/GlobeTemplate@3x.png Binary files differ.

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.