decoupled business logic from ui

This commit is contained in:
Alessandro Autiero
2022-10-16 22:52:17 +02:00
parent 699367200f
commit 691cd53f26
50 changed files with 1557 additions and 1613 deletions

Binary file not shown.

View File

@@ -7,12 +7,11 @@ import 'package:process_run/shell.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'; import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/node.dart';
import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart'; import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server_standalone.dart'; import 'package:reboot_launcher/src/util/server.dart';
import 'package:shelf_proxy/shelf_proxy.dart'; import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart';
import 'dart:ffi'; import 'dart:ffi';
@@ -23,8 +22,8 @@ import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
// Needed because binaries can't be loaded in any other way // Needed because binaries can't be loaded in any other way
const String _craniumDownload = "https://filebin.net/ybn0gme7dqjr4zup/cranium.dll"; const String _craniumDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848323825675/cranium.dll";
const String _consoleDownload = "https://filebin.net/ybn0gme7dqjr4zup/console.dll"; const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
Process? _gameProcess; Process? _gameProcess;
Process? _eacProcess; Process? _eacProcess;
@@ -121,13 +120,12 @@ Future<void> handleCLI(List<String> args) async {
stdout.writeln("Launching game(type: ${type.name})..."); stdout.writeln("Launching game(type: ${type.name})...");
await _startLauncherProcess(dummyVersion); await _startLauncherProcess(dummyVersion);
await _startEacProcess(dummyVersion);
if (result["type"] == "headless_server") { if (result["type"] == "headless_server") {
if(dummyVersion.executable == null){ if(dummyVersion.executable == null){
throw Exception("Missing game executable at: ${dummyVersion.location.path}"); throw Exception("Missing game executable at: ${dummyVersion.location.path}");
} }
await patchExe(dummyVersion.executable!); await patch(dummyVersion.executable!);
} }
var serverType = _getServerType(result); var serverType = _getServerType(result);
@@ -249,15 +247,6 @@ void _onClose() {
exit(0); exit(0);
} }
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.eacExecutable == null) {
return;
}
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
Win32Process(_eacProcess!.pid).suspend();
}
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async { Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.launcher == null) { if (dummyVersion.launcher == null) {
return; return;
@@ -296,33 +285,7 @@ Future<bool> _startServerIfNeeded(String? host, String? port, ServerType type) a
} }
Future<bool> _changeEmbeddedServerState() async { Future<bool> _changeEmbeddedServerState() async {
var node = await hasNode();
if(!node) {
throw Exception("Missing node, cannot start embedded server");
}
var free = await isLawinPortFree();
if(!free){
stdout.writeln("Server is already running on port 3551");
return true;
}
if(!serverLocation.existsSync()) {
await downloadServer(false);
}
var serverRunner = File("${serverLocation.path}/start.bat");
if (!(await serverRunner.exists())) {
return false;
}
var nodeModules = Directory("${serverLocation.path}/node_modules");
if (!(await nodeModules.exists())) {
await Process.run("${serverLocation.path}/install_packages.bat", [],
workingDirectory: serverLocation.path);
}
await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path);
return true; return true;
} }

View File

@@ -12,10 +12,11 @@ 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/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/page/home_page.dart'; import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
final GlobalKey appKey = GlobalKey();
void main(List<String> args) async { void main(List<String> args) async {
await Directory(safeBinariesDirectory) await Directory(safeBinariesDirectory)
.create(recursive: true); .create(recursive: true);
@@ -55,8 +56,6 @@ class RebootApplication extends StatefulWidget {
} }
class _RebootApplicationState extends State<RebootApplication> { class _RebootApplicationState extends State<RebootApplication> {
final SettingsController _settingsController = Get.find<SettingsController>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = SystemTheme.accentColor.accent.toAccentColor(); final color = SystemTheme.accentColor.accent.toAccentColor();
@@ -67,7 +66,7 @@ class _RebootApplicationState extends State<RebootApplication> {
color: color, color: color,
darkTheme: _createTheme(Brightness.dark), darkTheme: _createTheme(Brightness.dark),
theme: _createTheme(Brightness.light), theme: _createTheme(Brightness.light),
home: const HomePage(), home: HomePage(key: appKey),
); );
} }

View File

@@ -89,4 +89,8 @@ class GameController extends GetxController {
_selectedVersion(version); _selectedVersion(version);
_storage.write("version", version?.name); _storage.write("version", version?.name);
} }
void rename(FortniteVersion version, String result) {
versions.update((val) => version.name = result);
}
} }

View File

@@ -3,7 +3,7 @@ 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:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/server.dart';
import '../model/server_type.dart'; import '../model/server_type.dart';
@@ -17,6 +17,7 @@ class ServerController extends GetxController {
late final Rx<ServerType> type; late final Rx<ServerType> type;
late final RxBool warning; late final RxBool warning;
late RxBool started; late RxBool started;
Process? embeddedServer;
HttpServer? reverseProxy; HttpServer? reverseProxy;
ServerController() { ServerController() {
@@ -39,9 +40,7 @@ class ServerController extends GetxController {
return; return;
} }
loadBinary("release.bat", false) stop();
.then((value) => Process.run(value.path, []))
.then((value) => started(false));
}); });
host = TextEditingController(text: _readHost()); host = TextEditingController(text: _readHost());
@@ -65,4 +64,69 @@ class ServerController extends GetxController {
String _readPort() { String _readPort() {
return _storage.read("${type.value.id}_port") ?? _serverPort; return _storage.read("${type.value.id}_port") ?? _serverPort;
} }
Future<ServerResult> start() async {
var result = await checkServerPreconditions(host.text, port.text, type.value);
if(result.type != ServerResultType.canStart){
return result;
}
try{
switch(type()){
case ServerType.embedded:
embeddedServer = await startEmbeddedServer();
break;
case ServerType.remote:
var uriResult = await result.uri!;
if(uriResult == null){
return ServerResult(
type: ServerResultType.cannotPingServer
);
}
reverseProxy = await startRemoteServer(uriResult);
break;
case ServerType.local:
break;
}
}catch(error, stackTrace){
return ServerResult(
error: error,
stackTrace: stackTrace,
type: ServerResultType.unknownError
);
}
var myself = await pingSelf();
if(myself == null){
return ServerResult(
type: ServerResultType.cannotPingServer
);
}
started(true);
return ServerResult(
type: ServerResultType.started
);
}
Future<bool> stop() async {
started(false);
try{
switch(type()){
case ServerType.embedded:
await freeLawinPort();
break;
case ServerType.remote:
await reverseProxy?.close(force: true);
break;
case ServerType.local:
break;
}
return true;
}catch(_){
started(true);
return false;
}
}
} }

View File

@@ -6,7 +6,7 @@ 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'; import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
class SettingsController extends GetxController { class SettingsController extends GetxController {

View File

@@ -0,0 +1,70 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import '../util/checks.dart';
import '../widget/os/file_selector.dart';
class AddLocalVersion extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _gamePathController = TextEditingController();
AddLocalVersion({Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormBox(
controller: _nameController,
header: "Name",
placeholder: "Type the version's name",
autofocus: true,
validator: (text) => checkVersion(text, _gameController.versions.value)
),
const SizedBox(
height: 16.0
),
FileSelector(
label: "Location",
placeholder: "Type the game folder",
windowTitle: "Select game folder",
controller: _gamePathController,
validator: checkGameFolder,
folder: true
),
const SizedBox(height: 8.0),
],
),
buttons: [
DialogButton(
type: ButtonType.secondary
),
DialogButton(
text: "Save",
type: ButtonType.primary,
onTap: () {
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_gamePathController.text)));
},
)
]
);
}
}

View File

