From c27dbaa3064e7400a0d3af3ece6f25bea561d2d9 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Tue, 4 Oct 2022 17:28:10 +0200 Subject: [PATCH] Added headless switch Fixed UI --- lib/src/controller/game_controller.dart | 16 +-- lib/src/model/game_type.dart | 5 + lib/src/page/home_page.dart | 37 ++----- lib/src/page/info_page.dart | 2 +- lib/src/page/launcher_page.dart | 123 +++++++++++++++--------- lib/src/util/build.dart | 4 +- lib/src/util/os.dart | 2 - lib/src/util/reboot.dart | 2 +- lib/src/util/server.dart | 22 ++--- lib/src/widget/add_local_version.dart | 3 +- lib/src/widget/add_server_version.dart | 11 +-- lib/src/widget/deployment_selector.dart | 31 ------ lib/src/widget/host_checkbox.dart | 74 ++++++++++++++ lib/src/widget/launch_button.dart | 121 +++++++++++++++++------ lib/src/widget/scan_local_version.dart | 6 +- lib/src/widget/smart_check_box.dart | 27 ++++++ lib/src/widget/smart_switch.dart | 50 ++++++---- lib/src/widget/username_box.dart | 5 +- lib/src/widget/version_selector.dart | 69 ++++++++----- pubspec.yaml | 4 +- 20 files changed, 389 insertions(+), 225 deletions(-) create mode 100644 lib/src/model/game_type.dart delete mode 100644 lib/src/widget/deployment_selector.dart create mode 100644 lib/src/widget/host_checkbox.dart create mode 100644 lib/src/widget/smart_check_box.dart diff --git a/lib/src/controller/game_controller.dart b/lib/src/controller/game_controller.dart index f2d6dd9..3eb8ccb 100644 --- a/lib/src/controller/game_controller.dart +++ b/lib/src/controller/game_controller.dart @@ -5,6 +5,7 @@ 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/fortnite_version.dart'; +import 'package:reboot_launcher/src/model/game_type.dart'; class GameController extends GetxController { late final GetStorage _storage; @@ -12,8 +13,9 @@ class GameController extends GetxController { late final TextEditingController version; late final Rx> versions; late final Rxn _selectedVersion; - late final RxBool host; + late final Rx type; late final RxBool started; + Future? updater; Process? gameProcess; Process? launcherProcess; Process? eacProcess; @@ -34,15 +36,15 @@ class GameController extends GetxController { (element) => element.name == decodedSelectedVersionName); _selectedVersion = Rxn(decodedSelectedVersion); - host = RxBool(_storage.read("host") ?? false); - host.listen((value) { - _storage.write("host", value); - username.text = _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""; + type = Rx(GameType.values.elementAt(_storage.read("type") ?? 0)); + type.listen((value) { + _storage.write("type", value.index); + username.text = _storage.read("${type.value == GameType.client ? 'game' : 'host'}_username") ?? ""; }); - username = TextEditingController(text: _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""); + username = TextEditingController(text: _storage.read("${type.value == GameType.client ? 'game' : 'host'}_username") ?? ""); username.addListener(() async { - await _storage.write("${host.value ? 'host' : 'game'}_username", username.text); + await _storage.write("${type.value == GameType.client ? 'game' : 'host'}_username", username.text); }); started = RxBool(false); diff --git a/lib/src/model/game_type.dart b/lib/src/model/game_type.dart new file mode 100644 index 0000000..e559910 --- /dev/null +++ b/lib/src/model/game_type.dart @@ -0,0 +1,5 @@ +enum GameType { + client, + server, + headlessServer +} \ No newline at end of file diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart index 33e8ef7..f8d9cd5 100644 --- a/lib/src/page/home_page.dart +++ b/lib/src/page/home_page.dart @@ -1,7 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/src/page/info_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/server_page.dart'; @@ -10,8 +8,6 @@ import 'package:reboot_launcher/src/widget/window_border.dart'; import 'package:reboot_launcher/src/widget/window_buttons.dart'; import 'package:window_manager/window_manager.dart'; -import '../util/reboot.dart'; - class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -20,17 +16,12 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State with WindowListener { - late final Future _future; bool _focused = true; int _index = 0; @override void initState() { windowManager.addListener(this); - var storage = GetStorage("update"); - int? lastUpdateMs = storage.read("last_update"); - _future = compute(downloadRebootDll, lastUpdateMs); - _future.then((value) => storage.write("last_update", value)); super.initState(); } @@ -47,7 +38,7 @@ class _HomePageState extends State with WindowListener { @override void onWindowBlur() { - setState(() => _focused = false); + setState(() => _focused = !_focused); } @override @@ -66,12 +57,13 @@ class _HomePageState extends State with WindowListener { _createPane("Info", FluentIcons.info), ], trailing: WindowTitleBar(focused: _focused)), - content: FutureBuilder( - future: _future, - builder: (context, snapshot) => NavigationBody( - index: _index, - children: _createPages(snapshot) - ) + content: NavigationBody( + index: _index, + children: [ + const LauncherPage(), + ServerPage(), + const InfoPage() + ] ) ), @@ -81,19 +73,6 @@ class _HomePageState extends State with WindowListener { ); } - List _createPages(AsyncSnapshot snapshot) { - - return [ - LauncherPage( - ready: snapshot.hasData, - error: snapshot.error, - stackTrace: snapshot.stackTrace - ), - ServerPage(), - const InfoPage() - ]; - } - PaneItem _createPane(String label, IconData icon) { return PaneItem(icon: Icon(icon), title: Text(label)); } diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index edfe6e2..794b0a6 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -31,7 +31,7 @@ class InfoPage extends StatelessWidget { ), const Expanded( child: Align( - alignment: Alignment.bottomLeft, child: Text("Version 3.8${kDebugMode ? '-DEBUG' : ''}"))) + alignment: Alignment.bottomLeft, child: Text("Version 3.10${kDebugMode ? '-DEBUG' : ''}"))) ], ); } diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 59a0e7c..44a24f6 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -1,24 +1,25 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/widget/deployment_selector.dart'; +import 'package:reboot_launcher/src/widget/host_checkbox.dart'; import 'package:reboot_launcher/src/widget/launch_button.dart'; import 'package:reboot_launcher/src/widget/username_box.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../util/binary.dart'; +import '../util/reboot.dart'; import '../widget/warning_info.dart'; class LauncherPage extends StatefulWidget { - final bool ready; - final Object? error; - final StackTrace? stackTrace; - const LauncherPage( - {Key? key, required this.ready, required this.error, this.stackTrace}) + {Key? key}) : super(key: key); @override @@ -26,68 +27,94 @@ class LauncherPage extends StatefulWidget { } class _LauncherPageState extends State { + final GameController _gameController = Get.find(); final BuildController _buildController = Get.find(); - bool shouldWriteError = true; @override void initState() { - _buildController.cancelledDownload - .listen((value) => value ? _onCancelWarning() : {}); + if(_gameController.updater == null) { + _gameController.updater = compute(downloadRebootDll, _updateTime) + ..then((value) => _updateTime = value) + ..onError(_saveError); + _buildController.cancelledDownload + .listen((value) => value ? _onCancelWarning() : {}); + } + super.initState(); } + int? get _updateTime { + var storage = GetStorage("update"); + return storage.read("last_update"); + } + + set _updateTime(int? updateTime) { + var storage = GetStorage("update"); + storage.write("last_update", updateTime); + } + + Future _saveError(Object? error, StackTrace stackTrace) async { + var errorFile = await loadBinary("error.txt", true); + errorFile.writeAsString( + "Error: $error\nStacktrace: $stackTrace", mode: FileMode.write); + } + void _onCancelWarning() { WidgetsBinding.instance.addPostFrameCallback((_) { + if(!mounted) { + return; + } + showSnackbar(context, const Snackbar(content: Text("Download cancelled"))); - _buildController.cancelledDownload.value = false; + _buildController.cancelledDownload(false); }); } @override Widget build(BuildContext context) { - if (!widget.ready && widget.error == null) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - ProgressRing(), - SizedBox(height: 16.0), - Text("Updating Reboot DLL...") + return FutureBuilder( + future: _gameController.updater, + builder: (context, snapshot) { + if (!snapshot.hasData && !snapshot.hasError) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + ProgressRing(), + SizedBox(height: 16.0), + Text("Updating Reboot DLL...") + ], + ), + ], + ); + } + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if(snapshot.hasError) + _createUpdateError(snapshot), + UsernameBox(), + const VersionSelector(), + const DeploymentSelector(), + const LaunchButton() ], - ), - ], + ); + } ); } - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if(widget.error != null) - WarningInfo( - text: "Cannot update Reboot DLL", - icon: FluentIcons.info, - severity: InfoBarSeverity.warning, - onPressed: () async { - if (shouldWriteError) { - await errorFile.writeAsString( - "Error: ${widget.error}\nStacktrace: ${widget.stackTrace}", - mode: FileMode.write - ); - shouldWriteError = false; - } - - launchUrl(errorFile.uri); - }, - ), - UsernameBox(), - VersionSelector(), - DeploymentSelector(enabled: true), - const LaunchButton() - ], + Widget _createUpdateError(AsyncSnapshot snapshot) { + return WarningInfo( + text: "Cannot update Reboot DLL", + icon: FluentIcons.info, + severity: InfoBarSeverity.warning, + onPressed: () => loadBinary("error.txt", true) + .then((file) => launchUrl(file.uri)) ); } } diff --git a/lib/src/util/build.dart b/lib/src/util/build.dart index abc2a17..49ba938 100644 --- a/lib/src/util/build.dart +++ b/lib/src/util/build.dart @@ -65,9 +65,9 @@ Future> _fetchManifests() async { var children = tableEntry.querySelectorAll("td"); var name = children[0].text; - var separator = name.indexOf("-") + 1; + var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-")); var version = parser - .tryParse(name.substring(separator, name.indexOf("-", separator))); + .tryParse(minifiedName.replaceFirst("-CL", "")); if (version == null) { continue; } diff --git a/lib/src/util/os.dart b/lib/src/util/os.dart index db08852..2fc55e0 100644 --- a/lib/src/util/os.dart +++ b/lib/src/util/os.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:path/path.dart' as path; -File errorFile = File("${Platform.environment["Temp"]}/error.txt"); - const int appBarSize = 2; final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index aab6619..d0f62b2 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -38,7 +38,7 @@ Future downloadRebootDll(int? lastUpdateMs) async { if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) { outputDir.delete(); - return lastUpdateMs!; + return lastUpdateMs ?? now.millisecondsSinceEpoch; } await rebootDll.rename(oldRebootDll.path); diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index 66ddc2c..435a86d 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -84,7 +84,7 @@ Future changeReverseProxyState(BuildContext context, String host, S return null; } - return await shelf_io.serve(proxyHandler(uri), 'localhost', 3551); + return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551); }catch(error){ _showStartProxyError(context, error); return null; @@ -129,10 +129,8 @@ Future _showReverseProxyCheck(BuildContext context, String host, String po actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), )) ] @@ -175,10 +173,8 @@ void _showStartProxyError(BuildContext context, Object error) { actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), ) ) @@ -198,10 +194,8 @@ void _showStopProxyError(BuildContext context, Object error) { actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), ) ) @@ -285,10 +279,8 @@ Future _showServerDownloadInfo(BuildContext context, bool portable) async future: nodeFuture, builder: (builder, snapshot) => SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'), ) ) @@ -310,10 +302,8 @@ void _showEmbeddedError(BuildContext context, String path) { actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), )) ], diff --git a/lib/src/widget/add_local_version.dart b/lib/src/widget/add_local_version.dart index 32c4252..c3edc9c 100644 --- a/lib/src/widget/add_local_version.dart +++ b/lib/src/widget/add_local_version.dart @@ -29,9 +29,8 @@ class AddLocalVersion extends StatelessWidget { List _createLocalVersionActions(BuildContext context) { return [ - FilledButton( + Button( onPressed: () => _closeLocalVersionDialog(context, false), - style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), ), FilledButton( diff --git a/lib/src/widget/add_server_version.dart b/lib/src/widget/add_server_version.dart index 78c94c9..1ef664d 100644 --- a/lib/src/widget/add_server_version.dart +++ b/lib/src/widget/add_server_version.dart @@ -94,9 +94,8 @@ class _AddServerVersionState extends State { switch (_status) { case DownloadStatus.none: return [ - FilledButton( + Button( onPressed: () => _onClose(), - style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close')), FilledButton( onPressed: () => _startDownload(context), @@ -108,20 +107,16 @@ class _AddServerVersionState extends State { return [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => _onClose(), - style: - ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'))) ]; default: return [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => _onClose(), - style: - ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), child: Text( _status == DownloadStatus.downloading ? 'Stop' : 'Close')), ) diff --git a/lib/src/widget/deployment_selector.dart b/lib/src/widget/deployment_selector.dart deleted file mode 100644 index b759629..0000000 --- a/lib/src/widget/deployment_selector.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/widget/smart_switch.dart'; - -class DeploymentSelector extends StatelessWidget { - final GameController _gameController = Get.find(); - final bool enabled; - - DeploymentSelector({Key? key, required this.enabled}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Tooltip( - message: enabled ? "Whether the launched client should be used to host multiplayer games or not" : "Hosting is not allowed", - child: _buildSwitch(context) - ); - } - - SmartSwitch _buildSwitch(BuildContext context) { - return SmartSwitch( - value: _gameController.host, - onDisabledPress: !enabled - ? () => showSnackbar(context, - const Snackbar(content: Text("Hosting is not allowed"))) - : null, - label: "Host", - enabled: enabled - ); - } -} diff --git a/lib/src/widget/host_checkbox.dart b/lib/src/widget/host_checkbox.dart new file mode 100644 index 0000000..76c242e --- /dev/null +++ b/lib/src/widget/host_checkbox.dart @@ -0,0 +1,74 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/model/game_type.dart'; +import 'package:reboot_launcher/src/widget/smart_switch.dart'; + +class DeploymentSelector extends StatefulWidget { + const DeploymentSelector({Key? key}) : super(key: key); + + @override + State createState() => _DeploymentSelectorState(); +} + +class _DeploymentSelectorState extends State { + final Map _options = { + GameType.client: "Client", + GameType.server: "Server", + GameType.headlessServer: "Headless Server" + }; + final Map _descriptions = { + GameType.client: "A fortnite client will be launched to play multiplayer games", + GameType.server: "A fortnite client will be launched to host multiplayer games", + GameType.headlessServer: "A fortnite client will be launched in the background to host multiplayer games", + }; + final GameController _gameController = Get.find(); + bool? _value; + + @override + void initState() { + switch(_gameController.type.value){ + case GameType.client: + _value = false; + break; + case GameType.server: + _value = true; + break; + case GameType.headlessServer: + _value = null; + break; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Tooltip( + message: _descriptions[_gameController.type.value]!, + child: InfoLabel( + label: _options[_gameController.type.value]!, + child: Checkbox( + checked: _value, + onChanged: _onSelected + ), + ), + ); + } + + void _onSelected(bool? value){ + if(value == null){ + _gameController.type(GameType.client); + setState(() => _value = false); + return; + } + + if(value){ + _gameController.type(GameType.server); + setState(() => _value = true); + return; + } + + _gameController.type(GameType.headlessServer); + setState(() => _value = null); + } +} diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/launch_button.dart index f8b983a..90080f1 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/launch_button.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:process_run/shell.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/model/game_type.dart'; import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/patcher.dart'; @@ -13,6 +15,8 @@ import 'package:reboot_launcher/src/util/server.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; +import '../util/os.dart'; + class LaunchButton extends StatefulWidget { const LaunchButton( {Key? key}) @@ -25,7 +29,15 @@ class LaunchButton extends StatefulWidget { class _LaunchButtonState extends State { final GameController _gameController = Get.find(); final ServerController _serverController = Get.find(); - bool _lawinFail = false; + File? _logFile; + bool _fail = false; + + @override + void initState() { + loadBinary("log.txt", true) + .then((value) => _logFile = value); + super.initState(); + } @override Widget build(BuildContext context) { @@ -67,7 +79,7 @@ class _LaunchButtonState extends State { try { _updateServerState(true); var version = _gameController.selectedVersionObs.value!; - var hosting = _gameController.host.value; + var hosting = _gameController.type.value == GameType.headlessServer; if (version.launcher != null) { _gameController.launcherProcess = await Process.start(version.launcher!.path, []); Win32Process(_gameController.launcherProcess!.pid).suspend(); @@ -88,7 +100,18 @@ class _LaunchButtonState extends State { return; } - _gameController.gameProcess = await Process.start(version.executable!.path, _createProcessArguments()) + if(_logFile != null && await _logFile!.exists()){ + await _logFile!.delete(); + } + + var gamePath = version.executable?.path; + if(gamePath == null){ + _onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null); + _onStop(); + return; + } + + _gameController.gameProcess = await Process.start(gamePath, _createProcessArguments()) ..exitCode.then((_) => _onEnd()) ..outLines.forEach(_onGameOutput); await _injectOrShowError("cranium.dll"); @@ -96,9 +119,10 @@ class _LaunchButtonState extends State { if(hosting){ await _showServerLaunchingWarning(); } - } catch (exception) { - _closeDialogIfOpen(); - _onError(exception); + } catch (exception, stacktrace) { + _closeDialogIfOpen(false); + _onError(exception, stacktrace); + _onStop(); } } @@ -140,15 +164,15 @@ class _LaunchButtonState extends State { } void _onEnd() { - if(_lawinFail){ + if(_fail){ return; } - _closeDialogIfOpen(); + _closeDialogIfOpen(false); _onStop(); } - void _closeDialogIfOpen() { + void _closeDialogIfOpen(bool success) { if(!mounted){ return; } @@ -158,7 +182,7 @@ class _LaunchButtonState extends State { return; } - Navigator.of(context).pop(false); + Navigator.of(context).pop(success); } Future _showBrokenServerWarning() async { @@ -176,10 +200,33 @@ class _LaunchButtonState extends State { actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ) + ) + ], + ) + ); + } + + Future _showUnsupportedHeadless() async { + if(!mounted){ + return; + } + + showDialog( + context: context, + builder: (context) => ContentDialog( + content: const SizedBox( + width: double.infinity, + child: Text("This version of Fortnite doesn't support headless hosting", textAlign: TextAlign.center) + ), + actions: [ + SizedBox( + width: double.infinity, + child: Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), ) ) @@ -197,7 +244,7 @@ class _LaunchButtonState extends State { context: context, builder: (context) => ContentDialog( content: const InfoLabel( - label: "Launching reboot server...", + label: "Launching headless reboot server...", child: SizedBox( width: double.infinity, child: ProgressBar() @@ -206,13 +253,11 @@ class _LaunchButtonState extends State { actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () { Navigator.of(context).pop(false); _onStop(); }, - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Cancel'), ) ) @@ -228,30 +273,52 @@ class _LaunchButtonState extends State { } void _onGameOutput(String line) { + if(kDebugMode){ + print(line); + } + + if(_logFile != null){ + _logFile!.writeAsString("$line\n", mode: FileMode.append); + } + if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { _onStop(); return; } if(line.contains("port 3551 failed: Connection refused")){ - _lawinFail = true; - _closeDialogIfOpen(); + _fail = true; + _closeDialogIfOpen(false); _showBrokenServerWarning(); return; } - if (line.contains("Game Engine Initialized") && !_gameController.host.value) { + if(line.contains("HTTP 400 response from ")){ + _fail = true; + _closeDialogIfOpen(false); + _showUnsupportedHeadless(); + return; + } + + if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) { _injectOrShowError("console.dll"); return; } - if(line.contains("added to UI Party led ") && _gameController.host.value){ + if(line.contains("Region") && _gameController.type.value != GameType.client){ _injectOrShowError("reboot.dll") - .then((value) => Navigator.of(context).pop(true)); + .then((value) => _closeDialogIfOpen(true)); } } - Future _onError(Object exception) { + Future _onError(Object exception, StackTrace? stackTrace) async { + if (stackTrace != null) { + var errorFile = await loadBinary("error.txt", true); + errorFile.writeAsString( + "Error: $exception\nStacktrace: $stackTrace", mode: FileMode.write); + launchUrl(errorFile.uri); + } + return showDialog( context: context, builder: (context) => ContentDialog( @@ -262,10 +329,8 @@ class _LaunchButtonState extends State { actions: [ SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(true), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), )) ], @@ -316,8 +381,8 @@ class _LaunchButtonState extends State { "-AUTH_TYPE=epic" ]; - if(_gameController.host.value){ - args.addAll(["-log", "-nullrhi", "-nosplash", "-nosound", "-unattended"]); + if(_gameController.type.value == GameType.headlessServer){ + args.addAll(["-nullrhi", "-nosplash", "-nosound"]); } return args; diff --git a/lib/src/widget/scan_local_version.dart b/lib/src/widget/scan_local_version.dart index 06acd42..bced423 100644 --- a/lib/src/widget/scan_local_version.dart +++ b/lib/src/widget/scan_local_version.dart @@ -35,9 +35,8 @@ class _ScanLocalVersionState extends State { List _createLocalVersionActions(BuildContext context) { if(_future == null) { return [ - FilledButton( + Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), ), FilledButton( @@ -53,9 +52,8 @@ class _ScanLocalVersionState extends State { if(!snapshot.hasData || snapshot.hasError) { return SizedBox( width: double.infinity, - child: FilledButton( + child: Button( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), ), ); diff --git a/lib/src/widget/smart_check_box.dart b/lib/src/widget/smart_check_box.dart new file mode 100644 index 0000000..1f2ba9f --- /dev/null +++ b/lib/src/widget/smart_check_box.dart @@ -0,0 +1,27 @@ +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/widget/smart_switch.dart b/lib/src/widget/smart_switch.dart index 4dde298..ac31d52 100644 --- a/lib/src/widget/smart_switch.dart +++ b/lib/src/widget/smart_switch.dart @@ -3,17 +3,17 @@ import 'package:get/get.dart'; import 'package:system_theme/system_theme.dart'; class SmartSwitch extends StatefulWidget { - final String label; + final String? label; final bool enabled; final Function()? onDisabledPress; final Rx value; const SmartSwitch( {Key? key, - required this.label, - required this.value, - this.enabled = true, - this.onDisabledPress}) + required this.value, + this.label, + this.enabled = true, + this.onDisabledPress}) : super(key: key); @override @@ -23,22 +23,32 @@ class SmartSwitch extends StatefulWidget { class _SmartSwitchState extends State { @override Widget build(BuildContext context) { + return widget.label == null ? _createSwitch() : _createLabel(); + } + + InfoLabel _createLabel() { return InfoLabel( - label: widget.label, - child: Obx(() => ToggleSwitch( - enabled: widget.enabled, - onDisabledPress: widget.onDisabledPress, - checked: widget.value.value, - onChanged: _onChanged, - style: ToggleSwitchThemeData.standard(ThemeData( - checkedColor: _toolTipColor.withOpacity(_checkedOpacity), - uncheckedColor: _toolTipColor.withOpacity(_uncheckedOpacity), - borderInputColor: _toolTipColor.withOpacity(_uncheckedOpacity), - accentColor: _bodyColor - .withOpacity(widget.value.value - ? _checkedOpacity - : _uncheckedOpacity) - .toAccentColor()))))); + label: widget.label!, + child: _createSwitch() + ); + } + + Widget _createSwitch() { + return Obx(() => ToggleSwitch( + enabled: widget.enabled, + onDisabledPress: widget.onDisabledPress, + checked: widget.value.value, + onChanged: _onChanged, + style: ToggleSwitchThemeData.standard(ThemeData( + checkedColor: _toolTipColor.withOpacity(_checkedOpacity), + uncheckedColor: _toolTipColor.withOpacity(_uncheckedOpacity), + borderInputColor: _toolTipColor.withOpacity(_uncheckedOpacity), + accentColor: _bodyColor + .withOpacity(widget.value.value + ? _checkedOpacity + : _uncheckedOpacity) + .toAccentColor()))) + ); } Color get _toolTipColor => diff --git a/lib/src/widget/username_box.dart b/lib/src/widget/username_box.dart index dfb6951..9e56d17 100644 --- a/lib/src/widget/username_box.dart +++ b/lib/src/widget/username_box.dart @@ -1,6 +1,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/model/game_type.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; class UsernameBox extends StatelessWidget { @@ -11,10 +12,10 @@ class UsernameBox extends StatelessWidget { @override Widget build(BuildContext context) { return Obx(() => Tooltip( - message: _gameController.host.value ? "The username of the game hoster" : "The in-game username of your player", + message: _gameController.type.value != GameType.client ? "The username of the game hoster" : "The in-game username of your player", child: SmartInput( label: "Username", - placeholder: "Type your ${_gameController.host.value ? 'hosting' : "in-game"} username", + placeholder: "Type your ${_gameController.type.value != GameType.client ? 'hosting' : "in-game"} username", controller: _gameController.username, populate: true ), diff --git a/lib/src/widget/version_selector.dart b/lib/src/widget/version_selector.dart index bfc68ec..7748446 100644 --- a/lib/src/widget/version_selector.dart +++ b/lib/src/widget/version_selector.dart @@ -10,6 +10,7 @@ import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/widget/add_local_version.dart'; import 'package:reboot_launcher/src/widget/add_server_version.dart'; import 'package:reboot_launcher/src/widget/scan_local_version.dart'; +import 'package:reboot_launcher/src/widget/smart_check_box.dart'; import 'package:url_launcher/url_launcher.dart'; class VersionSelector extends StatefulWidget { @@ -23,6 +24,7 @@ class VersionSelector extends StatefulWidget { class _VersionSelectorState extends State { final GameController _gameController = Get.find(); + final CheckboxController _deleteFilesController = CheckboxController(); @override Widget build(BuildContext context) { @@ -146,55 +148,78 @@ class _VersionSelectorState extends State { } Navigator.of(context).pop(); - launchUrl(version.location.uri); + launchUrl(version.location.uri) + .onError((error, stackTrace) => _onExplorerError()); break; case 1: - _gameController.removeVersion(version); - if(!mounted){ return; } - await _openDeleteDialog(context, version); - if(!mounted){ + var result = await _openDeleteDialog(context, version) ?? false; + if(!mounted || !result){ return; } Navigator.of(context).pop(); + + _gameController.removeVersion(version); if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) { _gameController.selectedVersionObs.value = null; } + if (_deleteFilesController.value && await version.location.exists()) { + version.location.delete(recursive: true); + } + break; } } - Future _openDeleteDialog(BuildContext context, FortniteVersion version) { - return showDialog( + bool _onExplorerError() { + showSnackbar( + context, + const Snackbar( + content: Text("This version doesn't exist on the local machine", textAlign: TextAlign.center), + extended: true + ) + ); + return false; + } + + Future _openDeleteDialog(BuildContext context, FortniteVersion version) { + return showDialog( context: context, builder: (context) => ContentDialog( - content: const SizedBox( - width: double.infinity, - child: Text("Do you want to also delete the files for this version?", - textAlign: TextAlign.center)), + 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), + + SmartCheckBox( + controller: _deleteFilesController, + content: const Text("Delete version files from disk") + ) + ], + ), actions: [ - FilledButton( - onPressed: () => Navigator.of(context).pop(), + Button( + onPressed: () => Navigator.of(context).pop(false), child: const Text('Keep'), ), FilledButton( - onPressed: () async { - Navigator.of(context).pop(); - if (await version.location.exists()) { - version.location.delete(recursive: true); - } - }, - style: - ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), + onPressed: () => Navigator.of(context).pop(true), child: const Text('Delete'), ) ], - )); + ) + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index ffa8ec7..58b0ecd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "3.8.0" +version: "3.10.0" publish_to: 'none' @@ -53,7 +53,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 3.8.0.0 + msix_version: 3.10.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64