diff --git a/lib/src/controller/build_controller.dart b/lib/src/controller/build_controller.dart index 791fd84..9eee730 100644 --- a/lib/src/controller/build_controller.dart +++ b/lib/src/controller/build_controller.dart @@ -5,6 +5,11 @@ import 'package:reboot_launcher/src/model/fortnite_build.dart'; class BuildController extends GetxController { List? builds; FortniteBuild? _selectedBuild; + late RxBool cancelledDownload; + + BuildController() { + cancelledDownload = RxBool(false); + } FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0); diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index d75b974..19b7f5f 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:get/get.dart'; import 'package:path/path.dart' as path; class FortniteVersion { @@ -12,11 +13,21 @@ class FortniteVersion { FortniteVersion({required this.name, required this.location}); static File findExecutable(Directory directory, String 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"); + if(path.basename(directory.path) == "FortniteGame"){ + return File("$directory/Binaries/Win64/$name"); + } + + try{ + var gameDirectory = directory.listSync(recursive: true) + .firstWhereOrNull((element) => path.basename(element.path) == "FortniteGame"); + if(gameDirectory == null){ + return File("${directory.path}/Binaries/Win64/$name"); + } + + return File("${gameDirectory.path}/Binaries/Win64/$name"); + }catch(_){ + return File("${directory.path}/Binaries/Win64/$name"); + } } File get executable { diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart index 3f75784..8f78302 100644 --- a/lib/src/page/home_page.dart +++ b/lib/src/page/home_page.dart @@ -1,5 +1,8 @@ +import 'dart:ui'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/src/page/info_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart'; @@ -9,7 +12,10 @@ import 'package:reboot_launcher/src/widget/window_border.dart'; import 'package:window_manager/window_manager.dart'; import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/reboot.dart'; +import 'package:get/get.dart'; + +import '../controller/build_controller.dart'; +import '../util/reboot.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -67,18 +73,11 @@ class _HomePageState extends State with WindowListener { trailing: WindowTitleBar(focused: _focused)), 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)); - } - - return NavigationBody( + builder: (context, snapshot) => NavigationBody( index: _index, - children: _createPages(snapshot.hasData)); - }) + children: _createPages(snapshot) + ) + ) ), if(_focused && isWin11) @@ -87,30 +86,19 @@ class _HomePageState extends State with WindowListener { ); } - List _createPages(bool data) { + List _createPages(AsyncSnapshot snapshot) { + return [ - data ? const LauncherPage() : _createDownloadWarning(), + LauncherPage( + ready: snapshot.hasData, + error: snapshot.error, + stackTrace: snapshot.stackTrace + ), ServerPage(), const InfoPage() ]; } - Widget _createDownloadWarning() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - ProgressRing(), - SizedBox(height: 16.0), - Text("Updating Reboot DLL...") - ], - ), - ], - ); - } - PaneItem _createPane(String label, IconData icon) { return PaneItem(icon: Icon(icon), title: Text(label)); } diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index 6b5fdfc..eab5eb4 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -31,7 +31,7 @@ class InfoPage extends StatelessWidget { ), const Expanded( child: Align( - alignment: Alignment.bottomLeft, child: Text("Version 3.5${kDebugMode ? '-DEBUG' : ''}"))) + alignment: Alignment.bottomLeft, child: Text("Version 3.6${kDebugMode ? '-DEBUG' : ''}"))) ], ); } diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 07f1457..aa6be0b 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -1,19 +1,89 @@ +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/widget/deployment_selector.dart'; import 'package:reboot_launcher/src/widget/launch_button.dart'; import 'package:reboot_launcher/src/widget/username_box.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart'; +import 'package:url_launcher/url_launcher.dart'; -class LauncherPage extends StatelessWidget { - const LauncherPage({Key? key}) : super(key: key); +import '../widget/warning_info.dart'; + +class LauncherPage extends StatefulWidget { + final bool ready; + final Object? error; + final StackTrace? stackTrace; + + const LauncherPage( + {Key? key, required this.ready, required this.error, this.stackTrace}) + : super(key: key); + + @override + State createState() => _LauncherPageState(); +} + +class _LauncherPageState extends State { + final BuildController _buildController = Get.find(); + bool shouldWriteError = true; + + @override + void initState() { + _buildController.cancelledDownload + .listen((value) => value ? _onCancelWarning() : {}); + super.initState(); + } + + void _onCancelWarning() { + WidgetsBinding.instance.addPostFrameCallback((_) { + showSnackbar(context, + const Snackbar(content: Text("Download cancelled"))); + _buildController.cancelledDownload.value = false; + }); + } @override Widget build(BuildContext context) { + if (!widget.ready && widget.error == null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + ProgressRing(), + SizedBox(height: 16.0), + Text("Updating Reboot DLL...") + ], + ), + ], + ); + } + return Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ + if(widget.error != null) + WarningInfo( + text: "Cannot update Reboot DLL", + icon: FluentIcons.info, + severity: InfoBarSeverity.warning, + onPressed: () async { + if (shouldWriteError) { + await errorFile.writeAsString( + "Error: ${widget.error}\nStacktrace: ${widget.stackTrace}", + mode: FileMode.write + ); + shouldWriteError = false; + } + + launchUrl(errorFile.uri); + }, + ), UsernameBox(), VersionSelector(), DeploymentSelector(enabled: true), diff --git a/lib/src/page/server_page.dart b/lib/src/page/server_page.dart index c57713a..0485081 100644 --- a/lib/src/page/server_page.dart +++ b/lib/src/page/server_page.dart @@ -1,7 +1,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/widget/lawin_warning.dart'; +import 'package:reboot_launcher/src/widget/warning_info.dart'; import 'package:reboot_launcher/src/widget/local_server_switch.dart'; import 'package:reboot_launcher/src/widget/port_input.dart'; @@ -20,7 +20,9 @@ class ServerPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if(_serverController.warning.value) - LawinWarning( + WarningInfo( + text: "The lawin server handles authentication and parties, not game hosting", + icon: FluentIcons.accept, onPressed: () => _serverController.warning.value = false ), HostInput(), diff --git a/lib/src/util/build.dart b/lib/src/util/build.dart index e3f53d8..8e6b1a7 100644 --- a/lib/src/util/build.dart +++ b/lib/src/util/build.dart @@ -96,7 +96,8 @@ Future downloadManifestBuild( Future downloadArchiveBuild(String archiveUrl, String destination, Function(double) onProgress, Function() onRar) async { var tempFile = File( - "${Platform.environment["Temp"]}\\FortniteBuild${Random.secure().nextInt(1000000)}.rar"); + "$destination\\.temp\\FortniteBuild${Random.secure().nextInt(1000000)}.rar"); + await tempFile.parent.create(recursive: true); try { var client = http.Client(); var request = http.Request("GET", Uri.parse(archiveUrl)); diff --git a/lib/src/util/os.dart b/lib/src/util/os.dart index a9e69d9..048240d 100644 --- a/lib/src/util/os.dart +++ b/lib/src/util/os.dart @@ -1,5 +1,10 @@ import 'dart:io'; +import 'package:flutter_desktop_folder_picker/flutter_desktop_folder_picker.dart'; +import 'package:path/path.dart' as path; + +File errorFile = File("${Platform.environment["Temp"]}/error.txt"); + const int appBarSize = 2; final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); @@ -11,4 +16,31 @@ bool get isWin11 { var intBuild = int.tryParse(result); return intBuild != null && intBuild > 22000; +} + +Future openFilePicker(String title) async => + FlutterDesktopFolderPicker.openFolderPickerDialog(title: title); + +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; + } + + return 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 index f9401fc..a34bea6 100644 --- a/lib/src/widget/add_local_version.dart +++ b/lib/src/widget/add_local_version.dart @@ -20,8 +20,10 @@ class AddLocalVersion extends StatelessWidget { return Form( child: Builder( builder: (formContext) => ContentDialog( - constraints: - const BoxConstraints(maxWidth: 368, maxHeight: 278), + style: const ContentDialogThemeData( + padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) + ), + constraints: const BoxConstraints(maxWidth: 368, maxHeight: 258), content: _createLocalVersionDialogBody(), actions: _createLocalVersionActions(formContext)))); } @@ -65,16 +67,17 @@ class AddLocalVersion extends StatelessWidget { autofocus: true, validator: (text) { if (text == null || text.isEmpty) { - return 'Invalid version name'; + return 'Empty version name'; } if (_gameController.versions.value.any((element) => element.name == text)) { - return 'Existent game version'; + return 'This version already exists'; } return null; }, ), + SelectFile( label: "Location", placeholder: "Type the game folder", @@ -87,16 +90,12 @@ class AddLocalVersion extends StatelessWidget { String? _checkGameFolder(text) { if (text == null || text.isEmpty) { - return 'Invalid game path'; + return 'Empty game path'; } var directory = Directory(text); if (!directory.existsSync()) { - return "Nonexistent game path"; - } - - if (!directory.existsSync()) { - return "Nonexistent game path"; + return "Directory doesn't exist"; } if (!FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe").existsSync()) { diff --git a/lib/src/widget/add_server_version.dart b/lib/src/widget/add_server_version.dart index 07062b8..2489bb6 100644 --- a/lib/src/widget/add_server_version.dart +++ b/lib/src/widget/add_server_version.dart @@ -33,6 +33,10 @@ class _AddServerVersionState extends State { late Future _future; DownloadStatus _status = DownloadStatus.none; double _downloadProgress = 0; + DateTime? _downloadStartTime; + DateTime? _lastUpdateTime; + Duration? _lastUpdateTimeLeft; + String? _lastUpdateTimeFormatted; String? _error; Process? _manifestDownloadProcess; CancelableOperation? _driveDownloadOperation; @@ -62,7 +66,7 @@ class _AddServerVersionState extends State { if (_manifestDownloadProcess != null) { loadBinary("stop.bat", false) .then((value) => Process.runSync(value.path, [])); // kill doesn't work :/ - _onCancelDownload(); + _buildController.cancelledDownload.value = true; return; } @@ -71,13 +75,7 @@ class _AddServerVersionState extends State { } _driveDownloadOperation!.cancel(); - _onCancelDownload(); - } - - void _onCancelDownload() { - WidgetsBinding.instance.addPostFrameCallback((_) => - showSnackbar(context, - const Snackbar(content: Text("Download cancelled")))); + _buildController.cancelledDownload.value = true; } @override @@ -85,8 +83,10 @@ class _AddServerVersionState extends State { return Form( child: Builder( builder: (context) => ContentDialog( - constraints: - const BoxConstraints(maxWidth: 368, maxHeight: 338), + style: const ContentDialogThemeData( + padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) + ), + constraints: const BoxConstraints(maxWidth: 368, maxHeight: 321), content: _createDownloadVersionBody(), actions: _createDownloadVersionOption(context)))); } @@ -131,7 +131,7 @@ class _AddServerVersionState extends State { } void _onClose() { - Navigator.of(context).pop(true); + Navigator.of(context).pop(); } void _startDownload(BuildContext context) async { @@ -198,6 +198,7 @@ class _AddServerVersionState extends State { return; } + _downloadStartTime ??= DateTime.now(); setState(() { _status = DownloadStatus.downloading; _downloadProgress = progress; @@ -209,16 +210,24 @@ class _AddServerVersionState extends State { future: _future, builder: (context, snapshot) { if (snapshot.hasError) { - snapshot.printError(); - return Text("Cannot fetch builds: ${snapshot.error}", - textAlign: TextAlign.center); + 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 const InfoLabel( + return InfoLabel( label: "Fetching builds...", - child: SizedBox( - width: double.infinity, child: ProgressBar()), + child: Container( + padding: const EdgeInsets.only(bottom: 16.0), + width: double.infinity, + child: const ProgressBar() + ), ); } @@ -234,53 +243,130 @@ class _AddServerVersionState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const BuildSelector(), + + const SizedBox(height: 16.0), + VersionNameInput(controller: _nameController), + SelectFile( label: "Destination", placeholder: "Type the download destination", windowTitle: "Select download destination", - allowNavigator: false, controller: _pathController, - validator: _checkDownloadDestination), + validator: _checkDownloadDestination + ), ], ); case DownloadStatus.downloading: - return InfoLabel( - label: "Downloading", - child: InfoLabel( - label: "${_downloadProgress.round()}%", - child: SizedBox( - width: double.infinity, - child: ProgressBar(value: _downloadProgress.toDouble()))), + 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 const InfoLabel( - label: "Extracting", - child: SizedBox(width: double.infinity, child: ProgressBar()) + return const Padding( + padding: EdgeInsets.only(bottom: 16), + child: InfoLabel( + label: "Extracting...", + child: SizedBox(width: double.infinity, child: ProgressBar()) + ), ); case DownloadStatus.done: - return const SizedBox( - width: double.infinity, - child: Text("The download was completed successfully!", - textAlign: TextAlign.center)); + 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 SizedBox( - width: double.infinity, - child: Text( - "An exception was thrown during the download process:$_error", - textAlign: TextAlign.center)); + 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'; } - if (Directory(text).existsSync()) { - return "Existent download destination"; - } - return null; } } diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/launch_button.dart index 5d86438..a30b7ec 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/launch_button.dart @@ -110,16 +110,9 @@ class _LaunchButtonState extends State { return; } - if (!line.contains("Game Engine Initialized")) { - return; + if (line.contains("[UFortUIManagerWidget_NUI::SetUIState]") && line.contains("FrontEnd")) { + _injectOrShowError(_gameController.host.value ? "reboot.dll" : "console.dll"); } - - if (!_gameController.host.value) { - _injectOrShowError("console.dll"); - return; - } - - _injectOrShowError("reboot.dll"); } Future _onError(exception) { diff --git a/lib/src/widget/lawin_warning.dart b/lib/src/widget/lawin_warning.dart deleted file mode 100644 index 7886053..0000000 --- a/lib/src/widget/lawin_warning.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class LawinWarning extends StatelessWidget { - final VoidCallback onPressed; - - const LawinWarning({Key? key, required this.onPressed}) : super(key: key); - - @override - Widget build(BuildContext context) { - return InfoBar( - title: const Text( - "The lawin server handles authentication and parties, not game hosting"), - action: IconButton( - icon: const Icon(FluentIcons.accept), - onPressed: onPressed - ) - ); - } -} diff --git a/lib/src/widget/scan_local_version.dart b/lib/src/widget/scan_local_version.dart new file mode 100644 index 0000000..4185e63 --- /dev/null +++ b/lib/src/widget/scan_local_version.dart @@ -0,0 +1,170 @@ +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:reboot_launcher/src/util/os.dart'; +import 'package:reboot_launcher/src/widget/select_file.dart'; +import 'package:path/path.dart' as path; + + +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 [ + FilledButton( + onPressed: () => Navigator.of(context).pop(), + style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), + 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: FilledButton( + onPressed: () => Navigator.of(context).pop(), + style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), + 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: [ + SelectFile( + label: "Location", + placeholder: "Type the folder to scan", + windowTitle: "Select the folder to scan", + controller: _folderController, + validator: _checkScanFolder) + ], + ); + } + 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 const Padding( + padding: EdgeInsets.only(bottom: 16), + child: InfoLabel( + label: "Searching...", + child: 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/select_file.dart b/lib/src/widget/select_file.dart index 0d3bec2..5fdc80e 100644 --- a/lib/src/widget/select_file.dart +++ b/lib/src/widget/select_file.dart @@ -1,5 +1,7 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter_desktop_folder_picker/flutter_desktop_folder_picker.dart'; +import 'package:flutter/foundation.dart'; + +import '../util/os.dart'; class SelectFile extends StatefulWidget { final String label; @@ -24,6 +26,8 @@ class SelectFile extends StatefulWidget { } class _SelectFileState extends State { + bool _selecting = false; + @override Widget build(BuildContext context) { return InfoLabel( @@ -34,19 +38,36 @@ class _SelectFileState extends State { child: TextFormBox( controller: widget.controller, placeholder: widget.placeholder, - validator: widget.validator)), + validator: widget.validator, + hidePadding: true + ) + ), if (widget.allowNavigator) const SizedBox(width: 8.0), if (widget.allowNavigator) - IconButton( - icon: const Icon(FluentIcons.open_folder_horizontal), - onPressed: _onPressed) + Padding( + padding: const EdgeInsets.only(bottom: 21.0), + child: Tooltip( + message: "Select a folder", + child: Button( + onPressed: _onPressed, + child: const Icon(FluentIcons.open_folder_horizontal) + ), + ), + ) ], - )); + ) + ); } - void _onPressed() async { - var result = await FlutterDesktopFolderPicker.openFolderPickerDialog( - title: "Select the game folder"); - widget.controller.text = result ?? ""; + void _onPressed() { + if(_selecting){ + showSnackbar(context, const Snackbar(content: Text("Folder selector is already opened"))); + return; + } + + _selecting = true; + compute(openFilePicker, "Select the game folder") + .then((value) => widget.controller.text = value ?? "") + .then((_) => _selecting = false); } -} +} \ No newline at end of file diff --git a/lib/src/widget/version_name_input.dart b/lib/src/widget/version_name_input.dart index ea02d28..e5f2bf3 100644 --- a/lib/src/widget/version_name_input.dart +++ b/lib/src/widget/version_name_input.dart @@ -22,11 +22,11 @@ class VersionNameInput extends StatelessWidget { String? _validate(String? text) { if (text == null || text.isEmpty) { - return 'Invalid version name'; + return 'Empty version name'; } if (_gameController.versions.value.any((element) => element.name == text)) { - return 'Existent game version'; + return 'This version already exists'; } return null; diff --git a/lib/src/widget/version_selector.dart b/lib/src/widget/version_selector.dart index 4a0775e..06146fd 100644 --- a/lib/src/widget/version_selector.dart +++ b/lib/src/widget/version_selector.dart @@ -14,57 +14,72 @@ import 'package:reboot_launcher/src/widget/add_server_version.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/widget/scan_local_version.dart'; + +import '../controller/build_controller.dart'; class VersionSelector extends StatelessWidget { final GameController _gameController = Get.find(); + final bool enableScanner; - VersionSelector({Key? key}) : super(key: key); + VersionSelector({Key? key, this.enableScanner = false}) : super(key: key); @override Widget build(BuildContext context) { - return Tooltip( - message: "The version of Fortnite to launch", - child: InfoLabel( - label: "Version", - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Row( - children: [ - Expanded(child: _createSelector(context)), + return InfoLabel( + label: "Version", + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + children: [ + Expanded(child: _createSelector(context)), + const SizedBox( + width: 16, + ), + Tooltip( + message: "Add a local fortnite build to the versions list", + child: Button( + child: const Icon(FluentIcons.open_file), + onPressed: () => _openAddLocalVersionDialog(context)), + ), + const SizedBox( + width: 16, + ), + if(enableScanner) + Tooltip( + message: "Scan all fortnite builds in a directory", + child: Button( + child: const Icon(FluentIcons.site_scan), + onPressed: () => _openScanLocalVersionDialog(context)), + ), + if(enableScanner) const SizedBox( width: 16, ), - Tooltip( - message: "Add a local fortnite build to the versions list", - child: Button( - child: const Icon(FluentIcons.open_file), - onPressed: () => _openLocalVersionDialog(context)), - ), - const SizedBox( - width: 16, - ), - Tooltip( - message: "Download a fortnite build from the archive", - child: Button( - child: const Icon(FluentIcons.download), - onPressed: () => _openDownloadVersionDialog(context)), - ) - ], - ))), - ); + Tooltip( + message: "Download a fortnite build from the archive", + child: Button( + child: const Icon(FluentIcons.download), + onPressed: () => _openDownloadVersionDialog(context)), + ), + ], + ))); } 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())) + return Tooltip( + message: "The version of Fortnite to launch", + child: 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())) + ), ); } @@ -100,12 +115,18 @@ class VersionSelector extends StatelessWidget { ); } - void _openLocalVersionDialog(BuildContext context) async { + void _openAddLocalVersionDialog(BuildContext context) async { await showDialog( context: context, builder: (context) => AddLocalVersion()); } + void _openScanLocalVersionDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (context) => ScanLocalVersion()); + } + Future _openMenu( BuildContext context, FortniteVersion version, Offset offset) async { var result = await showMenu( @@ -142,7 +163,7 @@ class VersionSelector extends StatelessWidget { builder: (context) => ContentDialog( content: const SizedBox( width: double.infinity, - child: Text("Delete associated game path?", + child: Text("Do you want to also delete the files for this version?", textAlign: TextAlign.center)), actions: [ FilledButton( diff --git a/lib/src/widget/warning_info.dart b/lib/src/widget/warning_info.dart new file mode 100644 index 0000000..6dc957a --- /dev/null +++ b/lib/src/widget/warning_info.dart @@ -0,0 +1,28 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +class WarningInfo extends StatelessWidget { + final String text; + final VoidCallback onPressed; + final IconData icon; + final InfoBarSeverity severity; + + const WarningInfo( + {Key? key, + required this.text, + required this.icon, + required this.onPressed, + this.severity = InfoBarSeverity.info}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InfoBar( + severity: severity, + title: Text(text), + action: IconButton( + icon: Icon(icon), + onPressed: onPressed + ) + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index e0ebe21..f2b31e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "3.5.0" +version: "3.6.0" publish_to: 'none' @@ -48,7 +48,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 3.5.0.0 + msix_version: 3.6.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64