@@ -0,0 +1,323 @@
import 'dart:async';
import 'dart:io';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/build.dart';
import 'package:reboot_launcher/src/widget/home/version_name_input.dart';
import '../util/checks.dart';
import '../widget/home/build_selector.dart';
import '../widget/os/file_selector.dart';
import 'dialog.dart';
class AddServerVersion extends StatefulWidget {
const AddServerVersion({Key? key}) : super(key: key);
@override
State<AddServerVersion> createState() => _AddServerVersionState();
}
class _AddServerVersionState extends State<AddServerVersion> {
final GameController _gameController = Get.find<GameController>();
final BuildController _buildController = Get.find<BuildController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController();
late Future _future;
DownloadStatus _status = DownloadStatus.none;
String _timeLeft = "00:00:00";
double _downloadProgress = 0;
String? _error;
Process? _manifestDownloadProcess;
CancelableOperation? _driveDownloadOperation;
@override
void initState() {
_future = _buildController.builds != null
? Future.value(true)
: compute(fetchBuilds, null)
.then((value) => _buildController.builds = value);
super.initState();
}
@override
void dispose() {
_pathController.dispose();
_nameController.dispose();
_onDisposed();
super.dispose();
}
void _onDisposed() {
if (_status != DownloadStatus.downloading &&
_status != DownloadStatus.extracting) {
return;
}
if (_manifestDownloadProcess != null) {
loadBinary("stop.bat", true).then(
(value) => Process.runSync(value.path, []));
_buildController.cancelledDownload(true);
return;
}
if (_driveDownloadOperation == null) {
return;
}
_driveDownloadOperation!.cancel();
_buildController.cancelledDownload(true);
}
@override
Widget build(BuildContext context) {
return FormDialog(
content: _createDownloadVersionBody(),
buttons: _createDownloadVersionOption(context)
);
}
List<DialogButton> _createDownloadVersionOption(BuildContext context) {
switch (_status) {
case DownloadStatus.none:
return [
DialogButton(type: ButtonType.secondary),
DialogButton(
text: "Download",
type: ButtonType.primary,
onTap: () => _startDownload(context),
)
];
case DownloadStatus.error:
return [DialogButton(type: ButtonType.only)];
default:
return [
DialogButton(
text: _status == DownloadStatus.downloading ? "Stop" : "Close",
type: ButtonType.only)
];
}
}
void _startDownload(BuildContext context) async {
try {
setState(() => _status = DownloadStatus.downloading);
if (_buildController.selectedBuild.hasManifest) {
_manifestDownloadProcess = await downloadManifestBuild(
_buildController.selectedBuild.link,
_pathController.text,
_onDownloadProgress
);
_manifestDownloadProcess!.exitCode
.then((value) => _onDownloadComplete());
} else {
_driveDownloadOperation = CancelableOperation.fromFuture(
downloadArchiveBuild(
_buildController.selectedBuild.link,
_pathController.text,
(progress) => _onDownloadProgress(progress, _timeLeft),
_onUnrar)
).then((_) => _onDownloadComplete(),
onError: (error, _) => _handleError(error));
}
} catch (exception) {
_handleError(exception);
}
}
FutureOr? _handleError(Object exception) {
var message = exception.toString();
_onDownloadError(message.contains(":")
? " ${message.substring(message.indexOf(":") + 1)}"
: message);
return null;
}
void _onUnrar() {
setState(() => _status = DownloadStatus.extracting);
}
void _onDownloadComplete() {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.done;
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_pathController.text)));
});
}
void _onDownloadError(String message) {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.error;
_error = message;
});
}
void _onDownloadProgress(double progress, String timeLeft) {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.downloading;
_timeLeft = timeLeft;
_downloadProgress = progress;
});
}
Widget _createDownloadVersionBody() {
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.hasError) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => _status = DownloadStatus.error));
return Container(
width: double.infinity,
padding: const EdgeInsets.only(bottom: 16.0),
child: Text("Cannot fetch builds: ${snapshot.error}",
textAlign: TextAlign.center),
);
}
if (!snapshot.hasData) {
return InfoLabel(
label: "Fetching builds...",
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
width: double.infinity,
child: const ProgressBar()),
);
}
return _buildBody();
});
}
Widget _buildBody() {
switch (_status) {
case DownloadStatus.none:
return _createNoneBody();
case DownloadStatus.downloading:
return _createDownloadBody();
case DownloadStatus.extracting:
return _createExtractingBody();
case DownloadStatus.done:
return _createDoneBody();
case DownloadStatus.error:
return _createErrorBody();
}
}
Padding _createErrorBody() {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Text("An error was occurred while downloading:$_error",
textAlign: TextAlign.center)),
);
}
Padding _createDoneBody() {
return const Padding(
padding: EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Text("The download was completed successfully!",
textAlign: TextAlign.center)),
);
}
Padding _createExtractingBody() {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InfoLabel(
label: "Extracting...",
child: const SizedBox(width: double.infinity, child: ProgressBar())),
);
}
Column _createDownloadBody() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Downloading...",
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
const SizedBox(
height: 8,
),
if(_manifestDownloadProcess != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${_downloadProgress.round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
Text(
"Time left: $_timeLeft",
style: FluentTheme.maybeOf(context)?.typography.body,
),
],
),
if(_manifestDownloadProcess != null)
const SizedBox(
height: 8,
),
SizedBox(
width: double.infinity,
child: ProgressBar(value: _downloadProgress.toDouble())),
const SizedBox(
height: 16,
)
],
);
}
Column _createNoneBody() {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const BuildSelector(),
const SizedBox(height: 16.0),
VersionNameInput(controller: _nameController),
const SizedBox(height: 16.0),
FileSelector(
label: "Destination",
placeholder: "Type the download destination",
windowTitle: "Select download destination",
controller: _pathController,
validator: checkDownloadDestination,
folder: true),
const SizedBox(height: 8.0),
],
);
}
}
enum DownloadStatus { none, downloading, extracting, error, done }

229
lib/src/dialog/dialog.dart Normal file
View File

@@ -0,0 +1,229 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'dialog_button.dart';
abstract class AbstractDialog extends StatelessWidget {
const AbstractDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context);
}
class GenericDialog extends AbstractDialog {
final Widget header;
final List<DialogButton> buttons;
final EdgeInsets? padding;
const GenericDialog({super.key, required this.header, required this.buttons, this.padding});
@override
Widget build(BuildContext context) {
return ContentDialog(
style: ContentDialogThemeData(
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
),
content: header,
actions: buttons
);
}
}
class FormDialog extends AbstractDialog {
final Widget content;
final List<DialogButton> buttons;
const FormDialog({super.key, required this.content, required this.buttons});
@override
Widget build(BuildContext context) {
return Form(
child: Builder(
builder: (context) {
var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
return GenericDialog(
header: content,
buttons: parsed
);
}
)
);
}
DialogButton _createFormButton(DialogButton entry, BuildContext context) {
if (entry.type == ButtonType.secondary) {
return entry;
}
return DialogButton(
text: entry.text,
type: entry.type,
onTap: () {
if(!Form.of(context)!.validate()) {
return;
}
entry.onTap?.call();
}
);
}
}
class InfoDialog extends AbstractDialog {
final String text;
final List<DialogButton>? buttons;
const InfoDialog({required this.text, this.buttons, super.key});
InfoDialog.ofOnly({required this.text, required DialogButton button, super.key})
: buttons = [button];
@override
Widget build(BuildContext context) {
return GenericDialog(
header: SizedBox(
width: double.infinity,
child: Text(text, textAlign: TextAlign.center)
),
buttons: buttons ?? [_createDefaultButton()],
padding: const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 15.0)
);
}
DialogButton _createDefaultButton() {
return DialogButton(
text: "Close",
type: ButtonType.only
);
}
}
class ProgressDialog extends AbstractDialog {
final String text;
const ProgressDialog({required this.text, super.key});
@override
Widget build(BuildContext context) {
return GenericDialog(
header: InfoLabel(
label: text,
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
width: double.infinity,
child: const ProgressBar()
),
),
buttons: [
DialogButton(
text: "Close",
type: ButtonType.only
)
]
);
}
}
class FutureBuilderDialog extends AbstractDialog {
final Future future;
final String loadingMessage;
final Widget loadedBody;
final Function(Object) errorMessageBuilder;
final Function()? onError;
final bool closeAutomatically;
const FutureBuilderDialog(
{super.key,
required this.future,
required this.loadingMessage,
required this.loadedBody,
required this.errorMessageBuilder,
this.onError,
this.closeAutomatically = false});
static Container ofMessage(String message) {
return Container(
width: double.infinity,
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(
message,
textAlign: TextAlign.center
)
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) => GenericDialog(
header: _createBody(context, snapshot),
buttons: [_createButton(context, snapshot)]
)
);
}
Widget _createBody(BuildContext context, AsyncSnapshot snapshot){
if (snapshot.hasError) {
onError?.call();
return ofMessage(snapshot.error.toString());
}
if (!snapshot.hasData) {
return InfoLabel(
label: loadingMessage,
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
width: double.infinity,
child: const ProgressBar()),
);
}
if(closeAutomatically){
Navigator.of(context).pop(true);
}
return loadedBody;
}
DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot){
return DialogButton(
text: snapshot.hasData || snapshot.hasError ? "Close" : "Stop",
type: ButtonType.only,
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
);
}
}
class ErrorDialog extends AbstractDialog {
final Object exception;
final StackTrace? stackTrace;
final Function(Object) errorMessageBuilder;
const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace});
@override
Widget build(BuildContext context) {
return InfoDialog(
text: errorMessageBuilder(exception),
buttons: [
DialogButton(
type: stackTrace == null ? ButtonType.only : ButtonType.secondary
),
if(stackTrace != null)
DialogButton(
text: "Copy error",
type: ButtonType.primary,
onTap: () async {
FlutterClipboard.controlC("An error occurred: $exception\nStacktrace:\n $stackTrace.toString");
Navigator.of(context).pop();
showMessage("Copied error to clipboard");
},
)
],
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:fluent_ui/fluent_ui.dart';
class DialogButton extends StatefulWidget {
final String? text;
final Function()? onTap;
final ButtonType type;
const DialogButton(
{Key? key,
this.text,
this.onTap,
required this.type})
: assert(type != ButtonType.primary || onTap != null,
"OnTap handler cannot be null for primary buttons"),
assert(type != ButtonType.primary || text != null,
"Text cannot be null for primary buttons"),
super(key: key);
@override
State<DialogButton> createState() => _DialogButtonState();
}
class _DialogButtonState extends State<DialogButton> {
@override
Widget build(BuildContext context) {
return widget.type == ButtonType.only ? _createOnlyButton() : _createButton();
}
SizedBox _createOnlyButton() {
return SizedBox(
width: double.infinity,
child: _createButton()
);
}
Widget _createButton() {
return widget.type == ButtonType.primary ? _createPrimaryActionButton()
: _createSecondaryActionButton();
}
Widget _createPrimaryActionButton() {
return FilledButton(
onPressed: widget.onTap!,
child: Text(widget.text!),
);
}
Widget _createSecondaryActionButton() {
return Button(
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? "Close"),
);
}
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
}
enum ButtonType {
primary,
secondary,
only
}

View File

@@ -0,0 +1,39 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
Future<void> showBrokenError() async {
showDialog(
context: appKey.currentContext!,
builder: (context) => const InfoDialog(
text: "The lawin server is not working correctly"
)
);
}
Future<void> showMissingDllError(String name) async {
showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: "$name dll is not a valid dll, fix it in the settings tab"
)
);
}
Future<void> showTokenError() async {
showDialog(
context: appKey.currentContext!,
builder: (context) => const InfoDialog(
text: "A token error occurred, restart the game and the lawin server, then try again"
)
);
}
Future<void> showUnsupportedHeadless() async {
showDialog(
context: appKey.currentContext!,
builder: (context) => const InfoDialog(
text: "This version of Fortnite doesn't support headless hosting"
)
);
}

View File

