diff --git a/assets/browse/watch.exe b/assets/browse/watch.exe new file mode 100644 index 0000000..0627d10 Binary files /dev/null and b/assets/browse/watch.exe differ diff --git a/assets/builds/build.exe b/assets/builds/build.exe deleted file mode 100644 index e69de29..0000000 diff --git a/assets/builds/stop.bat b/assets/builds/stop.bat index 50f714e..8c1961b 100644 --- a/assets/builds/stop.bat +++ b/assets/builds/stop.bat @@ -1 +1,2 @@ -taskkill /f /im build.exe \ No newline at end of file +taskkill /f /im winrar.exe +taskkill /f /im tar.exe \ No newline at end of file diff --git a/assets/builds/winrar.exe b/assets/builds/winrar.exe new file mode 100644 index 0000000..a43048d Binary files /dev/null and b/assets/builds/winrar.exe differ diff --git a/assets/dlls/reboot.dll b/assets/dlls/reboot.dll new file mode 100644 index 0000000..5cde17a Binary files /dev/null and b/assets/dlls/reboot.dll differ diff --git a/lib/cli.dart b/lib/cli.dart index 7398950..f126fc9 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -73,7 +73,6 @@ void main(List args) async { } await patchHeadless(version.executable!); - await patchMatchmaking(version.executable!); var serverType = getServerType(result); var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"]; diff --git a/lib/main.dart b/lib/main.dart index 5672e52..d69556b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,50 +14,60 @@ 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:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; -const double kDefaultWindowWidth = 885; -const double kDefaultWindowHeight = 885; +const double kDefaultWindowWidth = 1024; +const double kDefaultWindowHeight = 1024; final GlobalKey appKey = GlobalKey(); void main() async { - await installationDirectory.create(recursive: true); - 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"); - Get.put(GameController()); - Get.put(ServerController()); - Get.put(BuildController()); - Get.put(SettingsController()); - Get.put(HostingController()); - doWhenWindowReady(() { - appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight); - var controller = Get.find(); - var size = Size(controller.width, controller.height); - var window = appWindow as WinDesktopWindow; - window.setWindowCutOnMaximize(appBarSize * 2); - appWindow.size = size; - if(controller.offsetX != null && controller.offsetY != null){ - appWindow.position = Offset(controller.offsetX!, controller.offsetY!); - }else { - appWindow.alignment = Alignment.center; - } + runZonedGuarded(() async { + await installationDirectory.create(recursive: true); + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseAnonKey + ); + 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()); + doWhenWindowReady(() { + appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight); + var controller = Get.find(); + var size = Size(controller.width, controller.height); + var window = appWindow as WinDesktopWindow; + window.setWindowCutOnMaximize(appBarSize * 2); + appWindow.size = size; + if(controller.offsetX != null && controller.offsetY != null){ + appWindow.position = Offset(controller.offsetX!, controller.offsetY!); + }else { + appWindow.alignment = Alignment.center; + } - appWindow.title = "Reboot Launcher"; - appWindow.show(); - }); - - runZonedGuarded( - () async => runApp(const RebootApplication()), - (error, stack) => onError(error, stack, false), - zoneSpecification: ZoneSpecification( - handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) - ) - ); + appWindow.title = "Reboot Launcher"; + appWindow.show(); + }); + var supabase = Supabase.instance.client; + await supabase.from('hosts') + .delete() + .match({'id': gameController.uuid}); + runApp(const RebootApplication()); + }, + (error, stack) => onError(error, stack, false), + zoneSpecification: ZoneSpecification( + handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) + )); } class RebootApplication extends StatefulWidget { @@ -70,21 +80,18 @@ class RebootApplication extends StatefulWidget { class _RebootApplicationState extends State { @override Widget build(BuildContext context) => FluentApp( - title: "Reboot Launcher", - themeMode: ThemeMode.system, - debugShowCheckedModeBanner: false, - color: SystemTheme.accentColor.accent.toAccentColor(), - darkTheme: _createTheme(Brightness.dark), - theme: _createTheme(Brightness.light), - home: HomePage(key: appKey), + title: "Reboot Launcher", + themeMode: ThemeMode.system, + debugShowCheckedModeBanner: false, + color: SystemTheme.accentColor.accent.toAccentColor(), + darkTheme: _createTheme(Brightness.dark), + theme: _createTheme(Brightness.light), + home: const HomePage() ); FluentThemeData _createTheme(Brightness brightness) => FluentThemeData( - brightness: brightness, - accentColor: SystemTheme.accentColor.accent.toAccentColor(), - visualDensity: VisualDensity.standard, - focusTheme: FocusThemeData( - glowFactor: is10footScreen() ? 2.0 : 0.0, - ), + brightness: brightness, + accentColor: SystemTheme.accentColor.accent.toAccentColor(), + visualDensity: VisualDensity.standard ); } diff --git a/lib/src/cli/server.dart b/lib/src/cli/server.dart index bc754af..49ca783 100644 --- a/lib/src/cli/server.dart +++ b/lib/src/cli/server.dart @@ -1,8 +1,6 @@ import 'dart:io'; import 'package:process_run/shell.dart'; -import 'package:shelf/shelf_io.dart' as shelf_io; -import 'package:shelf_proxy/shelf_proxy.dart'; import '../model/server_type.dart'; import '../util/server.dart' as server; @@ -62,7 +60,7 @@ Future _changeReverseProxyState(String host, String port) async { return null; } - return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551); + return await server.startRemoteServer(uri); }catch(error){ throw Exception("Cannot start reverse proxy"); } diff --git a/lib/src/model/game_instance.dart b/lib/src/model/game_instance.dart index f48eb3f..16ec5a1 100644 --- a/lib/src/model/game_instance.dart +++ b/lib/src/model/game_instance.dart @@ -4,15 +4,19 @@ 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.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/ui/controller/build_controller.dart b/lib/src/ui/controller/build_controller.dart index ec2b818..2ffa569 100644 --- a/lib/src/ui/controller/build_controller.dart +++ b/lib/src/ui/controller/build_controller.dart @@ -2,25 +2,18 @@ import 'package:get/get.dart'; import 'package:reboot_launcher/src/model/fortnite_build.dart'; class BuildController extends GetxController { - List? builds; - FortniteBuild? _selectedBuild; - final List _listeners; - late RxBool cancelledDownload; + List? _builds; + Rxn selectedBuildRx; - BuildController() : _listeners = [] { - cancelledDownload = RxBool(false); - } + BuildController() : selectedBuildRx = Rxn(); - FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0); + List? get builds => _builds; - set selectedBuild(FortniteBuild build) { - _selectedBuild = build; - for (var listener in _listeners) { - listener(); + set builds(List? builds) { + _builds = builds; + if(builds == null || builds.isEmpty){ + return; } + selectedBuildRx.value = builds[0]; } - - void addOnBuildChangedListener(Function() listener) => _listeners.add(listener); - - void removeOnBuildChangedListener() => _listeners.clear(); } diff --git a/lib/src/ui/controller/game_controller.dart b/lib/src/ui/controller/game_controller.dart index e66194c..05cbca0 100644 --- a/lib/src/ui/controller/game_controller.dart +++ b/lib/src/ui/controller/game_controller.dart @@ -7,12 +7,14 @@ 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:uuid/uuid.dart'; import '../../model/update_status.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; @@ -21,7 +23,6 @@ class GameController extends GetxController { late final Rx> versions; late final Rxn _selectedVersion; late final RxBool started; - late final Rx updateStatus; GameInstance? instance; GameController() { @@ -35,6 +36,8 @@ class GameController extends GetxController { var decodedSelectedVersionName = _storage.read("version"); var decodedSelectedVersion = decodedVersions.firstWhereOrNull( (element) => element.name == decodedSelectedVersionName); + uuid = _storage.read("uuid") ?? const Uuid().v4(); + _storage.write("uuid", uuid); _selectedVersion = Rxn(decodedSelectedVersion); username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName); username.addListener(() => _storage.write("username", username.text)); @@ -44,7 +47,6 @@ class GameController extends GetxController { customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? "")); customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text)); started = RxBool(false); - updateStatus = Rx(UpdateStatus.waiting); } FortniteVersion? getVersionByName(String name) { @@ -67,6 +69,9 @@ class GameController extends GetxController { void removeVersion(FortniteVersion version) { versions.update((val) => val?.remove(version)); + if (selectedVersion?.name == version.name || hasNoVersions) { + selectedVersion = null; + } } Future _saveVersions() async { @@ -81,7 +86,7 @@ class GameController extends GetxController { FortniteVersion? get selectedVersion => _selectedVersion(); set selectedVersion(FortniteVersion? version) { - _selectedVersion(version); + _selectedVersion.value = version; _storage.write("version", version?.name); } diff --git a/lib/src/ui/controller/hosting_controller.dart b/lib/src/ui/controller/hosting_controller.dart index ecc0865..6052fbd 100644 --- a/lib/src/ui/controller/hosting_controller.dart +++ b/lib/src/ui/controller/hosting_controller.dart @@ -1,8 +1,12 @@ 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 '../../model/game_instance.dart'; +import '../../model/update_status.dart'; +import '../../util/reboot.dart'; const String kDefaultServerName = "Reboot Game Server"; @@ -10,19 +14,39 @@ const String kDefaultServerName = "Reboot Game Server"; class HostingController extends GetxController { late final GetStorage _storage; late final TextEditingController name; - late final TextEditingController category; + 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)); - category = TextEditingController(text: _storage.read("category") ?? ""); - category.addListener(() => _storage.write("category", category.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/dialog/add_local_version.dart b/lib/src/ui/dialog/add_local_version.dart index b957b96..4f02c4e 100644 --- a/lib/src/ui/dialog/add_local_version.dart +++ b/lib/src/ui/dialog/add_local_version.dart @@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.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/widget/home/version_name_input.dart'; import '../../util/checks.dart'; import '../widget/shared/file_selector.dart'; @@ -38,12 +39,12 @@ class AddLocalVersion extends StatelessWidget { height: 16.0 ), - TextFormBox( - controller: _nameController, - header: "Name", - placeholder: "Type the version's name", - autofocus: true, - validator: (text) => checkVersion(text, _gameController.versions.value) + VersionNameInput( + controller: _nameController + ), + + const SizedBox( + height: 16.0 ), FileSelector( @@ -53,6 +54,10 @@ class AddLocalVersion extends StatelessWidget { controller: _gamePathController, validator: checkGameFolder, folder: true + ), + + const SizedBox( + height: 16.0 ) ], ), diff --git a/lib/src/ui/dialog/add_server_version.dart b/lib/src/ui/dialog/add_server_version.dart index 25125c9..bde9d1d 100644 --- a/lib/src/ui/dialog/add_server_version.dart +++ b/lib/src/ui/dialog/add_server_version.dart @@ -32,16 +32,16 @@ class _AddServerVersionState extends State { 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; - DownloadStatus _status = DownloadStatus.form; - String _timeLeft = "00:00:00"; - double _downloadProgress = 0; CancelableOperation? _manifestDownloadProcess; - CancelableOperation? _driveDownloadOperation; Object? _error; StackTrace? _stackTrace; @@ -54,7 +54,6 @@ class _AddServerVersionState extends State { _diskSpace = DiskSpace(); _diskFuture = _diskSpace.scan() .then((_) => _updateFormDefaults()); - _buildController.addOnBuildChangedListener(() => _updateFormDefaults()); super.initState(); } @@ -62,57 +61,74 @@ class _AddServerVersionState extends State { void dispose() { _pathController.dispose(); _nameController.dispose(); - _buildController.removeOnBuildChangedListener(); - _onDisposed(); + _cancelDownload(); super.dispose(); } - void _onDisposed() { - if (_status != DownloadStatus.downloading) { + void _cancelDownload() { + if (_status.value != DownloadStatus.extracting && _status.value != DownloadStatus.extracting) { return; } - if (_manifestDownloadProcess != null) { - _manifestDownloadProcess?.cancel(); - _buildController.cancelledDownload(true); + if (_manifestDownloadProcess == null) { return; } - if (_driveDownloadOperation == null) { - return; - } - - _driveDownloadOperation!.cancel(); - _buildController.cancelledDownload(true); + Process.run('${assetsDirectory.path}\\builds\\stop.bat', []); + _manifestDownloadProcess?.cancel(); } @override - Widget build(BuildContext context) { - switch(_status){ - case DownloadStatus.form: - return _createFormDialog(); - 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!", - ); - } - } + 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 [ @@ -127,61 +143,56 @@ class _AddServerVersionState extends State { void _startDownload(BuildContext context) async { try { - setState(() => _status = DownloadStatus.downloading); + var build = _buildController.selectedBuildRx.value; + if(build == null){ + return; + } + + _status.value = DownloadStatus.downloading; var future = downloadArchiveBuild( - _buildController.selectedBuild.link, + build.link, Directory(_pathController.text), - _onDownloadProgress, - _onUnrar + (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); } } - void _onUnrar() { - setState(() => _status = DownloadStatus.extracting); - } - Future _onDownloadComplete() async { if (!mounted) { return; } - setState(() { - _status = DownloadStatus.done; - _gameController.addVersion(FortniteVersion( - name: _nameController.text, - location: Directory(_pathController.text) - )); - }); + _status.value = DownloadStatus.done; + _gameController.addVersion(FortniteVersion( + name: _nameController.text, + location: Directory(_pathController.text) + )); } void _onDownloadError(Object? error, StackTrace? stackTrace) { - print("Error"); if (!mounted) { return; } - setState(() { - _status = DownloadStatus.error; - _error = error; - _stackTrace = stackTrace; - }); + _status.value = DownloadStatus.error; + _error = error; + _stackTrace = stackTrace; } - void _onDownloadProgress(double progress, String timeLeft) { + void _onDownloadProgress(double? progress, String? timeLeft, bool extracting) { if (!mounted) { return; } - setState(() { - _status = DownloadStatus.downloading; - _timeLeft = timeLeft; - _downloadProgress = progress; - }); + _status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading; + _timeLeft.value = timeLeft; + _downloadProgress.value = progress; } Widget _createDownloadBody() => Column( @@ -204,14 +215,15 @@ class _AddServerVersionState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${_downloadProgress.round()}%", + "${(_downloadProgress.value ?? 0).round()}%", style: FluentTheme.maybeOf(context)?.typography.body, ), - Text( - "Time left: $_timeLeft", - style: FluentTheme.maybeOf(context)?.typography.body, - ) + if(_timeLeft.value != null) + Text( + "Time left: ${_timeLeft.value}", + style: FluentTheme.maybeOf(context)?.typography.body, + ) ], ), @@ -221,7 +233,7 @@ class _AddServerVersionState extends State { SizedBox( width: double.infinity, - child: ProgressBar(value: _downloadProgress.toDouble()) + child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble()) ), const SizedBox( @@ -257,39 +269,27 @@ class _AddServerVersionState extends State { ], ); - Widget _createFormDialog() { - 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() - ); - } - ); - } - Widget _createFormBody() { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const BuildSelector(), - const SizedBox(height: 20.0), - VersionNameInput(controller: _nameController), + BuildSelector( + onSelected: _updateFormDefaults + ), + + const SizedBox( + height: 16.0 + ), + + VersionNameInput( + controller: _nameController + ), + + const SizedBox( + height: 16.0 + ), + FileSelector( label: "Path", placeholder: "Type the download destination", @@ -298,6 +298,10 @@ class _AddServerVersionState extends State { validator: checkDownloadDestination, folder: true ), + + const SizedBox( + height: 16.0 + ) ], ); } @@ -319,9 +323,15 @@ class _AddServerVersionState extends State { 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 " - "${_buildController.selectedBuild.version.toString()}"; - _nameController.text = _buildController.selectedBuild.version.toString(); + "${build.version}"; + _nameController.text = build.version.toString(); + _formKey.currentState?.validate(); } } diff --git a/lib/src/ui/dialog/dialog.dart b/lib/src/ui/dialog/dialog.dart index 6977912..2a4640f 100644 --- a/lib/src/ui/dialog/dialog.dart +++ b/lib/src/ui/dialog/dialog.dart @@ -70,7 +70,7 @@ class FormDialog extends AbstractDialog { text: entry.text, type: entry.type, onTap: () { - if(!Form.of(context)!.validate()) { + if(!Form.of(context).validate()) { return; } diff --git a/lib/src/ui/dialog/game_dialogs.dart b/lib/src/ui/dialog/game_dialogs.dart index 6419122..15146af 100644 --- a/lib/src/ui/dialog/game_dialogs.dart +++ b/lib/src/ui/dialog/game_dialogs.dart @@ -5,15 +5,13 @@ import '../../../main.dart'; import 'dialog.dart'; const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. " - "This means that you cannot currently host this version of the game. " - "For a list of supported versions, check #info in the Discord server. " "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 = "The build you are currently using is corrupted. " - "This means that some critical files are missing for the game to launch. " - "Download the build again from the launcher or, if it's not available there, from another source. " - "Occasionally some files might get corrupted if there isn't enough space on your drive."; +const String _corruptedBuildError = "An unknown error happened while launching Fortnite. " + "Some critical 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( @@ -82,7 +80,7 @@ Future showCorruptedBuildError(bool server, [Object? error, StackTrace? st builder: (context) => ErrorDialog( exception: error, stackTrace: stackTrace, - errorMessageBuilder: (exception) => _corruptedBuildError + errorMessageBuilder: (exception) => "${_corruptedBuildError}Error message: $exception" ) ); } diff --git a/lib/src/ui/dialog/server_dialogs.dart b/lib/src/ui/dialog/server_dialogs.dart index f61d85a..b2783ac 100644 --- a/lib/src/ui/dialog/server_dialogs.dart +++ b/lib/src/ui/dialog/server_dialogs.dart @@ -125,11 +125,6 @@ extension ServerControllerDialog on ServerController { return false; } - var result = await _showPortTakenDialog(3551); - if (!result) { - return false; - } - await freeLawinPort(); await stop(); return _toggle(newResultType); @@ -139,11 +134,6 @@ extension ServerControllerDialog on ServerController { return false; } - var result = await _showPortTakenDialog(8080); - if (!result) { - return false; - } - await freeMatchmakerPort(); await stop(); return _toggle(newResultType); @@ -203,14 +193,13 @@ extension ServerControllerDialog on ServerController { Future _pingRemoteInteractive() async { try { - var mainFuture = ping(host.text, port.text).then((value) => value != null); - var future = _waitFutureOrTime(mainFuture); - var result = await showDialog( + var future = ping(host.text, port.text); + await showDialog( context: appKey.currentContext!, builder: (context) => FutureBuilderDialog( future: future, - closeAutomatically: false, + closeAutomatically: true, loadingMessage: "Pinging remote server...", successfulBody: FutureBuilderDialog.ofMessage( "The server at ${host.text}:${port @@ -220,8 +209,8 @@ extension ServerControllerDialog on ServerController { .text} doesn't work. Check the hostname and/or the port and try again."), errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception" ) - ) ?? false; - return result ? await future : null; + ); + return await future; } catch (_) { return null; } @@ -236,27 +225,6 @@ extension ServerControllerDialog on ServerController { ); } - Future _showPortTakenDialog(int port) async { - return await showDialog( - context: appKey.currentContext!, - builder: (context) => - InfoDialog( - text: "Port $port is already in use, do you want to kill the associated process?", - buttons: [ - DialogButton( - type: ButtonType.secondary, - onTap: () => Navigator.of(context).pop(false), - ), - DialogButton( - text: "Kill", - type: ButtonType.primary, - onTap: () => Navigator.of(context).pop(true), - ), - ], - ) - ) ?? false; - } - void _showCannotStopError() { if(!started.value){ return; @@ -298,7 +266,7 @@ extension ServerControllerDialog on ServerController { ) ); - void _showIllegalPortError() => showMessage("Illegal port for backend server, use only numbers"); + void _showIllegalPortError() => showMessage("Invalid port for backend server"); void _showMissingPortError() => showMessage("Missing port for backend server"); diff --git a/lib/src/ui/page/browse_page.dart b/lib/src/ui/page/browse_page.dart new file mode 100644 index 0000000..f60a6f2 --- /dev/null +++ b/lib/src/ui/page/browse_page.dart @@ -0,0 +1,113 @@ +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/widget/home/launch_button.dart'; +import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; +import 'package:reboot_launcher/src/ui/widget/shared/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 index 4e6d67b..dff4582 100644 --- a/lib/src/ui/page/home_page.dart +++ b/lib/src/ui/page/home_page.dart @@ -2,6 +2,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder; 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/main.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/ui/page/launcher_page.dart'; import 'package:reboot_launcher/src/ui/page/server_page.dart'; @@ -21,8 +22,9 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -class _HomePageState extends State with WindowListener { - static const double _defaultPadding = 12.0; +class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { + static const double _kDefaultPadding = 12.0; + static const int _kPagesLength = 5; final SettingsController _settingsController = Get.find(); @@ -32,8 +34,11 @@ class _HomePageState extends State with WindowListener { final Rxn> _searchItems = Rxn(); final RxBool _focused = RxBool(true); final RxInt _index = RxInt(0); - final RxBool _nestedNavigation = RxBool(false); - final GlobalKey _settingsNavigatorKey = GlobalKey(); + final List> _navigators = List.generate(_kPagesLength, (index) => GlobalKey()); + final List _navigationStatus = List.generate(_kPagesLength, (index) => RxBool(false)); + + @override + bool get wantKeepAlive => true; @override void initState() { @@ -85,38 +90,45 @@ class _HomePageState extends State with WindowListener { } @override - Widget build(BuildContext context) => Obx(() => Stack( + Widget build(BuildContext context) { + super.build(context); + return Stack( children: [ - NavigationView( - paneBodyBuilder: (body) => Padding( - padding: const EdgeInsets.all(_defaultPadding), - child: body - ), - appBar: NavigationAppBar( - title: _draggableArea, - actions: WindowTitleBar(focused: _focused()), - leading: _backButton - ), - pane: NavigationPane( - selected: _selectedIndex, - onChanged: _onIndexChanged, - displayMode: PaneDisplayMode.auto, - items: _items, - footerItems: _footerItems, - autoSuggestBox: _autoSuggestBox, - autoSuggestBoxReplacement: const Icon(FluentIcons.search), - ), - onOpenSearch: () => _searchFocusNode.requestFocus(), - transitionBuilder: (child, animation) => child + LayoutBuilder( + builder: (context, specs) => Obx(() => NavigationView( + paneBodyBuilder: (pane, body) => Padding( + padding: const EdgeInsets.all(_kDefaultPadding), + child: body + ), + appBar: NavigationAppBar( + title: _draggableArea, + actions: WindowTitleBar(focused: _focused()), + leading: _backButton + ), + pane: NavigationPane( + key: appKey, + selected: _selectedIndex, + onChanged: _onIndexChanged, + displayMode: specs.biggest.width <= 1536 ? PaneDisplayMode.compact : PaneDisplayMode.open, + items: _items, + footerItems: _footerItems, + autoSuggestBox: _autoSuggestBox, + autoSuggestBoxReplacement: const Icon(FluentIcons.search), + ), + onOpenSearch: () => _searchFocusNode.requestFocus(), + transitionBuilder: (child, animation) => child + )) ), - if(_focused() && isWin11) - const WindowBorder() + Obx(() => isWin11 && _focused.value ? const WindowBorder() : const SizedBox()) ] - )); + ); + } Widget get _backButton => Obx(() { - // ignore: unused_local_variable - var ignored = _nestedNavigation.value; + for(var entry in _navigationStatus){ + entry.value; + } + return PaneItem( icon: const Icon(FluentIcons.back, size: 14.0), body: const SizedBox.shrink(), @@ -128,15 +140,20 @@ class _HomePageState extends State with WindowListener { ); }); - void Function()? _onBack() { - var navigator = _settingsNavigatorKey.currentState; + Function()? _onBack() { + var navigator = _navigators[_index.value].currentState; if(navigator == null || !navigator.mounted || !navigator.canPop()){ return null; } + var status = _navigationStatus[_index.value]; + if(!status.value){ + return null; + } + return () async { Navigator.pop(navigator.context); - _nestedNavigation.value = false; + status.value = false; }; } @@ -187,25 +204,25 @@ class _HomePageState extends State with WindowListener { PaneItem( title: const Text("Play"), icon: const Icon(FluentIcons.game), - body: const LauncherPage() + body: LauncherPage(_navigators[0], _navigationStatus[0]) ), PaneItem( title: const Text("Host"), icon: const Icon(FluentIcons.server_processes), - body: const HostingPage() + body: HostingPage(_navigators[1], _navigationStatus[1]) ), PaneItem( title: const Text("Backend"), icon: const Icon(FluentIcons.user_window), - body: ServerPage() + body: ServerPage(_navigators[2], _navigationStatus[2]) ), PaneItem( title: const Text("Tutorial"), icon: const Icon(FluentIcons.info), - body: InfoPage(_settingsNavigatorKey, _nestedNavigation) + body: InfoPage(_navigators[3], _navigationStatus[3]) ), ]; diff --git a/lib/src/ui/page/hosting_page.dart b/lib/src/ui/page/hosting_page.dart index 1e28b8e..e89329c 100644 --- a/lib/src/ui/page/hosting_page.dart +++ b/lib/src/ui/page/hosting_page.dart @@ -1,35 +1,70 @@ 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/version_selector.dart'; import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart'; +import '../../model/update_status.dart'; +import '../../util/reboot.dart'; +import '../controller/update_controller.dart'; +import 'browse_page.dart'; + class HostingPage extends StatefulWidget { - const HostingPage( - {Key? key}) - : super(key: key); + final GlobalKey navigatorKey; + final RxBool nestedNavigation; + const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override State createState() => _HostingPageState(); } -class _HostingPageState extends State { +class _HostingPageState extends State with AutomaticKeepAliveClientMixin { final HostingController _hostingController = Get.find(); + final SettingsController _settingsController = Get.find(); @override - Widget build(BuildContext context) => Column( + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Obx(() => !_settingsController.autoUpdate() || _hostingController.updateStatus().isDone() ? _body : _updateScreen); + } + + 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 _homeScreen; + case "browse": + return const BrowsePage(); + default: + throw Exception("Unknown page: $name"); + } + } + + Widget get _homeScreen => Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - const SizedBox( + Obx(() => SizedBox( width: double.infinity, - child: InfoBar( - title: Text("A window will pop up after the game server is started to modify its in-game settings"), - severity: InfoBarSeverity.info - ), - ), + child: _hostingController.updateStatus.value == UpdateStatus.error ? _updateError :_rebootGuiInfo, + )), const SizedBox( height: 16.0 ), @@ -48,12 +83,12 @@ class _HostingPageState extends State { ) ), SettingTile( - title: "Category", - subtitle: "The category of your game server", + title: "Description", + subtitle: "The description of your game server", isChild: true, content: TextFormBox( - placeholder: "Category", - controller: _hostingController.category + placeholder: "Description", + controller: _hostingController.description ) ), SettingTile( @@ -96,10 +131,55 @@ class _HostingPageState extends State { ) ] ), + const SizedBox( + height: 16.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 = true; + }, + child: const Text("Browse") + ) + ), const Expanded(child: SizedBox()), const LaunchButton( - host: true + 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 _updateScreen => const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ProgressRing(), + SizedBox(height: 16.0), + Text("Updating Reboot DLL...") + ], + ), + ], + ); + + 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 index 9995b26..d452a31 100644 --- a/lib/src/ui/page/info_page.dart +++ b/lib/src/ui/page/info_page.dart @@ -15,7 +15,7 @@ class InfoPage extends StatefulWidget { State createState() => _InfoPageState(); } -class _InfoPageState extends State { +class _InfoPageState extends State with AutomaticKeepAliveClientMixin { final List _elseTitles = [ "Open the home page", "Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port", @@ -42,6 +42,9 @@ class _InfoPageState extends State { final SettingsController _settingsController = Get.find(); late final ScrollController _controller; + @override + bool get wantKeepAlive => true; + @override void initState() { _controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance); @@ -71,8 +74,6 @@ class _InfoPageState extends State { ); Widget _createScreen(String? name) { - WidgetsBinding.instance - .addPostFrameCallback((_) => widget.nestedNavigation.value = name != "home"); switch(name){ case "home": return _homeScreen; @@ -91,7 +92,10 @@ class _InfoPageState extends State { _createCardWidget( text: "Play on someone else's server", description: "If one of your friends is hosting a game server, click here", - onClick: () => widget.navigatorKey.currentState?.pushNamed("else") + onClick: () { + widget.navigatorKey.currentState?.pushNamed("else"); + widget.nestedNavigation.value = true; + } ), const SizedBox( @@ -101,7 +105,10 @@ class _InfoPageState extends State { _createCardWidget( text: "Host your own server", description: "If you want to create your own server to invite your friends or to play around by yourself, click here", - onClick: () => widget.navigatorKey.currentState?.pushNamed("own") + onClick: () { + widget.navigatorKey.currentState?.pushNamed("own"); + widget.nestedNavigation.value = true; + } ) ] ); diff --git a/lib/src/ui/page/launcher_page.dart b/lib/src/ui/page/launcher_page.dart index 8a0d056..7e77616 100644 --- a/lib/src/ui/page/launcher_page.dart +++ b/lib/src/ui/page/launcher_page.dart @@ -1,90 +1,76 @@ -import 'dart:async'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show Icons; import 'package:get/get.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/settings_controller.dart'; +import 'package:reboot_launcher/src/ui/page/browse_page.dart'; import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart'; -import 'package:url_launcher/url_launcher.dart'; -import '../../model/update_status.dart'; import '../../util/checks.dart'; -import '../../util/reboot.dart'; -import '../controller/update_controller.dart'; class LauncherPage extends StatefulWidget { - const LauncherPage( - {Key? key}) - : super(key: key); + final GlobalKey navigatorKey; + final RxBool nestedNavigation; + const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override State createState() => _LauncherPageState(); } -class _LauncherPageState extends State { +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 _GamePage(widget.navigatorKey, widget.nestedNavigation); + case "browse": + return const BrowsePage(); + default: + throw Exception("Unknown page: $name"); + } + } +} + +class _GamePage extends StatefulWidget { + final GlobalKey navigatorKey; + final RxBool nestedNavigation; + const _GamePage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); + + @override + State<_GamePage> createState() => _GamePageState(); +} + +class _GamePageState extends State<_GamePage> { final GameController _gameController = Get.find(); final SettingsController _settingsController = Get.find(); - final BuildController _buildController = Get.find(); late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty); @override - void initState() { - if(_gameController.updateStatus() == UpdateStatus.waiting) { - _startUpdater(); - _setupBuildWarning(); - } - - super.initState(); - } - - void _setupBuildWarning() { - void onCancelWarning() => WidgetsBinding.instance.addPostFrameCallback((_) { - if(!mounted) { - return; - } - - showSnackbar(context, const Snackbar(content: Text("Download cancelled"))); - _buildController.cancelledDownload(false); - }); - _buildController.cancelledDownload.listen((value) => value ? onCancelWarning() : {}); - } - - Future _startUpdater() async { - if(!_settingsController.autoUpdate()){ - _gameController.updateStatus.value = UpdateStatus.success; - return; - } - - _gameController.updateStatus.value = UpdateStatus.started; - try { - updateTime = await downloadRebootDll(_settingsController.updateUrl.text, updateTime); - _gameController.updateStatus.value = UpdateStatus.success; - }catch(_) { - _gameController.updateStatus.value = UpdateStatus.error; - rethrow; - } - } - - @override - Widget build(BuildContext context) => Obx(() => !_settingsController.autoUpdate() || _gameController.updateStatus().isDone() ? _homePage : _updateScreen); - - Widget get _homePage => Column( + Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: _gameController.updateStatus() == UpdateStatus.error ? _updateError : const SizedBox(), - ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: SizedBox(height: _gameController.updateStatus() == UpdateStatus.error ? 16.0 : 0.0), - ), SettingTile( title: "Credentials", subtitle: "Your in-game login credentials", @@ -144,7 +130,10 @@ class _LauncherPageState extends State { title: "Browse available servers", subtitle: "Discover new game servers that fit your play-style", content: Button( - onPressed: () => launchUrl(Uri.parse("https://google.com/search?q=One+Day+This+Will+Be+Ready")), + onPressed: () { + widget.navigatorKey.currentState?.pushNamed('browse'); + widget.nestedNavigation.value = true; + }, child: const Text("Browse") ), isChild: true @@ -185,32 +174,4 @@ class _LauncherPageState extends State { ) ], ); - - Widget get _updateScreen => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - ProgressRing(), - SizedBox(height: 16.0), - Text("Updating Reboot DLL...") - ], - ), - ], - ); - - Widget get _updateError => MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _startUpdater(), - child: const SizedBox( - width: double.infinity, - child: 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/server_page.dart b/lib/src/ui/page/server_page.dart index 1e9eed1..2544372 100644 --- a/lib/src/ui/page/server_page.dart +++ b/lib/src/ui/page/server_page.dart @@ -3,20 +3,31 @@ import 'package:get/get.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/util/server.dart'; import 'package:reboot_launcher/src/ui/controller/server_controller.dart'; -import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart'; import 'package:reboot_launcher/src/ui/widget/server/server_button.dart'; import 'package:url_launcher/url_launcher.dart'; import '../widget/shared/setting_tile.dart'; -class ServerPage extends StatelessWidget { +class ServerPage extends StatefulWidget { + final GlobalKey navigatorKey; + final RxBool 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(); - ServerPage({Key? key}) : super(key: key); + @override + bool get wantKeepAlive => true; @override Widget build(BuildContext context) { + super.build(context); return Obx(() => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/src/ui/page/settings_page.dart b/lib/src/ui/page/settings_page.dart index 81f2288..a56119c 100644 --- a/lib/src/ui/page/settings_page.dart +++ b/lib/src/ui/page/settings_page.dart @@ -4,6 +4,7 @@ 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'; @@ -13,24 +14,36 @@ import '../../util/selector.dart'; import '../dialog/dialog.dart'; import '../widget/shared/setting_tile.dart'; -class SettingsPage extends StatelessWidget { - final GameController _gameController = Get.find(); - final SettingsController _settingsController = Get.find(); +class SettingsPage extends StatefulWidget { SettingsPage({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingTile( + 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 Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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 + title: "Game server", + description: "This file is injected to create a game server to host matches", + controller: _settingsController.rebootDll ), _createFileSetting( title: "Unreal engine console", @@ -43,115 +56,111 @@ class SettingsPage extends StatelessWidget { controller: _settingsController.authDll ), ], - ), - const SizedBox( - height: 16.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(), - onChanged: (value) => _settingsController.autoUpdate.value = value - )) - ), - const SizedBox( - height: 16.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: 16.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/new/choose")), - child: const Text("Report a bug"), - ) - ), - const SizedBox( - height: 16.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 settings 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: 16.0, - ), - SettingTile( - title: "Version status", - subtitle: "Current version: 7.0", - content: Button( - onPressed: () => launchUrl(installationDirectory.uri), - child: const Text("Show Files"), - ) - ), - ] - ); - - Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => ListTile( - title: Text(title), - subtitle: Text(description), - trailing: SizedBox( - width: 256, - child: Row( - children: [ - Expanded( - child: TextFormBox( - placeholder: "Path", - controller: controller, - validator: checkDll, - autovalidateMode: AutovalidateMode.always - ), - ), - const SizedBox( - width: 8.0, - ), - Padding( - padding: const EdgeInsets.only(bottom: 21.0), - child: Button( - onPressed: () async { - var selected = await compute(openFilePicker, "dll"); - controller.text = selected ?? controller.text; - }, - child: const Icon(FluentIcons.open_folder_horizontal), - ), + ), + const SizedBox( + height: 16.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: 16.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: 16.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: 16.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: 16.0, + ), + SettingTile( + title: "Version status", + subtitle: "Current version: 8.0", + 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/build_selector.dart b/lib/src/ui/widget/home/build_selector.dart index ef3268d..4bf8038 100644 --- a/lib/src/ui/widget/home/build_selector.dart +++ b/lib/src/ui/widget/home/build_selector.dart @@ -4,8 +4,9 @@ import 'package:reboot_launcher/src/ui/controller/build_controller.dart'; import 'package:reboot_launcher/src/model/fortnite_build.dart'; class BuildSelector extends StatefulWidget { + final Function() onSelected; - const BuildSelector({Key? key}) : super(key: key); + const BuildSelector({Key? key, required this.onSelected}) : super(key: key); @override State createState() => _BuildSelectorState(); @@ -18,14 +19,20 @@ class _BuildSelectorState extends State { Widget build(BuildContext context) { return InfoLabel( label: "Build", - child: ComboBox( + child: Obx(() => ComboBox( placeholder: const Text('Select a fortnite build'), isExpanded: true, items: _createItems(), - value: _buildController.selectedBuild, - onChanged: (value) => - value == null ? {} : setState(() => _buildController.selectedBuild = value) - ) + value: _buildController.selectedBuildRx.value, + onChanged: (value) { + if(value == null){ + return; + } + + _buildController.selectedBuildRx.value = value; + widget.onSelected(); + } + )) ); } diff --git a/lib/src/ui/widget/home/launch_button.dart b/lib/src/ui/widget/home/launch_button.dart index 4d5a4b6..ec1cf61 100644 --- a/lib/src/ui/widget/home/launch_button.dart +++ b/lib/src/ui/widget/home/launch_button.dart @@ -24,6 +24,7 @@ import 'package:reboot_launcher/src/../main.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/model/game_instance.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import '../../../util/process.dart'; @@ -37,6 +38,7 @@ class LaunchButton extends StatefulWidget { } class _LaunchButtonState extends State { + static const String _kLoadingRoute = '/loading'; final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()"; final List _corruptedBuildErrors = [ "when 0 bytes remain", @@ -50,6 +52,7 @@ class _LaunchButtonState extends State { "UOnlineAccountCommon::ForceLogout" ]; + final GlobalKey _headlessServerKey = GlobalKey(); final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); final ServerController _serverController = Get.find(); @@ -116,10 +119,8 @@ class _LaunchButtonState extends State { } try { - _fail = false; var version = _gameController.selectedVersion!; - var gamePath = version.executable?.path; - if(gamePath == null){ + if(version.executable?.path == null){ showMissingBuildError(version); _onStop(widget.host); return; @@ -131,7 +132,6 @@ class _LaunchButtonState extends State { return; } - await compute(patchMatchmaking, version.executable!); await compute(patchHeadless, version.executable!); var automaticallyStartedServer = await _startMatchMakingServer(); @@ -141,9 +141,9 @@ class _LaunchButtonState extends State { await _showServerLaunchingWarning(); } } catch (exception, stacktrace) { - _closeDialogIfOpen(false); - showCorruptedBuildError(widget.host, exception, stacktrace); + _closeLaunchingWidget(false); _onStop(widget.host); + showCorruptedBuildError(widget.host, exception, stacktrace); } } @@ -152,7 +152,8 @@ class _LaunchButtonState extends State { var launcherProcess = await _createLauncherProcess(version); var eacProcess = await _createEacProcess(version); var gameProcess = await _createGameProcess(version.executable!.path, host); - var instance = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer); + var watchDogProcess = _createWatchdogProcess(gameProcess, launcherProcess, eacProcess); + var instance = GameInstance(gameProcess, launcherProcess, eacProcess, watchDogProcess, hasChildServer); if(host){ _hostingController.instance = instance; }else{ @@ -161,6 +162,13 @@ class _LaunchButtonState extends State { _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; @@ -226,33 +234,50 @@ class _LaunchButtonState extends State { return; } - _closeDialogIfOpen(false); + _closeLaunchingWidget(false); _onStop(widget.host); } - void _closeDialogIfOpen(bool success) { + void _closeLaunchingWidget(bool success) { + var context = _headlessServerKey.currentContext; + if(context == null || !context.mounted){ + return; + } + var route = ModalRoute.of(appKey.currentContext!); if(route == null || route.isCurrent){ return; } - Navigator.of(appKey.currentContext!).pop(success); + 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: () =>_onEnd() + onStop: () => Navigator.of(context).pop(false) ) ) ?? false; - if(result){ + if(!result){ + _onStop(true); return; } - _onStop(widget.host); + if(!_hostingController.discoverable.value){ + return; + } + + 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' + }); } void _onGameOutput(String line, bool host) { @@ -280,7 +305,7 @@ class _LaunchButtonState extends State { } _fail = true; - _closeDialogIfOpen(false); + _closeLaunchingWidget(false); _showTokenError(host); return; } @@ -290,7 +315,7 @@ class _LaunchButtonState extends State { _injectOrShowError(Injectable.console, host); }else { _injectOrShowError(Injectable.reboot, host) - .then((value) => _closeDialogIfOpen(true)); + .then((value) => _closeLaunchingWidget(true)); } _injectOrShowError(Injectable.memoryFix, host); @@ -340,6 +365,13 @@ class _LaunchButtonState extends State { } _setStarted(host, false); + + if(host){ + var supabase = Supabase.instance.client; + await supabase.from('hosts') + .delete() + .match({'id': _gameController.uuid}); + } } Future _injectOrShowError(Injectable injectable, bool hosting) async { @@ -387,12 +419,8 @@ class _LaunchButtonState extends State { void _onDllFail(File dllPath, bool hosting) { WidgetsBinding.instance.addPostFrameCallback((_) { - if(_fail){ - return; - } - _fail = true; - _closeDialogIfOpen(false); + _closeLaunchingWidget(false); showMissingDllError(path.basename(dllPath.path)); _onStop(hosting); }); diff --git a/lib/src/ui/widget/home/version_name_input.dart b/lib/src/ui/widget/home/version_name_input.dart index 3a4fc56..871bca7 100644 --- a/lib/src/ui/widget/home/version_name_input.dart +++ b/lib/src/ui/widget/home/version_name_input.dart @@ -9,15 +9,16 @@ class VersionNameInput extends StatelessWidget { VersionNameInput({Key? key, required this.controller}) : super(key: key); @override - Widget build(BuildContext context) { - return TextFormBox( - header: "Name", - placeholder: "Type the version's name", - controller: controller, - autofocus: true, - validator: _validate, - ); - } + Widget build(BuildContext context) => InfoLabel( + label: "Name", + child: TextFormBox( + controller: controller, + placeholder: "Type the version's name", + autofocus: true, + validator: _validate, + autovalidateMode: AutovalidateMode.onUserInteraction + ), + ); String? _validate(String? text) { if (text == null || text.isEmpty) { diff --git a/lib/src/ui/widget/home/version_selector.dart b/lib/src/ui/widget/home/version_selector.dart index dc21501..2b07282 100644 --- a/lib/src/ui/widget/home/version_selector.dart +++ b/lib/src/ui/widget/home/version_selector.dart @@ -10,6 +10,7 @@ import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart'; import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart'; +import 'package:reboot_launcher/src/util/os.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart'; @@ -135,12 +136,8 @@ class _VersionSelectorState extends State { } _gameController.removeVersion(version); - if (_gameController.selectedVersion?.name == version.name || _gameController.hasNoVersions) { - _gameController.selectedVersion = null; - } - if (_deleteFilesController.value && await version.location.exists()) { - version.location.delete(recursive: true); + delete(version.location); } break; @@ -213,12 +210,14 @@ class _VersionSelectorState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextFormBox( - controller: nameController, - header: "Name", - placeholder: "Type the new version name", - autofocus: true, - validator: (text) => checkChangeVersion(text) + InfoLabel( + label: "Name", + child: TextFormBox( + controller: nameController, + placeholder: "Type the new version name", + autofocus: true, + validator: (text) => checkChangeVersion(text) + ) ), const SizedBox( @@ -228,6 +227,7 @@ class _VersionSelectorState extends State { FileSelector( placeholder: "Type the new game folder", windowTitle: "Select game folder", + label: "Path", controller: pathController, validator: checkGameFolder, folder: true diff --git a/lib/src/ui/widget/shared/file_selector.dart b/lib/src/ui/widget/shared/file_selector.dart index 2e13b1a..560d448 100644 --- a/lib/src/ui/widget/shared/file_selector.dart +++ b/lib/src/ui/widget/shared/file_selector.dart @@ -48,28 +48,16 @@ class _FileSelectorState extends State { ) : _buildBody; } - Widget get _buildBody => Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: TextFormBox( - controller: widget.controller, - placeholder: widget.placeholder, - validator: widget.validator, - autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction - ) + Widget get _buildBody => TextFormBox( + controller: widget.controller, + placeholder: widget.placeholder, + validator: widget.validator, + autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction, + suffix: !widget.allowNavigator ? null : Button( + onPressed: _onPressed, + child: const Icon(FluentIcons.open_folder_horizontal) ), - if (widget.allowNavigator) - const SizedBox(width: 16.0), - if (widget.allowNavigator) - Padding( - padding: const EdgeInsets.only(bottom: 21.0), - child: Button( - onPressed: _onPressed, - child: const Icon(FluentIcons.open_folder_horizontal) - ) - ) - ], + suffixMode: OverlayVisibilityMode.editing ); void _onPressed() { diff --git a/lib/src/ui/widget/shared/smart_input.dart b/lib/src/ui/widget/shared/smart_input.dart index 5d83629..1ff610d 100644 --- a/lib/src/ui/widget/shared/smart_input.dart +++ b/lib/src/ui/widget/shared/smart_input.dart @@ -26,16 +26,24 @@ class SmartInput extends StatelessWidget { @override Widget build(BuildContext context) { - return TextFormBox( + if(label != null){ + return InfoLabel( + label: label!, + child: _body + ); + } + + return _body; + } + + TextFormBox get _body => TextFormBox( enabled: enabled, controller: controller, - header: label, 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 index 20bbbd9..f654e10 100644 --- a/lib/src/util/build.dart +++ b/lib/src/util/build.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:io'; +import 'dart:isolate'; import 'package:archive/archive_io.dart'; import 'package:html/parser.dart' show parse; @@ -7,7 +9,6 @@ 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 'package:path/path.dart' as path; -import 'package:unrar_file/unrar_file.dart'; import 'os.dart'; @@ -44,21 +45,28 @@ Future> fetchBuilds(ignored) async { return results; } -Future downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function() onRar) async { - var outputDir = await destination.createTemp("build"); + +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"); - var startTime = DateTime.now().millisecondsSinceEpoch; + var tempFile = File("${outputDir.path}\\$fileName"); + if(tempFile.existsSync()) { + tempFile.deleteSync(recursive: true); + } + var client = http.Client(); - var response = await client.send( - http.Request("GET", Uri.parse(archiveUrl))); + 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(); @@ -69,17 +77,66 @@ Future downloadArchiveBuild(String archiveUrl, Directory destination, Func onProgress((received / length) * 100, toETA(eta)); return s; }).pipe(sink); - onRar(); - if(extension.toLowerCase() == ".zip"){ - await extractFileToDisk(tempFile.path, destination.path); - }else if(extension.toLowerCase() == ".rar") { - await UnrarFile.extract_rar(tempFile.path, destination.path); - } else { - throw Exception("Unknown file extension: $extension"); - } + + 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"); - }finally { - outputDir.delete(recursive: true); } +} + +// 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 1a41a36..7b578af 100644 --- a/lib/src/util/checks.dart +++ b/lib/src/util/checks.dart @@ -68,5 +68,13 @@ String? checkMatchmaking(String? text) { return "Empty hostname"; } + return null; +} + +String? checkUpdateUrl(String? text) { + if (text == null || text.isEmpty) { + return "Empty URL"; + } + return null; } \ No newline at end of file diff --git a/lib/src/util/error.dart b/lib/src/util/error.dart index edee107..377242c 100644 --- a/lib/src/util/error.dart +++ b/lib/src/util/error.dart @@ -3,6 +3,9 @@ import 'package:fluent_ui/fluent_ui.dart'; import '../../../main.dart'; import '../ui/dialog/dialog.dart'; + +String? lastError; + void onError(Object? exception, StackTrace? stackTrace, bool framework) { if(exception == null){ return; @@ -12,6 +15,16 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) { return; } + if(lastError == exception.toString()){ + return; + } + + lastError = exception.toString(); + var route = ModalRoute.of(appKey.currentContext!); + if(route != null && !route.isCurrent){ + Navigator.of(appKey.currentContext!).pop(false); + } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog( context: appKey.currentContext!, builder: (context) => diff --git a/lib/src/util/os.dart b/lib/src/util/os.dart index 955b254..30201b8 100644 --- a/lib/src/util/os.dart +++ b/lib/src/util/os.dart @@ -4,11 +4,12 @@ import 'package:win32/win32.dart'; import 'package:ffi/ffi.dart'; import 'dart:ffi'; -import 'package:path/path.dart' as path; 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"; + bool get isWin11 { var result = _regex.firstMatch(Platform.operatingSystemVersion)?.group(1); if(result == null){ @@ -19,6 +20,33 @@ bool get isWin11 { 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(); diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index 3681b62..8c765ac 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -12,8 +12,8 @@ final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll"); Future downloadRebootDll(String url, int? lastUpdateMs) async { Directory? outputDir; + var now = DateTime.now(); try { - var now = DateTime.now(); var lastUpdate = await _getLastUpdate(lastUpdateMs); var exists = await rebootDllFile.exists(); if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) { @@ -32,6 +32,12 @@ Future downloadRebootDll(String url, int? lastUpdateMs) async { 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) { diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index 32f995b..d98369d 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -36,7 +36,8 @@ Future startServer(bool detached) async { serverExeFile.path, [], workingDirectory: serverDirectory.path, - mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal + mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal, + runInShell: detached ); if(!detached) { serverLogFile.createSync(recursive: true); @@ -156,7 +157,14 @@ Future checkServerPreconditions(String host, String port, ServerTy ); } - if(int.tryParse(port) == null){ + 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 ); @@ -179,9 +187,7 @@ Future checkServerPreconditions(String host, String port, ServerTy ); } -Future startRemoteServer(Uri uri) async { - return await serve(proxyHandler(uri), "127.0.0.1", 3551); -} +Future startRemoteServer(Uri uri) async => await serve(proxyHandler(uri), "127.0.0.1", 3551); class ServerResult { final int? pid; diff --git a/lib/supabase.dart b/lib/supabase.dart new file mode 100644 index 0000000..6e6378f --- /dev/null +++ b/lib/supabase.dart @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..593c720 --- /dev/null +++ b/lib/watch.dart @@ -0,0 +1,51 @@ +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 882d152..a8383f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "7.0.0" +version: "8.0.0" publish_to: 'none' @@ -13,7 +13,7 @@ dependencies: bitsdojo_window: path: ./dependencies/bitsdojo_window-0.1.5 - fluent_ui: ^4.1.3 + fluent_ui: ^4.6.2 bitsdojo_window_windows: ^0.1.5 system_theme: ^2.0.0 http: ^0.13.5 @@ -40,7 +40,8 @@ dependencies: jaguar: ^3.1.3 hex: ^0.2.0 uuid: ^3.0.6 - unrar_file: ^1.1.0 + supabase_flutter: ^1.10.0 + supabase: ^1.9.1 dev_dependencies: flutter_test: @@ -48,11 +49,13 @@ dev_dependencies: flutter_lints: ^2.0.1 msix: ^3.6.3 + flutter_distributor: ^0.3.4 flutter: uses-material-design: true assets: - assets/builds/ + - assets/browse/ - assets/dlls/ - assets/icons/ - assets/images/ @@ -67,7 +70,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 7.0.0.0 + msix_version: 8.0.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64 diff --git a/release/release.bat b/release/release.bat index b8f0640..16dc7ae 100644 --- a/release/release.bat +++ b/release/release.bat @@ -1,3 +1,4 @@ +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/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 731d46f..ae6c766 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 7993f64..b1095ae 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links bitsdojo_window_windows screen_retriever system_theme diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index b9e550f..17411a8 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -20,6 +20,13 @@ add_executable(${BINARY_NAME} WIN32 # that need different build settings. apply_standard_settings(${BINARY_NAME}) +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index eddb8d2..726f74e 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif