Added headless switch

Fixed UI
This commit is contained in:
Alessandro Autiero
2022-10-04 17:28:10 +02:00
parent 9a759ac9e3
commit c27dbaa306
20 changed files with 389 additions and 225 deletions

View File

@@ -5,6 +5,7 @@ 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:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
class GameController extends GetxController { class GameController extends GetxController {
late final GetStorage _storage; late final GetStorage _storage;
@@ -12,8 +13,9 @@ class GameController extends GetxController {
late final TextEditingController version; late final TextEditingController version;
late final Rx<List<FortniteVersion>> versions; late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion; late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool host; late final Rx<GameType> type;
late final RxBool started; late final RxBool started;
Future? updater;
Process? gameProcess; Process? gameProcess;
Process? launcherProcess; Process? launcherProcess;
Process? eacProcess; Process? eacProcess;
@@ -34,15 +36,15 @@ class GameController extends GetxController {
(element) => element.name == decodedSelectedVersionName); (element) => element.name == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion); _selectedVersion = Rxn(decodedSelectedVersion);
host = RxBool(_storage.read("host") ?? false); type = Rx(GameType.values.elementAt(_storage.read("type") ?? 0));
host.listen((value) { type.listen((value) {
_storage.write("host", value); _storage.write("type", value.index);
username.text = _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""; username.text = _storage.read("${type.value == GameType.client ? 'game' : 'host'}_username") ?? "";
}); });
username = TextEditingController(text: _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""); username = TextEditingController(text: _storage.read("${type.value == GameType.client ? 'game' : 'host'}_username") ?? "");
username.addListener(() async { username.addListener(() async {
await _storage.write("${host.value ? 'host' : 'game'}_username", username.text); await _storage.write("${type.value == GameType.client ? 'game' : 'host'}_username", username.text);
}); });
started = RxBool(false); started = RxBool(false);

View File

@@ -0,0 +1,5 @@
enum GameType {
client,
server,
headlessServer
}

View File

@@ -1,7 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/page/info_page.dart'; import 'package:reboot_launcher/src/page/info_page.dart';
import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart';
import 'package:reboot_launcher/src/page/server_page.dart'; import 'package:reboot_launcher/src/page/server_page.dart';
@@ -10,8 +8,6 @@ import 'package:reboot_launcher/src/widget/window_border.dart';
import 'package:reboot_launcher/src/widget/window_buttons.dart'; import 'package:reboot_launcher/src/widget/window_buttons.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../util/reboot.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key); const HomePage({Key? key}) : super(key: key);
@@ -20,17 +16,12 @@ class HomePage extends StatefulWidget {
} }
class _HomePageState extends State<HomePage> with WindowListener { class _HomePageState extends State<HomePage> with WindowListener {
late final Future _future;
bool _focused = true; bool _focused = true;
int _index = 0; int _index = 0;
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
var storage = GetStorage("update");
int? lastUpdateMs = storage.read("last_update");
_future = compute(downloadRebootDll, lastUpdateMs);
_future.then((value) => storage.write("last_update", value));
super.initState(); super.initState();
} }
@@ -47,7 +38,7 @@ class _HomePageState extends State<HomePage> with WindowListener {
@override @override
void onWindowBlur() { void onWindowBlur() {
setState(() => _focused = false); setState(() => _focused = !_focused);
} }
@override @override
@@ -66,12 +57,13 @@ class _HomePageState extends State<HomePage> with WindowListener {
_createPane("Info", FluentIcons.info), _createPane("Info", FluentIcons.info),
], ],
trailing: WindowTitleBar(focused: _focused)), trailing: WindowTitleBar(focused: _focused)),
content: FutureBuilder( content: NavigationBody(
future: _future,
builder: (context, snapshot) => NavigationBody(
index: _index, index: _index,
children: _createPages(snapshot) children: [
) const LauncherPage(),
ServerPage(),
const InfoPage()
]
) )
), ),
@@ -81,19 +73,6 @@ class _HomePageState extends State<HomePage> with WindowListener {
); );
} }
List<Widget> _createPages(AsyncSnapshot snapshot) {
return [
LauncherPage(
ready: snapshot.hasData,
error: snapshot.error,
stackTrace: snapshot.stackTrace
),
ServerPage(),
const InfoPage()
];
}
PaneItem _createPane(String label, IconData icon) { PaneItem _createPane(String label, IconData icon) {
return PaneItem(icon: Icon(icon), title: Text(label)); return PaneItem(icon: Icon(icon), title: Text(label));
} }