@@ -0,0 +1,229 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/future.dart';
import 'package:sync/semaphore.dart';
import '../../main.dart';
import '../util/server.dart';
extension ServerControllerDialog on ServerController {
static Semaphore semaphore = Semaphore();
Future<bool> changeStateInteractive(bool ignorePrompts, [bool isRetry = false]) async {
try {
semaphore.acquire();
if (type() == ServerType.local) {
return _checkLocalServerInteractive(ignorePrompts);
}
var oldStarted = started();
started(!started());
if (oldStarted) {
var result = await stop();
if (!result) {
started(true);
_showCannotStopError();
return true;
}
return false;
}
var result = await start();
var handled = await _handleResultType(result, ignorePrompts, isRetry);
if (!handled) {
started(false);
return false;
}
embeddedServer?.exitCode.then((value) {
if (!started()) {
return;
}
_showUnexpectedError();
started(false);
});
return handled;
}finally{
semaphore.release();
}
}
Future<bool> _handleResultType(ServerResult result, bool ignorePrompts, bool isRetry) async {
switch (result.type) {
case ServerResultType.missingHostError:
_showMissingHostError();
return false;
case ServerResultType.missingPortError:
_showMissingPortError();
return false;
case ServerResultType.illegalPortError:
_showIllegalPortError();
return false;
case ServerResultType.cannotPingServer:
_showPingErrorDialog();
return false;
case ServerResultType.portTakenError:
if (isRetry) {
_showPortTakenError();
return false;
}
var result = await _showPortTakenDialog();
if (!result) {
return false;
}
await freeLawinPort();
return changeStateInteractive(ignorePrompts, true);
case ServerResultType.serverDownloadRequiredError:
if (isRetry) {
return false;
}
var result = await _downloadServerInteractive();
if (!result) {
return false;
}
return changeStateInteractive(ignorePrompts, true);
case ServerResultType.unknownError:
showDialog(
context: appKey.currentContext!,
builder: (context) =>
ErrorDialog(
exception: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
errorMessageBuilder: (
exception) => "Cannot start server: $exception"
)
);
return false;
case ServerResultType.started:
return true;
case ServerResultType.canStart:
case ServerResultType.stopped:
return false;
}
}
Future<bool> _checkLocalServerInteractive(bool ignorePrompts) async {
try {
var future = pingSelf();
if(!ignorePrompts) {
await showDialog(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: future,
loadingMessage: "Pinging server...",
loadedBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port
.text} works correctly"),
errorMessageBuilder: (
exception) => "An error occurred while pining the server: $exception"
)
);
}
return await future != null;
} catch (_) {
return false;
}
}
Future<bool> _downloadServerInteractive() async {
var download = compute(downloadServer, true);
return await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: download,
loadingMessage: "Downloading server...",
loadedBody: FutureBuilderDialog.ofMessage(
"The server was downloaded successfully"),
errorMessageBuilder: (
message) => "Cannot download server: $message"
)
) ?? download.isCompleted();
}
Future<void> _showPortTakenError() async {
showDialog(
context: appKey.currentContext!,
builder: (context) =>
const InfoDialog(
text: "Port 3551 is already in use and the associating process cannot be killed. Kill it manually and try again.",
)
);
}
Future<bool> _showPortTakenDialog() async {
return await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
InfoDialog(
text: "Port 3551 is already in use, do you want to kill the associated process?",
buttons: [
DialogButton(
type: ButtonType.secondary,
onTap: () => Navigator.of(context).pop(false),
),
DialogButton(
text: "Kill",
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(true),
),
],
)
) ?? false;
}
void _showPingErrorDialog() {
showDialog(
context: appKey.currentContext!,
builder: (context) =>
const InfoDialog(
text: "The lawin server is not working correctly. Check the configuration in the associated tab and try again."
)
);
}
void _showCannotStopError() {
showDialog(
context: appKey.currentContext!,
builder: (context) =>
const InfoDialog(
text: "Cannot stop lawin server"
)
);
}
void _showUnexpectedError() {
showDialog(
context: appKey.currentContext!,
builder: (context) =>
const InfoDialog(
text: "The lawin terminated died unexpectedly"
)
);
}
void _showIllegalPortError() {
showMessage("Illegal port for lawin server, use only numbers");
}
void _showMissingPortError() {
showMessage("Missing port for lawin server");
}
void _showMissingHostError() {
showMessage("Missing the host name for lawin server");
}
}

View File

@@ -0,0 +1,13 @@
import 'package:fluent_ui/fluent_ui.dart';
import '../../main.dart';
void showMessage(String text){
showSnackbar(
appKey.currentContext!,
Snackbar(
content: Text(text, textAlign: TextAlign.center),
extended: true
)
);
}

View File

@@ -30,10 +30,6 @@ class FortniteVersion {
return findExecutable(location, "FortniteLauncher.exe"); return findExecutable(location, "FortniteLauncher.exe");
} }
File? get eacExecutable {
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
}
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'name': name, 'name': name,
'location': location.path, 'location': location.path,

View File

@@ -5,10 +5,12 @@ import 'package:reboot_launcher/src/page/settings_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';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/window_border.dart'; import 'package:reboot_launcher/src/widget/os/window_border.dart';
import 'package:reboot_launcher/src/widget/window_buttons.dart'; import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'info_page.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key); const HomePage({Key? key}) : super(key: key);
@@ -18,9 +20,9 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener { class _HomePageState extends State<HomePage> with WindowListener {
static const double _headerSize = 48.0; static const double _headerSize = 48.0;
static const double _sectionSize = 97.0; static const double _sectionSize = 94.0;
static const int _headerButtonCount = 3; static const int _headerButtonCount = 3;
static const int _sectionButtonCount = 3; static const int _sectionButtonCount = 4;
bool _focused = true; bool _focused = true;
bool _shouldMaximize = false; bool _shouldMaximize = false;
@@ -78,6 +80,12 @@ class _HomePageState extends State<HomePage> with WindowListener {
title: const Text("Settings"), title: const Text("Settings"),
icon: const Icon(FluentIcons.settings), icon: const Icon(FluentIcons.settings),
body: SettingsPage() body: SettingsPage()
),
PaneItem(
title: const Text("Info"),
icon: const Icon(FluentIcons.info),
body: const InfoPage()
) )
] ]
), ),
@@ -104,8 +112,8 @@ class _HomePageState extends State<HomePage> with WindowListener {
Padding _createGestureHandler() { Padding _createGestureHandler() {
return Padding( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: _sectionSize * _headerButtonCount, left: _sectionSize * _sectionButtonCount,
right: _headerSize * _sectionButtonCount, right: _headerSize * _headerButtonCount,
), ),
child: SizedBox( child: SizedBox(
height: _headerSize, height: _headerSize,

View File

@@ -0,0 +1,61 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:url_launcher/url_launcher.dart';
const String _discordLink = "https://discord.gg/NJU4QjxSMF";
class InfoPage extends StatelessWidget {
const InfoPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
_createVersionInfo(),
Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
_createAutiesAvatar(),
const SizedBox(
height: 16.0,
),
const Text("Made by Auties00"),
const SizedBox(
height: 16.0,
),
_createDiscordButton()
],
),
],
)
),
);
}
Button _createDiscordButton() {
return Button(
child: const Text("Join the discord"),
onPressed: () => launchUrl(Uri.parse(_discordLink)));
}
CircleAvatar _createAutiesAvatar() {
return const CircleAvatar(
radius: 48,
backgroundImage: AssetImage("assets/images/auties.png"));
}
Align _createVersionInfo() {
return const Align(
alignment: Alignment.bottomRight,
child: Text("Version 4.0${kDebugMode ? '-DEBUG' : ''}")
);
}
}

View File

@@ -7,16 +7,15 @@ 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/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/host_checkbox.dart'; import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
import 'package:reboot_launcher/src/widget/launch_button.dart'; import 'package:reboot_launcher/src/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/widget/username_box.dart'; import 'package:reboot_launcher/src/widget/home/username_box.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart'; import 'package:reboot_launcher/src/widget/home/version_selector.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../controller/settings_controller.dart'; import '../controller/settings_controller.dart';
import '../util/binary.dart';
import '../util/reboot.dart'; import '../util/reboot.dart';
import '../widget/warning_info.dart'; import '../widget/shared/warning_info.dart';
class LauncherPage extends StatefulWidget { class LauncherPage extends StatefulWidget {
const LauncherPage( const LauncherPage(
@@ -59,6 +58,7 @@ class _LauncherPageState extends State<LauncherPage> {
var errorFile = await loadBinary("error.txt", true); var errorFile = await loadBinary("error.txt", true);
errorFile.writeAsString( errorFile.writeAsString(
"Error: $error\nStacktrace: $stackTrace", mode: FileMode.write); "Error: $error\nStacktrace: $stackTrace", mode: FileMode.write);
throw Exception("Cannot update reboot.dll");
} }
void _onCancelWarning() { void _onCancelWarning() {
@@ -104,7 +104,7 @@ class _LauncherPageState extends State<LauncherPage> {
_createUpdateError(snapshot), _createUpdateError(snapshot),
UsernameBox(), UsernameBox(),
const VersionSelector(), const VersionSelector(),
DeploymentSelector(), GameTypeSelector(),
const LaunchButton() const LaunchButton()
], ],
); );
@@ -118,8 +118,7 @@ class _LauncherPageState extends State<LauncherPage> {
text: "Cannot update Reboot DLL", text: "Cannot update Reboot DLL",
icon: FluentIcons.info, icon: FluentIcons.info,
severity: InfoBarSeverity.warning, severity: InfoBarSeverity.warning,
onPressed: () => loadBinary("error.txt", true) onPressed: () => loadBinary("error.txt", true).then((file) => launchUrl(file.uri))
.then((file) => launchUrl(file.uri))
); );
} }
} }

View File

