mirror of https://github.com/LIJI32/SameBoy.git
403 lines
17 KiB
Objective-C
403 lines
17 KiB
Objective-C
#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
|