View File

@@ -31,7 +31,7 @@ class InfoPage extends StatelessWidget {
), ),
const Expanded( const Expanded(
child: Align( child: Align(
alignment: Alignment.bottomLeft, child: Text("Version 3.8${kDebugMode ? '-DEBUG' : ''}"))) alignment: Alignment.bottomLeft, child: Text("Version 3.10${kDebugMode ? '-DEBUG' : ''}")))
], ],
); );
} }

View File

@@ -1,24 +1,25 @@
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.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/controller/build_controller.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/deployment_selector.dart'; import 'package:reboot_launcher/src/widget/host_checkbox.dart';
import 'package:reboot_launcher/src/widget/launch_button.dart'; import 'package:reboot_launcher/src/widget/launch_button.dart';
import 'package:reboot_launcher/src/widget/username_box.dart'; import 'package:reboot_launcher/src/widget/username_box.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../util/binary.dart';
import '../util/reboot.dart';
import '../widget/warning_info.dart'; import '../widget/warning_info.dart';
class LauncherPage extends StatefulWidget { class LauncherPage extends StatefulWidget {
final bool ready;
final Object? error;
final StackTrace? stackTrace;
const LauncherPage( const LauncherPage(
{Key? key, required this.ready, required this.error, this.stackTrace}) {Key? key})
: super(key: key); : super(key: key);
@override @override
@@ -26,27 +27,56 @@ class LauncherPage extends StatefulWidget {
} }
class _LauncherPageState extends State<LauncherPage> { class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>();
final BuildController _buildController = Get.find<BuildController>(); final BuildController _buildController = Get.find<BuildController>();
bool shouldWriteError = true;
@override @override
void initState() { void initState() {
if(_gameController.updater == null) {
_gameController.updater = compute(downloadRebootDll, _updateTime)
..then((value) => _updateTime = value)
..onError(_saveError);
_buildController.cancelledDownload _buildController.cancelledDownload
.listen((value) => value ? _onCancelWarning() : {}); .listen((value) => value ? _onCancelWarning() : {});
}
super.initState(); super.initState();
} }
int? get _updateTime {
var storage = GetStorage("update");
return storage.read("last_update");
}
set _updateTime(int? updateTime) {
var storage = GetStorage("update");
storage.write("last_update", updateTime);
}
Future<void> _saveError(Object? error, StackTrace stackTrace) async {
var errorFile = await loadBinary("error.txt", true);
errorFile.writeAsString(
"Error: $error\nStacktrace: $stackTrace", mode: FileMode.write);
}
void _onCancelWarning() { void _onCancelWarning() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if(!mounted) {
return;
}
showSnackbar(context, showSnackbar(context,
const Snackbar(content: Text("Download cancelled"))); const Snackbar(content: Text("Download cancelled")));
_buildController.cancelledDownload.value = false; _buildController.cancelledDownload(false);
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!widget.ready && widget.error == null) { return FutureBuilder(
future: _gameController.updater,
builder: (context, snapshot) {
if (!snapshot.hasData && !snapshot.hasError) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -66,28 +96,25 @@ class _LauncherPageState extends State<LauncherPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if(widget.error != null) if(snapshot.hasError)
WarningInfo( _createUpdateError(snapshot),
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(), UsernameBox(),
VersionSelector(), const VersionSelector(),
DeploymentSelector(enabled: true), const DeploymentSelector(),
const LaunchButton() const LaunchButton()
], ],
); );
} }
);
}
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
return WarningInfo(
text: "Cannot update Reboot DLL",
icon: FluentIcons.info,
severity: InfoBarSeverity.warning,
onPressed: () => loadBinary("error.txt", true)
.then((file) => launchUrl(file.uri))
);
}
} }