@@ -1,11 +1,11 @@
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/server_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/widget/host_input.dart'; import 'package:reboot_launcher/src/widget/server/host_input.dart';
import 'package:reboot_launcher/src/widget/local_server_switch.dart'; import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/port_input.dart'; import 'package:reboot_launcher/src/widget/server/port_input.dart';
import 'package:reboot_launcher/src/widget/server_button.dart'; import 'package:reboot_launcher/src/widget/server/server_button.dart';
import 'package:reboot_launcher/src/widget/warning_info.dart'; import 'package:reboot_launcher/src/widget/shared/warning_info.dart';
class ServerPage extends StatelessWidget { class ServerPage extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>(); final ServerController _serverController = Get.find<ServerController>();
@@ -28,8 +28,8 @@ class ServerPage extends StatelessWidget {
), ),
HostInput(), HostInput(),
PortInput(), PortInput(),
LocalServerSwitch(), ServerTypeSelector(),
ServerButton() const ServerButton()
] ]
)), )),
); );

View File

@@ -1,11 +1,11 @@
import 'dart:io';
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:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
import 'package:reboot_launcher/src/widget/smart_switch.dart';
import '../util/checks.dart';
import '../widget/os/file_selector.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
@@ -16,9 +16,7 @@ class SettingsPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Stack( child: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -29,7 +27,7 @@ class SettingsPage extends StatelessWidget {
windowTitle: "Select a dll", windowTitle: "Select a dll",
folder: false, folder: false,
extension: "dll", extension: "dll",
validator: _checkDll, validator: checkDll,
validatorMode: AutovalidateMode.always validatorMode: AutovalidateMode.always
), ),
@@ -40,7 +38,7 @@ class SettingsPage extends StatelessWidget {
windowTitle: "Select a dll", windowTitle: "Select a dll",
folder: false, folder: false,
extension: "dll", extension: "dll",
validator: _checkDll, validator: checkDll,
validatorMode: AutovalidateMode.always validatorMode: AutovalidateMode.always
), ),
@@ -51,7 +49,7 @@ class SettingsPage extends StatelessWidget {
windowTitle: "Select a dll", windowTitle: "Select a dll",
folder: false, folder: false,
extension: "dll", extension: "dll",
validator: _checkDll, validator: checkDll,
validatorMode: AutovalidateMode.always validatorMode: AutovalidateMode.always
), ),
@@ -59,31 +57,8 @@ class SettingsPage extends StatelessWidget {
value: _settingsController.autoUpdate, value: _settingsController.autoUpdate,
label: "Update DLLs" label: "Update DLLs"
) )
], ]
),
const Align(
alignment: Alignment.bottomRight,
child: Text("Version 3.13${kDebugMode ? '-DEBUG' : ''}")
)
],
), ),
); );
} }
String? _checkDll(String? text) {
if (text == null || text.isEmpty) {
return "Empty dll path";
}
if (!File(text).existsSync()) {
return "This dll doesn't exist";
}
if (!text.endsWith(".dll")) {
return "This file is not a dll";
}
return null;
}
} }

View File

@@ -1,32 +0,0 @@
import 'dart:io';
Future<File> loadBinary(String binary, bool safe) async{
var safeBinary = File("$safeBinariesDirectory\\$binary");
if(await safeBinary.exists()){
return safeBinary;
}
var internal = _locateInternalBinary(binary);
if(!safe){
return internal;
}
if(await internal.exists()){
await internal.copy(safeBinary.path);
}
return safeBinary;
}
File _locateInternalBinary(String binary){
return File("$internalBinariesDirectory\\$binary");
}
String get internalBinariesDirectory =>
"${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries";
Directory get tempDirectory =>
Directory("${Platform.environment["Temp"]}");
String get safeBinariesDirectory =>
"${Platform.environment["UserProfile"]}\\.reboot_launcher";

View File

@@ -5,9 +5,10 @@ import 'package:html/parser.dart' show parse;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:process_run/shell.dart'; import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/model/fortnite_build.dart'; import 'package:reboot_launcher/src/model/fortnite_build.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/version.dart' as parser; import 'package:reboot_launcher/src/util/version.dart' as parser;
import 'os.dart';
const _userAgent = const _userAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"; "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36";
@@ -80,13 +81,13 @@ Future<List<FortniteBuild>> _fetchManifests() async {
} }
Future<Process> downloadManifestBuild( Future<Process> downloadManifestBuild(
String manifestUrl, String destination, Function(double) onProgress) async { String manifestUrl, String destination, Function(double, String) onProgress) async {
var buildExe = await loadBinary("build.exe", false); var buildExe = await loadBinary("build.exe", false);
var process = await Process.start(buildExe.path, [manifestUrl, destination]); var process = await Process.start(buildExe.path, [manifestUrl, destination]);
process.errLines process.errLines
.where((message) => message.contains("%")) .where((message) => message.contains("%"))
.forEach((message) => onProgress(double.parse(message.split("%")[0]))); .forEach((message) => onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1)));
return process; return process;
} }

56
lib/src/util/checks.dart Normal file
View File

@@ -0,0 +1,56 @@
import 'dart:io';
import '../model/fortnite_version.dart';
String? checkVersion(String? text, List<FortniteVersion> versions) {
if (text == null || text.isEmpty) {
return 'Empty version name';
}
if (versions.any((element) => element.name == text)) {
return 'This version already exists';
}
return null;
}
String? checkGameFolder(text) {
if (text == null || text.isEmpty) {
return 'Empty game path';
}
var directory = Directory(text);
if (!directory.existsSync()) {
return "Directory doesn't exist";
}
if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
return "Invalid game path";
}
return null;
}
String? checkDownloadDestination(text) {
if (text == null || text.isEmpty) {
return 'Invalid download path';
}
return null;
}
String? checkDll(String? text) {
if (text == null || text.isEmpty) {
return "Empty dll path";
}
if (!File(text).existsSync()) {
return "This dll doesn't exist";
}
if (!text.endsWith(".dll")) {
return "This file is not a dll";
}
return null;
}

9
lib/src/util/future.dart Normal file
View File

@@ -0,0 +1,9 @@
import 'dart:async';
extension FutureExtension<T> on Future<T> {
bool isCompleted() {
final completer = Completer<T>();
then(completer.complete).catchError(completer.completeError);
return completer.isCompleted;
}
}

View File

@@ -1,24 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'binary.dart';
import 'package:http/http.dart' as http;
const String _nodeUrl = "https://nodejs.org/dist/v18.11.0/node-v18.11.0-win-x86.zip";
File get embeddedNode =>
File("$safeBinariesDirectory/node-v18.11.0-win-x86/node.exe");
Future<bool> hasNode() async {
var nodeProcess = await Process.run("where", ["node"]);
return nodeProcess.exitCode == 0;
}
Future<bool> downloadNode(ignored) async {
var response = await http.get(Uri.parse(_nodeUrl));
var tempZip = File("${tempDirectory.path}/nodejs.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, safeBinariesDirectory);
return true;
}

View File

@@ -32,26 +32,33 @@ Future<String?> openFilePicker(String extension) async {
return result.files.first.path; return result.files.first.path;
} }
Future<List<Directory>> scanInstallations(String input) => Directory(input) Future<File> loadBinary(String binary, bool safe) async{
.list(recursive: true) var safeBinary = File("$safeBinariesDirectory\\$binary");
.handleError((_) {}, test: (e) => e is FileSystemException) if(await safeBinary.exists()){
.where((element) => path.basename(element.path) == "FortniteClient-Win64-Shipping.exe") return safeBinary;
.map((element) => findContainer(File(element.path)))
.where((element) => element != null)
.map((element) => element!)
.toList();
Directory? findContainer(File file){
var last = file.parent;
for(var x = 0; x < 5; x++){
var name = path.basename(last.path);
if(name != "FortniteGame" || name == "Fortnite"){
last = last.parent;
continue;
} }
return last.parent; var internal = _locateInternalBinary(binary);
if(!safe){
return internal;
} }
return null; if(await internal.exists()){
await internal.copy(safeBinary.path);
} }
return safeBinary;
}
File _locateInternalBinary(String binary){
return File("$internalBinariesDirectory\\$binary");
}
String get internalBinariesDirectory =>
"${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries";
Directory get tempDirectory =>
Directory("${Platform.environment["Temp"]}");
String get safeBinariesDirectory =>
"${Platform.environment["UserProfile"]}\\.reboot_launcher";

View File

