mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
decoupled business logic from ui
This commit is contained in:
47
lib/cli.dart
47
lib/cli.dart
@@ -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/game_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/node.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.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:win32_suspend_process/win32_suspend_process.dart';
|
||||
import 'dart:ffi';
|
||||
@@ -23,8 +22,8 @@ import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
// Needed because binaries can't be loaded in any other way
|
||||
const String _craniumDownload = "https://filebin.net/ybn0gme7dqjr4zup/cranium.dll";
|
||||
const String _consoleDownload = "https://filebin.net/ybn0gme7dqjr4zup/console.dll";
|
||||
const String _craniumDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848323825675/cranium.dll";
|
||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
|
||||
|
||||
Process? _gameProcess;
|
||||
Process? _eacProcess;
|
||||
@@ -121,13 +120,12 @@ Future<void> handleCLI(List<String> args) async {
|
||||
|
||||
stdout.writeln("Launching game(type: ${type.name})...");
|
||||
await _startLauncherProcess(dummyVersion);
|
||||
await _startEacProcess(dummyVersion);
|
||||
if (result["type"] == "headless_server") {
|
||||
if(dummyVersion.executable == null){
|
||||
throw Exception("Missing game executable at: ${dummyVersion.location.path}");
|
||||
}
|
||||
|
||||
await patchExe(dummyVersion.executable!);
|
||||
await patch(dummyVersion.executable!);
|
||||
}
|
||||
|
||||
var serverType = _getServerType(result);
|
||||
@@ -249,15 +247,6 @@ void _onClose() {
|
||||
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 {
|
||||
if (dummyVersion.launcher == null) {
|
||||
return;
|
||||
@@ -296,33 +285,7 @@ Future<bool> _startServerIfNeeded(String? host, String? port, ServerType type) a
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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/settings_controller.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:system_theme/system_theme.dart';
|
||||
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
void main(List<String> args) async {
|
||||
await Directory(safeBinariesDirectory)
|
||||
.create(recursive: true);
|
||||
@@ -55,8 +56,6 @@ class RebootApplication extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RebootApplicationState extends State<RebootApplication> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = SystemTheme.accentColor.accent.toAccentColor();
|
||||
@@ -67,7 +66,7 @@ class _RebootApplicationState extends State<RebootApplication> {
|
||||
color: color,
|
||||
darkTheme: _createTheme(Brightness.dark),
|
||||
theme: _createTheme(Brightness.light),
|
||||
home: const HomePage(),
|
||||
home: HomePage(key: appKey),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,4 +89,8 @@ class GameController extends GetxController {
|
||||
_selectedVersion(version);
|
||||
_storage.write("version", version?.name);
|
||||
}
|
||||
|
||||
void rename(FortniteVersion version, String result) {
|
||||
versions.update((val) => version.name = result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.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';
|
||||
|
||||
@@ -17,6 +17,7 @@ class ServerController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final RxBool warning;
|
||||
late RxBool started;
|
||||
Process? embeddedServer;
|
||||
HttpServer? reverseProxy;
|
||||
|
||||
ServerController() {
|
||||
@@ -39,9 +40,7 @@ class ServerController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
loadBinary("release.bat", false)
|
||||
.then((value) => Process.run(value.path, []))
|
||||
.then((value) => started(false));
|
||||
stop();
|
||||
});
|
||||
|
||||
host = TextEditingController(text: _readHost());
|
||||
@@ -65,4 +64,69 @@ class ServerController extends GetxController {
|
||||
String _readPort() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.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';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
|
||||
70
lib/src/dialog/add_local_version.dart
Normal file
70
lib/src/dialog/add_local_version.dart
Normal 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)));
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
323
lib/src/dialog/add_server_version.dart
Normal file
323
lib/src/dialog/add_server_version.dart
Normal 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
229
lib/src/dialog/dialog.dart
Normal 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");
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
62
lib/src/dialog/dialog_button.dart
Normal file
62
lib/src/dialog/dialog_button.dart
Normal 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
|
||||
}
|
||||
39
lib/src/dialog/game_dialogs.dart
Normal file
39
lib/src/dialog/game_dialogs.dart
Normal 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
229
lib/src/dialog/server_dialogs.dart
Normal file
229
lib/src/dialog/server_dialogs.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
13
lib/src/dialog/snackbar.dart
Normal file
13
lib/src/dialog/snackbar.dart
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -30,10 +30,6 @@ class FortniteVersion {
|
||||
return findExecutable(location, "FortniteLauncher.exe");
|
||||
}
|
||||
|
||||
File? get eacExecutable {
|
||||
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'location': location.path,
|
||||
|
||||
@@ -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/server_page.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/widget/window_border.dart';
|
||||
import 'package:reboot_launcher/src/widget/window_buttons.dart';
|
||||
import 'package:reboot_launcher/src/widget/os/window_border.dart';
|
||||
import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'info_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -18,9 +20,9 @@ class HomePage extends StatefulWidget {
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener {
|
||||
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 _sectionButtonCount = 3;
|
||||
static const int _sectionButtonCount = 4;
|
||||
|
||||
bool _focused = true;
|
||||
bool _shouldMaximize = false;
|
||||
@@ -78,6 +80,12 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
title: const Text("Settings"),
|
||||
icon: const Icon(FluentIcons.settings),
|
||||
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() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _sectionSize * _headerButtonCount,
|
||||
right: _headerSize * _sectionButtonCount,
|
||||
left: _sectionSize * _sectionButtonCount,
|
||||
right: _headerSize * _headerButtonCount,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _headerSize,
|
||||
|
||||
61
lib/src/page/info_page.dart
Normal file
61
lib/src/page/info_page.dart
Normal 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' : ''}")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/widget/host_checkbox.dart';
|
||||
import 'package:reboot_launcher/src/widget/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/username_box.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/username_box.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import '../util/binary.dart';
|
||||
import '../util/reboot.dart';
|
||||
import '../widget/warning_info.dart';
|
||||
import '../widget/shared/warning_info.dart';
|
||||
|
||||
class LauncherPage extends StatefulWidget {
|
||||
const LauncherPage(
|
||||
@@ -59,6 +58,7 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
var errorFile = await loadBinary("error.txt", true);
|
||||
errorFile.writeAsString(
|
||||
"Error: $error\nStacktrace: $stackTrace", mode: FileMode.write);
|
||||
throw Exception("Cannot update reboot.dll");
|
||||
}
|
||||
|
||||
void _onCancelWarning() {
|
||||
@@ -104,7 +104,7 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
_createUpdateError(snapshot),
|
||||
UsernameBox(),
|
||||
const VersionSelector(),
|
||||
DeploymentSelector(),
|
||||
GameTypeSelector(),
|
||||
const LaunchButton()
|
||||
],
|
||||
);
|
||||
@@ -115,11 +115,10 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
|
||||
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
|
||||
return WarningInfo(
|
||||
text: "Cannot update Reboot DLL",
|
||||
icon: FluentIcons.info,
|
||||
severity: InfoBarSeverity.warning,
|
||||
onPressed: () => loadBinary("error.txt", true)
|
||||
.then((file) => launchUrl(file.uri))
|
||||
text: "Cannot update Reboot DLL",
|
||||
icon: FluentIcons.info,
|
||||
severity: InfoBarSeverity.warning,
|
||||
onPressed: () => loadBinary("error.txt", true).then((file) => launchUrl(file.uri))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/widget/host_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/local_server_switch.dart';
|
||||
import 'package:reboot_launcher/src/widget/port_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/server_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/warning_info.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/host_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/port_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/server_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/warning_info.dart';
|
||||
|
||||
class ServerPage extends StatelessWidget {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
@@ -28,8 +28,8 @@ class ServerPage extends StatelessWidget {
|
||||
),
|
||||
HostInput(),
|
||||
PortInput(),
|
||||
LocalServerSwitch(),
|
||||
ServerButton()
|
||||
ServerTypeSelector(),
|
||||
const ServerButton()
|
||||
]
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.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/smart_switch.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
|
||||
|
||||
import '../util/checks.dart';
|
||||
import '../widget/os/file_selector.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
@@ -16,9 +16,7 @@ class SettingsPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -29,7 +27,7 @@ class SettingsPage extends StatelessWidget {
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: _checkDll,
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always
|
||||
),
|
||||
|
||||
@@ -40,7 +38,7 @@ class SettingsPage extends StatelessWidget {
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: _checkDll,
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always
|
||||
),
|
||||
|
||||
@@ -51,7 +49,7 @@ class SettingsPage extends StatelessWidget {
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: _checkDll,
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always
|
||||
),
|
||||
|
||||
@@ -59,31 +57,8 @@ class SettingsPage extends StatelessWidget {
|
||||
value: _settingsController.autoUpdate,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -5,9 +5,10 @@ import 'package:html/parser.dart' show parse;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:process_run/shell.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 'os.dart';
|
||||
|
||||
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";
|
||||
|
||||
@@ -80,13 +81,13 @@ Future<List<FortniteBuild>> _fetchManifests() async {
|
||||
}
|
||||
|
||||
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 process = await Process.start(buildExe.path, [manifestUrl, destination]);
|
||||
|
||||
process.errLines
|
||||
.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;
|
||||
}
|
||||
|
||||
56
lib/src/util/checks.dart
Normal file
56
lib/src/util/checks.dart
Normal 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
9
lib/src/util/future.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -32,26 +32,33 @@ Future<String?> openFilePicker(String extension) async {
|
||||
return result.files.first.path;
|
||||
}
|
||||
|
||||
Future<List<Directory>> scanInstallations(String input) => Directory(input)
|
||||
.list(recursive: true)
|
||||
.handleError((_) {}, test: (e) => e is FileSystemException)
|
||||
.where((element) => path.basename(element.path) == "FortniteClient-Win64-Shipping.exe")
|
||||
.map((element) => findContainer(File(element.path)))
|
||||
.where((element) => element != null)
|
||||
.map((element) => element!)
|
||||
.toList();
|
||||
|
||||
Directory? findContainer(File file){
|
||||
var last = file.parent;
|
||||
for(var x = 0; x < 5; x++){
|
||||
var name = path.basename(last.path);
|
||||
if(name != "FortniteGame" || name == "Fortnite"){
|
||||
last = last.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
return last.parent;
|
||||
Future<File> loadBinary(String binary, bool safe) async{
|
||||
var safeBinary = File("$safeBinariesDirectory\\$binary");
|
||||
if(await safeBinary.exists()){
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
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";
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
Future<bool> patchExe(File file) async {
|
||||
Future<bool> patch(File file) async {
|
||||
if(_original.length != _patched.length){
|
||||
throw Exception("Cannot mutate length of binary file");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:archive/archive_io.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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 =
|
||||
"https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip";
|
||||
|
||||
@@ -1,394 +1,162 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/util/binary.dart';
|
||||
import 'package:reboot_launcher/src/util/node.dart';
|
||||
import 'package:reboot_launcher/src/util/server_standalone.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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 {
|
||||
host = host.trim();
|
||||
if(host.isEmpty){
|
||||
showSnackbar(
|
||||
context, const Snackbar(content: Text("Missing host name")));
|
||||
return false;
|
||||
}
|
||||
final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin\\Lawin.exe");
|
||||
const String _serverUrl =
|
||||
"https://cdn.discordapp.com/attachments/1026121175878881290/1031230792069820487/LawinServer.zip";
|
||||
|
||||
port = port.trim();
|
||||
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<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);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, String port, bool closeAutomatically, HttpServer? server) async {
|
||||
if(server != null){
|
||||
try{
|
||||
server.close(force: true);
|
||||
return null;
|
||||
}catch(error){
|
||||
_showStopProxyError(context, error);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
host = host.trim();
|
||||
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);
|
||||
await Process.run(releaseBat.path, []);
|
||||
return false;
|
||||
}
|
||||
|
||||
var free = await isLawinPortFree();
|
||||
if (!free) {
|
||||
var shouldKill = await _showAlreadyBindPortWarning(context);
|
||||
if (!shouldKill) {
|
||||
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);
|
||||
}
|
||||
|
||||
Future<bool> isLawinPortFree() async {
|
||||
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;
|
||||
var portBat = await loadBinary("port.bat", true);
|
||||
var process = await Process.run(portBat.path, []);
|
||||
return !process.outText.contains(" LISTENING ");
|
||||
}catch(_){
|
||||
return ServerSocket.bind("127.0.0.1", 3551)
|
||||
.then((socket) => socket.close())
|
||||
.then((_) => true)
|
||||
.onError((error, _) => false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _showServerDownloadInfo(BuildContext context) async {
|
||||
var nodeFuture = compute(downloadServer, 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));
|
||||
}
|
||||
Future<void> freeLawinPort() async {
|
||||
var releaseBat = await loadBinary("release.bat", false);
|
||||
await Process.run(releaseBat.path, []);
|
||||
}
|
||||
|
||||
if(snapshot.hasData){
|
||||
return const SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("The download was completed successfully!",
|
||||
textAlign: TextAlign.center)
|
||||
);
|
||||
}
|
||||
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"
|
||||
];
|
||||
|
||||
return InfoLabel(
|
||||
label: "Downloading lawin server...",
|
||||
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'),
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
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();
|
||||
if (!free) {
|
||||
return ServerResult(
|
||||
type: ServerResultType.portTakenError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(type == ServerType.embedded && !serverLocation.existsSync()){
|
||||
return ServerResult(
|
||||
type: ServerResultType.serverDownloadRequiredError
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
uri: ping(host, port),
|
||||
type: ServerResultType.canStart
|
||||
);
|
||||
|
||||
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<Process> startEmbeddedServer() async {
|
||||
return await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
|
||||
}
|
||||
|
||||
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(
|
||||
label: "Downloading node...",
|
||||
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<HttpServer> startRemoteServer(Uri uri) async {
|
||||
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
}
|
||||
|
||||
Future<bool> _showAlreadyBindPortWarning(BuildContext context) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
content: const Text(
|
||||
"Port 3551 is already in use, do you want to kill the associated process?",
|
||||
textAlign: TextAlign.center),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
FilledButton(
|
||||
child: const Text('Kill'),
|
||||
onPressed: () => Navigator.of(context).pop(true)),
|
||||
],
|
||||
)) ??
|
||||
false;
|
||||
class ServerResult {
|
||||
final Future<Uri?>? uri;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
final ServerResultType type;
|
||||
|
||||
ServerResult({this.uri, this.error, this.stackTrace, required this.type});
|
||||
}
|
||||
|
||||
enum ServerResultType {
|
||||
missingHostError,
|
||||
missingPortError,
|
||||
illegalPortError,
|
||||
cannotPingServer,
|
||||
portTakenError,
|
||||
serverDownloadRequiredError,
|
||||
canStart,
|
||||
started,
|
||||
unknownError,
|
||||
stopped
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -3,10 +3,10 @@ import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
|
||||
class DeploymentSelector extends StatelessWidget {
|
||||
class GameTypeSelector extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
DeploymentSelector({Key? key}) : super(key: key);
|
||||
GameTypeSelector({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_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/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/patcher.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:path/path.dart' as path;
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import '../model/server_type.dart';
|
||||
import '../util/server_standalone.dart';
|
||||
import '../../controller/settings_controller.dart';
|
||||
import '../../dialog/snackbar.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
const LaunchButton(
|
||||
@@ -63,15 +67,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onPressed() async {
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
showSnackbar(
|
||||
context, const Snackbar(content: Text("Please type a username")));
|
||||
showMessage("Missing in-game username");
|
||||
_updateServerState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gameController.selectedVersionObs.value == null) {
|
||||
showSnackbar(
|
||||
context, const Snackbar(content: Text("Please select a version")));
|
||||
showMessage("No version is selected");
|
||||
_updateServerState(false);
|
||||
return;
|
||||
}
|
||||
@@ -90,17 +92,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
Win32Process(_gameController.launcherProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
if (version.eacExecutable != null) {
|
||||
_gameController.eacProcess = await Process.start(version.eacExecutable!.path, []);
|
||||
Win32Process(_gameController.eacProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
if(hosting){
|
||||
await patchExe(version.executable!);
|
||||
await patch(version.executable!);
|
||||
}
|
||||
|
||||
await _startServerIfNecessary();
|
||||
if(!_serverController.started.value){
|
||||
if(!mounted){
|
||||
_onStop();
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _serverController.changeStateInteractive(true);
|
||||
if(!result){
|
||||
_onStop();
|
||||
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 {
|
||||
if (_gameController.started.value == value) {
|
||||
return;
|
||||
@@ -197,106 +163,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
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 {
|
||||
if(!mounted){
|
||||
return;
|
||||
@@ -304,26 +170,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
var result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
content: InfoLabel(
|
||||
label: "Launching headless reboot server...",
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar()
|
||||
)
|
||||
),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
_onStop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
)
|
||||
)
|
||||
],
|
||||
builder: (context) => InfoDialog.ofOnly(
|
||||
text: "Launching headless reboot server...",
|
||||
button: DialogButton(
|
||||
type: ButtonType.only,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(false);
|
||||
_onStop();
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -335,10 +190,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
void _onGameOutput(String line) {
|
||||
if(kDebugMode){
|
||||
print(line);
|
||||
}
|
||||
|
||||
if(_logFile != null){
|
||||
_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")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_showBrokenServerWarning();
|
||||
showBrokenError();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("HTTP 400 response from ")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_showUnsupportedHeadless();
|
||||
showUnsupportedHeadless();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_showTokenError();
|
||||
showTokenError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) {
|
||||
_injectOrShowError(Injectable.console);
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Region") && _gameController.type.value != GameType.client){
|
||||
_injectOrShowError(Injectable.reboot)
|
||||
.then((value) => _closeDialogIfOpen(true));
|
||||
if(line.contains("Region")){
|
||||
if(_gameController.type.value == GameType.client){
|
||||
_injectOrShowError(Injectable.console);
|
||||
}else {
|
||||
_injectOrShowError(Injectable.reboot)
|
||||
.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);
|
||||
}
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("Cannot launch fortnite: $exception",
|
||||
textAlign: TextAlign.center)),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Button(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Close'),
|
||||
))
|
||||
],
|
||||
));
|
||||
builder: (context) => ErrorDialog(
|
||||
exception: exception,
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _onStop() {
|
||||
@@ -422,12 +257,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(!dllPath.existsSync()) {
|
||||
await _downloadMissingDll(injectable);
|
||||
if(!dllPath.existsSync()){
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_showMissingDllError(path.basename(dllPath.path));
|
||||
_onStop();
|
||||
});
|
||||
_onDllFail(dllPath);
|
||||
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){
|
||||
switch(injectable){
|
||||
case Injectable.reboot:
|
||||
@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/widget/smart_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
|
||||
|
||||
class UsernameBox extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
@@ -5,16 +5,15 @@ import 'package:flutter/gestures.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/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/scan_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/smart_check_box.dart';
|
||||
import 'package:reboot_launcher/src/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
final bool enableScanner;
|
||||
import '../../dialog/add_server_version.dart';
|
||||
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
|
||||
State<VersionSelector> createState() => _VersionSelectorState();
|
||||
@@ -45,17 +44,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
const SizedBox(
|
||||
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(
|
||||
message: "Download a fortnite build from the archive",
|
||||
child: Button(
|
||||
@@ -125,33 +113,20 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
builder: (context) => AddLocalVersion());
|
||||
}
|
||||
|
||||
void _openScanLocalVersionDialog(BuildContext context) async {
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => const ScanLocalVersion());
|
||||
}
|
||||
|
||||
Future<void> _openMenu(
|
||||
BuildContext context, FortniteVersion version, Offset offset) async {
|
||||
var result = await showMenu<int?>(
|
||||
var result = await showMenu<ContextualOption>(
|
||||
context: context,
|
||||
offset: offset,
|
||||
builder: (context) => MenuFlyout(
|
||||
items: [
|
||||
MenuFlyoutItem(
|
||||
text: const Text('Open in explorer'),
|
||||
onPressed: () => Navigator.of(context).pop(0)
|
||||
),
|
||||
MenuFlyoutItem(
|
||||
text: const Text('Delete'),
|
||||
onPressed: () => Navigator.of(context).pop(1)
|
||||
),
|
||||
],
|
||||
items: ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.toList()
|
||||
)
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
case 0:
|
||||
case ContextualOption.openExplorer:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +136,21 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
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){
|
||||
return;
|
||||
}
|
||||
@@ -183,9 +172,19 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createOption(BuildContext context, ContextualOption entry) {
|
||||
return MenuFlyoutItem(
|
||||
text: Text(entry.name),
|
||||
onPressed: () => Navigator.of(context).pop(entry)
|
||||
);
|
||||
}
|
||||
|
||||
bool _onExplorerError() {
|
||||
showSnackbar(
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:fluent_ui/fluent_ui.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 {
|
||||
final String label;
|
||||
@@ -65,7 +66,7 @@ class _FileSelectorState extends State<FileSelector> {
|
||||
|
||||
void _onPressed() {
|
||||
if(_selecting){
|
||||
showSnackbar(context, const Snackbar(content: Text("Folder selector is already opened")));
|
||||
showMessage("Folder selector is already opened");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/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 {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/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 {
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.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>();
|
||||
|
||||
ServerButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
@@ -18,7 +23,7 @@ class ServerButton extends StatelessWidget {
|
||||
child: Obx(() => Tooltip(
|
||||
message: _helpMessage,
|
||||
child: Button(
|
||||
onPressed: () => _onPressed(context),
|
||||
onPressed: () async => _serverController.changeStateInteractive(false),
|
||||
child: Text(_buttonText())),
|
||||
)),
|
||||
),
|
||||
@@ -55,42 +60,4 @@ class ServerButton extends StatelessWidget {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
|
||||
class LocalServerSwitch extends StatelessWidget {
|
||||
class ServerTypeSelector extends StatelessWidget {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
LocalServerSwitch({Key? key}) : super(key: key);
|
||||
ServerTypeSelector({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Reference in New Issue
Block a user