This commit is contained in:
Alessandro Autiero
2023-06-04 20:08:50 +02:00
parent 61ab3c446d
commit cd7db4cf71
13 changed files with 415 additions and 184 deletions

View File

@@ -3,20 +3,18 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:jaguar/jaguar.dart';
import '../../model/server_type.dart'; import '../../model/server_type.dart';
import '../../util/server.dart'; import '../../util/server.dart';
class ServerController extends GetxController { class ServerController extends GetxController {
static const String _serverName = "127.0.0.1"; static const String _kDefaultServerHost = "127.0.0.1";
static const String _serverPort = "3551"; static const String _kDefaultServerPort = "3551";
late final GetStorage _storage; late final GetStorage _storage;
late final TextEditingController host; late final TextEditingController host;
late final TextEditingController port; late final TextEditingController port;
late final Rx<ServerType> type; late final Rx<ServerType> type;
late final RxBool warning;
late RxBool started; late RxBool started;
late RxBool detached; late RxBool detached;
HttpServer? remoteServer; HttpServer? remoteServer;
@@ -39,20 +37,31 @@ class ServerController extends GetxController {
host.addListener(() => _storage.write("${type.value.id}_host", host.text)); host.addListener(() => _storage.write("${type.value.id}_host", host.text));
port = TextEditingController(text: _readPort()); port = TextEditingController(text: _readPort());
port.addListener(() => _storage.write("${type.value.id}_port", port.text)); 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); 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 _readHost() {
String? value = _storage.read("${type.value.id}_host"); String? value = _storage.read("${type.value.id}_host");
return value != null && value.isNotEmpty ? value return value != null && value.isNotEmpty ? value
: type.value != ServerType.remote ? _serverName : ""; : type.value != ServerType.remote ? _kDefaultServerHost : "";
} }
String _readPort() { String _readPort() {
return _storage.read("${type.value.id}_port") ?? _serverPort; return _storage.read("${type.value.id}_port") ?? _kDefaultServerPort;
} }
Future<bool> stop() async { Future<bool> stop() async {

View File

@@ -21,6 +21,8 @@ class SettingsController extends GetxController {
late final TextEditingController authDll; late final TextEditingController authDll;
late final TextEditingController matchmakingIp; late final TextEditingController matchmakingIp;
late final RxBool autoUpdate; late final RxBool autoUpdate;
late final RxBool firstRun;
late final RxInt index;
late double width; late double width;
late double height; late double height;
late double? offsetX; late double? offsetX;
@@ -45,8 +47,11 @@ class SettingsController extends GetxController {
offsetX = _storage.read("offset_x"); offsetX = _storage.read("offset_x");
offsetY = _storage.read("offset_y"); offsetY = _storage.read("offset_y");
autoUpdate = RxBool(_storage.read("auto_update") ?? _kDefaultAutoUpdate); 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; 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) { TextEditingController _createController(String key, String name) {

View File

@@ -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/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.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/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'; import 'package:supabase_flutter/supabase_flutter.dart';

View File

@@ -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:reboot_launcher/src/ui/page/settings_page.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../controller/game_controller.dart';
import '../controller/settings_controller.dart'; import '../controller/settings_controller.dart';
import '../widget/os/window_border.dart'; import '../widget/os/window_border.dart';
import '../widget/os/window_buttons.dart'; import '../widget/os/window_buttons.dart';
@@ -27,15 +28,13 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
static const int _kPagesLength = 5; static const int _kPagesLength = 5;
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final GlobalKey _searchKey = GlobalKey(); final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode(); final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn(); final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
final RxBool _focused = RxBool(true); final RxBool _focused = RxBool(true);
final RxInt _index = RxInt(0);
final List<GlobalKey<NavigatorState>> _navigators = List.generate(_kPagesLength, (index) => GlobalKey()); final List<GlobalKey<NavigatorState>> _navigators = List.generate(_kPagesLength, (index) => GlobalKey());
final List<RxBool> _navigationStatus = List.generate(_kPagesLength, (index) => RxBool(false)); final List<RxInt> _navigationStatus = List.generate(_kPagesLength, (index) => RxInt(0));
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@@ -145,25 +144,25 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}); });
Function()? _onBack() { Function()? _onBack() {
var navigator = _navigators[_index.value].currentState; var navigator = _navigators[_settingsController.index.value].currentState;
if(navigator == null || !navigator.mounted || !navigator.canPop()){ if(navigator == null || !navigator.mounted || !navigator.canPop()){
return null; return null;
} }
var status = _navigationStatus[_index.value]; var status = _navigationStatus[_settingsController.index.value];
if(!status.value){ if(status.value <= 0){
return null; return null;
} }
return () async { return () async {
Navigator.pop(navigator.context); Navigator.pop(navigator.context);
status.value = false; status.value -= 1;
}; };
} }
void _onIndexChanged(int index) { void _onIndexChanged(int index) {
_navigationStatus[_index()].value = false; _navigationStatus[_settingsController.index()].value = 0;
_index.value = index; _settingsController.index.value = index;
} }
TextBox get _autoSuggestBox => TextBox( TextBox get _autoSuggestBox => TextBox(
@@ -182,14 +181,14 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
int? get _selectedIndex { int? get _selectedIndex {
var searchItems = _searchItems(); var searchItems = _searchItems();
if (searchItems == null) { if (searchItems == null) {
return _index(); return _settingsController.index();
} }
if(_index() >= _allItems.length){ if(_settingsController.index() >= _allItems.length){
return null; return null;
} }
var indexOnScreen = searchItems.indexOf(_allItems[_index()]); var indexOnScreen = searchItems.indexOf(_allItems[_settingsController.index()]);
if (indexOnScreen.isNegative) { if (indexOnScreen.isNegative) {
return null; return null;
} }
@@ -208,28 +207,27 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
]; ];
List<NavigationPaneItem> get _items => _searchItems() ?? [ List<NavigationPaneItem> get _items => _searchItems() ?? [
PaneItem(
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: InfoPage(_navigators[0], _navigationStatus[0])
),
PaneItem( PaneItem(
title: const Text("Play"), title: const Text("Play"),
icon: const Icon(FluentIcons.game), icon: const Icon(FluentIcons.game),
body: LauncherPage(_navigators[0], _navigationStatus[0]) body: LauncherPage(_navigators[1], _navigationStatus[1])
), ),
PaneItem( PaneItem(
title: const Text("Host"), title: const Text("Host"),
icon: const Icon(FluentIcons.server_processes), icon: const Icon(FluentIcons.server_processes),
body: HostingPage(_navigators[1], _navigationStatus[1]) body: HostingPage(_navigators[2], _navigationStatus[2])
), ),
PaneItem( PaneItem(
title: const Text("Backend"), title: const Text("Backend"),
icon: const Icon(FluentIcons.user_window), icon: const Icon(FluentIcons.user_window),
body: ServerPage(_navigators[2], _navigationStatus[2]) body: ServerPage(_navigators[3], _navigationStatus[3])
),
PaneItem(
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: InfoPage(_navigators[3], _navigationStatus[3])
), ),
]; ];

View File

@@ -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/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.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/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 '../../model/update_status.dart';
import 'browse_page.dart'; import 'browse_page.dart';
class HostingPage extends StatefulWidget { class HostingPage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation; final RxInt nestedNavigation;
const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override @override
@@ -71,7 +71,7 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
class _HostPage extends StatefulWidget { class _HostPage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation; final RxInt nestedNavigation;
const _HostPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); const _HostPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override @override
@@ -171,7 +171,7 @@ class _HostPageState extends State<_HostPage> with AutomaticKeepAliveClientMixin
content: Button( content: Button(
onPressed: () { onPressed: () {
widget.navigatorKey.currentState?.pushNamed('browse'); widget.navigatorKey.currentState?.pushNamed('browse');
widget.nestedNavigation.value = true; widget.nestedNavigation.value += 1;
}, },
child: const Text("Browse") child: const Text("Browse")
) )

View File

@@ -1,14 +1,24 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.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 '../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 { class InfoPage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation; final RxInt nestedNavigation;
const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override @override
@@ -16,29 +26,6 @@ class InfoPage extends StatefulWidget {
} }
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin { class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
final List<String> _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<String> _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<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
late final ScrollController _controller; late final ScrollController _controller;
@@ -61,120 +48,284 @@ class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin
} }
@override @override
Widget build(BuildContext context) => Navigator( Widget build(BuildContext context) {
key: widget.navigatorKey, super.build(context);
initialRoute: "home", return Navigator(
onGenerateRoute: (settings) { key: widget.navigatorKey,
var screen = _createScreen(settings.name); initialRoute: "introduction",
return FluentPageRoute( onGenerateRoute: (settings) {
builder: (context) => screen, var screen = _createScreen(settings.name);
settings: settings return FluentPageRoute(
); builder: (context) => screen,
}, settings: settings
); );
},
);
}
Widget _createScreen(String? name) { Widget _createScreen(String? name) {
switch(name){ switch(name){
case "home": case "introduction":
return _homeScreen; return _IntroductionPage(widget.navigatorKey, widget.nestedNavigation);
case "else": case "play":
return _createInstructions(false); return _PlayPage(widget.navigatorKey, widget.nestedNavigation);
case "own":
return _createInstructions(true);
default: default:
throw Exception("Unknown page: $name"); throw Exception("Unknown page: $name");
} }
} }
}
Widget get _homeScreen => Row( class _IntroductionPage extends StatefulWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, final GlobalKey<NavigatorState> 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: [ children: [
_createCardWidget( Expanded(
text: "Play on someone else's server", child: ListView(
description: "If one of your friends is hosting a game server, click here", children: [
onClick: () { SettingTile(
widget.navigatorKey.currentState?.pushNamed("else"); title: 'What is Project Reboot?',
widget.nestedNavigation.value = true; 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( const SizedBox(
width: 8.0, height: 8.0,
), ),
SizedBox(
_createCardWidget( width: double.infinity,
text: "Host your own server", height: 48,
description: "If you want to create your own server to invite your friends or to play around by yourself, click here", child: Button(
onClick: () { child: const Align(
widget.navigatorKey.currentState?.pushNamed("own"); alignment: Alignment.center,
widget.nestedNavigation.value = true; child: Text("How do I play?")
} ),
onPressed: () {
widget.navigatorKey.currentState?.pushNamed("play");
widget.nestedNavigation.value += 1;
}
),
) )
] ],
); );
}
}
SizedBox _createInstructions(bool own) { class _PlayPage extends StatefulWidget {
var titles = own ? _ownTitles : _elseTitles; final GlobalKey<NavigatorState> navigatorKey;
var codeName = own ? "own" : "else"; final RxInt nestedNavigation;
return SizedBox.expand( const _PlayPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
child: ListView.separated(
controller: _controller, @override
itemBuilder: (context, index) => Padding( State<_PlayPage> createState() => _PlayPageState();
padding: const EdgeInsets.only( }
right: 20.0
), class _PlayPageState extends State<_PlayPage> {
child: FluentCard( final GameController _gameController = Get.find<GameController>();
child: ListTile( final SettingsController _settingsController = Get.find<SettingsController>();
title: SelectableText("${index + 1}. ${titles[index]}"), final RxBool _localGameServer = RxBool(true);
subtitle: Padding( final TextEditingController _remoteGameServerController = TextEditingController();
padding: const EdgeInsets.only(top: 12.0), final StreamController _remoteGameServerStream = StreamController();
child: Image.asset("assets/images/tutorial_${codeName}_${index + 1}.png"),
) @override
) void initState() {
), var ip = _settingsController.matchmakingIp.text;
), _remoteGameServerController.text = isLocalHost(ip) ? "" : ip;
separatorBuilder: (context, index) => const SizedBox(height: 8.0), _remoteGameServerController.addListener(() => _remoteGameServerStream.add(null));
itemCount: titles.length, super.initState();
)
);
} }
Widget _createCardWidget({required String text, required String description, required Function() onClick}) => Expanded( @override
child: SizedBox( Widget build(BuildContext context) {
height: double.infinity, return Column(
child: MouseRegion( children: [
cursor: SystemMouseCursors.click, Expanded(
child: GestureDetector( child: ListView(
onTap: onClick, children: [
child: FluentCard( SettingTile(
child: Padding( title: '1. Select a username',
padding: const EdgeInsets.all(8.0), subtitle: 'Choose a name for other players to see while you are in-game',
child: Column( titleStyle: FluentTheme.of(context).typography.title,
mainAxisAlignment: MainAxisAlignment.center, expandedContentHeaderHeight: 80,
children: [ contentWidth: 0,
Text( expandedContent: [
text, SettingTile(
textAlign: TextAlign.center, title: "Username",
style: const TextStyle( subtitle: "The username that other players will see when you are in game",
fontSize: 20.0, isChild: true,
fontWeight: FontWeight.bold content: TextFormBox(
), placeholder: "Username",
), controller: _gameController.username,
autovalidateMode: AutovalidateMode.always
const SizedBox( ),
height: 8.0, ),
), ],
),
Text( const SizedBox(
description, height: 16.0,
textAlign: TextAlign.center ),
), 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;
}
) )
) ]
); );
}
} }

View File

@@ -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/page/browse_page.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.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/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/checks.dart';
import '../../util/os.dart'; import '../../util/os.dart';
class LauncherPage extends StatefulWidget { class LauncherPage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation; final RxInt nestedNavigation;
const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override @override
@@ -59,7 +59,7 @@ class _LauncherPageState extends State<LauncherPage> with AutomaticKeepAliveClie
class _GamePage extends StatefulWidget { class _GamePage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation; final RxInt nestedNavigation;
const _GamePage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); const _GamePage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override @override
@@ -156,7 +156,7 @@ class _GamePageState extends State<_GamePage> {
content: Button( content: Button(
onPressed: () { onPressed: () {
widget.navigatorKey.currentState?.pushNamed('browse'); widget.navigatorKey.currentState?.pushNamed('browse');
widget.nestedNavigation.value = true; widget.nestedNavigation.value += 1;
}, },
child: const Text("Browse") child: const Text("Browse")
), ),

View File

@@ -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:reboot_launcher/src/ui/widget/server/server_button.dart';
import 'package:url_launcher/url_launcher.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 { class ServerPage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation; final RxInt nestedNavigation;
const ServerPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key); const ServerPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@@ -95,13 +97,43 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
child: const Text("Open") 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( const SizedBox(
height: 8.0, height: 8.0,
), ),
ServerButton() const ServerButton()
], ],
)); ));
} }

View File

@@ -12,11 +12,10 @@ import '../../util/checks.dart';
import '../../util/os.dart'; import '../../util/os.dart';
import '../../util/selector.dart'; import '../../util/selector.dart';
import '../dialog/dialog.dart'; import '../dialog/dialog.dart';
import '../widget/shared/setting_tile.dart'; import '../widget/home/setting_tile.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
const SettingsPage({Key? key}) : super(key: key);
SettingsPage({Key? key}) : super(key: key);
@override @override
State<SettingsPage> createState() => _SettingsPageState(); State<SettingsPage> createState() => _SettingsPageState();
@@ -139,7 +138,7 @@ class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClie
), ),
SettingTile( SettingTile(
title: "Version status", title: "Version status",
subtitle: "Current version: 8.0", subtitle: "Current version: 8.1",
content: Button( content: Button(
onPressed: () => launchUrl(installationDirectory.uri), onPressed: () => launchUrl(installationDirectory.uri),
child: const Text("Show Files"), child: const Text("Show Files"),

View File

@@ -30,8 +30,11 @@ import '../../../util/process.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
final bool host; 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 @override
State<LaunchButton> createState() => _LaunchButtonState(); State<LaunchButton> createState() => _LaunchButtonState();
@@ -70,9 +73,7 @@ class _LaunchButtonState extends State<LaunchButton> {
child: Button( child: Button(
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(_hasStarted ? _stopMessage : _startMessage)
_hasStarted ? _stopMessage : _startMessage
),
), ),
onPressed: () => _executor = _start() onPressed: () => _executor = _start()
), ),
@@ -84,11 +85,15 @@ class _LaunchButtonState extends State<LaunchButton> {
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started; 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<void> _start() async { Future<void> _start() async {
if(widget.check != null && !widget.check!()){
return;
}
if (_hasStarted) { if (_hasStarted) {
_onStop(widget.host); _onStop(widget.host);
return; return;
@@ -132,6 +137,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
await compute(patchHeadless, version.executable!); await compute(patchHeadless, version.executable!);
// Is this needed? await compute(patchMatchmaking, version.executable!);
var automaticallyStartedServer = await _startMatchMakingServer(); var automaticallyStartedServer = await _startMatchMakingServer();
await _startGameProcesses(version, widget.host, automaticallyStartedServer); await _startGameProcesses(version, widget.host, automaticallyStartedServer);
@@ -182,6 +188,10 @@ class _LaunchButtonState extends State<LaunchButton> {
return false; return false;
} }
if(_hostingController.started()){
return false;
}
var version = _gameController.selectedVersion!; var version = _gameController.selectedVersion!;
await _startGameProcesses(version, true, false); await _startGameProcesses(version, true, false);
return true; return true;

View File

@@ -4,21 +4,28 @@ import 'package:reboot_launcher/src/ui/widget/shared/fluent_card.dart';
class SettingTile extends StatefulWidget { class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0; static const double kDefaultContentWidth = 200.0;
static const double kDefaultSpacing = 8.0; static const double kDefaultSpacing = 8.0;
static const double kDefaultHeaderHeight = 72;
final String title; final String title;
final TextStyle? titleStyle;
final String subtitle; final String subtitle;
final TextStyle? subtitleStyle;
final Widget? content; final Widget? content;
final double? contentWidth; final double? contentWidth;
final List<Widget>? expandedContent; final List<Widget>? expandedContent;
final double expandedContentHeaderHeight;
final double expandedContentSpacing; final double expandedContentSpacing;
final bool isChild; final bool isChild;
const SettingTile( const SettingTile(
{Key? key, {Key? key,
required this.title, required this.title,
this.titleStyle,
required this.subtitle, required this.subtitle,
this.subtitleStyle,
this.content, this.content,
this.contentWidth = kDefaultContentWidth, this.contentWidth = kDefaultContentWidth,
this.expandedContentHeaderHeight = kDefaultHeaderHeight,
this.expandedContentSpacing = kDefaultSpacing, this.expandedContentSpacing = kDefaultSpacing,
this.expandedContent, this.expandedContent,
this.isChild = false}) this.isChild = false})
@@ -44,7 +51,7 @@ class _SettingTileState extends State<SettingTile> {
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)), borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
), ),
header: _header, header: _header,
headerHeight: 72, headerHeight: widget.expandedContentHeaderHeight,
trailing: _trailing, trailing: _trailing,
content: _content content: _content
), ),
@@ -65,8 +72,14 @@ class _SettingTileState extends State<SettingTile> {
); );
Widget get _header => ListTile( Widget get _header => ListTile(
title: Text(widget.title), title: Text(
subtitle: Text(widget.subtitle) 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 { Widget get _contentCard {
@@ -83,8 +96,14 @@ class _SettingTileState extends State<SettingTile> {
} }
Widget get _contentCardBody => ListTile( Widget get _contentCardBody => ListTile(
title: Text(widget.title), title: Text(
subtitle: Text(widget.subtitle), widget.title,
style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle,
),
subtitle: Text(
widget.subtitle,
style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body
),
trailing: _trailing trailing: _trailing
); );
} }

View File

@@ -1,5 +1,8 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:reboot_launcher/src/util/server.dart';
import '../model/fortnite_version.dart'; import '../model/fortnite_version.dart';
String? checkVersion(String? text, List<FortniteVersion> versions) { String? checkVersion(String? text, List<FortniteVersion> versions) {
@@ -68,6 +71,11 @@ String? checkMatchmaking(String? text) {
return "Empty hostname"; return "Empty hostname";
} }
var ipParts = text.split(":");
if(ipParts.length > 2){
return "Wrong format, expected ip:port";
}
return null; return null;
} }

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Launcher for project reboot description: Launcher for project reboot
version: "8.0.0" version: "8.1.0"
publish_to: 'none' publish_to: 'none'
@@ -71,7 +71,7 @@ msix_config:
display_name: Reboot Launcher display_name: Reboot Launcher
publisher_display_name: Auties00 publisher_display_name: Auties00
identity_name: 31868Auties00.RebootLauncher identity_name: 31868Auties00.RebootLauncher
msix_version: 8.0.0.0 msix_version: 8.1.0.0
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
logo_path: ./assets/icons/reboot.ico logo_path: ./assets/icons/reboot.ico
architecture: x64 architecture: x64