mirror of https://github.com/LIJI32/SameBoy.git
Homebrew Hub integration in iOS
This commit is contained in:
parent
0bc0618a6a
commit
93cc19b3b9
|
@ -0,0 +1,41 @@
|
|||
#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
|
|
@ -0,0 +1,339 @@
|
|||
#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"]) {
|
||||
@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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 int)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
|
|
@ -0,0 +1,8 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
#import "GBHub.h"
|
||||
|
||||
@interface GBHubCell : UITableViewCell
|
||||
|
||||
@property GBHubGame *game;
|
||||
|
||||
@end
|
|
@ -0,0 +1,4 @@
|
|||
#import "GBHubCell.h"
|
||||
|
||||
@implementation GBHubCell
|
||||
@end
|
|
@ -0,0 +1,7 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
#import "GBHub.h"
|
||||
|
||||
@interface GBHubGameViewController : UIViewController
|
||||
- (instancetype)initWithGame:(GBHubGame *)game;
|
||||
@end
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
#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];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object: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
|
|
@ -0,0 +1,6 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface GBHubViewController : UITableViewController
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
#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;
|
||||
}
|
||||
|
||||
- (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;
|
||||
[GBHub.sharedHub refresh];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (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
|
|
@ -1,6 +1,7 @@
|
|||
#import "GBLoadROMTableViewController.h"
|
||||
#import "GBROMManager.h"
|
||||
#import "GBViewController.h"
|
||||
#import "GBHubViewController.h"
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
|
@ -32,7 +33,7 @@
|
|||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
if (section == 1) return 2;
|
||||
if (section == 1) return 3;
|
||||
return [GBROMManager sharedManager].allROMs.count;
|
||||
}
|
||||
|
||||
|
@ -42,7 +43,8 @@
|
|||
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
|
||||
switch (indexPath.item) {
|
||||
case 0: cell.textLabel.text = @"Import ROM files"; break;
|
||||
case 1: cell.textLabel.text = @"Show Library in Files"; break;
|
||||
case 1: cell.textLabel.text = @"Browse Homebrew Hub"; break;
|
||||
case 2: cell.textLabel.text = @"Show Library in Files"; break;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
@ -139,6 +141,11 @@
|
|||
return;
|
||||
}
|
||||
case 1: {
|
||||
[self.navigationController pushViewController:[[GBHubViewController alloc] init]
|
||||
animated:true];
|
||||
return;
|
||||
}
|
||||
case 2: {
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"shareddocuments://%@", NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject]]
|
||||
options:nil
|
||||
completionHandler:nil];
|
||||
|
@ -323,4 +330,9 @@ contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
|
|||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
- (NSString *)autosaveStateFileForROM:(NSString *)rom;
|
||||
- (NSString *)stateFile:(unsigned)index forROM:(NSString *)rom;
|
||||
- (NSString *)importROM:(NSString *)romFile keepOriginal:(bool)keep;
|
||||
- (NSString *)importROM:(NSString *)romFile withName:(NSString *)friendlyName keepOriginal:(bool)keep;
|
||||
- (NSString *)renameROM:(NSString *)rom toName:(NSString *)newName;
|
||||
- (NSString *)duplicateROM:(NSString *)rom;
|
||||
- (void)deleteROM:(NSString *)rom;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UILabel (TapLocation)
|
||||
- (unsigned)characterAtTap:(UITapGestureRecognizer *)tap;
|
||||
@end
|
|
@ -0,0 +1,27 @@
|
|||
#import "UILabel+TapLocation.h"
|
||||
|
||||
@implementation UILabel (TapLocation)
|
||||
|
||||
- (unsigned)characterAtTap:(UITapGestureRecognizer *)tap
|
||||
{
|
||||
CGPoint tapLocation = [tap locationInView:self];
|
||||
|
||||
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
|
||||
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
|
||||
[textStorage addLayoutManager:layoutManager];
|
||||
|
||||
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(self.frame.size.width,
|
||||
self.frame.size.height + 256)];
|
||||
textContainer.lineFragmentPadding = 0;
|
||||
textContainer.maximumNumberOfLines = 256;
|
||||
textContainer.lineBreakMode = self.lineBreakMode;
|
||||
|
||||
[layoutManager addTextContainer:textContainer];
|
||||
|
||||
return [layoutManager characterIndexForPoint:tapLocation
|
||||
inTextContainer:textContainer
|
||||
fractionOfDistanceBetweenInsertionPoints:NULL];
|
||||
|
||||
}
|
||||
|
||||
@end
|
Loading…
Reference in New Issue