Path: blob/main/Natives/LauncherNavigationController.m
589 views
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>1#import "authenticator/BaseAuthenticator.h"2#import "AFNetworking.h"3#import "ALTServerConnection.h"4#import "CustomControlsViewController.h"5#import "DownloadProgressViewController.h"6#import "JavaGUIViewController.h"7#import "LauncherMenuViewController.h"8#import "LauncherNavigationController.h"9#import "LauncherPreferences.h"10#import "MinecraftResourceDownloadTask.h"11#import "MinecraftResourceUtils.h"12#import "PickTextField.h"13#import "PLPickerView.h"14#import "PLProfiles.h"15#import "UIKit+AFNetworking.h"16#import "UIKit+hook.h"17#import "ios_uikit_bridge.h"18#import "utils.h"1920#include <sys/time.h>2122#define AUTORESIZE_MASKS UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin2324static void *ProgressObserverContext = &ProgressObserverContext;2526@interface LauncherNavigationController () <UIDocumentPickerDelegate, UIPickerViewDataSource, PLPickerViewDelegate, UIPopoverPresentationControllerDelegate> {27}2829@property(nonatomic) MinecraftResourceDownloadTask* task;30@property(nonatomic) DownloadProgressViewController* progressVC;31@property(nonatomic) PLPickerView* versionPickerView;32@property(nonatomic) UITextField* versionTextField;33@property(nonatomic) int profileSelectedAt;3435@end3637@implementation LauncherNavigationController3839- (void)viewDidLoad40{41[super viewDidLoad];4243if ([self respondsToSelector:@selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]) {44[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];45}4647self.versionTextField = [[PickTextField alloc] initWithFrame:CGRectMake(4, 4, self.toolbar.frame.size.width * 0.8 - 8, self.toolbar.frame.size.height - 8)];48[self.versionTextField addTarget:self.versionTextField action:@selector(resignFirstResponder) forControlEvents:UIControlEventEditingDidEndOnExit];49self.versionTextField.autoresizingMask = AUTORESIZE_MASKS;50self.versionTextField.placeholder = @"Specify version...";51self.versionTextField.leftView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];52self.versionTextField.rightView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"SpinnerArrow"] _imageWithSize:CGSizeMake(30, 30)]];53self.versionTextField.rightView.frame = CGRectMake(0, 0, self.versionTextField.frame.size.height * 0.9, self.versionTextField.frame.size.height * 0.9);54self.versionTextField.leftViewMode = UITextFieldViewModeAlways;55self.versionTextField.rightViewMode = UITextFieldViewModeAlways;56self.versionTextField.textAlignment = NSTextAlignmentCenter;5758self.versionPickerView = [[PLPickerView alloc] init];59self.versionPickerView.delegate = self;60self.versionPickerView.dataSource = self;61UIToolbar *versionPickToolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.frame.size.width, 44.0)];6263[self reloadProfileList];6465UIBarButtonItem *versionFlexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:self action:nil];66UIBarButtonItem *versionDoneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(versionClosePicker)];67versionPickToolbar.items = @[versionFlexibleSpace, versionDoneButton];68self.versionTextField.inputAccessoryView = versionPickToolbar;69self.versionTextField.inputView = self.versionPickerView;7071UIView *targetToolbar = self.toolbar;72[targetToolbar addSubview:self.versionTextField];7374self.progressViewMain = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 0, self.toolbar.frame.size.width, 4)];75self.progressViewMain.autoresizingMask = AUTORESIZE_MASKS;76self.progressViewMain.hidden = YES;77[targetToolbar addSubview:self.progressViewMain];7879self.buttonInstall = [UIButton buttonWithType:UIButtonTypeSystem];80setButtonPointerInteraction(self.buttonInstall);81[self.buttonInstall setTitle:localize(@"Play", nil) forState:UIControlStateNormal];82self.buttonInstall.autoresizingMask = AUTORESIZE_MASKS;83self.buttonInstall.backgroundColor = [UIColor colorWithRed:54/255.0 green:176/255.0 blue:48/255.0 alpha:1.0];84self.buttonInstall.layer.cornerRadius = 5;85self.buttonInstall.frame = CGRectMake(self.toolbar.frame.size.width * 0.8, 4, self.toolbar.frame.size.width * 0.2, self.toolbar.frame.size.height - 8);86self.buttonInstall.tintColor = UIColor.whiteColor;87self.buttonInstall.enabled = NO;88[self.buttonInstall addTarget:self action:@selector(performInstallOrShowDetails:) forControlEvents:UIControlEventPrimaryActionTriggered];89[targetToolbar addSubview:self.buttonInstall];9091self.progressText = [[UILabel alloc] initWithFrame:self.versionTextField.frame];92self.progressText.adjustsFontSizeToFitWidth = YES;93self.progressText.autoresizingMask = AUTORESIZE_MASKS;94self.progressText.font = [self.progressText.font fontWithSize:16];95self.progressText.textAlignment = NSTextAlignmentCenter;96self.progressText.userInteractionEnabled = NO;97[targetToolbar addSubview:self.progressText];9899[self fetchRemoteVersionList];100[NSNotificationCenter.defaultCenter addObserver:self101selector:@selector(receiveNotification:)102name:@"InstallModpack"103object:nil];104105if ([BaseAuthenticator.current isKindOfClass:MicrosoftAuthenticator.class]) {106// Perform token refreshment on startup107[self setInteractionEnabled:NO forDownloading:NO];108id callback = ^(NSString* status, BOOL success) {109self.progressText.text = status;110if (status == nil) {111[self setInteractionEnabled:YES forDownloading:NO];112} else if (!success) {113showDialog(localize(@"Error", nil), status);114}115};116[BaseAuthenticator.current refreshTokenWithCallback:callback];117}118}119120- (BOOL)isVersionInstalled:(NSString *)versionId {121NSString *localPath = [NSString stringWithFormat:@"%s/versions/%@", getenv("POJAV_GAME_DIR"), versionId];122BOOL isDirectory;123[NSFileManager.defaultManager fileExistsAtPath:localPath isDirectory:&isDirectory];124return isDirectory;125}126127- (void)fetchLocalVersionList {128if (!localVersionList) {129localVersionList = [NSMutableArray new];130}131[localVersionList removeAllObjects];132133NSFileManager *fileManager = [NSFileManager defaultManager];134NSString *versionPath = [NSString stringWithFormat:@"%s/versions/", getenv("POJAV_GAME_DIR")];135NSArray *list = [fileManager contentsOfDirectoryAtPath:versionPath error:Nil];136for (NSString *versionId in list) {137if (![self isVersionInstalled:versionId]) continue;138[localVersionList addObject:@{139@"id": versionId,140@"type": @"custom"141}];142}143}144145- (void)fetchRemoteVersionList {146self.buttonInstall.enabled = NO;147remoteVersionList = @[148@{@"id": @"latest-release", @"type": @"release"},149@{@"id": @"latest-snapshot", @"type": @"snapshot"}150].mutableCopy;151152AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];153[manager GET:@"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" parameters:nil headers:nil progress:^(NSProgress * _Nonnull progress) {154self.progressViewMain.progress = progress.fractionCompleted;155} success:^(NSURLSessionTask *task, NSDictionary *responseObject) {156[remoteVersionList addObjectsFromArray:responseObject[@"versions"]];157NSDebugLog(@"[VersionList] Got %d versions", remoteVersionList.count);158setPrefObject(@"internal.latest_version", responseObject[@"latest"]);159self.buttonInstall.enabled = YES;160} failure:^(NSURLSessionTask *operation, NSError *error) {161NSDebugLog(@"[VersionList] Warning: Unable to fetch version list: %@", error.localizedDescription);162self.buttonInstall.enabled = YES;163}];164}165166// Invoked by: startup, instance change event167- (void)reloadProfileList {168// Reload local version list169[self fetchLocalVersionList];170// Reload launcher_profiles.json171[PLProfiles updateCurrent];172[self.versionPickerView reloadAllComponents];173// Reload selected profile info174self.profileSelectedAt = [PLProfiles.current.profiles.allKeys indexOfObject:PLProfiles.current.selectedProfileName];175if (self.profileSelectedAt == -1) {176// This instance has no profiles?177return;178}179[self.versionPickerView selectRow:self.profileSelectedAt inComponent:0 animated:NO];180[self pickerView:self.versionPickerView didSelectRow:self.profileSelectedAt inComponent:0];181}182183#pragma mark - Options184- (void)enterCustomControls {185CustomControlsViewController *vc = [[CustomControlsViewController alloc] init];186vc.modalPresentationStyle = UIModalPresentationOverFullScreen;187vc.setDefaultCtrl = ^(NSString *name){188setPrefObject(@"control.default_ctrl", name);189};190vc.getDefaultCtrl = ^{191return getPrefObject(@"control.default_ctrl");192};193[self presentViewController:vc animated:YES completion:nil];194}195196- (void)enterModInstaller {197UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc]198initForOpeningContentTypes:@[[UTType typeWithMIMEType:@"application/java-archive"]]199asCopy:YES];200documentPicker.delegate = self;201documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;202[self presentViewController:documentPicker animated:YES completion:nil];203}204205- (void)enterModInstallerWithPath:(NSString *)path hitEnterAfterWindowShown:(BOOL)hitEnter {206JavaGUIViewController *vc = [[JavaGUIViewController alloc] init];207vc.filepath = path;208vc.hitEnterAfterWindowShown = hitEnter;209if (!vc.requiredJavaVersion) {210return;211}212[self invokeAfterJITEnabled:^{213vc.modalPresentationStyle = UIModalPresentationFullScreen;214NSLog(@"[ModInstaller] launching %@", vc.filepath);215[self presentViewController:vc animated:YES completion:nil];216}];217}218219- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {220[self enterModInstallerWithPath:url.path hitEnterAfterWindowShown:NO];221}222223- (void)setInteractionEnabled:(BOOL)enabled forDownloading:(BOOL)downloading {224for (UIControl *view in self.toolbar.subviews) {225if ([view isKindOfClass:UIControl.class]) {226view.alpha = enabled ? 1 : 0.2;227view.enabled = enabled;228}229}230self.progressViewMain.hidden = enabled;231self.progressText.text = nil;232if (downloading) {233[self.buttonInstall setTitle:localize(enabled ? @"Play" : @"Details", nil) forState:UIControlStateNormal];234self.buttonInstall.alpha = 1;235self.buttonInstall.enabled = YES;236}237UIApplication.sharedApplication.idleTimerDisabled = !enabled;238}239240- (void)launchMinecraft:(UIButton *)sender {241if (!self.versionTextField.hasText) {242[self.versionTextField becomeFirstResponder];243return;244}245246if (BaseAuthenticator.current == nil) {247// Present the account selector if none selected248UIViewController *view = [(UINavigationController *)self.splitViewController.viewControllers[0]249viewControllers][0];250[view performSelector:@selector(selectAccount:) withObject:sender];251return;252}253254[self setInteractionEnabled:NO forDownloading:YES];255256NSString *versionId = PLProfiles.current.profiles[self.versionTextField.text][@"lastVersionId"];257NSDictionary *object = [remoteVersionList filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(id == %@)", versionId]].firstObject;258if (!object) {259object = @{260@"id": versionId,261@"type": @"custom"262};263}264265self.task = [MinecraftResourceDownloadTask new];266dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{267__weak LauncherNavigationController *weakSelf = self;268self.task.handleError = ^{269dispatch_async(dispatch_get_main_queue(), ^{270[weakSelf setInteractionEnabled:YES forDownloading:YES];271weakSelf.task = nil;272weakSelf.progressVC = nil;273});274};275[self.task downloadVersion:object];276dispatch_async(dispatch_get_main_queue(), ^{277self.progressViewMain.observedProgress = self.task.progress;278[self.task.progress addObserver:self279forKeyPath:@"fractionCompleted"280options:NSKeyValueObservingOptionInitial281context:ProgressObserverContext];282});283});284}285286- (void)performInstallOrShowDetails:(UIButton *)sender {287if (self.task) {288if (!self.progressVC) {289self.progressVC = [[DownloadProgressViewController alloc] initWithTask:self.task];290}291UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:self.progressVC];292nav.modalPresentationStyle = UIModalPresentationPopover;293nav.popoverPresentationController.sourceView = sender;294[self presentViewController:nav animated:YES completion:nil];295} else {296[self launchMinecraft:sender];297}298}299300- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {301if (context != ProgressObserverContext) {302[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];303return;304}305306// Calculate download speed and ETA307static CGFloat lastMsTime;308static NSUInteger lastSecTime, lastCompletedUnitCount;309NSProgress *progress = self.task.textProgress;310struct timeval tv;311gettimeofday(&tv, NULL);312NSInteger completedUnitCount = self.task.progress.totalUnitCount * self.task.progress.fractionCompleted;313progress.completedUnitCount = completedUnitCount;314if (lastSecTime < tv.tv_sec) {315CGFloat currentTime = tv.tv_sec + tv.tv_usec / 1000000.0;316NSInteger throughput = (completedUnitCount - lastCompletedUnitCount) / (currentTime - lastMsTime);317progress.throughput = @(throughput);318progress.estimatedTimeRemaining = @((progress.totalUnitCount - completedUnitCount) / throughput);319lastCompletedUnitCount = completedUnitCount;320lastSecTime = tv.tv_sec;321lastMsTime = currentTime;322}323324dispatch_async(dispatch_get_main_queue(), ^{325self.progressText.text = progress.localizedAdditionalDescription;326327if (!progress.finished) return;328[self.progressVC dismissModalViewControllerAnimated:NO];329330self.progressViewMain.observedProgress = nil;331if (self.task.metadata) {332[self invokeAfterJITEnabled:^{333UIKit_launchMinecraftSurfaceVC(self.view.window, self.task.metadata);334}];335} else {336self.task = nil;337[self setInteractionEnabled:YES forDownloading:YES];338[self reloadProfileList];339}340});341}342343- (void)receiveNotification:(NSNotification *)notification {344if (![notification.name isEqualToString:@"InstallModpack"]) {345return;346}347[self setInteractionEnabled:NO forDownloading:YES];348self.task = [MinecraftResourceDownloadTask new];349NSDictionary *userInfo = notification.userInfo;350dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{351__weak LauncherNavigationController *weakSelf = self;352self.task.handleError = ^{353dispatch_async(dispatch_get_main_queue(), ^{354[weakSelf setInteractionEnabled:YES forDownloading:YES];355weakSelf.task = nil;356weakSelf.progressVC = nil;357});358};359[self.task downloadModpackFromAPI:notification.object detail:userInfo[@"detail"] atIndex:[userInfo[@"index"] unsignedLongValue]];360dispatch_async(dispatch_get_main_queue(), ^{361self.progressViewMain.observedProgress = self.task.progress;362[self.task.progress addObserver:self363forKeyPath:@"fractionCompleted"364options:NSKeyValueObservingOptionInitial365context:ProgressObserverContext];366});367});368}369370- (void)invokeAfterJITEnabled:(void(^)(void))handler {371localVersionList = remoteVersionList = nil;372BOOL hasTrollStoreJIT = getEntitlementValue(@"com.apple.private.local.sandboxed-jit");373374if (isJITEnabled(false)) {375[ALTServerManager.sharedManager stopDiscovering];376handler();377return;378} else if (hasTrollStoreJIT) {379NSURL *jitURL = [NSURL URLWithString:[NSString stringWithFormat:@"apple-magnifier://enable-jit?bundle-id=%@", NSBundle.mainBundle.bundleIdentifier]];380[UIApplication.sharedApplication openURL:jitURL options:@{} completionHandler:nil];381// Do not return, wait for TrollStore to enable JIT and jump back382} else if (getPrefBool(@"debug.debug_skip_wait_jit")) {383NSLog(@"Debug option skipped waiting for JIT. Java might not work.");384handler();385return;386}387388self.progressText.text = localize(@"launcher.wait_jit.title", nil);389390UIAlertController* alert = [UIAlertController alertControllerWithTitle:localize(@"launcher.wait_jit.title", nil)391message:hasTrollStoreJIT ? localize(@"launcher.wait_jit_trollstore.message", nil) : localize(@"launcher.wait_jit.message", nil)392preferredStyle:UIAlertControllerStyleAlert];393/* TODO:394UIAlertAction *cancel = [UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^{395396}];397[alert addAction:cancel];398*/399[self presentViewController:alert animated:YES completion:nil];400401dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{402while (!isJITEnabled(false)) {403// Perform check for every 200ms404usleep(1000*200);405}406dispatch_async(dispatch_get_main_queue(), ^{407[alert dismissViewControllerAnimated:YES completion:handler];408});409});410}411412#pragma mark - UIPopoverPresentationControllerDelegate413- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection {414return UIModalPresentationNone;415}416417#pragma mark - UIPickerView stuff418- (void)pickerView:(PLPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {419self.profileSelectedAt = row;420//((UIImageView *)self.versionTextField.leftView).image = [pickerView imageAtRow:row column:component];421((UIImageView *)self.versionTextField.leftView).image = [pickerView imageAtRow:row column:component];422self.versionTextField.text = [self pickerView:pickerView titleForRow:row forComponent:component];423PLProfiles.current.selectedProfileName = self.versionTextField.text;424}425426- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {427return 1;428}429430- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {431return PLProfiles.current.profiles.count;432}433434- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {435return PLProfiles.current.profiles.allValues[row][@"name"];436}437438- (void)pickerView:(UIPickerView *)pickerView enumerateImageView:(UIImageView *)imageView forRow:(NSInteger)row forComponent:(NSInteger)component {439UIImage *fallbackImage = [[UIImage imageNamed:@"DefaultProfile"] _imageWithSize:CGSizeMake(40, 40)];440NSString *urlString = PLProfiles.current.profiles.allValues[row][@"icon"];441[imageView setImageWithURL:[NSURL URLWithString:urlString] placeholderImage:fallbackImage];442}443444- (void)versionClosePicker {445[self.versionTextField endEditing:YES];446[self pickerView:self.versionPickerView didSelectRow:[self.versionPickerView selectedRowInComponent:0] inComponent:0];447}448449#pragma mark - View controller UI mode450451- (BOOL)prefersHomeIndicatorAutoHidden {452return YES;453}454455- (void)viewDidLayoutSubviews {456[super viewDidLayoutSubviews];457[sidebarViewController updateAccountInfo];458}459460@end461462463