View File

@@ -65,9 +65,9 @@ Future<List<FortniteBuild>> _fetchManifests() async {
var children = tableEntry.querySelectorAll("td"); var children = tableEntry.querySelectorAll("td");
var name = children[0].text; var name = children[0].text;
var separator = name.indexOf("-") + 1; var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-"));
var version = parser var version = parser
.tryParse(name.substring(separator, name.indexOf("-", separator))); .tryParse(minifiedName.replaceFirst("-CL", ""));
if (version == null) { if (version == null) {
continue; continue;
} }

View File

@@ -3,8 +3,6 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
File errorFile = File("${Platform.environment["Temp"]}/error.txt");
const int appBarSize = 2; const int appBarSize = 2;
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');

View File

@@ -38,7 +38,7 @@ Future<int> downloadRebootDll(int? lastUpdateMs) async {
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) { if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) {
outputDir.delete(); outputDir.delete();
return lastUpdateMs!; return lastUpdateMs ?? now.millisecondsSinceEpoch;
} }
await rebootDll.rename(oldRebootDll.path); await rebootDll.rename(oldRebootDll.path);

View File

@@ -84,7 +84,7 @@ Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, S
return null; return null;
} }
return await shelf_io.serve(proxyHandler(uri), 'localhost', 3551); return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
}catch(error){ }catch(error){
_showStartProxyError(context, error); _showStartProxyError(context, error);
return null; return null;
@@ -129,10 +129,8 @@ Future<Uri?> _showReverseProxyCheck(BuildContext context, String host, String po
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
)) ))
] ]
@@ -175,10 +173,8 @@ void _showStartProxyError(BuildContext context, Object error) {
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
) )
) )
@@ -198,10 +194,8 @@ void _showStopProxyError(BuildContext context, Object error) {
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
) )
) )
@@ -285,10 +279,8 @@ Future<bool> _showServerDownloadInfo(BuildContext context, bool portable) async
future: nodeFuture, future: nodeFuture,
builder: (builder, snapshot) => SizedBox( builder: (builder, snapshot) => SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError), onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'), child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'),
) )
) )
@@ -310,10 +302,8 @@ void _showEmbeddedError(BuildContext context, String path) {
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
)) ))
], ],

View File