@@ -9,7 +9,7 @@ final Uint8List _patched = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]); ]);
Future<bool> patchExe(File file) async { Future<bool> patch(File file) async {
if(_original.length != _patched.length){ if(_original.length != _patched.length){
throw Exception("Cannot mutate length of binary file"); throw Exception("Cannot mutate length of binary file");
} }

View File

@@ -4,7 +4,7 @@ import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/os.dart';
const _rebootUrl = const _rebootUrl =
"https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip"; "https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip";

View File

@@ -1,394 +1,162 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:process_run/shell.dart'; import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/node.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/server_standalone.dart'; import 'package:http/http.dart' as http;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart'; import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:shelf/shelf_io.dart';
import 'package:path/path.dart' as path;
Future<bool> checkLocalServer(BuildContext context, String host, String port, bool closeAutomatically) async { final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin\\Lawin.exe");
host = host.trim(); const String _serverUrl =
if(host.isEmpty){ "https://cdn.discordapp.com/attachments/1026121175878881290/1031230792069820487/LawinServer.zip";
showSnackbar(
context, const Snackbar(content: Text("Missing host name"))); Future<bool> downloadServer(ignored) async {
return false; var response = await http.get(Uri.parse(_serverUrl));
var tempZip = File("${Platform.environment["Temp"]}/lawin.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, serverLocation.parent.path);
return true;
} }
port = port.trim(); Future<bool> isLawinPortFree() async {
if(port.isEmpty){
showSnackbar(
context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center)));
return false;
}
if(int.tryParse(port) == null){
showSnackbar(
context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center)));
return false;
}
return await _showCheck(context, host, port, false, closeAutomatically) != null;
}
Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, String port, bool closeAutomatically, HttpServer? server) async {
if(server != null){
try { try {
server.close(force: true); var portBat = await loadBinary("port.bat", true);
return null; var process = await Process.run(portBat.path, []);
}catch(error){ return !process.outText.contains(" LISTENING ");
_showStopProxyError(context, error); }catch(_){
return server; return ServerSocket.bind("127.0.0.1", 3551)
.then((socket) => socket.close())
.then((_) => true)
.onError((error, _) => false);
} }
} }
host = host.trim(); Future<void> freeLawinPort() async {
if(host.isEmpty){
showSnackbar(
context, const Snackbar(content: Text("Missing host name")));
return null;
}
port = port.trim();
if(port.isEmpty){
showSnackbar(
context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center)));
return null;
}
if(int.tryParse(port) == null){
showSnackbar(
context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center)));
return null;
}
try{
var uri = await _showCheck(context, host, port, true, closeAutomatically);
if(uri == null){
return null;
}
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
}catch(error){
_showStartProxyError(context, error);
return null;
}
}
Future<Uri?> _showCheck(BuildContext context, String host, String port, bool remote, bool closeAutomatically) async {
var future = ping(host, port);
Uri? result;
return await showDialog(
context: context,
builder: (context) => ContentDialog(
content: FutureBuilder<Uri?>(
future: future,
builder: (context, snapshot) {
if(snapshot.hasError){
return SizedBox(
width: double.infinity,
child: Text("Cannot ping ${remote ? "remote" : "local"} server: ${snapshot.error}" , textAlign: TextAlign.center)
);
}
if(snapshot.connectionState == ConnectionState.done && !snapshot.hasData){
return SizedBox(
width: double.infinity,
child: Text(
"The ${remote ? "remote" : "local"} server doesn't work correctly ${remote ? "or the IP and/or the port are incorrect" : ""}",
textAlign: TextAlign.center
)
);
}
result = snapshot.data;
if(snapshot.hasData){
if(remote || closeAutomatically) {
Navigator.of(context).pop(result);
}
return const SizedBox(
width: double.infinity,
child: Text(
"The server works correctly",
textAlign: TextAlign.center
)
);
}
return InfoLabel(
label: "Pinging ${remote ? "remote" : "local"} lawin server...",
child: const SizedBox(
width: double.infinity,
child: ProgressBar()
)
);
}
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(result),
child: const Text('Close'),
))
]
)
);
}
void _showStartProxyError(BuildContext context, Object error) {
showDialog(
context: context,
builder: (context) => ContentDialog(
content: SizedBox(
width: double.infinity,
child: Text("Cannot create the reverse proxy: $error", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
)
)
],
)
);
}
void _showStopProxyError(BuildContext context, Object error) {
showDialog(
context: context,
builder: (context) => ContentDialog(
content: SizedBox(
width: double.infinity,
child: Text("Cannot kill the reverse proxy: $error", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
)
)
],
)
);
}
Future<bool> changeEmbeddedServerState(BuildContext context, bool running) async {
if (running) {
var releaseBat = await loadBinary("release.bat", false); var releaseBat = await loadBinary("release.bat", false);
await Process.run(releaseBat.path, []); await Process.run(releaseBat.path, []);
return false;
} }
List<String> createRebootArgs(String username, bool headless) {
var args = [
"-skippatchcheck",
"-epicapp=Fortnite",
"-epicenv=Prod",
"-epiclocale=en-us",
"-epicportal",
"-noeac",
"-fromfl=be",
"-fltoken=7ce411021b27b4343a44fdg8",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN=$username@projectreboot.dev",
"-AUTH_PASSWORD=Rebooted",
"-AUTH_TYPE=epic"
];
if(headless){
args.addAll(["-nullrhi", "-nosplash", "-nosound"]);
}
return args;
}
Future<Uri?> pingSelf() async => ping("127.0.0.1", "3551");
Future<Uri?> ping(String host, String port, [bool https=false]) async {
var hostName = _getHostName(host);
var declaredScheme = _getScheme(host);
try{
var uri = Uri(
scheme: declaredScheme ?? (https ? "https" : "http"),
host: hostName,
port: int.parse(port)
);
var client = HttpClient()
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
var body = utf8.decode(await response.single);
return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null;
}catch(_){
return https || declaredScheme != null ? null : await ping(host, port, true);
}
}
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type) async {
host = host.trim();
if(host.isEmpty){
return ServerResult(
type: ServerResultType.missingHostError
);
}
port = port.trim();
if(port.isEmpty){
return ServerResult(
type: ServerResultType.missingPortError
);
}
if(int.tryParse(port) == null){
return ServerResult(
type: ServerResultType.illegalPortError
);
}
if(type == ServerType.embedded || type == ServerType.remote){
var free = await isLawinPortFree(); var free = await isLawinPortFree();
if (!free) { if (!free) {
var shouldKill = await _showAlreadyBindPortWarning(context); return ServerResult(
if (!shouldKill) { type: ServerResultType.portTakenError
return false;
}
var releaseBat = await loadBinary("release.bat", false);
await Process.run(releaseBat.path, []);
}
var node = await hasNode();
var useLocalNode = false;
if(!node) {
useLocalNode = true;
if(!embeddedNode.existsSync()){
var result = await _showNodeDownloadInfo(context);
if(!result) {
return false;
}
}
}
if(!serverLocation.existsSync()) {
var result = await _showServerDownloadInfo(context);
if(!result){
return false;
}
}
var serverRunner = File("${serverLocation.path}/start.bat");
if (!serverRunner.existsSync()) {
_showEmbeddedError(context, "missing file ${serverRunner.path}");
return false;
}
var nodeModules = Directory("${serverLocation.path}/node_modules");
if (!nodeModules.existsSync()) {
await Process.run("${serverLocation.path}/install_packages.bat", [],
workingDirectory: serverLocation.path);
}
try {
var logFile = await loadBinary("server.txt", true);
if(logFile.existsSync()){
logFile.deleteSync();
}
var process = await Process.start(
!useLocalNode ? "node" : '"${embeddedNode.path}"',
["index.js"],
workingDirectory: serverLocation.path
); );
process.outLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append));
process.errLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append));
return true;
}catch(exception){
_showEmbeddedError(context, exception.toString());
return false;
} }
} }
Future<bool> _showServerDownloadInfo(BuildContext context) async { if(type == ServerType.embedded && !serverLocation.existsSync()){
var nodeFuture = compute(downloadServer, true); return ServerResult(
var result = await showDialog<bool>( type: ServerResultType.serverDownloadRequiredError
context: context,
builder: (context) => ContentDialog(
content: FutureBuilder(
future: nodeFuture,
builder: (context, snapshot) {
if(snapshot.hasError){
return SizedBox(
width: double.infinity,
child: Text("An error occurred while downloading: ${snapshot.error}",
textAlign: TextAlign.center));
}
if(snapshot.hasData){
return const SizedBox(
width: double.infinity,
child: Text("The download was completed successfully!",
textAlign: TextAlign.center)
); );
} }
return InfoLabel( return ServerResult(
label: "Downloading lawin server...", uri: ping(host, port),
child: const SizedBox( type: ServerResultType.canStart
width: double.infinity,
child: ProgressBar()
)
);
}
),
actions: [
FutureBuilder(
future: nodeFuture,
builder: (builder, snapshot) => SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError),
child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'),
)
)
)
],
)
);
return result != null && result;
}
void _showEmbeddedError(BuildContext context, String error) {
showDialog(
context: context,
builder: (context) => ContentDialog(
content: SizedBox(
width: double.infinity,
child: Text(
"Cannot start server: $error",
textAlign: TextAlign.center
)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
))
],
));
}
Future<bool> _showNodeDownloadInfo(BuildContext context) async {
var nodeFuture = compute(downloadNode, true);
var result = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
content: FutureBuilder(
future: nodeFuture,
builder: (context, snapshot) {
if(snapshot.hasError){
return SizedBox(
width: double.infinity,
child: Text("An error occurred while downloading: ${snapshot.error}",
textAlign: TextAlign.center));
}
if(snapshot.hasData){
return const SizedBox(
width: double.infinity,
child: Text("The download was completed successfully!",
textAlign: TextAlign.center)
); );
} }
return InfoLabel( Future<Process> startEmbeddedServer() async {
label: "Downloading node...", return await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
child: const SizedBox(
width: double.infinity,
child: ProgressBar()
)
);
}
),
actions: [
FutureBuilder(
future: nodeFuture,
builder: (builder, snapshot) => SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError),
child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'),
)
)
)
],
)
);
return result != null && result;
} }
Future<bool> _showAlreadyBindPortWarning(BuildContext context) async { Future<HttpServer> startRemoteServer(Uri uri) async {
return await showDialog<bool>( return await serve(proxyHandler(uri), "127.0.0.1", 3551);
context: context, }
builder: (context) => ContentDialog(
content: const Text( class ServerResult {
"Port 3551 is already in use, do you want to kill the associated process?", final Future<Uri?>? uri;
textAlign: TextAlign.center), final Object? error;
actions: [ final StackTrace? stackTrace;
Button( final ServerResultType type;
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Close'), ServerResult({this.uri, this.error, this.stackTrace, required this.type});
), }
FilledButton(
child: const Text('Kill'), enum ServerResultType {
onPressed: () => Navigator.of(context).pop(true)), missingHostError,
], missingPortError,
)) ?? illegalPortError,
false; cannotPingServer,
portTakenError,
serverDownloadRequiredError,
canStart,
started,
unknownError,
stopped
} }

View File

