SameBoy/iOS/GBHubViewController.m

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