@@ -29,9 +29,8 @@ class AddLocalVersion extends StatelessWidget {
List<Widget> _createLocalVersionActions(BuildContext context) { List<Widget> _createLocalVersionActions(BuildContext context) {
return [ return [
FilledButton( Button(
onPressed: () => _closeLocalVersionDialog(context, false), onPressed: () => _closeLocalVersionDialog(context, false),
style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
), ),
FilledButton( FilledButton(

View File

@@ -94,9 +94,8 @@ class _AddServerVersionState extends State<AddServerVersion> {
switch (_status) { switch (_status) {
case DownloadStatus.none: case DownloadStatus.none:
return [ return [
FilledButton( Button(
onPressed: () => _onClose(), onPressed: () => _onClose(),
style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close')), child: const Text('Close')),
FilledButton( FilledButton(
onPressed: () => _startDownload(context), onPressed: () => _startDownload(context),
@@ -108,20 +107,16 @@ class _AddServerVersionState extends State<AddServerVersion> {
return [ return [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => _onClose(), onPressed: () => _onClose(),
style:
ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'))) child: const Text('Close')))
]; ];
default: default:
return [ return [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => _onClose(), onPressed: () => _onClose(),
style:
ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: Text( child: Text(
_status == DownloadStatus.downloading ? 'Stop' : 'Close')), _status == DownloadStatus.downloading ? 'Stop' : 'Close')),
) )

View File

@@ -1,31 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/widget/smart_switch.dart';
class DeploymentSelector extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
final bool enabled;
DeploymentSelector({Key? key, required this.enabled}) : super(key: key);
@override
Widget build(BuildContext context) {
return Tooltip(
message: enabled ? "Whether the launched client should be used to host multiplayer games or not" : "Hosting is not allowed",
child: _buildSwitch(context)
);
}
SmartSwitch _buildSwitch(BuildContext context) {
return SmartSwitch(
value: _gameController.host,
onDisabledPress: !enabled
? () => showSnackbar(context,
const Snackbar(content: Text("Hosting is not allowed")))
: null,
label: "Host",
enabled: enabled
);
}
}

View File

@@ -0,0 +1,74 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/widget/smart_switch.dart';
class DeploymentSelector extends StatefulWidget {
const DeploymentSelector({Key? key}) : super(key: key);
@override
State<DeploymentSelector> createState() => _DeploymentSelectorState();
}
class _DeploymentSelectorState extends State<DeploymentSelector> {
final Map<GameType, String> _options = {
GameType.client: "Client",
GameType.server: "Server",
GameType.headlessServer: "Headless Server"
};
final Map<GameType, String> _descriptions = {
GameType.client: "A fortnite client will be launched to play multiplayer games",
GameType.server: "A fortnite client will be launched to host multiplayer games",
GameType.headlessServer: "A fortnite client will be launched in the background to host multiplayer games",
};
final GameController _gameController = Get.find<GameController>();
bool? _value;
@override
void initState() {
switch(_gameController.type.value){
case GameType.client:
_value = false;
break;
case GameType.server:
_value = true;
break;
case GameType.headlessServer:
_value = null;
break;
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Tooltip(
message: _descriptions[_gameController.type.value]!,
child: InfoLabel(
label: _options[_gameController.type.value]!,
child: Checkbox(
checked: _value,
onChanged: _onSelected
),
),
);
}
void _onSelected(bool? value){
if(value == null){
_gameController.type(GameType.client);
setState(() => _value = false);
return;
}
if(value){
_gameController.type(GameType.server);
setState(() => _value = true);
return;
}
_gameController.type(GameType.headlessServer);
setState(() => _value = null);
}
}

View File

@@ -2,10 +2,12 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:process_run/shell.dart'; import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/patcher.dart';
@@ -13,6 +15,8 @@ import 'package:reboot_launcher/src/util/server.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart';
import '../util/os.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
const LaunchButton( const LaunchButton(
{Key? key}) {Key? key})
@@ -25,7 +29,15 @@ class LaunchButton extends StatefulWidget {
class _LaunchButtonState extends State<LaunchButton> { class _LaunchButtonState extends State<LaunchButton> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final ServerController _serverController = Get.find<ServerController>(); final ServerController _serverController = Get.find<ServerController>();
bool _lawinFail = false; File? _logFile;
bool _fail = false;
@override
void initState() {
loadBinary("log.txt", true)
.then((value) => _logFile = value);
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -67,7 +79,7 @@ class _LaunchButtonState extends State<LaunchButton> {
try { try {
_updateServerState(true); _updateServerState(true);
var version = _gameController.selectedVersionObs.value!; var version = _gameController.selectedVersionObs.value!;
var hosting = _gameController.host.value; var hosting = _gameController.type.value == GameType.headlessServer;
if (version.launcher != null) { if (version.launcher != null) {
_gameController.launcherProcess = await Process.start(version.launcher!.path, []); _gameController.launcherProcess = await Process.start(version.launcher!.path, []);
Win32Process(_gameController.launcherProcess!.pid).suspend(); Win32Process(_gameController.launcherProcess!.pid).suspend();
@@ -88,7 +100,18 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
_gameController.gameProcess = await Process.start(version.executable!.path, _createProcessArguments()) if(_logFile != null && await _logFile!.exists()){
await _logFile!.delete();
}
var gamePath = version.executable?.path;
if(gamePath == null){
_onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null);
_onStop();
return;
}
_gameController.gameProcess = await Process.start(gamePath, _createProcessArguments())
..exitCode.then((_) => _onEnd()) ..exitCode.then((_) => _onEnd())
..outLines.forEach(_onGameOutput); ..outLines.forEach(_onGameOutput);
await _injectOrShowError("cranium.dll"); await _injectOrShowError("cranium.dll");
@@ -96,9 +119,10 @@ class _LaunchButtonState extends State<LaunchButton> {
if(hosting){ if(hosting){
await _showServerLaunchingWarning(); await _showServerLaunchingWarning();
} }
} catch (exception) { } catch (exception, stacktrace) {
_closeDialogIfOpen(); _closeDialogIfOpen(false);
_onError(exception); _onError(exception, stacktrace);
_onStop();
} }
} }
@@ -140,15 +164,15 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
void _onEnd() { void _onEnd() {
if(_lawinFail){ if(_fail){
return; return;
} }
_closeDialogIfOpen(); _closeDialogIfOpen(false);
_onStop(); _onStop();
} }
void _closeDialogIfOpen() { void _closeDialogIfOpen(bool success) {
if(!mounted){ if(!mounted){
return; return;
} }
@@ -158,7 +182,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
Navigator.of(context).pop(false); Navigator.of(context).pop(success);
} }
Future<void> _showBrokenServerWarning() async { Future<void> _showBrokenServerWarning() async {
@@ -176,10 +200,33 @@ class _LaunchButtonState extends State<LaunchButton> {
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
)
)
],
)
);
}
Future<void> _showUnsupportedHeadless() async {
if(!mounted){
return;
}
showDialog(
context: context,
builder: (context) => ContentDialog(
content: const SizedBox(
width: double.infinity,
child: Text("This version of Fortnite doesn't support headless hosting", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
) )
) )
@@ -197,7 +244,7 @@ class _LaunchButtonState extends State<LaunchButton> {
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => ContentDialog(
content: const InfoLabel( content: const InfoLabel(
label: "Launching reboot server...", label: "Launching headless reboot server...",
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ProgressBar() child: ProgressBar()
@@ -206,13 +253,11 @@ class _LaunchButtonState extends State<LaunchButton> {
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
_onStop(); _onStop();
}, },
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Cancel'), child: const Text('Cancel'),
) )
) )
@@ -228,30 +273,52 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
void _onGameOutput(String line) { void _onGameOutput(String line) {
if(kDebugMode){
print(line);
}
if(_logFile != null){
_logFile!.writeAsString("$line\n", mode: FileMode.append);
}
if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) {
_onStop(); _onStop();
return; return;
} }
if(line.contains("port 3551 failed: Connection refused")){ if(line.contains("port 3551 failed: Connection refused")){
_lawinFail = true; _fail = true;
_closeDialogIfOpen(); _closeDialogIfOpen(false);
_showBrokenServerWarning(); _showBrokenServerWarning();
return; return;
} }
if (line.contains("Game Engine Initialized") && !_gameController.host.value) { if(line.contains("HTTP 400 response from ")){
_fail = true;
_closeDialogIfOpen(false);
_showUnsupportedHeadless();
return;
}
if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) {
_injectOrShowError("console.dll"); _injectOrShowError("console.dll");
return; return;
} }
if(line.contains("added to UI Party led ") && _gameController.host.value){ if(line.contains("Region") && _gameController.type.value != GameType.client){
_injectOrShowError("reboot.dll") _injectOrShowError("reboot.dll")
.then((value) => Navigator.of(context).pop(true)); .then((value) => _closeDialogIfOpen(true));
} }
} }
Future<Object?> _onError(Object exception) { Future<Object?> _onError(Object exception, StackTrace? stackTrace) async {
if (stackTrace != null) {
var errorFile = await loadBinary("error.txt", true);
errorFile.writeAsString(
"Error: $exception\nStacktrace: $stackTrace", mode: FileMode.write);
launchUrl(errorFile.uri);
}
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => ContentDialog(
@@ -262,10 +329,8 @@ class _LaunchButtonState extends State<LaunchButton> {
actions: [ actions: [
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
)) ))
], ],
@@ -316,8 +381,8 @@ class _LaunchButtonState extends State<LaunchButton> {
"-AUTH_TYPE=epic" "-AUTH_TYPE=epic"
]; ];
if(_gameController.host.value){ if(_gameController.type.value == GameType.headlessServer){
args.addAll(["-log", "-nullrhi", "-nosplash", "-nosound", "-unattended"]); args.addAll(["-nullrhi", "-nosplash", "-nosound"]);
} }
return args; return args;

View File

@@ -35,9 +35,8 @@ class _ScanLocalVersionState extends State<ScanLocalVersion> {
List<Widget> _createLocalVersionActions(BuildContext context) { List<Widget> _createLocalVersionActions(BuildContext context) {
if(_future == null) { if(_future == null) {
return [ return [
FilledButton( Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
), ),
FilledButton( FilledButton(
@@ -53,9 +52,8 @@ class _ScanLocalVersionState extends State<ScanLocalVersion> {
if(!snapshot.hasData || snapshot.hasError) { if(!snapshot.hasData || snapshot.hasError) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Close'), child: const Text('Close'),
), ),
); );

View File

@@ -0,0 +1,27 @@
import 'package:fluent_ui/fluent_ui.dart';
class SmartCheckBox extends StatefulWidget {
final CheckboxController controller;
final Widget? content;
const SmartCheckBox({Key? key, required this.controller, this.content}) : super(key: key);
@override
State<SmartCheckBox> createState() => _SmartCheckBoxState();
}
class _SmartCheckBoxState extends State<SmartCheckBox> {
@override
Widget build(BuildContext context) {
return Checkbox(
checked: widget.controller.value,
onChanged: (checked) => setState(() => widget.controller.value = checked ?? false),
content: widget.content
);
}
}
class CheckboxController {
bool value;
CheckboxController({this.value = false});
}

View File

@@ -3,15 +3,15 @@ import 'package:get/get.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
class SmartSwitch extends StatefulWidget { class SmartSwitch extends StatefulWidget {
final String label; final String? label;
final bool enabled; final bool enabled;
final Function()? onDisabledPress; final Function()? onDisabledPress;
final Rx<bool> value; final Rx<bool> value;
const SmartSwitch( const SmartSwitch(
{Key? key, {Key? key,
required this.label,
required this.value, required this.value,
this.label,
this.enabled = true, this.enabled = true,
this.onDisabledPress}) this.onDisabledPress})
: super(key: key); : super(key: key);
@@ -23,9 +23,18 @@ class SmartSwitch extends StatefulWidget {
class _SmartSwitchState extends State<SmartSwitch> { class _SmartSwitchState extends State<SmartSwitch> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.label == null ? _createSwitch() : _createLabel();
}
InfoLabel _createLabel() {
return InfoLabel( return InfoLabel(
label: widget.label, label: widget.label!,
child: Obx(() => ToggleSwitch( child: _createSwitch()
);
}
Widget _createSwitch() {
return Obx(() => ToggleSwitch(
enabled: widget.enabled, enabled: widget.enabled,
onDisabledPress: widget.onDisabledPress, onDisabledPress: widget.onDisabledPress,
checked: widget.value.value, checked: widget.value.value,
@@ -38,7 +47,8 @@ class _SmartSwitchState extends State<SmartSwitch> {
.withOpacity(widget.value.value .withOpacity(widget.value.value
? _checkedOpacity ? _checkedOpacity
: _uncheckedOpacity) : _uncheckedOpacity)
.toAccentColor()))))); .toAccentColor())))
);
} }
Color get _toolTipColor => Color get _toolTipColor =>

View File

@@ -1,6 +1,7 @@
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:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/widget/smart_input.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart';
class UsernameBox extends StatelessWidget { class UsernameBox extends StatelessWidget {
@@ -11,10 +12,10 @@ class UsernameBox extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() => Tooltip( return Obx(() => Tooltip(
message: _gameController.host.value ? "The username of the game hoster" : "The in-game username of your player", message: _gameController.type.value != GameType.client ? "The username of the game hoster" : "The in-game username of your player",
child: SmartInput( child: SmartInput(
label: "Username", label: "Username",
placeholder: "Type your ${_gameController.host.value ? 'hosting' : "in-game"} username", placeholder: "Type your ${_gameController.type.value != GameType.client ? 'hosting' : "in-game"} username",
controller: _gameController.username, controller: _gameController.username,
populate: true populate: true
), ),

View File

@@ -10,6 +10,7 @@ import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/widget/add_local_version.dart'; import 'package:reboot_launcher/src/widget/add_local_version.dart';
import 'package:reboot_launcher/src/widget/add_server_version.dart'; import 'package:reboot_launcher/src/widget/add_server_version.dart';
import 'package:reboot_launcher/src/widget/scan_local_version.dart'; import 'package:reboot_launcher/src/widget/scan_local_version.dart';
import 'package:reboot_launcher/src/widget/smart_check_box.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget { class VersionSelector extends StatefulWidget {
@@ -23,6 +24,7 @@ class VersionSelector extends StatefulWidget {
class _VersionSelectorState extends State<VersionSelector> { class _VersionSelectorState extends State<VersionSelector> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final CheckboxController _deleteFilesController = CheckboxController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -146,55 +148,78 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
launchUrl(version.location.uri); launchUrl(version.location.uri)
.onError((error, stackTrace) => _onExplorerError());
break; break;
case 1: case 1:
_gameController.removeVersion(version);
if(!mounted){ if(!mounted){
return; return;
} }
await _openDeleteDialog(context, version); var result = await _openDeleteDialog(context, version) ?? false;
if(!mounted){ if(!mounted || !result){
return; return;
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
_gameController.removeVersion(version);
if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) { if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) {
_gameController.selectedVersionObs.value = null; _gameController.selectedVersionObs.value = null;
} }
if (_deleteFilesController.value && await version.location.exists()) {
version.location.delete(recursive: true);
}
break; break;
} }
} }
Future _openDeleteDialog(BuildContext context, FortniteVersion version) { bool _onExplorerError() {
return showDialog( showSnackbar(
context,
const Snackbar(
content: Text("This version doesn't exist on the local machine", textAlign: TextAlign.center),
extended: true
)
);
return false;
}
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
return showDialog<bool>(
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => ContentDialog(
content: const SizedBox( content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: double.infinity, width: double.infinity,
child: Text("Do you want to also delete the files for this version?", child: Text("Are you sure you want to delete this version?")),
textAlign: TextAlign.center)),
const SizedBox(height: 12.0),
SmartCheckBox(
controller: _deleteFilesController,
content: const Text("Delete version files from disk")
)
],
),
actions: [ actions: [
FilledButton( Button(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(false),
child: const Text('Keep'), child: const Text('Keep'),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () => Navigator.of(context).pop(true),
Navigator.of(context).pop();
if (await version.location.exists()) {
version.location.delete(recursive: true);
}
},
style:
ButtonStyle(backgroundColor: ButtonState.all(Colors.red)),
child: const Text('Delete'), child: const Text('Delete'),
) )
], ],
)); )
);
} }
} }

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Launcher for project reboot description: Launcher for project reboot
version: "3.8.0" version: "3.10.0"
publish_to: 'none' publish_to: 'none'
@@ -53,7 +53,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: 3.8.0.0 msix_version: 3.10.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