@@ -1,82 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/util/binary.dart';
final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin");
const String _serverUrl =
"https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip";
Future<bool> downloadServer(ignored) async {
var response = await http.get(Uri.parse(_serverUrl));
var tempZip = File("${Platform.environment["Temp"]}/lawin.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, serverLocation.parent.path);
var result = Directory("${serverLocation.parent.path}/LawinServer-main");
await result.rename("${serverLocation.parent.path}/${path.basename(serverLocation.path)}");
await Process.run("${serverLocation.path}/install_packages.bat", [], workingDirectory: serverLocation.path);
await updateEngineConfig();
return true;
}
Future<void> updateEngineConfig() async {
var engine = File("${serverLocation.path}/CloudStorage/DefaultEngine.ini");
var patchedEngine = await loadBinary("DefaultEngine.ini", true);
await engine.writeAsString(await patchedEngine.readAsString());
}
Future<bool> isLawinPortFree() async {
var portBat = await loadBinary("port.bat", true);
var process = await Process.run(portBat.path, []);
return !process.outText.contains(" LISTENING "); // Goofy way, best we got
}
List<String> createRebootArgs(String username, bool headless) {
var args = [
"-epicapp=Fortnite",
"-epicenv=Prod",
"-epiclocale=en-us",
"-epicportal",
"-skippatchcheck",
"-fromfl=eac",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN=$username@projectreboot.dev",
"-AUTH_PASSWORD=Rebooted",
"-AUTH_TYPE=epic"
];
if(headless){
args.addAll(["-nullrhi", "-nosplash", "-nosound"]);
}
return args;
}
Future<Uri?> ping(String host, String port, [bool https=false]) async {
var hostName = _getHostName(host);
var declaredScheme = _getScheme(host);
try{
var uri = Uri(
scheme: declaredScheme ?? (https ? "https" : "http"),
host: hostName,
port: int.parse(port)
);
var client = HttpClient()
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
var body = utf8.decode(await response.single);
return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null;
}catch(_){
return https || declaredScheme != null ? null : await ping(host, port, true);
}
}
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;

View File

@@ -1,115 +0,0 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
class AddLocalVersion extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _gamePathController = TextEditingController();
AddLocalVersion({Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Form(
child: Builder(
builder: (formContext) => ContentDialog(
style: const ContentDialogThemeData(
padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
),
content: _createLocalVersionDialogBody(),
actions: _createLocalVersionActions(formContext))));
}
List<Widget> _createLocalVersionActions(BuildContext context) {
return [
Button(
onPressed: () => _closeLocalVersionDialog(context, false),
child: const Text('Close'),
),
FilledButton(
child: const Text('Save'),
onPressed: () => _closeLocalVersionDialog(context, true))
];
}
Future<void> _closeLocalVersionDialog(BuildContext context, bool save) async {
if (save) {
if (!Form.of(context)!.validate()) {
return;
}
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_gamePathController.text)));
}
Navigator.of(context).pop(save);
}
Widget _createLocalVersionDialogBody() {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormBox(
controller: _nameController,
header: "Name",
placeholder: "Type the version's name",
autofocus: true,
validator: _checkVersion
),
const SizedBox(
height: 16.0
),
FileSelector(
label: "Location",
placeholder: "Type the game folder",
windowTitle: "Select game folder",
controller: _gamePathController,
validator: _checkGameFolder,
folder: true
),
const SizedBox(height: 8.0),
],
);
}
String? _checkVersion(String? text) {
if (text == null || text.isEmpty) {
return 'Empty version name';
}
if (_gameController.versions.value.any((element) => element.name == text)) {
return 'This version already exists';
}
return null;
}
String? _checkGameFolder(text) {
if (text == null || text.isEmpty) {
return 'Empty game path';
}
var directory = Directory(text);
if (!directory.existsSync()) {
return "Directory doesn't exist";
}
if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
return "Invalid game path";
}
return null;
}
}

View File

@@ -1,373 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/build.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/version_name_input.dart';
import 'build_selector.dart';
class AddServerVersion extends StatefulWidget {
const AddServerVersion(
{Key? key})
: super(key: key);
@override
State<AddServerVersion> createState() => _AddServerVersionState();
}
class _AddServerVersionState extends State<AddServerVersion> {
final GameController _gameController = Get.find<GameController>();
final BuildController _buildController = Get.find<BuildController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController();
late Future _future;
DownloadStatus _status = DownloadStatus.none;
double _downloadProgress = 0;
DateTime? _downloadStartTime;
DateTime? _lastUpdateTime;
Duration? _lastUpdateTimeLeft;
String? _lastUpdateTimeFormatted;
String? _error;
Process? _manifestDownloadProcess;
CancelableOperation? _driveDownloadOperation;
@override
void initState() {
_future = _buildController.builds != null
? Future.value(true)
: compute(fetchBuilds, null)
.then((value) => _buildController.builds = value);
super.initState();
}
@override
void dispose() {
_pathController.dispose();
_nameController.dispose();
_onDisposed();
super.dispose();
}
void _onDisposed() {
if(_status != DownloadStatus.downloading && _status != DownloadStatus.extracting){
return;
}
if (_manifestDownloadProcess != null) {
loadBinary("stop.bat", false)
.then((value) => Process.runSync(value.path, [])); // kill doesn't work :/
_buildController.cancelledDownload.value = true;
return;
}
if(_driveDownloadOperation == null){
return;
}
_driveDownloadOperation!.cancel();
_buildController.cancelledDownload.value = true;
}
@override
Widget build(BuildContext context) {
return Form(
child: Builder(
builder: (context) => ContentDialog(
style: const ContentDialogThemeData(
padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
),
content: _createDownloadVersionBody(),
actions: _createDownloadVersionOption(context))));
}
List<Widget> _createDownloadVersionOption(BuildContext context) {
switch (_status) {
case DownloadStatus.none:
return [
Button(
onPressed: () => _onClose(),
child: const Text('Close')),
FilledButton(
onPressed: () => _startDownload(context),
child: const Text('Download'),
)
];
case DownloadStatus.error:
return [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => _onClose(),
child: const Text('Close')))
];
default:
return [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => _onClose(),
child: Text(
_status == DownloadStatus.downloading ? 'Stop' : 'Close')),
)
];
}
}
void _onClose() {
Navigator.of(context).pop();
}
void _startDownload(BuildContext context) async {
if (!Form.of(context)!.validate()) {
return;
}
try {
setState(() => _status = DownloadStatus.downloading);
if (_buildController.selectedBuild.hasManifest) {
_manifestDownloadProcess = await downloadManifestBuild(
_buildController.selectedBuild.link, _pathController.text, _onDownloadProgress);
_manifestDownloadProcess!.exitCode.then((value) => _onDownloadComplete());
} else {
_driveDownloadOperation = CancelableOperation.fromFuture(
downloadArchiveBuild(_buildController.selectedBuild.link, _pathController.text,
_onDownloadProgress, _onUnrar))
.then((_) => _onDownloadComplete(),
onError: (error, _) => _handleError(error));
}
} catch (exception) {
_handleError(exception);
}
}
FutureOr? _handleError(Object exception) {
var message = exception.toString();
_onDownloadError(message.contains(":")
? " ${message.substring(message.indexOf(":") + 1)}"
: message);
return null;
}
void _onUnrar() {
setState(() => _status = DownloadStatus.extracting);
}
void _onDownloadComplete() {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.done;
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_pathController.text)));
});
}
void _onDownloadError(String message) {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.error;
_error = message;
});
}
void _onDownloadProgress(double progress) {
if (!mounted) {
return;
}
_downloadStartTime ??= DateTime.now();
setState(() {
_status = DownloadStatus.downloading;
_downloadProgress = progress;
});
}
Widget _createDownloadVersionBody() {
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.hasError) {
WidgetsBinding.instance.addPostFrameCallback((_) =>
setState(() => _status = DownloadStatus.error));
return Container(
width: double.infinity,
padding: const EdgeInsets.only(bottom: 16.0),
child: Text("Cannot fetch builds: ${snapshot.error}",
textAlign: TextAlign.center),
);
}
if (!snapshot.hasData) {
return InfoLabel(
label: "Fetching builds...",
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
width: double.infinity,
child: const ProgressBar()
),
);
}
return _buildBody();
});
}
Widget _buildBody() {
switch (_status) {
case DownloadStatus.none:
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const BuildSelector(),
const SizedBox(height: 16.0),
VersionNameInput(controller: _nameController),
const SizedBox(height: 16.0),
FileSelector(
label: "Destination",
placeholder: "Type the download destination",
windowTitle: "Select download destination",
controller: _pathController,
validator: _checkDownloadDestination,
folder: true
),
const SizedBox(height: 8.0),
],
);
case DownloadStatus.downloading:
var timeLeft = _timeLeft;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Downloading...",
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
const SizedBox(
height: 8,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${_downloadProgress.round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
Text(
"Time left: ${timeLeft ?? "00:00:00"}",
style: FluentTheme.maybeOf(context)?.typography.body,
),
],
),
const SizedBox(
height: 8,
),
SizedBox(
width: double.infinity,
child: ProgressBar(value: _downloadProgress.toDouble())
),
const SizedBox(
height: 16,
)
],
);
case DownloadStatus.extracting:
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InfoLabel(
label: "Extracting...",
child: const SizedBox(width: double.infinity, child: ProgressBar())
),
);
case DownloadStatus.done:
return const Padding(
padding: EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Text("The download was completed successfully!",
textAlign: TextAlign.center)),
);
case DownloadStatus.error:
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Text(
"An error was occurred while downloading:$_error",
textAlign: TextAlign.center)),
);
}
}
String? get _timeLeft {
if(_downloadStartTime == null){
return null;
}
var now = DateTime.now();
var elapsed = now.difference(_downloadStartTime!);
var msLeft = (elapsed.inMilliseconds * 100) / _downloadProgress;
if(!msLeft.isFinite){
return null;
}
var timeLeft = Duration(milliseconds: msLeft.round() - elapsed.inMilliseconds);
var delta = _lastUpdateTime == null || _lastUpdateTimeLeft == null ? -1
: timeLeft.inMilliseconds - _lastUpdateTimeLeft!.inMilliseconds;
var shouldSkip = delta == -1 || now.difference(_lastUpdateTime!).inMilliseconds > delta.abs() * 3;
_lastUpdateTime = now;
_lastUpdateTimeLeft = timeLeft;
if(shouldSkip){
return _lastUpdateTimeFormatted;
}
var twoDigitMinutes = _twoDigits(timeLeft.inMinutes.remainder(60));
var twoDigitSeconds = _twoDigits(timeLeft.inSeconds.remainder(60));
return _lastUpdateTimeFormatted =
"${_twoDigits(timeLeft.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
}
String _twoDigits(int n) => n.toString().padLeft(2, "0");
String? _checkDownloadDestination(text) {
if (text == null || text.isEmpty) {
return 'Invalid download path';
}
return null;
}
}
enum DownloadStatus { none, downloading, extracting, error, done }

