diff --git a/assets/binaries/build.exe b/assets/binaries/build.exe index d5870c7..a460e20 100644 Binary files a/assets/binaries/build.exe and b/assets/binaries/build.exe differ diff --git a/lib/cli.dart b/lib/cli.dart index 25621d9..49ef029 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -7,12 +7,11 @@ import 'package:process_run/shell.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/model/game_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/injector.dart'; -import 'package:reboot_launcher/src/util/node.dart'; import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/reboot.dart'; -import 'package:reboot_launcher/src/util/server_standalone.dart'; +import 'package:reboot_launcher/src/util/server.dart'; import 'package:shelf_proxy/shelf_proxy.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; import 'dart:ffi'; @@ -23,8 +22,8 @@ import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:http/http.dart' as http; // Needed because binaries can't be loaded in any other way -const String _craniumDownload = "https://filebin.net/ybn0gme7dqjr4zup/cranium.dll"; -const String _consoleDownload = "https://filebin.net/ybn0gme7dqjr4zup/console.dll"; +const String _craniumDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848323825675/cranium.dll"; +const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll"; Process? _gameProcess; Process? _eacProcess; @@ -121,13 +120,12 @@ Future handleCLI(List args) async { stdout.writeln("Launching game(type: ${type.name})..."); await _startLauncherProcess(dummyVersion); - await _startEacProcess(dummyVersion); if (result["type"] == "headless_server") { if(dummyVersion.executable == null){ throw Exception("Missing game executable at: ${dummyVersion.location.path}"); } - await patchExe(dummyVersion.executable!); + await patch(dummyVersion.executable!); } var serverType = _getServerType(result); @@ -249,15 +247,6 @@ void _onClose() { exit(0); } -Future _startEacProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.eacExecutable == null) { - return; - } - - _eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []); - Win32Process(_eacProcess!.pid).suspend(); -} - Future _startLauncherProcess(FortniteVersion dummyVersion) async { if (dummyVersion.launcher == null) { return; @@ -296,33 +285,7 @@ Future _startServerIfNeeded(String? host, String? port, ServerType type) a } Future _changeEmbeddedServerState() async { - var node = await hasNode(); - if(!node) { - throw Exception("Missing node, cannot start embedded server"); - } - var free = await isLawinPortFree(); - if(!free){ - stdout.writeln("Server is already running on port 3551"); - return true; - } - - if(!serverLocation.existsSync()) { - await downloadServer(false); - } - - var serverRunner = File("${serverLocation.path}/start.bat"); - if (!(await serverRunner.exists())) { - return false; - } - - var nodeModules = Directory("${serverLocation.path}/node_modules"); - if (!(await nodeModules.exists())) { - await Process.run("${serverLocation.path}/install_packages.bat", [], - workingDirectory: serverLocation.path); - } - - await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path); return true; } diff --git a/lib/main.dart b/lib/main.dart index 5da556d..526e7a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,10 +12,11 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/page/home_page.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:system_theme/system_theme.dart'; +final GlobalKey appKey = GlobalKey(); + void main(List args) async { await Directory(safeBinariesDirectory) .create(recursive: true); @@ -55,8 +56,6 @@ class RebootApplication extends StatefulWidget { } class _RebootApplicationState extends State { - final SettingsController _settingsController = Get.find(); - @override Widget build(BuildContext context) { final color = SystemTheme.accentColor.accent.toAccentColor(); @@ -67,7 +66,7 @@ class _RebootApplicationState extends State { color: color, darkTheme: _createTheme(Brightness.dark), theme: _createTheme(Brightness.light), - home: const HomePage(), + home: HomePage(key: appKey), ); } diff --git a/lib/src/controller/game_controller.dart b/lib/src/controller/game_controller.dart index 38f9bb4..d44e1df 100644 --- a/lib/src/controller/game_controller.dart +++ b/lib/src/controller/game_controller.dart @@ -89,4 +89,8 @@ class GameController extends GetxController { _selectedVersion(version); _storage.write("version", version?.name); } + + void rename(FortniteVersion version, String result) { + versions.update((val) => version.name = result); + } } diff --git a/lib/src/controller/server_controller.dart b/lib/src/controller/server_controller.dart index 01a33bc..bf95751 100644 --- a/lib/src/controller/server_controller.dart +++ b/lib/src/controller/server_controller.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/server.dart'; import '../model/server_type.dart'; @@ -17,6 +17,7 @@ class ServerController extends GetxController { late final Rx type; late final RxBool warning; late RxBool started; + Process? embeddedServer; HttpServer? reverseProxy; ServerController() { @@ -39,9 +40,7 @@ class ServerController extends GetxController { return; } - loadBinary("release.bat", false) - .then((value) => Process.run(value.path, [])) - .then((value) => started(false)); + stop(); }); host = TextEditingController(text: _readHost()); @@ -65,4 +64,69 @@ class ServerController extends GetxController { String _readPort() { return _storage.read("${type.value.id}_port") ?? _serverPort; } + + Future start() async { + var result = await checkServerPreconditions(host.text, port.text, type.value); + if(result.type != ServerResultType.canStart){ + return result; + } + + try{ + switch(type()){ + case ServerType.embedded: + embeddedServer = await startEmbeddedServer(); + break; + case ServerType.remote: + var uriResult = await result.uri!; + if(uriResult == null){ + return ServerResult( + type: ServerResultType.cannotPingServer + ); + } + + reverseProxy = await startRemoteServer(uriResult); + break; + case ServerType.local: + break; + } + }catch(error, stackTrace){ + return ServerResult( + error: error, + stackTrace: stackTrace, + type: ServerResultType.unknownError + ); + } + + var myself = await pingSelf(); + if(myself == null){ + return ServerResult( + type: ServerResultType.cannotPingServer + ); + } + + started(true); + return ServerResult( + type: ServerResultType.started + ); + } + + Future stop() async { + started(false); + try{ + switch(type()){ + case ServerType.embedded: + await freeLawinPort(); + break; + case ServerType.remote: + await reverseProxy?.close(force: true); + break; + case ServerType.local: + break; + } + return true; + }catch(_){ + started(true); + return false; + } + } } \ No newline at end of file diff --git a/lib/src/controller/settings_controller.dart b/lib/src/controller/settings_controller.dart index db82931..c0e2cc8 100644 --- a/lib/src/controller/settings_controller.dart +++ b/lib/src/controller/settings_controller.dart @@ -6,7 +6,7 @@ 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'; -import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/os.dart'; import 'package:system_theme/system_theme.dart'; class SettingsController extends GetxController { diff --git a/lib/src/dialog/add_local_version.dart b/lib/src/dialog/add_local_version.dart new file mode 100644 index 0000000..056a255 --- /dev/null +++ b/lib/src/dialog/add_local_version.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +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/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; + +import '../util/checks.dart'; +import '../widget/os/file_selector.dart'; + +class AddLocalVersion extends StatelessWidget { + final GameController _gameController = Get.find(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _gamePathController = TextEditingController(); + + AddLocalVersion({Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return FormDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormBox( + controller: _nameController, + header: "Name", + placeholder: "Type the version's name", + autofocus: true, + validator: (text) => checkVersion(text, _gameController.versions.value) + ), + + const SizedBox( + height: 16.0 + ), + + FileSelector( + label: "Location", + placeholder: "Type the game folder", + windowTitle: "Select game folder", + controller: _gamePathController, + validator: checkGameFolder, + folder: true + ), + + const SizedBox(height: 8.0), + ], + ), + buttons: [ + DialogButton( + type: ButtonType.secondary + ), + + DialogButton( + text: "Save", + type: ButtonType.primary, + onTap: () { + _gameController.addVersion(FortniteVersion( + name: _nameController.text, + location: Directory(_gamePathController.text))); + }, + ) + ] + ); + } +} diff --git a/lib/src/dialog/add_server_version.dart b/lib/src/dialog/add_server_version.dart new file mode 100644 index 0000000..21dc48e --- /dev/null +++ b/lib/src/dialog/add_server_version.dart @@ -0,0 +1,323 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; +import 'package:reboot_launcher/src/util/os.dart'; +import 'package:reboot_launcher/src/util/build.dart'; +import 'package:reboot_launcher/src/widget/home/version_name_input.dart'; + +import '../util/checks.dart'; +import '../widget/home/build_selector.dart'; +import '../widget/os/file_selector.dart'; +import 'dialog.dart'; + +class AddServerVersion extends StatefulWidget { + const AddServerVersion({Key? key}) : super(key: key); + + @override + State createState() => _AddServerVersionState(); +} + +class _AddServerVersionState extends State { + final GameController _gameController = Get.find(); + final BuildController _buildController = Get.find(); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _pathController = TextEditingController(); + late Future _future; + DownloadStatus _status = DownloadStatus.none; + String _timeLeft = "00:00:00"; + double _downloadProgress = 0; + String? _error; + Process? _manifestDownloadProcess; + CancelableOperation? _driveDownloadOperation; + + @override + void initState() { + _future = _buildController.builds != null + ? Future.value(true) + : compute(fetchBuilds, null) + .then((value) => _buildController.builds = value); + super.initState(); + } + + @override + void dispose() { + _pathController.dispose(); + _nameController.dispose(); + _onDisposed(); + super.dispose(); + } + + void _onDisposed() { + if (_status != DownloadStatus.downloading && + _status != DownloadStatus.extracting) { + return; + } + + if (_manifestDownloadProcess != null) { + loadBinary("stop.bat", true).then( + (value) => Process.runSync(value.path, [])); + _buildController.cancelledDownload(true); + return; + } + + if (_driveDownloadOperation == null) { + return; + } + + _driveDownloadOperation!.cancel(); + _buildController.cancelledDownload(true); + } + + @override + Widget build(BuildContext context) { + return FormDialog( + content: _createDownloadVersionBody(), + buttons: _createDownloadVersionOption(context) + ); + } + + List _createDownloadVersionOption(BuildContext context) { + switch (_status) { + case DownloadStatus.none: + return [ + DialogButton(type: ButtonType.secondary), + DialogButton( + text: "Download", + type: ButtonType.primary, + onTap: () => _startDownload(context), + ) + ]; + + case DownloadStatus.error: + return [DialogButton(type: ButtonType.only)]; + default: + return [ + DialogButton( + text: _status == DownloadStatus.downloading ? "Stop" : "Close", + type: ButtonType.only) + ]; + } + } + + void _startDownload(BuildContext context) async { + try { + setState(() => _status = DownloadStatus.downloading); + if (_buildController.selectedBuild.hasManifest) { + _manifestDownloadProcess = await downloadManifestBuild( + _buildController.selectedBuild.link, + _pathController.text, + _onDownloadProgress + ); + _manifestDownloadProcess!.exitCode + .then((value) => _onDownloadComplete()); + } else { + _driveDownloadOperation = CancelableOperation.fromFuture( + downloadArchiveBuild( + _buildController.selectedBuild.link, + _pathController.text, + (progress) => _onDownloadProgress(progress, _timeLeft), + _onUnrar) + ).then((_) => _onDownloadComplete(), + onError: (error, _) => _handleError(error)); + } + } catch (exception) { + _handleError(exception); + } + } + + FutureOr? _handleError(Object exception) { + var message = exception.toString(); + _onDownloadError(message.contains(":") + ? " ${message.substring(message.indexOf(":") + 1)}" + : message); + return null; + } + + void _onUnrar() { + setState(() => _status = DownloadStatus.extracting); + } + + void _onDownloadComplete() { + if (!mounted) { + return; + } + + setState(() { + _status = DownloadStatus.done; + _gameController.addVersion(FortniteVersion( + name: _nameController.text, + location: Directory(_pathController.text))); + }); + } + + void _onDownloadError(String message) { + if (!mounted) { + return; + } + + setState(() { + _status = DownloadStatus.error; + _error = message; + }); + } + + void _onDownloadProgress(double progress, String timeLeft) { + if (!mounted) { + return; + } + + setState(() { + _status = DownloadStatus.downloading; + _timeLeft = timeLeft; + _downloadProgress = progress; + }); + } + + Widget _createDownloadVersionBody() { + return FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.hasError) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() => _status = DownloadStatus.error)); + return Container( + width: double.infinity, + padding: const EdgeInsets.only(bottom: 16.0), + child: Text("Cannot fetch builds: ${snapshot.error}", + textAlign: TextAlign.center), + ); + } + + if (!snapshot.hasData) { + return InfoLabel( + label: "Fetching builds...", + child: Container( + padding: const EdgeInsets.only(bottom: 16.0), + width: double.infinity, + child: const ProgressBar()), + ); + } + + return _buildBody(); + }); + } + + Widget _buildBody() { + switch (_status) { + case DownloadStatus.none: + return _createNoneBody(); + case DownloadStatus.downloading: + return _createDownloadBody(); + case DownloadStatus.extracting: + return _createExtractingBody(); + case DownloadStatus.done: + return _createDoneBody(); + case DownloadStatus.error: + return _createErrorBody(); + } + } + + Padding _createErrorBody() { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: SizedBox( + width: double.infinity, + child: Text("An error was occurred while downloading:$_error", + textAlign: TextAlign.center)), + ); + } + + Padding _createDoneBody() { + return const Padding( + padding: EdgeInsets.only(bottom: 16), + child: SizedBox( + width: double.infinity, + child: Text("The download was completed successfully!", + textAlign: TextAlign.center)), + ); + } + + Padding _createExtractingBody() { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: InfoLabel( + label: "Extracting...", + child: const SizedBox(width: double.infinity, child: ProgressBar())), + ); + } + + Column _createDownloadBody() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Downloading...", + style: FluentTheme.maybeOf(context)?.typography.body, + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 8, + ), + if(_manifestDownloadProcess != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${_downloadProgress.round()}%", + style: FluentTheme.maybeOf(context)?.typography.body, + ), + Text( + "Time left: $_timeLeft", + style: FluentTheme.maybeOf(context)?.typography.body, + ), + ], + ), + if(_manifestDownloadProcess != null) + const SizedBox( + height: 8, + ), + SizedBox( + width: double.infinity, + child: ProgressBar(value: _downloadProgress.toDouble())), + const SizedBox( + height: 16, + ) + ], + ); + } + + Column _createNoneBody() { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const BuildSelector(), + const SizedBox(height: 16.0), + VersionNameInput(controller: _nameController), + const SizedBox(height: 16.0), + FileSelector( + label: "Destination", + placeholder: "Type the download destination", + windowTitle: "Select download destination", + controller: _pathController, + validator: checkDownloadDestination, + folder: true), + const SizedBox(height: 8.0), + ], + ); + } +} + +enum DownloadStatus { none, downloading, extracting, error, done } diff --git a/lib/src/dialog/dialog.dart b/lib/src/dialog/dialog.dart new file mode 100644 index 0000000..8979b7a --- /dev/null +++ b/lib/src/dialog/dialog.dart @@ -0,0 +1,229 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:reboot_launcher/src/dialog/snackbar.dart'; + +import 'dialog_button.dart'; + +abstract class AbstractDialog extends StatelessWidget { + const AbstractDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context); +} + +class GenericDialog extends AbstractDialog { + final Widget header; + final List buttons; + final EdgeInsets? padding; + + const GenericDialog({super.key, required this.header, required this.buttons, this.padding}); + + @override + Widget build(BuildContext context) { + return ContentDialog( + style: ContentDialogThemeData( + padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) + ), + content: header, + actions: buttons + ); + } +} + +class FormDialog extends AbstractDialog { + final Widget content; + final List buttons; + + const FormDialog({super.key, required this.content, required this.buttons}); + + @override + Widget build(BuildContext context) { + return Form( + child: Builder( + builder: (context) { + var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList(); + return GenericDialog( + header: content, + buttons: parsed + ); + } + ) + ); + } + + DialogButton _createFormButton(DialogButton entry, BuildContext context) { + if (entry.type == ButtonType.secondary) { + return entry; + } + + return DialogButton( + text: entry.text, + type: entry.type, + onTap: () { + if(!Form.of(context)!.validate()) { + return; + } + + entry.onTap?.call(); + } + ); + } +} + +class InfoDialog extends AbstractDialog { + final String text; + final List? buttons; + + const InfoDialog({required this.text, this.buttons, super.key}); + + InfoDialog.ofOnly({required this.text, required DialogButton button, super.key}) + : buttons = [button]; + + @override + Widget build(BuildContext context) { + return GenericDialog( + header: SizedBox( + width: double.infinity, + child: Text(text, textAlign: TextAlign.center) + ), + buttons: buttons ?? [_createDefaultButton()], + padding: const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 15.0) + ); + } + + DialogButton _createDefaultButton() { + return DialogButton( + text: "Close", + type: ButtonType.only + ); + } +} + +class ProgressDialog extends AbstractDialog { + final String text; + + const ProgressDialog({required this.text, super.key}); + + @override + Widget build(BuildContext context) { + return GenericDialog( + header: InfoLabel( + label: text, + child: Container( + padding: const EdgeInsets.only(bottom: 16.0), + width: double.infinity, + child: const ProgressBar() + ), + ), + buttons: [ + DialogButton( + text: "Close", + type: ButtonType.only + ) + ] + ); + } +} + +class FutureBuilderDialog extends AbstractDialog { + final Future future; + final String loadingMessage; + final Widget loadedBody; + final Function(Object) errorMessageBuilder; + final Function()? onError; + final bool closeAutomatically; + + const FutureBuilderDialog( + {super.key, + required this.future, + required this.loadingMessage, + required this.loadedBody, + required this.errorMessageBuilder, + this.onError, + this.closeAutomatically = false}); + + static Container ofMessage(String message) { + return Container( + width: double.infinity, + padding: const EdgeInsets.only(bottom: 16.0), + child: Text( + message, + textAlign: TextAlign.center + ) + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: future, + builder: (context, snapshot) => GenericDialog( + header: _createBody(context, snapshot), + buttons: [_createButton(context, snapshot)] + ) + ); + } + + Widget _createBody(BuildContext context, AsyncSnapshot snapshot){ + if (snapshot.hasError) { + onError?.call(); + return ofMessage(snapshot.error.toString()); + } + + if (!snapshot.hasData) { + return InfoLabel( + label: loadingMessage, + child: Container( + padding: const EdgeInsets.only(bottom: 16.0), + width: double.infinity, + child: const ProgressBar()), + ); + } + + if(closeAutomatically){ + Navigator.of(context).pop(true); + } + + return loadedBody; + } + + DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot){ + return DialogButton( + text: snapshot.hasData || snapshot.hasError ? "Close" : "Stop", + type: ButtonType.only, + onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData) + ); + } +} + +class ErrorDialog extends AbstractDialog { + final Object exception; + final StackTrace? stackTrace; + final Function(Object) errorMessageBuilder; + + const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace}); + + @override + Widget build(BuildContext context) { + return InfoDialog( + text: errorMessageBuilder(exception), + buttons: [ + DialogButton( + type: stackTrace == null ? ButtonType.only : ButtonType.secondary + ), + + if(stackTrace != null) + DialogButton( + text: "Copy error", + type: ButtonType.primary, + onTap: () async { + FlutterClipboard.controlC("An error occurred: $exception\nStacktrace:\n $stackTrace.toString"); + Navigator.of(context).pop(); + showMessage("Copied error to clipboard"); + }, + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/src/dialog/dialog_button.dart b/lib/src/dialog/dialog_button.dart new file mode 100644 index 0000000..fcc37b9 --- /dev/null +++ b/lib/src/dialog/dialog_button.dart @@ -0,0 +1,62 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class DialogButton extends StatefulWidget { + final String? text; + final Function()? onTap; + final ButtonType type; + + const DialogButton( + {Key? key, + this.text, + this.onTap, + required this.type}) + : assert(type != ButtonType.primary || onTap != null, + "OnTap handler cannot be null for primary buttons"), + assert(type != ButtonType.primary || text != null, + "Text cannot be null for primary buttons"), + super(key: key); + + @override + State createState() => _DialogButtonState(); +} + +class _DialogButtonState extends State { + @override + Widget build(BuildContext context) { + return widget.type == ButtonType.only ? _createOnlyButton() : _createButton(); + } + + SizedBox _createOnlyButton() { + return SizedBox( + width: double.infinity, + child: _createButton() + ); + } + + Widget _createButton() { + return widget.type == ButtonType.primary ? _createPrimaryActionButton() + : _createSecondaryActionButton(); + } + + Widget _createPrimaryActionButton() { + return FilledButton( + onPressed: widget.onTap!, + child: Text(widget.text!), + ); + } + + Widget _createSecondaryActionButton() { + return Button( + onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, + child: Text(widget.text ?? "Close"), + ); + } + + void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null); +} + +enum ButtonType { + primary, + secondary, + only +} diff --git a/lib/src/dialog/game_dialogs.dart b/lib/src/dialog/game_dialogs.dart new file mode 100644 index 0000000..c1dafc2 --- /dev/null +++ b/lib/src/dialog/game_dialogs.dart @@ -0,0 +1,39 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; + +Future showBrokenError() async { + showDialog( + context: appKey.currentContext!, + builder: (context) => const InfoDialog( + text: "The lawin server is not working correctly" + ) + ); +} + +Future showMissingDllError(String name) async { + showDialog( + context: appKey.currentContext!, + builder: (context) => InfoDialog( + text: "$name dll is not a valid dll, fix it in the settings tab" + ) + ); +} + +Future showTokenError() async { + showDialog( + context: appKey.currentContext!, + builder: (context) => const InfoDialog( + text: "A token error occurred, restart the game and the lawin server, then try again" + ) + ); +} + +Future showUnsupportedHeadless() async { + showDialog( + context: appKey.currentContext!, + builder: (context) => const InfoDialog( + text: "This version of Fortnite doesn't support headless hosting" + ) + ); +} \ No newline at end of file diff --git a/lib/src/dialog/server_dialogs.dart b/lib/src/dialog/server_dialogs.dart new file mode 100644 index 0000000..21217a7 --- /dev/null +++ b/lib/src/dialog/server_dialogs.dart @@ -0,0 +1,229 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/dialog/snackbar.dart'; +import 'package:reboot_launcher/src/model/server_type.dart'; +import 'package:reboot_launcher/src/util/future.dart'; +import 'package:sync/semaphore.dart'; + +import '../../main.dart'; +import '../util/server.dart'; + +extension ServerControllerDialog on ServerController { + static Semaphore semaphore = Semaphore(); + + Future changeStateInteractive(bool ignorePrompts, [bool isRetry = false]) async { + try { + semaphore.acquire(); + if (type() == ServerType.local) { + return _checkLocalServerInteractive(ignorePrompts); + } + + var oldStarted = started(); + started(!started()); + if (oldStarted) { + var result = await stop(); + if (!result) { + started(true); + _showCannotStopError(); + return true; + } + + return false; + } + + var result = await start(); + var handled = await _handleResultType(result, ignorePrompts, isRetry); + if (!handled) { + started(false); + return false; + } + + embeddedServer?.exitCode.then((value) { + if (!started()) { + return; + } + + _showUnexpectedError(); + started(false); + }); + + return handled; + }finally{ + semaphore.release(); + } + } + + Future _handleResultType(ServerResult result, bool ignorePrompts, bool isRetry) async { + switch (result.type) { + case ServerResultType.missingHostError: + _showMissingHostError(); + return false; + case ServerResultType.missingPortError: + _showMissingPortError(); + return false; + case ServerResultType.illegalPortError: + _showIllegalPortError(); + return false; + case ServerResultType.cannotPingServer: + _showPingErrorDialog(); + return false; + case ServerResultType.portTakenError: + if (isRetry) { + _showPortTakenError(); + return false; + } + + var result = await _showPortTakenDialog(); + if (!result) { + return false; + } + + await freeLawinPort(); + return changeStateInteractive(ignorePrompts, true); + case ServerResultType.serverDownloadRequiredError: + if (isRetry) { + return false; + } + + var result = await _downloadServerInteractive(); + if (!result) { + return false; + } + + return changeStateInteractive(ignorePrompts, true); + case ServerResultType.unknownError: + showDialog( + context: appKey.currentContext!, + builder: (context) => + ErrorDialog( + exception: result.error ?? Exception("Unknown error"), + stackTrace: result.stackTrace, + errorMessageBuilder: ( + exception) => "Cannot start server: $exception" + ) + ); + return false; + case ServerResultType.started: + return true; + case ServerResultType.canStart: + case ServerResultType.stopped: + return false; + } + } + + Future _checkLocalServerInteractive(bool ignorePrompts) async { + try { + var future = pingSelf(); + if(!ignorePrompts) { + await showDialog( + context: appKey.currentContext!, + builder: (context) => + FutureBuilderDialog( + future: future, + loadingMessage: "Pinging server...", + loadedBody: FutureBuilderDialog.ofMessage( + "The server at ${host.text}:${port + .text} works correctly"), + errorMessageBuilder: ( + exception) => "An error occurred while pining the server: $exception" + ) + ); + } + return await future != null; + } catch (_) { + return false; + } + } + + Future _downloadServerInteractive() async { + var download = compute(downloadServer, true); + return await showDialog( + context: appKey.currentContext!, + builder: (context) => + FutureBuilderDialog( + future: download, + loadingMessage: "Downloading server...", + loadedBody: FutureBuilderDialog.ofMessage( + "The server was downloaded successfully"), + errorMessageBuilder: ( + message) => "Cannot download server: $message" + ) + ) ?? download.isCompleted(); + } + + Future _showPortTakenError() async { + showDialog( + context: appKey.currentContext!, + builder: (context) => + const InfoDialog( + text: "Port 3551 is already in use and the associating process cannot be killed. Kill it manually and try again.", + ) + ); + } + + Future _showPortTakenDialog() async { + return await showDialog( + context: appKey.currentContext!, + builder: (context) => + InfoDialog( + text: "Port 3551 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 _showPingErrorDialog() { + showDialog( + context: appKey.currentContext!, + builder: (context) => + const InfoDialog( + text: "The lawin server is not working correctly. Check the configuration in the associated tab and try again." + ) + ); + } + + void _showCannotStopError() { + showDialog( + context: appKey.currentContext!, + builder: (context) => + const InfoDialog( + text: "Cannot stop lawin server" + ) + ); + } + + void _showUnexpectedError() { + showDialog( + context: appKey.currentContext!, + builder: (context) => + const InfoDialog( + text: "The lawin terminated died unexpectedly" + ) + ); + } + + void _showIllegalPortError() { + showMessage("Illegal port for lawin server, use only numbers"); + } + + void _showMissingPortError() { + showMessage("Missing port for lawin server"); + } + + void _showMissingHostError() { + showMessage("Missing the host name for lawin server"); + } +} \ No newline at end of file diff --git a/lib/src/dialog/snackbar.dart b/lib/src/dialog/snackbar.dart new file mode 100644 index 0000000..b7f76d7 --- /dev/null +++ b/lib/src/dialog/snackbar.dart @@ -0,0 +1,13 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../main.dart'; + +void showMessage(String text){ + showSnackbar( + appKey.currentContext!, + Snackbar( + content: Text(text, textAlign: TextAlign.center), + extended: true + ) + ); +} \ No newline at end of file diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index 8ecfb0f..33dbc3b 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -30,10 +30,6 @@ class FortniteVersion { return findExecutable(location, "FortniteLauncher.exe"); } - File? get eacExecutable { - return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe"); - } - Map toJson() => { 'name': name, 'location': location.path, diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart index 990d0f1..466b883 100644 --- a/lib/src/page/home_page.dart +++ b/lib/src/page/home_page.dart @@ -5,10 +5,12 @@ import 'package:reboot_launcher/src/page/settings_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/server_page.dart'; import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/widget/window_border.dart'; -import 'package:reboot_launcher/src/widget/window_buttons.dart'; +import 'package:reboot_launcher/src/widget/os/window_border.dart'; +import 'package:reboot_launcher/src/widget/os/window_buttons.dart'; import 'package:window_manager/window_manager.dart'; +import 'info_page.dart'; + class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -18,9 +20,9 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with WindowListener { static const double _headerSize = 48.0; - static const double _sectionSize = 97.0; + static const double _sectionSize = 94.0; static const int _headerButtonCount = 3; - static const int _sectionButtonCount = 3; + static const int _sectionButtonCount = 4; bool _focused = true; bool _shouldMaximize = false; @@ -78,6 +80,12 @@ class _HomePageState extends State with WindowListener { title: const Text("Settings"), icon: const Icon(FluentIcons.settings), body: SettingsPage() + ), + + PaneItem( + title: const Text("Info"), + icon: const Icon(FluentIcons.info), + body: const InfoPage() ) ] ), @@ -104,8 +112,8 @@ class _HomePageState extends State with WindowListener { Padding _createGestureHandler() { return Padding( padding: const EdgeInsets.only( - left: _sectionSize * _headerButtonCount, - right: _headerSize * _sectionButtonCount, + left: _sectionSize * _sectionButtonCount, + right: _headerSize * _headerButtonCount, ), child: SizedBox( height: _headerSize, diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart new file mode 100644 index 0000000..c94cf92 --- /dev/null +++ b/lib/src/page/info_page.dart @@ -0,0 +1,61 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const String _discordLink = "https://discord.gg/NJU4QjxSMF"; + +class InfoPage extends StatelessWidget { + const InfoPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + _createVersionInfo(), + + Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + _createAutiesAvatar(), + const SizedBox( + height: 16.0, + ), + const Text("Made by Auties00"), + const SizedBox( + height: 16.0, + ), + _createDiscordButton() + ], + ), + ], + ) + ), + ); + } + + Button _createDiscordButton() { + return Button( + child: const Text("Join the discord"), + onPressed: () => launchUrl(Uri.parse(_discordLink))); + } + + CircleAvatar _createAutiesAvatar() { + return const CircleAvatar( + radius: 48, + backgroundImage: AssetImage("assets/images/auties.png")); + } + + Align _createVersionInfo() { + return const Align( + alignment: Alignment.bottomRight, + child: Text("Version 4.0${kDebugMode ? '-DEBUG' : ''}") + ); + } +} \ No newline at end of file diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 0d01270..1863748 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -7,16 +7,15 @@ 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/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:reboot_launcher/src/widget/home/game_type_selector.dart'; +import 'package:reboot_launcher/src/widget/home/launch_button.dart'; +import 'package:reboot_launcher/src/widget/home/username_box.dart'; +import 'package:reboot_launcher/src/widget/home/version_selector.dart'; import 'package:url_launcher/url_launcher.dart'; import '../controller/settings_controller.dart'; -import '../util/binary.dart'; import '../util/reboot.dart'; -import '../widget/warning_info.dart'; +import '../widget/shared/warning_info.dart'; class LauncherPage extends StatefulWidget { const LauncherPage( @@ -59,6 +58,7 @@ class _LauncherPageState extends State { var errorFile = await loadBinary("error.txt", true); errorFile.writeAsString( "Error: $error\nStacktrace: $stackTrace", mode: FileMode.write); + throw Exception("Cannot update reboot.dll"); } void _onCancelWarning() { @@ -104,7 +104,7 @@ class _LauncherPageState extends State { _createUpdateError(snapshot), UsernameBox(), const VersionSelector(), - DeploymentSelector(), + GameTypeSelector(), const LaunchButton() ], ); @@ -115,11 +115,10 @@ class _LauncherPageState extends State { 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)) + 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/page/server_page.dart b/lib/src/page/server_page.dart index ecf2935..d03dcdf 100644 --- a/lib/src/page/server_page.dart +++ b/lib/src/page/server_page.dart @@ -1,11 +1,11 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; -import 'package:reboot_launcher/src/widget/host_input.dart'; -import 'package:reboot_launcher/src/widget/local_server_switch.dart'; -import 'package:reboot_launcher/src/widget/port_input.dart'; -import 'package:reboot_launcher/src/widget/server_button.dart'; -import 'package:reboot_launcher/src/widget/warning_info.dart'; +import 'package:reboot_launcher/src/widget/server/host_input.dart'; +import 'package:reboot_launcher/src/widget/server/server_type_selector.dart'; +import 'package:reboot_launcher/src/widget/server/port_input.dart'; +import 'package:reboot_launcher/src/widget/server/server_button.dart'; +import 'package:reboot_launcher/src/widget/shared/warning_info.dart'; class ServerPage extends StatelessWidget { final ServerController _serverController = Get.find(); @@ -28,8 +28,8 @@ class ServerPage extends StatelessWidget { ), HostInput(), PortInput(), - LocalServerSwitch(), - ServerButton() + ServerTypeSelector(), + const ServerButton() ] )), ); diff --git a/lib/src/page/settings_page.dart b/lib/src/page/settings_page.dart index 56e4279..954c6ab 100644 --- a/lib/src/page/settings_page.dart +++ b/lib/src/page/settings_page.dart @@ -1,11 +1,11 @@ -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; -import 'package:reboot_launcher/src/widget/smart_switch.dart'; +import 'package:reboot_launcher/src/widget/shared/smart_switch.dart'; + +import '../util/checks.dart'; +import '../widget/os/file_selector.dart'; class SettingsPage extends StatelessWidget { final SettingsController _settingsController = Get.find(); @@ -16,9 +16,7 @@ class SettingsPage extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(12.0), - child: Stack( - children: [ - Column( + child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -29,7 +27,7 @@ class SettingsPage extends StatelessWidget { windowTitle: "Select a dll", folder: false, extension: "dll", - validator: _checkDll, + validator: checkDll, validatorMode: AutovalidateMode.always ), @@ -40,7 +38,7 @@ class SettingsPage extends StatelessWidget { windowTitle: "Select a dll", folder: false, extension: "dll", - validator: _checkDll, + validator: checkDll, validatorMode: AutovalidateMode.always ), @@ -51,7 +49,7 @@ class SettingsPage extends StatelessWidget { windowTitle: "Select a dll", folder: false, extension: "dll", - validator: _checkDll, + validator: checkDll, validatorMode: AutovalidateMode.always ), @@ -59,31 +57,8 @@ class SettingsPage extends StatelessWidget { value: _settingsController.autoUpdate, label: "Update DLLs" ) - ], - ), - - const Align( - alignment: Alignment.bottomRight, - child: Text("Version 3.13${kDebugMode ? '-DEBUG' : ''}") - ) - ], + ] ), ); } - - String? _checkDll(String? text) { - if (text == null || text.isEmpty) { - return "Empty dll path"; - } - - if (!File(text).existsSync()) { - return "This dll doesn't exist"; - } - - if (!text.endsWith(".dll")) { - return "This file is not a dll"; - } - - return null; - } } diff --git a/lib/src/util/binary.dart b/lib/src/util/binary.dart deleted file mode 100644 index 665dd51..0000000 --- a/lib/src/util/binary.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:io'; - -Future loadBinary(String binary, bool safe) async{ - var safeBinary = File("$safeBinariesDirectory\\$binary"); - if(await safeBinary.exists()){ - return safeBinary; - } - - var internal = _locateInternalBinary(binary); - if(!safe){ - return internal; - } - - if(await internal.exists()){ - await internal.copy(safeBinary.path); - } - - return safeBinary; -} - -File _locateInternalBinary(String binary){ - return File("$internalBinariesDirectory\\$binary"); -} - -String get internalBinariesDirectory => - "${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries"; - -Directory get tempDirectory => - Directory("${Platform.environment["Temp"]}"); - -String get safeBinariesDirectory => - "${Platform.environment["UserProfile"]}\\.reboot_launcher"; \ No newline at end of file diff --git a/lib/src/util/build.dart b/lib/src/util/build.dart index ff744b8..75baf68 100644 --- a/lib/src/util/build.dart +++ b/lib/src/util/build.dart @@ -5,9 +5,10 @@ import 'package:html/parser.dart' show parse; import 'package:http/http.dart' as http; import 'package:process_run/shell.dart'; import 'package:reboot_launcher/src/model/fortnite_build.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/version.dart' as parser; +import 'os.dart'; + const _userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"; @@ -80,13 +81,13 @@ Future> _fetchManifests() async { } Future downloadManifestBuild( - String manifestUrl, String destination, Function(double) onProgress) async { + String manifestUrl, String destination, Function(double, String) onProgress) async { var buildExe = await loadBinary("build.exe", false); var process = await Process.start(buildExe.path, [manifestUrl, destination]); process.errLines .where((message) => message.contains("%")) - .forEach((message) => onProgress(double.parse(message.split("%")[0]))); + .forEach((message) => onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1))); return process; } diff --git a/lib/src/util/checks.dart b/lib/src/util/checks.dart new file mode 100644 index 0000000..acb7307 --- /dev/null +++ b/lib/src/util/checks.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import '../model/fortnite_version.dart'; + +String? checkVersion(String? text, List versions) { + if (text == null || text.isEmpty) { + return 'Empty version name'; + } + + if (versions.any((element) => element.name == text)) { + return 'This version already exists'; + } + + return null; +} + +String? checkGameFolder(text) { + if (text == null || text.isEmpty) { + return 'Empty game path'; + } + + var directory = Directory(text); + if (!directory.existsSync()) { + return "Directory doesn't exist"; + } + + if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { + return "Invalid game path"; + } + + return null; +} + +String? checkDownloadDestination(text) { + if (text == null || text.isEmpty) { + return 'Invalid download path'; + } + + return null; +} + +String? checkDll(String? text) { + if (text == null || text.isEmpty) { + return "Empty dll path"; + } + + if (!File(text).existsSync()) { + return "This dll doesn't exist"; + } + + if (!text.endsWith(".dll")) { + return "This file is not a dll"; + } + + return null; +} \ No newline at end of file diff --git a/lib/src/util/future.dart b/lib/src/util/future.dart new file mode 100644 index 0000000..b7e96c8 --- /dev/null +++ b/lib/src/util/future.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +extension FutureExtension on Future { + bool isCompleted() { + final completer = Completer(); + then(completer.complete).catchError(completer.completeError); + return completer.isCompleted; + } +} \ No newline at end of file diff --git a/lib/src/util/node.dart b/lib/src/util/node.dart deleted file mode 100644 index 618ae98..0000000 --- a/lib/src/util/node.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive_io.dart'; - -import 'binary.dart'; -import 'package:http/http.dart' as http; - -const String _nodeUrl = "https://nodejs.org/dist/v18.11.0/node-v18.11.0-win-x86.zip"; - -File get embeddedNode => - File("$safeBinariesDirectory/node-v18.11.0-win-x86/node.exe"); - -Future hasNode() async { - var nodeProcess = await Process.run("where", ["node"]); - return nodeProcess.exitCode == 0; -} - -Future downloadNode(ignored) async { - var response = await http.get(Uri.parse(_nodeUrl)); - var tempZip = File("${tempDirectory.path}/nodejs.zip"); - await tempZip.writeAsBytes(response.bodyBytes); - await extractFileToDisk(tempZip.path, safeBinariesDirectory); - return true; -} \ No newline at end of file diff --git a/lib/src/util/os.dart b/lib/src/util/os.dart index a587f26..16bcd93 100644 --- a/lib/src/util/os.dart +++ b/lib/src/util/os.dart @@ -32,26 +32,33 @@ Future openFilePicker(String extension) async { return result.files.first.path; } -Future> scanInstallations(String input) => Directory(input) - .list(recursive: true) - .handleError((_) {}, test: (e) => e is FileSystemException) - .where((element) => path.basename(element.path) == "FortniteClient-Win64-Shipping.exe") - .map((element) => findContainer(File(element.path))) - .where((element) => element != null) - .map((element) => element!) - .toList(); - -Directory? findContainer(File file){ - var last = file.parent; - for(var x = 0; x < 5; x++){ - var name = path.basename(last.path); - if(name != "FortniteGame" || name == "Fortnite"){ - last = last.parent; - continue; - } - - return last.parent; +Future loadBinary(String binary, bool safe) async{ + var safeBinary = File("$safeBinariesDirectory\\$binary"); + if(await safeBinary.exists()){ + return safeBinary; } - return null; -} \ No newline at end of file + var internal = _locateInternalBinary(binary); + if(!safe){ + return internal; + } + + if(await internal.exists()){ + await internal.copy(safeBinary.path); + } + + return safeBinary; +} + +File _locateInternalBinary(String binary){ + return File("$internalBinariesDirectory\\$binary"); +} + +String get internalBinariesDirectory => + "${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries"; + +Directory get tempDirectory => + Directory("${Platform.environment["Temp"]}"); + +String get safeBinariesDirectory => + "${Platform.environment["UserProfile"]}\\.reboot_launcher"; \ No newline at end of file diff --git a/lib/src/util/patcher.dart b/lib/src/util/patcher.dart index 812c916..a3c940e 100644 --- a/lib/src/util/patcher.dart +++ b/lib/src/util/patcher.dart @@ -9,7 +9,7 @@ final Uint8List _patched = Uint8List.fromList([ 45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 ]); -Future patchExe(File file) async { +Future patch(File file) async { if(_original.length != _patched.length){ throw Exception("Cannot mutate length of binary file"); } diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index 8d3865f..a30542e 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -4,7 +4,7 @@ import 'package:archive/archive_io.dart'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; -import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/os.dart'; const _rebootUrl = "https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip"; diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index edb5bf8..fe19bf5 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -1,394 +1,162 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; +import 'package:archive/archive_io.dart'; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; -import 'package:reboot_launcher/src/util/node.dart'; -import 'package:reboot_launcher/src/util/server_standalone.dart'; -import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:reboot_launcher/src/model/server_type.dart'; +import 'package:reboot_launcher/src/util/os.dart'; +import 'package:http/http.dart' as http; import 'package:shelf_proxy/shelf_proxy.dart'; +import 'package:shelf/shelf_io.dart'; +import 'package:path/path.dart' as path; -Future checkLocalServer(BuildContext context, String host, String port, bool closeAutomatically) async { - host = host.trim(); - if(host.isEmpty){ - showSnackbar( - context, const Snackbar(content: Text("Missing host name"))); - return false; - } +final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin\\Lawin.exe"); +const String _serverUrl = + "https://cdn.discordapp.com/attachments/1026121175878881290/1031230792069820487/LawinServer.zip"; - port = port.trim(); - if(port.isEmpty){ - showSnackbar( - context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center))); - return false; - } - - if(int.tryParse(port) == null){ - showSnackbar( - context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center))); - return false; - } - - return await _showCheck(context, host, port, false, closeAutomatically) != null; +Future downloadServer(ignored) async { + var response = await http.get(Uri.parse(_serverUrl)); + var tempZip = File("${Platform.environment["Temp"]}/lawin.zip"); + await tempZip.writeAsBytes(response.bodyBytes); + await extractFileToDisk(tempZip.path, serverLocation.parent.path); + return true; } - -Future changeReverseProxyState(BuildContext context, String host, String port, bool closeAutomatically, HttpServer? server) async { - if(server != null){ - try{ - server.close(force: true); - return null; - }catch(error){ - _showStopProxyError(context, error); - return server; - } - } - - host = host.trim(); - if(host.isEmpty){ - showSnackbar( - context, const Snackbar(content: Text("Missing host name"))); - return null; - } - - port = port.trim(); - if(port.isEmpty){ - showSnackbar( - context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center))); - return null; - } - - if(int.tryParse(port) == null){ - showSnackbar( - context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center))); - return null; - } - - try{ - var uri = await _showCheck(context, host, port, true, closeAutomatically); - if(uri == null){ - return null; - } - - return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551); - }catch(error){ - _showStartProxyError(context, error); - return null; - } -} - -Future _showCheck(BuildContext context, String host, String port, bool remote, bool closeAutomatically) async { - var future = ping(host, port); - Uri? result; - return await showDialog( - context: context, - builder: (context) => ContentDialog( - content: FutureBuilder( - future: future, - builder: (context, snapshot) { - if(snapshot.hasError){ - return SizedBox( - width: double.infinity, - child: Text("Cannot ping ${remote ? "remote" : "local"} server: ${snapshot.error}" , textAlign: TextAlign.center) - ); - } - - if(snapshot.connectionState == ConnectionState.done && !snapshot.hasData){ - return SizedBox( - width: double.infinity, - child: Text( - "The ${remote ? "remote" : "local"} server doesn't work correctly ${remote ? "or the IP and/or the port are incorrect" : ""}", - textAlign: TextAlign.center - ) - ); - } - - result = snapshot.data; - if(snapshot.hasData){ - if(remote || closeAutomatically) { - Navigator.of(context).pop(result); - } - - return const SizedBox( - width: double.infinity, - child: Text( - "The server works correctly", - textAlign: TextAlign.center - ) - ); - } - - return InfoLabel( - label: "Pinging ${remote ? "remote" : "local"} lawin server...", - child: const SizedBox( - width: double.infinity, - child: ProgressBar() - ) - ); - } - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(result), - child: const Text('Close'), - )) - ] - ) - ); -} - -void _showStartProxyError(BuildContext context, Object error) { - showDialog( - context: context, - builder: (context) => ContentDialog( - content: SizedBox( - width: double.infinity, - child: Text("Cannot create the reverse proxy: $error", textAlign: TextAlign.center) - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ) - ) - ], - ) - ); -} - -void _showStopProxyError(BuildContext context, Object error) { - showDialog( - context: context, - builder: (context) => ContentDialog( - content: SizedBox( - width: double.infinity, - child: Text("Cannot kill the reverse proxy: $error", textAlign: TextAlign.center) - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ) - ) - ], - ) - ); -} - -Future changeEmbeddedServerState(BuildContext context, bool running) async { - if (running) { - var releaseBat = await loadBinary("release.bat", false); - await Process.run(releaseBat.path, []); - return false; - } - - var free = await isLawinPortFree(); - if (!free) { - var shouldKill = await _showAlreadyBindPortWarning(context); - if (!shouldKill) { - return false; - } - - var releaseBat = await loadBinary("release.bat", false); - await Process.run(releaseBat.path, []); - } - - var node = await hasNode(); - var useLocalNode = false; - if(!node) { - useLocalNode = true; - if(!embeddedNode.existsSync()){ - var result = await _showNodeDownloadInfo(context); - if(!result) { - return false; - } - } - } - - if(!serverLocation.existsSync()) { - var result = await _showServerDownloadInfo(context); - if(!result){ - return false; - } - } - - var serverRunner = File("${serverLocation.path}/start.bat"); - if (!serverRunner.existsSync()) { - _showEmbeddedError(context, "missing file ${serverRunner.path}"); - return false; - } - - var nodeModules = Directory("${serverLocation.path}/node_modules"); - if (!nodeModules.existsSync()) { - await Process.run("${serverLocation.path}/install_packages.bat", [], - workingDirectory: serverLocation.path); - } - +Future isLawinPortFree() async { try { - var logFile = await loadBinary("server.txt", true); - if(logFile.existsSync()){ - logFile.deleteSync(); - } - - var process = await Process.start( - !useLocalNode ? "node" : '"${embeddedNode.path}"', - ["index.js"], - workingDirectory: serverLocation.path - ); - process.outLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append)); - process.errLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append)); - return true; - }catch(exception){ - _showEmbeddedError(context, exception.toString()); - return false; + var portBat = await loadBinary("port.bat", true); + var process = await Process.run(portBat.path, []); + return !process.outText.contains(" LISTENING "); + }catch(_){ + return ServerSocket.bind("127.0.0.1", 3551) + .then((socket) => socket.close()) + .then((_) => true) + .onError((error, _) => false); } } -Future _showServerDownloadInfo(BuildContext context) async { - var nodeFuture = compute(downloadServer, true); - var result = await showDialog( - context: context, - builder: (context) => ContentDialog( - content: FutureBuilder( - future: nodeFuture, - builder: (context, snapshot) { - if(snapshot.hasError){ - return SizedBox( - width: double.infinity, - child: Text("An error occurred while downloading: ${snapshot.error}", - textAlign: TextAlign.center)); - } +Future freeLawinPort() async { + var releaseBat = await loadBinary("release.bat", false); + await Process.run(releaseBat.path, []); +} - if(snapshot.hasData){ - return const SizedBox( - width: double.infinity, - child: Text("The download was completed successfully!", - textAlign: TextAlign.center) - ); - } +List createRebootArgs(String username, bool headless) { + var args = [ + "-skippatchcheck", + "-epicapp=Fortnite", + "-epicenv=Prod", + "-epiclocale=en-us", + "-epicportal", + "-noeac", + "-fromfl=be", + "-fltoken=7ce411021b27b4343a44fdg8", + "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", + "-AUTH_LOGIN=$username@projectreboot.dev", + "-AUTH_PASSWORD=Rebooted", + "-AUTH_TYPE=epic" + ]; - return InfoLabel( - label: "Downloading lawin server...", - child: const SizedBox( - width: double.infinity, - child: ProgressBar() - ) - ); - } - ), - actions: [ - FutureBuilder( - future: nodeFuture, - builder: (builder, snapshot) => SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError), - child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'), - ) - ) - ) - ], - ) + if(headless){ + args.addAll(["-nullrhi", "-nosplash", "-nosound"]); + } + + return args; +} + +Future pingSelf() async => ping("127.0.0.1", "3551"); + +Future ping(String host, String port, [bool https=false]) async { + var hostName = _getHostName(host); + var declaredScheme = _getScheme(host); + try{ + var uri = Uri( + scheme: declaredScheme ?? (https ? "https" : "http"), + host: hostName, + port: int.parse(port) + ); + var client = HttpClient() + ..connectionTimeout = const Duration(seconds: 5); + var request = await client.getUrl(uri); + var response = await request.close(); + var body = utf8.decode(await response.single); + return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null; + }catch(_){ + return https || declaredScheme != null ? null : await ping(host, port, true); + } +} + +String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", ""); + +String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; + +Future checkServerPreconditions(String host, String port, ServerType type) async { + host = host.trim(); + if(host.isEmpty){ + return ServerResult( + type: ServerResultType.missingHostError + ); + } + + port = port.trim(); + if(port.isEmpty){ + return ServerResult( + type: ServerResultType.missingPortError + ); + } + + if(int.tryParse(port) == null){ + return ServerResult( + type: ServerResultType.illegalPortError + ); + } + + if(type == ServerType.embedded || type == ServerType.remote){ + var free = await isLawinPortFree(); + if (!free) { + return ServerResult( + type: ServerResultType.portTakenError + ); + } + } + + if(type == ServerType.embedded && !serverLocation.existsSync()){ + return ServerResult( + type: ServerResultType.serverDownloadRequiredError + ); + } + + return ServerResult( + uri: ping(host, port), + type: ServerResultType.canStart ); - - return result != null && result; } -void _showEmbeddedError(BuildContext context, String error) { - showDialog( - context: context, - builder: (context) => ContentDialog( - content: SizedBox( - width: double.infinity, - child: Text( - "Cannot start server: $error", - textAlign: TextAlign.center - ) - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - )) - ], - )); +Future startEmbeddedServer() async { + return await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path); } -Future _showNodeDownloadInfo(BuildContext context) async { - var nodeFuture = compute(downloadNode, true); - var result = await showDialog( - context: context, - builder: (context) => ContentDialog( - content: FutureBuilder( - future: nodeFuture, - builder: (context, snapshot) { - if(snapshot.hasError){ - return SizedBox( - width: double.infinity, - child: Text("An error occurred while downloading: ${snapshot.error}", - textAlign: TextAlign.center)); - } - - if(snapshot.hasData){ - return const SizedBox( - width: double.infinity, - child: Text("The download was completed successfully!", - textAlign: TextAlign.center) - ); - } - - return InfoLabel( - label: "Downloading node...", - child: const SizedBox( - width: double.infinity, - child: ProgressBar() - ) - ); - } - ), - actions: [ - FutureBuilder( - future: nodeFuture, - builder: (builder, snapshot) => SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError), - child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'), - ) - ) - ) - ], - ) - ); - - return result != null && result; +Future startRemoteServer(Uri uri) async { + return await serve(proxyHandler(uri), "127.0.0.1", 3551); } -Future _showAlreadyBindPortWarning(BuildContext context) async { - return await showDialog( - context: context, - builder: (context) => ContentDialog( - content: const Text( - "Port 3551 is already in use, do you want to kill the associated process?", - textAlign: TextAlign.center), - actions: [ - Button( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Close'), - ), - FilledButton( - child: const Text('Kill'), - onPressed: () => Navigator.of(context).pop(true)), - ], - )) ?? - false; +class ServerResult { + final Future? uri; + final Object? error; + final StackTrace? stackTrace; + final ServerResultType type; + + ServerResult({this.uri, this.error, this.stackTrace, required this.type}); +} + +enum ServerResultType { + missingHostError, + missingPortError, + illegalPortError, + cannotPingServer, + portTakenError, + serverDownloadRequiredError, + canStart, + started, + unknownError, + stopped } \ No newline at end of file diff --git a/lib/src/util/server_standalone.dart b/lib/src/util/server_standalone.dart deleted file mode 100644 index e941420..0000000 --- a/lib/src/util/server_standalone.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:archive/archive_io.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; - -final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin"); -const String _serverUrl = - "https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip"; - -Future downloadServer(ignored) async { - var response = await http.get(Uri.parse(_serverUrl)); - var tempZip = File("${Platform.environment["Temp"]}/lawin.zip"); - await tempZip.writeAsBytes(response.bodyBytes); - await extractFileToDisk(tempZip.path, serverLocation.parent.path); - var result = Directory("${serverLocation.parent.path}/LawinServer-main"); - await result.rename("${serverLocation.parent.path}/${path.basename(serverLocation.path)}"); - await Process.run("${serverLocation.path}/install_packages.bat", [], workingDirectory: serverLocation.path); - await updateEngineConfig(); - return true; -} - -Future updateEngineConfig() async { - var engine = File("${serverLocation.path}/CloudStorage/DefaultEngine.ini"); - var patchedEngine = await loadBinary("DefaultEngine.ini", true); - await engine.writeAsString(await patchedEngine.readAsString()); -} - -Future isLawinPortFree() async { - var portBat = await loadBinary("port.bat", true); - var process = await Process.run(portBat.path, []); - return !process.outText.contains(" LISTENING "); // Goofy way, best we got -} - -List createRebootArgs(String username, bool headless) { - var args = [ - "-epicapp=Fortnite", - "-epicenv=Prod", - "-epiclocale=en-us", - "-epicportal", - "-skippatchcheck", - "-fromfl=eac", - "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", - "-AUTH_LOGIN=$username@projectreboot.dev", - "-AUTH_PASSWORD=Rebooted", - "-AUTH_TYPE=epic" - ]; - - if(headless){ - args.addAll(["-nullrhi", "-nosplash", "-nosound"]); - } - - return args; -} - -Future ping(String host, String port, [bool https=false]) async { - var hostName = _getHostName(host); - var declaredScheme = _getScheme(host); - try{ - var uri = Uri( - scheme: declaredScheme ?? (https ? "https" : "http"), - host: hostName, - port: int.parse(port) - ); - var client = HttpClient() - ..connectionTimeout = const Duration(seconds: 5); - var request = await client.getUrl(uri); - var response = await request.close(); - var body = utf8.decode(await response.single); - return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null; - }catch(_){ - return https || declaredScheme != null ? null : await ping(host, port, true); - } -} - -String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", ""); - -String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; \ No newline at end of file diff --git a/lib/src/widget/add_local_version.dart b/lib/src/widget/add_local_version.dart deleted file mode 100644 index 71e4701..0000000 --- a/lib/src/widget/add_local_version.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'dart:io'; - -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/fortnite_version.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; - -class AddLocalVersion extends StatelessWidget { - final GameController _gameController = Get.find(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _gamePathController = TextEditingController(); - - AddLocalVersion({Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Form( - child: Builder( - builder: (formContext) => ContentDialog( - style: const ContentDialogThemeData( - padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) - ), - content: _createLocalVersionDialogBody(), - actions: _createLocalVersionActions(formContext)))); - } - - List _createLocalVersionActions(BuildContext context) { - return [ - Button( - onPressed: () => _closeLocalVersionDialog(context, false), - child: const Text('Close'), - ), - FilledButton( - child: const Text('Save'), - onPressed: () => _closeLocalVersionDialog(context, true)) - ]; - } - - Future _closeLocalVersionDialog(BuildContext context, bool save) async { - if (save) { - if (!Form.of(context)!.validate()) { - return; - } - - _gameController.addVersion(FortniteVersion( - name: _nameController.text, - location: Directory(_gamePathController.text))); - } - - Navigator.of(context).pop(save); - } - - Widget _createLocalVersionDialogBody() { - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormBox( - controller: _nameController, - header: "Name", - placeholder: "Type the version's name", - autofocus: true, - validator: _checkVersion - ), - - const SizedBox( - height: 16.0 - ), - - FileSelector( - label: "Location", - placeholder: "Type the game folder", - windowTitle: "Select game folder", - controller: _gamePathController, - validator: _checkGameFolder, - folder: true - ), - - const SizedBox(height: 8.0), - ], - ); - } - - String? _checkVersion(String? text) { - if (text == null || text.isEmpty) { - return 'Empty version name'; - } - - if (_gameController.versions.value.any((element) => element.name == text)) { - return 'This version already exists'; - } - - return null; - } - - String? _checkGameFolder(text) { - if (text == null || text.isEmpty) { - return 'Empty game path'; - } - - var directory = Directory(text); - if (!directory.existsSync()) { - return "Directory doesn't exist"; - } - - if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { - return "Invalid game path"; - } - - return null; - } -} diff --git a/lib/src/widget/add_server_version.dart b/lib/src/widget/add_server_version.dart deleted file mode 100644 index 9018e42..0000000 --- a/lib/src/widget/add_server_version.dart +++ /dev/null @@ -1,373 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/controller/build_controller.dart'; -import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; -import 'package:reboot_launcher/src/util/build.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; -import 'package:reboot_launcher/src/widget/version_name_input.dart'; - -import 'build_selector.dart'; - -class AddServerVersion extends StatefulWidget { - const AddServerVersion( - {Key? key}) - : super(key: key); - - @override - State createState() => _AddServerVersionState(); -} - -class _AddServerVersionState extends State { - final GameController _gameController = Get.find(); - final BuildController _buildController = Get.find(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _pathController = TextEditingController(); - late Future _future; - DownloadStatus _status = DownloadStatus.none; - double _downloadProgress = 0; - DateTime? _downloadStartTime; - DateTime? _lastUpdateTime; - Duration? _lastUpdateTimeLeft; - String? _lastUpdateTimeFormatted; - String? _error; - Process? _manifestDownloadProcess; - CancelableOperation? _driveDownloadOperation; - - @override - void initState() { - _future = _buildController.builds != null - ? Future.value(true) - : compute(fetchBuilds, null) - .then((value) => _buildController.builds = value); - super.initState(); - } - - @override - void dispose() { - _pathController.dispose(); - _nameController.dispose(); - _onDisposed(); - super.dispose(); - } - - void _onDisposed() { - if(_status != DownloadStatus.downloading && _status != DownloadStatus.extracting){ - return; - } - - if (_manifestDownloadProcess != null) { - loadBinary("stop.bat", false) - .then((value) => Process.runSync(value.path, [])); // kill doesn't work :/ - _buildController.cancelledDownload.value = true; - return; - } - - if(_driveDownloadOperation == null){ - return; - } - - _driveDownloadOperation!.cancel(); - _buildController.cancelledDownload.value = true; - } - - @override - Widget build(BuildContext context) { - return Form( - child: Builder( - builder: (context) => ContentDialog( - style: const ContentDialogThemeData( - padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) - ), - content: _createDownloadVersionBody(), - actions: _createDownloadVersionOption(context)))); - } - - List _createDownloadVersionOption(BuildContext context) { - switch (_status) { - case DownloadStatus.none: - return [ - Button( - onPressed: () => _onClose(), - child: const Text('Close')), - FilledButton( - onPressed: () => _startDownload(context), - child: const Text('Download'), - ) - ]; - - case DownloadStatus.error: - return [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => _onClose(), - child: const Text('Close'))) - ]; - default: - return [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => _onClose(), - child: Text( - _status == DownloadStatus.downloading ? 'Stop' : 'Close')), - ) - ]; - } - } - - void _onClose() { - Navigator.of(context).pop(); - } - - void _startDownload(BuildContext context) async { - if (!Form.of(context)!.validate()) { - return; - } - - try { - setState(() => _status = DownloadStatus.downloading); - if (_buildController.selectedBuild.hasManifest) { - _manifestDownloadProcess = await downloadManifestBuild( - _buildController.selectedBuild.link, _pathController.text, _onDownloadProgress); - _manifestDownloadProcess!.exitCode.then((value) => _onDownloadComplete()); - } else { - _driveDownloadOperation = CancelableOperation.fromFuture( - downloadArchiveBuild(_buildController.selectedBuild.link, _pathController.text, - _onDownloadProgress, _onUnrar)) - .then((_) => _onDownloadComplete(), - onError: (error, _) => _handleError(error)); - } - } catch (exception) { - _handleError(exception); - } - } - - FutureOr? _handleError(Object exception) { - var message = exception.toString(); - _onDownloadError(message.contains(":") - ? " ${message.substring(message.indexOf(":") + 1)}" - : message); - return null; - } - - void _onUnrar() { - setState(() => _status = DownloadStatus.extracting); - } - - void _onDownloadComplete() { - if (!mounted) { - return; - } - - setState(() { - _status = DownloadStatus.done; - _gameController.addVersion(FortniteVersion( - name: _nameController.text, - location: Directory(_pathController.text))); - }); - } - - void _onDownloadError(String message) { - if (!mounted) { - return; - } - - setState(() { - _status = DownloadStatus.error; - _error = message; - }); - } - - void _onDownloadProgress(double progress) { - if (!mounted) { - return; - } - - _downloadStartTime ??= DateTime.now(); - setState(() { - _status = DownloadStatus.downloading; - _downloadProgress = progress; - }); - } - - Widget _createDownloadVersionBody() { - return FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (snapshot.hasError) { - WidgetsBinding.instance.addPostFrameCallback((_) => - setState(() => _status = DownloadStatus.error)); - return Container( - width: double.infinity, - padding: const EdgeInsets.only(bottom: 16.0), - child: Text("Cannot fetch builds: ${snapshot.error}", - textAlign: TextAlign.center), - ); - } - - if (!snapshot.hasData) { - return InfoLabel( - label: "Fetching builds...", - child: Container( - padding: const EdgeInsets.only(bottom: 16.0), - width: double.infinity, - child: const ProgressBar() - ), - ); - } - - return _buildBody(); - }); - } - - Widget _buildBody() { - switch (_status) { - case DownloadStatus.none: - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const BuildSelector(), - - const SizedBox(height: 16.0), - - VersionNameInput(controller: _nameController), - - const SizedBox(height: 16.0), - - FileSelector( - label: "Destination", - placeholder: "Type the download destination", - windowTitle: "Select download destination", - controller: _pathController, - validator: _checkDownloadDestination, - folder: true - ), - - const SizedBox(height: 8.0), - ], - ); - case DownloadStatus.downloading: - var timeLeft = _timeLeft; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "Downloading...", - style: FluentTheme.maybeOf(context)?.typography.body, - textAlign: TextAlign.start, - ), - ), - - const SizedBox( - height: 8, - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${_downloadProgress.round()}%", - style: FluentTheme.maybeOf(context)?.typography.body, - ), - - Text( - "Time left: ${timeLeft ?? "00:00:00"}", - style: FluentTheme.maybeOf(context)?.typography.body, - ), - ], - ), - - const SizedBox( - height: 8, - ), - - SizedBox( - width: double.infinity, - child: ProgressBar(value: _downloadProgress.toDouble()) - ), - - const SizedBox( - height: 16, - ) - ], - ); - case DownloadStatus.extracting: - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: InfoLabel( - label: "Extracting...", - child: const SizedBox(width: double.infinity, child: ProgressBar()) - ), - ); - case DownloadStatus.done: - return const Padding( - padding: EdgeInsets.only(bottom: 16), - child: SizedBox( - width: double.infinity, - child: Text("The download was completed successfully!", - textAlign: TextAlign.center)), - ); - case DownloadStatus.error: - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: SizedBox( - width: double.infinity, - child: Text( - "An error was occurred while downloading:$_error", - textAlign: TextAlign.center)), - ); - } - } - - String? get _timeLeft { - if(_downloadStartTime == null){ - return null; - } - - var now = DateTime.now(); - var elapsed = now.difference(_downloadStartTime!); - var msLeft = (elapsed.inMilliseconds * 100) / _downloadProgress; - if(!msLeft.isFinite){ - return null; - } - - var timeLeft = Duration(milliseconds: msLeft.round() - elapsed.inMilliseconds); - var delta = _lastUpdateTime == null || _lastUpdateTimeLeft == null ? -1 - : timeLeft.inMilliseconds - _lastUpdateTimeLeft!.inMilliseconds; - var shouldSkip = delta == -1 || now.difference(_lastUpdateTime!).inMilliseconds > delta.abs() * 3; - _lastUpdateTime = now; - _lastUpdateTimeLeft = timeLeft; - if(shouldSkip){ - return _lastUpdateTimeFormatted; - } - - var twoDigitMinutes = _twoDigits(timeLeft.inMinutes.remainder(60)); - var twoDigitSeconds = _twoDigits(timeLeft.inSeconds.remainder(60)); - return _lastUpdateTimeFormatted = - "${_twoDigits(timeLeft.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; - } - - String _twoDigits(int n) => n.toString().padLeft(2, "0"); - - String? _checkDownloadDestination(text) { - if (text == null || text.isEmpty) { - return 'Invalid download path'; - } - - return null; - } -} - -enum DownloadStatus { none, downloading, extracting, error, done } diff --git a/lib/src/widget/build_selector.dart b/lib/src/widget/home/build_selector.dart similarity index 100% rename from lib/src/widget/build_selector.dart rename to lib/src/widget/home/build_selector.dart diff --git a/lib/src/widget/host_checkbox.dart b/lib/src/widget/home/game_type_selector.dart similarity index 91% rename from lib/src/widget/host_checkbox.dart rename to lib/src/widget/home/game_type_selector.dart index 73b0d4b..baabba7 100644 --- a/lib/src/widget/host_checkbox.dart +++ b/lib/src/widget/home/game_type_selector.dart @@ -3,10 +3,10 @@ import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/model/game_type.dart'; -class DeploymentSelector extends StatelessWidget { +class GameTypeSelector extends StatelessWidget { final GameController _gameController = Get.find(); - DeploymentSelector({Key? key}) : super(key: key); + GameTypeSelector({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/home/launch_button.dart similarity index 50% rename from lib/src/widget/launch_button.dart rename to lib/src/widget/home/launch_button.dart index f54c99a..2298fb8 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/home/launch_button.dart @@ -1,14 +1,19 @@ import 'dart:async'; import 'dart:io'; +import 'package:clipboard/clipboard.dart'; 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/dialog/dialog.dart'; +import 'package:reboot_launcher/src/dialog/dialog_button.dart'; +import 'package:reboot_launcher/src/dialog/game_dialogs.dart'; +import 'package:reboot_launcher/src/dialog/server_dialogs.dart'; import 'package:reboot_launcher/src/model/game_type.dart'; -import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/reboot.dart'; @@ -17,9 +22,8 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; import 'package:path/path.dart' as path; -import '../controller/settings_controller.dart'; -import '../model/server_type.dart'; -import '../util/server_standalone.dart'; +import '../../controller/settings_controller.dart'; +import '../../dialog/snackbar.dart'; class LaunchButton extends StatefulWidget { const LaunchButton( @@ -63,15 +67,13 @@ class _LaunchButtonState extends State { void _onPressed() async { if (_gameController.username.text.isEmpty) { - showSnackbar( - context, const Snackbar(content: Text("Please type a username"))); + showMessage("Missing in-game username"); _updateServerState(false); return; } if (_gameController.selectedVersionObs.value == null) { - showSnackbar( - context, const Snackbar(content: Text("Please select a version"))); + showMessage("No version is selected"); _updateServerState(false); return; } @@ -90,17 +92,17 @@ class _LaunchButtonState extends State { Win32Process(_gameController.launcherProcess!.pid).suspend(); } - if (version.eacExecutable != null) { - _gameController.eacProcess = await Process.start(version.eacExecutable!.path, []); - Win32Process(_gameController.eacProcess!.pid).suspend(); - } - if(hosting){ - await patchExe(version.executable!); + await patch(version.executable!); } - await _startServerIfNecessary(); - if(!_serverController.started.value){ + if(!mounted){ + _onStop(); + return; + } + + var result = await _serverController.changeStateInteractive(true); + if(!result){ _onStop(); return; } @@ -131,42 +133,6 @@ class _LaunchButtonState extends State { } } - Future _startServerIfNecessary() async { - if (!mounted) { - return; - } - - if(_serverController.started.value){ - return; - } - - switch(_serverController.type.value){ - case ServerType.embedded: - var result = await changeEmbeddedServerState(context, false); - _serverController.started(result); - break; - case ServerType.remote: - _serverController.reverseProxy = await changeReverseProxyState( - context, - _serverController.host.text, - _serverController.port.text, - false, - _serverController.reverseProxy - ); - _serverController.started(_serverController.reverseProxy != null); - break; - case ServerType.local: - var result = await checkLocalServer( - context, - _serverController.host.text, - _serverController.port.text, - true - ); - _serverController.started(result); - break; - } - } - Future _updateServerState(bool value) async { if (_gameController.started.value == value) { return; @@ -197,106 +163,6 @@ class _LaunchButtonState extends State { Navigator.of(context).pop(success); } - Future _showBrokenServerWarning() async { - if(!mounted){ - return; - } - - showDialog( - context: context, - builder: (context) => ContentDialog( - content: const SizedBox( - width: double.infinity, - child: Text("The lawin server is not working correctly", textAlign: TextAlign.center) - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ) - ) - ], - ) - ); - } - - Future _showMissingDllError(String name) async { - if(!mounted){ - return; - } - - showDialog( - context: context, - builder: (context) => ContentDialog( - content: SizedBox( - width: double.infinity, - child: Text("$name dll is not a valid dll, fix it in the settings tab", textAlign: TextAlign.center) - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ) - ) - ], - ) - ); - } - - Future _showTokenError() async { - if(!mounted){ - return; - } - - showDialog( - context: context, - builder: (context) => ContentDialog( - content: const SizedBox( - width: double.infinity, - child: Text("A token error occurred, restart the game and the lawin server, then try again", textAlign: TextAlign.center) - ), - actions: [ - SizedBox( - width: double.infinity, - 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(), - child: const Text('Close'), - ) - ) - ], - ) - ); - } - Future _showServerLaunchingWarning() async { if(!mounted){ return; @@ -304,26 +170,15 @@ class _LaunchButtonState extends State { var result = await showDialog( context: context, - builder: (context) => ContentDialog( - content: InfoLabel( - label: "Launching headless reboot server...", - child: const SizedBox( - width: double.infinity, - child: ProgressBar() - ) - ), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () { - Navigator.of(context).pop(false); - _onStop(); - }, - child: const Text('Cancel'), - ) - ) - ], + builder: (context) => InfoDialog.ofOnly( + text: "Launching headless reboot server...", + button: DialogButton( + type: ButtonType.only, + onTap: () { + Navigator.of(context).pop(false); + _onStop(); + } + ) ) ); @@ -335,10 +190,6 @@ class _LaunchButtonState extends State { } void _onGameOutput(String line) { - if(kDebugMode){ - print(line); - } - if(_logFile != null){ _logFile!.writeAsString("$line\n", mode: FileMode.append); } @@ -351,59 +202,43 @@ class _LaunchButtonState extends State { if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){ _fail = true; _closeDialogIfOpen(false); - _showBrokenServerWarning(); + showBrokenError(); return; } if(line.contains("HTTP 400 response from ")){ _fail = true; _closeDialogIfOpen(false); - _showUnsupportedHeadless(); + showUnsupportedHeadless(); return; } if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){ _fail = true; _closeDialogIfOpen(false); - _showTokenError(); + showTokenError(); return; } - if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) { - _injectOrShowError(Injectable.console); - return; - } - - if(line.contains("Region") && _gameController.type.value != GameType.client){ - _injectOrShowError(Injectable.reboot) - .then((value) => _closeDialogIfOpen(true)); + if(line.contains("Region")){ + if(_gameController.type.value == GameType.client){ + _injectOrShowError(Injectable.console); + }else { + _injectOrShowError(Injectable.reboot) + .then((value) => _closeDialogIfOpen(true)); + } } } 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( - content: SizedBox( - width: double.infinity, - child: Text("Cannot launch fortnite: $exception", - textAlign: TextAlign.center)), - actions: [ - SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Close'), - )) - ], - )); + builder: (context) => ErrorDialog( + exception: exception, + stackTrace: stackTrace, + errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception" + ) + ); } void _onStop() { @@ -422,12 +257,7 @@ class _LaunchButtonState extends State { if(!dllPath.existsSync()) { await _downloadMissingDll(injectable); if(!dllPath.existsSync()){ - WidgetsBinding.instance.addPostFrameCallback((_) { - _fail = true; - _closeDialogIfOpen(false); - _showMissingDllError(path.basename(dllPath.path)); - _onStop(); - }); + _onDllFail(dllPath); return; } } @@ -445,6 +275,15 @@ class _LaunchButtonState extends State { } } + void _onDllFail(File dllPath) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _fail = true; + _closeDialogIfOpen(false); + showMissingDllError(path.basename(dllPath.path)); + _onStop(); + }); + } + File _getDllPath(Injectable injectable){ switch(injectable){ case Injectable.reboot: diff --git a/lib/src/widget/username_box.dart b/lib/src/widget/home/username_box.dart similarity index 92% rename from lib/src/widget/username_box.dart rename to lib/src/widget/home/username_box.dart index df8cd70..75e7776 100644 --- a/lib/src/widget/username_box.dart +++ b/lib/src/widget/home/username_box.dart @@ -2,7 +2,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'; +import 'package:reboot_launcher/src/widget/shared/smart_input.dart'; class UsernameBox extends StatelessWidget { final GameController _gameController = Get.find(); diff --git a/lib/src/widget/version_name_input.dart b/lib/src/widget/home/version_name_input.dart similarity index 100% rename from lib/src/widget/version_name_input.dart rename to lib/src/widget/home/version_name_input.dart diff --git a/lib/src/widget/version_selector.dart b/lib/src/widget/home/version_selector.dart similarity index 68% rename from lib/src/widget/version_selector.dart rename to lib/src/widget/home/version_selector.dart index d66f0a3..2f30810 100644 --- a/lib/src/widget/version_selector.dart +++ b/lib/src/widget/home/version_selector.dart @@ -5,16 +5,15 @@ import 'package:flutter/gestures.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; 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:reboot_launcher/src/dialog/add_local_version.dart'; +import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart'; import 'package:url_launcher/url_launcher.dart'; -class VersionSelector extends StatefulWidget { - final bool enableScanner; +import '../../dialog/add_server_version.dart'; +import '../../util/checks.dart'; - const VersionSelector({Key? key, this.enableScanner = false}) : super(key: key); +class VersionSelector extends StatefulWidget { + const VersionSelector({Key? key}) : super(key: key); @override State createState() => _VersionSelectorState(); @@ -45,17 +44,6 @@ class _VersionSelectorState extends State { const SizedBox( width: 16, ), - if(widget.enableScanner) - Tooltip( - message: "Scan all fortnite builds in a directory", - child: Button( - child: const Icon(FluentIcons.site_scan), - onPressed: () => _openScanLocalVersionDialog(context)), - ), - if(widget.enableScanner) - const SizedBox( - width: 16, - ), Tooltip( message: "Download a fortnite build from the archive", child: Button( @@ -125,33 +113,20 @@ class _VersionSelectorState extends State { builder: (context) => AddLocalVersion()); } - void _openScanLocalVersionDialog(BuildContext context) async { - await showDialog( - context: context, - builder: (context) => const ScanLocalVersion()); - } - Future _openMenu( BuildContext context, FortniteVersion version, Offset offset) async { - var result = await showMenu( + var result = await showMenu( context: context, offset: offset, builder: (context) => MenuFlyout( - items: [ - MenuFlyoutItem( - text: const Text('Open in explorer'), - onPressed: () => Navigator.of(context).pop(0) - ), - MenuFlyoutItem( - text: const Text('Delete'), - onPressed: () => Navigator.of(context).pop(1) - ), - ], + items: ContextualOption.values + .map((entry) => _createOption(context, entry)) + .toList() ) ); switch (result) { - case 0: + case ContextualOption.openExplorer: if(!mounted){ return; } @@ -161,7 +136,21 @@ class _VersionSelectorState extends State { .onError((error, stackTrace) => _onExplorerError()); break; - case 1: + case ContextualOption.rename: + if(!mounted){ + return; + } + + Navigator.of(context).pop(); + var result = await _openRenameDialog(context, version); + if(result == null){ + return; + } + + _gameController.rename(version, result); + break; + + case ContextualOption.delete: if(!mounted){ return; } @@ -183,9 +172,19 @@ class _VersionSelectorState extends State { } break; + + case null: + break; } } + MenuFlyoutItem _createOption(BuildContext context, ContextualOption entry) { + return MenuFlyoutItem( + text: Text(entry.name), + onPressed: () => Navigator.of(context).pop(entry) + ); + } + bool _onExplorerError() { showSnackbar( context, @@ -231,4 +230,61 @@ class _VersionSelectorState extends State { ) ); } + + Future _openRenameDialog(BuildContext context, FortniteVersion version) { + var controller = TextEditingController(text: version.name); + return showDialog( + context: context, + builder: (context) => Form( + child: Builder( + builder: (context) => ContentDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormBox( + controller: controller, + header: "Name", + placeholder: "Type the new version name", + autofocus: true, + validator: (text) => checkVersion(text, _gameController.versions.value) + ), + + const SizedBox(height: 8.0), + ], + ), + actions: [ + Button( + onPressed: () => Navigator.of(context).pop(null), + child: const Text('Close'), + ), + FilledButton( + onPressed: () { + if (!Form.of(context)!.validate()) { + return; + } + + Navigator.of(context).pop(controller.text); + }, + child: const Text('Save') + ) + ] + ) + ) + ) + ); + } +} + +enum ContextualOption { + openExplorer, + rename, + delete; + + String get name { + return this == ContextualOption.openExplorer ? "Open in explorer" + : this == ContextualOption.rename ? "Rename" + : "Delete"; + } } diff --git a/lib/src/widget/file_selector.dart b/lib/src/widget/os/file_selector.dart similarity index 94% rename from lib/src/widget/file_selector.dart rename to lib/src/widget/os/file_selector.dart index 123833d..057284d 100644 --- a/lib/src/widget/file_selector.dart +++ b/lib/src/widget/os/file_selector.dart @@ -1,7 +1,8 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; +import 'package:reboot_launcher/src/dialog/snackbar.dart'; -import '../util/os.dart'; +import '../../util/os.dart'; class FileSelector extends StatefulWidget { final String label; @@ -65,7 +66,7 @@ class _FileSelectorState extends State { void _onPressed() { if(_selecting){ - showSnackbar(context, const Snackbar(content: Text("Folder selector is already opened"))); + showMessage("Folder selector is already opened"); return; } diff --git a/lib/src/widget/window_border.dart b/lib/src/widget/os/window_border.dart similarity index 100% rename from lib/src/widget/window_border.dart rename to lib/src/widget/os/window_border.dart diff --git a/lib/src/widget/window_buttons.dart b/lib/src/widget/os/window_buttons.dart similarity index 100% rename from lib/src/widget/window_buttons.dart rename to lib/src/widget/os/window_buttons.dart diff --git a/lib/src/widget/scan_local_version.dart b/lib/src/widget/scan_local_version.dart deleted file mode 100644 index cd52f35..0000000 --- a/lib/src/widget/scan_local_version.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:io'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as path; -import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; - - -class ScanLocalVersion extends StatefulWidget { - const ScanLocalVersion({Key? key}) - : super(key: key); - - @override - State createState() => _ScanLocalVersionState(); -} - -class _ScanLocalVersionState extends State { - final TextEditingController _folderController = TextEditingController(); - Future>? _future; - - @override - Widget build(BuildContext context) { - return Form( - child: Builder( - builder: (formContext) => ContentDialog( - style: const ContentDialogThemeData( - padding: EdgeInsets.only(left: 20, right: 20, top: 20.0, bottom: 0.0) - ), - constraints: const BoxConstraints(maxWidth: 368, maxHeight: 169), - content: _createLocalVersionDialogBody(), - actions: _createLocalVersionActions(formContext)))); - } - - List _createLocalVersionActions(BuildContext context) { - if(_future == null) { - return [ - Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - FilledButton( - child: const Text('Scan'), - onPressed: () => _scanFolder(context, true)) - ]; - } - - return [ - FutureBuilder( - future: _future, - builder: (context, snapshot) { - if(!snapshot.hasData || snapshot.hasError) { - return SizedBox( - width: double.infinity, - child: Button( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), - ), - ); - } - - return SizedBox( - width: double.infinity, - child: FilledButton( - child: const Text('Save'), - onPressed: () => Navigator.of(context).pop() - ) - ); - } - ) - ]; - } - - Future _scanFolder(BuildContext context, bool save) async { - setState(() { - _future = compute(scanInstallations, _folderController.text); - }); - } - - Widget _createLocalVersionDialogBody() { - if(_future == null) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FileSelector( - label: "Location", - placeholder: "Type the folder to scan", - windowTitle: "Select the folder to scan", - controller: _folderController, - validator: _checkScanFolder, - folder: true - ) - ], - ); - } - return FutureBuilder>( - future: _future, - builder: (context, snapshot) { - if(snapshot.hasError) { - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: SizedBox( - width: double.infinity, - child: Text( - "An error was occurred while scanning:${snapshot.error}", - textAlign: TextAlign.center)), - ); - } - - if(!snapshot.hasData){ - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: InfoLabel( - label: "Searching...", - child: const SizedBox(width: double.infinity, child: ProgressBar()) - ), - ); - } - - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: SizedBox( - width: double.infinity, - child: Column( - children: [ - const Text( - "Successfully completed scan", - textAlign: TextAlign.center), - - _createResultsDropDown(snapshot.data!) - ], - )), - ); - } - ); - } - - Widget _createResultsDropDown(List data) { - return Expanded( - child: DropDownButton( - leading: const Text("Results"), - items: data.map((element) => _createResultItem(element)).toList() - ), - ); - } - - MenuFlyoutItem _createResultItem(element) { - return MenuFlyoutItem( - text: SizedBox( - width: double.infinity, - child: Text(path.basename(element.path)) - ), - trailing: const Expanded(child: SizedBox()), - onPressed: () {}); - } - - String? _checkScanFolder(text) { - if (text == null || text.isEmpty) { - return 'Invalid folder to scan'; - } - - var directory = Directory(text); - if (!directory.existsSync()) { - return "Directory doesn't exist"; - } - - return null; - } -} \ No newline at end of file diff --git a/lib/src/widget/host_input.dart b/lib/src/widget/server/host_input.dart similarity index 91% rename from lib/src/widget/host_input.dart rename to lib/src/widget/server/host_input.dart index ba48d74..517e0c7 100644 --- a/lib/src/widget/host_input.dart +++ b/lib/src/widget/server/host_input.dart @@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/widget/smart_input.dart'; +import 'package:reboot_launcher/src/widget/shared/smart_input.dart'; class HostInput extends StatelessWidget { final ServerController _serverController = Get.find(); diff --git a/lib/src/widget/port_input.dart b/lib/src/widget/server/port_input.dart similarity index 91% rename from lib/src/widget/port_input.dart rename to lib/src/widget/server/port_input.dart index 9e3035f..c84341a 100644 --- a/lib/src/widget/port_input.dart +++ b/lib/src/widget/server/port_input.dart @@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/widget/smart_input.dart'; +import 'package:reboot_launcher/src/widget/shared/smart_input.dart'; class PortInput extends StatelessWidget { diff --git a/lib/src/widget/server_button.dart b/lib/src/widget/server/server_button.dart similarity index 51% rename from lib/src/widget/server_button.dart rename to lib/src/widget/server/server_button.dart index 6c6ddcf..43e9478 100644 --- a/lib/src/widget/server_button.dart +++ b/lib/src/widget/server/server_button.dart @@ -1,14 +1,19 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/dialog/server_dialogs.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; -import 'package:reboot_launcher/src/util/server.dart'; -class ServerButton extends StatelessWidget { +class ServerButton extends StatefulWidget { + const ServerButton({Key? key}) : super(key: key); + + @override + State createState() => _ServerButtonState(); +} + +class _ServerButtonState extends State { final ServerController _serverController = Get.find(); - ServerButton({Key? key}) : super(key: key); - @override Widget build(BuildContext context) { return Align( @@ -18,7 +23,7 @@ class ServerButton extends StatelessWidget { child: Obx(() => Tooltip( message: _helpMessage, child: Button( - onPressed: () => _onPressed(context), + onPressed: () async => _serverController.changeStateInteractive(false), child: Text(_buttonText())), )), ), @@ -55,42 +60,4 @@ class ServerButton extends StatelessWidget { return "Check if a local lawin server is running"; } } - - void _onPressed(BuildContext context) async { - var running = _serverController.started.value; - _serverController.started.value = !running; - switch(_serverController.type.value){ - case ServerType.embedded: - var updatedRunning = await changeEmbeddedServerState(context, running); - _updateStarted(updatedRunning); - break; - case ServerType.remote: - _serverController.reverseProxy = await changeReverseProxyState( - context, - _serverController.host.text, - _serverController.port.text, - false, - _serverController.reverseProxy - ); - _updateStarted(_serverController.reverseProxy != null); - break; - case ServerType.local: - var result = await checkLocalServer( - context, - _serverController.host.text, - _serverController.port.text, - false - ); - _updateStarted(result); - break; - } - } - - void _updateStarted(bool updatedRunning) { - if (updatedRunning == _serverController.started.value) { - return; - } - - _serverController.started.value = updatedRunning; - } } diff --git a/lib/src/widget/local_server_switch.dart b/lib/src/widget/server/server_type_selector.dart similarity index 91% rename from lib/src/widget/local_server_switch.dart rename to lib/src/widget/server/server_type_selector.dart index cc3f8f1..8428595 100644 --- a/lib/src/widget/local_server_switch.dart +++ b/lib/src/widget/server/server_type_selector.dart @@ -3,10 +3,10 @@ import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; -class LocalServerSwitch extends StatelessWidget { +class ServerTypeSelector extends StatelessWidget { final ServerController _serverController = Get.find(); - LocalServerSwitch({Key? key}) : super(key: key); + ServerTypeSelector({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/src/widget/smart_check_box.dart b/lib/src/widget/shared/smart_check_box.dart similarity index 100% rename from lib/src/widget/smart_check_box.dart rename to lib/src/widget/shared/smart_check_box.dart diff --git a/lib/src/widget/smart_input.dart b/lib/src/widget/shared/smart_input.dart similarity index 100% rename from lib/src/widget/smart_input.dart rename to lib/src/widget/shared/smart_input.dart diff --git a/lib/src/widget/smart_switch.dart b/lib/src/widget/shared/smart_switch.dart similarity index 100% rename from lib/src/widget/smart_switch.dart rename to lib/src/widget/shared/smart_switch.dart diff --git a/lib/src/widget/warning_info.dart b/lib/src/widget/shared/warning_info.dart similarity index 100% rename from lib/src/widget/warning_info.dart rename to lib/src/widget/shared/warning_info.dart diff --git a/pubspec.yaml b/pubspec.yaml index f8b61d7..a9b7bb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "3.13.0" +version: "4.0.0" publish_to: 'none' @@ -34,6 +34,8 @@ dependencies: shelf_proxy: ^1.0.2 args: ^2.3.1 win32: 3.0.0 + clipboard: ^0.1.3 + sync: ^0.3.0 dependency_overrides: win32: ^3.0.0 @@ -56,7 +58,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 3.13.0.0 + msix_version: 4.0.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64