From cd7db4cf71b6b459c729da581ef5976dce50e0f1 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Sun, 4 Jun 2023 20:08:50 +0200 Subject: [PATCH] 8.1 --- lib/src/ui/controller/server_controller.dart | 27 +- .../ui/controller/settings_controller.dart | 7 +- lib/src/ui/page/browse_page.dart | 2 +- lib/src/ui/page/home_page.dart | 42 +- lib/src/ui/page/hosting_page.dart | 8 +- lib/src/ui/page/info_page.dart | 397 ++++++++++++------ lib/src/ui/page/launcher_page.dart | 8 +- lib/src/ui/page/server_page.dart | 38 +- lib/src/ui/page/settings_page.dart | 7 +- lib/src/ui/widget/home/launch_button.dart | 22 +- .../widget/{shared => home}/setting_tile.dart | 29 +- lib/src/util/checks.dart | 8 + pubspec.yaml | 4 +- 13 files changed, 415 insertions(+), 184 deletions(-) rename lib/src/ui/widget/{shared => home}/setting_tile.dart (71%) diff --git a/lib/src/ui/controller/server_controller.dart b/lib/src/ui/controller/server_controller.dart index 5f4cdaa..95e0c0a 100644 --- a/lib/src/ui/controller/server_controller.dart +++ b/lib/src/ui/controller/server_controller.dart @@ -3,20 +3,18 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:jaguar/jaguar.dart'; import '../../model/server_type.dart'; import '../../util/server.dart'; class ServerController extends GetxController { - static const String _serverName = "127.0.0.1"; - static const String _serverPort = "3551"; + static const String _kDefaultServerHost = "127.0.0.1"; + static const String _kDefaultServerPort = "3551"; late final GetStorage _storage; late final TextEditingController host; late final TextEditingController port; late final Rx type; - late final RxBool warning; late RxBool started; late RxBool detached; HttpServer? remoteServer; @@ -39,20 +37,31 @@ class ServerController extends GetxController { host.addListener(() => _storage.write("${type.value.id}_host", host.text)); port = TextEditingController(text: _readPort()); port.addListener(() => _storage.write("${type.value.id}_port", port.text)); - warning = RxBool(_storage.read("lawin_value") ?? true); - warning.listen((value) => _storage.write("lawin_value", value)); detached = RxBool(_storage.read("detached") ?? false); - warning.listen((value) => _storage.write("detached", value)); + detached.listen((value) => _storage.write("detached", value)); + } + + void reset() async { + await stop(); + type.value = ServerType.values.elementAt(0); + for(var type in ServerType.values){ + _storage.write("${type.id}_host", null); + _storage.write("${type.id}_port", null); + } + + host.text = type.value != ServerType.remote ? _kDefaultServerHost : ""; + port.text = _kDefaultServerPort; + detached.value = false; } String _readHost() { String? value = _storage.read("${type.value.id}_host"); return value != null && value.isNotEmpty ? value - : type.value != ServerType.remote ? _serverName : ""; + : type.value != ServerType.remote ? _kDefaultServerHost : ""; } String _readPort() { - return _storage.read("${type.value.id}_port") ?? _serverPort; + return _storage.read("${type.value.id}_port") ?? _kDefaultServerPort; } Future stop() async { diff --git a/lib/src/ui/controller/settings_controller.dart b/lib/src/ui/controller/settings_controller.dart index 5cb8b59..2e4eb91 100644 --- a/lib/src/ui/controller/settings_controller.dart +++ b/lib/src/ui/controller/settings_controller.dart @@ -21,6 +21,8 @@ class SettingsController extends GetxController { late final TextEditingController authDll; late final TextEditingController matchmakingIp; late final RxBool autoUpdate; + late final RxBool firstRun; + late final RxInt index; late double width; late double height; late double? offsetX; @@ -45,8 +47,11 @@ class SettingsController extends GetxController { offsetX = _storage.read("offset_x"); offsetY = _storage.read("offset_y"); autoUpdate = RxBool(_storage.read("auto_update") ?? _kDefaultAutoUpdate); - autoUpdate.listen((value) async => _storage.write("auto_update", value)); + autoUpdate.listen((value) => _storage.write("auto_update", value)); scrollingDistance = 0.0; + firstRun = RxBool(_storage.read("fr") ?? true); + firstRun.listen((value) => _storage.write("fr", value)); + index = RxInt(firstRun() ? 0 : 1); } TextEditingController _createController(String key, String name) { diff --git a/lib/src/ui/page/browse_page.dart b/lib/src/ui/page/browse_page.dart index f60a6f2..81d237d 100644 --- a/lib/src/ui/page/browse_page.dart +++ b/lib/src/ui/page/browse_page.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart'; +import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; diff --git a/lib/src/ui/page/home_page.dart b/lib/src/ui/page/home_page.dart index e73c529..9e04ab4 100644 --- a/lib/src/ui/page/home_page.dart +++ b/lib/src/ui/page/home_page.dart @@ -9,6 +9,7 @@ import 'package:reboot_launcher/src/ui/page/server_page.dart'; import 'package:reboot_launcher/src/ui/page/settings_page.dart'; import 'package:window_manager/window_manager.dart'; +import '../controller/game_controller.dart'; import '../controller/settings_controller.dart'; import '../widget/os/window_border.dart'; import '../widget/os/window_buttons.dart'; @@ -25,17 +26,15 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { static const double _kDefaultPadding = 12.0; static const int _kPagesLength = 5; - + final SettingsController _settingsController = Get.find(); - final GlobalKey _searchKey = GlobalKey(); final FocusNode _searchFocusNode = FocusNode(); final TextEditingController _searchController = TextEditingController(); final Rxn> _searchItems = Rxn(); final RxBool _focused = RxBool(true); - final RxInt _index = RxInt(0); final List> _navigators = List.generate(_kPagesLength, (index) => GlobalKey()); - final List _navigationStatus = List.generate(_kPagesLength, (index) => RxBool(false)); + final List _navigationStatus = List.generate(_kPagesLength, (index) => RxInt(0)); @override bool get wantKeepAlive => true; @@ -145,25 +144,25 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA }); Function()? _onBack() { - var navigator = _navigators[_index.value].currentState; + var navigator = _navigators[_settingsController.index.value].currentState; if(navigator == null || !navigator.mounted || !navigator.canPop()){ return null; } - var status = _navigationStatus[_index.value]; - if(!status.value){ + var status = _navigationStatus[_settingsController.index.value]; + if(status.value <= 0){ return null; } return () async { Navigator.pop(navigator.context); - status.value = false; + status.value -= 1; }; } void _onIndexChanged(int index) { - _navigationStatus[_index()].value = false; - _index.value = index; + _navigationStatus[_settingsController.index()].value = 0; + _settingsController.index.value = index; } TextBox get _autoSuggestBox => TextBox( @@ -182,14 +181,14 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA int? get _selectedIndex { var searchItems = _searchItems(); if (searchItems == null) { - return _index(); + return _settingsController.index(); } - if(_index() >= _allItems.length){ + if(_settingsController.index() >= _allItems.length){ return null; } - var indexOnScreen = searchItems.indexOf(_allItems[_index()]); + var indexOnScreen = searchItems.indexOf(_allItems[_settingsController.index()]); if (indexOnScreen.isNegative) { return null; } @@ -208,28 +207,27 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA ]; List get _items => _searchItems() ?? [ + PaneItem( + title: const Text("Tutorial"), + icon: const Icon(FluentIcons.info), + body: InfoPage(_navigators[0], _navigationStatus[0]) + ), PaneItem( title: const Text("Play"), icon: const Icon(FluentIcons.game), - body: LauncherPage(_navigators[0], _navigationStatus[0]) + body: LauncherPage(_navigators[1], _navigationStatus[1]) ), PaneItem( title: const Text("Host"), icon: const Icon(FluentIcons.server_processes), - body: HostingPage(_navigators[1], _navigationStatus[1]) + body: HostingPage(_navigators[2], _navigationStatus[2]) ), PaneItem( title: const Text("Backend"), icon: const Icon(FluentIcons.user_window), - body: ServerPage(_navigators[2], _navigationStatus[2]) - ), - - PaneItem( - title: const Text("Tutorial"), - icon: const Icon(FluentIcons.info), - body: InfoPage(_navigators[3], _navigationStatus[3]) + body: ServerPage(_navigators[3], _navigationStatus[3]) ), ]; diff --git a/lib/src/ui/page/hosting_page.dart b/lib/src/ui/page/hosting_page.dart index 2f82c54..d162942 100644 --- a/lib/src/ui/page/hosting_page.dart +++ b/lib/src/ui/page/hosting_page.dart @@ -4,14 +4,14 @@ import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/ui/controller/settings_controller.dart'; import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart'; +import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; import '../../model/update_status.dart'; import 'browse_page.dart'; class HostingPage extends StatefulWidget { final GlobalKey navigatorKey; - final RxBool nestedNavigation; + final RxInt nestedNavigation; const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override @@ -71,7 +71,7 @@ class _HostingPageState extends State with AutomaticKeepAliveClient class _HostPage extends StatefulWidget { final GlobalKey navigatorKey; - final RxBool nestedNavigation; + final RxInt nestedNavigation; const _HostPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override @@ -171,7 +171,7 @@ class _HostPageState extends State<_HostPage> with AutomaticKeepAliveClientMixin content: Button( onPressed: () { widget.navigatorKey.currentState?.pushNamed('browse'); - widget.nestedNavigation.value = true; + widget.nestedNavigation.value += 1; }, child: const Text("Browse") ) diff --git a/lib/src/ui/page/info_page.dart b/lib/src/ui/page/info_page.dart index d452a31..2fad66c 100644 --- a/lib/src/ui/page/info_page.dart +++ b/lib/src/ui/page/info_page.dart @@ -1,14 +1,24 @@ +import 'dart:async'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; +import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; +import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; +import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; +import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/util/server.dart'; +import '../../util/os.dart'; +import '../controller/game_controller.dart'; import '../controller/settings_controller.dart'; -import '../widget/shared/fluent_card.dart'; +import '../dialog/dialog.dart'; +import '../dialog/dialog_button.dart'; +import '../widget/home/version_selector.dart'; class InfoPage extends StatefulWidget { final GlobalKey navigatorKey; - final RxBool nestedNavigation; + final RxInt nestedNavigation; const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override @@ -16,29 +26,6 @@ class InfoPage extends StatefulWidget { } class _InfoPageState extends State with AutomaticKeepAliveClientMixin { - final List _elseTitles = [ - "Open the home page", - "Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port", - "Type your username if you haven't already", - "Select the exact version that the host is using from the dropdown menu\n If necessary, install it using the download button", - "As you want to play, select client from the dropdown menu", - "Click launch to open the game\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again", - "Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1" - ]; - final List _ownTitles = [ - "Open the home page", - "Type 127.0.0.1 as the matchmaking host\n If you didn't know, 127.0.0.1 is the ip for your local machine", - "Type your username if you haven't already", - "Select the version you want to host\n If necessary, install it using the download button\n Check the supported versions in #info in the Discord server\n Fortnite 7.40 is the best one to use usually", - "As you want to host, select headless server from the dropdown menu\n If the headless server doesn't work for your version, use the normal server instead\n The difference between the two is that the first doesn't render a fortnite instance\n Both will not allow you to play, only to host\n You will see an infinite loading screen when using the normal server\n If you want to also play continue reading", - "Click launch to start the server and wait until the Reboot GUI shows up\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again", - "To allow your friends to join your server, follow the instructions on playit.gg\n If you are an advanced user, open port 7777 on your router\n Finally, share your playit ip or public IPv4 address with your friends\n If you just want to play by yourself, skip this step", - "When you want to start the game, click on the 'Start Bus Countdown' button\n Before clicking that button, make all of your friends join\n This is because joining mid-game isn't allowed", - "If you also want to play, start a client by selecting Client from the dropdown menu\n Don't close or open again the launcher, use the same window\n Remember to keep both the headless server(or server) and client open\n If you want to close the client or server, simply switch between them using the menu\n The launcher will remember what instances you have opened", - "Click launch to open the game\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again", - "Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1" - ]; - final SettingsController _settingsController = Get.find(); late final ScrollController _controller; @@ -61,120 +48,284 @@ class _InfoPageState extends State with AutomaticKeepAliveClientMixin } @override - Widget build(BuildContext context) => Navigator( - key: widget.navigatorKey, - initialRoute: "home", - onGenerateRoute: (settings) { - var screen = _createScreen(settings.name); - return FluentPageRoute( - builder: (context) => screen, - settings: settings - ); - }, - ); + Widget build(BuildContext context) { + super.build(context); + return Navigator( + key: widget.navigatorKey, + initialRoute: "introduction", + onGenerateRoute: (settings) { + var screen = _createScreen(settings.name); + return FluentPageRoute( + builder: (context) => screen, + settings: settings + ); + }, + ); + } Widget _createScreen(String? name) { switch(name){ - case "home": - return _homeScreen; - case "else": - return _createInstructions(false); - case "own": - return _createInstructions(true); + case "introduction": + return _IntroductionPage(widget.navigatorKey, widget.nestedNavigation); + case "play": + return _PlayPage(widget.navigatorKey, widget.nestedNavigation); default: throw Exception("Unknown page: $name"); } } +} - Widget get _homeScreen => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, +class _IntroductionPage extends StatefulWidget { + final GlobalKey navigatorKey; + final RxInt nestedNavigation; + const _IntroductionPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); + + @override + State<_IntroductionPage> createState() => _IntroductionPageState(); +} + +class _IntroductionPageState extends State<_IntroductionPage> { + @override + Widget build(BuildContext context) { + return Column( children: [ - _createCardWidget( - text: "Play on someone else's server", - description: "If one of your friends is hosting a game server, click here", - onClick: () { - widget.navigatorKey.currentState?.pushNamed("else"); - widget.nestedNavigation.value = true; - } + Expanded( + child: ListView( + children: [ + SettingTile( + title: 'What is Project Reboot?', + subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. The project was started on Discord by Milxnor and it\'s still being actively developed. Also, it\'s open source on Github!', + titleStyle: FluentTheme.of(context).typography.title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'What is a game server?', + subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. You can join someone else\'s game server, or host one on your PC. You can host your own game server by going to the "Host" tab. Just like in Minecraft, a client needs to connect to a server hosted on a certain IP or domain. In short, remember that you need both a client and a server to play!', + titleStyle: FluentTheme.of(context).typography.title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'What is a client?', + subtitle: 'A client is the actual Fortnite game. You can download any version of Fortnite from the launcher in the "Play" tab. You can also import versions from your local PC, but remember that these may be corrupted. If a local version doesn\'t work, try installing it from the launcher before reporting a bug.', + titleStyle: FluentTheme.of(context).typography.title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'What is a backend?', + subtitle: 'A backend is the program that handles authentication, parties and voice chats. By default, a LawinV1 server will be started for you to play. You can use also use a backend running locally, that is on your PC, or remotely, that is on another PC. Changing the backend settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! If you need to restore these settings, click on "Restore Defaults" in the "Backend" tab.', + titleStyle: FluentTheme.of(context).typography.title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'Do I need to update DLLs?', + subtitle: 'No, all the files that the launcher uses are automatically updated. Though, you need to update the launcher yourself if you haven\'t downloaded it from the Microsoft Store. You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. Unless you are an advanced user, changing these options is not recommended', + titleStyle: FluentTheme.of(context).typography.title, + contentWidth: null, + ), + const SizedBox( + height: 8.0, + ), + SettingTile( + title: 'Where can I report bugs or ask for new features?', + subtitle: 'Go to the "Settings" tab and click on report bug. Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement', + titleStyle: FluentTheme.of(context).typography.title, + contentWidth: null, + ) + ], + ), ), - const SizedBox( - width: 8.0, + height: 8.0, ), - - _createCardWidget( - text: "Host your own server", - description: "If you want to create your own server to invite your friends or to play around by yourself, click here", - onClick: () { - widget.navigatorKey.currentState?.pushNamed("own"); - widget.nestedNavigation.value = true; - } + SizedBox( + width: double.infinity, + height: 48, + child: Button( + child: const Align( + alignment: Alignment.center, + child: Text("How do I play?") + ), + onPressed: () { + widget.navigatorKey.currentState?.pushNamed("play"); + widget.nestedNavigation.value += 1; + } + ), ) - ] - ); + ], + ); + } +} - SizedBox _createInstructions(bool own) { - var titles = own ? _ownTitles : _elseTitles; - var codeName = own ? "own" : "else"; - return SizedBox.expand( - child: ListView.separated( - controller: _controller, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.only( - right: 20.0 - ), - child: FluentCard( - child: ListTile( - title: SelectableText("${index + 1}. ${titles[index]}"), - subtitle: Padding( - padding: const EdgeInsets.only(top: 12.0), - child: Image.asset("assets/images/tutorial_${codeName}_${index + 1}.png"), - ) - ) - ), - ), - separatorBuilder: (context, index) => const SizedBox(height: 8.0), - itemCount: titles.length, - ) - ); +class _PlayPage extends StatefulWidget { + final GlobalKey navigatorKey; + final RxInt nestedNavigation; + const _PlayPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); + + @override + State<_PlayPage> createState() => _PlayPageState(); +} + +class _PlayPageState extends State<_PlayPage> { + final GameController _gameController = Get.find(); + final SettingsController _settingsController = Get.find(); + final RxBool _localGameServer = RxBool(true); + final TextEditingController _remoteGameServerController = TextEditingController(); + final StreamController _remoteGameServerStream = StreamController(); + + @override + void initState() { + var ip = _settingsController.matchmakingIp.text; + _remoteGameServerController.text = isLocalHost(ip) ? "" : ip; + _remoteGameServerController.addListener(() => _remoteGameServerStream.add(null)); + super.initState(); } - Widget _createCardWidget({required String text, required String description, required Function() onClick}) => Expanded( - child: SizedBox( - height: double.infinity, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onClick, - child: FluentCard( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - text, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold - ), - ), - - const SizedBox( - height: 8.0, - ), - - Text( - description, - textAlign: TextAlign.center - ), - ], + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: ListView( + children: [ + SettingTile( + title: '1. Select a username', + subtitle: 'Choose a name for other players to see while you are in-game', + titleStyle: FluentTheme.of(context).typography.title, + expandedContentHeaderHeight: 80, + contentWidth: 0, + expandedContent: [ + SettingTile( + title: "Username", + subtitle: "The username that other players will see when you are in game", + isChild: true, + content: TextFormBox( + placeholder: "Username", + controller: _gameController.username, + autovalidateMode: AutovalidateMode.always + ), + ), + ], + ), + const SizedBox( + height: 16.0, + ), + SettingTile( + title: '2. Download Fortnite', + subtitle: 'Download or import the version of Fortnite you want to play. Make sure that it\'s the same as the game server\'s you want to join!', + titleStyle: FluentTheme.of(context).typography.title, + expandedContentHeaderHeight: 80, + contentWidth: 0, + expandedContent: [ + const SettingTile( + title: "Version", + subtitle: "Select the version of Fortnite you want to play", + content: VersionSelector(), + isChild: true, + ), + SettingTile( + title: "Add a version from this PC's local storage", + subtitle: "Versions coming from your local disk are not guaranteed to work", + isChild: true, + content: Button( + onPressed: () => VersionSelector.openAddDialog(context), + child: const Text("Add build"), + ), + ), + SettingTile( + title: "Download any version from the cloud", + subtitle: "A curated list of supported versions by Project Reboot", + content: Button( + onPressed: () => VersionSelector.openDownloadDialog(context), + child: const Text("Download"), + ), + isChild: true + ) + ], + ), + const SizedBox( + height: 16.0, + ), + StreamBuilder( + stream: _remoteGameServerStream.stream, + builder: (context, snapshot) => SettingTile( + title: '3. Choose a game server', + subtitle: 'Select the game server you want to use to play Fortnite.', + titleStyle: FluentTheme.of(context).typography.title, + expandedContentHeaderHeight: 80, + contentWidth: 0, + expandedContent: [ + SettingTile( + title: "Local Game Server", + subtitle: "Select this option if you want to host the game server on your PC", + contentWidth: null, + isChild: true, + content: Obx(() => Checkbox( + checked: _remoteGameServerController.text.isEmpty && _localGameServer(), + onChanged: (value) { + _localGameServer.value = value ?? false; + _remoteGameServerController.text = _settingsController.matchmakingIp.text = ""; + } + )) + ), + SettingTile( + title: "Remote Game Server", + subtitle: "Select this option if you want to join a match hosted on someone else's pc", + isChild: true, + content: TextFormBox( + controller: _remoteGameServerController, + onChanged: (value) =>_localGameServer.value = false, + placeholder: "Type the game server's ip", + validator: checkMatchmaking ) ) - ) - ) + ], + ), + ) + ], + ), + ), + const SizedBox( + height: 8.0, + ), + LaunchButton( + startLabel: 'Start playing', + stopLabel: 'Close game', + host: false, + check: () { + if(_gameController.selectedVersion == null){ + showMessage("Select a Fortnite version before continuing"); + return false; + } + + if(!_localGameServer() && _remoteGameServerController.text.isEmpty){ + showMessage("Select a game server before continuing"); + return false; + } + + if(_localGameServer()){ + _settingsController.matchmakingIp.text = "127.0.0.1"; + _gameController.autoStartGameServer.value = true; + }else { + _settingsController.matchmakingIp.text = _remoteGameServerController.text; + } + + _settingsController.firstRun.value = false; + return true; + } ) - ) - ); + ] + ); + } } \ No newline at end of file diff --git a/lib/src/ui/page/launcher_page.dart b/lib/src/ui/page/launcher_page.dart index 9c3f05a..dddb996 100644 --- a/lib/src/ui/page/launcher_page.dart +++ b/lib/src/ui/page/launcher_page.dart @@ -11,14 +11,14 @@ import 'package:reboot_launcher/src/ui/dialog/snackbar.dart'; import 'package:reboot_launcher/src/ui/page/browse_page.dart'; import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart'; import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart'; -import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart'; +import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart'; import '../../util/checks.dart'; import '../../util/os.dart'; class LauncherPage extends StatefulWidget { final GlobalKey navigatorKey; - final RxBool nestedNavigation; + final RxInt nestedNavigation; const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override @@ -59,7 +59,7 @@ class _LauncherPageState extends State with AutomaticKeepAliveClie class _GamePage extends StatefulWidget { final GlobalKey navigatorKey; - final RxBool nestedNavigation; + final RxInt nestedNavigation; const _GamePage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @override @@ -156,7 +156,7 @@ class _GamePageState extends State<_GamePage> { content: Button( onPressed: () { widget.navigatorKey.currentState?.pushNamed('browse'); - widget.nestedNavigation.value = true; + widget.nestedNavigation.value += 1; }, child: const Text("Browse") ), diff --git a/lib/src/ui/page/server_page.dart b/lib/src/ui/page/server_page.dart index 890008f..ece1285 100644 --- a/lib/src/ui/page/server_page.dart +++ b/lib/src/ui/page/server_page.dart @@ -7,11 +7,13 @@ import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart'; import 'package:reboot_launcher/src/ui/widget/server/server_button.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../widget/shared/setting_tile.dart'; +import '../dialog/dialog.dart'; +import '../dialog/dialog_button.dart'; +import '../widget/home/setting_tile.dart'; class ServerPage extends StatefulWidget { final GlobalKey navigatorKey; - final RxBool nestedNavigation; + final RxInt nestedNavigation; const ServerPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); @@ -95,13 +97,43 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi child: const Text("Open") ) ), + const SizedBox( + height: 16.0, + ), + SettingTile( + title: "Reset Backend", + subtitle: "Resets the launcher's backend to its default settings", + content: Button( + onPressed: () => showDialog( + context: context, + builder: (context) => InfoDialog( + text: "Do you want to reset the backend? This action is irreversible", + buttons: [ + DialogButton( + type: ButtonType.secondary, + text: "Close", + ), + DialogButton( + type: ButtonType.primary, + text: "Reset", + onTap: () { + _serverController.reset(); + Navigator.of(context).pop(); + }, + ) + ], + ) + ), + child: const Text("Reset"), + ) + ), ] ), ), const SizedBox( height: 8.0, ), - ServerButton() + const ServerButton() ], )); } diff --git a/lib/src/ui/page/settings_page.dart b/lib/src/ui/page/settings_page.dart index aef9295..10a3b20 100644 --- a/lib/src/ui/page/settings_page.dart +++ b/lib/src/ui/page/settings_page.dart @@ -12,11 +12,10 @@ import '../../util/checks.dart'; import '../../util/os.dart'; import '../../util/selector.dart'; import '../dialog/dialog.dart'; -import '../widget/shared/setting_tile.dart'; +import '../widget/home/setting_tile.dart'; class SettingsPage extends StatefulWidget { - - SettingsPage({Key? key}) : super(key: key); + const SettingsPage({Key? key}) : super(key: key); @override State createState() => _SettingsPageState(); @@ -139,7 +138,7 @@ class _SettingsPageState extends State with AutomaticKeepAliveClie ), SettingTile( title: "Version status", - subtitle: "Current version: 8.0", + subtitle: "Current version: 8.1", content: Button( onPressed: () => launchUrl(installationDirectory.uri), child: const Text("Show Files"), diff --git a/lib/src/ui/widget/home/launch_button.dart b/lib/src/ui/widget/home/launch_button.dart index 0693eb3..75c3552 100644 --- a/lib/src/ui/widget/home/launch_button.dart +++ b/lib/src/ui/widget/home/launch_button.dart @@ -30,8 +30,11 @@ import '../../../util/process.dart'; class LaunchButton extends StatefulWidget { final bool host; + final String? startLabel; + final String? stopLabel; + final bool Function()? check; - const LaunchButton({Key? key, required this.host}) : super(key: key); + const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel, this.check}) : super(key: key); @override State createState() => _LaunchButtonState(); @@ -70,9 +73,7 @@ class _LaunchButtonState extends State { child: Button( child: Align( alignment: Alignment.center, - child: Text( - _hasStarted ? _stopMessage : _startMessage - ), + child: Text(_hasStarted ? _stopMessage : _startMessage) ), onPressed: () => _executor = _start() ), @@ -84,11 +85,15 @@ class _LaunchButtonState extends State { void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started; - String get _startMessage => widget.host ? "Start hosting" : "Launch fortnite"; + String get _startMessage => widget.startLabel ?? (widget.host ? "Start hosting" : "Launch fortnite"); - String get _stopMessage => widget.host ? "Stop hosting" : "Close fortnite"; + String get _stopMessage => widget.stopLabel ?? (widget.host ? "Stop hosting" : "Close fortnite"); Future _start() async { + if(widget.check != null && !widget.check!()){ + return; + } + if (_hasStarted) { _onStop(widget.host); return; @@ -132,6 +137,7 @@ class _LaunchButtonState extends State { } await compute(patchHeadless, version.executable!); + // Is this needed? await compute(patchMatchmaking, version.executable!); var automaticallyStartedServer = await _startMatchMakingServer(); await _startGameProcesses(version, widget.host, automaticallyStartedServer); @@ -182,6 +188,10 @@ class _LaunchButtonState extends State { return false; } + if(_hostingController.started()){ + return false; + } + var version = _gameController.selectedVersion!; await _startGameProcesses(version, true, false); return true; diff --git a/lib/src/ui/widget/shared/setting_tile.dart b/lib/src/ui/widget/home/setting_tile.dart similarity index 71% rename from lib/src/ui/widget/shared/setting_tile.dart rename to lib/src/ui/widget/home/setting_tile.dart index ed70967..28381f4 100644 --- a/lib/src/ui/widget/shared/setting_tile.dart +++ b/lib/src/ui/widget/home/setting_tile.dart @@ -4,21 +4,28 @@ import 'package:reboot_launcher/src/ui/widget/shared/fluent_card.dart'; class SettingTile extends StatefulWidget { static const double kDefaultContentWidth = 200.0; static const double kDefaultSpacing = 8.0; + static const double kDefaultHeaderHeight = 72; final String title; + final TextStyle? titleStyle; final String subtitle; + final TextStyle? subtitleStyle; final Widget? content; final double? contentWidth; final List? expandedContent; + final double expandedContentHeaderHeight; final double expandedContentSpacing; final bool isChild; const SettingTile( {Key? key, required this.title, + this.titleStyle, required this.subtitle, + this.subtitleStyle, this.content, this.contentWidth = kDefaultContentWidth, + this.expandedContentHeaderHeight = kDefaultHeaderHeight, this.expandedContentSpacing = kDefaultSpacing, this.expandedContent, this.isChild = false}) @@ -44,7 +51,7 @@ class _SettingTileState extends State { borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)), ), header: _header, - headerHeight: 72, + headerHeight: widget.expandedContentHeaderHeight, trailing: _trailing, content: _content ), @@ -65,8 +72,14 @@ class _SettingTileState extends State { ); Widget get _header => ListTile( - title: Text(widget.title), - subtitle: Text(widget.subtitle) + title: Text( + widget.title, + style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle, + ), + subtitle: Text( + widget.subtitle, + style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body + ) ); Widget get _contentCard { @@ -83,8 +96,14 @@ class _SettingTileState extends State { } Widget get _contentCardBody => ListTile( - title: Text(widget.title), - subtitle: Text(widget.subtitle), + title: Text( + widget.title, + style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle, + ), + subtitle: Text( + widget.subtitle, + style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body + ), trailing: _trailing ); } \ No newline at end of file diff --git a/lib/src/util/checks.dart b/lib/src/util/checks.dart index 7b578af..b1de1ee 100644 --- a/lib/src/util/checks.dart +++ b/lib/src/util/checks.dart @@ -1,5 +1,8 @@ +import 'dart:async'; import 'dart:io'; +import 'package:reboot_launcher/src/util/server.dart'; + import '../model/fortnite_version.dart'; String? checkVersion(String? text, List versions) { @@ -68,6 +71,11 @@ String? checkMatchmaking(String? text) { return "Empty hostname"; } + var ipParts = text.split(":"); + if(ipParts.length > 2){ + return "Wrong format, expected ip:port"; + } + return null; } diff --git a/pubspec.yaml b/pubspec.yaml index c13fa50..30c6511 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "8.0.0" +version: "8.1.0" publish_to: 'none' @@ -71,7 +71,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 8.0.0.0 + msix_version: 8.1.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64