View File

@@ -3,10 +3,10 @@ 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/model/game_type.dart';
class DeploymentSelector extends StatelessWidget { class GameTypeSelector extends StatelessWidget {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
DeploymentSelector({Key? key}) : super(key: key); GameTypeSelector({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,14 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:clipboard/clipboard.dart';
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: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/dialog/dialog.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/game_dialogs.dart';
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/model/game_type.dart'; import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/os.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';
import 'package:reboot_launcher/src/util/reboot.dart'; import 'package:reboot_launcher/src/util/reboot.dart';
@@ -17,9 +22,8 @@ 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 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../controller/settings_controller.dart'; import '../../controller/settings_controller.dart';
import '../model/server_type.dart'; import '../../dialog/snackbar.dart';
import '../util/server_standalone.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
const LaunchButton( const LaunchButton(
@@ -63,15 +67,13 @@ class _LaunchButtonState extends State<LaunchButton> {
void _onPressed() async { void _onPressed() async {
if (_gameController.username.text.isEmpty) { if (_gameController.username.text.isEmpty) {
showSnackbar( showMessage("Missing in-game username");
context, const Snackbar(content: Text("Please type a username")));
_updateServerState(false); _updateServerState(false);
return; return;
} }
if (_gameController.selectedVersionObs.value == null) { if (_gameController.selectedVersionObs.value == null) {
showSnackbar( showMessage("No version is selected");
context, const Snackbar(content: Text("Please select a version")));
_updateServerState(false); _updateServerState(false);
return; return;
} }
@@ -90,17 +92,17 @@ class _LaunchButtonState extends State<LaunchButton> {
Win32Process(_gameController.launcherProcess!.pid).suspend(); Win32Process(_gameController.launcherProcess!.pid).suspend();
} }
if (version.eacExecutable != null) {
_gameController.eacProcess = await Process.start(version.eacExecutable!.path, []);
Win32Process(_gameController.eacProcess!.pid).suspend();
}
if(hosting){ if(hosting){
await patchExe(version.executable!); await patch(version.executable!);
} }
await _startServerIfNecessary(); if(!mounted){
if(!_serverController.started.value){ _onStop();
return;
}
var result = await _serverController.changeStateInteractive(true);
if(!result){
_onStop(); _onStop();
return; return;
} }
@@ -131,42 +133,6 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<void> _startServerIfNecessary() async {
if (!mounted) {
return;
}
if(_serverController.started.value){
return;
}
switch(_serverController.type.value){
case ServerType.embedded:
var result = await changeEmbeddedServerState(context, false);
_serverController.started(result);
break;
case ServerType.remote:
_serverController.reverseProxy = await changeReverseProxyState(
context,
_serverController.host.text,
_serverController.port.text,
false,
_serverController.reverseProxy
);
_serverController.started(_serverController.reverseProxy != null);
break;
case ServerType.local:
var result = await checkLocalServer(
context,
_serverController.host.text,
_serverController.port.text,
true
);
_serverController.started(result);
break;
}
}
Future<void> _updateServerState(bool value) async { Future<void> _updateServerState(bool value) async {
if (_gameController.started.value == value) { if (_gameController.started.value == value) {
return; return;
@@ -197,106 +163,6 @@ class _LaunchButtonState extends State<LaunchButton> {
Navigator.of(context).pop(success); Navigator.of(context).pop(success);
} }
Future<void> _showBrokenServerWarning() async {
if(!mounted){
return;
}
showDialog(
context: context,
builder: (context) => ContentDialog(
content: const SizedBox(
width: double.infinity,
child: Text("The lawin server is not working correctly", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
)
)
],
)
);
}
Future<void> _showMissingDllError(String name) async {
if(!mounted){
return;
}
showDialog(
context: context,
builder: (context) => ContentDialog(
content: SizedBox(
width: double.infinity,
child: Text("$name dll is not a valid dll, fix it in the settings tab", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
)
)
],
)
);
}
Future<void> _showTokenError() async {
if(!mounted){
return;
}
showDialog(
context: context,
builder: (context) => ContentDialog(
content: const SizedBox(
width: double.infinity,
child: Text("A token error occurred, restart the game and the lawin server, then try again", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
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(),
child: const Text('Close'),
)
)
],
)
);
}
Future<void> _showServerLaunchingWarning() async { Future<void> _showServerLaunchingWarning() async {
if(!mounted){ if(!mounted){
return; return;
@@ -304,27 +170,16 @@ class _LaunchButtonState extends State<LaunchButton> {
var result = await showDialog<bool>( var result = await showDialog<bool>(
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => InfoDialog.ofOnly(
content: InfoLabel( text: "Launching headless reboot server...",
label: "Launching headless reboot server...", button: DialogButton(
child: const SizedBox( type: ButtonType.only,
width: double.infinity, onTap: () {
child: ProgressBar()
)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
_onStop(); _onStop();
}, }
child: const Text('Cancel'),
) )
) )
],
)
); );
if(result != null && result){ if(result != null && result){
@@ -335,10 +190,6 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
void _onGameOutput(String line) { void _onGameOutput(String line) {
if(kDebugMode){
print(line);
}
if(_logFile != null){ if(_logFile != null){
_logFile!.writeAsString("$line\n", mode: FileMode.append); _logFile!.writeAsString("$line\n", mode: FileMode.append);
} }
@@ -351,59 +202,43 @@ class _LaunchButtonState extends State<LaunchButton> {
if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){ if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){
_fail = true; _fail = true;
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
_showBrokenServerWarning(); showBrokenError();
return; return;
} }
if(line.contains("HTTP 400 response from ")){ if(line.contains("HTTP 400 response from ")){
_fail = true; _fail = true;
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
_showUnsupportedHeadless(); showUnsupportedHeadless();
return; return;
} }
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){ if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
_fail = true; _fail = true;
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
_showTokenError(); showTokenError();
return; return;
} }
if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) { if(line.contains("Region")){
if(_gameController.type.value == GameType.client){
_injectOrShowError(Injectable.console); _injectOrShowError(Injectable.console);
return; }else {
}
if(line.contains("Region") && _gameController.type.value != GameType.client){
_injectOrShowError(Injectable.reboot) _injectOrShowError(Injectable.reboot)
.then((value) => _closeDialogIfOpen(true)); .then((value) => _closeDialogIfOpen(true));
} }
} }
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);
} }
Future<Object?> _onError(Object exception, StackTrace? stackTrace) async {
return showDialog( return showDialog(
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => ErrorDialog(
content: SizedBox( exception: exception,
width: double.infinity, stackTrace: stackTrace,
child: Text("Cannot launch fortnite: $exception", errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception"
textAlign: TextAlign.center)), )
actions: [ );
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Close'),
))
],
));
} }
void _onStop() { void _onStop() {
@@ -422,12 +257,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if(!dllPath.existsSync()) { if(!dllPath.existsSync()) {
await _downloadMissingDll(injectable); await _downloadMissingDll(injectable);
if(!dllPath.existsSync()){ if(!dllPath.existsSync()){
WidgetsBinding.instance.addPostFrameCallback((_) { _onDllFail(dllPath);
_fail = true;
_closeDialogIfOpen(false);
_showMissingDllError(path.basename(dllPath.path));
_onStop();
});
return; return;
} }
} }
@@ -445,6 +275,15 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
void _onDllFail(File dllPath) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_fail = true;
_closeDialogIfOpen(false);
showMissingDllError(path.basename(dllPath.path));
_onStop();
});
}
File _getDllPath(Injectable injectable){ File _getDllPath(Injectable injectable){
switch(injectable){ switch(injectable){
case Injectable.reboot: case Injectable.reboot:

View File

@@ -2,7 +2,7 @@ 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/model/game_type.dart';
import 'package:reboot_launcher/src/widget/smart_input.dart'; import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
class UsernameBox extends StatelessWidget { class UsernameBox extends StatelessWidget {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();

View File

@@ -5,16 +5,15 @@ import 'package:flutter/gestures.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/fortnite_version.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/widget/add_local_version.dart'; import 'package:reboot_launcher/src/dialog/add_local_version.dart';
import 'package:reboot_launcher/src/widget/add_server_version.dart'; import 'package:reboot_launcher/src/widget/shared/smart_check_box.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 { import '../../dialog/add_server_version.dart';
final bool enableScanner; import '../../util/checks.dart';
const VersionSelector({Key? key, this.enableScanner = false}) : super(key: key); class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);
@override @override
State<VersionSelector> createState() => _VersionSelectorState(); State<VersionSelector> createState() => _VersionSelectorState();
@@ -45,17 +44,6 @@ class _VersionSelectorState extends State<VersionSelector> {
const SizedBox( const SizedBox(
width: 16, width: 16,
), ),
if(widget.enableScanner)
Tooltip(
message: "Scan all fortnite builds in a directory",
child: Button(
child: const Icon(FluentIcons.site_scan),
onPressed: () => _openScanLocalVersionDialog(context)),
),
if(widget.enableScanner)
const SizedBox(
width: 16,
),
Tooltip( Tooltip(
message: "Download a fortnite build from the archive", message: "Download a fortnite build from the archive",
child: Button( child: Button(
@@ -125,33 +113,20 @@ class _VersionSelectorState extends State<VersionSelector> {
builder: (context) => AddLocalVersion()); builder: (context) => AddLocalVersion());
} }
void _openScanLocalVersionDialog(BuildContext context) async {
await showDialog<bool>(
context: context,
builder: (context) => const ScanLocalVersion());
}
Future<void> _openMenu( Future<void> _openMenu(
BuildContext context, FortniteVersion version, Offset offset) async { BuildContext context, FortniteVersion version, Offset offset) async {
var result = await showMenu<int?>( var result = await showMenu<ContextualOption>(
context: context, context: context,
offset: offset, offset: offset,
builder: (context) => MenuFlyout( builder: (context) => MenuFlyout(
items: [ items: ContextualOption.values
MenuFlyoutItem( .map((entry) => _createOption(context, entry))
text: const Text('Open in explorer'), .toList()
onPressed: () => Navigator.of(context).pop(0)
),
MenuFlyoutItem(
text: const Text('Delete'),
onPressed: () => Navigator.of(context).pop(1)
),
],
) )
); );
switch (result) { switch (result) {
case 0: case ContextualOption.openExplorer:
if(!mounted){ if(!mounted){
return; return;
} }
@@ -161,7 +136,21 @@ class _VersionSelectorState extends State<VersionSelector> {
.onError((error, stackTrace) => _onExplorerError()); .onError((error, stackTrace) => _onExplorerError());
break; break;
case 1: case ContextualOption.rename:
if(!mounted){
return;
}
Navigator.of(context).pop();
var result = await _openRenameDialog(context, version);
if(result == null){
return;
}
_gameController.rename(version, result);
break;
case ContextualOption.delete:
if(!mounted){ if(!mounted){
return; return;
} }
@@ -183,9 +172,19 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
break; break;
case null:
break;
} }
} }
MenuFlyoutItem _createOption(BuildContext context, ContextualOption entry) {
return MenuFlyoutItem(
text: Text(entry.name),
onPressed: () => Navigator.of(context).pop(entry)
);
}
bool _onExplorerError() { bool _onExplorerError() {
showSnackbar( showSnackbar(
context, context,
@@ -231,4 +230,61 @@ class _VersionSelectorState extends State<VersionSelector> {
) )
); );
} }
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
var controller = TextEditingController(text: version.name);
return showDialog<String?>(
context: context,
builder: (context) => Form(
child: Builder(
builder: (context) => ContentDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormBox(
controller: controller,
header: "Name",
placeholder: "Type the new version name",
autofocus: true,
validator: (text) => checkVersion(text, _gameController.versions.value)
),
const SizedBox(height: 8.0),
],
),
actions: [
Button(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('Close'),
),
FilledButton(
onPressed: () {
if (!Form.of(context)!.validate()) {
return;
}
Navigator.of(context).pop(controller.text);
},
child: const Text('Save')
)
]
)
)
)
);
}
}
enum ContextualOption {
openExplorer,
rename,
delete;
String get name {
return this == ContextualOption.openExplorer ? "Open in explorer"
: this == ContextualOption.rename ? "Rename"
: "Delete";
}
} }

View File

@@ -1,7 +1,8 @@
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:reboot_launcher/src/dialog/snackbar.dart';
import '../util/os.dart'; import '../../util/os.dart';
class FileSelector extends StatefulWidget { class FileSelector extends StatefulWidget {
final String label; final String label;
@@ -65,7 +66,7 @@ class _FileSelectorState extends State<FileSelector> {
void _onPressed() { void _onPressed() {
if(_selecting){ if(_selecting){
showSnackbar(context, const Snackbar(content: Text("Folder selector is already opened"))); showMessage("Folder selector is already opened");
return; return;
} }

View File

@@ -1,170 +0,0 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
class ScanLocalVersion extends StatefulWidget {
const ScanLocalVersion({Key? key})
: super(key: key);
@override
State<ScanLocalVersion> createState() => _ScanLocalVersionState();
}
class _ScanLocalVersionState extends State<ScanLocalVersion> {
final TextEditingController _folderController = TextEditingController();
Future<List<Directory>>? _future;
@override
Widget build(BuildContext context) {
return Form(
child: Builder(
builder: (formContext) => ContentDialog(
style: const ContentDialogThemeData(
padding: EdgeInsets.only(left: 20, right: 20, top: 20.0, bottom: 0.0)
),
constraints: const BoxConstraints(maxWidth: 368, maxHeight: 169),
content: _createLocalVersionDialogBody(),
actions: _createLocalVersionActions(formContext))));
}
List<Widget> _createLocalVersionActions(BuildContext context) {
if(_future == null) {
return [
Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
FilledButton(
child: const Text('Scan'),
onPressed: () => _scanFolder(context, true))
];
}
return [
FutureBuilder(
future: _future,
builder: (context, snapshot) {
if(!snapshot.hasData || snapshot.hasError) {
return SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
);
}
return SizedBox(
width: double.infinity,
child: FilledButton(
child: const Text('Save'),
onPressed: () => Navigator.of(context).pop()
)
);
}
)
];
}
Future<void> _scanFolder(BuildContext context, bool save) async {
setState(() {
_future = compute(scanInstallations, _folderController.text);
});
}
Widget _createLocalVersionDialogBody() {
if(_future == null) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FileSelector(
label: "Location",
placeholder: "Type the folder to scan",
windowTitle: "Select the folder to scan",
controller: _folderController,
validator: _checkScanFolder,
folder: true
)
],
);
}
return FutureBuilder<List<Directory>>(
future: _future,
builder: (context, snapshot) {
if(snapshot.hasError) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Text(
"An error was occurred while scanning:${snapshot.error}",
textAlign: TextAlign.center)),
);
}
if(!snapshot.hasData){
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InfoLabel(
label: "Searching...",
child: const SizedBox(width: double.infinity, child: ProgressBar())
),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Column(
children: [
const Text(
"Successfully completed scan",
textAlign: TextAlign.center),
_createResultsDropDown(snapshot.data!)
],
)),
);
}
);
}
Widget _createResultsDropDown(List<Directory> data) {
return Expanded(
child: DropDownButton(
leading: const Text("Results"),
items: data.map((element) => _createResultItem(element)).toList()
),
);
}
MenuFlyoutItem _createResultItem(element) {
return MenuFlyoutItem(
text: SizedBox(
width: double.infinity,
child: Text(path.basename(element.path))
),
trailing: const Expanded(child: SizedBox()),
onPressed: () {});
}
String? _checkScanFolder(text) {
if (text == null || text.isEmpty) {
return 'Invalid folder to scan';
}
var directory = Directory(text);
if (!directory.existsSync()) {
return "Directory doesn't exist";
}
return null;
}
}

