mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
decoupled business logic from ui
This commit is contained in:
Binary file not shown.
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/fortnite_version.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/injector.dart';
|
import 'package:reboot_launcher/src/util/injector.dart';
|
||||||
import 'package:reboot_launcher/src/util/node.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||||
import 'package:reboot_launcher/src/util/server_standalone.dart';
|
import 'package:reboot_launcher/src/util/server.dart';
|
||||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
@@ -23,8 +22,8 @@ import 'package:shelf/shelf_io.dart' as shelf_io;
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
// Needed because binaries can't be loaded in any other way
|
// Needed because binaries can't be loaded in any other way
|
||||||
const String _craniumDownload = "https://filebin.net/ybn0gme7dqjr4zup/cranium.dll";
|
const String _craniumDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848323825675/cranium.dll";
|
||||||
const String _consoleDownload = "https://filebin.net/ybn0gme7dqjr4zup/console.dll";
|
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
|
||||||
|
|
||||||
Process? _gameProcess;
|
Process? _gameProcess;
|
||||||
Process? _eacProcess;
|
Process? _eacProcess;
|
||||||
@@ -121,13 +120,12 @@ Future<void> handleCLI(List<String> args) async {
|
|||||||
|
|
||||||
stdout.writeln("Launching game(type: ${type.name})...");
|
stdout.writeln("Launching game(type: ${type.name})...");
|
||||||
await _startLauncherProcess(dummyVersion);
|
await _startLauncherProcess(dummyVersion);
|
||||||
await _startEacProcess(dummyVersion);
|
|
||||||
if (result["type"] == "headless_server") {
|
if (result["type"] == "headless_server") {
|
||||||
if(dummyVersion.executable == null){
|
if(dummyVersion.executable == null){
|
||||||
throw Exception("Missing game executable at: ${dummyVersion.location.path}");
|
throw Exception("Missing game executable at: ${dummyVersion.location.path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await patchExe(dummyVersion.executable!);
|
await patch(dummyVersion.executable!);
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverType = _getServerType(result);
|
var serverType = _getServerType(result);
|
||||||
@@ -249,15 +247,6 @@ void _onClose() {
|
|||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.eacExecutable == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
|
||||||
Win32Process(_eacProcess!.pid).suspend();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
||||||
if (dummyVersion.launcher == null) {
|
if (dummyVersion.launcher == null) {
|
||||||
return;
|
return;
|
||||||
@@ -296,33 +285,7 @@ Future<bool> _startServerIfNeeded(String? host, String? port, ServerType type) a
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _changeEmbeddedServerState() async {
|
Future<bool> _changeEmbeddedServerState() async {
|
||||||
var node = await hasNode();
|
|
||||||
if(!node) {
|
|
||||||
throw Exception("Missing node, cannot start embedded server");
|
|
||||||
}
|
|
||||||
|
|
||||||
var free = await isLawinPortFree();
|
|
||||||
if(!free){
|
|
||||||
stdout.writeln("Server is already running on port 3551");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!serverLocation.existsSync()) {
|
|
||||||
await downloadServer(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverRunner = File("${serverLocation.path}/start.bat");
|
|
||||||
if (!(await serverRunner.exists())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeModules = Directory("${serverLocation.path}/node_modules");
|
|
||||||
if (!(await nodeModules.exists())) {
|
|
||||||
await Process.run("${serverLocation.path}/install_packages.bat", [],
|
|
||||||
workingDirectory: serverLocation.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/page/home_page.dart';
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
|
final GlobalKey appKey = GlobalKey();
|
||||||
|
|
||||||
void main(List<String> args) async {
|
void main(List<String> args) async {
|
||||||
await Directory(safeBinariesDirectory)
|
await Directory(safeBinariesDirectory)
|
||||||
.create(recursive: true);
|
.create(recursive: true);
|
||||||
@@ -55,8 +56,6 @@ class RebootApplication extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RebootApplicationState extends State<RebootApplication> {
|
class _RebootApplicationState extends State<RebootApplication> {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = SystemTheme.accentColor.accent.toAccentColor();
|
final color = SystemTheme.accentColor.accent.toAccentColor();
|
||||||
@@ -67,7 +66,7 @@ class _RebootApplicationState extends State<RebootApplication> {
|
|||||||
color: color,
|
color: color,
|
||||||
darkTheme: _createTheme(Brightness.dark),
|
darkTheme: _createTheme(Brightness.dark),
|
||||||
theme: _createTheme(Brightness.light),
|
theme: _createTheme(Brightness.light),
|
||||||
home: const HomePage(),
|
home: HomePage(key: appKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,4 +89,8 @@ class GameController extends GetxController {
|
|||||||
_selectedVersion(version);
|
_selectedVersion(version);
|
||||||
_storage.write("version", version?.name);
|
_storage.write("version", version?.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void rename(FortniteVersion version, String result) {
|
||||||
|
versions.update((val) => version.name = result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
import 'package:reboot_launcher/src/util/server.dart';
|
||||||
|
|
||||||
import '../model/server_type.dart';
|
import '../model/server_type.dart';
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ class ServerController extends GetxController {
|
|||||||
late final Rx<ServerType> type;
|
late final Rx<ServerType> type;
|
||||||
late final RxBool warning;
|
late final RxBool warning;
|
||||||
late RxBool started;
|
late RxBool started;
|
||||||
|
Process? embeddedServer;
|
||||||
HttpServer? reverseProxy;
|
HttpServer? reverseProxy;
|
||||||
|
|
||||||
ServerController() {
|
ServerController() {
|
||||||
@@ -39,9 +40,7 @@ class ServerController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBinary("release.bat", false)
|
stop();
|
||||||
.then((value) => Process.run(value.path, []))
|
|
||||||
.then((value) => started(false));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
host = TextEditingController(text: _readHost());
|
host = TextEditingController(text: _readHost());
|
||||||
@@ -65,4 +64,69 @@ class ServerController extends GetxController {
|
|||||||
String _readPort() {
|
String _readPort() {
|
||||||
return _storage.read("${type.value.id}_port") ?? _serverPort;
|
return _storage.read("${type.value.id}_port") ?? _serverPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ServerResult> start() async {
|
||||||
|
var result = await checkServerPreconditions(host.text, port.text, type.value);
|
||||||
|
if(result.type != ServerResultType.canStart){
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
switch(type()){
|
||||||
|
case ServerType.embedded:
|
||||||
|
embeddedServer = await startEmbeddedServer();
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
var uriResult = await result.uri!;
|
||||||
|
if(uriResult == null){
|
||||||
|
return ServerResult(
|
||||||
|
type: ServerResultType.cannotPingServer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reverseProxy = await startRemoteServer(uriResult);
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}catch(error, stackTrace){
|
||||||
|
return ServerResult(
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
type: ServerResultType.unknownError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var myself = await pingSelf();
|
||||||
|
if(myself == null){
|
||||||
|
return ServerResult(
|
||||||
|
type: ServerResultType.cannotPingServer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
started(true);
|
||||||
|
return ServerResult(
|
||||||
|
type: ServerResultType.started
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stop() async {
|
||||||
|
started(false);
|
||||||
|
try{
|
||||||
|
switch(type()){
|
||||||
|
case ServerType.embedded:
|
||||||
|
await freeLawinPort();
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
await reverseProxy?.close(force: true);
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}catch(_){
|
||||||
|
started(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
class SettingsController extends GetxController {
|
class SettingsController extends GetxController {
|
||||||
|
|||||||
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");
|
return findExecutable(location, "FortniteLauncher.exe");
|
||||||
}
|
}
|
||||||
|
|
||||||
File? get eacExecutable {
|
|
||||||
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'name': name,
|
'name': name,
|
||||||
'location': location.path,
|
'location': location.path,
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import 'package:reboot_launcher/src/page/settings_page.dart';
|
|||||||
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/server_page.dart';
|
import 'package:reboot_launcher/src/page/server_page.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/widget/window_border.dart';
|
import 'package:reboot_launcher/src/widget/os/window_border.dart';
|
||||||
import 'package:reboot_launcher/src/widget/window_buttons.dart';
|
import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import 'info_page.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -18,9 +20,9 @@ class HomePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WindowListener {
|
class _HomePageState extends State<HomePage> with WindowListener {
|
||||||
static const double _headerSize = 48.0;
|
static const double _headerSize = 48.0;
|
||||||
static const double _sectionSize = 97.0;
|
static const double _sectionSize = 94.0;
|
||||||
static const int _headerButtonCount = 3;
|
static const int _headerButtonCount = 3;
|
||||||
static const int _sectionButtonCount = 3;
|
static const int _sectionButtonCount = 4;
|
||||||
|
|
||||||
bool _focused = true;
|
bool _focused = true;
|
||||||
bool _shouldMaximize = false;
|
bool _shouldMaximize = false;
|
||||||
@@ -78,6 +80,12 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
title: const Text("Settings"),
|
title: const Text("Settings"),
|
||||||
icon: const Icon(FluentIcons.settings),
|
icon: const Icon(FluentIcons.settings),
|
||||||
body: SettingsPage()
|
body: SettingsPage()
|
||||||
|
),
|
||||||
|
|
||||||
|
PaneItem(
|
||||||
|
title: const Text("Info"),
|
||||||
|
icon: const Icon(FluentIcons.info),
|
||||||
|
body: const InfoPage()
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
@@ -104,8 +112,8 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
Padding _createGestureHandler() {
|
Padding _createGestureHandler() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: _sectionSize * _headerButtonCount,
|
left: _sectionSize * _sectionButtonCount,
|
||||||
right: _headerSize * _sectionButtonCount,
|
right: _headerSize * _headerButtonCount,
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: _headerSize,
|
height: _headerSize,
|
||||||
|
|||||||
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/build_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/widget/host_checkbox.dart';
|
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/launch_button.dart';
|
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/username_box.dart';
|
import 'package:reboot_launcher/src/widget/home/username_box.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../controller/settings_controller.dart';
|
import '../controller/settings_controller.dart';
|
||||||
import '../util/binary.dart';
|
|
||||||
import '../util/reboot.dart';
|
import '../util/reboot.dart';
|
||||||
import '../widget/warning_info.dart';
|
import '../widget/shared/warning_info.dart';
|
||||||
|
|
||||||
class LauncherPage extends StatefulWidget {
|
class LauncherPage extends StatefulWidget {
|
||||||
const LauncherPage(
|
const LauncherPage(
|
||||||
@@ -59,6 +58,7 @@ class _LauncherPageState extends State<LauncherPage> {
|
|||||||
var errorFile = await loadBinary("error.txt", true);
|
var errorFile = await loadBinary("error.txt", true);
|
||||||
errorFile.writeAsString(
|
errorFile.writeAsString(
|
||||||
"Error: $error\nStacktrace: $stackTrace", mode: FileMode.write);
|
"Error: $error\nStacktrace: $stackTrace", mode: FileMode.write);
|
||||||
|
throw Exception("Cannot update reboot.dll");
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCancelWarning() {
|
void _onCancelWarning() {
|
||||||
@@ -104,7 +104,7 @@ class _LauncherPageState extends State<LauncherPage> {
|
|||||||
_createUpdateError(snapshot),
|
_createUpdateError(snapshot),
|
||||||
UsernameBox(),
|
UsernameBox(),
|
||||||
const VersionSelector(),
|
const VersionSelector(),
|
||||||
DeploymentSelector(),
|
GameTypeSelector(),
|
||||||
const LaunchButton()
|
const LaunchButton()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -118,8 +118,7 @@ class _LauncherPageState extends State<LauncherPage> {
|
|||||||
text: "Cannot update Reboot DLL",
|
text: "Cannot update Reboot DLL",
|
||||||
icon: FluentIcons.info,
|
icon: FluentIcons.info,
|
||||||
severity: InfoBarSeverity.warning,
|
severity: InfoBarSeverity.warning,
|
||||||
onPressed: () => loadBinary("error.txt", true)
|
onPressed: () => loadBinary("error.txt", true).then((file) => launchUrl(file.uri))
|
||||||
.then((file) => launchUrl(file.uri))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/widget/host_input.dart';
|
import 'package:reboot_launcher/src/widget/server/host_input.dart';
|
||||||
import 'package:reboot_launcher/src/widget/local_server_switch.dart';
|
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/port_input.dart';
|
import 'package:reboot_launcher/src/widget/server/port_input.dart';
|
||||||
import 'package:reboot_launcher/src/widget/server_button.dart';
|
import 'package:reboot_launcher/src/widget/server/server_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/warning_info.dart';
|
import 'package:reboot_launcher/src/widget/shared/warning_info.dart';
|
||||||
|
|
||||||
class ServerPage extends StatelessWidget {
|
class ServerPage extends StatelessWidget {
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
final ServerController _serverController = Get.find<ServerController>();
|
||||||
@@ -28,8 +28,8 @@ class ServerPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
HostInput(),
|
HostInput(),
|
||||||
PortInput(),
|
PortInput(),
|
||||||
LocalServerSwitch(),
|
ServerTypeSelector(),
|
||||||
ServerButton()
|
const ServerButton()
|
||||||
]
|
]
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
|
||||||
import 'package:reboot_launcher/src/widget/smart_switch.dart';
|
|
||||||
|
import '../util/checks.dart';
|
||||||
|
import '../widget/os/file_selector.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
@@ -16,9 +16,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Stack(
|
child: Column(
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -29,7 +27,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
windowTitle: "Select a dll",
|
windowTitle: "Select a dll",
|
||||||
folder: false,
|
folder: false,
|
||||||
extension: "dll",
|
extension: "dll",
|
||||||
validator: _checkDll,
|
validator: checkDll,
|
||||||
validatorMode: AutovalidateMode.always
|
validatorMode: AutovalidateMode.always
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
windowTitle: "Select a dll",
|
windowTitle: "Select a dll",
|
||||||
folder: false,
|
folder: false,
|
||||||
extension: "dll",
|
extension: "dll",
|
||||||
validator: _checkDll,
|
validator: checkDll,
|
||||||
validatorMode: AutovalidateMode.always
|
validatorMode: AutovalidateMode.always
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -51,7 +49,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
windowTitle: "Select a dll",
|
windowTitle: "Select a dll",
|
||||||
folder: false,
|
folder: false,
|
||||||
extension: "dll",
|
extension: "dll",
|
||||||
validator: _checkDll,
|
validator: checkDll,
|
||||||
validatorMode: AutovalidateMode.always
|
validatorMode: AutovalidateMode.always
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -59,31 +57,8 @@ class SettingsPage extends StatelessWidget {
|
|||||||
value: _settingsController.autoUpdate,
|
value: _settingsController.autoUpdate,
|
||||||
label: "Update DLLs"
|
label: "Update DLLs"
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
),
|
|
||||||
|
|
||||||
const Align(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: Text("Version 3.13${kDebugMode ? '-DEBUG' : ''}")
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _checkDll(String? text) {
|
|
||||||
if (text == null || text.isEmpty) {
|
|
||||||
return "Empty dll path";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File(text).existsSync()) {
|
|
||||||
return "This dll doesn't exist";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text.endsWith(".dll")) {
|
|
||||||
return "This file is not a dll";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:http/http.dart' as http;
|
||||||
import 'package:process_run/shell.dart';
|
import 'package:process_run/shell.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
||||||
|
|
||||||
|
import 'os.dart';
|
||||||
|
|
||||||
const _userAgent =
|
const _userAgent =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36";
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
@@ -80,13 +81,13 @@ Future<List<FortniteBuild>> _fetchManifests() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Process> downloadManifestBuild(
|
Future<Process> downloadManifestBuild(
|
||||||
String manifestUrl, String destination, Function(double) onProgress) async {
|
String manifestUrl, String destination, Function(double, String) onProgress) async {
|
||||||
var buildExe = await loadBinary("build.exe", false);
|
var buildExe = await loadBinary("build.exe", false);
|
||||||
var process = await Process.start(buildExe.path, [manifestUrl, destination]);
|
var process = await Process.start(buildExe.path, [manifestUrl, destination]);
|
||||||
|
|
||||||
process.errLines
|
process.errLines
|
||||||
.where((message) => message.contains("%"))
|
.where((message) => message.contains("%"))
|
||||||
.forEach((message) => onProgress(double.parse(message.split("%")[0])));
|
.forEach((message) => onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1)));
|
||||||
|
|
||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|||||||
56
lib/src/util/checks.dart
Normal file
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;
|
return result.files.first.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Directory>> scanInstallations(String input) => Directory(input)
|
Future<File> loadBinary(String binary, bool safe) async{
|
||||||
.list(recursive: true)
|
var safeBinary = File("$safeBinariesDirectory\\$binary");
|
||||||
.handleError((_) {}, test: (e) => e is FileSystemException)
|
if(await safeBinary.exists()){
|
||||||
.where((element) => path.basename(element.path) == "FortniteClient-Win64-Shipping.exe")
|
return safeBinary;
|
||||||
.map((element) => findContainer(File(element.path)))
|
|
||||||
.where((element) => element != null)
|
|
||||||
.map((element) => element!)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
Directory? findContainer(File file){
|
|
||||||
var last = file.parent;
|
|
||||||
for(var x = 0; x < 5; x++){
|
|
||||||
var name = path.basename(last.path);
|
|
||||||
if(name != "FortniteGame" || name == "Fortnite"){
|
|
||||||
last = last.parent;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return last.parent;
|
var internal = _locateInternalBinary(binary);
|
||||||
|
if(!safe){
|
||||||
|
return internal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if(await internal.exists()){
|
||||||
|
await internal.copy(safeBinary.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return safeBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
File _locateInternalBinary(String binary){
|
||||||
|
return File("$internalBinariesDirectory\\$binary");
|
||||||
|
}
|
||||||
|
|
||||||
|
String get internalBinariesDirectory =>
|
||||||
|
"${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries";
|
||||||
|
|
||||||
|
Directory get tempDirectory =>
|
||||||
|
Directory("${Platform.environment["Temp"]}");
|
||||||
|
|
||||||
|
String get safeBinariesDirectory =>
|
||||||
|
"${Platform.environment["UserProfile"]}\\.reboot_launcher";
|
||||||
@@ -9,7 +9,7 @@ final Uint8List _patched = Uint8List.fromList([
|
|||||||
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
|
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Future<bool> patchExe(File file) async {
|
Future<bool> patch(File file) async {
|
||||||
if(_original.length != _patched.length){
|
if(_original.length != _patched.length){
|
||||||
throw Exception("Cannot mutate length of binary file");
|
throw Exception("Cannot mutate length of binary file");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:archive/archive_io.dart';
|
|||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
|
||||||
const _rebootUrl =
|
const _rebootUrl =
|
||||||
"https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip";
|
"https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip";
|
||||||
|
|||||||
@@ -1,394 +1,162 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:process_run/shell.dart';
|
import 'package:process_run/shell.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/node.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/server_standalone.dart';
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
||||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||||
|
import 'package:shelf/shelf_io.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
Future<bool> checkLocalServer(BuildContext context, String host, String port, bool closeAutomatically) async {
|
final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin\\Lawin.exe");
|
||||||
host = host.trim();
|
const String _serverUrl =
|
||||||
if(host.isEmpty){
|
"https://cdn.discordapp.com/attachments/1026121175878881290/1031230792069820487/LawinServer.zip";
|
||||||
showSnackbar(
|
|
||||||
context, const Snackbar(content: Text("Missing host name")));
|
Future<bool> downloadServer(ignored) async {
|
||||||
return false;
|
var response = await http.get(Uri.parse(_serverUrl));
|
||||||
|
var tempZip = File("${Platform.environment["Temp"]}/lawin.zip");
|
||||||
|
await tempZip.writeAsBytes(response.bodyBytes);
|
||||||
|
await extractFileToDisk(tempZip.path, serverLocation.parent.path);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
port = port.trim();
|
Future<bool> isLawinPortFree() async {
|
||||||
if(port.isEmpty){
|
|
||||||
showSnackbar(
|
|
||||||
context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(int.tryParse(port) == null){
|
|
||||||
showSnackbar(
|
|
||||||
context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _showCheck(context, host, port, false, closeAutomatically) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, String port, bool closeAutomatically, HttpServer? server) async {
|
|
||||||
if(server != null){
|
|
||||||
try {
|
try {
|
||||||
server.close(force: true);
|
var portBat = await loadBinary("port.bat", true);
|
||||||
return null;
|
var process = await Process.run(portBat.path, []);
|
||||||
}catch(error){
|
return !process.outText.contains(" LISTENING ");
|
||||||
_showStopProxyError(context, error);
|
}catch(_){
|
||||||
return server;
|
return ServerSocket.bind("127.0.0.1", 3551)
|
||||||
|
.then((socket) => socket.close())
|
||||||
|
.then((_) => true)
|
||||||
|
.onError((error, _) => false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
host = host.trim();
|
Future<void> freeLawinPort() async {
|
||||||
if(host.isEmpty){
|
|
||||||
showSnackbar(
|
|
||||||
context, const Snackbar(content: Text("Missing host name")));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
port = port.trim();
|
|
||||||
if(port.isEmpty){
|
|
||||||
showSnackbar(
|
|
||||||
context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center)));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(int.tryParse(port) == null){
|
|
||||||
showSnackbar(
|
|
||||||
context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center)));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try{
|
|
||||||
var uri = await _showCheck(context, host, port, true, closeAutomatically);
|
|
||||||
if(uri == null){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
|
|
||||||
}catch(error){
|
|
||||||
_showStartProxyError(context, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> _showCheck(BuildContext context, String host, String port, bool remote, bool closeAutomatically) async {
|
|
||||||
var future = ping(host, port);
|
|
||||||
Uri? result;
|
|
||||||
return await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: FutureBuilder<Uri?>(
|
|
||||||
future: future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if(snapshot.hasError){
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("Cannot ping ${remote ? "remote" : "local"} server: ${snapshot.error}" , textAlign: TextAlign.center)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(snapshot.connectionState == ConnectionState.done && !snapshot.hasData){
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text(
|
|
||||||
"The ${remote ? "remote" : "local"} server doesn't work correctly ${remote ? "or the IP and/or the port are incorrect" : ""}",
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = snapshot.data;
|
|
||||||
if(snapshot.hasData){
|
|
||||||
if(remote || closeAutomatically) {
|
|
||||||
Navigator.of(context).pop(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text(
|
|
||||||
"The server works correctly",
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return InfoLabel(
|
|
||||||
label: "Pinging ${remote ? "remote" : "local"} lawin server...",
|
|
||||||
child: const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ProgressBar()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(result),
|
|
||||||
child: const Text('Close'),
|
|
||||||
))
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showStartProxyError(BuildContext context, Object error) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("Cannot create the reverse proxy: $error", textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showStopProxyError(BuildContext context, Object error) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("Cannot kill the reverse proxy: $error", textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> changeEmbeddedServerState(BuildContext context, bool running) async {
|
|
||||||
if (running) {
|
|
||||||
var releaseBat = await loadBinary("release.bat", false);
|
var releaseBat = await loadBinary("release.bat", false);
|
||||||
await Process.run(releaseBat.path, []);
|
await Process.run(releaseBat.path, []);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> createRebootArgs(String username, bool headless) {
|
||||||
|
var args = [
|
||||||
|
"-skippatchcheck",
|
||||||
|
"-epicapp=Fortnite",
|
||||||
|
"-epicenv=Prod",
|
||||||
|
"-epiclocale=en-us",
|
||||||
|
"-epicportal",
|
||||||
|
"-noeac",
|
||||||
|
"-fromfl=be",
|
||||||
|
"-fltoken=7ce411021b27b4343a44fdg8",
|
||||||
|
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||||
|
"-AUTH_LOGIN=$username@projectreboot.dev",
|
||||||
|
"-AUTH_PASSWORD=Rebooted",
|
||||||
|
"-AUTH_TYPE=epic"
|
||||||
|
];
|
||||||
|
|
||||||
|
if(headless){
|
||||||
|
args.addAll(["-nullrhi", "-nosplash", "-nosound"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uri?> pingSelf() async => ping("127.0.0.1", "3551");
|
||||||
|
|
||||||
|
Future<Uri?> ping(String host, String port, [bool https=false]) async {
|
||||||
|
var hostName = _getHostName(host);
|
||||||
|
var declaredScheme = _getScheme(host);
|
||||||
|
try{
|
||||||
|
var uri = Uri(
|
||||||
|
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||||
|
host: hostName,
|
||||||
|
port: int.parse(port)
|
||||||
|
);
|
||||||
|
var client = HttpClient()
|
||||||
|
..connectionTimeout = const Duration(seconds: 5);
|
||||||
|
var request = await client.getUrl(uri);
|
||||||
|
var response = await request.close();
|
||||||
|
var body = utf8.decode(await response.single);
|
||||||
|
return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null;
|
||||||
|
}catch(_){
|
||||||
|
return https || declaredScheme != null ? null : await ping(host, port, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||||
|
|
||||||
|
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||||
|
|
||||||
|
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type) async {
|
||||||
|
host = host.trim();
|
||||||
|
if(host.isEmpty){
|
||||||
|
return ServerResult(
|
||||||
|
type: ServerResultType.missingHostError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
port = port.trim();
|
||||||
|
if(port.isEmpty){
|
||||||
|
return ServerResult(
|
||||||
|
type: ServerResultType.missingPortError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(int.tryParse(port) == null){
|
||||||
|
return ServerResult(
|
||||||
|
type: ServerResultType.illegalPortError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type == ServerType.embedded || type == ServerType.remote){
|
||||||
var free = await isLawinPortFree();
|
var free = await isLawinPortFree();
|
||||||
if (!free) {
|
if (!free) {
|
||||||
var shouldKill = await _showAlreadyBindPortWarning(context);
|
return ServerResult(
|
||||||
if (!shouldKill) {
|
type: ServerResultType.portTakenError
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var releaseBat = await loadBinary("release.bat", false);
|
|
||||||
await Process.run(releaseBat.path, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
var node = await hasNode();
|
|
||||||
var useLocalNode = false;
|
|
||||||
if(!node) {
|
|
||||||
useLocalNode = true;
|
|
||||||
if(!embeddedNode.existsSync()){
|
|
||||||
var result = await _showNodeDownloadInfo(context);
|
|
||||||
if(!result) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!serverLocation.existsSync()) {
|
|
||||||
var result = await _showServerDownloadInfo(context);
|
|
||||||
if(!result){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverRunner = File("${serverLocation.path}/start.bat");
|
|
||||||
if (!serverRunner.existsSync()) {
|
|
||||||
_showEmbeddedError(context, "missing file ${serverRunner.path}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeModules = Directory("${serverLocation.path}/node_modules");
|
|
||||||
if (!nodeModules.existsSync()) {
|
|
||||||
await Process.run("${serverLocation.path}/install_packages.bat", [],
|
|
||||||
workingDirectory: serverLocation.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var logFile = await loadBinary("server.txt", true);
|
|
||||||
if(logFile.existsSync()){
|
|
||||||
logFile.deleteSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
var process = await Process.start(
|
|
||||||
!useLocalNode ? "node" : '"${embeddedNode.path}"',
|
|
||||||
["index.js"],
|
|
||||||
workingDirectory: serverLocation.path
|
|
||||||
);
|
);
|
||||||
process.outLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append));
|
|
||||||
process.errLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append));
|
|
||||||
return true;
|
|
||||||
}catch(exception){
|
|
||||||
_showEmbeddedError(context, exception.toString());
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _showServerDownloadInfo(BuildContext context) async {
|
if(type == ServerType.embedded && !serverLocation.existsSync()){
|
||||||
var nodeFuture = compute(downloadServer, true);
|
return ServerResult(
|
||||||
var result = await showDialog<bool>(
|
type: ServerResultType.serverDownloadRequiredError
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: FutureBuilder(
|
|
||||||
future: nodeFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if(snapshot.hasError){
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("An error occurred while downloading: ${snapshot.error}",
|
|
||||||
textAlign: TextAlign.center));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(snapshot.hasData){
|
|
||||||
return const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("The download was completed successfully!",
|
|
||||||
textAlign: TextAlign.center)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InfoLabel(
|
return ServerResult(
|
||||||
label: "Downloading lawin server...",
|
uri: ping(host, port),
|
||||||
child: const SizedBox(
|
type: ServerResultType.canStart
|
||||||
width: double.infinity,
|
|
||||||
child: ProgressBar()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
FutureBuilder(
|
|
||||||
future: nodeFuture,
|
|
||||||
builder: (builder, snapshot) => SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError),
|
|
||||||
child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return result != null && result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showEmbeddedError(BuildContext context, String error) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text(
|
|
||||||
"Cannot start server: $error",
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _showNodeDownloadInfo(BuildContext context) async {
|
|
||||||
var nodeFuture = compute(downloadNode, true);
|
|
||||||
var result = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: FutureBuilder(
|
|
||||||
future: nodeFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if(snapshot.hasError){
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("An error occurred while downloading: ${snapshot.error}",
|
|
||||||
textAlign: TextAlign.center));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(snapshot.hasData){
|
|
||||||
return const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("The download was completed successfully!",
|
|
||||||
textAlign: TextAlign.center)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InfoLabel(
|
Future<Process> startEmbeddedServer() async {
|
||||||
label: "Downloading node...",
|
return await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
|
||||||
child: const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ProgressBar()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
FutureBuilder(
|
|
||||||
future: nodeFuture,
|
|
||||||
builder: (builder, snapshot) => SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError),
|
|
||||||
child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return result != null && result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _showAlreadyBindPortWarning(BuildContext context) async {
|
Future<HttpServer> startRemoteServer(Uri uri) async {
|
||||||
return await showDialog<bool>(
|
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||||
context: context,
|
}
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: const Text(
|
class ServerResult {
|
||||||
"Port 3551 is already in use, do you want to kill the associated process?",
|
final Future<Uri?>? uri;
|
||||||
textAlign: TextAlign.center),
|
final Object? error;
|
||||||
actions: [
|
final StackTrace? stackTrace;
|
||||||
Button(
|
final ServerResultType type;
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Close'),
|
ServerResult({this.uri, this.error, this.stackTrace, required this.type});
|
||||||
),
|
}
|
||||||
FilledButton(
|
|
||||||
child: const Text('Kill'),
|
enum ServerResultType {
|
||||||
onPressed: () => Navigator.of(context).pop(true)),
|
missingHostError,
|
||||||
],
|
missingPortError,
|
||||||
)) ??
|
illegalPortError,
|
||||||
false;
|
cannotPingServer,
|
||||||
|
portTakenError,
|
||||||
|
serverDownloadRequiredError,
|
||||||
|
canStart,
|
||||||
|
started,
|
||||||
|
unknownError,
|
||||||
|
stopped
|
||||||
}
|
}
|
||||||
@@ -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/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
|
|
||||||
class DeploymentSelector extends StatelessWidget {
|
class GameTypeSelector extends StatelessWidget {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
|
||||||
DeploymentSelector({Key? key}) : super(key: key);
|
GameTypeSelector({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:process_run/shell.dart';
|
import 'package:process_run/shell.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/game_dialogs.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/binary.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/injector.dart';
|
import 'package:reboot_launcher/src/util/injector.dart';
|
||||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||||
@@ -17,9 +22,8 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import '../controller/settings_controller.dart';
|
import '../../controller/settings_controller.dart';
|
||||||
import '../model/server_type.dart';
|
import '../../dialog/snackbar.dart';
|
||||||
import '../util/server_standalone.dart';
|
|
||||||
|
|
||||||
class LaunchButton extends StatefulWidget {
|
class LaunchButton extends StatefulWidget {
|
||||||
const LaunchButton(
|
const LaunchButton(
|
||||||
@@ -63,15 +67,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
void _onPressed() async {
|
void _onPressed() async {
|
||||||
if (_gameController.username.text.isEmpty) {
|
if (_gameController.username.text.isEmpty) {
|
||||||
showSnackbar(
|
showMessage("Missing in-game username");
|
||||||
context, const Snackbar(content: Text("Please type a username")));
|
|
||||||
_updateServerState(false);
|
_updateServerState(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_gameController.selectedVersionObs.value == null) {
|
if (_gameController.selectedVersionObs.value == null) {
|
||||||
showSnackbar(
|
showMessage("No version is selected");
|
||||||
context, const Snackbar(content: Text("Please select a version")));
|
|
||||||
_updateServerState(false);
|
_updateServerState(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,17 +92,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
Win32Process(_gameController.launcherProcess!.pid).suspend();
|
Win32Process(_gameController.launcherProcess!.pid).suspend();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version.eacExecutable != null) {
|
|
||||||
_gameController.eacProcess = await Process.start(version.eacExecutable!.path, []);
|
|
||||||
Win32Process(_gameController.eacProcess!.pid).suspend();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(hosting){
|
if(hosting){
|
||||||
await patchExe(version.executable!);
|
await patch(version.executable!);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _startServerIfNecessary();
|
if(!mounted){
|
||||||
if(!_serverController.started.value){
|
_onStop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _serverController.changeStateInteractive(true);
|
||||||
|
if(!result){
|
||||||
_onStop();
|
_onStop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,42 +133,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startServerIfNecessary() async {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_serverController.started.value){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(_serverController.type.value){
|
|
||||||
case ServerType.embedded:
|
|
||||||
var result = await changeEmbeddedServerState(context, false);
|
|
||||||
_serverController.started(result);
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
_serverController.reverseProxy = await changeReverseProxyState(
|
|
||||||
context,
|
|
||||||
_serverController.host.text,
|
|
||||||
_serverController.port.text,
|
|
||||||
false,
|
|
||||||
_serverController.reverseProxy
|
|
||||||
);
|
|
||||||
_serverController.started(_serverController.reverseProxy != null);
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
var result = await checkLocalServer(
|
|
||||||
context,
|
|
||||||
_serverController.host.text,
|
|
||||||
_serverController.port.text,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
_serverController.started(result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateServerState(bool value) async {
|
Future<void> _updateServerState(bool value) async {
|
||||||
if (_gameController.started.value == value) {
|
if (_gameController.started.value == value) {
|
||||||
return;
|
return;
|
||||||
@@ -197,106 +163,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
Navigator.of(context).pop(success);
|
Navigator.of(context).pop(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showBrokenServerWarning() async {
|
|
||||||
if(!mounted){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("The lawin server is not working correctly", textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showMissingDllError(String name) async {
|
|
||||||
if(!mounted){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("$name dll is not a valid dll, fix it in the settings tab", textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showTokenError() async {
|
|
||||||
if(!mounted){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("A token error occurred, restart the game and the lawin server, then try again", textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showUnsupportedHeadless() async {
|
|
||||||
if(!mounted){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text("This version of Fortnite doesn't support headless hosting", textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: const Text('Close'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showServerLaunchingWarning() async {
|
Future<void> _showServerLaunchingWarning() async {
|
||||||
if(!mounted){
|
if(!mounted){
|
||||||
return;
|
return;
|
||||||
@@ -304,27 +170,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
var result = await showDialog<bool>(
|
var result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ContentDialog(
|
builder: (context) => InfoDialog.ofOnly(
|
||||||
content: InfoLabel(
|
text: "Launching headless reboot server...",
|
||||||
label: "Launching headless reboot server...",
|
button: DialogButton(
|
||||||
child: const SizedBox(
|
type: ButtonType.only,
|
||||||
width: double.infinity,
|
onTap: () {
|
||||||
child: ProgressBar()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(false);
|
Navigator.of(context).pop(false);
|
||||||
_onStop();
|
_onStop();
|
||||||
},
|
}
|
||||||
child: const Text('Cancel'),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if(result != null && result){
|
if(result != null && result){
|
||||||
@@ -335,10 +190,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onGameOutput(String line) {
|
void _onGameOutput(String line) {
|
||||||
if(kDebugMode){
|
|
||||||
print(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_logFile != null){
|
if(_logFile != null){
|
||||||
_logFile!.writeAsString("$line\n", mode: FileMode.append);
|
_logFile!.writeAsString("$line\n", mode: FileMode.append);
|
||||||
}
|
}
|
||||||
@@ -351,59 +202,43 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){
|
if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){
|
||||||
_fail = true;
|
_fail = true;
|
||||||
_closeDialogIfOpen(false);
|
_closeDialogIfOpen(false);
|
||||||
_showBrokenServerWarning();
|
showBrokenError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(line.contains("HTTP 400 response from ")){
|
if(line.contains("HTTP 400 response from ")){
|
||||||
_fail = true;
|
_fail = true;
|
||||||
_closeDialogIfOpen(false);
|
_closeDialogIfOpen(false);
|
||||||
_showUnsupportedHeadless();
|
showUnsupportedHeadless();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
|
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
|
||||||
_fail = true;
|
_fail = true;
|
||||||
_closeDialogIfOpen(false);
|
_closeDialogIfOpen(false);
|
||||||
_showTokenError();
|
showTokenError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) {
|
if(line.contains("Region")){
|
||||||
|
if(_gameController.type.value == GameType.client){
|
||||||
_injectOrShowError(Injectable.console);
|
_injectOrShowError(Injectable.console);
|
||||||
return;
|
}else {
|
||||||
}
|
|
||||||
|
|
||||||
if(line.contains("Region") && _gameController.type.value != GameType.client){
|
|
||||||
_injectOrShowError(Injectable.reboot)
|
_injectOrShowError(Injectable.reboot)
|
||||||
.then((value) => _closeDialogIfOpen(true));
|
.then((value) => _closeDialogIfOpen(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Object?> _onError(Object exception, StackTrace? stackTrace) async {
|
|
||||||
if (stackTrace != null) {
|
|
||||||
var errorFile = await loadBinary("error.txt", true);
|
|
||||||
errorFile.writeAsString(
|
|
||||||
"Error: $exception\nStacktrace: $stackTrace", mode: FileMode.write);
|
|
||||||
launchUrl(errorFile.uri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Object?> _onError(Object exception, StackTrace? stackTrace) async {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ContentDialog(
|
builder: (context) => ErrorDialog(
|
||||||
content: SizedBox(
|
exception: exception,
|
||||||
width: double.infinity,
|
stackTrace: stackTrace,
|
||||||
child: Text("Cannot launch fortnite: $exception",
|
errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception"
|
||||||
textAlign: TextAlign.center)),
|
)
|
||||||
actions: [
|
);
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: const Text('Close'),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStop() {
|
void _onStop() {
|
||||||
@@ -422,12 +257,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
if(!dllPath.existsSync()) {
|
if(!dllPath.existsSync()) {
|
||||||
await _downloadMissingDll(injectable);
|
await _downloadMissingDll(injectable);
|
||||||
if(!dllPath.existsSync()){
|
if(!dllPath.existsSync()){
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_onDllFail(dllPath);
|
||||||
_fail = true;
|
|
||||||
_closeDialogIfOpen(false);
|
|
||||||
_showMissingDllError(path.basename(dllPath.path));
|
|
||||||
_onStop();
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,6 +275,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onDllFail(File dllPath) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_fail = true;
|
||||||
|
_closeDialogIfOpen(false);
|
||||||
|
showMissingDllError(path.basename(dllPath.path));
|
||||||
|
_onStop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
File _getDllPath(Injectable injectable){
|
File _getDllPath(Injectable injectable){
|
||||||
switch(injectable){
|
switch(injectable){
|
||||||
case Injectable.reboot:
|
case Injectable.reboot:
|
||||||
@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
import 'package:reboot_launcher/src/widget/smart_input.dart';
|
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
|
||||||
|
|
||||||
class UsernameBox extends StatelessWidget {
|
class UsernameBox extends StatelessWidget {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
@@ -5,16 +5,15 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||||
import 'package:reboot_launcher/src/widget/add_local_version.dart';
|
import 'package:reboot_launcher/src/dialog/add_local_version.dart';
|
||||||
import 'package:reboot_launcher/src/widget/add_server_version.dart';
|
import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart';
|
||||||
import 'package:reboot_launcher/src/widget/scan_local_version.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/smart_check_box.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class VersionSelector extends StatefulWidget {
|
import '../../dialog/add_server_version.dart';
|
||||||
final bool enableScanner;
|
import '../../util/checks.dart';
|
||||||
|
|
||||||
const VersionSelector({Key? key, this.enableScanner = false}) : super(key: key);
|
class VersionSelector extends StatefulWidget {
|
||||||
|
const VersionSelector({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<VersionSelector> createState() => _VersionSelectorState();
|
State<VersionSelector> createState() => _VersionSelectorState();
|
||||||
@@ -45,17 +44,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
),
|
),
|
||||||
if(widget.enableScanner)
|
|
||||||
Tooltip(
|
|
||||||
message: "Scan all fortnite builds in a directory",
|
|
||||||
child: Button(
|
|
||||||
child: const Icon(FluentIcons.site_scan),
|
|
||||||
onPressed: () => _openScanLocalVersionDialog(context)),
|
|
||||||
),
|
|
||||||
if(widget.enableScanner)
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Download a fortnite build from the archive",
|
message: "Download a fortnite build from the archive",
|
||||||
child: Button(
|
child: Button(
|
||||||
@@ -125,33 +113,20 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
builder: (context) => AddLocalVersion());
|
builder: (context) => AddLocalVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openScanLocalVersionDialog(BuildContext context) async {
|
|
||||||
await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => const ScanLocalVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openMenu(
|
Future<void> _openMenu(
|
||||||
BuildContext context, FortniteVersion version, Offset offset) async {
|
BuildContext context, FortniteVersion version, Offset offset) async {
|
||||||
var result = await showMenu<int?>(
|
var result = await showMenu<ContextualOption>(
|
||||||
context: context,
|
context: context,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
builder: (context) => MenuFlyout(
|
builder: (context) => MenuFlyout(
|
||||||
items: [
|
items: ContextualOption.values
|
||||||
MenuFlyoutItem(
|
.map((entry) => _createOption(context, entry))
|
||||||
text: const Text('Open in explorer'),
|
.toList()
|
||||||
onPressed: () => Navigator.of(context).pop(0)
|
|
||||||
),
|
|
||||||
MenuFlyoutItem(
|
|
||||||
text: const Text('Delete'),
|
|
||||||
onPressed: () => Navigator.of(context).pop(1)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case 0:
|
case ContextualOption.openExplorer:
|
||||||
if(!mounted){
|
if(!mounted){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,7 +136,21 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
.onError((error, stackTrace) => _onExplorerError());
|
.onError((error, stackTrace) => _onExplorerError());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case ContextualOption.rename:
|
||||||
|
if(!mounted){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
var result = await _openRenameDialog(context, version);
|
||||||
|
if(result == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameController.rename(version, result);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ContextualOption.delete:
|
||||||
if(!mounted){
|
if(!mounted){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,9 +172,19 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case null:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MenuFlyoutItem _createOption(BuildContext context, ContextualOption entry) {
|
||||||
|
return MenuFlyoutItem(
|
||||||
|
text: Text(entry.name),
|
||||||
|
onPressed: () => Navigator.of(context).pop(entry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool _onExplorerError() {
|
bool _onExplorerError() {
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
context,
|
context,
|
||||||
@@ -231,4 +230,61 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
|
||||||
|
var controller = TextEditingController(text: version.name);
|
||||||
|
return showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Form(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => ContentDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFormBox(
|
||||||
|
controller: controller,
|
||||||
|
header: "Name",
|
||||||
|
placeholder: "Type the new version name",
|
||||||
|
autofocus: true,
|
||||||
|
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Button(
|
||||||
|
onPressed: () => Navigator.of(context).pop(null),
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (!Form.of(context)!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context).pop(controller.text);
|
||||||
|
},
|
||||||
|
child: const Text('Save')
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ContextualOption {
|
||||||
|
openExplorer,
|
||||||
|
rename,
|
||||||
|
delete;
|
||||||
|
|
||||||
|
String get name {
|
||||||
|
return this == ContextualOption.openExplorer ? "Open in explorer"
|
||||||
|
: this == ContextualOption.rename ? "Rename"
|
||||||
|
: "Delete";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||||
|
|
||||||
import '../util/os.dart';
|
import '../../util/os.dart';
|
||||||
|
|
||||||
class FileSelector extends StatefulWidget {
|
class FileSelector extends StatefulWidget {
|
||||||
final String label;
|
final String label;
|
||||||
@@ -65,7 +66,7 @@ class _FileSelectorState extends State<FileSelector> {
|
|||||||
|
|
||||||
void _onPressed() {
|
void _onPressed() {
|
||||||
if(_selecting){
|
if(_selecting){
|
||||||
showSnackbar(context, const Snackbar(content: Text("Folder selector is already opened")));
|
showMessage("Folder selector is already opened");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
import 'package:reboot_launcher/src/widget/smart_input.dart';
|
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
|
||||||
|
|
||||||
class HostInput extends StatelessWidget {
|
class HostInput extends StatelessWidget {
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
final ServerController _serverController = Get.find<ServerController>();
|
||||||
@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
import 'package:reboot_launcher/src/widget/smart_input.dart';
|
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
|
||||||
|
|
||||||
|
|
||||||
class PortInput extends StatelessWidget {
|
class PortInput extends StatelessWidget {
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
|
|
||||||
class ServerButton extends StatelessWidget {
|
class ServerButton extends StatefulWidget {
|
||||||
|
const ServerButton({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServerButton> createState() => _ServerButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServerButtonState extends State<ServerButton> {
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
final ServerController _serverController = Get.find<ServerController>();
|
||||||
|
|
||||||
ServerButton({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Align(
|
return Align(
|
||||||
@@ -18,7 +23,7 @@ class ServerButton extends StatelessWidget {
|
|||||||
child: Obx(() => Tooltip(
|
child: Obx(() => Tooltip(
|
||||||
message: _helpMessage,
|
message: _helpMessage,
|
||||||
child: Button(
|
child: Button(
|
||||||
onPressed: () => _onPressed(context),
|
onPressed: () async => _serverController.changeStateInteractive(false),
|
||||||
child: Text(_buttonText())),
|
child: Text(_buttonText())),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -55,42 +60,4 @@ class ServerButton extends StatelessWidget {
|
|||||||
return "Check if a local lawin server is running";
|
return "Check if a local lawin server is running";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPressed(BuildContext context) async {
|
|
||||||
var running = _serverController.started.value;
|
|
||||||
_serverController.started.value = !running;
|
|
||||||
switch(_serverController.type.value){
|
|
||||||
case ServerType.embedded:
|
|
||||||
var updatedRunning = await changeEmbeddedServerState(context, running);
|
|
||||||
_updateStarted(updatedRunning);
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
_serverController.reverseProxy = await changeReverseProxyState(
|
|
||||||
context,
|
|
||||||
_serverController.host.text,
|
|
||||||
_serverController.port.text,
|
|
||||||
false,
|
|
||||||
_serverController.reverseProxy
|
|
||||||
);
|
|
||||||
_updateStarted(_serverController.reverseProxy != null);
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
var result = await checkLocalServer(
|
|
||||||
context,
|
|
||||||
_serverController.host.text,
|
|
||||||
_serverController.port.text,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
_updateStarted(result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateStarted(bool updatedRunning) {
|
|
||||||
if (updatedRunning == _serverController.started.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_serverController.started.value = updatedRunning;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,10 @@ import 'package:get/get.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
|
|
||||||
class LocalServerSwitch extends StatelessWidget {
|
class ServerTypeSelector extends StatelessWidget {
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
final ServerController _serverController = Get.find<ServerController>();
|
||||||
|
|
||||||
LocalServerSwitch({Key? key}) : super(key: key);
|
ServerTypeSelector({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: reboot_launcher
|
name: reboot_launcher
|
||||||
description: Launcher for project reboot
|
description: Launcher for project reboot
|
||||||
version: "3.13.0"
|
version: "4.0.0"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@ dependencies:
|
|||||||
shelf_proxy: ^1.0.2
|
shelf_proxy: ^1.0.2
|
||||||
args: ^2.3.1
|
args: ^2.3.1
|
||||||
win32: 3.0.0
|
win32: 3.0.0
|
||||||
|
clipboard: ^0.1.3
|
||||||
|
sync: ^0.3.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
win32: ^3.0.0
|
win32: ^3.0.0
|
||||||
@@ -56,7 +58,7 @@ msix_config:
|
|||||||
display_name: Reboot Launcher
|
display_name: Reboot Launcher
|
||||||
publisher_display_name: Auties00
|
publisher_display_name: Auties00
|
||||||
identity_name: 31868Auties00.RebootLauncher
|
identity_name: 31868Auties00.RebootLauncher
|
||||||
msix_version: 3.13.0.0
|
msix_version: 4.0.0.0
|
||||||
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
||||||
logo_path: ./assets/icons/reboot.ico
|
logo_path: ./assets/icons/reboot.ico
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|||||||
Reference in New Issue
Block a user