diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea7..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 5dfd401..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Form to suggest a feature -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/assets/images/auth.png b/assets/images/auth.png index 999befe..341b02f 100644 Binary files a/assets/images/auth.png and b/assets/images/auth.png differ diff --git a/assets/images/browse.png b/assets/images/browse.png new file mode 100644 index 0000000..1ebf03c Binary files /dev/null and b/assets/images/browse.png differ diff --git a/assets/images/cloud.png b/assets/images/matchmaker.png similarity index 100% rename from assets/images/cloud.png rename to assets/images/matchmaker.png diff --git a/assets/lawin/lawin.bat b/assets/lawin/lawin.bat deleted file mode 100644 index 5a40289..0000000 --- a/assets/lawin/lawin.bat +++ /dev/null @@ -1,3 +0,0 @@ -cd %UserProfile%\.reboot_launcher\backend-lawin -lawinserver-win.exe -pause \ No newline at end of file diff --git a/assets/lawin/run.bat b/assets/lawin/run.bat new file mode 100644 index 0000000..1785ca1 --- /dev/null +++ b/assets/lawin/run.bat @@ -0,0 +1,2 @@ +lawinserver-win.exe +pause \ No newline at end of file diff --git a/assets/builds/stop.bat b/assets/misc/stop.bat similarity index 100% rename from assets/builds/stop.bat rename to assets/misc/stop.bat diff --git a/assets/browse/watch.exe b/assets/misc/watch.exe similarity index 73% rename from assets/browse/watch.exe rename to assets/misc/watch.exe index 0627d10..c48be37 100644 Binary files a/assets/browse/watch.exe and b/assets/misc/watch.exe differ diff --git a/assets/builds/winrar.exe b/assets/misc/winrar.exe similarity index 100% rename from assets/builds/winrar.exe rename to assets/misc/winrar.exe diff --git a/lib/cli.dart b/lib/cli.dart deleted file mode 100644 index 58edec4..0000000 --- a/lib/cli.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:reboot_launcher/src/cli/compatibility.dart'; -import 'package:reboot_launcher/src/cli/config.dart'; -import 'package:reboot_launcher/src/cli/game.dart'; -import 'package:reboot_launcher/src/cli/reboot.dart'; -import 'package:reboot_launcher/src/cli/server.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/util/patcher.dart'; -import 'package:reboot_launcher/src/util/reboot.dart'; -import 'package:reboot_launcher/src/util/server.dart' as server; - -late String? username; -late bool host; -late bool verbose; -late String dll; -late FortniteVersion version; -late bool autoRestart; - -void main(List args) async { - stdout.writeln("Reboot Launcher"); - stdout.writeln("Wrote by Auties00"); - stdout.writeln("Version 5.3"); - - kill(); - - var gameJson = await getControllerJson("game"); - var serverJson = await getControllerJson("server"); - var settingsJson = await getControllerJson("settings"); - var versions = getVersions(gameJson); - var parser = ArgParser() - ..addCommand("list") - ..addCommand("launch") - ..addOption("version") - ..addOption("username") - ..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson)) - ..addOption("server-host") - ..addOption("server-port") - ..addOption("matchmaking-address") - ..addOption("dll", defaultsTo: settingsJson["reboot"] ?? rebootDllFile) - ..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true) - ..addFlag("log", defaultsTo: false) - ..addFlag("host", defaultsTo: false) - ..addFlag("auto-restart", defaultsTo: false, negatable: true); - var result = parser.parse(args); - if (result.command?.name == "list") { - stdout.writeln("Versions list: "); - versions.map((entry) => "${entry.location.path}(${entry.name})") - .forEach((element) => stdout.writeln(element)); - return; - } - - dll = result["dll"]; - host = result["host"]; - username = result["username"] ?? gameJson["username"]; - verbose = result["log"]; - - version = _createVersion(gameJson["version"], result["version"], versions); - await downloadRequiredDLLs(); - if(result["update"]) { - stdout.writeln("Updating reboot dll..."); - try { - await downloadRebootDll(rebootDownloadUrl, 0); - }catch(error){ - stderr.writeln("Cannot update reboot dll: $error"); - } - } - - stdout.writeln("Launching game..."); - var executable = await version.executable; - if(executable == null){ - throw Exception("Missing game executable at: ${version.location.path}"); - } - - var serverType = getServerType(result); - var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"]; - var serverPort = result["server-port"] ?? serverJson["${serverType.id}_port"]; - var started = await startServer(serverHost, serverPort, serverType); - if(!started){ - stderr.writeln("Cannot start server!"); - return; - } - - server.writeMatchmakingIp(result["matchmaking-address"]); - autoRestart = result["auto-restart"]; - await startGame(); -} - -FortniteVersion _createVersion(String? versionName, String? versionPath, List versions) { - if (versionPath != null) { - return FortniteVersion(name: "dummy", location: Directory(versionPath)); - } - - if(versionName != null){ - try { - return versions.firstWhere((element) => versionName == element.name); - }catch(_){ - throw Exception("Cannot find version $versionName"); - } - } - - throw Exception( - "Specify a version using --version or open the launcher GUI and select it manually"); -} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c7013da..0ece01b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,25 +1,30 @@ import 'dart:async'; +import 'package:app_links/app_links.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:reboot_launcher/src/util/error.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/ui/controller/build_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/page/home_page.dart'; -import 'package:reboot_launcher/supabase.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/controller/update_controller.dart'; +import 'package:reboot_launcher/src/dialog/message.dart'; +import 'package:reboot_launcher/src/interactive/error.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/interactive/server.dart'; +import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/util/watch.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; -import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:url_protocol/url_protocol.dart'; -const double kDefaultWindowWidth = 1024; +const double kDefaultWindowWidth = 1536; const double kDefaultWindowHeight = 1024; -final GlobalKey appKey = GlobalKey(); +const String kCustomUrlSchema = "reboot"; void main() async { runZonedGuarded(() async { @@ -30,44 +35,109 @@ void main() async { ); WidgetsFlutterBinding.ensureInitialized(); await SystemTheme.accentColor.load(); - await GetStorage.init("reboot_game"); - await GetStorage.init("reboot_server"); - await GetStorage.init("reboot_update"); - await GetStorage.init("reboot_settings"); - await GetStorage.init("reboot_hosting"); - var gameController = GameController(); - Get.put(gameController); - Get.put(ServerController()); - Get.put(BuildController()); - Get.put(SettingsController()); - Get.put(HostingController()); - await windowManager.ensureInitialized(); - var controller = Get.find(); - var size = Size(controller.width, controller.height); - await windowManager.setSize(size); - if(controller.offsetX != null && controller.offsetY != null){ - await windowManager.setPosition(Offset(controller.offsetX!, controller.offsetY!)); - }else { - await windowManager.setAlignment(Alignment.center); - }; - await Window.initialize(); - await Window.setEffect( - effect: WindowEffect.acrylic, - color: Colors.transparent, - dark: SystemTheme.isDarkMode - ); - var supabase = Supabase.instance.client; - await supabase.from('hosts') - .delete() - .match({'id': gameController.uuid}); + var storageError = await _initStorage(); + var urlError = await _initUrlHandler(); + var windowError = await _initWindow(); + var observerError = _initObservers(); runApp(const RebootApplication()); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors([urlError, storageError, windowError, observerError])); }, (error, stack) => onError(error, stack, false), zoneSpecification: ZoneSpecification( - handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) + handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) )); } +void _handleErrors(List errors) => errors.where((element) => element != null).forEach((element) => onError(element, null, false)); + +Future _initUrlHandler() async { + try { + registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); + var appLinks = AppLinks(); + var initialUrl = await appLinks.getInitialAppLink(); + if(initialUrl != null) { + + } + + var gameController = Get.find(); + var matchmakerController = Get.find(); + appLinks.uriLinkStream.listen((uri) { + var uuid = _parseCustomUrl(uri); + var server = gameController.findServerById(uuid); + if(server != null) { + matchmakerController.joinServer(server); + return; + } + + showMessage( + "No server found: invalid or expired link", + duration: snackbarLongDuration, + severity: InfoBarSeverity.error + ); + }); + return null; + }catch(error) { + return error; + } +} + +String _parseCustomUrl(Uri uri) => uri.host; + +Future _initWindow() async { + try { + await windowManager.ensureInitialized(); + var settingsController = Get.find(); + var size = Size(settingsController.width, settingsController.height); + await windowManager.setSize(size); + if(settingsController.offsetX != null && settingsController.offsetY != null){ + await windowManager.setPosition(Offset(settingsController.offsetX!, settingsController.offsetY!)); + }else { + await windowManager.setAlignment(Alignment.center); + } + return null; + }catch(error) { + return error; + } +} + +Object? _initObservers() { + try { + var gameController = Get.find(); + var gameInstance = gameController.instance.value; + gameInstance?.startObserver(); + var hostingController = Get.find(); + var hostingInstance = hostingController.instance.value; + hostingInstance?.startObserver(); + return null; + }catch(error) { + return error; + } +} + +Future _initStorage() async { + try { + await GetStorage("reboot_game", settingsDirectory.path).initStorage; + await GetStorage("reboot_authenticator", settingsDirectory.path).initStorage; + await GetStorage("reboot_matchmaker", settingsDirectory.path).initStorage; + await GetStorage("reboot_update", settingsDirectory.path).initStorage; + await GetStorage("reboot_settings", settingsDirectory.path).initStorage; + await GetStorage("reboot_hosting", settingsDirectory.path).initStorage; + Get.put(GameController()); + Get.put(AuthenticatorController()); + Get.put(MatchmakerController()); + Get.put(BuildController()); + Get.put(SettingsController()); + Get.put(HostingController()); + var updateController = UpdateController(); + Get.put(updateController); + updateController.update(); + return null; + }catch(error) { + print(error); + return error; + } +} + class RebootApplication extends StatefulWidget { const RebootApplication({Key? key}) : super(key: key); diff --git a/lib/src/cli/compatibility.dart b/lib/src/cli/compatibility.dart deleted file mode 100644 index 9756966..0000000 --- a/lib/src/cli/compatibility.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:collection'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io'; - -import 'package:ffi/ffi.dart'; -import 'package:win32/win32.dart'; - -Future> getControllerJson(String name) async { - var folder = await _getWindowsPath(FOLDERID_Documents); - if(folder == null){ - throw Exception("Missing documents folder"); - } - - var file = File("$folder\\$name.gs"); - if(!file.existsSync()){ - return HashMap(); - } - - return jsonDecode(file.readAsStringSync()); -} - -Future _getWindowsPath(String folderID) { - final Pointer> pathPtrPtr = calloc>(); - final Pointer knownFolderID = calloc()..ref.setGUID(folderID); - - try { - final int hr = SHGetKnownFolderPath( - knownFolderID, - KF_FLAG_DEFAULT, - NULL, - pathPtrPtr, - ); - - if (FAILED(hr)) { - if (hr == E_INVALIDARG || hr == E_FAIL) { - throw WindowsException(hr); - } - return Future.value(); - } - - final String path = pathPtrPtr.value.toDartString(); - return Future.value(path); - } finally { - calloc.free(pathPtrPtr); - calloc.free(knownFolderID); - } -} \ No newline at end of file diff --git a/lib/src/cli/config.dart b/lib/src/cli/config.dart deleted file mode 100644 index 49c8b03..0000000 --- a/lib/src/cli/config.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:convert'; - -import 'package:args/args.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; - -Iterable getServerTypes() => ServerType.values.map((entry) => entry.id); - -String getDefaultServerType(Map json) { - var type = ServerType.values.elementAt(json["type"] ?? 0); - return type.id; -} - -ServerType getServerType(ArgResults result) { - var type = ServerType.of(result["server-type"]); - if(type == null){ - throw Exception("Unknown server type: $result. Use --server-type only with ${getServerTypes().join(", ")}"); - } - - return type; -} - -List getVersions(Map gameJson) { - Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]"); - return iterable.map((entry) => FortniteVersion.fromJson(entry)) - .toList(); -} \ No newline at end of file diff --git a/lib/src/cli/game.dart b/lib/src/cli/game.dart deleted file mode 100644 index fea89f7..0000000 --- a/lib/src/cli/game.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'dart:io'; - -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/cli.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/util/injector.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/process.dart'; -import 'package:reboot_launcher/src/util/server.dart'; - -final List _errorStrings = [ - "port 3551 failed: Connection refused", - "Unable to login to Fortnite servers", - "HTTP 400 response from ", - "Network failure when attempting to check platform restrictions", - "UOnlineAccountCommon::ForceLogout" -]; - -Process? _gameProcess; -Process? _launcherProcess; -Process? _eacProcess; - -Future startGame() async { - await _startLauncherProcess(version); - await _startEacProcess(version); - - var executable = await version.executable; - if (executable == null) { - throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?"); - } - - if (username == null) { - username = "Reboot${host ? 'Host' : 'Player'}"; - stdout.writeln("No username was specified, using $username by default. Use --username to specify one"); - } - - _gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, "")) - ..exitCode.then((_) => _onClose()) - ..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose)); -} - - -Future _startLauncherProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.launcher == null) { - return; - } - - _launcherProcess = await Process.start(dummyVersion.launcher!.path, []); - suspend(_launcherProcess!.pid); -} - -Future _startEacProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.eacExecutable == null) { - return; - } - - _eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []); - suspend(_eacProcess!.pid); -} - -void _onGameOutput(String line, String dll, bool hosting, bool verbose) { - if(verbose) { - stdout.writeln(line); - } - - if(line.contains("Platform has ")){ - _injectOrShowError("cobalt.dll"); - return; - } - - if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { - _onClose(); - return; - } - - if(_errorStrings.any((element) => line.contains(element))){ - stderr.writeln("The backend doesn't work! Token expired"); - _onClose(); - return; - } - - if(line.contains("Region ")){ - if(hosting) { - _injectOrShowError(dll, false); - }else { - _injectOrShowError("console.dll"); - } - - _injectOrShowError("memoryleak.dll"); - } -} - -void _kill() { - _gameProcess?.kill(ProcessSignal.sigabrt); - _launcherProcess?.kill(ProcessSignal.sigabrt); - _eacProcess?.kill(ProcessSignal.sigabrt); -} - -Future _injectOrShowError(String binary, [bool locate = true]) async { - if (_gameProcess == null) { - return; - } - - try { - stdout.writeln("Injecting $binary..."); - var dll = locate ? File("${assetsDirectory.path}\\dlls\\$binary") : File(binary); - if(!dll.existsSync()){ - throw Exception("Cannot inject $dll: missing file"); - } - - await injectDll(_gameProcess!.pid, dll.path); - } catch (exception) { - throw Exception("Cannot inject binary: $binary"); - } -} - -void _onClose() { - _kill(); - sleep(const Duration(seconds: 3)); - stdout.writeln("The game was closed"); - if(autoRestart){ - stdout.writeln("Restarting automatically game"); - startGame(); - return; - } - - exit(0); -} \ No newline at end of file diff --git a/lib/src/cli/reboot.dart b/lib/src/cli/reboot.dart deleted file mode 100644 index b0e122d..0000000 --- a/lib/src/cli/reboot.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive_io.dart'; -import 'package:http/http.dart' as http; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/server.dart'; - -const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll"; -const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll"; -const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memoryleak.dll"; -const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip"; - -Future downloadRequiredDLLs() async { - stdout.writeln("Downloading necessary components..."); - var consoleDll = File("${assetsDirectory.path}\\dlls\\console.dll"); - if(!consoleDll.existsSync()){ - var response = await http.get(Uri.parse(_consoleDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download console.dll"); - } - - await consoleDll.writeAsBytes(response.bodyBytes); - } - - var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll"); - if(!craniumDll.existsSync()){ - var response = await http.get(Uri.parse(_baseDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download cobalt.dll"); - } - - await craniumDll.writeAsBytes(response.bodyBytes); - } - - var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll"); - if(!memoryFixDll.existsSync()){ - var response = await http.get(Uri.parse(_memoryFixDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download memoryleak.dll"); - } - - await memoryFixDll.writeAsBytes(response.bodyBytes); - } - - if(!serverDirectory.existsSync()){ - var response = await http.get(Uri.parse(_embeddedConfigDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download embedded server config"); - } - - var tempZip = File("${tempDirectory.path}/reboot_config.zip"); - await tempZip.writeAsBytes(response.bodyBytes); - await extractFileToDisk(tempZip.path, serverDirectory.path); - } -} \ No newline at end of file diff --git a/lib/src/cli/server.dart b/lib/src/cli/server.dart deleted file mode 100644 index e710b58..0000000 --- a/lib/src/cli/server.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:io'; - -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/util/server.dart' as server; - -Future startServer(String? host, String? port, ServerType type) async { - stdout.writeln("Starting backend server..."); - switch(type){ - case ServerType.local: - var result = await server.ping(host ?? "127.0.0.1", port ?? "3551"); - if(result == null){ - throw Exception("Local backend server is not running"); - } - - stdout.writeln("Detected local backend server"); - return true; - case ServerType.embedded: - stdout.writeln("Starting an embedded server..."); - await server.startServer(false); - var result = await server.ping(host ?? "127.0.0.1", port ?? "3551"); - if(result == null){ - throw Exception("Cannot start embedded server"); - } - - return true; - case ServerType.remote: - if(host == null){ - throw Exception("Missing host for remote server"); - } - - if(port == null){ - throw Exception("Missing host for remote server"); - } - - stdout.writeln("Starting a reverse proxy to $host:$port"); - return await _changeReverseProxyState(host, port) != null; - } -} - -Future _changeReverseProxyState(String host, String port) async { - host = host.trim(); - if(host.isEmpty){ - throw Exception("Missing host name"); - } - - port = port.trim(); - if(port.isEmpty){ - throw Exception("Missing port"); - } - - if(int.tryParse(port) == null){ - throw Exception("Invalid port, use only numbers"); - } - - try{ - var uri = await server.ping(host, port); - if(uri == null){ - return null; - } - - return await server.startRemoteServer(uri); - }catch(error){ - throw Exception("Cannot start reverse proxy"); - } -} - -void kill() async { - var shell = Shell( - commandVerbose: false, - commentVerbose: false, - verbose: false - ); - try { - await shell.run("taskkill /f /im FortniteLauncher.exe"); - await shell.run("taskkill /f /im FortniteClient-Win64-Shipping_EAC.exe"); - }catch(_){ - - } -} diff --git a/lib/src/controller/authenticator_controller.dart b/lib/src/controller/authenticator_controller.dart new file mode 100644 index 0000000..910f644 --- /dev/null +++ b/lib/src/controller/authenticator_controller.dart @@ -0,0 +1,24 @@ +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; + +class AuthenticatorController extends ServerController { + AuthenticatorController() : super(); + + @override + String get controllerName => "authenticator"; + + @override + String get storageName => "reboot_authenticator"; + + @override + String get defaultHost => kDefaultAuthenticatorHost; + + @override + String get defaultPort => kDefaultAuthenticatorPort; + + @override + Future get isPortFree => isAuthenticatorPortFree(); + + @override + Future freePort() => freeAuthenticatorPort(); +} \ No newline at end of file diff --git a/lib/src/ui/controller/build_controller.dart b/lib/src/controller/build_controller.dart similarity index 55% rename from lib/src/ui/controller/build_controller.dart rename to lib/src/controller/build_controller.dart index 2ffa569..fd46757 100644 --- a/lib/src/ui/controller/build_controller.dart +++ b/lib/src/controller/build_controller.dart @@ -1,11 +1,11 @@ import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/fortnite_build.dart'; +import 'package:reboot_common/common.dart'; class BuildController extends GetxController { List? _builds; - Rxn selectedBuildRx; + Rxn selectedBuild; - BuildController() : selectedBuildRx = Rxn(); + BuildController() : selectedBuild = Rxn(); List? get builds => _builds; @@ -14,6 +14,11 @@ class BuildController extends GetxController { if(builds == null || builds.isEmpty){ return; } - selectedBuildRx.value = builds[0]; + selectedBuild.value = builds[0]; + } + + void reset(){ + _builds = null; + selectedBuild.value = null; } } diff --git a/lib/src/ui/controller/game_controller.dart b/lib/src/controller/game_controller.dart similarity index 69% rename from lib/src/ui/controller/game_controller.dart rename to lib/src/controller/game_controller.dart index 3bd6862..47934a0 100644 --- a/lib/src/ui/controller/game_controller.dart +++ b/lib/src/controller/game_controller.dart @@ -1,28 +1,25 @@ import 'dart:async'; import 'dart:convert'; -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/model/game_instance.dart'; +import 'package:reboot_common/common.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; - -const String kDefaultPlayerName = "Player"; - class GameController extends GetxController { late final String uuid; late final GetStorage _storage; late final TextEditingController username; late final TextEditingController password; - late final RxBool showPassword; late final TextEditingController customLaunchArgs; late final Rx> versions; late final Rxn _selectedVersion; late final RxBool started; late final RxBool autoStartGameServer; - GameInstance? instance; + late final Rxn>> servers; + late final Rxn instance; GameController() { _storage = GetStorage("reboot_game"); @@ -33,8 +30,7 @@ class GameController extends GetxController { versions = Rx(decodedVersions); versions.listen((data) => _saveVersions()); var decodedSelectedVersionName = _storage.read("version"); - var decodedSelectedVersion = decodedVersions.firstWhereOrNull( - (element) => element.name == decodedSelectedVersionName); + var decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName); uuid = _storage.read("uuid") ?? const Uuid().v4(); _storage.write("uuid", uuid); _selectedVersion = Rxn(decodedSelectedVersion); @@ -42,12 +38,35 @@ class GameController extends GetxController { username.addListener(() => _storage.write("username", username.text)); password = TextEditingController(text: _storage.read("password") ?? ""); password.addListener(() => _storage.write("password", password.text)); - showPassword = RxBool(false); - customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? "")); + customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? ""); customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text)); started = RxBool(false); autoStartGameServer = RxBool(_storage.read("auto_game_server") ?? true); autoStartGameServer.listen((value) => _storage.write("auto_game_server", value)); + var supabase = Supabase.instance.client; + servers = Rxn(); + supabase.from('hosts') + .stream(primaryKey: ['id']) + .map((event) => event.where((element) => element["ip"] != null).toSet()) + .listen((event) { + if(servers.value == null) { + servers.value = event; + }else { + servers.value?.addAll(event); + } + }); + var serializedInstance = _storage.read("instance"); + instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null); + instance.listen((value) => _storage.write("instance", jsonEncode(value?.toJson()))); + } + + void reset() { + username.text = kDefaultPlayerName; + password.text = ""; + customLaunchArgs.text = ""; + versions.value = []; + autoStartGameServer.value = true; + instance.value = null; } FortniteVersion? getVersionByName(String name) { @@ -94,4 +113,13 @@ class GameController extends GetxController { void updateVersion(FortniteVersion version, Function(FortniteVersion) function) { versions.update((val) => function(version)); } + + Map? findServerById(String uuid) { + try { + print(uuid); + return servers.value?.firstWhere((element) => element["id"] == uuid); + } on StateError catch(_) { + return null; + } + } } diff --git a/lib/src/controller/hosting_controller.dart b/lib/src/controller/hosting_controller.dart new file mode 100644 index 0000000..536c9c4 --- /dev/null +++ b/lib/src/controller/hosting_controller.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:reboot_common/common.dart'; + +const String kDefaultServerName = "Reboot Game Server"; +const String kDefaultDescription = "Just another server"; + +class HostingController extends GetxController { + late final GetStorage _storage; + late final TextEditingController name; + late final TextEditingController description; + late final TextEditingController password; + late final RxBool showPassword; + late final RxBool discoverable; + late final RxBool started; + late final Rxn instance; + + HostingController() { + _storage = GetStorage("reboot_hosting"); + name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName); + name.addListener(() => _storage.write("name", name.text)); + description = TextEditingController(text: _storage.read("description") ?? kDefaultDescription); + description.addListener(() => _storage.write("description", description.text)); + password = TextEditingController(text: _storage.read("password") ?? ""); + password.addListener(() => _storage.write("password", password.text)); + discoverable = RxBool(_storage.read("discoverable") ?? true); + discoverable.listen((value) => _storage.write("discoverable", value)); + started = RxBool(false); + showPassword = RxBool(false); + var serializedInstance = _storage.read("instance"); + instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null); + instance.listen((value) => _storage.write("instance", jsonEncode(value?.toJson()))); + } + + void reset() { + name.text = kDefaultServerName; + description.text = kDefaultDescription; + showPassword.value = false; + discoverable.value = false; + started.value = false; + instance.value = null; + } +} diff --git a/lib/src/controller/matchmaker_controller.dart b/lib/src/controller/matchmaker_controller.dart new file mode 100644 index 0000000..c15ec56 --- /dev/null +++ b/lib/src/controller/matchmaker_controller.dart @@ -0,0 +1,30 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; + +class MatchmakerController extends ServerController { + late final TextEditingController gameServerAddress; + + MatchmakerController() : super() { + gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? kDefaultMatchmakerHost); + gameServerAddress.addListener(() => storage.write("game_server_address", gameServerAddress.text)); + } + + @override + String get controllerName => "matchmaker"; + + @override + String get storageName => "reboot_matchmaker"; + + @override + String get defaultHost => kDefaultMatchmakerHost; + + @override + String get defaultPort => kDefaultMatchmakerPort; + + @override + Future get isPortFree => isMatchmakerPortFree(); + + @override + Future freePort() => freeMatchmakerPort(); +} \ No newline at end of file diff --git a/lib/src/controller/server_controller.dart b/lib/src/controller/server_controller.dart new file mode 100644 index 0000000..2a359dd --- /dev/null +++ b/lib/src/controller/server_controller.dart @@ -0,0 +1,189 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:reboot_common/common.dart'; +import 'package:sync/semaphore.dart'; + +abstract class ServerController extends GetxController { + late final GetStorage storage; + late final TextEditingController host; + late final TextEditingController port; + late final Rx type; + late final Semaphore semaphore; + late RxBool started; + late RxBool detached; + Process? embeddedServer; + HttpServer? localServer; + HttpServer? remoteServer; + + ServerController() { + storage = GetStorage(storageName); + started = RxBool(false); + type = Rx(ServerType.values.elementAt(storage.read("type") ?? 0)); + type.listen((value) { + host.text = _readHost(); + port.text = _readPort(); + storage.write("type", value.index); + if (!started.value) { + return; + } + + stop(); + }); + host = TextEditingController(text: _readHost()); + host.addListener(() => + storage.write("${type.value.name}_host", host.text)); + port = TextEditingController(text: _readPort()); + port.addListener(() => + storage.write("${type.value.name}_port", port.text)); + detached = RxBool(storage.read("detached") ?? false); + detached.listen((value) => storage.write("detached", value)); + semaphore = Semaphore(); + } + + String get controllerName; + + String get storageName; + + String get defaultHost; + + String get defaultPort; + + Future get isPortFree; + + Future get isPortTaken async => !(await isPortFree); + + Future freePort(); + + void reset() async { + type.value = ServerType.values.elementAt(0); + for (var type in ServerType.values) { + storage.write("${type.name}_host", null); + storage.write("${type.name}_port", null); + } + + host.text = type.value != ServerType.remote ? defaultHost : ""; + port.text = defaultPort; + detached.value = false; + } + + String _readHost() { + String? value = storage.read("${type.value.name}_host"); + return value != null && value.isNotEmpty ? value + : type.value != ServerType.remote ? defaultHost : ""; + } + + String _readPort() => + storage.read("${type.value.name}_port") ?? defaultPort; + + Stream start() async* { + try { + var host = this.host.text.trim(); + if (host.isEmpty) { + yield ServerResult(ServerResultType.missingHostError); + return; + } + + var port = this.port.text.trim(); + if (port.isEmpty) { + yield ServerResult(ServerResultType.missingPortError); + return; + } + + var portNumber = int.tryParse(port); + if (portNumber == null) { + yield ServerResult(ServerResultType.illegalPortError); + return; + } + + if (type() != ServerType.local && await isPortTaken) { + yield ServerResult(ServerResultType.freeingPort); + var result = await freePort(); + yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); + if(!result) { + return; + } + } + switch(type()){ + case ServerType.embedded: + embeddedServer = await startEmbeddedAuthenticator(detached()); + break; + case ServerType.remote: + yield ServerResult(ServerResultType.pingingRemote); + var uriResult = await ping(host, port); + if(uriResult == null) { + yield ServerResult(ServerResultType.pingError); + return; + } + + remoteServer = await startRemoteAuthenticatorProxy(uriResult); + break; + case ServerType.local: + if(port != defaultPort) { + localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$defaultPort")); + } + + break; + } + + yield ServerResult(ServerResultType.pingingLocal); + var uriResult = await pingSelf(defaultPort); + if(uriResult == null) { + yield ServerResult(ServerResultType.pingError); + return; + } + + yield ServerResult(ServerResultType.startSuccess); + started.value = true; + }catch(error, stackTrace) { + yield ServerResult( + ServerResultType.startError, + error: error, + stackTrace: stackTrace + ); + } + } + + Future stop() async { + started.value = false; + try{ + switch(type()){ + case ServerType.embedded: + freePort(); + break; + case ServerType.remote: + await remoteServer?.close(force: true); + remoteServer = null; + break; + case ServerType.local: + await localServer?.close(force: true); + localServer = null; + break; + } + return true; + }catch(_){ + started.value = true; + return false; + } + } + + Stream restart() async* { + await resetWinNat(); + if(started()) { + await stop(); + } + + yield* start(); + } + + Stream toggle() async* { + if(started()) { + await stop(); + }else { + yield* start(); + } + } +} \ No newline at end of file diff --git a/lib/src/ui/controller/settings_controller.dart b/lib/src/controller/settings_controller.dart similarity index 60% rename from lib/src/ui/controller/settings_controller.dart rename to lib/src/controller/settings_controller.dart index e10af0d..3df868e 100644 --- a/lib/src/ui/controller/settings_controller.dart +++ b/lib/src/controller/settings_controller.dart @@ -1,28 +1,19 @@ -import 'dart:ui'; - -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/main.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/server.dart'; - -import 'package:reboot_launcher/src/util/reboot.dart'; +import 'package:reboot_common/common.dart'; +import 'package:window_manager/window_manager.dart'; class SettingsController extends GetxController { static const String _kDefaultIp = "127.0.0.1"; - static const bool _kDefaultAutoUpdate = true; late final GetStorage _storage; late final String originalDll; - late final TextEditingController updateUrl; late final TextEditingController rebootDll; late final TextEditingController consoleDll; late final TextEditingController authDll; - late final TextEditingController matchmakingIp; - late final RxBool autoUpdate; late final RxBool firstRun; - late final RxInt index; late double width; late double height; late double? offsetX; @@ -31,27 +22,16 @@ class SettingsController extends GetxController { SettingsController() { _storage = GetStorage("reboot_settings"); - updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl); - updateUrl.addListener(() => _storage.write("update_url", updateUrl.text)); rebootDll = _createController("reboot", "reboot.dll"); consoleDll = _createController("console", "console.dll"); authDll = _createController("cobalt", "cobalt.dll"); - matchmakingIp = TextEditingController(text: _storage.read("ip") ?? _kDefaultIp); - matchmakingIp.addListener(() async { - var text = matchmakingIp.text; - _storage.write("ip", text); - writeMatchmakingIp(text); - }); width = _storage.read("width") ?? kDefaultWindowWidth; height = _storage.read("height") ?? kDefaultWindowHeight; offsetX = _storage.read("offset_x"); offsetY = _storage.read("offset_y"); - autoUpdate = RxBool(_storage.read("auto_update") ?? _kDefaultAutoUpdate); - autoUpdate.listen((value) => _storage.write("auto_update", value)); scrollingDistance = 0.0; firstRun = RxBool(_storage.read("first_run") ?? true); firstRun.listen((value) => _storage.write("first_run", value)); - index = RxInt(firstRun() ? 3 : 0); } TextEditingController _createController(String key, String name) { @@ -60,9 +40,10 @@ class SettingsController extends GetxController { return controller; } - void saveWindowSize() { - _storage.write("width", window.physicalSize.width); - _storage.write("height", window.physicalSize.height); + void saveWindowSize() async { + var size = await windowManager.getSize(); + _storage.write("width", size.width); + _storage.write("height", size.height); } void saveWindowOffset(Offset position) { @@ -71,13 +52,11 @@ class SettingsController extends GetxController { } void reset(){ - updateUrl.text = rebootDownloadUrl; rebootDll.text = _controllerDefaultPath("reboot.dll"); consoleDll.text = _controllerDefaultPath("console.dll"); authDll.text = _controllerDefaultPath("cobalt.dll"); - matchmakingIp.text = _kDefaultIp; + firstRun.value = true; writeMatchmakingIp(_kDefaultIp); - autoUpdate.value = _kDefaultAutoUpdate; } String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name"; diff --git a/lib/src/controller/update_controller.dart b/lib/src/controller/update_controller.dart new file mode 100644 index 0000000..0cf9cfe --- /dev/null +++ b/lib/src/controller/update_controller.dart @@ -0,0 +1,47 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:reboot_common/common.dart'; + +class UpdateController { + late final GetStorage _storage; + late final RxnInt timestamp; + late final Rx status; + late final Rx timer; + late final TextEditingController url; + + UpdateController() { + _storage = GetStorage("reboot_update"); + timestamp = RxnInt(_storage.read("ts")); + timestamp.listen((value) => _storage.write("ts", value)); + var timerIndex = _storage.read("timer"); + timer = Rx(timerIndex == null ? UpdateTimer.never : UpdateTimer.values.elementAt(timerIndex)); + timer.listen((value) => _storage.write("timer", value.index)); + url = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl); + url.addListener(() => _storage.write("update_url", url.text)); + status = Rx(UpdateStatus.waiting); + } + + Future update() async { + if(timer.value == UpdateTimer.never) { + status.value = UpdateStatus.success; + return; + } + + try { + timestamp.value = await downloadRebootDll(url.text, timestamp.value); + status.value = UpdateStatus.success; + }catch(_) { + status.value = UpdateStatus.error; + rethrow; + } + } + + void reset() { + timestamp.value = null; + timer.value = UpdateTimer.never; + url.text = rebootDownloadUrl; + status.value = UpdateStatus.waiting; + update(); + } +} \ No newline at end of file diff --git a/lib/src/ui/dialog/dialog.dart b/lib/src/dialog/dialog.dart similarity index 90% rename from lib/src/ui/dialog/dialog.dart rename to lib/src/dialog/dialog.dart index 8fcf82d..ed91fce 100644 --- a/lib/src/ui/dialog/dialog.dart +++ b/lib/src/dialog/dialog.dart @@ -1,9 +1,17 @@ import 'package:clipboard/clipboard.dart'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog; +import 'package:reboot_launcher/src/dialog/message.dart'; +import 'package:reboot_launcher/src/page/home_page.dart'; import 'dialog_button.dart'; +Future showDialog({required WidgetBuilder builder}) => fluent.showDialog( + context: pageKey.currentContext!, + useRootNavigator: false, + builder: builder +); + abstract class AbstractDialog extends StatelessWidget { const AbstractDialog({Key? key}) : super(key: key); @@ -19,19 +27,13 @@ class GenericDialog extends AbstractDialog { const GenericDialog({super.key, required this.header, required this.buttons, this.padding}); @override - Widget build(BuildContext context) { - return Stack( - children: [ - ContentDialog( - style: ContentDialogThemeData( - padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) - ), - content: header, - actions: buttons - ), - ], - ); - } + Widget build(BuildContext context) => ContentDialog( + style: ContentDialogThemeData( + padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) + ), + content: header, + actions: buttons + ); } class FormDialog extends AbstractDialog { diff --git a/lib/src/ui/dialog/dialog_button.dart b/lib/src/dialog/dialog_button.dart similarity index 58% rename from lib/src/ui/dialog/dialog_button.dart rename to lib/src/dialog/dialog_button.dart index 8e9eda1..1cab3df 100644 --- a/lib/src/ui/dialog/dialog_button.dart +++ b/lib/src/dialog/dialog_button.dart @@ -1,4 +1,4 @@ -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; class DialogButton extends StatefulWidget { final String? text; @@ -22,39 +22,30 @@ class DialogButton extends StatefulWidget { class _DialogButtonState extends State { @override - Widget build(BuildContext context) { - return widget.type == ButtonType.only ? _createOnlyButton() : _createButton(); - } + Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button; - SizedBox _createOnlyButton() { - return SizedBox( - width: double.infinity, - child: _createButton() - ); - } + SizedBox get _onlyButton => SizedBox( + width: double.infinity, + child: _button + ); - Widget _createButton() { - return widget.type == ButtonType.primary ? _createPrimaryActionButton() - : _createSecondaryActionButton(); - } + Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton; - Widget _createPrimaryActionButton() { - return FilledButton( + Widget get _primaryButton { + return Button( onPressed: widget.onTap!, child: Text(widget.text!), ); } - Widget _createSecondaryActionButton() { + Widget get _secondaryButton { return Button( onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, child: Text(widget.text ?? "Close"), ); } - void _onDefaultSecondaryActionTap() { - Navigator.of(context).pop(null); - } + void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null); } enum ButtonType { diff --git a/lib/src/dialog/message.dart b/lib/src/dialog/message.dart new file mode 100644 index 0000000..217a54a --- /dev/null +++ b/lib/src/dialog/message.dart @@ -0,0 +1,38 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; + +import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:sync/semaphore.dart'; + +Semaphore _semaphore = Semaphore(); +OverlayEntry? _lastOverlay; + +void showMessage(String text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration}) { + try { + _semaphore.acquire(); + if(_lastOverlay?.mounted == true) { + _lastOverlay?.remove(); + } + var pageIndexValue = pageIndex.value; + _lastOverlay = showSnackbar( + pageKey.currentContext!, + InfoBar( + title: Text(text), + isLong: true, + isIconVisible: true, + content: SizedBox( + width: double.infinity, + child: loading ? const ProgressBar() : const SizedBox() + ), + severity: severity + ), + margin: EdgeInsets.only( + left: 330.0, + right: 16.0, + bottom: pageIndexValue == 0 || pageIndexValue == 1 || pageIndexValue == 3 || pageIndexValue == 4 ? 72 : 16 + ), + duration: duration + ); + }finally { + _semaphore.release(); + } +} \ No newline at end of file diff --git a/lib/src/util/error.dart b/lib/src/interactive/error.dart similarity index 64% rename from lib/src/util/error.dart rename to lib/src/interactive/error.dart index e146f77..c5083db 100644 --- a/lib/src/util/error.dart +++ b/lib/src/interactive/error.dart @@ -1,7 +1,7 @@ -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; -import 'package:reboot_launcher/main.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog.dart'; +import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; String? lastError; @@ -11,7 +11,7 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) { return; } - if(appKey.currentContext == null || appKey.currentState?.mounted == false){ + if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){ return; } @@ -20,13 +20,12 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) { } lastError = exception.toString(); - var route = ModalRoute.of(appKey.currentContext!); + var route = ModalRoute.of(pageKey.currentContext!); if(route != null && !route.isCurrent){ - Navigator.of(appKey.currentContext!).pop(false); + Navigator.of(pageKey.currentContext!).pop(false); } WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog( - context: appKey.currentContext!, builder: (context) => ErrorDialog( exception: exception, diff --git a/lib/src/ui/dialog/game_dialogs.dart b/lib/src/interactive/game.dart similarity index 80% rename from lib/src/ui/dialog/game_dialogs.dart rename to lib/src/interactive/game.dart index 0953595..7faa061 100644 --- a/lib/src/ui/dialog/game_dialogs.dart +++ b/lib/src/interactive/game.dart @@ -1,21 +1,18 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; +import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/main.dart'; -import 'dialog.dart'; +import '../dialog/dialog.dart'; const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. " "If you are unsure which version works best, use build 7.40. " "If you are a passionate programmer you can add support by opening a PR on Github. "; -const String _corruptedBuildError = "An unknown error happened while launching Fortnite. " - "Some critical could be missing in your installation. " +const String _corruptedBuildError = "An unknown occurred while launching Fortnite. " + "Some critical files could be missing in your installation. " "Download the build again from the launcher, not locally, or from a different source. " "Alternatively, something could have gone wrong in the launcher. "; Future showBrokenError() async { showDialog( - context: appKey.currentContext!, builder: (context) => const InfoDialog( text: "The backend server is not working correctly" ) @@ -24,7 +21,6 @@ Future showBrokenError() async { Future showMissingDllError(String name) async { showDialog( - context: appKey.currentContext!, builder: (context) => InfoDialog( text: "$name dll is not a valid dll, fix it in the settings tab" ) @@ -33,7 +29,6 @@ Future showMissingDllError(String name) async { Future showTokenErrorFixable() async { showDialog( - context: appKey.currentContext!, builder: (context) => const InfoDialog( text: "A token error occurred. " "The backend server has been automatically restarted to fix the issue. " @@ -44,7 +39,6 @@ Future showTokenErrorFixable() async { Future showTokenErrorCouldNotFix() async { showDialog( - context: appKey.currentContext!, builder: (context) => const InfoDialog( text: "A token error occurred. " "The game couldn't be recovered, open an issue on Discord." @@ -54,7 +48,6 @@ Future showTokenErrorCouldNotFix() async { Future showTokenErrorUnfixable() async { showDialog( - context: appKey.currentContext!, builder: (context) => const InfoDialog( text: "A token error occurred. " "This issue cannot be resolved automatically as the server isn't embedded." @@ -67,7 +60,6 @@ Future showTokenErrorUnfixable() async { Future showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async { if(error == null) { showDialog( - context: appKey.currentContext!, builder: (context) => InfoDialog( text: server ? _unsupportedServerError : _corruptedBuildError ) @@ -76,7 +68,6 @@ Future showCorruptedBuildError(bool server, [Object? error, StackTrace? st } showDialog( - context: appKey.currentContext!, builder: (context) => ErrorDialog( exception: error, stackTrace: stackTrace, @@ -87,7 +78,6 @@ Future showCorruptedBuildError(bool server, [Object? error, StackTrace? st Future showMissingBuildError(FortniteVersion version) async { showDialog( - context: appKey.currentContext!, builder: (context) => InfoDialog( text: "${version.location.path} no longer contains a Fortnite executable. " "This probably means that you deleted it or move it somewhere else." diff --git a/lib/src/interactive/profile.dart b/lib/src/interactive/profile.dart new file mode 100644 index 0000000..ce901f1 --- /dev/null +++ b/lib/src/interactive/profile.dart @@ -0,0 +1,83 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:flutter/material.dart' show Icons; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; + +final GameController _gameController = Get.find(); + +Future showProfileForm(BuildContext context) async{ + var showPassword = RxBool(false); + var oldUsername = _gameController.username.text; + var showPasswordTrailing = RxBool(oldUsername.isNotEmpty); + var oldPassword = _gameController.password.text; + var result = await showDialog( + builder: (context) => Obx(() => FormDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InfoLabel( + label: "Username/Email", + child: TextFormBox( + placeholder: "Type your username or email", + controller: _gameController.username, + autovalidateMode: AutovalidateMode.always, + enableSuggestions: true, + autofocus: true, + autocorrect: false, + ) + ), + const SizedBox(height: 16.0), + InfoLabel( + label: "Password", + child: TextFormBox( + placeholder: "Type your password, if you have one", + controller: _gameController.password, + autovalidateMode: AutovalidateMode.always, + obscureText: !showPassword.value, + enableSuggestions: false, + autocorrect: false, + onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty, + suffix: Button( + onPressed: () => showPassword.value = !showPassword.value, + style: ButtonStyle( + shape: ButtonState.all(const CircleBorder()), + backgroundColor: ButtonState.all(Colors.transparent) + ), + child: Icon( + showPassword.value ? Icons.visibility_off : Icons.visibility, + color: showPasswordTrailing.value ? null : Colors.transparent + ), + ) + ) + ), + const SizedBox(height: 8.0) + ], + ), + buttons: [ + DialogButton( + text: "Cancel", + type: ButtonType.secondary + ), + + DialogButton( + text: "Save", + type: ButtonType.primary, + onTap: () { + Navigator.of(context).pop(true); + } + ) + ] + )) + ) ?? false; + if(result) { + return true; + } + + _gameController.username.text = oldUsername; + _gameController.password.text = oldPassword; + return false; +} diff --git a/lib/src/interactive/server.dart b/lib/src/interactive/server.dart new file mode 100644 index 0000000..a0cabc8 --- /dev/null +++ b/lib/src/interactive/server.dart @@ -0,0 +1,222 @@ +import 'dart:async'; + +import 'package:clipboard/clipboard.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:flutter/material.dart' show Icons; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/dialog/message.dart'; + +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/util/cryptography.dart'; + +extension ServerControllerDialog on ServerController { + Future restartInteractive() async { + var stream = restart(); + return await _handleStream(stream, false); + } + + Future toggleInteractive([bool showSuccessMessage = true]) async { + var stream = toggle(); + return await _handleStream(stream, showSuccessMessage); + } + + + Future _handleStream(Stream stream, bool showSuccessMessage) async { + var completer = Completer(); + stream.listen((event) { + switch (event.type) { + case ServerResultType.missingHostError: + showMessage( + "Cannot launch game: missing hostname in $controllerName configuration", + severity: InfoBarSeverity.error + ); + break; + case ServerResultType.missingPortError: + showMessage( + "Cannot launch game: missing port in $controllerName configuration", + severity: InfoBarSeverity.error + ); + break; + case ServerResultType.illegalPortError: + showMessage( + "Cannot launch game: invalid port in $controllerName configuration", + severity: InfoBarSeverity.error + ); + break; + case ServerResultType.freeingPort: + case ServerResultType.freePortSuccess: + case ServerResultType.freePortError: + showMessage( + "Message", + loading: event.type == ServerResultType.freeingPort, + severity: event.type == ServerResultType.freeingPort ? InfoBarSeverity.info : event.type == ServerResultType.freePortSuccess ? InfoBarSeverity.success : InfoBarSeverity.error + ); + break; + case ServerResultType.pingingRemote: + showMessage( + "Pinging remote server...", + severity: InfoBarSeverity.info, + loading: true, + duration: const Duration(seconds: 10) + ); + break; + case ServerResultType.pingingLocal: + showMessage( + "Pinging ${type().name} server...", + severity: InfoBarSeverity.info, + loading: true, + duration: const Duration(seconds: 10) + ); + break; + case ServerResultType.pingError: + showMessage( + "Cannot ping ${type().name} server", + severity: InfoBarSeverity.error + ); + break; + case ServerResultType.startSuccess: + if(showSuccessMessage) { + showMessage( + "The $controllerName was started successfully", + severity: InfoBarSeverity.success + ); + } + completer.complete(true); + break; + case ServerResultType.startError: + showMessage( + "An error occurred while starting the $controllerName: ${event.error ?? "unknown error"}", + severity: InfoBarSeverity.error + ); + break; + } + + if(event.type.isError) { + completer.complete(false); + } + }); + + var result = await completer.future; + if(result && type() == ServerType.embedded) { + watchProcess(embeddedServer!.pid).then((value) { + if(started()) { + pageIndex.value = 3; + started.value = false; + WidgetsBinding.instance.addPostFrameCallback((_) => showMessage( + "The $controllerName was terminated unexpectedly: if this wasn't intentional, file a bug report", + severity: InfoBarSeverity.warning, + duration: snackbarLongDuration + )); + } + }); + } + + return result; + } +} + +extension MatchmakerControllerExtension on MatchmakerController { + Future joinServer(Map entry) async { + var hashedPassword = entry["password"]; + var hasPassword = hashedPassword != null; + var embedded = type.value == ServerType.embedded; + var author = entry["author"]; + var encryptedIp = entry["ip"]; + if(!hasPassword) { + _onSuccess(embedded, encryptedIp, author); + return; + } + + var confirmPassword = await _askForPassword(); + if(confirmPassword == null) { + return; + } + + if(!checkPassword(confirmPassword, hashedPassword)) { + showMessage( + "Wrong password: please try again", + duration: snackbarLongDuration, + severity: InfoBarSeverity.error + ); + return; + } + + var decryptedIp = aes256Decrypt(encryptedIp, confirmPassword); + _onSuccess(embedded, decryptedIp, author); + } + + + Future _askForPassword() async { + var confirmPasswordController = TextEditingController(); + var showPassword = RxBool(false); + var showPasswordTrailing = RxBool(false); + return await showDialog( + builder: (context) => FormDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InfoLabel( + label: "Password", + child: Obx(() => TextFormBox( + placeholder: "Type the server's password", + controller: confirmPasswordController, + autovalidateMode: AutovalidateMode.always, + obscureText: !showPassword.value, + enableSuggestions: false, + autofocus: true, + autocorrect: false, + onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty, + suffix: Button( + onPressed: () => showPasswordTrailing.value = !showPasswordTrailing.value, + style: ButtonStyle( + shape: ButtonState.all(const CircleBorder()), + backgroundColor: ButtonState.all(Colors.transparent) + ), + child: Icon( + showPassword.value ? Icons.visibility_off : Icons.visibility, + color: showPassword.value ? null : Colors.transparent + ), + ) + )) + ), + const SizedBox(height: 8.0) + ], + ), + buttons: [ + DialogButton( + text: "Cancel", + type: ButtonType.secondary + ), + + DialogButton( + text: "Confirm", + type: ButtonType.primary, + onTap: () => Navigator.of(context).pop(confirmPasswordController.text) + ) + ] + ) + ); + } + + void _onSuccess(bool embedded, String decryptedIp, String author) { + if(embedded) { + gameServerAddress.text = decryptedIp; + pageIndex.value = 0; + }else { + FlutterClipboard.controlC(decryptedIp); + } + WidgetsBinding.instance.addPostFrameCallback((_) => showMessage( + embedded ? "You joined $author's server successfully!" : "Copied IP to the clipboard", + duration: snackbarLongDuration, + severity: InfoBarSeverity.success + )); + } +} \ No newline at end of file diff --git a/lib/src/model/fortnite_build.dart b/lib/src/model/fortnite_build.dart deleted file mode 100644 index 7956f9a..0000000 --- a/lib/src/model/fortnite_build.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:version/version.dart'; - -class FortniteBuild { - final Version version; - final String link; - - FortniteBuild({required this.version, required this.link}); -} diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart deleted file mode 100644 index 61ebfde..0000000 --- a/lib/src/model/fortnite_version.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as path; -import 'package:reboot_launcher/src/util/patcher.dart'; - -class FortniteVersion { - String name; - Directory location; - - FortniteVersion.fromJson(json) - : name = json["name"], - location = Directory(json["location"]); - - FortniteVersion({required this.name, required this.location}); - - static File? findExecutable(Directory directory, String name) { - try{ - var result = directory.listSync(recursive: true) - .firstWhere((element) => path.basename(element.path) == name); - return File(result.path); - }catch(_){ - return null; - } - } - - Future get executable async { - var result = findExecutable(location, "FortniteClient-Win64-Shipping-Reboot.exe"); - if(result != null) { - return result; - } - - var original = findExecutable(location, "FortniteClient-Win64-Shipping.exe"); - if(original == null) { - return null; - } - - await Future.wait([ - compute(patchMatchmaking, original), - compute(patchHeadless, original) - ]); - return original; - } - - File? get launcher { - return findExecutable(location, "FortniteLauncher.exe"); - } - - File? get eacExecutable { - return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe"); - } - - Map toJson() => { - 'name': name, - 'location': location.path - }; - - @override - String toString() { - return 'FortniteVersion{name: $name, location: $location'; - } -} diff --git a/lib/src/model/game_instance.dart b/lib/src/model/game_instance.dart deleted file mode 100644 index 16ec5a1..0000000 --- a/lib/src/model/game_instance.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:io'; - -class GameInstance { - final Process gameProcess; - final Process? launcherProcess; - final Process? eacProcess; - final int? watchDogProcessPid; - bool tokenError; - bool hasChildServer; - - GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.watchDogProcessPid, this.hasChildServer) - : tokenError = false; - - void kill() { - gameProcess.kill(ProcessSignal.sigabrt); - launcherProcess?.kill(ProcessSignal.sigabrt); - eacProcess?.kill(ProcessSignal.sigabrt); - if(watchDogProcessPid != null){ - Process.killPid(watchDogProcessPid!, ProcessSignal.sigabrt); - } - } -} diff --git a/lib/src/model/server_type.dart b/lib/src/model/server_type.dart deleted file mode 100644 index dde963d..0000000 --- a/lib/src/model/server_type.dart +++ /dev/null @@ -1,32 +0,0 @@ -enum ServerType { - embedded, - remote, - local; - - static ServerType? of(String id){ - try { - return ServerType.values - .firstWhere((element) => element.id == id); - }catch(_){ - return null; - } - } - - String get id { - return this == ServerType.embedded ? "embedded" - : this == ServerType.remote ? "remote" - : "local"; - } - - String get name { - return this == ServerType.embedded ? "Embedded (Lawin)" - : this == ServerType.remote ? "Remote" - : "Local"; - } - - String get message { - return this == ServerType.embedded ? "A server will be automatically started in the background" - : this == ServerType.remote ? "A reverse proxy to the remote server will be created" - : "Assumes that you are running yourself the server locally"; - } -} \ No newline at end of file diff --git a/lib/src/model/update_status.dart b/lib/src/model/update_status.dart deleted file mode 100644 index 7df08bf..0000000 --- a/lib/src/model/update_status.dart +++ /dev/null @@ -1,8 +0,0 @@ -enum UpdateStatus { - waiting, - started, - success, - error; - - bool isDone() => this == UpdateStatus.success || this == UpdateStatus.error; -} \ No newline at end of file diff --git a/lib/src/page/authenticator_page.dart b/lib/src/page/authenticator_page.dart new file mode 100644 index 0000000..c915903 --- /dev/null +++ b/lib/src/page/authenticator_page.dart @@ -0,0 +1,131 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/widget/server/start_button.dart'; +import 'package:reboot_launcher/src/widget/server/type_selector.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; + +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; + +class AuthenticatorPage extends StatefulWidget { + const AuthenticatorPage({Key? key}) : super(key: key); + + @override + State createState() => _AuthenticatorPageState(); +} + +class _AuthenticatorPageState extends State with AutomaticKeepAliveClientMixin { + final AuthenticatorController _authenticatorController = Get.find(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Obx(() => Column( + children: [ + Expanded( + child: ListView( + children: [ + SettingTile( + title: "Authenticator configuration", + subtitle: "This section contains the authenticator's configuration", + content: const ServerTypeSelector( + authenticator: true + ), + expandedContent: [ + if(_authenticatorController.type.value == ServerType.remote) + SettingTile( + title: "Host", + subtitle: "The hostname of the authenticator", + isChild: true, + content: TextFormBox( + placeholder: "Host", + controller: _authenticatorController.host, + readOnly: !_isRemote + ) + ), + if(_authenticatorController.type.value != ServerType.embedded) + SettingTile( + title: "Port", + subtitle: "The port of the authenticator", + isChild: true, + content: TextFormBox( + placeholder: "Port", + controller: _authenticatorController.port, + readOnly: !_isRemote + ) + ), + if(_authenticatorController.type.value == ServerType.embedded) + SettingTile( + title: "Detached", + subtitle: "Whether the embedded authenticator should be started as a separate process, useful for debugging", + contentWidth: null, + isChild: true, + content: Obx(() => ToggleSwitch( + checked: _authenticatorController.detached(), + onChanged: (value) => _authenticatorController.detached.value = value + )) + ), + ], + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Installation directory", + subtitle: "Opens the folder where the embedded authenticator is located", + content: Button( + onPressed: () => launchUrl(authenticatorDirectory.uri), + child: const Text("Show Files") + ) + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Reset authenticator", + subtitle: "Resets the authenticator's settings to their default values", + content: Button( + onPressed: () => showDialog( + builder: (context) => InfoDialog( + text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", + buttons: [ + DialogButton( + type: ButtonType.secondary, + text: "Close", + ), + DialogButton( + type: ButtonType.primary, + text: "Reset", + onTap: () { + _authenticatorController.reset(); + Navigator.of(context).pop(); + }, + ) + ], + ) + ), + child: const Text("Reset"), + ) + ) + ] + ), + ), + const SizedBox( + height: 8.0, + ), + const ServerButton( + authenticator: true + ) + ], + )); + } + + bool get _isRemote => _authenticatorController.type.value == ServerType.remote; +} diff --git a/lib/src/page/browse_page.dart b/lib/src/page/browse_page.dart new file mode 100644 index 0000000..081213e --- /dev/null +++ b/lib/src/page/browse_page.dart @@ -0,0 +1,265 @@ + +import 'dart:async'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; + +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/interactive/server.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:skeletons/skeletons.dart'; + +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; + +class BrowsePage extends StatefulWidget { + const BrowsePage({Key? key}) : super(key: key); + + @override + State createState() => _BrowsePageState(); +} + +class _BrowsePageState extends State with AutomaticKeepAliveClientMixin { + final GameController _gameController = Get.find(); + final MatchmakerController _matchmakerController = Get.find(); + final TextEditingController _filterController = TextEditingController(); + final StreamController _filterControllerStream = StreamController(); + + @override + Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( + future: Future.delayed(const Duration(seconds: 1)), // Fake delay to show loading + builder: (context, futureSnapshot) => Obx(() { + var ready = futureSnapshot.connectionState == ConnectionState.done; + var data = _gameController.servers.value; + if(ready && data?.isEmpty == true) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "No servers are available right now", + style: FluentTheme.of(context).typography.titleLarge, + ), + Text( + "Host a server yourself or come back later", + style: FluentTheme.of(context).typography.body + ), + ], + ); + } + + return Column( + children: [ + _buildSearchBar(ready), + + const SizedBox( + height: 16, + ), + + Expanded( + child: StreamBuilder( + stream: _filterControllerStream.stream, + builder: (context, filterSnapshot) { + var items = _getItems(data, filterSnapshot.data, ready); + var itemsCount = items != null ? items.length * 2 : null; + if(itemsCount == 0) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "No results found", + style: FluentTheme.of(context).typography.titleLarge, + ), + Text( + "No server matches your query", + style: FluentTheme.of(context).typography.body + ), + ], + ); + } + + return ListView.builder( + itemCount: itemsCount, + itemBuilder: (context, index) { + if(index % 2 != 0) { + return const SizedBox( + height: 8.0 + ); + } + + var entry = _getItem(index ~/ 2, items); + if(!ready || entry == null) { + return const SettingTile( + content: SkeletonAvatar( + style: SkeletonAvatarStyle( + height: 32, + width: 64 + ), + ) + ); + } + + var hasPassword = entry["password"] != null; + return SettingTile( + title: "${_formatName(entry)} • ${entry["author"]}", + subtitle: "${_formatDescription(entry)} • ${_formatVersion(entry)}", + content: Button( + onPressed: () => _matchmakerController.joinServer(entry), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if(hasPassword) + const Icon(FluentIcons.lock), + if(hasPassword) + const SizedBox(width: 8.0), + Text(_matchmakerController.type.value == ServerType.embedded ? "Join Server" : "Copy IP"), + ], + ), + ) + ); + } + ); + } + ), + ) + ], + ); + } + ), + ); + } + + Set>? _getItems(Set>? data, String? filter, bool ready) { + if (!ready) { + return null; + } + + if (data == null) { + return null; + } + + return data.where((entry) => _isValidItem(entry, filter)).toSet(); + } + + bool _isValidItem(Map entry, String? filter) => + (entry["discoverable"] ?? false) && (filter == null || _filterServer(entry, filter)); + + bool _filterServer(Map element, String filter) { + String? id = element["id"]; + if(id?.toLowerCase().contains(filter) == true) { + return true; + } + + var uri = Uri.tryParse(filter); + if(uri != null && id?.toLowerCase().contains(uri.host.toLowerCase()) == true) { + return true; + } + + String? name = element["name"]; + if(name?.toLowerCase().contains(filter) == true) { + return true; + } + + String? author = element["author"]; + if(author?.toLowerCase().contains(filter) == true) { + return true; + } + + String? description = element["description"]; + if(description?.toLowerCase().contains(filter) == true) { + return true; + } + + return false; + } + + Widget _buildSearchBar(bool ready) { + if(ready) { + return TextBox( + placeholder: 'Find a server', + controller: _filterController, + onChanged: (value) => _filterControllerStream.add(value), + suffix: _searchBarIcon, + ); + } + + return const SkeletonLine( + style: SkeletonLineStyle( + height: 32 + ) + ); + } + + Widget get _searchBarIcon => Button( + onPressed: _filterController.text.isEmpty ? null : () { + _filterController.clear(); + _filterControllerStream.add(""); + }, + style: ButtonStyle( + backgroundColor: _filterController.text.isNotEmpty ? null : ButtonState.all(Colors.transparent), + border: _filterController.text.isNotEmpty ? null : ButtonState.all(const BorderSide(color: Colors.transparent)) + ), + child: _searchBarIconData + ); + + Widget get _searchBarIconData { + var color = FluentTheme.of(context).resources.textFillColorPrimary; + if (_filterController.text.isNotEmpty) { + return Icon( + FluentIcons.clear, + size: 8.0, + color: color + ); + } + + return Transform.flip( + flipX: true, + child: Icon( + FluentIcons.search, + size: 12.0, + color: color + ), + ); + } + + Map? _getItem(int index, Set? data) { + if(data == null) { + return null; + } + + if (index >= data.length) { + return null; + } + + return data.elementAt(index); + } + + String _formatName(Map entry) { + String result = entry['name']; + return result.isEmpty ? kDefaultServerName : result; + } + + String _formatDescription(Map entry) { + String result = entry['description']; + return result.isEmpty ? kDefaultDescription : result; + } + + String _formatVersion(Map entry) { + var version = entry['version']; + var versionSplit = version.indexOf("-"); + var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version; + String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion; + if(result.toLowerCase().startsWith("fortnite ")) { + result = result.substring(0, 10); + } + + return "Fortnite $result"; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart new file mode 100644 index 0000000..7a52cf7 --- /dev/null +++ b/lib/src/page/home_page.dart @@ -0,0 +1,253 @@ +import 'dart:collection'; + +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/page/browse_page.dart'; +import 'package:reboot_launcher/src/page/authenticator_page.dart'; +import 'package:reboot_launcher/src/page/matchmaker_page.dart'; +import 'package:reboot_launcher/src/page/play_page.dart'; +import 'package:reboot_launcher/src/page/settings_page.dart'; +import 'package:reboot_launcher/src/util/os.dart'; +import 'package:reboot_launcher/src/widget/home/pane.dart'; +import 'package:reboot_launcher/src/widget/home/profile.dart'; + +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/widget/os/border.dart'; +import 'package:reboot_launcher/src/widget/os/title_bar.dart'; +import 'package:window_manager/window_manager.dart'; +import 'hosting_page.dart'; +import 'info_page.dart'; + +const int pagesLength = 7; +final RxInt pageIndex = RxInt(0); +final Queue _pagesStack = Queue(); +final List _pageKeys = List.generate(pagesLength, (index) => GlobalKey()); +GlobalKey get pageKey => _pageKeys[pageIndex.value]; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { + static const double _kDefaultPadding = 12.0; + + final SettingsController _settingsController = Get.find(); + final GlobalKey _searchKey = GlobalKey(); + final FocusNode _searchFocusNode = FocusNode(); + final TextEditingController _searchController = TextEditingController(); + final RxBool _focused = RxBool(true); + final RxBool _fullScreen = RxBool(false); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + windowManager.show(); + windowManager.addListener(this); + _searchController.addListener(_onSearch); + super.initState(); + } + + void _onSearch() { + // TODO: Implement + } + + @override + void dispose() { + windowManager.removeListener(this); + _searchFocusNode.dispose(); + _searchController.dispose(); + super.dispose(); + } + + @override + void onWindowEnterFullScreen() { + _fullScreen.value = true; + } + + @override + void onWindowLeaveFullScreen() { + _fullScreen.value = false; + } + + @override + void onWindowFocus() { + _focused.value = true; + } + + @override + void onWindowBlur() { + _focused.value = false; + } + + @override + void onWindowResized() { + _settingsController.saveWindowSize(); + super.onWindowResized(); + } + + @override + void onWindowMoved() { + windowManager.getPosition() + .then((value) => _settingsController.saveWindowOffset(value)); + super.onWindowMoved(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Stack( + children: [ + Obx(() => NavigationPaneTheme( + data: NavigationPaneThemeData( + backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.9), + ), + child: NavigationView( + paneBodyBuilder: (pane, body) => Padding( + padding: const EdgeInsets.all(_kDefaultPadding), + child: SizedBox( + key: pageKey, + child: body + ) + ), + appBar: NavigationAppBar( + height: 32, + title: _draggableArea, + actions: WindowTitleBar(focused: _focused()), + leading: _backButton, + automaticallyImplyLeading: false, + ), + pane: NavigationPane( + selected: pageIndex.value, + onChanged: (index) { + _pagesStack.add(pageIndex.value); + pageIndex.value = index; + }, + menuButton: const SizedBox(), + displayMode: PaneDisplayMode.open, + items: _items, + header: const ProfileWidget(), + autoSuggestBox: _autoSuggestBox, + autoSuggestBoxReplacement: const Icon(FluentIcons.search), + ), + contentShape: const RoundedRectangleBorder(), + onOpenSearch: () => _searchFocusNode.requestFocus(), + transitionBuilder: (child, animation) => child + ) + )), + if (isWin11) + Obx(() => !_fullScreen.value && _focused.value ? const WindowBorder() : const SizedBox()) + ] + ); + } + + Widget get _backButton => Obx(() { + pageIndex.value; + return Button( + style: ButtonStyle( + padding: ButtonState.all(const EdgeInsets.only(top: 6.0)), + backgroundColor: ButtonState.all(Colors.transparent), + border: ButtonState.all(const BorderSide(color: Colors.transparent)) + ), + onPressed: _pagesStack.isEmpty ? null : () => pageIndex.value = _pagesStack.removeLast(), + child: const Icon(FluentIcons.back, size: 12.0), + ); + }); + + GestureDetector get _draggableArea => GestureDetector( + onDoubleTap: () async => await windowManager.isMaximized() ? await windowManager.restore() : await windowManager.maximize(), + onHorizontalDragStart: (event) => windowManager.startDragging(), + onVerticalDragStart: (event) => windowManager.startDragging() + ); + + Widget get _autoSuggestBox => Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TextBox( + key: _searchKey, + controller: _searchController, + placeholder: 'Find a setting', + focusNode: _searchFocusNode, + autofocus: true, + suffix: Button( + onPressed: null, + style: ButtonStyle( + backgroundColor: ButtonState.all(Colors.transparent), + border: ButtonState.all(const BorderSide(color: Colors.transparent)) + ), + child: Transform.flip( + flipX: true, + child: Icon( + FluentIcons.search, + size: 12.0, + color: FluentTheme.of(context).resources.textFillColorPrimary + ), + ) + ) + ), + ); + + List get _items => [ + RebootPaneItem( + title: const Text("Play"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/play.png") + ), + body: const PlayPage() + ), + RebootPaneItem( + title: const Text("Host"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/host.png") + ), + body: const HostingPage() + ), + RebootPaneItem( + title: const Text("Server Browser"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/browse.png") + ), + body: const BrowsePage() + ), + RebootPaneItem( + title: const Text("Authenticator"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/auth.png") + ), + body: const AuthenticatorPage() + ), + RebootPaneItem( + title: const Text("Matchmaker"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/matchmaker.png") + ), + body: const MatchmakerPage() + ), + RebootPaneItem( + title: const Text("Info"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/info.png") + ), + body: const InfoPage() + ), + RebootPaneItem( + title: const Text("Settings"), + icon: SizedBox.square( + dimension: 24, + child: Image.asset("assets/images/settings.png") + ), + body: const SettingsPage() + ), + ]; + + String get searchValue => _searchController.text; +} diff --git a/lib/src/page/hosting_page.dart b/lib/src/page/hosting_page.dart new file mode 100644 index 0000000..6f57bcb --- /dev/null +++ b/lib/src/page/hosting_page.dart @@ -0,0 +1,215 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:dart_ipify/dart_ipify.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/update_controller.dart'; +import 'package:reboot_launcher/src/dialog/message.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:flutter/material.dart' show Icons; + +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/widget/game/start_button.dart'; +import 'package:reboot_launcher/src/widget/version/version_selector.dart'; + +class HostingPage extends StatefulWidget { + const HostingPage({Key? key}) : super(key: key); + + @override + State createState() => _HostingPageState(); +} + +class _HostingPageState extends State with AutomaticKeepAliveClientMixin { + final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); + final UpdateController _updateController = Get.find(); + late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + children: [ + Expanded( + child: ListView( + children: [ + Obx(() => Column( + children: _updateController.status.value != UpdateStatus.error ? [] : [ + SizedBox( + width: double.infinity, + child: _updateError + ), + const SizedBox( + height: 8.0 + ), + ], + )), + SettingTile( + title: "Game Server", + subtitle: "Provide basic information about your server", + expandedContent: [ + SettingTile( + title: "Name", + subtitle: "The name of your game server", + isChild: true, + content: TextFormBox( + placeholder: "Name", + controller: _hostingController.name + ) + ), + SettingTile( + title: "Description", + subtitle: "The description of your game server", + isChild: true, + content: TextFormBox( + placeholder: "Description", + controller: _hostingController.description + ) + ), + SettingTile( + title: "Password", + subtitle: "The password of your game server for the server browser", + isChild: true, + content: Obx(() => TextFormBox( + placeholder: "Password", + controller: _hostingController.password, + autovalidateMode: AutovalidateMode.always, + obscureText: !_hostingController.showPassword.value, + enableSuggestions: false, + autocorrect: false, + onChanged: (text) => _showPasswordTrailing.value = text.isNotEmpty, + suffix: Button( + onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value, + style: ButtonStyle( + shape: ButtonState.all(const CircleBorder()), + backgroundColor: ButtonState.all(Colors.transparent) + ), + child: Icon( + _hostingController.showPassword.value ? Icons.visibility_off : Icons.visibility, + color: _showPasswordTrailing.value ? null : Colors.transparent + ), + ) + )) + ), + SettingTile( + title: "Discoverable", + subtitle: "Make your server available to other players on the server browser", + isChild: true, + contentWidth: null, + content: Obx(() => ToggleSwitch( + checked: _hostingController.discoverable(), + onChanged: (value) => _hostingController.discoverable.value = value + )) + ) + ], + ), + const SizedBox( + height: 8.0, + ), + const SettingTile( + title: "Version", + subtitle: "Select the version of Fortnite you want to host", + content: VersionSelector(), + expandedContent: [ + SettingTile( + title: "Add a version from this PC's local storage", + subtitle: "Versions coming from your local disk are not guaranteed to work", + content: Button( + onPressed: VersionSelector.openAddDialog, + child: Text("Add build"), + ), + isChild: true + ), + SettingTile( + title: "Download any version from the cloud", + subtitle: "Download any Fortnite build easily from the cloud", + content: Button( + onPressed: VersionSelector.openDownloadDialog, + child: Text("Download"), + ), + isChild: true + ) + ] + ), + const SizedBox( + height: 8.0 + ), + SettingTile( + title: "Share", + subtitle: "Make it easy for other people to join your server with the options in this section", + expandedContent: [ + SettingTile( + title: "Link", + subtitle: "Copies a link for your server to the clipboard (requires the Reboot Launcher)", + isChild: true, + content: Button( + onPressed: () async { + FlutterClipboard.controlC("$kCustomUrlSchema://${_gameController.uuid}"); + showMessage( + "Copied your link to the clipboard", + severity: InfoBarSeverity.success + ); + }, + child: const Text("Copy Link"), + ) + ), + SettingTile( + title: "Public IP", + subtitle: "Copies your current public IP to the clipboard (doesn't require the Reboot Launcher)", + isChild: true, + content: Button( + onPressed: () async { + try { + showMessage( + "Obtaining your public IP...", + loading: true, + duration: null + ); + var ip = await Ipify.ipv4(); + FlutterClipboard.controlC(ip); + showMessage( + "Copied your IP to the clipboard", + severity: InfoBarSeverity.success + ); + }catch(error) { + showMessage( + "An error occurred while obtaining your public IP: $error", + severity: InfoBarSeverity.error, + duration: snackbarLongDuration + ); + } + }, + child: const Text("Copy IP"), + ) + ) + ], + ) + ], + ), + ), + const SizedBox( + height: 8.0, + ), + const LaunchButton( + host: true + ) + ], + ); + } + + Widget get _updateError => MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: _updateController.update, + child: const InfoBar( + title: Text("The reboot dll couldn't be downloaded: click here to try again"), + severity: InfoBarSeverity.info + ), + ), + ); +} \ No newline at end of file diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart new file mode 100644 index 0000000..11a6c5a --- /dev/null +++ b/lib/src/page/info_page.dart @@ -0,0 +1,133 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; + +import 'package:reboot_launcher/src/controller/settings_controller.dart'; + +class InfoPage extends StatefulWidget { + const InfoPage({Key? key}) : super(key: key); + + @override + State createState() => _InfoPageState(); +} + +class _InfoPageState extends State with AutomaticKeepAliveClientMixin { + final SettingsController _settingsController = Get.find(); + late final ScrollController _controller; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + _controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance); + _controller.addListener(() { + _settingsController.scrollingDistance = _controller.offset; + }); + + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + children: [ + Expanded( + child: ListView( + children: [ + SettingTile( + title: 'What is Project Reboot?', + subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. ' + 'The project was started on Discord by Milxnor. ' + 'The project is no longer being actively maintained.', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'What is a game server?', + subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. ' + 'You can join someone else\'s game server, or host one on your PC by going to the "Host" tab. ', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'What is a client?', + subtitle: 'A client is the actual Fortnite game. ' + 'You can download any version of Fortnite from the launcher in the "Play" tab. ' + 'You can also import versions from your local PC, but remember that these may be corrupted. ' + 'If a local version doesn\'t work, try installing it from the launcher before reporting a bug.', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'What is an authenticator?', + subtitle: 'An authenticator is a program that handles authentication, parties and voice chats. ' + 'By default, a LawinV1 server will be started for you to play. ' + 'You can use also use an authenticator running locally(on your PC) or remotely(on another PC). ' + 'Changing the authenticator settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! ' + 'If you need to restore these settings, go to the "Settings" tab and click on "Restore Defaults". ', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'Do I need to update DLLs?', + subtitle: 'No, all the files that the launcher uses are automatically updated. ' + 'You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. ' + 'Unless you are an advanced user, changing these options is not recommended', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'Where can I report bugs or ask for new features?', + subtitle: 'Go to the "Settings" tab and click on report bug. ' + 'Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ) + ], + ), + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/src/page/matchmaker_page.dart b/lib/src/page/matchmaker_page.dart new file mode 100644 index 0000000..1136185 --- /dev/null +++ b/lib/src/page/matchmaker_page.dart @@ -0,0 +1,139 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/widget/server/type_selector.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; + +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/widget/server/start_button.dart'; + +class MatchmakerPage extends StatefulWidget { + const MatchmakerPage({Key? key}) : super(key: key); + + @override + State createState() => _MatchmakerPageState(); +} + +class _MatchmakerPageState extends State with AutomaticKeepAliveClientMixin { + final MatchmakerController _matchmakerController = Get.find(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + children: [ + Expanded( + child: ListView( + children: [ + Obx(() => SettingTile( + title: "Matchmaker configuration", + subtitle: "This section contains the matchmaker's configuration", + content: const ServerTypeSelector( + authenticator: false + ), + expandedContent: [ + if(_matchmakerController.type.value == ServerType.remote) + SettingTile( + title: "Host", + subtitle: "The hostname of the matchmaker", + isChild: true, + content: TextFormBox( + placeholder: "Host", + controller: _matchmakerController.host, + readOnly: _matchmakerController.type.value != ServerType.remote + ) + ), + if(_matchmakerController.type.value != ServerType.embedded) + SettingTile( + title: "Port", + subtitle: "The port of the matchmaker", + isChild: true, + content: TextFormBox( + placeholder: "Port", + controller: _matchmakerController.port, + readOnly: _matchmakerController.type.value != ServerType.remote + ) + ), + if(_matchmakerController.type.value == ServerType.embedded) + SettingTile( + title: "Game server address", + subtitle: "The address of the game server used by the matchmaker", + isChild: true, + content: TextFormBox( + placeholder: "Address", + controller: _matchmakerController.gameServerAddress + ) + ), + if(_matchmakerController.type.value == ServerType.embedded) + SettingTile( + title: "Detached", + subtitle: "Whether the embedded matchmaker should be started as a separate process, useful for debugging", + contentWidth: null, + isChild: true, + content: Obx(() => ToggleSwitch( + checked: _matchmakerController.detached.value, + onChanged: (value) => _matchmakerController.detached.value = value + )), + ) + ] + )), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Installation directory", + subtitle: "Opens the folder where the embedded matchmaker is located", + content: Button( + onPressed: () => launchUrl(authenticatorDirectory.uri), + child: const Text("Show Files") + ) + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Reset matchmaker", + subtitle: "Resets the authenticator's settings to their default values", + content: Button( + onPressed: () => showDialog( + builder: (context) => InfoDialog( + text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", + buttons: [ + DialogButton( + type: ButtonType.secondary, + text: "Close", + ), + DialogButton( + type: ButtonType.primary, + text: "Reset", + onTap: () { + _matchmakerController.reset(); + Navigator.of(context).pop(); + }, + ) + ], + ) + ), + child: const Text("Reset"), + ) + ) + ] + ), + ), + const SizedBox( + height: 8.0, + ), + const ServerButton( + authenticator: false + ) + ], + ); + } +} diff --git a/lib/src/page/play_page.dart b/lib/src/page/play_page.dart new file mode 100644 index 0000000..1058514 --- /dev/null +++ b/lib/src/page/play_page.dart @@ -0,0 +1,109 @@ + +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/widget/game/start_button.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:reboot_launcher/src/widget/version/version_selector.dart'; + + +class PlayPage extends StatefulWidget { + const PlayPage({Key? key}) : super(key: key); + + @override + State createState() => _PlayPageState(); +} + +class _PlayPageState extends State { + final MatchmakerController _matchmakerController = Get.find(); + final HostingController _hostingController = Get.find(); + late final RxBool _selfServer; + + @override + void initState() { + _selfServer = RxBool(_isLocalPlay); + _matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay); + _hostingController.started.listen((_) => _selfServer.value = _isLocalPlay); + super.initState(); + } + + bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text) + && !_hostingController.started.value; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: ListView( + children: [ + const SettingTile( + title: "Version", + subtitle: "Select the version of Fortnite you want to host", + content: VersionSelector(), + expandedContent: [ + SettingTile( + title: "Add a version from this PC's local storage", + subtitle: "Versions coming from your local disk are not guaranteed to work", + content: Button( + onPressed: VersionSelector.openAddDialog, + child: Text("Add build"), + ), + isChild: true + ), + SettingTile( + title: "Download any version from the cloud", + subtitle: "Download any Fortnite build easily from the cloud", + content: Button( + onPressed: VersionSelector.openDownloadDialog, + child: Text("Download"), + ), + isChild: true + ) + ] + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Game Server", + subtitle: "Helpful shortcuts to find the server where you want to play", + expandedContent: [ + SettingTile( + title: "Host a server", + subtitle: "Do you want to play with your friends? Host a server for them!", + content: Button( + onPressed: () => pageIndex.value = 1, + child: const Text("Host") + ), + isChild: true + ), + SettingTile( + title: "Join a server", + subtitle: "Find a server where you can play on the launcher's server browser", + content: Button( + onPressed: () => pageIndex.value = 2, + child: const Text("Browse") + ), + isChild: true + ) + ] + ), + ], + ) + ), + const SizedBox( + height: 8.0, + ), + const LaunchButton( + startLabel: 'Launch Fortnite', + stopLabel: 'Close Fortnite', + host: false + ) + ] + ); + } +} \ No newline at end of file diff --git a/lib/src/page/settings_page.dart b/lib/src/page/settings_page.dart new file mode 100644 index 0000000..0557a7f --- /dev/null +++ b/lib/src/page/settings_page.dart @@ -0,0 +1,182 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/controller/update_controller.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/widget/common/file_selector.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State with AutomaticKeepAliveClientMixin { + final BuildController _buildController = Get.find(); + final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); + final AuthenticatorController _authenticatorController = Get.find(); + final SettingsController _settingsController = Get.find(); + final UpdateController _updateController = Get.find(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + SettingTile( + title: "Client settings", + subtitle: "This section contains the dlls used to make the Fortnite client work", + expandedContent: [ + _createFileSetting( + title: "Unreal engine console", + description: "This file is injected to unlock the Unreal Engine Console", + controller: _settingsController.consoleDll + ), + _createFileSetting( + title: "Authentication patcher", + description: "This file is injected to redirect all HTTP requests to the launcher's authenticator", + controller: _settingsController.authDll + ), + SettingTile( + title: "Custom launch arguments", + subtitle: "Additional arguments to use when launching the game", + isChild: true, + content: TextFormBox( + placeholder: "Arguments...", + controller: _gameController.customLaunchArgs, + ) + ), + ], + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Server settings", + subtitle: "This section contains settings related to the game server implementation", + expandedContent: [ + _createFileSetting( + title: "Game server", + description: "This file is injected to create a game server & host matches", + controller: _settingsController.rebootDll + ), + SettingTile( + title: "Update mirror", + subtitle: "The URL used to update the game server dll", + content: TextFormBox( + placeholder: "URL", + controller: _updateController.url, + validator: checkUpdateUrl + ), + isChild: true + ), + SettingTile( + title: "Update timer", + subtitle: "Determines when the game server dll should be updated", + content: Obx(() => DropDownButton( + leading: Text(_updateController.timer.value.text), + items: UpdateTimer.values.map((entry) => MenuFlyoutItem( + text: Text(entry.text), + onPressed: () => _updateController.timer.value = entry + )).toList() + )), + isChild: true + ), + ], + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: "Launcher utilities", + subtitle: "This section contains handy settings for the launcher", + expandedContent: [ + SettingTile( + title: "Installation directory", + subtitle: "Opens the installation directory", + isChild: true, + content: Button( + onPressed: () => launchUrl(installationDirectory.uri), + child: const Text("Show Files"), + ) + ), + SettingTile( + title: "Create a bug report", + subtitle: "Help me fix bugs by reporting them", + isChild: true, + content: Button( + onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")), + child: const Text("Report a bug"), + ) + ), + SettingTile( + title: "Reset settings", + subtitle: "Resets the launcher's settings to their default values", + isChild: true, + content: Button( + onPressed: () => showDialog( + builder: (context) => InfoDialog( + text: "Do you want to reset all the launcher's settings to their default values? This action is irreversible", + buttons: [ + DialogButton( + type: ButtonType.secondary, + text: "Close", + ), + DialogButton( + type: ButtonType.primary, + text: "Reset", + onTap: () { + _buildController.reset(); + _gameController.reset(); + _hostingController.reset(); + _authenticatorController.reset(); + _settingsController.reset(); + _updateController.reset(); + Navigator.of(context).pop(); + }, + ) + ], + ) + ), + child: const Text("Reset"), + ) + ) + ], + ), + ] + ); + } + + Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile( + title: title, + subtitle: description, + content: FileSelector( + placeholder: "Path", + windowTitle: "Select a file", + controller: controller, + validator: checkDll, + extension: "dll", + folder: false + ), + isChild: true + ); +} + +extension _UpdateTimerExtension on UpdateTimer { + String get text => this == UpdateTimer.never ? "Never" : "Every $name"; +} \ No newline at end of file diff --git a/lib/src/ui/controller/hosting_controller.dart b/lib/src/ui/controller/hosting_controller.dart deleted file mode 100644 index 7e37855..0000000 --- a/lib/src/ui/controller/hosting_controller.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/update_controller.dart'; - -import 'package:reboot_launcher/src/model/game_instance.dart'; -import 'package:reboot_launcher/src/model/update_status.dart'; -import 'package:reboot_launcher/src/util/reboot.dart'; - - -const String kDefaultServerName = "Reboot Game Server"; - -class HostingController extends GetxController { - late final GetStorage _storage; - late final TextEditingController name; - late final TextEditingController description; - late final RxBool discoverable; - late final RxBool started; - late final Rx updateStatus; - GameInstance? instance; - - HostingController() { - _storage = GetStorage("reboot_hosting"); - name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName); - name.addListener(() => _storage.write("name", name.text)); - description = TextEditingController(text: _storage.read("description") ?? ""); - description.addListener(() => _storage.write("description", description.text)); - discoverable = RxBool(_storage.read("discoverable") ?? false); - discoverable.listen((value) => _storage.write("discoverable", value)); - updateStatus = Rx(UpdateStatus.waiting); - started = RxBool(false); - startUpdater(); - } - - Future startUpdater() async { - var settings = Get.find(); - if(!settings.autoUpdate()){ - updateStatus.value = UpdateStatus.success; - return; - } - - updateStatus.value = UpdateStatus.started; - try { - updateTime = await downloadRebootDll(settings.updateUrl.text, updateTime); - updateStatus.value = UpdateStatus.success; - }catch(_) { - updateStatus.value = UpdateStatus.error; - rethrow; - } - } -} diff --git a/lib/src/ui/controller/server_controller.dart b/lib/src/ui/controller/server_controller.dart deleted file mode 100644 index 729f43e..0000000 --- a/lib/src/ui/controller/server_controller.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'dart:io'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; - -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/util/server.dart'; - -class ServerController extends GetxController { - static const String _kDefaultServerHost = "127.0.0.1"; - static const String _kDefaultServerPort = "3551"; - - late final GetStorage _storage; - late final TextEditingController host; - late final TextEditingController port; - late final Rx type; - late RxBool started; - late RxBool detached; - HttpServer? remoteServer; - - ServerController() { - _storage = GetStorage("reboot_server"); - started = RxBool(false); - type = Rx(ServerType.values.elementAt(_storage.read("type") ?? 0)); - type.listen((value) { - host.text = _readHost(); - port.text = _readPort(); - _storage.write("type", value.index); - if(!started.value) { - return; - } - - stop(); - }); - host = TextEditingController(text: _readHost()); - host.addListener(() => _storage.write("${type.value.id}_host", host.text)); - port = TextEditingController(text: _readPort()); - port.addListener(() => _storage.write("${type.value.id}_port", port.text)); - detached = RxBool(_storage.read("detached") ?? false); - detached.listen((value) => _storage.write("detached", value)); - } - - void reset() async { - await stop(); - type.value = ServerType.values.elementAt(0); - for(var type in ServerType.values){ - _storage.write("${type.id}_host", null); - _storage.write("${type.id}_port", null); - } - - host.text = type.value != ServerType.remote ? _kDefaultServerHost : ""; - port.text = _kDefaultServerPort; - detached.value = false; - } - - String _readHost() { - String? value = _storage.read("${type.value.id}_host"); - return value != null && value.isNotEmpty ? value - : type.value != ServerType.remote ? _kDefaultServerHost : ""; - } - - String _readPort() { - return _storage.read("${type.value.id}_port") ?? _kDefaultServerPort; - } - - Future stop() async { - started.value = false; - try{ - switch(type()){ - case ServerType.embedded: - stopServer(); - break; - case ServerType.remote: - await remoteServer?.close(force: true); - remoteServer = null; - break; - case ServerType.local: - break; - } - return true; - }catch(_){ - started.value = true; - return false; - } - } -} \ No newline at end of file diff --git a/lib/src/ui/controller/update_controller.dart b/lib/src/ui/controller/update_controller.dart deleted file mode 100644 index 148bcde..0000000 --- a/lib/src/ui/controller/update_controller.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:get_storage/get_storage.dart'; - -final GetStorage _storage = GetStorage("reboot_update"); - -int? get updateTime => _storage.read("last_update_v2"); -set updateTime(int? updateTime) => _storage.write("last_update_v2", updateTime); \ No newline at end of file diff --git a/lib/src/ui/dialog/add_server_version.dart b/lib/src/ui/dialog/add_server_version.dart deleted file mode 100644 index 73a6622..0000000 --- a/lib/src/ui/dialog/add_server_version.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/util/build.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:universal_disk_space/universal_disk_space.dart'; - -import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/ui/controller/build_controller.dart'; -import 'package:reboot_launcher/src/ui/widget/home/build_selector.dart'; -import 'package:reboot_launcher/src/ui/widget/home/version_name_input.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart'; -import 'dialog.dart'; -import 'dialog_button.dart'; - -class AddServerVersion extends StatefulWidget { - const AddServerVersion({Key? key}) : super(key: key); - - @override - State createState() => _AddServerVersionState(); -} - -class _AddServerVersionState extends State { - final GameController _gameController = Get.find(); - final BuildController _buildController = Get.find(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _pathController = TextEditingController(); - final Rx _status = Rx(DownloadStatus.form); - final GlobalKey _formKey = GlobalKey(); - final Rxn _timeLeft = Rxn(); - final Rxn _downloadProgress = Rxn(); - - late DiskSpace _diskSpace; - late Future _fetchFuture; - late Future _diskFuture; - - CancelableOperation? _manifestDownloadProcess; - Object? _error; - StackTrace? _stackTrace; - - @override - void initState() { - _fetchFuture = _buildController.builds != null - ? Future.value(true) - : compute(fetchBuilds, null) - .then((value) => _buildController.builds = value); - _diskSpace = DiskSpace(); - _diskFuture = _diskSpace.scan() - .then((_) => _updateFormDefaults()); - super.initState(); - } - - @override - void dispose() { - _pathController.dispose(); - _nameController.dispose(); - _cancelDownload(); - super.dispose(); - } - - void _cancelDownload() { - if (_status.value != DownloadStatus.extracting && _status.value != DownloadStatus.extracting) { - return; - } - - if (_manifestDownloadProcess == null) { - return; - } - - Process.run('${assetsDirectory.path}\\builds\\stop.bat', []); - _manifestDownloadProcess?.cancel(); - } - - @override - Widget build(BuildContext context) => Form( - key: _formKey, - child: Obx(() { - switch(_status.value){ - case DownloadStatus.form: - return FutureBuilder( - future: Future.wait([_fetchFuture, _diskFuture]), - builder: (context, snapshot) { - if (snapshot.hasError) { - WidgetsBinding.instance - .addPostFrameCallback((_) => - _onDownloadError(snapshot.error, snapshot.stackTrace)); - } - - if (!snapshot.hasData) { - return ProgressDialog( - text: "Fetching builds and disks...", - onStop: () => Navigator.of(context).pop() - ); - } - - return FormDialog( - content: _createFormBody(), - buttons: _createFormButtons() - ); - } - ); - case DownloadStatus.downloading: - return GenericDialog( - header: _createDownloadBody(), - buttons: _createCloseButton() - ); - case DownloadStatus.extracting: - return GenericDialog( - header: _createExtractingBody(), - buttons: _createCloseButton() - ); - case DownloadStatus.error: - return ErrorDialog( - exception: _error ?? Exception("unknown error"), - stackTrace: _stackTrace, - errorMessageBuilder: (exception) => "Cannot download version: $exception" - ); - case DownloadStatus.done: - return const InfoDialog( - text: "The download was completed successfully!", - ); - } - }) - ); - - List _createFormButtons() { - return [ - DialogButton(type: ButtonType.secondary), - DialogButton( - text: "Download", - type: ButtonType.primary, - onTap: () => _startDownload(context), - ) - ]; - } - - void _startDownload(BuildContext context) async { - try { - var build = _buildController.selectedBuildRx.value; - if(build == null){ - return; - } - - _status.value = DownloadStatus.downloading; - var future = downloadArchiveBuild( - build.link, - Directory(_pathController.text), - (progress, eta) => _onDownloadProgress(progress, eta, false), - (progress, eta) => _onDownloadProgress(progress, eta, true), - ); - future.then((value) => _onDownloadComplete()); - future.onError((error, stackTrace) => _onDownloadError(error, stackTrace)); - _manifestDownloadProcess = CancelableOperation.fromFuture(future); - } catch (exception, stackTrace) { - _onDownloadError(exception, stackTrace); - } - } - - Future _onDownloadComplete() async { - if (!mounted) { - return; - } - - _status.value = DownloadStatus.done; - _gameController.addVersion(FortniteVersion( - name: _nameController.text, - location: Directory(_pathController.text) - )); - } - - void _onDownloadError(Object? error, StackTrace? stackTrace) { - if (!mounted) { - return; - } - - _status.value = DownloadStatus.error; - _error = error; - _stackTrace = stackTrace; - } - - void _onDownloadProgress(double? progress, String? timeLeft, bool extracting) { - if (!mounted) { - return; - } - - _status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading; - _timeLeft.value = timeLeft; - _downloadProgress.value = progress; - } - - Widget _createDownloadBody() => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "Downloading...", - style: FluentTheme.maybeOf(context)?.typography.body, - textAlign: TextAlign.start, - ), - ), - - const SizedBox( - height: 8.0, - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${(_downloadProgress.value ?? 0).round()}%", - style: FluentTheme.maybeOf(context)?.typography.body, - ), - - if(_timeLeft.value != null) - Text( - "Time left: ${_timeLeft.value}", - style: FluentTheme.maybeOf(context)?.typography.body, - ) - ], - ), - - const SizedBox( - height: 8.0, - ), - - SizedBox( - width: double.infinity, - child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble()) - ), - - const SizedBox( - height: 8.0, - ) - ], - ); - - Widget _createExtractingBody() => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "Extracting...", - style: FluentTheme.maybeOf(context)?.typography.body, - textAlign: TextAlign.start, - ), - ), - - const SizedBox( - height: 8.0, - ), - - const SizedBox( - width: double.infinity, - child: ProgressBar() - ), - - const SizedBox( - height: 8.0, - ) - ], - ); - - Widget _createFormBody() { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BuildSelector( - onSelected: _updateFormDefaults - ), - - const SizedBox( - height: 16.0 - ), - - VersionNameInput( - controller: _nameController - ), - - const SizedBox( - height: 16.0 - ), - - FileSelector( - label: "Path", - placeholder: "Type the download destination", - windowTitle: "Select download destination", - controller: _pathController, - validator: checkDownloadDestination, - folder: true - ), - - const SizedBox( - height: 16.0 - ) - ], - ); - } - - List _createCloseButton() { - return [ - DialogButton( - text: "Stop", - type: ButtonType.only - ) - ]; - } - - Future _updateFormDefaults() async { - if(_diskSpace.disks.isEmpty){ - return; - } - - await _fetchFuture; - var bestDisk = _diskSpace.disks - .reduce((first, second) => first.availableSpace > second.availableSpace ? first : second); - var build = _buildController.selectedBuildRx.value; - if(build== null){ - return; - } - - _pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite " - "${build.version}"; - _nameController.text = build.version.toString(); - _formKey.currentState?.validate(); - } -} - -enum DownloadStatus { form, downloading, extracting, error, done } diff --git a/lib/src/ui/dialog/server_dialogs.dart b/lib/src/ui/dialog/server_dialogs.dart deleted file mode 100644 index d4e580e..0000000 --- a/lib/src/ui/dialog/server_dialogs.dart +++ /dev/null @@ -1,284 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; -import 'package:sync/semaphore.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'package:reboot_launcher/main.dart'; -import 'package:reboot_launcher/src/util/server.dart'; -import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; -import 'dialog.dart'; -import 'dialog_button.dart'; - -extension ServerControllerDialog on ServerController { - static Semaphore semaphore = Semaphore(); - - Future restart(bool closeLocalPromptAutomatically) async { - await resetWinNat(); - return (!started() || await stop()) && await toggle(closeLocalPromptAutomatically); - } - - Future toggle(bool closeLocalPromptAutomatically) async { - try{ - semaphore.acquire(); - if (type() == ServerType.local) { - return _pingSelfInteractive(closeLocalPromptAutomatically); - } - - var result = await _toggle(); - if(!result){ - started.value = false; - return false; - } - - var ping = await _pingSelfInteractive(true); - if(!ping){ - started.value = false; - return false; - } - - return true; - }finally{ - semaphore.release(); - } - } - - Future _toggle([ServerResultType? lastResultType]) async { - if (started.value) { - var result = await stop(); - if (!result) { - started.value = true; - _showCannotStopError(); - return true; - } - - return false; - } - - started.value = true; - var conditions = await checkServerPreconditions(host.text, port.text, type.value); - var result = conditions.type == ServerResultType.canStart ? await _startServer() : conditions; - if(result.type == ServerResultType.alreadyStarted) { - started.value = false; - return true; - } - - var handled = await _handleResultType(result, lastResultType); - if (!handled) { - return false; - } - - return handled; - } - - Future _startServer() async { - try{ - switch(type()){ - case ServerType.embedded: - startServer(detached()); - break; - case ServerType.remote: - var uriResult = await _pingRemoteInteractive(); - if(uriResult == null){ - return ServerResult( - type: ServerResultType.cannotPingServer - ); - } - - remoteServer = await startRemoteServer(uriResult); - break; - case ServerType.local: - break; - } - }catch(error, stackTrace){ - return ServerResult( - error: error, - stackTrace: stackTrace, - type: ServerResultType.unknownError - ); - } - - return ServerResult( - type: ServerResultType.canStart - ); - } - - Future _handleResultType(ServerResult result, ServerResultType? lastResultType) async { - var newResultType = result.type; - switch (newResultType) { - case ServerResultType.missingHostError: - _showMissingHostError(); - return false; - case ServerResultType.missingPortError: - _showMissingPortError(); - return false; - case ServerResultType.illegalPortError: - _showIllegalPortError(); - return false; - case ServerResultType.cannotPingServer: - return false; - case ServerResultType.backendPortTakenError: - if (lastResultType == ServerResultType.backendPortTakenError) { - _showPortTakenError(3551); - return false; - } - - await freeLawinPort(); - await stop(); - return _toggle(newResultType); - case ServerResultType.matchmakerPortTakenError: - if (lastResultType == ServerResultType.matchmakerPortTakenError) { - _showPortTakenError(8080); - return false; - } - - await freeMatchmakerPort(); - await stop(); - return _toggle(newResultType); - case ServerResultType.unknownError: - if(lastResultType == ServerResultType.unknownError) { - _showUnknownError(result); - return false; - } - - await resetWinNat(); - await stop(); - return _toggle(newResultType); - case ServerResultType.alreadyStarted: - case ServerResultType.canStart: - return true; - case ServerResultType.stopped: - return false; - } - } - - Future _pingSelfInteractive(bool closeAutomatically) async { - try { - Future ping() async { - for(var i = 0; i < 3; i++){ - var result = await pingSelf(port.text); - if(result != null){ - return true; - }else { - await Future.delayed(const Duration(seconds: 1)); - } - } - - return false; - } - - var future = _waitFutureOrTime(ping()); - var result = await showDialog( - context: appKey.currentContext!, - builder: (context) => - FutureBuilderDialog( - future: future, - loadingMessage: "Pinging ${type().id} server...", - successfulBody: FutureBuilderDialog.ofMessage( - "The ${type().id} server works correctly"), - unsuccessfulBody: FutureBuilderDialog.ofMessage( - "The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."), - errorMessageBuilder: ( - exception) => "An error occurred while pining the ${type().id} server: $exception", - closeAutomatically: closeAutomatically - ) - ) ?? false; - return result && await future; - } catch (_) { - return false; - } - } - - Future _pingRemoteInteractive() async { - try { - var future = ping(host.text, port.text); - await showDialog( - context: appKey.currentContext!, - builder: (context) => - FutureBuilderDialog( - future: future, - closeAutomatically: true, - loadingMessage: "Pinging remote server...", - successfulBody: FutureBuilderDialog.ofMessage( - "The server at ${host.text}:${port - .text} works correctly"), - unsuccessfulBody: FutureBuilderDialog.ofMessage( - "The server at ${host.text}:${port - .text} doesn't work. Check the hostname and/or the port and try again."), - errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception" - ) - ); - return await future; - } catch (_) { - return null; - } - } - - Future _showPortTakenError(int port) async { - showDialog( - context: appKey.currentContext!, - builder: (context) => InfoDialog( - text: "Port $port is already in use and the associating process cannot be killed. Kill it manually and try again.", - ) - ); - } - - void _showCannotStopError() { - if(!started.value){ - return; - } - - showDialog( - context: appKey.currentContext!, - builder: (context) => - const InfoDialog( - text: "Cannot stop backend server" - ) - ); - } - - void showUnexpectedServerError() => showDialog( - context: appKey.currentContext!, - builder: (context) => InfoDialog( - text: "The backend server died unexpectedly", - buttons: [ - DialogButton( - text: "Close", - type: ButtonType.secondary, - onTap: () => Navigator.of(context).pop(), - ), - - DialogButton( - text: "Open log", - type: ButtonType.primary, - onTap: () { - if(serverLogFile.existsSync()){ - showMessage("No log is available"); - }else { - launchUrl(serverLogFile.uri); - } - Navigator.of(context).pop(); - } - ), - ], - ) - ); - - void _showIllegalPortError() => showMessage("Invalid port for backend server"); - - void _showMissingPortError() => showMessage("Missing port for backend server"); - - void _showMissingHostError() => showMessage("Missing the host name for backend server"); - - Future _showUnknownError(ServerResult result) => showDialog( - context: appKey.currentContext!, - builder: (context) => - ErrorDialog( - exception: result.error ?? Exception("Unknown error"), - stackTrace: result.stackTrace, - errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred" - ) - ); - - Future _waitFutureOrTime(Future resultFuture) => Future.wait([resultFuture, Future.delayed(const Duration(seconds: 1)).then((value) => true)]).then((value) => value.reduce((f, s) => f && s)); -} \ No newline at end of file diff --git a/lib/src/ui/dialog/snackbar.dart b/lib/src/ui/dialog/snackbar.dart deleted file mode 100644 index 4134bdf..0000000 --- a/lib/src/ui/dialog/snackbar.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -import 'package:reboot_launcher/main.dart'; - -void showMessage(String text){ - showSnackbar( - appKey.currentContext!, - Snackbar( - content: Text(text, textAlign: TextAlign.center), - extended: true - ) - ); -} \ No newline at end of file diff --git a/lib/src/ui/page/browse_page.dart b/lib/src/ui/page/browse_page.dart deleted file mode 100644 index 3f3ed5c..0000000 --- a/lib/src/ui/page/browse_page.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - - -class BrowsePage extends StatefulWidget { - const BrowsePage( - {Key? key}) - : super(key: key); - - @override - State createState() => _BrowsePageState(); -} - -class _BrowsePageState extends State with AutomaticKeepAliveClientMixin { - Future? _query; - Stream>>? _stream; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - if(_query != null) { - return; - } - - _query = _stream != null ? Future.value(_stream) : _initStream(); - } - - Future _initStream() async { - var supabase = Supabase.instance.client; - _stream = supabase.from('hosts') - .stream(primaryKey: ['id']) - .asBroadcastStream(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return FutureBuilder( - future: _query, - builder: (context, value) => StreamBuilder>>( - stream: _stream, - builder: (context, snapshot) { - if(snapshot.hasError){ - return Center( - child: Text( - "Cannot fetch servers: ${snapshot.error}", - textAlign: TextAlign.center - ) - ); - } - - var data = snapshot.data; - if(data == null){ - return const SizedBox(); - } - - return Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Server Browser', - textAlign: TextAlign.start, - style: FluentTheme.of(context).typography.title - ), - const SizedBox( - height: 4.0 - ), - const Text( - 'Looking for a match? This is the right place!', - textAlign: TextAlign.start - ), - - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: ListView.builder( - itemCount: data.length, - itemBuilder: (context, index) { - var version = data[index]['version']; - var versionSplit = version.indexOf("-"); - version = versionSplit != -1 ? version.substring(0, versionSplit) : version; - version = version.endsWith(".0") ? version.substring(0, version.length - 2) : version; - return SettingTile( - title: "${data[index]['name']} • Fortnite $version", - subtitle: data[index]['description'], - content: Button( - onPressed: () {}, - child: const Text('Join'), - ) - ); - } - ), - ), - ) - ], - ), - ); - } - ) - ); - } -} \ No newline at end of file diff --git a/lib/src/ui/page/home_page.dart b/lib/src/ui/page/home_page.dart deleted file mode 100644 index fc4bbd4..0000000 --- a/lib/src/ui/page/home_page.dart +++ /dev/null @@ -1,288 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/main.dart'; -import 'package:reboot_launcher/src/ui/page/launcher_page.dart'; -import 'package:reboot_launcher/src/ui/page/server_page.dart'; -import 'package:reboot_launcher/src/ui/page/settings_page.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/profile_widget.dart'; - -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/widget/os/window_border.dart'; -import 'package:reboot_launcher/src/ui/widget/os/window_title_bar.dart'; -import 'package:window_manager/window_manager.dart'; -import 'hosting_page.dart'; -import 'info_page.dart'; - -class HomePage extends StatefulWidget { - const HomePage({Key? key}) : super(key: key); - - @override - State createState() => _HomePageState(); -} - -class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { - static const double _kDefaultPadding = 12.0; - static const int _kPagesLength = 6; - - final SettingsController _settingsController = Get.find(); - final GlobalKey _searchKey = GlobalKey(); - final FocusNode _searchFocusNode = FocusNode(); - final TextEditingController _searchController = TextEditingController(); - final Rxn> _searchItems = Rxn(); - final RxBool _focused = RxBool(true); - final List> _navigators = List.generate(_kPagesLength, (index) => GlobalKey()); - final List _navigationStatus = List.generate(_kPagesLength, (index) => RxInt(0)); - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - windowManager.show(); - windowManager.addListener(this); - _searchController.addListener(_onSearch); - super.initState(); - } - - void _onSearch() { - if (searchValue.isEmpty) { - _searchItems.value = null; - return; - } - - _searchItems.value = _allItems - .whereType() - .where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase())) - .toList() - .cast(); - } - - @override - void dispose() { - windowManager.removeListener(this); - _searchFocusNode.dispose(); - _searchController.dispose(); - super.dispose(); - } - - @override - void onWindowFocus() { - _focused.value = true; - } - - @override - void onWindowBlur() { - _focused.value = false; - } - - @override - void onWindowResized() { - _settingsController.saveWindowSize(); - super.onWindowResized(); - } - - @override - void onWindowMoved() { - windowManager.getPosition() - .then((value) => _settingsController.saveWindowOffset(value)); - super.onWindowMoved(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Stack(children: [ - LayoutBuilder( - builder: (context, specs) => Obx(() => NavigationPaneTheme( - data: NavigationPaneThemeData( - backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.9), - ), - child: NavigationView( - paneBodyBuilder: (pane, body) => Padding( - padding: const EdgeInsets.all(_kDefaultPadding), - child: body - ), - appBar: NavigationAppBar( - height: 32, - title: _draggableArea, - actions: WindowTitleBar(focused: _focused()), - automaticallyImplyLeading: false, - leading: _backButton - ), - pane: NavigationPane( - key: appKey, - selected: _selectedIndex, - onChanged: _onIndexChanged, - menuButton: const SizedBox(), - displayMode: PaneDisplayMode.open, - items: _items, - header: ProfileWidget(), - footerItems: _footerItems, - autoSuggestBox: _autoSuggestBox, - autoSuggestBoxReplacement: const Icon(FluentIcons.search), - ), - contentShape: const RoundedRectangleBorder(), - onOpenSearch: () => _searchFocusNode.requestFocus(), - transitionBuilder: (child, animation) => child), - ) - ) - ), - if (isWin11) - Obx(() => _focused.value ? const WindowBorder() : const SizedBox()) - ]); - } - - GestureDetector get _draggableArea => GestureDetector( - onDoubleTap: () async => await windowManager.isMaximized() ? await windowManager.restore() : await windowManager.maximize(), - onHorizontalDragStart: (event) => windowManager.startDragging(), - onVerticalDragStart: (event) => windowManager.startDragging() - ); - - Widget get _backButton => Obx(() { - for (var entry in _navigationStatus) { - entry.value; - } - - var onBack = _onBack(); - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Button( - onPressed: onBack, - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.transparent), - border: ButtonState.all(BorderSide(color: Colors.transparent)) - ), - child: const Icon(FluentIcons.back, size: 13.0) - ) - ); - }); - - Function()? _onBack() { - var navigator = _navigators[_settingsController.index.value].currentState; - if (navigator == null || !navigator.mounted || !navigator.canPop()) { - return null; - } - - var status = _navigationStatus[_settingsController.index.value]; - if (status.value <= 0) { - return null; - } - - return () async { - Navigator.pop(navigator.context); - status.value -= 1; - }; - } - - void _onIndexChanged(int index) { - _navigationStatus[_settingsController.index()].value = 0; - _settingsController.index.value = index; - } - - Widget get _autoSuggestBox => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: TextBox( - key: _searchKey, - controller: _searchController, - placeholder: 'Find a setting', - focusNode: _searchFocusNode, - autofocus: true - ), - ); - - int? get _selectedIndex { - var searchItems = _searchItems(); - if (searchItems == null) { - return _settingsController.index(); - } - - if (_settingsController.index() >= _allItems.length) { - return null; - } - - var indexOnScreen = - searchItems.indexOf(_allItems[_settingsController.index()]); - if (indexOnScreen.isNegative) { - return null; - } - - return indexOnScreen; - } - - List get _allItems => [..._items, ..._footerItems]; - - List get _footerItems => searchValue.isNotEmpty ? [] : [ - PaneItem( - title: const Text("Downloads"), - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/download.png") - ) - ), - body: const SettingsPage() - ), - PaneItem( - title: const Text("Settings"), - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/settings.png") - ) - ), - body: const SettingsPage() - ) - ]; - - List get _items => _searchItems() ?? [ - PaneItem( - title: const Text("Play"), - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/play.png") - ) - ), - body: LauncherPage(_navigators[0], _navigationStatus[0]) - ), - PaneItem( - title: const Text("Host"), - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/host.png") - ) - ), - body: HostingPage(_navigators[1], _navigationStatus[1]) - ), - PaneItem( - title: const Text("Authenticator"), - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/cloud.png") - ) - ), - body: ServerPage(_navigators[2], _navigationStatus[2]) - ), - PaneItem( - title: const Text("Tutorial"), - icon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/info.png") - ) - ), - body: InfoPage(_navigators[3], _navigationStatus[3]) - ) - ]; - - String get searchValue => _searchController.text; -} diff --git a/lib/src/ui/page/hosting_page.dart b/lib/src/ui/page/hosting_page.dart deleted file mode 100644 index 5649032..0000000 --- a/lib/src/ui/page/hosting_page.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; -import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; -import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; - -import 'package:reboot_launcher/src/model/update_status.dart'; -import 'browse_page.dart'; - -class HostingPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State createState() => _HostingPageState(); -} - -class _HostingPageState extends State with AutomaticKeepAliveClientMixin { - final HostingController _hostingController = Get.find(); - final SettingsController _settingsController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Obx(() => !_settingsController.autoUpdate() || _hostingController.updateStatus().isDone() ? _body : _updateScreen); - } - - Widget get _updateScreen => const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ProgressRing(), - SizedBox(height: 8.0), - Text("Updating Reboot DLL...") - ], - ), - ], - ); - - Widget get _body => Navigator( - key: widget.navigatorKey, - initialRoute: "home", - onGenerateRoute: (settings) { - var screen = _createScreen(settings.name); - return FluentPageRoute( - builder: (context) => screen, - settings: settings - ); - }, - ); - - Widget _createScreen(String? name) { - switch(name){ - case "home": - return _HostPage(widget.navigatorKey, widget.nestedNavigation); - case "browse": - return const BrowsePage(); - default: - throw Exception("Unknown page: $name"); - } - } -} - -class _HostPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const _HostPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State<_HostPage> createState() => _HostPageState(); -} - -class _HostPageState extends State<_HostPage> with AutomaticKeepAliveClientMixin { - final HostingController _hostingController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Column( - children: [ - Expanded( - child: ListView( - children: [ - Obx(() => SizedBox( - width: double.infinity, - child: _hostingController.updateStatus.value == UpdateStatus.error ? _updateError : _rebootGuiInfo, - )), - const SizedBox( - height: 8.0 - ), - SettingTile( - title: "Game Server", - subtitle: "Provide basic information about your server", - expandedContentSpacing: 0, - expandedContent: [ - SettingTile( - title: "Name", - subtitle: "The name of your game server", - isChild: true, - content: TextFormBox( - placeholder: "Name", - controller: _hostingController.name - ) - ), - SettingTile( - title: "Description", - subtitle: "The description of your game server", - isChild: true, - content: TextFormBox( - placeholder: "Description", - controller: _hostingController.description - ) - ), - SettingTile( - title: "Discoverable", - subtitle: "Make your server available to other players on the server browser", - isChild: true, - contentWidth: null, - content: Obx(() => ToggleSwitch( - checked: _hostingController.discoverable(), - onChanged: (value) => _hostingController.discoverable.value = value - )) - ), - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Version", - subtitle: "Select the version of Fortnite you want to host", - content: const VersionSelector(), - expandedContent: [ - SettingTile( - title: "Add a version from this PC's local storage", - subtitle: "Versions coming from your local disk are not guaranteed to work", - content: Button( - onPressed: () => VersionSelector.openAddDialog(context), - child: const Text("Add build"), - ), - isChild: true - ), - SettingTile( - title: "Download any version from the cloud", - subtitle: "A curated list of supported versions by Project Reboot", - content: Button( - onPressed: () => VersionSelector.openDownloadDialog(context), - child: const Text("Download"), - ), - isChild: true - ) - ] - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Browse available servers", - subtitle: "See a list of other game servers that are being hosted", - content: Button( - onPressed: () { - widget.navigatorKey.currentState?.pushNamed('browse'); - widget.nestedNavigation.value += 1; - }, - child: const Text("Browse") - ) - ), - ], - ), - ), - const SizedBox( - height: 8.0, - ), - const LaunchButton( - host: true - ) - ], - ); - } - - InfoBar get _rebootGuiInfo => const InfoBar( - title: Text("A window will pop up after the game server is started to modify its in-game settings"), - severity: InfoBarSeverity.info - ); - - Widget get _updateError => MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: _hostingController.startUpdater, - child: const InfoBar( - title: Text("The reboot dll couldn't be downloaded: click here to try again"), - severity: InfoBarSeverity.info - ), - ), - ); -} \ No newline at end of file diff --git a/lib/src/ui/page/info_page.dart b/lib/src/ui/page/info_page.dart deleted file mode 100644 index 3274a8f..0000000 --- a/lib/src/ui/page/info_page.dart +++ /dev/null @@ -1,327 +0,0 @@ -import 'dart:async'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; -import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; -import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; -import 'package:reboot_launcher/src/util/checks.dart'; - -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; - -class InfoPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State createState() => _InfoPageState(); -} - -class _InfoPageState extends State with AutomaticKeepAliveClientMixin { - final SettingsController _settingsController = Get.find(); - late final ScrollController _controller; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - _controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance); - _controller.addListener(() { - _settingsController.scrollingDistance = _controller.offset; - }); - super.initState(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Navigator( - key: widget.navigatorKey, - initialRoute: "introduction", - onGenerateRoute: (settings) { - var screen = _createScreen(settings.name); - return FluentPageRoute( - builder: (context) => screen, - settings: settings - ); - }, - ); - } - - Widget _createScreen(String? name) { - switch(name){ - case "introduction": - return _IntroductionPage(widget.navigatorKey, widget.nestedNavigation); - case "play": - return _PlayPage(widget.navigatorKey, widget.nestedNavigation); - default: - throw Exception("Unknown page: $name"); - } - } -} - -class _IntroductionPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const _IntroductionPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State<_IntroductionPage> createState() => _IntroductionPageState(); -} - -class _IntroductionPageState extends State<_IntroductionPage> { - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ListView( - children: [ - SettingTile( - title: 'What is Project Reboot?', - subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. The project was started on Discord by Milxnor and it\'s still being actively developed. Also, it\'s open source on Github!', - titleStyle: FluentTheme.of(context).typography.title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'What is a game server?', - subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. You can join someone else\'s game server, or host one on your PC. You can host your own game server by going to the "Host" tab. Just like in Minecraft, a client needs to connect to a server hosted on a certain IP or domain. In short, remember that you need both a client and a server to play!', - titleStyle: FluentTheme.of(context).typography.title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'What is a client?', - subtitle: 'A client is the actual Fortnite game. You can download any version of Fortnite from the launcher in the "Play" tab. You can also import versions from your local PC, but remember that these may be corrupted. If a local version doesn\'t work, try installing it from the launcher before reporting a bug.', - titleStyle: FluentTheme.of(context).typography.title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'What is a backend?', - subtitle: 'A backend is the program that handles authentication, parties and voice chats. By default, a LawinV1 server will be started for you to play. You can use also use a backend running locally, that is on your PC, or remotely, that is on another PC. Changing the backend settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! If you need to restore these settings, click on "Restore Defaults" in the "Backend" tab.', - titleStyle: FluentTheme.of(context).typography.title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'Do I need to update DLLs?', - subtitle: 'No, all the files that the launcher uses are automatically updated. Though, you need to update the launcher yourself if you haven\'t downloaded it from the Microsoft Store. You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. Unless you are an advanced user, changing these options is not recommended', - titleStyle: FluentTheme.of(context).typography.title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'Where can I report bugs or ask for new features?', - subtitle: 'Go to the "Settings" tab and click on report bug. Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement', - titleStyle: FluentTheme.of(context).typography.title, - contentWidth: null, - ) - ], - ), - ), - const SizedBox( - height: 8.0, - ), - SizedBox( - width: double.infinity, - height: 48, - child: Button( - child: const Align( - alignment: Alignment.center, - child: Text("How do I play?") - ), - onPressed: () { - widget.navigatorKey.currentState?.pushNamed("play"); - widget.nestedNavigation.value += 1; - } - ), - ) - ], - ); - } -} - -class _PlayPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const _PlayPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State<_PlayPage> createState() => _PlayPageState(); -} - -class _PlayPageState extends State<_PlayPage> { - final GameController _gameController = Get.find(); - final SettingsController _settingsController = Get.find(); - final RxBool _localGameServer = RxBool(true); - final TextEditingController _remoteGameServerController = TextEditingController(); - final StreamController _remoteGameServerStream = StreamController(); - - @override - void initState() { - var ip = _settingsController.matchmakingIp.text; - _remoteGameServerController.text = isLocalHost(ip) ? "" : ip; - _remoteGameServerController.addListener(() => _remoteGameServerStream.add(null)); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ListView( - children: [ - SettingTile( - title: '1. Select a username', - subtitle: 'Choose a name for other players to see while you are in-game', - titleStyle: FluentTheme.of(context).typography.title, - expandedContentHeaderHeight: 80, - contentWidth: 0, - expandedContent: [ - SettingTile( - title: "Username", - subtitle: "The username that other players will see when you are in game", - isChild: true, - content: TextFormBox( - placeholder: "Username", - controller: _gameController.username, - autovalidateMode: AutovalidateMode.always - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: '2. Download Fortnite', - subtitle: 'Download or import the version of Fortnite you want to play. Make sure that it\'s the same as the game server\'s you want to join!', - titleStyle: FluentTheme.of(context).typography.title, - expandedContentHeaderHeight: 80, - contentWidth: 0, - expandedContent: [ - const SettingTile( - title: "Version", - subtitle: "Select the version of Fortnite you want to play", - content: VersionSelector(), - isChild: true, - ), - SettingTile( - title: "Add a version from this PC's local storage", - subtitle: "Versions coming from your local disk are not guaranteed to work", - isChild: true, - content: Button( - onPressed: () => VersionSelector.openAddDialog(context), - child: const Text("Add build"), - ), - ), - SettingTile( - title: "Download any version from the cloud", - subtitle: "A curated list of supported versions by Project Reboot", - content: Button( - onPressed: () => VersionSelector.openDownloadDialog(context), - child: const Text("Download"), - ), - isChild: true - ) - ], - ), - const SizedBox( - height: 8.0, - ), - StreamBuilder( - stream: _remoteGameServerStream.stream, - builder: (context, snapshot) => SettingTile( - title: '3. Choose a game server', - subtitle: 'Select the game server you want to use to play Fortnite.', - titleStyle: FluentTheme.of(context).typography.title, - expandedContentHeaderHeight: 80, - contentWidth: 0, - expandedContent: [ - SettingTile( - title: "Local Game Server", - subtitle: "Select this option if you want to host the game server on your PC", - contentWidth: null, - isChild: true, - content: Obx(() => Checkbox( - checked: _remoteGameServerController.text.isEmpty && _localGameServer(), - onChanged: (value) { - _localGameServer.value = value ?? false; - _remoteGameServerController.text = _settingsController.matchmakingIp.text = ""; - } - )) - ), - SettingTile( - title: "Remote Game Server", - subtitle: "Select this option if you want to join a match hosted on someone else's pc", - isChild: true, - content: TextFormBox( - controller: _remoteGameServerController, - onChanged: (value) =>_localGameServer.value = false, - placeholder: "Type the game server's ip", - validator: checkMatchmaking - ) - ) - ], - ), - ) - ], - ), - ), - const SizedBox( - height: 8.0, - ), - LaunchButton( - startLabel: 'Start playing', - stopLabel: 'Close game', - host: false, - check: () { - if(_gameController.selectedVersion == null){ - showMessage("Select a Fortnite version before continuing"); - return false; - } - - if(!_localGameServer() && _remoteGameServerController.text.isEmpty){ - showMessage("Select a game server before continuing"); - return false; - } - - if(_localGameServer()){ - _settingsController.matchmakingIp.text = "127.0.0.1"; - _gameController.autoStartGameServer.value = true; - }else { - _settingsController.matchmakingIp.text = _remoteGameServerController.text; - } - - _settingsController.firstRun.value = false; - return true; - } - ) - ] - ); - } -} \ No newline at end of file diff --git a/lib/src/ui/page/launcher_page.dart b/lib/src/ui/page/launcher_page.dart deleted file mode 100644 index 1d84ecb..0000000 --- a/lib/src/ui/page/launcher_page.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/ui/page/browse_page.dart'; -import 'package:reboot_launcher/src/ui/page/play_page.dart'; - - -class LauncherPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State createState() => _LauncherPageState(); -} - -class _LauncherPageState extends State with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Navigator( - key: widget.navigatorKey, - initialRoute: "home", - onGenerateRoute: (settings) { - var screen = _createScreen(settings.name); - return FluentPageRoute( - builder: (context) => screen, - settings: settings - ); - }, - ); - } - - Widget _createScreen(String? name) { - switch(name){ - case "home": - return PlayPage(widget.navigatorKey, widget.nestedNavigation); - case "browse": - return const BrowsePage(); - default: - throw Exception("Unknown page: $name"); - } - } -} \ No newline at end of file diff --git a/lib/src/ui/page/play_page.dart b/lib/src/ui/page/play_page.dart deleted file mode 100644 index 29af481..0000000 --- a/lib/src/ui/page/play_page.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:async'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:get/get_rx/src/rx_types/rx_types.dart'; - -import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; -import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; -import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; -import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; - -class PlayPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - const PlayPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State createState() => _PlayPageState(); -} - -class _PlayPageState extends State { - final GameController _gameController = Get.find(); - final SettingsController _settingsController = Get.find(); - final StreamController _matchmakingStream = StreamController(); - - @override - void initState() { - _gameController.password.addListener(() => _matchmakingStream.add(null)); - _settingsController.matchmakingIp.addListener(() => - _matchmakingStream.add(null)); - super.initState(); - } - - @override - Widget build(BuildContext context) => Column( - children: [ - Expanded( - child: ListView( - children: [ - SettingTile( - title: "Version", - subtitle: "Select the version of Fortnite you want to play", - content: const VersionSelector(), - expandedContent: [ - SettingTile( - title: "Add a version from this PC's local storage", - subtitle: "Versions coming from your local disk are not guaranteed to work", - content: Button( - onPressed: () => - VersionSelector.openAddDialog(context), - child: const Text("Add build"), - ), - isChild: true - ), - SettingTile( - title: "Download any version from the cloud", - subtitle: "A curated list of supported versions by Project Reboot", - content: Button( - onPressed: () => - VersionSelector.openDownloadDialog(context), - child: const Text("Download"), - ), - isChild: true - ) - ] - ), - const SizedBox( - height: 8.0, - ), - StreamBuilder( - stream: _matchmakingStream.stream, - builder: (context, value) => - SettingTile( - title: "Matchmaking host", - subtitle: "Enter the IP address of the game server hosting the match", - content: TextFormBox( - placeholder: "IP:PORT", - controller: _settingsController.matchmakingIp, - validator: checkMatchmaking, - autovalidateMode: AutovalidateMode.always - ), - expandedContent: [ - SettingTile( - title: "Automatically start game server", - subtitle: "This option is available when the matchmaker is set to localhost", - contentWidth: null, - content: !isLocalHost( - _settingsController.matchmakingIp.text) || - _gameController.password.text.isNotEmpty - ? _disabledAutoGameServerSwitch - : _autoGameServerSwitch, - isChild: true - ), - SettingTile( - title: "Browse available servers", - subtitle: "Discover new game servers that fit your play-style", - content: Button( - onPressed: () { - widget.navigatorKey.currentState - ?.pushNamed('browse'); - widget.nestedNavigation.value += 1; - }, - child: const Text("Browse") - ), - isChild: true - ) - ] - ) - ), - ], - ), - ), - const SizedBox( - height: 8.0, - ), - const LaunchButton( - host: false - ) - ], - ); - - Widget get _disabledAutoGameServerSwitch => Container( - foregroundDecoration: const BoxDecoration( - color: Colors.grey, - backgroundBlendMode: BlendMode.saturation, - ), - child: _autoGameServerSwitch, - ); - - Widget get _autoGameServerSwitch => Obx(() => ToggleSwitch( - checked: _gameController.autoStartGameServer() && - isLocalHost(_settingsController.matchmakingIp.text) && - _gameController.password.text.isEmpty, - onChanged: (value) { - if (!isLocalHost(_settingsController.matchmakingIp.text)) { - showMessage( - "This option isn't available when the matchmaker isn't set to 127.0.0.1"); - return; - } - - if (_gameController.password.text.isNotEmpty) { - showMessage( - "This option isn't available when the password isn't empty(LawinV2)"); - return; - } - - _gameController.autoStartGameServer.value = value; - } - )); -} \ No newline at end of file diff --git a/lib/src/ui/page/server_page.dart b/lib/src/ui/page/server_page.dart deleted file mode 100644 index d481271..0000000 --- a/lib/src/ui/page/server_page.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; -import 'package:reboot_launcher/src/ui/widget/server/server_button.dart'; -import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart'; -import 'package:reboot_launcher/src/util/server.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'package:reboot_launcher/src/ui/dialog/dialog.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart'; -import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; - -class ServerPage extends StatefulWidget { - final GlobalKey navigatorKey; - final RxInt nestedNavigation; - - const ServerPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); - - @override - State createState() => _ServerPageState(); -} - -class _ServerPageState extends State with AutomaticKeepAliveClientMixin { - final ServerController _serverController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Obx(() => Column( - children: [ - Expanded( - child: ListView( - children: [ - const SizedBox( - width: double.infinity, - child: InfoBar( - title: Text("The backend server handles authentication and parties, not game hosting"), - severity: InfoBarSeverity.info - ), - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Host", - subtitle: "Enter the host of the backend server", - content: TextFormBox( - placeholder: "Host", - controller: _serverController.host, - enabled: _isRemote - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Port", - subtitle: "Enter the port of the backend server", - content: TextFormBox( - placeholder: "Port", - controller: _serverController.port, - enabled: _isRemote - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Type", - subtitle: "Select the type of backend to use", - content: ServerTypeSelector() - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Detached", - subtitle: "Choose whether the backend should be started as a separate process, useful for debugging", - contentWidth: null, - content: Obx(() => ToggleSwitch( - checked: _serverController.detached(), - onChanged: (value) => _serverController.detached.value = value - )) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Server files", - subtitle: "The location where the backend is stored", - content: Button( - onPressed: () => launchUrl(serverDirectory.uri), - child: const Text("Open") - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Reset Backend", - subtitle: "Resets the launcher's backend to its default settings", - content: Button( - onPressed: () => showDialog( - context: context, - builder: (context) => InfoDialog( - text: "Do you want to reset the backend? This action is irreversible", - buttons: [ - DialogButton( - type: ButtonType.secondary, - text: "Close", - ), - DialogButton( - type: ButtonType.primary, - text: "Reset", - onTap: () { - _serverController.reset(); - Navigator.of(context).pop(); - }, - ) - ], - ) - ), - child: const Text("Reset"), - ) - ), - ] - ), - ), - const SizedBox( - height: 8.0, - ), - const ServerButton() - ], - )); - } - - bool get _isRemote => _serverController.type.value == ServerType.remote; -} diff --git a/lib/src/ui/page/settings_page.dart b/lib/src/ui/page/settings_page.dart deleted file mode 100644 index d4fee61..0000000 --- a/lib/src/ui/page/settings_page.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog.dart'; -import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; - -class SettingsPage extends StatefulWidget { - const SettingsPage({Key? key}) : super(key: key); - - @override - State createState() => _SettingsPageState(); -} - -class _SettingsPageState extends State with AutomaticKeepAliveClientMixin { - final GameController _gameController = Get.find(); - - final SettingsController _settingsController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return ListView( - children: [ - SettingTile( - title: "File settings", - subtitle: "This section contains all the settings related to files used by Fortnite", - expandedContent: [ - _createFileSetting( - title: "Game server", - description: "This file is injected to create a game server to host matches", - controller: _settingsController.rebootDll - ), - _createFileSetting( - title: "Unreal engine console", - description: "This file is injected to unlock the Unreal Engine Console in-game", - controller: _settingsController.consoleDll - ), - _createFileSetting( - title: "Authentication patcher", - description: "This file is injected to redirect all HTTP requests to the local backend", - controller: _settingsController.authDll - ), - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Automatic updates", - subtitle: "Choose whether the launcher and its files should be automatically updated", - contentWidth: null, - content: Obx(() => ToggleSwitch( - checked: _settingsController.autoUpdate.value, - onChanged: (value) => _settingsController.autoUpdate.value = value - )), - expandedContentSpacing: 0, - expandedContent: [ - SettingTile( - title: "Update Mirror", - subtitle: "The URL used to pull the latest update once a day", - content: Obx(() => TextFormBox( - placeholder: "URL", - controller: _settingsController.updateUrl, - enabled: _settingsController.autoUpdate.value, - validator: checkUpdateUrl - )), - isChild: true - ) - ] - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Custom launch arguments", - subtitle: "Enter additional arguments to use when launching the game", - content: TextFormBox( - placeholder: "Arguments...", - controller: _gameController.customLaunchArgs, - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Create a bug report", - subtitle: "Help me fix bugs by reporting them", - content: Button( - onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")), - child: const Text("Report a bug"), - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Reset settings", - subtitle: "Resets the launcher's settings to their default values", - content: Button( - onPressed: () => showDialog( - context: context, - builder: (context) => InfoDialog( - text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", - buttons: [ - DialogButton( - type: ButtonType.secondary, - text: "Close", - ), - DialogButton( - type: ButtonType.primary, - text: "Reset", - onTap: () { - _settingsController.reset(); - Navigator.of(context).pop(); - }, - ) - ], - ) - ), - child: const Text("Reset"), - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Version status", - subtitle: "Current version: 8.1", - content: Button( - onPressed: () => launchUrl(installationDirectory.uri), - child: const Text("Show Files"), - ) - ), - ] - ); - } - - Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile( - title: title, - subtitle: description, - content: FileSelector( - placeholder: "Path", - windowTitle: "Select a file", - controller: controller, - validator: checkDll, - extension: "dll", - folder: false - ), - isChild: true - ); -} diff --git a/lib/src/ui/widget/home/setting_tile.dart b/lib/src/ui/widget/home/setting_tile.dart deleted file mode 100644 index 26ccca8..0000000 --- a/lib/src/ui/widget/home/setting_tile.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class SettingTile extends StatefulWidget { - static const double kDefaultContentWidth = 200.0; - static const double kDefaultSpacing = 8.0; - static const double kDefaultHeaderHeight = 72; - - final String title; - final TextStyle? titleStyle; - final String subtitle; - final TextStyle? subtitleStyle; - final Widget? content; - final double? contentWidth; - final List? expandedContent; - final double expandedContentHeaderHeight; - final double expandedContentSpacing; - final bool isChild; - - const SettingTile( - {Key? key, - required this.title, - this.titleStyle, - required this.subtitle, - this.subtitleStyle, - this.content, - this.contentWidth = kDefaultContentWidth, - this.expandedContentHeaderHeight = kDefaultHeaderHeight, - this.expandedContentSpacing = kDefaultSpacing, - this.expandedContent, - this.isChild = false}) - : super(key: key); - - @override - State createState() => _SettingTileState(); -} - -class _SettingTileState extends State { - @override - Widget build(BuildContext context) { - if(widget.expandedContent == null){ - return _contentCard; - } - - return Expander( - initiallyExpanded: true, - headerShape: (open) => const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)), - ), - header: SizedBox( - height: widget.expandedContentHeaderHeight, - child: _header - ), - trailing: _trailing, - content: _content - ); - } - - Widget get _content { - var contents = widget.expandedContent!; - var items = List.generate(contents.length * 2, (index) => index % 2 == 0 ? contents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing)); - return Column( - children: items - ); - } - - Widget get _trailing => SizedBox( - width: widget.contentWidth, - child: widget.content - ); - - Widget get _header => ListTile( - title: Text( - widget.title, - style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle, - ), - subtitle: Text( - widget.subtitle, - style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body - ) - ); - - Widget get _contentCard { - if (widget.isChild) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: _contentCardBody - ); - } - - return Card( - borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)), - child: _contentCardBody - ); - } - - Widget get _contentCardBody => ListTile( - title: Text( - widget.title, - style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle, - ), - subtitle: Text( - widget.subtitle, - style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body - ), - trailing: _trailing - ); -} \ No newline at end of file diff --git a/lib/src/ui/widget/server/server_button.dart b/lib/src/ui/widget/server/server_button.dart deleted file mode 100644 index 92da23c..0000000 --- a/lib/src/ui/widget/server/server_button.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; -import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart'; - -class ServerButton extends StatefulWidget { - const ServerButton({Key? key}) : super(key: key); - - @override - State createState() => _ServerButtonState(); -} - -class _ServerButtonState extends State { - final ServerController _serverController = Get.find(); - - @override - Widget build(BuildContext context) => Align( - alignment: AlignmentDirectional.bottomCenter, - child: SizedBox( - width: double.infinity, - child: Obx(() => SizedBox( - height: 48, - child: Button( - child: Align( - alignment: Alignment.center, - child: Text(_buttonText), - ), - onPressed: () => _serverController.toggle(false) - ), - )), - ), - ); - - String get _buttonText { - if(_serverController.type.value == ServerType.local){ - return "Check backend"; - } - - if(_serverController.started.value){ - return "Stop backend"; - } - - return "Start backend"; - } -} diff --git a/lib/src/ui/widget/server/server_type_selector.dart b/lib/src/ui/widget/server/server_type_selector.dart deleted file mode 100644 index 4b536eb..0000000 --- a/lib/src/ui/widget/server/server_type_selector.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; - -class ServerTypeSelector extends StatelessWidget { - final ServerController _serverController = Get.find(); - - ServerTypeSelector({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return DropDownButton( - leading: Text(_serverController.type.value.name), - items: ServerType.values - .map((type) => _createItem(type)) - .toList() - ); - } - - MenuFlyoutItem _createItem(ServerType type) { - return MenuFlyoutItem( - text: Tooltip( - message: type.message, - child: Text(type.name) - ), - onPressed: () async { - await _serverController.stop(); - _serverController.type(type); - } - ); - } - -} diff --git a/lib/src/ui/widget/shared/boxed_pane_item.dart b/lib/src/ui/widget/shared/boxed_pane_item.dart deleted file mode 100644 index ba30243..0000000 --- a/lib/src/ui/widget/shared/boxed_pane_item.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class SquaredPaneItem extends PaneItem { - SquaredPaneItem({ - super.key, - required super.title, - required super.icon, - required super.body, - }); - - @override - Widget build( - BuildContext context, - bool selected, - VoidCallback? onPressed, { - PaneDisplayMode? displayMode, - bool showTextOnTop = true, - int? itemIndex, - bool? autofocus, - }) { - return Column( - children: [ - SizedBox.square( - dimension: 48, - child: icon - ), - title! - ], - ); - } -} diff --git a/lib/src/ui/widget/shared/profile_widget.dart b/lib/src/ui/widget/shared/profile_widget.dart deleted file mode 100644 index 77f9bd7..0000000 --- a/lib/src/ui/widget/shared/profile_widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; - -import '../../controller/game_controller.dart'; - -class ProfileWidget extends StatelessWidget { - final GameController _gameController = Get.find(); - - ProfileWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 12.0 - ), - child: GestureDetector( - child: Row( - children: [ - Container( - width: 64, - height: 64, - decoration: const BoxDecoration( - shape: BoxShape.circle - ), - child: Image.asset("assets/images/user.png") - ), - const SizedBox( - width: 12.0, - ), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Auties00", - textAlign: TextAlign.start, - style: TextStyle( - fontWeight: FontWeight.w600 - ), - ), - Text( - "alautiero@gmail.com", - textAlign: TextAlign.start, - style: TextStyle( - fontWeight: FontWeight.w100 - ), - ) - ], - ) - ], - ), - ), - ); - } -} diff --git a/lib/src/ui/widget/shared/smart_check_box.dart b/lib/src/ui/widget/shared/smart_check_box.dart deleted file mode 100644 index 1f2ba9f..0000000 --- a/lib/src/ui/widget/shared/smart_check_box.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class SmartCheckBox extends StatefulWidget { - final CheckboxController controller; - final Widget? content; - const SmartCheckBox({Key? key, required this.controller, this.content}) : super(key: key); - - @override - State createState() => _SmartCheckBoxState(); -} - -class _SmartCheckBoxState extends State { - @override - Widget build(BuildContext context) { - return Checkbox( - checked: widget.controller.value, - onChanged: (checked) => setState(() => widget.controller.value = checked ?? false), - content: widget.content - ); - } -} - -class CheckboxController { - bool value; - - CheckboxController({this.value = false}); -} diff --git a/lib/src/ui/widget/shared/smart_input.dart b/lib/src/ui/widget/shared/smart_input.dart deleted file mode 100644 index 1ff610d..0000000 --- a/lib/src/ui/widget/shared/smart_input.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class SmartInput extends StatelessWidget { - final String? label; - final String placeholder; - final TextEditingController controller; - final TextInputType type; - final bool enabled; - final VoidCallback? onTap; - final bool readOnly; - final AutovalidateMode validatorMode; - final String? Function(String?)? validator; - - const SmartInput( - {Key? key, - required this.placeholder, - required this.controller, - this.label, - this.onTap, - this.enabled = true, - this.readOnly = false, - this.type = TextInputType.text, - this.validatorMode = AutovalidateMode.disabled, - this.validator}) - : super(key: key); - - @override - Widget build(BuildContext context) { - if(label != null){ - return InfoLabel( - label: label!, - child: _body - ); - } - - return _body; - } - - TextFormBox get _body => TextFormBox( - enabled: enabled, - controller: controller, - keyboardType: type, - placeholder: placeholder, - onTap: onTap, - readOnly: readOnly, - autovalidateMode: validatorMode, - validator: validator - ); -} diff --git a/lib/src/util/build.dart b/lib/src/util/build.dart deleted file mode 100644 index 097238e..0000000 --- a/lib/src/util/build.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:html/parser.dart' show parse; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; -import 'package:reboot_launcher/src/model/fortnite_build.dart'; -import 'package:reboot_launcher/src/util/time.dart'; -import 'package:reboot_launcher/src/util/version.dart' as parser; - -import 'os.dart'; - -final Uri _manifestSourceUrl = Uri.parse( - "https://github.com/simplyblk/Fortnitebuilds/blob/main/README.md"); - -Future> fetchBuilds(ignored) async { - var response = await http.get(_manifestSourceUrl); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - var document = parse(response.body); - var elements = document.getElementsByTagName("table") - .map((element) => element.querySelector("tbody")) - .expand((element) => element?.getElementsByTagName("tr") ?? []) - .toList(); - var results = []; - for (var tableEntry in elements) { - var children = tableEntry.querySelectorAll("td"); - var version = parser.tryParse(children[0].text); - if (version == null) { - continue; - } - - var link = children[3].firstChild?.attributes?["href"]; - if (link == null || link == "N/A") { - continue; - } - - results.add(FortniteBuild(version: version, link: link)); - } - - return results; -} - - -Future downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function(double?, String?) onDecompress) async { - var outputDir = Directory("${destination.path}\\.build"); - outputDir.createSync(recursive: true); - try { - destination.createSync(recursive: true); - var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1); - var extension = path.extension(fileName); - var tempFile = File("${outputDir.path}\\$fileName"); - if(tempFile.existsSync()) { - tempFile.deleteSync(recursive: true); - } - - var client = http.Client(); - var request = http.Request("GET", Uri.parse(archiveUrl)); - request.headers['Connection'] = 'Keep-Alive'; - var response = await client.send(request); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - var startTime = DateTime.now().millisecondsSinceEpoch; - var length = response.contentLength!; - var received = 0; - var sink = tempFile.openWrite(); - await response.stream.map((s) { - received += s.length; - var now = DateTime.now(); - var eta = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch; - onProgress((received / length) * 100, toETA(eta)); - return s; - }).pipe(sink); - - var receiverPort = ReceivePort(); - var file = _CompressedFile(extension, tempFile.path, destination.path, receiverPort.sendPort); - Isolate.spawn<_CompressedFile>(_decompress, file); - var completer = Completer(); - receiverPort.forEach((element) { - onDecompress(element.progress, element.eta); - if(element.progress != null && element.progress >= 100){ - completer.complete(null); - } - }); - await completer.future; - delete(outputDir); - } catch(message) { - throw Exception("Cannot download build: $message"); - } -} - -// TODO: Progress report somehow -Future _decompress(_CompressedFile file) async { - try{ - file.sendPort.send(_FileUpdate(null, null)); - switch (file.extension.toLowerCase()) { - case '.zip': - var process = await Process.start( - 'tar', - ['-xf', file.tempFile, '-C', file.destination], - mode: ProcessStartMode.inheritStdio - ); - await process.exitCode; - break; - case '.rar': - var process = await Process.start( - '${assetsDirectory.path}\\builds\\winrar.exe', - ['x', file.tempFile, '*.*', file.destination], - mode: ProcessStartMode.inheritStdio - ); - await process.exitCode; - break; - default: - break; - } - file.sendPort.send(_FileUpdate(100, null)); - }catch(exception){ - rethrow; - } -} - -class _CompressedFile { - final String extension; - final String tempFile; - final String destination; - final SendPort sendPort; - - _CompressedFile(this.extension, this.tempFile, this.destination, this.sendPort); -} - -class _FileUpdate { - final double? progress; - final String? eta; - - _FileUpdate(this.progress, this.eta); -} \ No newline at end of file diff --git a/lib/src/util/checks.dart b/lib/src/util/checks.dart index 02d7259..b5afd08 100644 --- a/lib/src/util/checks.dart +++ b/lib/src/util/checks.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; +import 'package:reboot_common/common.dart'; String? checkVersion(String? text, List versions) { if (text == null || text.isEmpty) { @@ -32,7 +32,7 @@ String? checkGameFolder(text) { return "Directory doesn't exist"; } - if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { + if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { return "Invalid game path"; } diff --git a/lib/src/util/cryptography.dart b/lib/src/util/cryptography.dart new file mode 100644 index 0000000..7aa0bec --- /dev/null +++ b/lib/src/util/cryptography.dart @@ -0,0 +1,52 @@ +import 'dart:math'; +import 'dart:typed_data'; +import 'package:bcrypt/bcrypt.dart'; +import 'package:pointycastle/export.dart'; +import 'dart:convert'; + +const int _ivLength = 16; +const int _keyLength = 32; + +String hashPassword(String plaintext) => BCrypt.hashpw(plaintext, BCrypt.gensalt()); + +bool checkPassword(String password, String hashedText) => BCrypt.checkpw(password, hashedText); + +String aes256Encrypt(String plainText, String password) { + final random = Random.secure(); + final iv = Uint8List.fromList(List.generate(_ivLength, (index) => random.nextInt(256))); + final keyDerivationData = Uint8List.fromList(utf8.encode(password)); + final derive = PBKDF2KeyDerivator(HMac(SHA256Digest(), _ivLength * 8)); + var params = Pbkdf2Parameters(iv, _ivLength * 8, _keyLength); + derive.init(params); + final key = derive.process(keyDerivationData); + final cipherParams = PaddedBlockCipherParameters( + KeyParameter(key), + null, + ); + final aes = AESEngine(); + final paddingCipher = PaddedBlockCipherImpl(PKCS7Padding(), aes); + paddingCipher.init(true, cipherParams); + final plainBytes = Uint8List.fromList(utf8.encode(plainText)); + final encryptedBytes = paddingCipher.process(plainBytes); + return base64.encode([...iv, ...encryptedBytes]); +} + +String aes256Decrypt(String encryptedText, String password) { + final encryptedBytes = base64.decode(encryptedText); + final salt = encryptedBytes.sublist(0, _ivLength); + final payload = encryptedBytes.sublist(_ivLength); + final keyDerivationData = Uint8List.fromList(utf8.encode(password)); + final derive = PBKDF2KeyDerivator(HMac(SHA256Digest(), _ivLength * 8)); + var params = Pbkdf2Parameters(salt, _ivLength * 8, _keyLength); + derive.init(params); + final key = derive.process(keyDerivationData); + final cipherParams = PaddedBlockCipherParameters( + KeyParameter(key), + null, + ); + final aes = AESEngine(); + final paddingCipher = PaddedBlockCipherImpl(PKCS7Padding(), aes); + paddingCipher.init(false, cipherParams); + final decryptedBytes = paddingCipher.process(payload); + return utf8.decode(decryptedBytes); +} diff --git a/lib/src/util/injector.dart b/lib/src/util/injector.dart deleted file mode 100644 index 552e739..0000000 --- a/lib/src/util/injector.dart +++ /dev/null @@ -1,91 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import 'dart:ffi'; - -import 'package:ffi/ffi.dart'; -import 'package:win32/win32.dart'; - -final _kernel32 = DynamicLibrary.open('kernel32.dll'); -final _CreateRemoteThread = _kernel32.lookupFunction< - IntPtr Function( - IntPtr hProcess, - Pointer lpThreadAttributes, - IntPtr dwStackSize, - Pointer loadLibraryAddress, - Pointer lpParameter, - Uint32 dwCreationFlags, - Pointer lpThreadId), - int Function( - int hProcess, - Pointer lpThreadAttributes, - int dwStackSize, - Pointer loadLibraryAddress, - Pointer lpParameter, - int dwCreationFlags, - Pointer lpThreadId)>('CreateRemoteThread'); -int CreateRemoteThread( - int hProcess, - Pointer lpThreadAttributes, - int dwStackSize, - Pointer loadLibraryAddress, - Pointer lpParameter, - int dwCreationFlags, - Pointer lpThreadId) => - _CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize, - loadLibraryAddress, lpParameter, dwCreationFlags, lpThreadId); - -Future injectDll(int pid, String dll) async { - var process = OpenProcess( - 0x43A, - 0, - pid - ); - - var processAddress = GetProcAddress( - GetModuleHandle("KERNEL32".toNativeUtf16()), - "LoadLibraryA".toNativeUtf8() - ); - - if (processAddress == nullptr) { - throw Exception("Cannot get process address for pid $pid"); - } - - var dllAddress = VirtualAllocEx( - process, - nullptr, - dll.length + 1, - 0x3000, - 0x4 - ); - - var writeMemoryResult = WriteProcessMemory( - process, - dllAddress, - dll.toNativeUtf8(), - dll.length, - nullptr - ); - - if (writeMemoryResult != 1) { - throw Exception("Memory write failed"); - } - - var createThreadResult = CreateRemoteThread( - process, - nullptr, - 0, - processAddress, - dllAddress, - 0, - nullptr - ); - - if (createThreadResult == -1) { - throw Exception("Thread creation failed"); - } - - var closeResult = CloseHandle(process); - if(closeResult != 1){ - throw Exception("Cannot close handle"); - } -} diff --git a/lib/src/util/os.dart b/lib/src/util/os.dart index 6945269..f6c5517 100644 --- a/lib/src/util/os.dart +++ b/lib/src/util/os.dart @@ -1,88 +1,13 @@ -import 'dart:ffi'; import 'dart:io'; -import 'package:ffi/ffi.dart'; -import 'package:win32/win32.dart'; - - -const int appBarSize = 2; -final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); - -bool isLocalHost(String host) => host.trim() == "127.0.0.1" || host.trim().toLowerCase() == "localhost" || host.trim() == "0.0.0.0"; +final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))'); bool get isWin11 { - var result = _regex.firstMatch(Platform.operatingSystemVersion)?.group(1); + var result = _winBuildRegex.firstMatch(Platform.operatingSystemVersion)?.group(1); if(result == null){ return false; } var intBuild = int.tryParse(result); return intBuild != null && intBuild > 22000; -} - -int startBackgroundProcess(String executable, List args) { - var executablePath = TEXT('$executable ${args.map((entry) => '"$entry"').join(" ")}'); - var startupInfo = calloc(); - var processInfo = calloc(); - var success = CreateProcess( - nullptr, - executablePath, - nullptr, - nullptr, - FALSE, - CREATE_NO_WINDOW, - nullptr, - nullptr, - startupInfo, - processInfo - ); - if (success == 0) { - var error = GetLastError(); - throw Exception("Cannot start process: $error"); - } - - var pid = processInfo.ref.dwProcessId; - free(startupInfo); - free(processInfo); - return pid; -} - -Future runElevated(String executable, String args) async { - var shellInput = calloc(); - shellInput.ref.lpFile = executable.toNativeUtf16(); - shellInput.ref.lpParameters = args.toNativeUtf16(); - shellInput.ref.nShow = SW_HIDE; - shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; - shellInput.ref.lpVerb = "runas".toNativeUtf16(); - shellInput.ref.cbSize = sizeOf(); - var shellResult = ShellExecuteEx(shellInput); - return shellResult == 1; -} - -Directory get installationDirectory => - File(Platform.resolvedExecutable).parent; - -Directory get logsDirectory => - Directory("${installationDirectory.path}\\logs"); - -Directory get assetsDirectory => - Directory("${installationDirectory.path}\\data\\flutter_assets\\assets"); - -Directory get tempDirectory => - Directory(Platform.environment["Temp"]!); - -Future delete(FileSystemEntity file) async { - try { - await file.delete(recursive: true); - return true; - }catch(_){ - return Future.delayed(const Duration(seconds: 5)).then((value) async { - try { - await file.delete(recursive: true); - return true; - }catch(_){ - return false; - } - }); - } } \ No newline at end of file diff --git a/lib/src/util/patcher.dart b/lib/src/util/patcher.dart deleted file mode 100644 index 94e4d61..0000000 --- a/lib/src/util/patcher.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; - -final Uint8List _originalHeadless = Uint8List.fromList([ - 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0 -]); - -final Uint8List _patchedHeadless = Uint8List.fromList([ - 45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 -]); - -final Uint8List _originalMatchmaking = Uint8List.fromList([ - 63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61 -]); - -final Uint8List _patchedMatchmaking = Uint8List.fromList([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -]); - -Future patchHeadless(File file) async => - _patch(file, _originalHeadless, _patchedHeadless); - -Future patchMatchmaking(File file) async => - await _patch(file, _originalMatchmaking, _patchedMatchmaking); - -Future _patch(File file, Uint8List original, Uint8List patched) async { - try { - if(original.length != patched.length){ - throw Exception("Cannot mutate length of binary file"); - } - - var read = await file.readAsBytes(); - var length = await file.length(); - var offset = 0; - var counter = 0; - while(offset < length){ - if(read[offset] == original[counter]){ - counter++; - }else { - counter = 0; - } - - offset++; - if(counter == original.length){ - for(var index = 0; index < patched.length; index++){ - read[offset - counter + index] = patched[index]; - } - - await file.writeAsBytes(read, mode: FileMode.write); - return true; - } - } - - return false; - }catch(_){ - return false; - } -} \ No newline at end of file diff --git a/lib/src/util/selector.dart b/lib/src/util/picker.dart similarity index 100% rename from lib/src/util/selector.dart rename to lib/src/util/picker.dart diff --git a/lib/src/util/process.dart b/lib/src/util/process.dart deleted file mode 100644 index 59c7b93..0000000 --- a/lib/src/util/process.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:ffi'; - -import 'package:win32/win32.dart'; - -final _ntdll = DynamicLibrary.open('ntdll.dll'); - -// ignore: non_constant_identifier_names -int NtResumeProcess(int hWnd) { - final function = _ntdll.lookupFunction('NtResumeProcess'); - return function(hWnd); -} - -// ignore: non_constant_identifier_names -int NtSuspendProcess(int hWnd) { - final function = _ntdll.lookupFunction('NtSuspendProcess'); - return function(hWnd); -} - -bool suspend(int pid) { - final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); - final result = NtSuspendProcess(processHandle); - CloseHandle(processHandle); - return (result == 0) ? true : false; -} - -bool resume(int pid) { - final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); - final result = NtResumeProcess(processHandle); - CloseHandle(processHandle); - return (result == 0) ? true : false; -} diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart deleted file mode 100644 index 8c765ac..0000000 --- a/lib/src/util/reboot.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive_io.dart'; -import 'package:crypto/crypto.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; -import 'package:reboot_launcher/src/util/os.dart'; - -const String rebootDownloadUrl = - "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/main/Release.zip"; -final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll"); - -Future downloadRebootDll(String url, int? lastUpdateMs) async { - Directory? outputDir; - var now = DateTime.now(); - try { - var lastUpdate = await _getLastUpdate(lastUpdateMs); - var exists = await rebootDllFile.exists(); - if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) { - return lastUpdateMs!; - } - - var response = await http.get(Uri.parse(rebootDownloadUrl)); - outputDir = await installationDirectory.createTemp("reboot_out"); - var tempZip = File("${outputDir.path}\\reboot.zip"); - await tempZip.writeAsBytes(response.bodyBytes); - await extractFileToDisk(tempZip.path, outputDir.path); - var rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path); - if (!exists || sha1.convert(await rebootDllFile.readAsBytes()) != sha1.convert(await rebootDll.readAsBytes())) { - await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes()); - } - - return now.millisecondsSinceEpoch; - }catch(message) { - if(url == rebootDownloadUrl){ - var asset = File('${assetsDirectory.path}\\dlls\\reboot.dll'); - await rebootDllFile.writeAsBytes(asset.readAsBytesSync()); - return now.millisecondsSinceEpoch; - } - - throw Exception("Cannot download reboot.zip, invalid zip: $message"); - }finally{ - if(outputDir != null) { - delete(outputDir); - } - } -} - -Future _getLastUpdate(int? lastUpdateMs) async { - return lastUpdateMs != null - ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) - : null; -} diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart deleted file mode 100644 index 5d0e89c..0000000 --- a/lib/src/util/server.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:http/http.dart' as http; -import 'package:ini/ini.dart'; -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:shelf/shelf_io.dart'; -import 'package:shelf_proxy/shelf_proxy.dart'; - -final serverLogFile = File("${logsDirectory.path}\\server.log"); -final serverDirectory = Directory("${assetsDirectory.path}\\lawin"); -final serverExeFile = File("${serverDirectory.path}\\lawinserver-win.exe"); - -Future writeMatchmakingIp(String text) async { - var file = File("${assetsDirectory.path}\\lawin\\Config\\config.ini"); - if(!file.existsSync()){ - return; - } - - var splitIndex = text.indexOf(":"); - var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text; - var port = splitIndex != -1 ? text.substring(splitIndex + 1) : "7777"; - var config = Config.fromString(file.readAsStringSync()); - config.set("GameServer", "ip", ip); - config.set("GameServer", "port", port); - file.writeAsStringSync(config.toString()); -} - -Future startServer(bool detached) async { - var process = await Process.start( - serverExeFile.path, - [], - workingDirectory: serverDirectory.path, - mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal, - runInShell: detached - ); - if(!detached) { - serverLogFile.createSync(recursive: true); - process.outLines.forEach((element) => - serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append)); - process.errLines.forEach((element) => - serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append)); - } -} - -Future stopServer() async { - await freeLawinPort(); - await freeMatchmakerPort(); -} - -Future isLawinPortFree() async { - return http.get(Uri.parse("http://127.0.0.1:3551/unknown")) - .timeout(const Duration(milliseconds: 500)) - .then((value) => false) - .onError((error, stackTrace) => true); -} - -Future isMatchmakerPortFree() async { - return HttpServer.bind("127.0.0.1", 8080) - .then((socket) => socket.close()) - .then((_) => true) - .onError((error, _) => false); -} - -Future freeLawinPort() async { - var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_lawin.bat"); - await Process.run(releaseBat.path, []); -} - -Future freeMatchmakerPort() async { - var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_matchmaker.bat"); - await Process.run(releaseBat.path, []); -} - -Future resetWinNat() async { - var binary = File("${serverDirectory.path}\\winnat.bat"); - await runElevated(binary.path, ""); -} - -List createRebootArgs(String username, String password, bool host, String additionalArgs) { - if(password.isEmpty) { - username = username.isEmpty ? kDefaultPlayerName : username; - username = host ? "$username${Random().nextInt(1000)}" : username; - username = '$username@projectreboot.dev'; - } - password = password.isNotEmpty ? password : "Rebooted"; - var args = [ - "-epicapp=Fortnite", - "-epicenv=Prod", - "-epiclocale=en-us", - "-epicportal", - "-skippatchcheck", - "-nobe", - "-fromfl=eac", - "-fltoken=3db3ba5dcbd2e16703f3978d", - "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", - "-AUTH_LOGIN=$username", - "-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}", - "-AUTH_TYPE=epic" - ]; - - if(host){ - args.addAll([ - "-nullrhi", - "-nosplash", - "-nosound", - ]); - } - - if(additionalArgs.isNotEmpty){ - args.addAll(additionalArgs.split(" ")); - } - - return args; -} - -Future pingSelf(String port) async => ping("127.0.0.1", port); - -Future ping(String host, String port, [bool https=false]) async { - var hostName = _getHostName(host); - var declaredScheme = _getScheme(host); - try{ - var uri = Uri( - scheme: declaredScheme ?? (https ? "https" : "http"), - host: hostName, - port: int.parse(port), - path: "unknown" - ); - var client = HttpClient() - ..connectionTimeout = const Duration(seconds: 5); - var request = await client.getUrl(uri); - var response = await request.close(); - var body = utf8.decode(await response.single); - return body.contains("epicgames") || body.contains("lawinserver") ? uri : null; - }catch(_){ - return https || declaredScheme != null ? null : await ping(host, port, true); - } -} - -String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", ""); - -String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; - -Future checkServerPreconditions(String host, String port, ServerType type) async { - host = host.trim(); - if(host.isEmpty){ - return ServerResult( - type: ServerResultType.missingHostError - ); - } - - port = port.trim(); - if(port.isEmpty){ - return ServerResult( - type: ServerResultType.missingPortError - ); - } - - var portNumber = int.tryParse(port); - if(portNumber == null){ - return ServerResult( - type: ServerResultType.illegalPortError - ); - } - - if(isLocalHost(host) && portNumber == 3551 && type == ServerType.remote){ - return ServerResult( - type: ServerResultType.illegalPortError - ); - } - - if(type != ServerType.local && !(await isLawinPortFree())){ - return ServerResult( - type: ServerResultType.backendPortTakenError - ); - } - - if(type == ServerType.embedded && !(await isMatchmakerPortFree())){ - return ServerResult( - type: ServerResultType.backendPortTakenError - ); - } - - return ServerResult( - type: ServerResultType.canStart - ); -} - -Future startRemoteServer(Uri uri) async => await serve(proxyHandler(uri), "127.0.0.1", 3551); - -class ServerResult { - final int? pid; - final Object? error; - final StackTrace? stackTrace; - final ServerResultType type; - - ServerResult({this.pid, this.error, this.stackTrace, required this.type}); -} - -enum ServerResultType { - missingHostError, - missingPortError, - illegalPortError, - cannotPingServer, - backendPortTakenError, - matchmakerPortTakenError, - canStart, - alreadyStarted, - unknownError, - stopped -} \ No newline at end of file diff --git a/lib/src/util/time.dart b/lib/src/util/time.dart deleted file mode 100644 index 9c44a98..0000000 --- a/lib/src/util/time.dart +++ /dev/null @@ -1,48 +0,0 @@ -String toETA(double milliseconds){ - var roundedMilliseconds = milliseconds.toInt(); - var duration = Duration(milliseconds: roundedMilliseconds); - return "${duration.inHours.toString().padLeft(2, "0")}:" - "${duration.inMinutes.remainder(60).toString().padLeft(2, "0")}:" - "${duration.inSeconds.remainder(60).toString().padLeft(2, "0")}"; -} - -extension DateTimeIso on DateTime { - String toIsoString() { - String y = (year >= -9999 && year <= 9999) ? _fourDigits(year) : _sixDigits(year); - String m = _twoDigits(month); - String d = _twoDigits(day); - String h = _twoDigits(hour); - String min = _twoDigits(minute); - String sec = _twoDigits(second); - String ms = _threeDigits(millisecond); - return "$y-$m-${d}T$h:$min:$sec.${ms}Z"; - } - - static String _fourDigits(int n) { - int absN = n.abs(); - String sign = n < 0 ? "-" : ""; - if (absN >= 1000) return "$n"; - if (absN >= 100) return "${sign}0$absN"; - if (absN >= 10) return "${sign}00$absN"; - return "${sign}000$absN"; - } - - static String _sixDigits(int n) { - assert(n < -9999 || n > 9999); - int absN = n.abs(); - String sign = n < 0 ? "-" : "+"; - if (absN >= 100000) return "$sign$absN"; - return "${sign}0$absN"; - } - - static String _threeDigits(int n) { - if (n >= 100) return "$n"; - if (n >= 10) return "0$n"; - return "00$n"; - } - - static String _twoDigits(int n) { - if (n >= 10) return "$n"; - return "0$n"; - } -} \ No newline at end of file diff --git a/lib/src/util/version.dart b/lib/src/util/version.dart deleted file mode 100644 index 3960d72..0000000 --- a/lib/src/util/version.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:version/version.dart'; - -Version? tryParse(String version) { - try { - return Version.parse(version); - } on FormatException catch (_) { - return null; - } -} diff --git a/lib/src/util/watch.dart b/lib/src/util/watch.dart new file mode 100644 index 0000000..3855c29 --- /dev/null +++ b/lib/src/util/watch.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +final SupabaseClient _supabase = Supabase.instance.client; +final GameController _gameController = Get.find(); +final HostingController _hostingController = Get.find(); + +extension GameInstanceWatcher on GameInstance { + Future startObserver() async { + if(watchPid != null) { + Process.killPid(watchPid!, ProcessSignal.sigabrt); + } + + watchProcess(gamePid).then((value) async { + if(hosting) { + _onHostingStopped(); + } + + _onGameStopped(); + }); + + watchPid = startBackgroundProcess( + '${assetsDirectory.path}\\misc\\watch.exe', + [_gameController.uuid, gamePid.toString(), launcherPid?.toString() ?? "-1", eacPid?.toString() ?? "-1", hosting.toString()] + ); + } + + void _onGameStopped() { + _gameController.started.value = false; + _gameController.instance.value?.kill(); + if(linkedHosting) { + _onHostingStopped(); + } + } + + Future _onHostingStopped() async { + _hostingController.started.value = false; + _hostingController.instance.value?.kill(); + await _supabase.from('hosts') + .delete() + .match({'id': _gameController.uuid}); + } +} \ No newline at end of file diff --git a/lib/src/ui/widget/shared/file_selector.dart b/lib/src/widget/common/file_selector.dart similarity index 92% rename from lib/src/ui/widget/shared/file_selector.dart rename to lib/src/widget/common/file_selector.dart index 2b3b601..e89c581 100644 --- a/lib/src/ui/widget/shared/file_selector.dart +++ b/lib/src/widget/common/file_selector.dart @@ -1,7 +1,7 @@ -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:flutter/foundation.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; -import 'package:reboot_launcher/src/util/selector.dart'; +import 'package:reboot_launcher/src/dialog/message.dart'; +import 'package:reboot_launcher/src/util/picker.dart'; class FileSelector extends StatefulWidget { final String placeholder; diff --git a/lib/src/widget/common/setting_tile.dart b/lib/src/widget/common/setting_tile.dart new file mode 100644 index 0000000..f014387 --- /dev/null +++ b/lib/src/widget/common/setting_tile.dart @@ -0,0 +1,133 @@ +import 'package:auto_animated_list/auto_animated_list.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:skeletons/skeletons.dart'; + +class SettingTile extends StatefulWidget { + static const double kDefaultContentWidth = 200.0; + static const double kDefaultSpacing = 8.0; + static const double kDefaultHeaderHeight = 72; + + final String? title; + final TextStyle? titleStyle; + final String? subtitle; + final TextStyle? subtitleStyle; + final Widget? content; + final double? contentWidth; + final List? expandedContent; + final double expandedContentHeaderHeight; + final double expandedContentSpacing; + final bool isChild; + + const SettingTile( + {Key? key, + this.title, + this.titleStyle, + this.subtitle, + this.subtitleStyle, + this.content, + this.contentWidth = kDefaultContentWidth, + this.expandedContentHeaderHeight = kDefaultHeaderHeight, + this.expandedContentSpacing = kDefaultSpacing, + this.expandedContent, + this.isChild = false}) + : assert( + (title == null && subtitle == null) || + (title != null && subtitle != null), + "Title and subtitle can only be null together"), + super(key: key); + + @override + State createState() => _SettingTileState(); +} + +class _SettingTileState extends State { + @override + Widget build(BuildContext context) { + if (widget.expandedContent == null || widget.expandedContent?.isEmpty == true) { + return _contentCard; + } + + return Expander( + initiallyExpanded: true, + headerShape: (open) => const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)), + ), + header: SizedBox( + height: widget.expandedContentHeaderHeight, + child: _buildTile(false) + ), + trailing: _trailing, + content: _expandedContent + ); + } + + Widget get _expandedContent { + var expandedContents = widget.expandedContent!; + var separatedContents = List.generate(expandedContents.length * 2, (index) => index % 2 == 0 ? expandedContents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing)); + return AutoAnimatedList( + scrollDirection: Axis.vertical, + shrinkWrap: true, + items: separatedContents, + itemBuilder: (context, child, index, animation) => FadeTransition( + opacity: animation, + child: child + ) + ); + } + + Widget get _trailing => + SizedBox(width: widget.contentWidth, child: widget.content); + + Widget get _contentCard { + if (widget.isChild) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: _buildTile(true) + ); + } + + return Card( + borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)), + child: _buildTile(true) + ); + } + + Widget _buildTile(bool trailing) { + return ListTile( + title: widget.title == null ? _skeletonTitle : _title, + subtitle: widget.title == null ? _skeletonSubtitle : _subtitle, + trailing: trailing ? _trailing : null + ); + } + + Widget get _title => Text( + widget.title!, + style: + widget.titleStyle ?? FluentTheme.of(context).typography.subtitle, + ); + + Widget get _skeletonTitle => const SkeletonLine( + style: SkeletonLineStyle( + padding: EdgeInsets.only( + right: 24.0 + ), + height: 18 + ), + ); + + Widget get _subtitle => Text( + widget.subtitle!, + style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body + ); + + Widget get _skeletonSubtitle => const SkeletonLine( + style: SkeletonLineStyle( + padding: EdgeInsets.only( + top: 8.0, + bottom: 8.0, + right: 24.0 + ), + height: 13 + ) + ); +} diff --git a/lib/src/ui/widget/home/launch_button.dart b/lib/src/widget/game/start_button.dart similarity index 62% rename from lib/src/ui/widget/home/launch_button.dart rename to lib/src/widget/game/start_button.dart index cb60ebc..91647bc 100644 --- a/lib/src/ui/widget/home/launch_button.dart +++ b/lib/src/widget/game/start_button.dart @@ -1,62 +1,42 @@ import 'dart:async'; import 'dart:io'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; +import 'package:dart_ipify/dart_ipify.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:get/get.dart'; import 'package:path/path.dart' as path; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/../main.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/model/game_instance.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; -import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog.dart'; -import 'package:reboot_launcher/src/ui/dialog/game_dialogs.dart'; -import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; -import 'package:reboot_launcher/src/util/injector.dart'; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/server.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/interactive/game.dart'; +import 'package:reboot_launcher/src/interactive/server.dart'; +import 'package:reboot_launcher/src/dialog/message.dart'; +import 'package:reboot_launcher/src/util/cryptography.dart'; +import 'package:reboot_launcher/src/util/watch.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:reboot_launcher/src/util/process.dart'; - class LaunchButton extends StatefulWidget { final bool host; final String? startLabel; final String? stopLabel; - final bool Function()? check; + final bool Function()? onTap; - const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel, this.check}) : super(key: key); + const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel, this.onTap}) : super(key: key); @override State createState() => _LaunchButtonState(); } class _LaunchButtonState extends State { - final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()"; - final List _corruptedBuildErrors = [ - "when 0 bytes remain", - "Pak chunk signature verification failed!" - ]; - final List _errorStrings = [ - "port 3551 failed: Connection refused", - "Unable to login to Fortnite servers", - "HTTP 400 response from ", - "Network failure when attempting to check platform restrictions", - "UOnlineAccountCommon::ForceLogout" - ]; - - final GlobalKey _headlessServerKey = GlobalKey(); final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); - final ServerController _serverController = Get.find(); + final AuthenticatorController _authenticatorController = Get.find(); final SettingsController _settingsController = Get.find(); - final File _logFile = File("${assetsDirectory.path}\\logs\\game.log"); + final File _logFile = File("${logsDirectory.path}\\game.log"); + Completer _completer = Completer(); bool _fail = false; Future? _executor; @@ -87,7 +67,7 @@ class _LaunchButtonState extends State { String get _stopMessage => widget.stopLabel ?? (widget.host ? "Stop hosting" : "Close fortnite"); Future _start() async { - if(widget.check != null && !widget.check!()){ + if(widget.onTap != null && !widget.onTap!()){ return; } @@ -96,23 +76,13 @@ class _LaunchButtonState extends State { return; } - _setStarted(widget.host, true); - if (_gameController.username.text.isEmpty) { - if(_serverController.type() != ServerType.local){ - showMessage("Missing username"); - _onStop(widget.host); - return; - } - - showMessage("No username: expecting self sign in"); - } - - if (_gameController.selectedVersion == null) { - showMessage("No version is selected"); + if(_gameController.selectedVersion == null){ + showMessage("Select a Fortnite version before continuing"); _onStop(widget.host); return; } + _setStarted(widget.host, true); for (var element in Injectable.values) { if(await _getDllPath(element, widget.host) == null) { return; @@ -128,7 +98,7 @@ class _LaunchButtonState extends State { return; } - var result = _serverController.started() || await _serverController.toggle(true); + var result = _authenticatorController.started() || await _authenticatorController.toggleInteractive(false); if(!result){ _onStop(widget.host); return; @@ -136,7 +106,6 @@ class _LaunchButtonState extends State { var automaticallyStartedServer = await _startMatchMakingServer(); await _startGameProcesses(version, widget.host, automaticallyStartedServer); - if(widget.host){ await _showServerLaunchingWarning(); } @@ -147,7 +116,7 @@ class _LaunchButtonState extends State { } } - Future _startGameProcesses(FortniteVersion version, bool host, bool hasChildServer) async { + Future _startGameProcesses(FortniteVersion version, bool host, bool linkedHosting) async { _setStarted(host, true); var launcherProcess = await _createLauncherProcess(version); var eacProcess = await _createEacProcess(version); @@ -159,29 +128,23 @@ class _LaunchButtonState extends State { } var gameProcess = await _createGameProcess(executable.path, host); - var watchDogProcess = _createWatchdogProcess(gameProcess, launcherProcess, eacProcess); - var instance = GameInstance(gameProcess, launcherProcess, eacProcess, watchDogProcess, hasChildServer); + var instance = GameInstance(gameProcess, launcherProcess, eacProcess, host, linkedHosting); + instance.startObserver(); if(host){ - _hostingController.instance = instance; + _hostingController.instance.value = instance; }else{ - _gameController.instance = instance; + _gameController.instance.value = instance; } _injectOrShowError(Injectable.sslBypass, host); } - int _createWatchdogProcess(Process? gameProcess, Process? launcherProcess, Process? eacProcess) => startBackgroundProcess( - '${assetsDirectory.path}\\browse\\watch.exe', - [_gameController.uuid, _getProcessPid(gameProcess), _getProcessPid(launcherProcess), _getProcessPid(eacProcess)] - ); - - String _getProcessPid(Process? process) => process?.pid.toString() ?? "-1"; - Future _startMatchMakingServer() async { if(widget.host){ return false; } - var matchmakingIp = _settingsController.matchmakingIp.text; + // var matchmakingIp = _settingsController.matchmakingIp.text; + var matchmakingIp = "127.0.0.1"; if(!isLocalHost(matchmakingIp)) { return false; } @@ -199,14 +162,14 @@ class _LaunchButtonState extends State { return true; } - Future _createGameProcess(String gamePath, bool host) async { + Future _createGameProcess(String gamePath, bool host) async { var gameArgs = createRebootArgs(_safeUsername, _gameController.password.text, host, _gameController.customLaunchArgs.text); var gameProcess = await Process.start(gamePath, gameArgs); gameProcess ..exitCode.then((_) => _onEnd()) ..outLines.forEach((line) => _onGameOutput(line, host)) ..errLines.forEach((line) => _onGameOutput(line, host)); - return gameProcess; + return gameProcess.pid; } String get _safeUsername { @@ -227,26 +190,28 @@ class _LaunchButtonState extends State { return username; } - Future _createLauncherProcess(FortniteVersion version) async { + Future _createLauncherProcess(FortniteVersion version) async { var launcherFile = version.launcher; if (launcherFile == null) { return null; } var launcherProcess = await Process.start(launcherFile.path, []); - suspend(launcherProcess.pid); - return launcherProcess; + var pid = launcherProcess.pid; + suspend(pid); + return pid; } - Future _createEacProcess(FortniteVersion version) async { + Future _createEacProcess(FortniteVersion version) async { var eacFile = version.eacExecutable; if (eacFile == null) { return null; } var eacProcess = await Process.start(eacFile.path, []); - suspend(eacProcess.pid); - return eacProcess; + var pid = eacProcess.pid; + suspend(pid); + return pid; } void _onEnd() { @@ -259,29 +224,22 @@ class _LaunchButtonState extends State { } void _closeLaunchingWidget(bool success) { - var context = _headlessServerKey.currentContext; - if(context == null || !context.mounted){ - return; + showMessage( + success ? "The headless server was started successfully" : "An error occurred while starting the headless server", + severity: success ? InfoBarSeverity.success : InfoBarSeverity.error + ); + if(!_completer.isCompleted) { + _completer.complete(success); } - - var route = ModalRoute.of(appKey.currentContext!); - if(route == null || route.isCurrent){ - return; - } - - Navigator.of(context).pop(success); } Future _showServerLaunchingWarning() async { - var result = await showDialog( - context: appKey.currentContext!, - builder: (context) => ProgressDialog( - key: _headlessServerKey, - text: "Launching headless server...", - onStop: () => Navigator.of(context).pop(false) - ) - ) ?? false; - + showMessage( + "Launching headless server...", + loading: true, + duration: null + ); + var result = await _completer.future; if(!result){ _onStop(true); return; @@ -291,24 +249,36 @@ class _LaunchButtonState extends State { return; } + var password = _hostingController.password.text; + var hasPassword = password.isNotEmpty; + var ip = await Ipify.ipv4(); + if(hasPassword) { + ip = aes256Encrypt(ip, password); + } + var supabase = Supabase.instance.client; await supabase.from('hosts').insert({ 'id': _gameController.uuid, 'name': _hostingController.name.text, 'description': _hostingController.description.text, - 'version': _gameController.selectedVersion?.name ?? 'unknown' + 'author': _gameController.username.text, + 'ip': ip, + 'version': _gameController.selectedVersion?.name, + 'password': hasPassword ? hashPassword(password) : null, + 'timestamp': DateTime.now().toIso8601String(), + 'discoverable': _hostingController.discoverable.value }); } void _onGameOutput(String line, bool host) { _logFile.createSync(recursive: true); _logFile.writeAsString("$line\n", mode: FileMode.append); - if (line.contains(_shutdownLine)) { + if (line.contains(shutdownLine)) { _onStop(host); return; } - if(_corruptedBuildErrors.any((element) => line.contains(element))){ + if(corruptedBuildErrors.any((element) => line.contains(element))){ if(_fail){ return; } @@ -319,7 +289,7 @@ class _LaunchButtonState extends State { return; } - if(_errorStrings.any((element) => line.contains(element))){ + if(cannotConnectErrors.any((element) => line.contains(element))){ if(_fail){ return; } @@ -339,27 +309,20 @@ class _LaunchButtonState extends State { } _injectOrShowError(Injectable.memoryFix, host); - var instance = host ? _hostingController.instance : _gameController.instance; + var instance = host ? _hostingController.instance.value : _gameController.instance.value; instance?.tokenError = false; } } Future _showTokenError(bool host) async { - var instance = host ? _hostingController.instance : _gameController.instance; - if(_serverController.type() != ServerType.embedded) { + var instance = host ? _hostingController.instance.value : _gameController.instance.value; + if(_authenticatorController.type() != ServerType.embedded) { showTokenErrorUnfixable(); instance?.tokenError = true; return; } - var tokenError = instance?.tokenError; - instance?.tokenError = true; - await _serverController.restart(true); - if (tokenError == true) { - showTokenErrorCouldNotFix(); - return; - } - + await _authenticatorController.restartInteractive(); showTokenErrorFixable(); _onStop(host); _start(); @@ -370,17 +333,17 @@ class _LaunchButtonState extends State { await _executor; } - var instance = host ? _hostingController.instance : _gameController.instance; + var instance = host ? _hostingController.instance.value : _gameController.instance.value; if(instance != null){ - if(instance.hasChildServer){ + if(instance.linkedHosting){ _onStop(true); } instance.kill(); if(host){ - _hostingController.instance = null; + _hostingController.instance.value = null; }else { - _gameController.instance = null; + _gameController.instance.value = null; } } @@ -392,22 +355,24 @@ class _LaunchButtonState extends State { .delete() .match({'id': _gameController.uuid}); } + + _completer = Completer(); } Future _injectOrShowError(Injectable injectable, bool hosting) async { - var instance = hosting ? _hostingController.instance : _gameController.instance; + var instance = hosting ? _hostingController.instance.value : _gameController.instance.value; if (instance == null) { return; } try { - var gameProcess = instance.gameProcess; + var gameProcess = instance.gamePid; var dllPath = await _getDllPath(injectable, hosting); if(dllPath == null) { return; } - await injectDll(gameProcess.pid, dllPath.path); + await injectDll(gameProcess, dllPath.path); } catch (exception) { showMessage("Cannot inject $injectable.dll: $exception"); _onStop(hosting); diff --git a/lib/src/widget/home/pane.dart b/lib/src/widget/home/pane.dart new file mode 100644 index 0000000..21f229a --- /dev/null +++ b/lib/src/widget/home/pane.dart @@ -0,0 +1,338 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; + +class RebootPaneItem extends PaneItem { + RebootPaneItem({required super.title, required super.icon, required super.body}); + + @override + Widget build( + BuildContext context, + bool selected, + VoidCallback? onPressed, { + PaneDisplayMode? displayMode, + bool showTextOnTop = true, + int? itemIndex, + bool? autofocus, + }) { + final maybeBody = _InheritedNavigationView.maybeOf(context); + final mode = displayMode ?? + maybeBody?.displayMode ?? + maybeBody?.pane?.displayMode ?? + PaneDisplayMode.minimal; + assert(mode != PaneDisplayMode.auto); + assert(debugCheckHasFluentTheme(context)); + + final isTransitioning = maybeBody?.isTransitioning ?? false; + + final theme = NavigationPaneTheme.of(context); + final titleText = title?.getProperty() ?? ''; + + final baseStyle = title?.getProperty() ?? const TextStyle(); + + final isTop = mode == PaneDisplayMode.top; + final isMinimal = mode == PaneDisplayMode.minimal; + final isCompact = mode == PaneDisplayMode.compact; + + final onItemTapped = + (onPressed == null && onTap == null) || !enabled || isTransitioning + ? null + : () { + onPressed?.call(); + onTap?.call(); + }; + + final button = HoverButton( + autofocus: autofocus ?? this.autofocus, + focusNode: focusNode, + onPressed: onItemTapped, + cursor: mouseCursor, + focusEnabled: isMinimal ? (maybeBody?.minimalPaneOpen ?? false) : true, + forceEnabled: enabled, + builder: (context, states) { + var textStyle = () { + var style = !isTop + ? (selected + ? theme.selectedTextStyle?.resolve(states) + : theme.unselectedTextStyle?.resolve(states)) + : (selected + ? theme.selectedTopTextStyle?.resolve(states) + : theme.unselectedTopTextStyle?.resolve(states)); + if (style == null) return baseStyle; + return style.merge(baseStyle); + }(); + + final textResult = titleText.isNotEmpty + ? Padding( + padding: theme.labelPadding ?? EdgeInsets.zero, + child: RichText( + text: title!.getProperty(textStyle)!, + maxLines: 1, + overflow: TextOverflow.fade, + softWrap: false, + textAlign: title?.getProperty() ?? TextAlign.start, + textHeightBehavior: title?.getProperty(), + textWidthBasis: title?.getProperty() ?? + TextWidthBasis.parent, + ), + ) + : const SizedBox.shrink(); + Widget result() { + final iconThemeData = IconThemeData( + color: textStyle.color ?? + (selected + ? theme.selectedIconColor?.resolve(states) + : theme.unselectedIconColor?.resolve(states)), + size: textStyle.fontSize ?? 16.0, + ); + switch (mode) { + case PaneDisplayMode.compact: + return Container( + key: itemKey, + constraints: const BoxConstraints( + minHeight: kPaneItemMinHeight, + ), + alignment: AlignmentDirectional.center, + child: Padding( + padding: theme.iconPadding ?? EdgeInsets.zero, + child: IconTheme.merge( + data: iconThemeData, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: () { + if (infoBadge != null) { + return Stack( + alignment: AlignmentDirectional.center, + clipBehavior: Clip.none, + children: [ + icon, + PositionedDirectional( + end: -8, + top: -8, + child: infoBadge!, + ), + ], + ); + } + return icon; + }(), + ), + ), + ), + ); + case PaneDisplayMode.minimal: + case PaneDisplayMode.open: + final shouldShowTrailing = !isTransitioning; + + return ConstrainedBox( + key: itemKey, + constraints: const BoxConstraints( + minHeight: kPaneItemMinHeight, + ), + child: Row(children: [ + Padding( + padding: theme.iconPadding ?? EdgeInsets.zero, + child: IconTheme.merge( + data: iconThemeData, + child: Center(child: icon), + ), + ), + Expanded(child: textResult), + if (shouldShowTrailing) ...[ + if (infoBadge != null) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: infoBadge!, + ), + if (trailing != null) + IconTheme.merge( + data: const IconThemeData(size: 16.0), + child: trailing!, + ), + ], + ]), + ); + case PaneDisplayMode.top: + Widget result = Row(mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: theme.iconPadding ?? EdgeInsets.zero, + child: IconTheme.merge( + data: iconThemeData, + child: Center(child: icon), + ), + ), + if (showTextOnTop) textResult, + if (trailing != null) + IconTheme.merge( + data: const IconThemeData(size: 16.0), + child: trailing!, + ), + ]); + if (infoBadge != null) { + return Stack(key: itemKey, clipBehavior: Clip.none, children: [ + result, + if (infoBadge != null) + PositionedDirectional( + end: -3, + top: 3, + child: infoBadge!, + ), + ]); + } + return KeyedSubtree(key: itemKey, child: result); + default: + throw '$mode is not a supported type'; + } + } + + return Semantics( + label: titleText.isEmpty ? null : titleText, + selected: selected, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 6.0), + decoration: BoxDecoration( + color: () { + final tileColor = this.tileColor ?? + theme.tileColor ?? + kDefaultPaneItemColor(context, isTop); + final newStates = states.toSet()..remove(ButtonStates.disabled); + if (selected && selectedTileColor != null) { + return selectedTileColor!.resolve(newStates); + } + return tileColor.resolve( + selected + ? { + states.isHovering + ? ButtonStates.pressing + : ButtonStates.hovering, + } + : newStates, + ); + }(), + borderRadius: BorderRadius.circular(4.0), + ), + child: FocusBorder( + focused: states.isFocused, + renderOutside: false, + child: () { + final showTooltip = ((isTop && !showTextOnTop) || isCompact) && + titleText.isNotEmpty && + !states.isDisabled; + + if (showTooltip) { + return Tooltip( + richMessage: title?.getProperty(), + style: TooltipThemeData(textStyle: baseStyle), + child: result(), + ); + } + + return result(); + }(), + ), + ), + ); + }, + ); + + final index = () { + if (itemIndex != null) return itemIndex; + if (maybeBody?.pane?.indicator != null) { + return maybeBody!.pane!.effectiveIndexOf(this); + } + }(); + + return Padding( + key: key, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12.0, vertical: 2.0), + child: () { + if (maybeBody?.pane?.indicator != null && + index != null && + !index.isNegative) { + final key = PaneItemKeys.of(index, context); + + return Stack(children: [ + button, + Positioned.fill( + child: _InheritedNavigationView.merge( + currentItemIndex: index, + currentItemSelected: selected, + child: KeyedSubtree( + key: key, + child: maybeBody!.pane!.indicator!, + ), + ), + ), + ]); + } + + return button; + }(), + ); + } +} + +class _InheritedNavigationView extends InheritedWidget { + const _InheritedNavigationView({ + super.key, + required super.child, + required this.displayMode, + this.minimalPaneOpen = false, + this.pane, + this.previousItemIndex = 0, + this.currentItemIndex = -1, + this.isTransitioning = false, + }); + + final PaneDisplayMode displayMode; + + final bool minimalPaneOpen; + + final NavigationPane? pane; + + final int previousItemIndex; + + final int currentItemIndex; + + final bool isTransitioning; + + static _InheritedNavigationView? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType<_InheritedNavigationView>(); + } + + static Widget merge({ + Key? key, + required Widget child, + int? currentItemIndex, + NavigationPane? pane, + PaneDisplayMode? displayMode, + bool? minimalPaneOpen, + int? previousItemIndex, + bool? currentItemSelected, + bool? isTransitioning, + }) { + return Builder(builder: (context) { + final current = _InheritedNavigationView.maybeOf(context); + return _InheritedNavigationView( + key: key, + displayMode: + displayMode ?? current?.displayMode ?? PaneDisplayMode.open, + minimalPaneOpen: minimalPaneOpen ?? current?.minimalPaneOpen ?? false, + currentItemIndex: currentItemIndex ?? current?.currentItemIndex ?? -1, + pane: pane ?? current?.pane, + previousItemIndex: previousItemIndex ?? current?.previousItemIndex ?? 0, + isTransitioning: isTransitioning ?? current?.isTransitioning ?? false, + child: child, + ); + }); + } + + @override + bool updateShouldNotify(covariant _InheritedNavigationView oldWidget) { + return oldWidget.displayMode != displayMode || + oldWidget.minimalPaneOpen != minimalPaneOpen || + oldWidget.pane != pane || + oldWidget.previousItemIndex != previousItemIndex || + oldWidget.currentItemIndex != currentItemIndex || + oldWidget.isTransitioning != isTransitioning; + } +} diff --git a/lib/src/widget/home/profile.dart b/lib/src/widget/home/profile.dart new file mode 100644 index 0000000..a7a7031 --- /dev/null +++ b/lib/src/widget/home/profile.dart @@ -0,0 +1,101 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; + +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/interactive/profile.dart'; + +class ProfileWidget extends StatefulWidget { + const ProfileWidget({Key? key}) : super(key: key); + + @override + State createState() => _ProfileWidgetState(); +} + +class _ProfileWidgetState extends State { + final GameController _gameController = Get.find(); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 12.0 + ), + child: Button( + style: ButtonStyle( + padding: ButtonState.all(EdgeInsets.zero), + backgroundColor: ButtonState.all(Colors.transparent), + border: ButtonState.all(const BorderSide(color: Colors.transparent)) + ), + onPressed: () async { + if(await showProfileForm(context)) { + setState(() {}); + } + }, + child: Row( + children: [ + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + shape: BoxShape.circle + ), + child: Image.asset("assets/images/user.png") + ), + const SizedBox( + width: 12.0, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _username, + textAlign: TextAlign.start, + style: const TextStyle( + fontWeight: FontWeight.w600 + ), + maxLines: 1 + ), + Text( + _email, + textAlign: TextAlign.start, + style: const TextStyle( + fontWeight: FontWeight.w100 + ), + maxLines: 1 + ) + ], + ) + ], + ), + ), + ); + + String get _username { + var username = _gameController.username.text; + if(username.isEmpty) { + return kDefaultPlayerName; + } + + var atIndex = username.indexOf("@"); + if(atIndex == -1) { + return username.substring(0, 1).toUpperCase() + username.substring(1); + } + + var result = username.substring(0, atIndex); + return result.substring(0, 1).toUpperCase() + result.substring(1); + } + + String get _email { + var username = _gameController.username.text; + if(username.isEmpty) { + return "$kDefaultPlayerName@projectreboot.dev"; + } + + if(username.contains("@")) { + return username.toLowerCase(); + } + + return "$username@projectreboot.dev".toLowerCase(); + } +} diff --git a/lib/src/ui/widget/os/window_border.dart b/lib/src/widget/os/border.dart similarity index 86% rename from lib/src/ui/widget/os/window_border.dart rename to lib/src/widget/os/border.dart index ae90b8b..98ab9ac 100644 --- a/lib/src/ui/widget/os/window_border.dart +++ b/lib/src/widget/os/border.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:reboot_launcher/src/util/os.dart'; +import 'package:reboot_common/common.dart'; import 'package:system_theme/system_theme.dart'; class WindowBorder extends StatelessWidget { @@ -17,7 +17,7 @@ class WindowBorder extends StatelessWidget { borderRadius: BorderRadius.circular(10), border: Border.all( color: SystemTheme.accentColor.accent, - width: appBarSize.toDouble() + width: appBarWidth.toDouble() ) ) ), diff --git a/lib/src/ui/widget/os/window_button.dart b/lib/src/widget/os/buttons.dart similarity index 99% rename from lib/src/ui/widget/os/window_button.dart rename to lib/src/widget/os/buttons.dart index cfa2b12..98437eb 100644 --- a/lib/src/ui/widget/os/window_button.dart +++ b/lib/src/widget/os/buttons.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; import 'icons.dart'; -import 'mouse_state_builder.dart'; +import 'mouse.dart'; typedef WindowButtonIconBuilder = Widget Function( WindowButtonContext buttonContext); diff --git a/lib/src/ui/widget/os/icons.dart b/lib/src/widget/os/icons.dart similarity index 80% rename from lib/src/ui/widget/os/icons.dart rename to lib/src/widget/os/icons.dart index 68ece22..a2ec1fd 100644 --- a/lib/src/ui/widget/os/icons.dart +++ b/lib/src/widget/os/icons.dart @@ -2,9 +2,6 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; -// Switched to CustomPaint icons by https://github.com/esDotDev - -/// Close class CloseIcon extends StatelessWidget { final Color color; @@ -12,22 +9,20 @@ class CloseIcon extends StatelessWidget { @override Widget build(BuildContext context) => Align( - alignment: Alignment.topLeft, - child: Stack(children: [ - // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason. - Transform.rotate( - angle: pi * .25, - child: - Center(child: Container(width: 14, height: 1, color: color))), - Transform.rotate( - angle: pi * -.25, - child: - Center(child: Container(width: 14, height: 1, color: color))), - ]), - ); + alignment: Alignment.topLeft, + child: Stack(children: [ + Transform.rotate( + angle: pi * .25, + child: + Center(child: Container(width: 14, height: 1, color: color))), + Transform.rotate( + angle: pi * -.25, + child: + Center(child: Container(width: 14, height: 1, color: color))), + ]), + ); } -/// Maximize class MaximizeIcon extends StatelessWidget { final Color color; @@ -47,7 +42,6 @@ class _MaximizePainter extends _IconPainter { } } -/// Restore class RestoreIcon extends StatelessWidget { final Color color; @@ -76,7 +70,6 @@ class _RestorePainter extends _IconPainter { } } -/// Minimize class MinimizeIcon extends StatelessWidget { final Color color; @@ -97,7 +90,6 @@ class _MinimizePainter extends _IconPainter { } } -/// Helpers abstract class _IconPainter extends CustomPainter { _IconPainter(this.color); diff --git a/lib/src/ui/widget/os/mouse_state_builder.dart b/lib/src/widget/os/mouse.dart similarity index 96% rename from lib/src/ui/widget/os/mouse_state_builder.dart rename to lib/src/widget/os/mouse.dart index 395951e..167785e 100644 --- a/lib/src/ui/widget/os/mouse_state_builder.dart +++ b/lib/src/widget/os/mouse.dart @@ -69,6 +69,8 @@ class _MouseStateBuilderState extends State { }); }, onTapUp: (_) {}, - child: widget.builder(context, _mouseState))); + child: widget.builder(context, _mouseState) + ) + ); } } diff --git a/lib/src/ui/widget/os/window_title_bar.dart b/lib/src/widget/os/title_bar.dart similarity index 94% rename from lib/src/ui/widget/os/window_title_bar.dart rename to lib/src/widget/os/title_bar.dart index 156a455..fab2596 100644 --- a/lib/src/ui/widget/os/window_title_bar.dart +++ b/lib/src/widget/os/title_bar.dart @@ -1,6 +1,6 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/ui/widget/os/window_button.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:reboot_launcher/src/util/os.dart'; +import 'package:reboot_launcher/src/widget/os/buttons.dart'; import 'package:system_theme/system_theme.dart'; class WindowTitleBar extends StatelessWidget { diff --git a/lib/src/widget/server/start_button.dart b/lib/src/widget/server/start_button.dart new file mode 100644 index 0000000..a2d5b48 --- /dev/null +++ b/lib/src/widget/server/start_button.dart @@ -0,0 +1,49 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/interactive/server.dart'; + +class ServerButton extends StatefulWidget { + final bool authenticator; + const ServerButton({Key? key, required this.authenticator}) : super(key: key); + + @override + State createState() => _ServerButtonState(); +} + +class _ServerButtonState extends State { + late final ServerController _controller = widget.authenticator ? Get.find() : Get.find(); + + @override + Widget build(BuildContext context) => Align( + alignment: AlignmentDirectional.bottomCenter, + child: SizedBox( + width: double.infinity, + child: Obx(() => SizedBox( + height: 48, + child: Button( + child: Align( + alignment: Alignment.center, + child: Text(_buttonText), + ), + onPressed: () => _controller.toggleInteractive() + ), + )), + ), + ); + + String get _buttonText { + if(_controller.type.value == ServerType.local){ + return "Check ${_controller.controllerName}"; + } + + if(_controller.started.value){ + return "Stop ${_controller.controllerName}"; + } + + return "Start ${_controller.controllerName}"; + } +} diff --git a/lib/src/widget/server/type_selector.dart b/lib/src/widget/server/type_selector.dart new file mode 100644 index 0000000..4147d4a --- /dev/null +++ b/lib/src/widget/server/type_selector.dart @@ -0,0 +1,57 @@ +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:get/get.dart'; +import 'package:reboot_common/src/model/server_type.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; + +class ServerTypeSelector extends StatefulWidget { + final bool authenticator; + + const ServerTypeSelector({Key? key, required this.authenticator}) + : super(key: key); + + @override + State createState() => _ServerTypeSelectorState(); +} + +class _ServerTypeSelectorState extends State { + late final ServerController _controller = widget.authenticator ? Get.find() : Get.find(); + + @override + Widget build(BuildContext context) { + return Obx(() => DropDownButton( + leading: Text(_controller.type.value.label), + items: ServerType.values + .map((type) => _createItem(type)) + .toList() + )); + } + + MenuFlyoutItem _createItem(ServerType type) { + return MenuFlyoutItem( + text: Tooltip( + message: type.message, + child: Text(type.label) + ), + onPressed: () async { + await _controller.stop(); + _controller.type.value = type; + } + ); + } +} + +extension ServerTypeExtension on ServerType { + String get label { + return this == ServerType.embedded ? "Embedded" + : this == ServerType.remote ? "Remote" + : "Local"; + } + + String get message { + return this == ServerType.embedded ? "A server will be automatically started in the background" + : this == ServerType.remote ? "A reverse proxy to the remote server will be created" + : "Assumes that you are running yourself the server locally"; + } +} diff --git a/lib/src/ui/dialog/add_local_version.dart b/lib/src/widget/version/add_local_version.dart similarity index 60% rename from lib/src/ui/dialog/add_local_version.dart rename to lib/src/widget/version/add_local_version.dart index fd1a6b5..de95a15 100644 --- a/lib/src/ui/dialog/add_local_version.dart +++ b/lib/src/widget/version/add_local_version.dart @@ -1,23 +1,42 @@ import 'dart:io'; -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/widget/home/version_name_input.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart'; -import 'dialog.dart'; -import 'dialog_button.dart'; +import 'package:reboot_launcher/src/widget/common/file_selector.dart'; +import 'package:reboot_launcher/src/widget/version/version_name_input.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:path/path.dart' as path; -class AddLocalVersion extends StatelessWidget { +class AddLocalVersion extends StatefulWidget { + const AddLocalVersion({Key? key}) + : super(key: key); + + @override + State createState() => _AddLocalVersionState(); +} + +class _AddLocalVersionState extends State { final GameController _gameController = Get.find(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _gamePathController = TextEditingController(); - AddLocalVersion({Key? key}) - : super(key: key); + @override + void initState() { + _gamePathController.addListener(() async { + var file = Directory(_gamePathController.text); + if(await file.exists()) { + if(_nameController.text.isEmpty) { + _nameController.text = path.basename(_gamePathController.text); + } + } + }); + super.initState(); + } @override Widget build(BuildContext context) { @@ -47,7 +66,7 @@ class AddLocalVersion extends StatelessWidget { ), FileSelector( - label: "Path", + label: "Game folder", placeholder: "Type the game folder", windowTitle: "Select game folder", controller: _gamePathController, @@ -70,10 +89,10 @@ class AddLocalVersion extends StatelessWidget { type: ButtonType.primary, onTap: () { Navigator.of(context).pop(); - _gameController.addVersion(FortniteVersion( + WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( name: _nameController.text, location: Directory(_gamePathController.text) - )); + ))); }, ) ] diff --git a/lib/src/widget/version/add_server_version.dart b/lib/src/widget/version/add_server_version.dart new file mode 100644 index 0000000..9dbc4c4 --- /dev/null +++ b/lib/src/widget/version/add_server_version.dart @@ -0,0 +1,342 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/widget/version/version_build_selector.dart'; +import 'package:reboot_launcher/src/widget/version/version_name_input.dart'; +import 'package:universal_disk_space/universal_disk_space.dart'; + +import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/widget/common/file_selector.dart'; +import '../../dialog/dialog.dart'; +import '../../dialog/dialog_button.dart'; + +class AddServerVersion extends StatefulWidget { + const AddServerVersion({Key? key}) : super(key: key); + + @override + State createState() => _AddServerVersionState(); +} + +class _AddServerVersionState extends State { + final GameController _gameController = Get.find(); + final BuildController _buildController = Get.find(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _pathController = TextEditingController(); + final Rx _status = Rx(DownloadStatus.form); + final GlobalKey _formKey = GlobalKey(); + final RxnInt _timeLeft = RxnInt(); + final Rxn _downloadProgress = Rxn(); + + late DiskSpace _diskSpace; + late Future _fetchFuture; + late Future _diskFuture; + + SendPort? _downloadPort; + Object? _error; + StackTrace? _stackTrace; + + @override + void initState() { + _fetchFuture = _buildController.builds != null + ? Future.value(true) + : compute(fetchBuilds, null) + .then((value) => _buildController.builds = value); + _diskSpace = DiskSpace(); + _diskFuture = _diskSpace.scan() + .then((_) => _updateFormDefaults()); + super.initState(); + } + + @override + void dispose() { + _pathController.dispose(); + _nameController.dispose(); + _cancelDownload(); + super.dispose(); + } + + void _cancelDownload() { + Process.run('${assetsDirectory.path}\\misc\\stop.bat', []); + _downloadPort?.send("kill"); + } + + @override + Widget build(BuildContext context) => Form( + key: _formKey, + child: Obx(() { + switch(_status.value){ + case DownloadStatus.form: + return FutureBuilder( + future: Future.wait([_fetchFuture, _diskFuture]), + builder: (context, snapshot) { + if (snapshot.hasError) { + WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace)); + } + + if (!snapshot.hasData) { + return ProgressDialog( + text: "Fetching builds and disks...", + onStop: () => Navigator.of(context).pop() + ); + } + + return FormDialog( + content: _formBody, + buttons: _formButtons + ); + } + ); + case DownloadStatus.downloading: + return GenericDialog( + header: _downloadBody, + buttons: _stopButton + ); + case DownloadStatus.extracting: + return GenericDialog( + header: _extractingBody, + buttons: _stopButton + ); + case DownloadStatus.error: + return ErrorDialog( + exception: _error ?? Exception("unknown error"), + stackTrace: _stackTrace, + errorMessageBuilder: (exception) => "Cannot download version: $exception" + ); + case DownloadStatus.done: + return const InfoDialog( + text: "The download was completed successfully!", + ); + } + }) + ); + + List get _formButtons => [ + DialogButton(type: ButtonType.secondary), + DialogButton( + text: "Download", + type: ButtonType.primary, + onTap: () => _startDownload(context), + ) + ]; + + void _startDownload(BuildContext context) async { + try { + var build = _buildController.selectedBuild.value; + if(build == null){ + return; + } + + _status.value = DownloadStatus.downloading; + var communicationPort = ReceivePort(); + communicationPort.listen((message) { + if(message is ArchiveDownloadProgress) { + _onDownloadProgress(message.progress, message.minutesLeft, message.extracting); + }else if(message is SendPort) { + _downloadPort = message; + }else { + _onDownloadError("Unexpected message: $message", null); + } + }); + var options = ArchiveDownloadOptions( + build.link, + Directory(_pathController.text), + communicationPort.sendPort + ); + var errorPort = ReceivePort(); + errorPort.listen((message) => _onDownloadError(message, null)); + var exitPort = ReceivePort(); + exitPort.listen((message) { + if(_status.value != DownloadStatus.error) { + _onDownloadComplete(); + } + }); + await Isolate.spawn( + downloadArchiveBuild, + options, + onError: errorPort.sendPort, + onExit: exitPort.sendPort, + errorsAreFatal: true + ); + } catch (exception, stackTrace) { + _onDownloadError(exception, stackTrace); + } + } + + Future _onDownloadComplete() async { + if (!mounted) { + return; + } + + _status.value = DownloadStatus.done; + WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( + name: _nameController.text, + location: Directory(_pathController.text) + ))); + } + + void _onDownloadError(Object? error, StackTrace? stackTrace) { + if (!mounted) { + return; + } + + _status.value = DownloadStatus.error; + _error = error; + _stackTrace = stackTrace; + } + + void _onDownloadProgress(double progress, int timeLeft, bool extracting) { + if (!mounted) { + return; + } + + _status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading; + _timeLeft.value = timeLeft; + _downloadProgress.value = progress; + } + + Widget get _downloadBody { + var timeLeft = _timeLeft.value; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Downloading...", + style: FluentTheme.maybeOf(context)?.typography.body, + textAlign: TextAlign.start, + ), + ), + + const SizedBox( + height: 8.0, + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${(_downloadProgress.value ?? 0).round()}%", + style: FluentTheme.maybeOf(context)?.typography.body, + ), + + if(timeLeft != null) + Text( + "Time left: ${timeLeft == 0 ? "less than a minute" : "about $timeLeft minute${timeLeft > 1 ? 's' : ''}"}", + style: FluentTheme.maybeOf(context)?.typography.body, + ) + ], + ), + + const SizedBox( + height: 8.0, + ), + + SizedBox( + width: double.infinity, + child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble()) + ), + + const SizedBox( + height: 8.0, + ) + ], + ); + } + + Widget get _extractingBody => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Extracting...", + style: FluentTheme.maybeOf(context)?.typography.body, + textAlign: TextAlign.start, + ), + ), + + const SizedBox( + height: 8.0, + ), + + const SizedBox( + width: double.infinity, + child: ProgressBar() + ), + + const SizedBox( + height: 8.0, + ) + ], + ); + + Widget get _formBody => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BuildSelector( + onSelected: _updateFormDefaults + ), + + const SizedBox( + height: 16.0 + ), + + VersionNameInput( + controller: _nameController + ), + + const SizedBox( + height: 16.0 + ), + + FileSelector( + label: "Installation directory", + placeholder: "Type the installation directory", + windowTitle: "Select installation directory", + controller: _pathController, + validator: checkDownloadDestination, + folder: true + ), + + const SizedBox( + height: 16.0 + ) + ], + ); + + List get _stopButton => [ + DialogButton( + text: "Stop", + type: ButtonType.only + ) + ]; + + Future _updateFormDefaults() async { + if(_diskSpace.disks.isEmpty){ + return; + } + + await _fetchFuture; + var bestDisk = _diskSpace.disks + .reduce((first, second) => first.availableSpace > second.availableSpace ? first : second); + var build = _buildController.selectedBuild.value; + if(build== null){ + return; + } + + _pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}"; + _nameController.text = build.version.toString(); + _formKey.currentState?.validate(); + } +} + +enum DownloadStatus { form, downloading, extracting, error, done } diff --git a/lib/src/ui/widget/home/build_selector.dart b/lib/src/widget/version/version_build_selector.dart similarity index 79% rename from lib/src/ui/widget/home/build_selector.dart rename to lib/src/widget/version/version_build_selector.dart index 45d9f9f..abfc188 100644 --- a/lib/src/ui/widget/home/build_selector.dart +++ b/lib/src/widget/version/version_build_selector.dart @@ -1,7 +1,7 @@ -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/fortnite_build.dart'; -import 'package:reboot_launcher/src/ui/controller/build_controller.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; class BuildSelector extends StatefulWidget { final Function() onSelected; @@ -23,13 +23,13 @@ class _BuildSelectorState extends State { placeholder: const Text('Select a fortnite build'), isExpanded: true, items: _createItems(), - value: _buildController.selectedBuildRx.value, + value: _buildController.selectedBuild.value, onChanged: (value) { if(value == null){ return; } - _buildController.selectedBuildRx.value = value; + _buildController.selectedBuild.value = value; widget.onSelected(); } )) diff --git a/lib/src/ui/widget/home/version_name_input.dart b/lib/src/widget/version/version_name_input.dart similarity index 86% rename from lib/src/ui/widget/home/version_name_input.dart rename to lib/src/widget/version/version_name_input.dart index 871bca7..e958e20 100644 --- a/lib/src/ui/widget/home/version_name_input.dart +++ b/lib/src/widget/version/version_name_input.dart @@ -1,6 +1,6 @@ -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:get/get.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class VersionNameInput extends StatelessWidget { final GameController _gameController = Get.find(); diff --git a/lib/src/ui/widget/home/version_selector.dart b/lib/src/widget/version/version_selector.dart similarity index 65% rename from lib/src/ui/widget/home/version_selector.dart rename to lib/src/widget/version/version_selector.dart index cdb634e..298c7c1 100644 --- a/lib/src/ui/widget/home/version_selector.dart +++ b/lib/src/widget/version/version_selector.dart @@ -1,37 +1,31 @@ import 'dart:async'; import 'dart:io'; -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide showDialog; import 'package:flutter/gestures.dart'; import 'package:get/get.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/ui/controller/game_controller.dart'; -import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart'; -import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog.dart'; -import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/widget/version/add_local_version.dart'; +import 'package:reboot_launcher/src/widget/version/add_server_version.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/dialog/message.dart'; import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/util/os.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart'; +import 'package:reboot_launcher/src/widget/common/file_selector.dart'; class VersionSelector extends StatefulWidget { const VersionSelector({Key? key}) : super(key: key); - static void openDownloadDialog(BuildContext context) async { - await showDialog( - context: context, - builder: (dialogContext) => const AddServerVersion() - ); - } + static Future openDownloadDialog() => showDialog( + builder: (context) => const AddServerVersion(), + ); - static void openAddDialog(BuildContext context) async { - await showDialog( - context: context, - builder: (context) => AddLocalVersion()); - } + static Future openAddDialog() => showDialog( + builder: (context) => const AddLocalVersion(), + ); @override State createState() => _VersionSelectorState(); @@ -39,21 +33,21 @@ class VersionSelector extends StatefulWidget { class _VersionSelectorState extends State { final GameController _gameController = Get.find(); - final CheckboxController _deleteFilesController = CheckboxController(); + final RxBool _deleteFilesController = RxBool(false); final FlyoutController _flyoutController = FlyoutController(); @override Widget build(BuildContext context) => Obx(() => _createOptionsMenu( - version: _gameController.selectedVersion, - close: false, - child: FlyoutTarget( - controller: _flyoutController, - child: DropDownButton( - leading: Text(_gameController.selectedVersion?.name ?? "Select a version"), - items: _createSelectorItems(context) - ), - ) - )); + version: _gameController.selectedVersion, + close: false, + child: FlyoutTarget( + controller: _flyoutController, + child: DropDownButton( + leading: Text(_gameController.selectedVersion?.name ?? "Select a version"), + items: _createSelectorItems(context) + ), + ) + )); List _createSelectorItems(BuildContext context) => _gameController.hasNoVersions ? [_createDefaultVersionItem()] : _gameController.versions.value @@ -154,48 +148,42 @@ class _VersionSelectorState extends State { } bool _onExplorerError() { - showSnackbar( - context, - const Snackbar( - content: Text("This version doesn't exist on the local machine", textAlign: TextAlign.center), - extended: true - ) - ); + showMessage("This version doesn't exist on the local machine"); return false; } Future _openDeleteDialog(BuildContext context, FortniteVersion version) { return showDialog( - context: context, builder: (context) => ContentDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - width: double.infinity, - child: Text("Are you sure you want to delete this version?")), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: double.infinity, + child: Text("Are you sure you want to delete this version?")), - const SizedBox(height: 12.0), + const SizedBox(height: 12.0), - SmartCheckBox( - controller: _deleteFilesController, - content: const Text("Delete version files from disk") - ) - ], - ), - actions: [ - Button( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Keep'), - ), - FilledButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Delete'), - ) - ], + Obx(() => Checkbox( + checked: _deleteFilesController.value, + onChanged: (bool? value) => _deleteFilesController.value = value ?? false, + content: const Text("Delete version files from disk") + )) + ], + ), + actions: [ + Button( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Keep'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Delete'), ) + ], + ) ); } @@ -203,7 +191,6 @@ class _VersionSelectorState extends State { var nameController = TextEditingController(text: version.name); var pathController = TextEditingController(text: version.location.path); return showDialog( - context: context, builder: (context) => FormDialog( content: Column( mainAxisSize: MainAxisSize.min, @@ -211,13 +198,13 @@ class _VersionSelectorState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoLabel( - label: "Name", - child: TextFormBox( - controller: nameController, - placeholder: "Type the new version name", - autofocus: true, - validator: (text) => checkChangeVersion(text) - ) + label: "Name", + child: TextFormBox( + controller: nameController, + placeholder: "Type the new version name", + autofocus: true, + validator: (text) => checkChangeVersion(text) + ) ), const SizedBox( diff --git a/lib/supabase.dart b/lib/supabase.dart deleted file mode 100644 index 6e6378f..0000000 --- a/lib/supabase.dart +++ /dev/null @@ -1,2 +0,0 @@ -const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co'; -const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M'; \ No newline at end of file diff --git a/lib/watch.dart b/lib/watch.dart deleted file mode 100644 index 593c720..0000000 --- a/lib/watch.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:io'; -import 'package:reboot_launcher/supabase.dart'; -import 'package:supabase/supabase.dart'; - -void main(List args) async { - if(args.length != 4){ - stderr.writeln("Wrong args length: $args"); - return; - } - - var instance = _GameInstance(args[0], int.parse(args[1]), int.parse(args[2]), int.parse(args[3])); - var supabase = SupabaseClient(supabaseUrl, supabaseAnonKey); - while(true){ - sleep(const Duration(seconds: 2)); - stdout.writeln("Looking up tasks..."); - var result = Process.runSync('tasklist', []); - var output = result.stdout.toString(); - if(output.contains(" ${instance.gameProcess} ")) { - continue; - } - - stdout.writeln("Killing $instance"); - Process.killPid(instance.gameProcess, ProcessSignal.sigabrt); - if(instance.launcherProcess != -1){ - Process.killPid(instance.launcherProcess, ProcessSignal.sigabrt); - } - - if(instance.eacProcess != -1){ - Process.killPid(instance.eacProcess, ProcessSignal.sigabrt); - } - - await supabase.from('hosts') - .delete() - .match({'id': instance.uuid}); - exit(0); - } -} - -class _GameInstance { - final String uuid; - final int gameProcess; - final int launcherProcess; - final int eacProcess; - - _GameInstance(this.uuid, this.gameProcess, this.launcherProcess, this.eacProcess); - - @override - String toString() { - return '{uuid: $uuid, gameProcess: $gameProcess, launcherProcess: $launcherProcess, eacProcess: $eacProcess}'; - } -} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index ae24dfe..61d7bbd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,47 +1,52 @@ name: reboot_launcher -description: Launcher for project reboot -version: "8.1.0" +description: Graphical User Interface for Project Reboot +version: "1.0.0" publish_to: 'none' environment: - sdk: ">=2.17.6 <=3.3.3" + sdk: ">=2.19.0 <=3.3.3" dependencies: flutter: sdk: flutter + reboot_common: + path: ./../common fluent_ui: ^4.7.3 bitsdojo_window_windows: ^0.1.5 system_theme: ^2.0.0 - http: ^0.13.5 - html: ^0.15.0 - shared_preferences: ^2.0.15 file_picker: ^5.2.0+1 - context_menus: ^1.0.1 - process_run: ^0.12.3+2 url_launcher: ^6.1.5 archive: ^3.3.1 - version: ^3.0.2 crypto: ^3.0.2 async: ^2.8.2 get: ^4.6.5 get_storage: ^2.0.3 window_manager: ^0.2.7 - shelf_proxy: ^1.0.2 - args: ^2.3.1 win32: 3.0.0 clipboard: ^0.1.3 - sync: ^0.3.0 - ini: ^2.1.0 universal_disk_space: ^0.2.3 - jaguar: ^3.1.3 - hex: ^0.2.0 uuid: ^3.0.6 supabase_flutter: ^1.10.0 - supabase: ^1.9.1 - fluentui_system_icons: ^1.1.202 + skeletons: ^0.0.3 + bcrypt: ^1.1.3 + upnp2: ^3.0.10 + dart_ipify: ^1.1.1 + path: ^1.8.3 + pointycastle: ^3.7.3 + sync: ^0.3.0 + process_run: ^0.13.1 + auto_animated_list: ^1.0.4 flutter_acrylic: ^1.1.3 + app_links: ^3.4.3 + url_protocol: ^1.0.0 + +dependency_overrides: + xml: ^6.3.0 + http: ^0.13.5 + win32: ^3.0.0 + ffi: ^2.0.0 dev_dependencies: flutter_test: @@ -54,8 +59,7 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/builds/ - - assets/browse/ + - assets/misc/ - assets/dlls/ - assets/icons/ - assets/images/ @@ -74,5 +78,4 @@ msix_config: publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64 - store: true capabilities: "internetClient" \ No newline at end of file diff --git a/release/README.md b/release/README.md deleted file mode 100644 index a85d9af..0000000 --- a/release/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Reboot Launcher - -Developed by Auties00 - -The main page - -### Username input - -Pretty easy to use, just write the name that you want people to see in game. It will automatically change based on the state of the host switch that you can see in the same page. This is done because, as the launcher supports friends, you probably want different usernames. Anyways they will be memorized and change as I just wrote based on the host switch. - -### Version Selector - -Just use it to select a Fortnite version to launch, if you have none use the first button to the right if you already have the one you need installed on your pc(Add a local build) or the second to the right if you need to install it from the cloud(Download a build). - -### Add a local build - -It's the first button to the right of the version selector, you can use it add a Fortnite build that you already have on the launcher - -### Download a build - -It's the second button to the right of the version selector in the home, you can use it to download practically any Fortnite build that exists. -Builds marked as "Fortnite Manifest" are very fast to download and can be downloaded freely. -Builds marked as "Google Drive" are slower to download as the file has to be unarchived. Also if you download too many builds in a very short amount of time Google may rate limit you(error code 429). -Some google builds may be unavailable temporarily(status code 404). -In conclusion if you have to download a build, prefer the ones marked as "Fortnite Manifest" - -### Launch / Close - -Use this button to launch the game. Then click it again when it says close to close the game. Remember this last part because some goofy ass actually asked me why the game was crashing, plot twist: they were closing the game. - -### Host Switch - -Whether the reboot.dll should be injected for you to be able to host games. If not on, you will be instead able to play games - -## Server page - -Pretty much don't touch this except you really know what you are doing - - - -### Host - -The host of the remote server to use for the backend server. Only enabled if you are not using the embedded server. - -### Port - -The port of the remote server to use for the backend server. Only enabled if you are not using the embedded server. - -### Embedded - -A switch to determine whether an embedded backend server should be used or if you want to use a remote one - -### Check address / Start or Stop server - -If the embedded switch is off, this button checks that the remote backend actually works. If it's on, instead, it will start or stop the lawin server. If you click on the launch button in the launcher page, the server will automatically start if you are using the embedded server and if it's not already running. If the 3551 port is already in use on your pc, the launcher will tell you and provide an option to stop the associated process automatically. - -# Info - -Just a nice page to see the version of the launcher and the join discord button - -# FAQ - -1. Does the Reboot DLL auto update? - Yes, once every 24 hours -2. Can I launch multiple game instances? - Obviously, just open the launcher again. You can have as many windows as you like. -3. Where can I download the launcher? - Discord or soon the Microsoft store \ No newline at end of file diff --git a/release/release.bat b/release/release.bat deleted file mode 100644 index 16dc7ae..0000000 --- a/release/release.bat +++ /dev/null @@ -1,4 +0,0 @@ -dart compile exe ./lib/watch.dart --output ./assets/browse/watch.exe -flutter_distributor package --platform windows --targets exe -flutter pub run msix:create -dart compile exe ./lib/cli.dart --output ./dist/cli/reboot.exe diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index d39742d..5927a20 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -5,6 +5,7 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME); #include #include +#include "app_links/app_links_plugin_c_api.h" #include #include "flutter_window.h" @@ -35,11 +36,52 @@ bool CheckOneInstance(){ return true; } +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +bool SendAppLinkToInstance(const std::wstring& title) { + // Find our exact window + HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); + + if (hwnd) { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + + switch(place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { _putenv_s("OPENSSL_ia32cap", "~0x20000000"); + if(SendAppLinkToInstance(L"Reboot Launcher")) { + return EXIT_SUCCESS; + } + if(!CheckOneInstance()){ - return false; + return EXIT_SUCCESS; } // Attach to console when present (e.g., 'flutter run') or create a diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index 47e2ff0..6ca7a80 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -8,135 +8,135 @@ namespace { -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; + int g_active_window_count = 0; -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} + int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); + } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); - } -} + void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } + } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; +public: + ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); + // Returns the singleton registar instance. + static WindowClassRegistrar *GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; } - return instance_; - } - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t *GetWindowClass(); - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); - private: - WindowClassRegistrar() = default; +private: + WindowClassRegistrar() = default; - static WindowClassRegistrar* instance_; + static WindowClassRegistrar *instance_; - bool class_registered_ = false; + bool class_registered_ = false; }; -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; +WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; +const wchar_t *WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; } Win32Window::Win32Window() { - ++g_active_window_count; + ++g_active_window_count; } Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); + --g_active_window_count; + Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); +bool Win32Window::CreateAndShow(const std::wstring &title, + const Point &origin, + const Size &size) { + Destroy(); - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); + const wchar_t *window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; - HWND window = CreateWindow( - window_class, - title.c_str(), - WS_OVERLAPPED & ~WS_VISIBLE, - Scale(origin.x, scale_factor), - Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), - Scale(size.height, scale_factor), - nullptr, - nullptr, - GetModuleHandle(nullptr), - this - ); + HWND window = CreateWindow( + window_class, + title.c_str(), + WS_OVERLAPPED & ~WS_VISIBLE, + Scale(origin.x, scale_factor), + Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), + Scale(size.height, scale_factor), + nullptr, + nullptr, + GetModuleHandle(nullptr), + this + ); - if (!window) { - return false; - } + if (!window) { + return false; + } - return OnCreate(); + return OnCreate(); } // static @@ -144,19 +144,19 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window *that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } - return DefWindowProc(window, message, wparam, lparam); + return DefWindowProc(window, message, wparam, lparam); } LRESULT @@ -164,92 +164,92 @@ Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; } - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); + return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { - OnDestroy(); + OnDestroy(); - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } } -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); +Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); - SetFocus(child_content_); + SetFocus(child_content_); } RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; } HWND Win32Window::GetHandle() { - return window_handle_; + return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; + quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; + // No-op; provided for subclasses. + return true; } void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} + // No-op; provided for subclasses. +} \ No newline at end of file