diff --git a/assets/binaries/console.dll b/assets/binaries/console.dll index 4f81c63..ecb5dad 100644 Binary files a/assets/binaries/console.dll and b/assets/binaries/console.dll differ diff --git a/assets/binaries/stop.bat b/assets/binaries/stop.bat index 50f714e..a828c5e 100644 --- a/assets/binaries/stop.bat +++ b/assets/binaries/stop.bat @@ -1 +1,2 @@ -taskkill /f /im build.exe \ No newline at end of file +taskkill /f /im build.exe +taskkill /f /im winrar.exe \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d5f63bf..a89be0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,14 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:system_theme/system_theme.dart'; import 'package:reboot_launcher/src/page/home_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await GetStorage.init("game"); + await GetStorage.init("server"); SystemTheme.accentColor.load(); doWhenWindowReady(() { const size = Size(600, 380); diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index 7042037..d75b974 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:path/path.dart' as path; class FortniteVersion { String name; @@ -11,8 +12,11 @@ class FortniteVersion { FortniteVersion({required this.name, required this.location}); static File findExecutable(Directory directory, String name) { - return File( - "${directory.path}/FortniteGame/Binaries/Win64/$name"); + var home = path.basename(directory.path) == "FortniteGame" + ? directory + : directory.listSync(recursive: true).firstWhere( + (element) => path.basename(element.path) == "FortniteGame"); + return File("${home.path}/Binaries/Win64/$name"); } File get executable { diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart index 35c7c2a..2ee2906 100644 --- a/lib/src/page/home_page.dart +++ b/lib/src/page/home_page.dart @@ -1,19 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/game_process_controller.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:reboot_launcher/src/page/info_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/server_page.dart'; import 'package:reboot_launcher/src/widget/window_buttons.dart'; -import '../model/fortnite_version.dart'; -import '../util/generic_controller.dart'; -import '../util/reboot.dart'; -import '../util/version_controller.dart'; - class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -22,129 +12,27 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - late final TextEditingController _usernameController; - late final VersionController _versionController; - late final GenericController _rebootController; - late final GenericController _localController; - late final TextEditingController _hostController; - late final TextEditingController _portController; - late final GameProcessController _gameProcessController; - late final GenericController _serverController; - late final GenericController _startedServerController; - late final GenericController _startedGameController; - late Future _future; - bool _loaded = false; + final List _children = [LauncherPage(), ServerPage(), const InfoPage()]; int _index = 0; - @override - void initState(){ - _future = _load(); - super.initState(); - } - - Future _load() async { - if (_loaded) { - return false; - } - - var preferences = await SharedPreferences.getInstance(); - await downloadRebootDll(preferences); - - Iterable json = jsonDecode(preferences.getString("versions") ?? "[]"); - var versions = - json.map((entry) => FortniteVersion.fromJson(entry)).toList(); - var selectedVersion = preferences.getString("version"); - _versionController = VersionController( - versions: versions, - serializer: _saveVersions, - selectedVersion: selectedVersion != null - ? versions.firstWhere((element) => element.name == selectedVersion) - : null); - - _rebootController = - GenericController(initialValue: preferences.getBool("reboot") ?? false); - - _usernameController = - TextEditingController(text: preferences.getString("${_rebootController.value ? "host" : "game"}_username")); - - _localController = - GenericController(initialValue: preferences.getBool("local") ?? true); - - _hostController = - TextEditingController(text: preferences.getString("host")); - - _portController = - TextEditingController(text: preferences.getString("port")); - - _gameProcessController = GameProcessController(); - - _serverController = GenericController(initialValue: null); - - _startedServerController = GenericController(initialValue: false); - - _startedGameController = GenericController(initialValue: false); - - _loaded = true; - - return true; - } - - Future _saveVersions() async { - var preferences = await SharedPreferences.getInstance(); - var versions = - _versionController.versions.map((entry) => entry.toJson()).toList(); - preferences.setString("versions", jsonEncode(versions)); - } - @override Widget build(BuildContext context) { return NavigationView( - pane: NavigationPane( - selected: _index, - onChanged: (index) => setState(() => _index = index), - displayMode: PaneDisplayMode.top, - indicator: const EndNavigationIndicator(), - items: [ - _createPane("Launcher", FluentIcons.game), - _createPane("Server", FluentIcons.server_enviroment), - _createPane("Info", FluentIcons.info), - ], - trailing: const WindowTitleBar()), - content: FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text( - "An error occurred while loading the launcher: ${snapshot.error}", - textAlign: TextAlign.center)); - } - - if (!snapshot.hasData) { - return const Center(child: ProgressRing()); - } - - return NavigationBody(index: _index, children: [ - LauncherPage( - usernameController: _usernameController, - versionController: _versionController, - rebootController: _rebootController, - serverController: _serverController, - localController: _localController, - gameProcessController: _gameProcessController, - startedGameController: _startedGameController, - startedServerController: _startedServerController - ), - ServerPage( - localController: _localController, - hostController: _hostController, - portController: _portController, - serverController: _serverController, - startedServerController: _startedServerController - ), - const InfoPage() - ]); - }), + pane: NavigationPane( + selected: _index, + onChanged: (index) => setState(() => _index = index), + displayMode: PaneDisplayMode.top, + indicator: const EndNavigationIndicator(), + items: [ + _createPane("Launcher", FluentIcons.game), + _createPane("Server", FluentIcons.server_enviroment), + _createPane("Info", FluentIcons.info), + ], + trailing: const WindowTitleBar()), + content: NavigationBody( + index: _index, + children: _children + ) ); } diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index 1eae6bd..4cae42d 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -10,10 +10,7 @@ class InfoPage extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - const Expanded( - child: SizedBox() - ), - + const Expanded(child: SizedBox()), Column( children: [ const CircleAvatar( @@ -31,13 +28,9 @@ class InfoPage extends StatelessWidget { onPressed: () => launchUrl(Uri.parse(_discordLink))), ], ), - const Expanded( child: Align( - alignment: Alignment.bottomLeft, - child: Text("Version 1.0") - ) - ) + alignment: Alignment.bottomLeft, child: Text("Version 2.2"))) ], ); } diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 22010ae..bb934d5 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -1,69 +1,33 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/game_process_controller.dart'; -import 'package:reboot_launcher/src/util/generic_controller.dart'; -import 'package:reboot_launcher/src/util/version_controller.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/widget/deployment_selector.dart'; import 'package:reboot_launcher/src/widget/launch_button.dart'; +import 'package:reboot_launcher/src/widget/restart_warning.dart'; import 'package:reboot_launcher/src/widget/username_box.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../widget/version_selector.dart'; +import 'package:reboot_launcher/src/widget/version_selector.dart'; + +import 'package:reboot_launcher/src/controller/warning_controller.dart'; class LauncherPage extends StatelessWidget { - final TextEditingController usernameController; - final VersionController versionController; - final GenericController rebootController; - final GenericController serverController; - final GenericController localController; - final GameProcessController gameProcessController; - final GenericController startedGameController; - final GenericController startedServerController; - final StreamController _streamController = StreamController(); + final WarningController _warningController = Get.put(WarningController()); + final GameController _gameController = Get.put(GameController()); - LauncherPage( - {Key? key, - required this.usernameController, - required this.versionController, - required this.rebootController, - required this.serverController, - required this.localController, - required this.gameProcessController, - required this.startedGameController, - required this.startedServerController}) - : super(key: key); + LauncherPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StreamBuilder( - stream: _streamController.stream, - builder: (context, snapshot) => UsernameBox( - controller: usernameController, - rebootController: rebootController)), - VersionSelector( - controller: versionController, - ), - DeploymentSelector( - controller: rebootController, - onSelected: () => _streamController.add(null), - enabled: true - ), - LaunchButton( - usernameController: usernameController, - versionController: versionController, - rebootController: rebootController, - serverController: serverController, - localController: localController, - gameProcessController: gameProcessController, - startedGameController: startedGameController, - startedServerController: startedServerController) - ], - ); + return Obx(() => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_warningController.warning.value) const RestartWarning(), + UsernameBox(), + VersionSelector(), + DeploymentSelector(enabled: true), + const LaunchButton() + ], + )); } } diff --git a/lib/src/page/server_page.dart b/lib/src/page/server_page.dart index bf47794..ab84306 100644 --- a/lib/src/page/server_page.dart +++ b/lib/src/page/server_page.dart @@ -1,62 +1,32 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/generic_controller.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/controller/warning_controller.dart'; import 'package:reboot_launcher/src/widget/local_server_switch.dart'; import 'package:reboot_launcher/src/widget/port_input.dart'; -import '../widget/host_input.dart'; -import '../widget/server_button.dart'; +import 'package:reboot_launcher/src/widget/host_input.dart'; +import 'package:reboot_launcher/src/widget/server_button.dart'; -class ServerPage extends StatefulWidget { - final GenericController localController; - final TextEditingController hostController; - final TextEditingController portController; - final GenericController serverController; - final GenericController startedServerController; +import 'package:reboot_launcher/src/widget/restart_warning.dart'; - const ServerPage( - {Key? key, - required this.localController, - required this.hostController, - required this.serverController, - required this.portController, - required this.startedServerController}) - : super(key: key); +class ServerPage extends StatelessWidget { + final WarningController _warningController = Get.put(WarningController()); + final ServerController _serverController = Get.put(ServerController()); - @override - State createState() => _ServerPageState(); -} - -class _ServerPageState extends State { - final StreamController _controller = StreamController.broadcast(); + ServerPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StreamBuilder( - stream: _controller.stream, - builder: (context, snapshot) => HostInput( - controller: widget.hostController, - localController: widget.localController)), - StreamBuilder( - stream: _controller.stream, - builder: (context, snapshot) => PortInput( - controller: widget.portController, - localController: widget.localController)), - LocalServerSwitch( - controller: widget.localController, - onSelected: (_) => _controller.add(null)), - ServerButton( - localController: widget.localController, - portController: widget.portController, - hostController: widget.hostController, - serverController: widget.serverController, - startController: widget.startedServerController) - ]); + return Obx(() => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_warningController.warning.value) const RestartWarning(), + HostInput(), + PortInput(), + LocalServerSwitch(), + ServerButton() + ])); } } diff --git a/lib/src/util/builds_scraper.dart b/lib/src/util/builds_scraper.dart deleted file mode 100644 index 6de7a6c..0000000 --- a/lib/src/util/builds_scraper.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:http/http.dart' as http; -import './../util/version.dart' as parser; -import 'package:html/parser.dart' show parse; - -import '../model/fortnite_build.dart'; - -final _cookieRegex = RegExp("(?<=document.cookie=\")(.*)(?=\";doc)"); -final _manifestSourceUrl = Uri.parse( - "https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md"); -final _archiveCookieUrl = Uri.parse("http://allinstaller.xyz/rel"); -final _archiveSourceUrl = Uri.parse("http://allinstaller.xyz/rel?i=1"); - -Future> fetchBuilds() async => - [...await _fetchArchives(), ...await _fetchManifests()]..sort((first, second) => first.version.compareTo(second.version)); - -Future> _fetchArchives() async { - var cookieResponse = await http.get(_archiveCookieUrl); - var cookie = _cookieRegex.stringMatch(cookieResponse.body); - var response = - await http.get(_archiveSourceUrl, headers: {"Cookie": cookie!}); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - var document = parse(response.body); - var results = []; - for (var build in document.querySelectorAll("a[href^='https']")) { - var version = parser.tryParse(build.text.replaceAll("Build ", "")); - if(version == null){ - continue; - } - - results.add(FortniteBuild( - version: version, - link: build.attributes["href"]!, - hasManifest: false - )); - } - - return results; -} - -Future> _fetchManifests() async { - var response = await http.get(_manifestSourceUrl); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - var document = parse(response.body); - var table = document.querySelector("table"); - if (table == null) { - throw Exception("Missing data table"); - } - - var results = []; - for (var tableEntry in table.querySelectorAll("tbody > tr")) { - var children = tableEntry.querySelectorAll("td"); - - var name = children[0].text; - var separator = name.indexOf("-") + 1; - var version = parser.tryParse(name.substring(separator, name.indexOf("-", separator))); - if(version == null){ - continue; - } - - var link = children[2].firstChild!.attributes["href"]!; - results.add(FortniteBuild( - version: version, - link: link, - hasManifest: true - )); - } - - return results; -} diff --git a/lib/src/util/download_build.dart b/lib/src/util/download_build.dart deleted file mode 100644 index a76e560..0000000 --- a/lib/src/util/download_build.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io'; -import 'dart:math'; -import 'package:http/http.dart' as http; - -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:unrar_file/unrar_file.dart'; - -Future downloadManifestBuild(String manifestUrl, String destination, - Function(double) onProgress) async { - var process = await Process.start(await locateAndCopyBinary("build.exe"), [manifestUrl, destination]); - - process.errLines - .where((message) => message.contains("%")) - .forEach((message) => onProgress(double.parse(message.split("%")[0]))); - - return process; -} - -Future downloadArchiveBuild(String archiveUrl, String destination, - Function(double) onProgress, Function() onRar) async { - var tempFile = File("${Platform.environment["Temp"]}/FortniteBuild${Random.secure().nextInt(1000000)}.rar"); - try{ - var client = http.Client(); - var response = await client.send(http.Request("GET", Uri.parse(archiveUrl))); - if(response.statusCode != 200){ - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - print(archiveUrl); - var length = response.contentLength!; - var received = 0; - var sink = tempFile.openWrite(); - await response.stream.map((s) { - received += s.length; - onProgress((received / length) * 100); - return s; - }).pipe(sink); - onRar(); - UnrarFile.extract_rar(tempFile, destination); - }finally{ - tempFile.delete(); - } -} diff --git a/lib/src/util/game_process_controller.dart b/lib/src/util/game_process_controller.dart deleted file mode 100644 index 02ed167..0000000 --- a/lib/src/util/game_process_controller.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'dart:io'; - -class GameProcessController { - Process? gameProcess; - Process? launcherProcess; - Process? eacProcess; - - void kill(){ - gameProcess?.kill(ProcessSignal.sigabrt); - launcherProcess?.kill(ProcessSignal.sigabrt); - eacProcess?.kill(ProcessSignal.sigabrt); - } -} diff --git a/lib/src/util/generic_controller.dart b/lib/src/util/generic_controller.dart deleted file mode 100644 index 8722587..0000000 --- a/lib/src/util/generic_controller.dart +++ /dev/null @@ -1,5 +0,0 @@ -class GenericController { - T value; - - GenericController({required T initialValue}) : this.value = initialValue; -} diff --git a/lib/src/util/injector.dart b/lib/src/util/injector.dart index 8635fda..d0b1676 100644 --- a/lib/src/util/injector.dart +++ b/lib/src/util/injector.dart @@ -1,13 +1,14 @@ import 'dart:io'; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; File injectLogFile = File("${Platform.environment["Temp"]}/server.txt"); // This can be done easily with win32 apis but for some reason it doesn't work on all machines +// Update: it was a missing permission error, it could be refactored now Future injectDll(int pid, String dll) async { - var shell = Shell(workingDirectory: binariesDirectory); + var shell = Shell(workingDirectory: internalBinariesDirectory); var process = await shell.run("./injector.exe -p $pid --inject \"$dll\""); var success = process.outText.contains("Successfully injected module"); if (!success) { diff --git a/lib/src/util/locate_binary.dart b/lib/src/util/locate_binary.dart deleted file mode 100644 index 8589e59..0000000 --- a/lib/src/util/locate_binary.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:io'; - -Future locateAndCopyBinary(String binary) async{ - var originalFile = locateBinary(binary); - var tempFile = File("${Platform.environment["Temp"]}\\$binary"); - if(!(await tempFile.exists())){ - await originalFile.copy("${Platform.environment["Temp"]}\\$binary"); - } - - return tempFile.path; -} - -File locateBinary(String binary){ - return File("$binariesDirectory\\$binary"); -} - -String get binariesDirectory => - "${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries"; diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index 671f0a4..daa0e2f 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -16,7 +16,7 @@ Future _getLastUpdate(SharedPreferences preferences) async { Future downloadRebootDll(SharedPreferences preferences) async { var now = DateTime.now(); - var oldRebootDll = locateBinary("reboot.dll"); + var oldRebootDll = await loadBinary("reboot.dll", true); var lastUpdate = await _getLastUpdate(preferences); var exists = await oldRebootDll.exists(); if(lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists){ @@ -26,9 +26,10 @@ Future downloadRebootDll(SharedPreferences preferences) async { var response = await http.get(Uri.parse(_rebootUrl)); var tempZip = File("${Platform.environment["Temp"]}/reboot.zip") ..writeAsBytesSync(response.bodyBytes); - await extractFileToDisk(tempZip.path, binariesDirectory); - locateBinary("Project Reboot.pdb").delete(); - var rebootDll = locateBinary("Project Reboot.dll"); + await extractFileToDisk(tempZip.path, safeBinariesDirectory); + var pdb = await loadBinary("Project Reboot.pdb", true); + pdb.delete(); + var rebootDll = await loadBinary("Project Reboot.dll", true); if (!(await rebootDll.exists())) { throw Exception("Missing reboot dll"); } diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index a4c95fa..14d3cd2 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -3,14 +3,15 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.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/locate_binary.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:reboot_launcher/src/controller/warning_controller.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:url_launcher/url_launcher.dart'; -final serverLocation = Directory("${Platform.environment["UserProfile"]}/.lawin"); +final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin"); const String _serverUrl = "https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip"; const String _nodeUrl = @@ -28,9 +29,8 @@ Future downloadServer() async { Future updateEngineConfig() async { var engine = File("${serverLocation.path}/CloudStorage/DefaultEngine.ini"); - await engine.writeAsString(await locateBinary("DefaultEngine.ini").readAsString()); - var preferences = await SharedPreferences.getInstance(); - preferences.setBool("config_update", true); + var patchedEngine = await loadBinary("DefaultEngine.ini", true); + await engine.writeAsString(await patchedEngine.readAsString()); } Future downloadNode() async { @@ -44,7 +44,8 @@ Future downloadNode() async { } Future isPortFree() async { - var process = await Process.run(await locateAndCopyBinary("port.bat"), []); + var portBat = await loadBinary("port.bat", false); + var process = await Process.run(portBat.path, []); return !process.outText.contains(" LISTENING "); // Goofy way, best we got } @@ -57,7 +58,6 @@ void checkAddress(BuildContext context, String host, String port) { builder: (context, snapshot) { if(snapshot.hasData){ return SizedBox( - height: 32, width: double.infinity, child: Text(snapshot.data! ? "Valid address" : "Invalid address" , textAlign: TextAlign.center) ); @@ -66,7 +66,6 @@ void checkAddress(BuildContext context, String host, String port) { return const InfoLabel( label: "Checking address...", child: SizedBox( - height: 32, width: double.infinity, child: ProgressBar() ) @@ -98,8 +97,9 @@ Future _pingAddress(String host, String port) async { } Future startEmbedded(BuildContext context, bool running, bool needsFreePort) async { + var releaseBat = await loadBinary("release.bat", false); if (running) { - await Process.run(await locateAndCopyBinary("release.bat"), []); + await Process.run(releaseBat.path, []); return null; } @@ -110,7 +110,7 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre return null; } - await Process.run(await locateAndCopyBinary("release.bat"), []); + await Process.run(releaseBat.path, []); } if (!(await serverLocation.exists())) { @@ -136,7 +136,7 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre context, const Snackbar( content: Text( - "Node installer download cancelled" + "Node download cancelled" ) ) ); @@ -144,11 +144,9 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre return null; } + var controller = Get.find(); + controller.warning(true); await launchUrl(result.uri); - showSnackbar( - context, - const Snackbar( - content: Text("Start the server when node is installed"))); // Using a infobar could be nicer return null; } @@ -158,11 +156,6 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre workingDirectory: serverLocation.path); } - var preferences = await SharedPreferences.getInstance(); - if(!(preferences.getBool("config_update") ?? false)){ - await updateEngineConfig(); - } - return await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path); } @@ -193,7 +186,6 @@ Future _showNodeInfo(BuildContext context) async { return const InfoLabel( label: "Downloading node installer...", child: SizedBox( - height: 32, width: double.infinity, child: ProgressBar() ) @@ -245,7 +237,6 @@ Future _showMissingNodeWarning(BuildContext context) async { context: context, builder: (context) => ContentDialog( content: const SizedBox( - height: 32, width: double.infinity, child: Text("Node is required to run the embedded server", textAlign: TextAlign.center)), diff --git a/lib/src/util/version_controller.dart b/lib/src/util/version_controller.dart deleted file mode 100644 index 5af7b28..0000000 --- a/lib/src/util/version_controller.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class VersionController { - final List versions; - final Function serializer; - FortniteVersion? _selectedVersion; - - VersionController( - {required this.versions, - required this.serializer, - FortniteVersion? selectedVersion}) - : _selectedVersion = selectedVersion; - - void add(FortniteVersion version) { - versions.add(version); - serializer(); - } - - FortniteVersion removeByName(String versionName) { - var version = versions.firstWhere((element) => element.name == versionName); - remove(version); - return version; - } - - void remove(FortniteVersion version) { - versions.remove(version); - serializer(); - } - - bool get isEmpty => versions.isEmpty; - - bool get isNotEmpty => versions.isNotEmpty; - - FortniteVersion? get selectedVersion => _selectedVersion; - - set selectedVersion(FortniteVersion? selectedVersion) { - _selectedVersion = selectedVersion; - SharedPreferences.getInstance().then((preferences) => - _selectedVersion == null - ? preferences.remove("version") - : preferences.setString("version", selectedVersion!.name)); - } -} diff --git a/lib/src/widget/add_local_version.dart b/lib/src/widget/add_local_version.dart index 737a282..f9401fc 100644 --- a/lib/src/widget/add_local_version.dart +++ b/lib/src/widget/add_local_version.dart @@ -1,17 +1,18 @@ 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/widget/select_file.dart'; -import '../model/fortnite_version.dart'; -import '../util/version_controller.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; class AddLocalVersion extends StatelessWidget { - final VersionController controller; + final GameController _gameController = Get.find(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _gamePathController = TextEditingController(); - AddLocalVersion({required this.controller, Key? key}) + AddLocalVersion({Key? key}) : super(key: key); @override @@ -44,7 +45,7 @@ class AddLocalVersion extends StatelessWidget { return; } - controller.add(FortniteVersion( + _gameController.addVersion(FortniteVersion( name: _nameController.text, location: Directory(_gamePathController.text))); } @@ -67,7 +68,7 @@ class AddLocalVersion extends StatelessWidget { return 'Invalid version name'; } - if (controller.versions.any((element) => element.name == text)) { + if (_gameController.versions.value.any((element) => element.name == text)) { return 'Existent game version'; } diff --git a/lib/src/widget/add_server_version.dart b/lib/src/widget/add_server_version.dart index 4ac6076..ccbe65a 100644 --- a/lib/src/widget/add_server_version.dart +++ b/lib/src/widget/add_server_version.dart @@ -1,24 +1,21 @@ import 'dart:async'; import 'dart:io'; +import 'package:async/async.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/download_build.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:reboot_launcher/src/util/version_controller.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/util/build.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/widget/select_file.dart'; import 'package:reboot_launcher/src/widget/version_name_input.dart'; -import '../model/fortnite_build.dart'; -import '../model/fortnite_version.dart'; -import '../util/builds_scraper.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'build_selector.dart'; class AddServerVersion extends StatefulWidget { - final VersionController controller; - final Function onCancel; - const AddServerVersion( - {required this.controller, Key? key, required this.onCancel}) + {Key? key}) : super(key: key); @override @@ -26,39 +23,55 @@ class AddServerVersion extends StatefulWidget { } class _AddServerVersionState extends State { - static List? _builds; - late GenericController _buildController; - late TextEditingController _nameController; - late TextEditingController _pathController; - late DownloadStatus _status; + final GameController _gameController = Get.find(); + final BuildController _buildController = Get.put(BuildController()); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _pathController = TextEditingController(); late Future _future; + DownloadStatus _status = DownloadStatus.none; double _downloadProgress = 0; String? _error; - Process? _process; - bool _disposed = false; + Process? _manifestDownloadProcess; + CancelableOperation? _driveDownloadOperation; @override void initState() { _future = _fetchBuilds(); - _buildController = GenericController(initialValue: null); - _nameController = TextEditingController(); - _pathController = TextEditingController(); - _status = DownloadStatus.none; super.initState(); } @override void dispose() { - _disposed = true; _pathController.dispose(); _nameController.dispose(); - if (_process != null && _status == DownloadStatus.downloading) { - locateAndCopyBinary("stop.bat") - .then((value) => Process.runSync(value, [])); // kill doesn't work :/ - widget.onCancel(); + _onDisposed(); + super.dispose(); + } + + void _onDisposed() { + if(_status != DownloadStatus.downloading && _status != DownloadStatus.extracting){ + return; } - super.dispose(); + if (_manifestDownloadProcess != null) { + loadBinary("stop.bat", false) + .then((value) => Process.runSync(value.path, [])); // kill doesn't work :/ + _onCancelDownload(); + return; + } + + if(_driveDownloadOperation == null){ + return; + } + + _driveDownloadOperation!.cancel(); + _onCancelDownload(); + } + + void _onCancelDownload() { + WidgetsBinding.instance.addPostFrameCallback((_) => + showSnackbar(context, + const Snackbar(content: Text("Download cancelled")))); } @override @@ -122,27 +135,28 @@ class _AddServerVersionState extends State { try { setState(() => _status = DownloadStatus.downloading); - var build = _buildController.value!; - if (build.hasManifest) { - _process = await downloadManifestBuild( - build.link, _pathController.text, _onDownloadProgress); - _process!.exitCode.then((value) => _onDownloadComplete()); + if (_buildController.selectedBuild.hasManifest) { + _manifestDownloadProcess = await downloadManifestBuild( + _buildController.selectedBuild.link, _pathController.text, _onDownloadProgress); + _manifestDownloadProcess!.exitCode.then((value) => _onDownloadComplete()); } else { - downloadArchiveBuild( - build.link, _pathController.text, _onDownloadProgress, _onUnrar) - .then((value) => _onDownloadComplete()) - .catchError(_handleError); + _driveDownloadOperation = CancelableOperation.fromFuture( + downloadArchiveBuild(_buildController.selectedBuild.link, _pathController.text, + _onDownloadProgress, _onUnrar)) + .then((_) => _onDownloadComplete(), + onError: (error, _) => _handleError(error)); } } catch (exception) { _handleError(exception); } } - void _handleError(Object exception) { + FutureOr? _handleError(Object exception) { var message = exception.toString(); _onDownloadError(message.contains(":") ? " ${message.substring(message.indexOf(":") + 1)}" : message); + return null; } void _onUnrar() { @@ -150,20 +164,20 @@ class _AddServerVersionState extends State { } void _onDownloadComplete() { - if (_disposed) { + if (!mounted) { return; } setState(() { _status = DownloadStatus.done; - widget.controller.add(FortniteVersion( + _gameController.addVersion(FortniteVersion( name: _nameController.text, location: Directory(_pathController.text))); }); } void _onDownloadError(String message) { - if (_disposed) { + if (!mounted) { return; } @@ -174,7 +188,7 @@ class _AddServerVersionState extends State { } void _onDownloadProgress(double progress) { - if (_disposed) { + if (!mounted) { return; } @@ -189,6 +203,7 @@ class _AddServerVersionState extends State { future: _future, builder: (context, snapshot) { if (snapshot.hasError) { + snapshot.printError(); return Text("Cannot fetch builds: ${snapshot.error}", textAlign: TextAlign.center); } @@ -197,7 +212,7 @@ class _AddServerVersionState extends State { return const InfoLabel( label: "Fetching builds...", child: SizedBox( - height: 32, width: double.infinity, child: ProgressBar()), + width: double.infinity, child: ProgressBar()), ); } @@ -212,11 +227,8 @@ class _AddServerVersionState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - BuildSelector(builds: _builds!, controller: _buildController), - VersionNameInput( - controller: _nameController, - versions: widget.controller.versions, - ), + const BuildSelector(), + VersionNameInput(controller: _nameController), SelectFile( label: "Destination", placeholder: "Type the download destination", @@ -238,10 +250,7 @@ class _AddServerVersionState extends State { case DownloadStatus.extracting: return const InfoLabel( label: "Extracting", - child: InfoLabel( - label: "This might take a while...", - child: SizedBox(width: double.infinity, child: ProgressBar()), - ), + child: SizedBox(width: double.infinity, child: ProgressBar()) ); case DownloadStatus.done: return const SizedBox( @@ -258,11 +267,11 @@ class _AddServerVersionState extends State { } Future _fetchBuilds() async { - if (_builds != null) { + if (_buildController.builds != null) { return false; } - _builds = await fetchBuilds(); + _buildController.builds = await fetchBuilds(); return true; } diff --git a/lib/src/widget/build_selector.dart b/lib/src/widget/build_selector.dart index e0a2f86..e094b91 100644 --- a/lib/src/widget/build_selector.dart +++ b/lib/src/widget/build_selector.dart @@ -1,46 +1,46 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; -import '../model/fortnite_build.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/model/fortnite_build.dart'; class BuildSelector extends StatefulWidget { - final List builds; - final GenericController controller; - const BuildSelector( - {required this.builds, required this.controller, Key? key}) - : super(key: key); + const BuildSelector({Key? key}) : super(key: key); @override State createState() => _BuildSelectorState(); } class _BuildSelectorState extends State { - String? value; + final BuildController _buildController = Get.find(); @override Widget build(BuildContext context) { - widget.controller.value = widget.controller.value ?? widget.builds[0]; return InfoLabel( - label: "Build", - child: Combobox( - placeholder: const Text('Select a fortnite build'), - isExpanded: true, - items: _createItems(), - value: widget.controller.value, - onChanged: (value) => value == null ? {} : setState(() => widget.controller.value = value) - ), + label: "Build", + child: Combobox( + placeholder: const Text('Select a fortnite build'), + isExpanded: true, + items: _createItems(), + value: _buildController.selectedBuild, + onChanged: (value) => + value == null ? {} : setState(() => _buildController.selectedBuild = value) + ) ); } List> _createItems() { - return widget.builds.map((element) => _createItem(element)).toList(); + return _buildController.builds! + .map((element) => _createItem(element)) + .toList(); } ComboboxItem _createItem(FortniteBuild element) { return ComboboxItem( value: element, - child: Text("${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"), + child: Text( + "${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"), ); } } diff --git a/lib/src/widget/deployment_selector.dart b/lib/src/widget/deployment_selector.dart index 85a3649..e19e20c 100644 --- a/lib/src/widget/deployment_selector.dart +++ b/lib/src/widget/deployment_selector.dart @@ -1,36 +1,25 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_switch.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class DeploymentSelector extends StatelessWidget { - final GenericController controller; - final VoidCallback onSelected; + final GameController _gameController = Get.find(); final bool enabled; - const DeploymentSelector( - {Key? key, - required this.controller, - required this.onSelected, - required this.enabled}) - : super(key: key); + DeploymentSelector({Key? key, required this.enabled}) : super(key: key); @override Widget build(BuildContext context) { return SmartSwitch( + value: _gameController.host, onDisabledPress: !enabled ? () => showSnackbar(context, const Snackbar(content: Text("Hosting is not allowed"))) : null, - keyName: "reboot", label: "Host", - controller: controller, - onSelected: _onSelected, - enabled: enabled); - } - - void _onSelected(bool value) { - controller.value = value; - onSelected(); + enabled: enabled + ); } } diff --git a/lib/src/widget/host_input.dart b/lib/src/widget/host_input.dart index c53a1fd..2396d29 100644 --- a/lib/src/widget/host_input.dart +++ b/lib/src/widget/host_input.dart @@ -1,27 +1,28 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; class HostInput extends StatelessWidget { - final TextEditingController controller; - final GenericController localController; + final ServerController _serverController = Get.put(ServerController()); - const HostInput( - {Key? key, required this.controller, required this.localController}) - : super(key: key); + HostInput({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SmartInput( - keyName: "host", - label: "Host", - placeholder: "Type the host name", - controller: controller, - enabled: !localController.value, - onTap: () => localController.value - ? showSnackbar(context, const Snackbar(content: Text("The host is locked when embedded is on"))) - : {}, - ); + return Obx(() => SmartInput( + keyName: "host", + label: "Host", + placeholder: "Type the host name", + controller: _serverController.host, + enabled: !_serverController.embedded.value, + onTap: () => _serverController.embedded.value + ? showSnackbar( + context, + const Snackbar( + content: Text("The host is locked when embedded is on"))) + : {}, + )); } } diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/launch_button.dart index 171a066..3597e5e 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/launch_button.dart @@ -1,37 +1,21 @@ +import 'dart:async'; import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/game_process_controller.dart'; -import 'package:reboot_launcher/src/util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/util/injector.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; -import '../util/server.dart'; -import '../util/version_controller.dart'; +import 'package:reboot_launcher/src/util/server.dart'; class LaunchButton extends StatefulWidget { - final TextEditingController usernameController; - final VersionController versionController; - final GenericController rebootController; - final GenericController localController; - final GenericController serverController; - final GameProcessController gameProcessController; - final GenericController startedGameController; - final GenericController startedServerController; - const LaunchButton( - {Key? key, - required this.usernameController, - required this.versionController, - required this.rebootController, - required this.serverController, - required this.localController, - required this.gameProcessController, - required this.startedGameController, - required this.startedServerController}) + {Key? key}) : super(key: key); @override @@ -39,6 +23,9 @@ class LaunchButton extends StatefulWidget { } class _LaunchButtonState extends State { + final GameController _gameController = Get.find(); + final ServerController _serverController = Get.find(); + @override Widget build(BuildContext context) { return Align( @@ -46,91 +33,95 @@ class _LaunchButtonState extends State { child: SizedBox( width: double.infinity, child: Listener( - child: Button( - onPressed: _onPressed, - child: Text(widget.startedGameController.value - ? "Close" - : "Launch")), + child: Obx(() => Button( + onPressed: () => _onPressed(context), + child: Text(_gameController.started.value ? "Close" : "Launch") + )), ), ), ); } - void _onPressed() async { - // Set state immediately for responsive reasons - if (widget.usernameController.text.isEmpty) { + void _onPressed(BuildContext context) async { + if (_gameController.username.text.isEmpty) { showSnackbar( context, const Snackbar(content: Text("Please type a username"))); - setState(() => widget.startedGameController.value = false); + _updateServerState(false); return; } - if (widget.versionController.selectedVersion == null) { + if (_gameController.selectedVersionObs.value == null) { showSnackbar( context, const Snackbar(content: Text("Please select a version"))); - setState(() => widget.startedGameController.value = false); + _updateServerState(false); return; } - if (widget.startedGameController.value) { + if (_gameController.started.value) { _onStop(); return; } - if (widget.serverController.value == null && widget.localController.value && await isPortFree()) { + _updateServerState(true); + if (!_serverController.started.value && _serverController.embedded.value && await isPortFree()) { var process = await startEmbedded(context, false, false); - widget.serverController.value = process; - widget.startedServerController.value = process != null; + _serverController.process = process; + _serverController.started(process != null); } _onStart(); - setState(() => widget.startedGameController.value = true); + } + + Future _updateServerState(bool value) async { + if (_serverController.started.value == value) { + return; + } + + _serverController.started(value); } Future _onStart() async { - try{ - var version = widget.versionController.selectedVersion!; - - if(await version.launcher.exists()) { - widget.gameProcessController.launcherProcess = - await Process.start(version.launcher.path, []); - Win32Process(widget.gameProcessController.launcherProcess!.pid) - .suspend(); + try { + _gameController.started(true); + var version = _gameController.selectedVersionObs.value!; + if (await version.launcher.exists()) { + _gameController.launcherProcess = await Process.start(version.launcher.path, []); + Win32Process(_gameController.launcherProcess!.pid).suspend(); } - if(await version.eacExecutable.exists()){ - widget.gameProcessController.eacProcess = await Process.start(version.eacExecutable.path, []); - Win32Process(widget.gameProcessController.eacProcess!.pid).suspend(); + if (await version.eacExecutable.exists()) { + _gameController.eacProcess = await Process.start(version.eacExecutable.path, []); + Win32Process(_gameController.eacProcess!.pid).suspend(); } - widget.gameProcessController.gameProcess = await Process.start(widget.versionController.selectedVersion!.executable.path, _createProcessArguments()) + _gameController.gameProcess = await Process.start(version.executable.path, _createProcessArguments()) ..exitCode.then((_) => _onStop()) ..outLines.forEach(_onGameOutput); _injectOrShowError("cranium.dll"); - }catch(exception){ - setState(() => widget.startedGameController.value = false); + } catch (exception) { + _gameController.started(false); _onError(exception); } } void _onGameOutput(line) { - if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { - _onStop(); - return; - } - - if (!line.contains("Game Engine Initialized")) { - return; - } - - if (!widget.rebootController.value) { - _injectOrShowError("console.dll"); - return; - } - - _injectOrShowError("reboot.dll"); + if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { + _onStop(); + return; } + if (!line.contains("Game Engine Initialized")) { + return; + } + + if (!_gameController.host.value) { + _injectOrShowError("console.dll"); + return; + } + + _injectOrShowError("reboot.dll"); + } + Future _onError(exception) { return showDialog( context: context, @@ -153,24 +144,25 @@ class _LaunchButtonState extends State { } void _onStop() { - setState(() => widget.startedGameController.value = false); - widget.gameProcessController.kill(); + _updateServerState(false); + _gameController.kill(); } void _injectOrShowError(String binary) async { - var gameProcess = widget.gameProcessController.gameProcess; + var gameProcess = _gameController.gameProcess; if (gameProcess == null) { return; } - try{ - var success = await injectDll(gameProcess.pid, await locateAndCopyBinary(binary)); - if(success){ + try { + var dll = await loadBinary(binary, true); + var success = await injectDll(gameProcess.pid, dll.path); + if (success) { return; } _onInjectError(binary); - }catch(exception){ + } catch (exception) { _onInjectError(binary); } } @@ -191,7 +183,7 @@ class _LaunchButtonState extends State { "-fromfl=eac", "-fltoken=3db3ba5dcbd2e16703f3978d", "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", - "-AUTH_LOGIN=${widget.usernameController.text}@projectreboot.dev", + "-AUTH_LOGIN=${_gameController.username.text}@projectreboot.dev", "-AUTH_PASSWORD=Rebooted", "-AUTH_TYPE=epic" ]; diff --git a/lib/src/widget/local_server_switch.dart b/lib/src/widget/local_server_switch.dart index 65341e1..9dbfe06 100644 --- a/lib/src/widget/local_server_switch.dart +++ b/lib/src/widget/local_server_switch.dart @@ -1,22 +1,19 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_switch.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; class LocalServerSwitch extends StatelessWidget { - final GenericController controller; - final Function(bool)? onSelected; + final ServerController _serverController = Get.put(ServerController()); - const LocalServerSwitch({Key? key, required this.controller, this.onSelected}) - : super(key: key); + LocalServerSwitch({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SmartSwitch( - keyName: "local", - label: "Embedded", - controller: controller, - onSelected: onSelected + value: _serverController.embedded, + label: "Embedded" ); } } diff --git a/lib/src/widget/port_input.dart b/lib/src/widget/port_input.dart index f19fe2c..5ee112c 100644 --- a/lib/src/widget/port_input.dart +++ b/lib/src/widget/port_input.dart @@ -1,29 +1,28 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; class PortInput extends StatelessWidget { - final TextEditingController controller; - final GenericController localController; + final ServerController _serverController = Get.put(ServerController()); - const PortInput({ - Key? key, - required this.controller, - required this.localController - }) : super(key: key); + PortInput({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SmartInput( + return Obx(() => SmartInput( keyName: "port", label: "Port", placeholder: "Type the host port", - controller: controller, - enabled: !localController.value, - onTap: () => localController.value - ? showSnackbar(context, const Snackbar(content: Text("The port is locked when embedded is on"))) + controller: _serverController.port, + enabled: !_serverController.embedded.value, + onTap: () => _serverController.embedded.value + ? showSnackbar( + context, + const Snackbar( + content: Text("The port is locked when embedded is on"))) : {}, - ); + )); } -} \ No newline at end of file +} diff --git a/lib/src/widget/server_button.dart b/lib/src/widget/server_button.dart index 3515ccc..d119f0e 100644 --- a/lib/src/widget/server_button.dart +++ b/lib/src/widget/server_button.dart @@ -1,66 +1,44 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; -import '../util/server.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/util/server.dart'; -class ServerButton extends StatefulWidget { - final GenericController localController; - final TextEditingController hostController; - final TextEditingController portController; - final GenericController serverController; - final GenericController startController; +class ServerButton extends StatelessWidget { + final ServerController _serverController = Get.put(ServerController()); + ServerButton({Key? key}) : super(key: key); - const ServerButton( - {Key? key, - required this.localController, - required this.hostController, - required this.portController, - required this.serverController, required this.startController}) - : super(key: key); - - @override - State createState() => _ServerButtonState(); -} - -class _ServerButtonState extends State { @override Widget build(BuildContext context) { return Align( alignment: AlignmentDirectional.bottomCenter, child: SizedBox( width: double.infinity, - child: Button( - onPressed: _onPressed, - child: Text(widget.localController.value - ? !widget.startController.value + child: Obx(() => Button( + onPressed: () => _onPressed(context), + child: Text(_serverController.embedded.value + ? !_serverController.started.value ? "Start" : "Stop" - : "Check address")), + : "Check address"))), ), ); } - void _onPressed() async { - if (widget.localController.value) { - var oldRunning = widget.startController.value; - setState(() => widget.startController.value = !widget.startController.value); // Needed to make the UI feel smooth - var process = await startEmbedded(context, oldRunning, true); - var updatedRunning = process != null; - if(updatedRunning != oldRunning){ - setState(() => widget.startController.value = updatedRunning); - } - - widget.serverController.value = process; + void _onPressed(BuildContext context) async { + if (!_serverController.embedded.value) { + checkAddress(context, _serverController.host.text, _serverController.port.text); return; } - checkAddress(context, widget.hostController.text, widget.portController.text); + var running = _serverController.started.value; + _serverController.started(!running); + var process = await startEmbedded(context, running, true); + var updatedRunning = process != null; + if (updatedRunning != _serverController.started.value) { + _serverController.started.value = updatedRunning; + } + + _serverController.process = process; } } diff --git a/lib/src/widget/smart_input.dart b/lib/src/widget/smart_input.dart index 010a452..2eec361 100644 --- a/lib/src/widget/smart_input.dart +++ b/lib/src/widget/smart_input.dart @@ -1,7 +1,6 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -class SmartInput extends StatefulWidget { +class SmartInput extends StatelessWidget { final String keyName; final String label; final String placeholder; @@ -22,49 +21,16 @@ class SmartInput extends StatefulWidget { this.populate = false, this.type = TextInputType.text}) : super(key: key); - - @override - State createState() => _SmartInputState(); -} - -class _SmartInputState extends State { + @override Widget build(BuildContext context) { - return widget.populate ? _buildPopulatedTextBox() : _buildTextBox(); - } - - FutureBuilder _buildPopulatedTextBox(){ - return FutureBuilder( - future: SharedPreferences.getInstance(), - builder: (context, snapshot) { - _update(snapshot.data); - return _buildTextBox(); - } - ); - } - - void _update(SharedPreferences? preferences) { - if(preferences == null){ - return; - } - - widget.controller.text = preferences.getString(widget.keyName) ?? ""; - } - - TextBox _buildTextBox() { return TextBox( - enabled: widget.enabled, - controller: widget.controller, - header: widget.label, - keyboardType: widget.type, - placeholder: widget.placeholder, - onChanged: _save, - onTap: widget.onTap, + enabled: enabled, + controller: controller, + header: label, + keyboardType: type, + placeholder: placeholder, + onTap: onTap, ); } - - Future _save(String value) async { - final preferences = await SharedPreferences.getInstance(); - preferences.setString(widget.keyName, value); - } } diff --git a/lib/src/widget/smart_selector.dart b/lib/src/widget/smart_selector.dart deleted file mode 100644 index 6f992ee..0000000 --- a/lib/src/widget/smart_selector.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class SmartSelector extends StatefulWidget { - final String keyName; - final String? label; - final String placeholder; - final List options; - final SmartSelectorItem Function(String)? itemBuilder; - final Function(String)? onSelected; - final bool serializer; - final String? initialValue; - final bool enabled; - final bool useFirstItemByDefault; - - const SmartSelector({Key? key, - required this.keyName, - required this.placeholder, - required this.options, - required this.initialValue, - this.itemBuilder, - this.onSelected, - this.label, - this.serializer = true, - this.enabled = true, - this.useFirstItemByDefault = true}) - : super(key: key); - - @override - State createState() => _SmartSelectorState(); -} - -class _SmartSelectorState extends State { - String? _selected; - - @override - void initState() { - _selected = widget.initialValue; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return widget.label == null ? _buildBody() : _buildLabel(); - } - - InfoLabel _buildLabel() { - return InfoLabel(label: widget.label!, child: _buildBody()); - } - - SizedBox _buildBody() { - return SizedBox( - width: double.infinity, - child: DropDownButton( - leading: Text(_selected ?? widget.placeholder), - items: widget.options.map(_createOption).toList() - ), - ); - } - - MenuFlyoutItem _createOption(String option) { - var function = widget.itemBuilder ?? _createDefaultItem; - var item = function(option); - return MenuFlyoutItem( - key: item.key, - text: item.text, - onPressed: () => widget.enabled && item.clickable ? _onSelected(option) : {}, - leading: item.leading, - trailing: item.trailing, - selected: item.selected - ); - } - - SmartSelectorItem _createDefaultItem(String name) { - return SmartSelectorItem( - text: SizedBox(width: double.infinity, child: Text(name))); - } - - void _onSelected(String name) { - setState(() { - widget.onSelected?.call(name); - _selected = name; - if(!widget.serializer){ - return; - } - - _serialize(name); - }); - } - - Future _serialize(String value) async { - final preferences = await SharedPreferences.getInstance(); - preferences.setString(widget.keyName, value); - } -} - -class SmartSelectorItem { - final Key? key; - final Widget? leading; - final Widget text; - final Widget? trailing; - final bool selected; - final bool clickable; - - SmartSelectorItem({this.key, - this.leading, - required this.text, - this.trailing, - this.selected = false, - this.clickable = true}); -} diff --git a/lib/src/widget/smart_switch.dart b/lib/src/widget/smart_switch.dart index ff21c3d..d1581da 100644 --- a/lib/src/widget/smart_switch.dart +++ b/lib/src/widget/smart_switch.dart @@ -1,23 +1,17 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:get/get.dart'; import 'package:system_theme/system_theme.dart'; -import '../util/generic_controller.dart'; - class SmartSwitch extends StatefulWidget { - final String keyName; final String label; final bool enabled; - final Function(bool)? onSelected; final Function()? onDisabledPress; - final GenericController controller; + final Rx value; const SmartSwitch( {Key? key, - required this.keyName, required this.label, - required this.controller, - this.onSelected, + required this.value, this.enabled = true, this.onDisabledPress}) : super(key: key); @@ -27,29 +21,24 @@ class SmartSwitch extends StatefulWidget { } class _SmartSwitchState extends State { - Future _save(bool state) async { - final preferences = await SharedPreferences.getInstance(); - preferences.setBool(widget.keyName, state); - } - @override Widget build(BuildContext context) { return InfoLabel( label: widget.label, - child: ToggleSwitch( + child: Obx(() => ToggleSwitch( enabled: widget.enabled, onDisabledPress: widget.onDisabledPress, - checked: widget.controller.value, + checked: widget.value.value, onChanged: _onChanged, style: ToggleSwitchThemeData.standard(ThemeData( checkedColor: _toolTipColor.withOpacity(_checkedOpacity), uncheckedColor: _toolTipColor.withOpacity(_uncheckedOpacity), borderInputColor: _toolTipColor.withOpacity(_uncheckedOpacity), accentColor: _bodyColor - .withOpacity(widget.controller.value + .withOpacity(widget.value.value ? _checkedOpacity : _uncheckedOpacity) - .toAccentColor())))); + .toAccentColor()))))); } Color get _toolTipColor => @@ -66,10 +55,6 @@ class _SmartSwitchState extends State { return; } - setState(() { - widget.controller.value = checked; - widget.onSelected?.call(widget.controller.value); - _save(checked); - }); + setState(() => widget.value(checked)); } } diff --git a/lib/src/widget/username_box.dart b/lib/src/widget/username_box.dart index 00ec451..91afce3 100644 --- a/lib/src/widget/username_box.dart +++ b/lib/src/widget/username_box.dart @@ -1,22 +1,22 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class UsernameBox extends StatelessWidget { - final TextEditingController controller; - final GenericController rebootController; + final GameController _gameController = Get.find(); - const UsernameBox({Key? key, required this.controller, required this.rebootController}) : super(key: key); + UsernameBox({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SmartInput( - keyName: "${rebootController.value ? 'host' : 'game'}_username", - label: "Username", - placeholder: "Type your ${rebootController.value ? 'hosting' : "in-game"} username", - controller: controller, - populate: true - ); + return Obx(() => SmartInput( + keyName: "${_gameController.host.value ? 'host' : 'game'}_username", + label: "Username", + placeholder: "Type your ${_gameController.host.value ? 'hosting' : "in-game"} username", + controller: _gameController.username, + populate: true + )); } } diff --git a/lib/src/widget/version_name_input.dart b/lib/src/widget/version_name_input.dart index b95d11d..ea02d28 100644 --- a/lib/src/widget/version_name_input.dart +++ b/lib/src/widget/version_name_input.dart @@ -1,29 +1,31 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; -import '../model/fortnite_version.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class VersionNameInput extends StatelessWidget { + final GameController _gameController = Get.find(); final TextEditingController controller; - final List versions; - const VersionNameInput({required this.controller, required this.versions, Key? key}) : super(key: key); + + VersionNameInput({Key? key, required this.controller}) : super(key: key); @override Widget build(BuildContext context) { return TextFormBox( - controller: controller, header: "Name", placeholder: "Type the version's name", + controller: controller, autofocus: true, validator: _validate, ); } - String? _validate(String? text){ + String? _validate(String? text) { if (text == null || text.isEmpty) { return 'Invalid version name'; } - if (versions.any((element) => element.name == text)) { + if (_gameController.versions.value.any((element) => element.name == text)) { return 'Existent game version'; } diff --git a/lib/src/widget/version_selector.dart b/lib/src/widget/version_selector.dart index 05c7892..daca65f 100644 --- a/lib/src/widget/version_selector.dart +++ b/lib/src/widget/version_selector.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:async'; import 'dart:io'; @@ -5,24 +7,18 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show showMenu, PopupMenuEntry, PopupMenuItem; -import 'package:reboot_launcher/src/util/version_controller.dart'; +import 'package:get/get.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/smart_selector.dart'; -import '../model/fortnite_version.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; -class VersionSelector extends StatefulWidget { - final VersionController controller; +import 'package:reboot_launcher/src/controller/game_controller.dart'; - const VersionSelector({Key? key, required this.controller}) : super(key: key); +class VersionSelector extends StatelessWidget { + final GameController _gameController = Get.find(); - @override - State createState() => _VersionSelectorState(); -} - -class _VersionSelectorState extends State { - final StreamController _streamController = StreamController(); + VersionSelector({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -32,24 +28,7 @@ class _VersionSelectorState extends State { alignment: AlignmentDirectional.centerStart, child: Row( children: [ - Expanded( - child: StreamBuilder( - stream: _streamController.stream, - builder: (context, snapshot) => SmartSelector( - keyName: "version", - placeholder: "Select a version", - options: widget.controller.isEmpty ? ["No versions available"] : widget.controller.versions - .map((element) => element.name) - .toList(), - useFirstItemByDefault: false, - itemBuilder: (name) => _createVersionItem(name, widget.controller.versions.isNotEmpty), - onSelected: _onSelected, - serializer: false, - initialValue: widget.controller.selectedVersion?.name, - enabled: widget.controller.versions.isNotEmpty - ) - ) - ), + Expanded(child: _createSelector(context)), const SizedBox( width: 16, ), @@ -57,8 +36,7 @@ class _VersionSelectorState extends State { message: "Add a local fortnite build to the versions list", child: Button( child: const Icon(FluentIcons.open_file), - onPressed: () => _openLocalVersionDialog(context) - ), + onPressed: () => _openLocalVersionDialog(context)), ), const SizedBox( width: 16, @@ -73,117 +51,107 @@ class _VersionSelectorState extends State { ))); } - void _onSelected(String selected) { - widget.controller.selectedVersion = widget.controller.versions - .firstWhere((element) => selected == element.name); + Widget _createSelector(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Obx(() => DropDownButton( + leading: Text(_gameController.selectedVersionObs.value?.name ?? + "Select a version"), + items: _gameController.hasNoVersions + ? [_createDefaultVersionItem()] + : _gameController.versions.value + .map((version) => _createVersionItem(context, version)) + .toList())) + ); } - SmartSelectorItem _createVersionItem(String name, bool enabled) { - return SmartSelectorItem( - text: _withListener(name, enabled, SizedBox(width: double.infinity, child: Text(name))), - trailing: const Expanded(child: SizedBox())); + MenuFlyoutItem _createVersionItem( + BuildContext context, FortniteVersion version) { + return MenuFlyoutItem( + text: Listener( + onPointerDown: (event) async { + if (event.kind != PointerDeviceKind.mouse || + event.buttons != kSecondaryMouseButton) { + return; + } + + await _openMenu(context, version, event.position); + }, + child: SizedBox(width: double.infinity, child: Text(version.name))), + trailing: const Expanded(child: SizedBox()), + onPressed: () => _gameController.selectedVersion = version); } - Listener _withListener(String name, bool enabled, Widget child) { - return Listener( - onPointerDown: (event) { - if (event.kind != PointerDeviceKind.mouse || - event.buttons != kSecondaryMouseButton - || !enabled) { - return; - } - - _openMenu(context, name, event.position); - }, - child: child - ); + MenuFlyoutItem _createDefaultVersionItem() { + return MenuFlyoutItem( + text: const SizedBox( + width: double.infinity, child: Text("No versions available")), + trailing: const Expanded(child: SizedBox()), + onPressed: () {}); } void _openDownloadVersionDialog(BuildContext context) async { - await showDialog( + await showDialog( context: context, - builder: (dialogContext) => AddServerVersion( - controller: widget.controller, - onCancel: () => WidgetsBinding.instance - .addPostFrameCallback((_) => showSnackbar( - context, - const Snackbar(content: Text("Download cancelled")) - )) - ) + builder: (dialogContext) => const AddServerVersion() ); - - _streamController.add(true); } void _openLocalVersionDialog(BuildContext context) async { - var result = await showDialog( + await showDialog( context: context, - builder: (context) => AddLocalVersion(controller: widget.controller)); - - if(result == null || !result){ - return; - } - - _streamController.add(false); + builder: (context) => AddLocalVersion()); } - void _openMenu( - BuildContext context, String name, Offset offset) { - showMenu( + Future _openMenu( + BuildContext context, FortniteVersion version, Offset offset) async { + var result = await showMenu( context: context, items: [ const PopupMenuItem(value: 0, child: Text("Open in explorer")), const PopupMenuItem(value: 1, child: Text("Delete")) ], - position: RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy), - ).then((value) { - if(value == 0){ + position: + RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy), + ); + + switch (result) { + case 0: Navigator.of(context).pop(); - Process.run( - "explorer.exe", - [widget.controller.versions.firstWhere((element) => element.name == name).location.path] - ); - return; - } + Process.run("explorer.exe", [version.location.path]); + break; - if(value != 1) { - return; - } + case 1: + _gameController.removeVersion(version); + await _openDeleteDialog(context, version); + Navigator.of(context).pop(); + if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) { + _gameController.selectedVersionObs.value = null; + } - Navigator.of(context).pop(); - var version = widget.controller.removeByName(name); - _openDeleteDialog(context, version); - _streamController.add(false); - if (widget.controller.selectedVersion?.name != name && - widget.controller.isNotEmpty) { - return; - } - - widget.controller.selectedVersion = null; - _streamController.add(false); - }); + break; + } } - void _openDeleteDialog(BuildContext context, FortniteVersion version) { - showDialog( + Future _openDeleteDialog(BuildContext context, FortniteVersion version) { + return showDialog( context: context, builder: (context) => ContentDialog( content: const SizedBox( - height: 32, width: double.infinity, child: Text("Delete associated game path?", textAlign: TextAlign.center)), actions: [ FilledButton( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.green)), child: const Text('Keep'), ), FilledButton( - onPressed: () { + onPressed: () async { Navigator.of(context).pop(); - version.location.delete(); + if (await version.location.exists()) { + version.location.delete(recursive: true); + } }, style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), diff --git a/pubspec.yaml b/pubspec.yaml index d5547de..6baa62e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,8 +24,10 @@ dependencies: archive: ^3.3.1 win32_suspend_process: ^1.0.0 version: ^3.0.2 - unrar_file: ^1.1.0 crypto: ^3.0.2 + async: ^2.8.2 + get: ^4.6.5 + get_storage: ^2.0.3 dev_dependencies: flutter_test: @@ -52,7 +54,7 @@ msix_config: force_update_from_any_version: false publisher_display_name: Reboot publisher: it.auties.reboot - msix_version: 2.0.0.0 + msix_version: 2.1.0.0 logo_path: ./assets/icons/fortnite.ico architecture: x64 capabilities: "internetClient"