View File

@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.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/server_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/smart_input.dart'; import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
class HostInput extends StatelessWidget { class HostInput extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>(); final ServerController _serverController = Get.find<ServerController>();

View File

@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.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/server_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/smart_input.dart'; import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
class PortInput extends StatelessWidget { class PortInput extends StatelessWidget {

View File

@@ -1,14 +1,19 @@
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/server_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/server.dart';
class ServerButton extends StatelessWidget { class ServerButton extends StatefulWidget {
const ServerButton({Key? key}) : super(key: key);
@override
State<ServerButton> createState() => _ServerButtonState();
}
class _ServerButtonState extends State<ServerButton> {
final ServerController _serverController = Get.find<ServerController>(); final ServerController _serverController = Get.find<ServerController>();
ServerButton({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( return Align(
@@ -18,7 +23,7 @@ class ServerButton extends StatelessWidget {
child: Obx(() => Tooltip( child: Obx(() => Tooltip(
message: _helpMessage, message: _helpMessage,
child: Button( child: Button(
onPressed: () => _onPressed(context), onPressed: () async => _serverController.changeStateInteractive(false),
child: Text(_buttonText())), child: Text(_buttonText())),
)), )),
), ),
@@ -55,42 +60,4 @@ class ServerButton extends StatelessWidget {
return "Check if a local lawin server is running"; return "Check if a local lawin server is running";
} }
} }
void _onPressed(BuildContext context) async {
var running = _serverController.started.value;
_serverController.started.value = !running;
switch(_serverController.type.value){
case ServerType.embedded:
var updatedRunning = await changeEmbeddedServerState(context, running);
_updateStarted(updatedRunning);
break;
case ServerType.remote:
_serverController.reverseProxy = await changeReverseProxyState(
context,
_serverController.host.text,
_serverController.port.text,
false,
_serverController.reverseProxy
);
_updateStarted(_serverController.reverseProxy != null);
break;
case ServerType.local:
var result = await checkLocalServer(
context,
_serverController.host.text,
_serverController.port.text,
false
);
_updateStarted(result);
break;
}
}
void _updateStarted(bool updatedRunning) {
if (updatedRunning == _serverController.started.value) {
return;
}
_serverController.started.value = updatedRunning;
}
} }

View File

@@ -3,10 +3,10 @@ import 'package:get/get.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/server_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
class LocalServerSwitch extends StatelessWidget { class ServerTypeSelector extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>(); final ServerController _serverController = Get.find<ServerController>();
LocalServerSwitch({Key? key}) : super(key: key); ServerTypeSelector({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Launcher for project reboot description: Launcher for project reboot
version: "3.13.0" version: "4.0.0"
publish_to: 'none' publish_to: 'none'
@@ -34,6 +34,8 @@ dependencies:
shelf_proxy: ^1.0.2 shelf_proxy: ^1.0.2
args: ^2.3.1 args: ^2.3.1
win32: 3.0.0 win32: 3.0.0
clipboard: ^0.1.3
sync: ^0.3.0
dependency_overrides: dependency_overrides:
win32: ^3.0.0 win32: ^3.0.0
@@ -56,7 +58,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.13.0.0 msix_version: 4.0.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