mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
Reboot v3
This commit is contained in:
20
lib/cli.dart
20
lib/cli.dart
@@ -7,14 +7,12 @@ import 'package:reboot_launcher/src/cli/game.dart';
|
||||
import 'package:reboot_launcher/src/cli/reboot.dart';
|
||||
import 'package:reboot_launcher/src/cli/server.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart' as server;
|
||||
|
||||
late String? username;
|
||||
late GameType type;
|
||||
late bool host;
|
||||
late bool verbose;
|
||||
late String dll;
|
||||
late FortniteVersion version;
|
||||
@@ -40,10 +38,10 @@ void main(List<String> args) async {
|
||||
..addOption("server-host")
|
||||
..addOption("server-port")
|
||||
..addOption("matchmaking-address")
|
||||
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path)
|
||||
..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson))
|
||||
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? rebootDllFile)
|
||||
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
|
||||
..addFlag("log", defaultsTo: false)
|
||||
..addFlag("host", defaultsTo: false)
|
||||
..addFlag("auto-restart", defaultsTo: false, negatable: true);
|
||||
var result = parser.parse(args);
|
||||
if (result.command?.name == "list") {
|
||||
@@ -54,8 +52,8 @@ void main(List<String> args) async {
|
||||
}
|
||||
|
||||
dll = result["dll"];
|
||||
type = getGameType(result);
|
||||
username = result["username"] ?? gameJson["${type == GameType.client ? "game" : "server"}_username"];
|
||||
host = result["host"];
|
||||
username = result["username"] ?? gameJson["username"];
|
||||
verbose = result["log"];
|
||||
|
||||
version = _createVersion(gameJson["version"], result["version"], versions);
|
||||
@@ -69,7 +67,7 @@ void main(List<String> args) async {
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln("Launching game(type: ${type.name})...");
|
||||
stdout.writeln("Launching game...");
|
||||
if(version.executable == null){
|
||||
throw Exception("Missing game executable at: ${version.location.path}");
|
||||
}
|
||||
@@ -78,9 +76,9 @@ void main(List<String> args) async {
|
||||
await patchMatchmaking(version.executable!);
|
||||
|
||||
var serverType = getServerType(result);
|
||||
var host = result["server-host"] ?? serverJson["${serverType.id}_host"];
|
||||
var port = result["server-port"] ?? serverJson["${serverType.id}_port"];
|
||||
var started = await startServer(host, port, serverType);
|
||||
var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"];
|
||||
var serverPort = result["server-port"] ?? serverJson["${serverType.id}_port"];
|
||||
var started = await startServer(serverHost, serverPort, serverType);
|
||||
if(!started){
|
||||
stderr.writeln("Cannot start server!");
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
|
||||
@@ -7,32 +6,36 @@ import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/cli.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/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||
import 'package:reboot_launcher/src/util/error.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/home_page.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
const double kDefaultWindowWidth = 885;
|
||||
const double kDefaultWindowHeight = 885;
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
void main() async {
|
||||
await safeBinariesDirectory.create(recursive: true);
|
||||
await installationDirectory.create(recursive: true);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemTheme.accentColor.load();
|
||||
await GetStorage.init("game");
|
||||
await GetStorage.init("server");
|
||||
await GetStorage.init("update");
|
||||
await GetStorage.init("settings");
|
||||
await GetStorage.init("reboot_game");
|
||||
await GetStorage.init("reboot_server");
|
||||
await GetStorage.init("reboot_update");
|
||||
await GetStorage.init("reboot_settings");
|
||||
await GetStorage.init("reboot_hosting");
|
||||
Get.put(GameController());
|
||||
Get.put(ServerController());
|
||||
Get.put(BuildController());
|
||||
Get.put(SettingsController());
|
||||
Get.put(HostingController());
|
||||
doWhenWindowReady(() {
|
||||
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
|
||||
var controller = Get.find<SettingsController>();
|
||||
var size = Size(controller.width, controller.height);
|
||||
var window = appWindow as WinDesktopWindow;
|
||||
@@ -44,14 +47,16 @@ void main() async {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
windowManager.setPreventClose(true);
|
||||
appWindow.title = "Reboot Launcher";
|
||||
appWindow.show();
|
||||
});
|
||||
|
||||
runZonedGuarded(() =>
|
||||
runApp(const RebootApplication()),
|
||||
(error, stack) => onError(error, stack, false)
|
||||
runZonedGuarded(
|
||||
() async => runApp(const RebootApplication()),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,27 +69,22 @@ class RebootApplication extends StatefulWidget {
|
||||
|
||||
class _RebootApplicationState extends State<RebootApplication> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = SystemTheme.accentColor.accent.toAccentColor();
|
||||
return FluentApp(
|
||||
title: "Reboot Launcher",
|
||||
themeMode: ThemeMode.system,
|
||||
debugShowCheckedModeBanner: false,
|
||||
color: color,
|
||||
darkTheme: _createTheme(Brightness.dark),
|
||||
theme: _createTheme(Brightness.light),
|
||||
home: HomePage(key: appKey),
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => FluentApp(
|
||||
title: "Reboot Launcher",
|
||||
themeMode: ThemeMode.system,
|
||||
debugShowCheckedModeBanner: false,
|
||||
color: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
darkTheme: _createTheme(Brightness.dark),
|
||||
theme: _createTheme(Brightness.light),
|
||||
home: HomePage(key: appKey),
|
||||
);
|
||||
|
||||
FluentThemeData _createTheme(Brightness brightness) {
|
||||
return FluentThemeData(
|
||||
brightness: brightness,
|
||||
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
visualDensity: VisualDensity.standard,
|
||||
focusTheme: FocusThemeData(
|
||||
glowFactor: is10footScreen() ? 2.0 : 0.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
|
||||
brightness: brightness,
|
||||
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
visualDensity: VisualDensity.standard,
|
||||
focusTheme: FocusThemeData(
|
||||
glowFactor: is10footScreen() ? 2.0 : 0.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,8 @@ import 'dart:convert';
|
||||
import 'package:args/args.dart';
|
||||
|
||||
import '../model/fortnite_version.dart';
|
||||
import '../model/game_type.dart';
|
||||
import '../model/server_type.dart';
|
||||
|
||||
Iterable<String> getGameTypes() => GameType.values.map((entry) => entry.id);
|
||||
|
||||
Iterable<String> getServerTypes() => ServerType.values.map((entry) => entry.id);
|
||||
|
||||
String getDefaultServerType(Map<String, dynamic> json) {
|
||||
@@ -15,15 +12,6 @@ String getDefaultServerType(Map<String, dynamic> json) {
|
||||
return type.id;
|
||||
}
|
||||
|
||||
GameType getGameType(ArgResults result) {
|
||||
var type = GameType.of(result["type"]);
|
||||
if(type == null){
|
||||
throw Exception("Unknown game type: $result. Use --type only with ${getGameTypes().join(", ")}");
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
ServerType getServerType(ArgResults result) {
|
||||
var type = ServerType.of(result["server-type"]);
|
||||
if(type == null){
|
||||
@@ -33,18 +21,6 @@ ServerType getServerType(ArgResults result) {
|
||||
return type;
|
||||
}
|
||||
|
||||
String getDefaultGameType(Map<String, dynamic> json){
|
||||
var type = GameType.values.elementAt(json["type"] ?? 0);
|
||||
switch(type){
|
||||
case GameType.client:
|
||||
return "client";
|
||||
case GameType.server:
|
||||
return "server";
|
||||
case GameType.headlessServer:
|
||||
return "headless_server";
|
||||
}
|
||||
}
|
||||
|
||||
List<FortniteVersion> getVersions(Map<String, dynamic> gameJson) {
|
||||
Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]");
|
||||
return iterable.map((entry) => FortniteVersion.fromJson(entry))
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/cli.dart';
|
||||
|
||||
import '../model/fortnite_version.dart';
|
||||
import '../model/game_type.dart';
|
||||
import '../util/injector.dart';
|
||||
import '../util/os.dart';
|
||||
import '../util/process.dart';
|
||||
@@ -32,15 +31,14 @@ Future<void> startGame() async {
|
||||
.path} no longer contains a Fortnite executable, did you delete or move it?");
|
||||
}
|
||||
|
||||
var hosting = type != GameType.client;
|
||||
if (username == null) {
|
||||
username = "Reboot${hosting ? 'Host' : 'Player'}";
|
||||
username = "Reboot${host ? 'Host' : 'Player'}";
|
||||
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
|
||||
}
|
||||
|
||||
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type, ""))
|
||||
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, host, ""))
|
||||
..exitCode.then((_) => _onClose())
|
||||
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
|
||||
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose));
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +66,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
||||
}
|
||||
|
||||
if(line.contains("Platform has ")){
|
||||
_injectOrShowError("craniumv2.dll");
|
||||
_injectOrShowError("cobalt.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,7 +88,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
||||
_injectOrShowError("console.dll");
|
||||
}
|
||||
|
||||
_injectOrShowError("leakv2.dll");
|
||||
_injectOrShowError("memoryleak.dll");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +105,7 @@ Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
||||
|
||||
try {
|
||||
stdout.writeln("Injecting $binary...");
|
||||
var dll = locate ? await loadBinary(binary, true) : File(binary);
|
||||
var dll = locate ? File("${assetsDirectory.path}\\dlls\\$binary") : File(binary);
|
||||
if(!dll.existsSync()){
|
||||
throw Exception("Cannot inject $dll: missing file");
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1009257632315494520/1051137082766131250/Cranium.dll";
|
||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
|
||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1013220721494863872/1033484506633617500/MemoryLeakFixer.dll";
|
||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
|
||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
|
||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memoryleak.dll";
|
||||
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
|
||||
|
||||
Future<void> downloadRequiredDLLs() async {
|
||||
stdout.writeln("Downloading necessary components...");
|
||||
var consoleDll = await loadBinary("console.dll", true);
|
||||
var consoleDll = File("${assetsDirectory.path}\\dlls\\console.dll");
|
||||
if(!consoleDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_consoleDownload));
|
||||
if(response.statusCode != 200){
|
||||
@@ -22,30 +23,27 @@ Future<void> downloadRequiredDLLs() async {
|
||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var craniumDll = await loadBinary("craniumv2.dll", true);
|
||||
var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll");
|
||||
if(!craniumDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_baseDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download craniumv2.dll");
|
||||
throw Exception("Cannot download cobalt.dll");
|
||||
}
|
||||
|
||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var memoryFixDll = await loadBinary("leakv2.dll", true);
|
||||
var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
|
||||
if(!memoryFixDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_memoryFixDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download leakv2.dll");
|
||||
throw Exception("Cannot download memoryleak.dll");
|
||||
}
|
||||
|
||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var config = loadEmbedded("config/");
|
||||
var profiles = loadEmbedded("profiles/");
|
||||
var responses = loadEmbedded("responses/");
|
||||
if(!config.existsSync() || !profiles.existsSync() || !responses.existsSync()){
|
||||
if(!serverDirectory.existsSync()){
|
||||
var response = await http.get(Uri.parse(_embeddedConfigDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download embedded server config");
|
||||
@@ -53,7 +51,6 @@ Future<void> downloadRequiredDLLs() async {
|
||||
|
||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
|
||||
await extractFileToDisk(tempZip.path, "${safeBinariesDirectory.path}\\cli");
|
||||
await extractFileToDisk(tempZip.path, serverDirectory.path);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Future<bool> startServer(String? host, String? port, ServerType type) async {
|
||||
return true;
|
||||
case ServerType.embedded:
|
||||
stdout.writeln("Starting an embedded server...");
|
||||
await server.startServer();
|
||||
await server.startServer(false);
|
||||
var result = await server.ping(host ?? "127.0.0.1", port ?? "3551");
|
||||
if(result == null){
|
||||
throw Exception("Cannot start embedded server");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'game_type.dart';
|
||||
|
||||
class GameInstance {
|
||||
final Process gameProcess;
|
||||
final Process? launcherProcess;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
enum GameType {
|
||||
client,
|
||||
server,
|
||||
headlessServer;
|
||||
|
||||
static GameType? of(String id){
|
||||
try {
|
||||
return GameType.values
|
||||
.firstWhere((element) => element.id == id);
|
||||
}catch(_){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String get id {
|
||||
return this == GameType.client ? "client"
|
||||
: this == GameType.server ? "server"
|
||||
: "headless_server";
|
||||
}
|
||||
|
||||
String get name {
|
||||
return this == GameType.client ? "Game client"
|
||||
: this == GameType.server ? "Game server"
|
||||
: "Headless game server";
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class RebootDownload {
|
||||
final int updateTime;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
RebootDownload(this.updateTime, [this.error, this.stackTrace]);
|
||||
|
||||
bool get hasError => error != null;
|
||||
}
|
||||
8
lib/src/model/update_status.dart
Normal file
8
lib/src/model/update_status.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
enum UpdateStatus {
|
||||
waiting,
|
||||
started,
|
||||
success,
|
||||
error;
|
||||
|
||||
bool isDone() => this == UpdateStatus.success || this == UpdateStatus.error;
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/main.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/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/model/reboot_download.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/setting_tile.dart';
|
||||
|
||||
import '../dialog/dialog_button.dart';
|
||||
import '../util/checks.dart';
|
||||
import '../util/reboot.dart';
|
||||
|
||||
class LauncherPage extends StatefulWidget {
|
||||
const LauncherPage(
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<LauncherPage> createState() => _LauncherPageState();
|
||||
}
|
||||
|
||||
class _LauncherPageState extends State<LauncherPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if(_gameController.updater == null) {
|
||||
_startUpdater();
|
||||
_setupBuildWarning();
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setupBuildWarning() {
|
||||
_buildController.cancelledDownload
|
||||
.listen((value) => value ? _onCancelWarning() : {});
|
||||
}
|
||||
|
||||
void _startUpdater() {
|
||||
_gameController.updater = StreamController.broadcast();
|
||||
downloadRebootDll(_settingsController.updateUrl.text, _updateTime)
|
||||
..then((result) async {
|
||||
if(!result.hasError){
|
||||
_updateTime = result.updateTime;
|
||||
_gameController.updated = true;
|
||||
_gameController.failing = false;
|
||||
_gameController.error = false;
|
||||
_gameController.updater?.add(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if(_gameController.failing){
|
||||
_gameController.updated = false;
|
||||
_gameController.failing = false;
|
||||
_gameController.error = true;
|
||||
_gameController.updater?.add(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.failing = true;
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "An error occurred while downloading the reboot dll: this usually means that your antivirus flagged it. "
|
||||
"Do you want to add an exclusion to Windows Defender to fix the issue? "
|
||||
"If you are using a different antivirus disable it manually as this won't work. ",
|
||||
buttons: [
|
||||
ErrorDialog.createCopyErrorButton(
|
||||
error: result.error ?? Exception("Unknown error"),
|
||||
stackTrace: result.stackTrace,
|
||||
type: ButtonType.secondary,
|
||||
onClick: () {
|
||||
Navigator.pop(context);
|
||||
_gameController.updated = false;
|
||||
_gameController.failing = false;
|
||||
_gameController.error = true;
|
||||
_gameController.updater?.add(false);
|
||||
}
|
||||
),
|
||||
DialogButton(
|
||||
text: "Add",
|
||||
type: ButtonType.primary,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
var binary = await loadBinary("antivirus.bat", true);
|
||||
var result = await runElevated(binary.path, "");
|
||||
if(!result) {
|
||||
_gameController.failing = false;
|
||||
}
|
||||
|
||||
_startUpdater();
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
})
|
||||
..catchError((error, stackTrace) {
|
||||
_gameController.error = true;
|
||||
_gameController.updater?.add(false);
|
||||
return RebootDownload(0, error, stackTrace);
|
||||
});
|
||||
}
|
||||
|
||||
int? get _updateTime {
|
||||
var storage = GetStorage("update");
|
||||
return storage.read("last_update_v2");
|
||||
}
|
||||
|
||||
set _updateTime(int? updateTime) {
|
||||
var storage = GetStorage("update");
|
||||
storage.write("last_update_v2", updateTime);
|
||||
}
|
||||
|
||||
void _onCancelWarning() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
showSnackbar(context,
|
||||
const Snackbar(content: Text("Download cancelled")));
|
||||
_buildController.cancelledDownload(false);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => StreamBuilder<bool>(
|
||||
stream: _gameController.updater!.stream,
|
||||
builder: (context, snapshot) => !_gameController.updated && !_gameController.error ? _updateScreen : _homeScreen
|
||||
);
|
||||
|
||||
Widget get _homeScreen => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _gameController.error ? _updateError : const SizedBox(),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SizedBox(height: _gameController.error ? 16.0 : 0.0),
|
||||
),
|
||||
SettingTile(
|
||||
title: "Username",
|
||||
subtitle: "Enter the name that others will see once you are in-game",
|
||||
content: TextFormBox(
|
||||
placeholder: "username",
|
||||
controller: _gameController.username,
|
||||
validator: checkMatchmaking,
|
||||
autovalidateMode: AutovalidateMode.always
|
||||
)
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Matchmaking host",
|
||||
subtitle: "Enter the IP address of the game server hosting the match",
|
||||
content: TextFormBox(
|
||||
placeholder: "ip:port",
|
||||
controller: _settingsController.matchmakingIp,
|
||||
validator: checkMatchmaking,
|
||||
autovalidateMode: AutovalidateMode.always
|
||||
),
|
||||
expandedContent: [
|
||||
ListTile(
|
||||
title: const Text(
|
||||
"Automatically start a game server",
|
||||
style: TextStyle(
|
||||
fontSize: 14
|
||||
),
|
||||
),
|
||||
subtitle: const Text("Choose whether an headless server should be automatically started when matchmaking is on localhost"),
|
||||
trailing: Obx(() => ToggleSwitch(
|
||||
checked: _gameController.autostartGameServer(),
|
||||
onChanged: (value) => _gameController.autostartGameServer.value = value
|
||||
))
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Version",
|
||||
subtitle: "Select the version of Fortnite you want to play with your friends",
|
||||
content: const VersionSelector(),
|
||||
expandedContent: [
|
||||
ListTile(
|
||||
title: const Text(
|
||||
"Add a version from this PC's local storage",
|
||||
style: TextStyle(
|
||||
fontSize: 14
|
||||
),
|
||||
),
|
||||
trailing: Button(
|
||||
onPressed: () => VersionSelector.openAddDialog(context),
|
||||
child: const Text("Add build "),
|
||||
),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text(
|
||||
"Download any version from the cloud",
|
||||
style: TextStyle(
|
||||
fontSize: 14
|
||||
),
|
||||
),
|
||||
trailing: Button(
|
||||
onPressed: () => VersionSelector.openDownloadDialog(context),
|
||||
child: const Text("Download"),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Instance type",
|
||||
subtitle: "Select the type of instance you want to launch",
|
||||
content: GameTypeSelector()
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
const LaunchButton()
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _updateScreen => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _updateError {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_gameController.updated = false;
|
||||
_gameController.failing = false;
|
||||
_gameController.error = false;
|
||||
_gameController.updater?.add(false);
|
||||
_startUpdater();
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("The Reboot dll wasn't downloaded: disable your antivirus or proxy and click here to try again"
|
||||
),
|
||||
severity: InfoBarSeverity.info
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,68 +7,44 @@ import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
|
||||
import '../../model/update_status.dart';
|
||||
|
||||
const String kDefaultPlayerName = "Player";
|
||||
|
||||
class GameController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final TextEditingController username;
|
||||
late final TextEditingController password;
|
||||
late final RxBool showPassword;
|
||||
late final TextEditingController customLaunchArgs;
|
||||
late final Rx<List<FortniteVersion>> versions;
|
||||
late final Rxn<FortniteVersion> _selectedVersion;
|
||||
late final Rx<GameType> type;
|
||||
late final HashMap<GameType, GameInstance> gameInstancesMap;
|
||||
late final RxBool started;
|
||||
late final RxBool autostartGameServer;
|
||||
late bool updated;
|
||||
late bool error;
|
||||
late bool failing;
|
||||
StreamController<bool>? updater;
|
||||
|
||||
late final Rx<UpdateStatus> updateStatus;
|
||||
GameInstance? instance;
|
||||
|
||||
GameController() {
|
||||
_storage = GetStorage("game");
|
||||
|
||||
Iterable decodedVersionsJson =
|
||||
jsonDecode(_storage.read("versions") ?? "[]");
|
||||
_storage = GetStorage("reboot_game");
|
||||
Iterable decodedVersionsJson = jsonDecode(_storage.read("versions") ?? "[]");
|
||||
var decodedVersions = decodedVersionsJson
|
||||
.map((entry) => FortniteVersion.fromJson(entry))
|
||||
.toList();
|
||||
versions = Rx(decodedVersions);
|
||||
versions.listen((data) => saveVersions());
|
||||
|
||||
versions.listen((data) => _saveVersions());
|
||||
var decodedSelectedVersionName = _storage.read("version");
|
||||
var decodedSelectedVersion = decodedVersions.firstWhereOrNull(
|
||||
(element) => element.name == decodedSelectedVersionName);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
|
||||
type = Rx(GameType.values.elementAt(_storage.read("type") ?? 0));
|
||||
type.listen((value) {
|
||||
_storage.write("type", value.index);
|
||||
username.text = _readUsername();
|
||||
});
|
||||
|
||||
username = TextEditingController(text: _readUsername());
|
||||
username.addListener(() => _storage.write("${type.value == GameType.client ? 'game' : 'host'}_username", username.text));
|
||||
|
||||
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
|
||||
username.addListener(() => _storage.write("username", username.text));
|
||||
password = TextEditingController(text: _storage.read("password") ?? "");
|
||||
password.addListener(() => _storage.write("password", password.text));
|
||||
showPassword = RxBool(false);
|
||||
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? ""));
|
||||
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
|
||||
|
||||
gameInstancesMap= HashMap();
|
||||
|
||||
started = RxBool(false);
|
||||
|
||||
autostartGameServer = RxBool(_storage.read("auto_start_game_server") ?? true);
|
||||
autostartGameServer.listen((value) => _storage.write("auto_start_game_server", value));
|
||||
|
||||
updated = false;
|
||||
|
||||
error = false;
|
||||
|
||||
failing = false;
|
||||
}
|
||||
|
||||
String _readUsername() {
|
||||
var client = type.value == GameType.client;
|
||||
return _storage.read("${client ? 'game' : 'host'}_username") ?? (client ? "" : "HostingServer");
|
||||
updateStatus = Rx(UpdateStatus.waiting);
|
||||
}
|
||||
|
||||
FortniteVersion? getVersionByName(String name) {
|
||||
@@ -76,12 +52,15 @@ class GameController extends GetxController {
|
||||
}
|
||||
|
||||
void addVersion(FortniteVersion version) {
|
||||
var empty = versions.value.isEmpty;
|
||||
versions.update((val) => val?.add(version));
|
||||
if(empty){
|
||||
selectedVersion = version;
|
||||
}
|
||||
}
|
||||
|
||||
FortniteVersion removeVersionByName(String versionName) {
|
||||
var version =
|
||||
versions.value.firstWhere((element) => element.name == versionName);
|
||||
var version = versions.value.firstWhere((element) => element.name == versionName);
|
||||
removeVersion(version);
|
||||
return version;
|
||||
}
|
||||
@@ -90,9 +69,8 @@ class GameController extends GetxController {
|
||||
versions.update((val) => val?.remove(version));
|
||||
}
|
||||
|
||||
Future saveVersions() async {
|
||||
var serialized =
|
||||
jsonEncode(versions.value.map((entry) => entry.toJson()).toList());
|
||||
Future<void> _saveVersions() async {
|
||||
var serialized = jsonEncode(versions.value.map((entry) => entry.toJson()).toList());
|
||||
await _storage.write("versions", serialized);
|
||||
}
|
||||
|
||||
@@ -100,8 +78,6 @@ class GameController extends GetxController {
|
||||
|
||||
bool get hasNoVersions => versions.value.isEmpty;
|
||||
|
||||
GameInstance? get currentGameInstance => gameInstancesMap[type()];
|
||||
|
||||
FortniteVersion? get selectedVersion => _selectedVersion();
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
28
lib/src/ui/controller/hosting_controller.dart
Normal file
28
lib/src/ui/controller/hosting_controller.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import '../../model/game_instance.dart';
|
||||
|
||||
|
||||
const String kDefaultServerName = "Reboot Game Server";
|
||||
|
||||
class HostingController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final TextEditingController name;
|
||||
late final TextEditingController category;
|
||||
late final RxBool discoverable;
|
||||
late final RxBool started;
|
||||
GameInstance? instance;
|
||||
|
||||
HostingController() {
|
||||
_storage = GetStorage("reboot_hosting");
|
||||
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
|
||||
name.addListener(() => _storage.write("name", name.text));
|
||||
category = TextEditingController(text: _storage.read("category") ?? "");
|
||||
category.addListener(() => _storage.write("category", category.text));
|
||||
discoverable = RxBool(_storage.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage.write("discoverable", value));
|
||||
started = RxBool(false);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
import '../model/server_type.dart';
|
||||
import '../util/server.dart';
|
||||
import '../../model/server_type.dart';
|
||||
import '../../util/server.dart';
|
||||
|
||||
class ServerController extends GetxController {
|
||||
static const String _serverName = "127.0.0.1";
|
||||
@@ -18,14 +18,12 @@ class ServerController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final RxBool warning;
|
||||
late RxBool started;
|
||||
late RxBool loginAutomatically;
|
||||
late RxBool detached;
|
||||
HttpServer? remoteServer;
|
||||
|
||||
ServerController() {
|
||||
_storage = GetStorage("server");
|
||||
_storage = GetStorage("reboot_server");
|
||||
started = RxBool(false);
|
||||
loginAutomatically = RxBool(_storage.read("login_automatically") ?? false);
|
||||
loginAutomatically.listen((value) => _storage.write("login_automatically", value));
|
||||
type = Rx(ServerType.values.elementAt(_storage.read("type") ?? 0));
|
||||
type.listen((value) {
|
||||
host.text = _readHost();
|
||||
@@ -43,6 +41,8 @@ class ServerController extends GetxController {
|
||||
port.addListener(() => _storage.write("${type.value.id}_port", port.text));
|
||||
warning = RxBool(_storage.read("lawin_value") ?? true);
|
||||
warning.listen((value) => _storage.write("lawin_value", value));
|
||||
detached = RxBool(_storage.read("detached") ?? false);
|
||||
warning.listen((value) => _storage.write("detached", value));
|
||||
}
|
||||
|
||||
String _readHost() {
|
||||
@@ -1,16 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:ini/ini.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
import '../util/reboot.dart';
|
||||
import '../../util/reboot.dart';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
static const String _kDefaultIp = "127.0.0.1";
|
||||
static const bool _kDefaultAutoUpdate = true;
|
||||
|
||||
late final GetStorage _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController updateUrl;
|
||||
@@ -26,41 +28,30 @@ class SettingsController extends GetxController {
|
||||
late double scrollingDistance;
|
||||
|
||||
SettingsController() {
|
||||
_storage = GetStorage("settings");
|
||||
|
||||
_storage = GetStorage("reboot_settings");
|
||||
updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
|
||||
updateUrl.addListener(() => _storage.write("update_url", updateUrl.text));
|
||||
|
||||
rebootDll = _createController("reboot", "reboot.dll");
|
||||
|
||||
consoleDll = _createController("console", "console.dll");
|
||||
|
||||
authDll = _createController("cranium2", "craniumv2.dll");
|
||||
|
||||
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
|
||||
authDll = _createController("cobalt", "cobalt.dll");
|
||||
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? _kDefaultIp);
|
||||
matchmakingIp.addListener(() async {
|
||||
var text = matchmakingIp.text;
|
||||
_storage.write("ip", text);
|
||||
writeMatchmakingIp(text);
|
||||
});
|
||||
|
||||
width = _storage.read("width") ?? 912;
|
||||
height = _storage.read("height") ?? 660;
|
||||
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
|
||||
autoUpdate = RxBool(_storage.read("auto_update") ?? false);
|
||||
autoUpdate = RxBool(_storage.read("auto_update") ?? _kDefaultAutoUpdate);
|
||||
autoUpdate.listen((value) async => _storage.write("auto_update", value));
|
||||
|
||||
scrollingDistance = 0.0;
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, String name) {
|
||||
loadBinary(name, true);
|
||||
|
||||
var controller = TextEditingController(text: _storage.read(key) ?? "${safeBinariesDirectory.path}\\$name");
|
||||
var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name));
|
||||
controller.addListener(() => _storage.write(key, controller.text));
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
@@ -73,4 +64,16 @@ class SettingsController extends GetxController {
|
||||
_storage.write("offset_x", position.dx);
|
||||
_storage.write("offset_y", position.dy);
|
||||
}
|
||||
|
||||
void reset(){
|
||||
updateUrl.text = rebootDownloadUrl;
|
||||
rebootDll.text = _controllerDefaultPath("reboot.dll");
|
||||
consoleDll.text = _controllerDefaultPath("console.dll");
|
||||
authDll.text = _controllerDefaultPath("cobalt.dll");
|
||||
matchmakingIp.text = _kDefaultIp;
|
||||
writeMatchmakingIp(_kDefaultIp);
|
||||
autoUpdate.value = _kDefaultAutoUpdate;
|
||||
}
|
||||
|
||||
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name";
|
||||
}
|
||||
6
lib/src/ui/controller/update_controller.dart
Normal file
6
lib/src/ui/controller/update_controller.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
final GetStorage _storage = GetStorage("reboot_update");
|
||||
|
||||
int? get updateTime => _storage.read("last_update_v2");
|
||||
set updateTime(int? updateTime) => _storage.write("last_update_v2", updateTime);
|
||||
@@ -2,14 +2,14 @@ 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 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
|
||||
import '../util/checks.dart';
|
||||
import '../../util/checks.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
import '../widget/shared/smart_check_box.dart';
|
||||
import 'dialog.dart';
|
||||
import 'dialog_button.dart';
|
||||
|
||||
class AddLocalVersion extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
@@ -24,9 +24,20 @@ class AddLocalVersion extends StatelessWidget {
|
||||
return FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("Local builds are not guaranteed to work"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
TextFormBox(
|
||||
controller: _nameController,
|
||||
header: "Name",
|
||||
@@ -35,19 +46,14 @@ class AddLocalVersion extends StatelessWidget {
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: "Path",
|
||||
placeholder: "Type the game folder",
|
||||
windowTitle: "Select game folder",
|
||||
controller: _gamePathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
@@ -5,19 +5,20 @@ 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/error.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 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
|
||||
import '../util/checks.dart';
|
||||
import '../../util/checks.dart';
|
||||
import '../controller/build_controller.dart';
|
||||
import '../widget/home/build_selector.dart';
|
||||
import '../widget/home/version_name_input.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
import 'dialog.dart';
|
||||
import 'dialog_button.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
const AddServerVersion({Key? key}) : super(key: key);
|
||||
@@ -39,7 +40,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
DownloadStatus _status = DownloadStatus.form;
|
||||
String _timeLeft = "00:00:00";
|
||||
double _downloadProgress = 0;
|
||||
Process? _manifestDownloadProcess;
|
||||
CancelableOperation? _manifestDownloadProcess;
|
||||
CancelableOperation? _driveDownloadOperation;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
@@ -49,7 +50,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
@@ -67,14 +68,12 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
}
|
||||
|
||||
void _onDisposed() {
|
||||
if (_status != DownloadStatus.downloading &&
|
||||
_status != DownloadStatus.extracting) {
|
||||
if (_status != DownloadStatus.downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_manifestDownloadProcess != null) {
|
||||
loadBinary("stop.bat", true).then(
|
||||
(value) => Process.runSync(value.path, []));
|
||||
_manifestDownloadProcess?.cancel();
|
||||
_buildController.cancelledDownload(true);
|
||||
return;
|
||||
}
|
||||
@@ -129,13 +128,14 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
setState(() => _status = DownloadStatus.downloading);
|
||||
_manifestDownloadProcess = await downloadManifestBuild(
|
||||
var future = downloadArchiveBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
_onDownloadProgress
|
||||
Directory(_pathController.text),
|
||||
_onDownloadProgress,
|
||||
_onUnrar
|
||||
);
|
||||
_manifestDownloadProcess!.exitCode
|
||||
.then((value) => _onDownloadComplete());
|
||||
future.then((value) => _onDownloadComplete());
|
||||
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
setState(() => _status = DownloadStatus.extracting);
|
||||
}
|
||||
|
||||
void _onDownloadComplete() {
|
||||
Future<void> _onDownloadComplete() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -160,6 +160,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
print("Error");
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -183,59 +184,78 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
});
|
||||
}
|
||||
|
||||
Padding _createExtractingBody() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: InfoLabel(
|
||||
label: "Extracting...",
|
||||
child: const SizedBox(width: double.infinity, child: ProgressBar())),
|
||||
);
|
||||
}
|
||||
Widget _createDownloadBody() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Downloading...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
Widget _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,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _createExtractingBody() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Extracting...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar()
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _createFormDialog() {
|
||||
return FutureBuilder(
|
||||
@@ -265,21 +285,19 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
Widget _createFormBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const BuildSelector(),
|
||||
const SizedBox(height: 16.0),
|
||||
const SizedBox(height: 20.0),
|
||||
VersionNameInput(controller: _nameController),
|
||||
const SizedBox(height: 16.0),
|
||||
FileSelector(
|
||||
label: "Path",
|
||||
placeholder: "Type the download destination",
|
||||
windowTitle: "Select download destination",
|
||||
controller: _pathController,
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
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 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
|
||||
import 'dialog_button.dart';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../../../main.dart';
|
||||
import 'dialog.dart';
|
||||
|
||||
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
|
||||
"This means that you cannot currently host this version of the game. "
|
||||
@@ -1,32 +1,30 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_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/os.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../util/server.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../util/server.dart';
|
||||
import '../controller/server_controller.dart';
|
||||
import 'dialog.dart';
|
||||
import 'dialog_button.dart';
|
||||
|
||||
extension ServerControllerDialog on ServerController {
|
||||
static Semaphore semaphore = Semaphore();
|
||||
|
||||
Future<bool> restart() async {
|
||||
Future<bool> restart(bool closeLocalPromptAutomatically) async {
|
||||
await resetWinNat();
|
||||
return (!started() || await stop()) && await toggle();
|
||||
return (!started() || await stop()) && await toggle(closeLocalPromptAutomatically);
|
||||
}
|
||||
|
||||
Future<bool> toggle() async {
|
||||
Future<bool> toggle(bool closeLocalPromptAutomatically) async {
|
||||
try{
|
||||
semaphore.acquire();
|
||||
if (type() == ServerType.local) {
|
||||
return _pingSelfInteractive();
|
||||
return _pingSelfInteractive(closeLocalPromptAutomatically);
|
||||
}
|
||||
|
||||
var result = await _toggle();
|
||||
@@ -35,7 +33,7 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var ping = await _pingSelfInteractive();
|
||||
var ping = await _pingSelfInteractive(true);
|
||||
if(!ping){
|
||||
started.value = false;
|
||||
return false;
|
||||
@@ -79,7 +77,7 @@ extension ServerControllerDialog on ServerController {
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
startServer();
|
||||
startServer(detached());
|
||||
break;
|
||||
case ServerType.remote:
|
||||
var uriResult = await _pingRemoteInteractive();
|
||||
@@ -166,15 +164,27 @@ extension ServerControllerDialog on ServerController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _pingSelfInteractive() async {
|
||||
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
|
||||
try {
|
||||
var resultFuture = compute(pingSelf, port.text)
|
||||
.then((value) => value != null);
|
||||
await showDialog<bool>(
|
||||
Future<bool> ping() async {
|
||||
for(var i = 0; i < 3; i++){
|
||||
var result = await pingSelf(port.text);
|
||||
if(result != null){
|
||||
return true;
|
||||
}else {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var future = _waitFutureOrTime(ping());
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: _waitFutureOrTime(resultFuture),
|
||||
future: future,
|
||||
loadingMessage: "Pinging ${type().id} server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server works correctly"),
|
||||
@@ -182,10 +192,10 @@ extension ServerControllerDialog on ServerController {
|
||||
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
||||
errorMessageBuilder: (
|
||||
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
||||
closeAutomatically: true
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
);
|
||||
return await resultFuture;
|
||||
) ?? false;
|
||||
return result && await future;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
@@ -193,12 +203,14 @@ extension ServerControllerDialog on ServerController {
|
||||
|
||||
Future<Uri?> _pingRemoteInteractive() async {
|
||||
try {
|
||||
var mainFuture = ping(host.text, port.text);
|
||||
await showDialog<bool>(
|
||||
var mainFuture = ping(host.text, port.text).then((value) => value != null);
|
||||
var future = _waitFutureOrTime(mainFuture);
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: _waitFutureOrTime(mainFuture.then((value) => value != null)),
|
||||
future: future,
|
||||
closeAutomatically: false,
|
||||
loadingMessage: "Pinging remote server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
@@ -209,7 +221,7 @@ extension ServerControllerDialog on ServerController {
|
||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
||||
)
|
||||
) ?? false;
|
||||
return await mainFuture;
|
||||
return result ? await future : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
@@ -259,60 +271,48 @@ extension ServerControllerDialog on ServerController {
|
||||
);
|
||||
}
|
||||
|
||||
void showUnexpectedServerError() {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The backend server died unexpectedly",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
void showUnexpectedServerError() => showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The backend server died unexpectedly",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
DialogButton(
|
||||
text: "Open log",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
launchUrl(serverLogFile.uri);
|
||||
if(serverLogFile.existsSync()){
|
||||
showMessage("No log is available");
|
||||
}else {
|
||||
launchUrl(serverLogFile.uri);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
void _showIllegalPortError() {
|
||||
showMessage("Illegal port for backend server, use only numbers");
|
||||
}
|
||||
void _showIllegalPortError() => showMessage("Illegal port for backend server, use only numbers");
|
||||
|
||||
void _showMissingPortError() {
|
||||
showMessage("Missing port for backend server");
|
||||
}
|
||||
void _showMissingPortError() => showMessage("Missing port for backend server");
|
||||
|
||||
void _showMissingHostError() {
|
||||
showMessage("Missing the host name for backend server");
|
||||
}
|
||||
}
|
||||
void _showMissingHostError() => showMessage("Missing the host name for backend server");
|
||||
|
||||
Future<Object?> _showUnknownError(ServerResult result) {
|
||||
return showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: result.error ?? Exception("Unknown error"),
|
||||
stackTrace: result.stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
|
||||
)
|
||||
);
|
||||
}
|
||||
Future<Object?> _showUnknownError(ServerResult result) => showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: result.error ?? Exception("Unknown error"),
|
||||
stackTrace: result.stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
|
||||
)
|
||||
);
|
||||
|
||||
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) {
|
||||
return Future.wait<bool>([
|
||||
resultFuture,
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
.then((value) => true)
|
||||
]).then((value) => value.reduce((f, s) => f && s));
|
||||
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) => Future.wait<bool>([resultFuture, Future.delayed(const Duration(seconds: 1)).then((value) => true)]).then((value) => value.reduce((f, s) => f && s));
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../page/home_page.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
void showMessage(String text){
|
||||
showSnackbar(
|
||||
@@ -2,21 +2,16 @@ import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/page/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
||||
import 'package:reboot_launcher/src/page/server_page.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/widget/os/window_border.dart';
|
||||
import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/launcher_page.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/server_page.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/settings_page.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import '../model/server_type.dart';
|
||||
import '../widget/os/window_border.dart';
|
||||
import '../widget/os/window_buttons.dart';
|
||||
import 'hosting_page.dart';
|
||||
import 'info_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -29,9 +24,7 @@ class HomePage extends StatefulWidget {
|
||||
class _HomePageState extends State<HomePage> with WindowListener {
|
||||
static const double _defaultPadding = 12.0;
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
@@ -39,8 +32,8 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final RxInt _index = RxInt(0);
|
||||
bool _navigated = false;
|
||||
bool _shouldMaximize = false;
|
||||
final RxBool _nestedNavigation = RxBool(false);
|
||||
final GlobalKey<NavigatorState> _settingsNavigatorKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -91,35 +84,6 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
super.onWindowMoved();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
if(!_gameController.started() || !_serverController.started()) {
|
||||
windowManager.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return InfoDialog(
|
||||
text: "Closing the launcher while a backend is running may make the game not work correctly. Are you sure you want to proceed?",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Don't close",
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
onTap: () => windowManager.destroy(),
|
||||
text: "Close",
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => Stack(
|
||||
children: [
|
||||
@@ -130,7 +94,8 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused())
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: _selectedIndex,
|
||||
@@ -149,11 +114,34 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
]
|
||||
));
|
||||
|
||||
void _onIndexChanged(int index) {
|
||||
_index.value = index;
|
||||
_navigated = true;
|
||||
Widget get _backButton => Obx(() {
|
||||
// ignore: unused_local_variable
|
||||
var ignored = _nestedNavigation.value;
|
||||
return PaneItem(
|
||||
icon: const Icon(FluentIcons.back, size: 14.0),
|
||||
body: const SizedBox.shrink(),
|
||||
).build(
|
||||
context,
|
||||
false,
|
||||
_onBack(),
|
||||
displayMode: PaneDisplayMode.compact
|
||||
);
|
||||
});
|
||||
|
||||
void Function()? _onBack() {
|
||||
var navigator = _settingsNavigatorKey.currentState;
|
||||
if(navigator == null || !navigator.mounted || !navigator.canPop()){
|
||||
return null;
|
||||
}
|
||||
|
||||
return () async {
|
||||
Navigator.pop(navigator.context);
|
||||
_nestedNavigation.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
void _onIndexChanged(int index) => _index.value = index;
|
||||
|
||||
TextBox get _autoSuggestBox => TextBox(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
@@ -162,15 +150,7 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
);
|
||||
|
||||
GestureDetector get _draggableArea => GestureDetector(
|
||||
onDoubleTap: () {
|
||||
if(!_shouldMaximize){
|
||||
return;
|
||||
}
|
||||
|
||||
appWindow.maximizeOrRestore();
|
||||
_shouldMaximize = false;
|
||||
},
|
||||
onDoubleTapDown: (details) => _shouldMaximize = true,
|
||||
onDoubleTap: () => appWindow.maximizeOrRestore(),
|
||||
onHorizontalDragStart: (event) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (event) => appWindow.startDragging()
|
||||
);
|
||||
@@ -205,32 +185,29 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
|
||||
List<NavigationPaneItem> get _items => _searchItems() ?? [
|
||||
PaneItem(
|
||||
title: const Text("Home"),
|
||||
title: const Text("Play"),
|
||||
icon: const Icon(FluentIcons.game),
|
||||
body: const LauncherPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Host"),
|
||||
icon: const Icon(FluentIcons.server_processes),
|
||||
body: const HostingPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Backend"),
|
||||
icon: const Icon(FluentIcons.server_enviroment),
|
||||
icon: const Icon(FluentIcons.user_window),
|
||||
body: ServerPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage(),
|
||||
onTap: _onTutorial
|
||||
body: InfoPage(_settingsNavigatorKey, _nestedNavigation)
|
||||
),
|
||||
];
|
||||
|
||||
void _onTutorial() {
|
||||
if(!_navigated){
|
||||
setState(() => _settingsController.scrollingDistance = 0);
|
||||
}
|
||||
|
||||
_navigated = false;
|
||||
}
|
||||
|
||||
String get searchValue => _searchController.text;
|
||||
}
|
||||
105
lib/src/ui/page/hosting_page.dart
Normal file
105
lib/src/ui/page/hosting_page.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
|
||||
|
||||
|
||||
class HostingPage extends StatefulWidget {
|
||||
const HostingPage(
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<HostingPage> createState() => _HostingPageState();
|
||||
}
|
||||
|
||||
class _HostingPageState extends State<HostingPage> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
SettingTile(
|
||||
title: "Game Server",
|
||||
subtitle: "Provide basic information about your server",
|
||||
expandedContentSpacing: 0,
|
||||
expandedContent: [
|
||||
SettingTile(
|
||||
title: "Name",
|
||||
subtitle: "The name of your game server",
|
||||
isChild: true,
|
||||
content: TextFormBox(
|
||||
placeholder: "Name",
|
||||
controller: _hostingController.name
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
title: "Category",
|
||||
subtitle: "The category of your game server",
|
||||
isChild: true,
|
||||
content: TextFormBox(
|
||||
placeholder: "Category",
|
||||
controller: _hostingController.category
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
title: "Discoverable",
|
||||
subtitle: "Make your server available to other players on the server browser",
|
||||
isChild: true,
|
||||
contentWidth: null,
|
||||
content: Obx(() => ToggleSwitch(
|
||||
checked: _hostingController.discoverable(),
|
||||
onChanged: (value) => _hostingController.discoverable.value = value
|
||||
))
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Version",
|
||||
subtitle: "Select the version of Fortnite you want to host",
|
||||
content: const VersionSelector(),
|
||||
expandedContent: [
|
||||
SettingTile(
|
||||
title: "Add a version from this PC's local storage",
|
||||
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
||||
content: Button(
|
||||
onPressed: () => VersionSelector.openAddDialog(context),
|
||||
child: const Text("Add build"),
|
||||
),
|
||||
isChild: true
|
||||
),
|
||||
SettingTile(
|
||||
title: "Download any version from the cloud",
|
||||
subtitle: "A curated list of supported versions by Project Reboot",
|
||||
content: Button(
|
||||
onPressed: () => VersionSelector.openDownloadDialog(context),
|
||||
child: const Text("Download"),
|
||||
),
|
||||
isChild: true
|
||||
)
|
||||
]
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
const LaunchButton(
|
||||
host: true
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import '../widget/shared/fluent_card.dart';
|
||||
|
||||
class InfoPage extends StatefulWidget {
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final RxBool nestedNavigation;
|
||||
const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<InfoPage> createState() => _InfoPageState();
|
||||
@@ -36,7 +39,6 @@ class _InfoPageState extends State<InfoPage> {
|
||||
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
|
||||
];
|
||||
|
||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final ScrollController _controller;
|
||||
|
||||
@@ -57,7 +59,7 @@ class _InfoPageState extends State<InfoPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Navigator(
|
||||
key: _navigatorKey,
|
||||
key: widget.navigatorKey,
|
||||
initialRoute: "home",
|
||||
onGenerateRoute: (settings) {
|
||||
var screen = _createScreen(settings.name);
|
||||
@@ -69,6 +71,8 @@ class _InfoPageState extends State<InfoPage> {
|
||||
);
|
||||
|
||||
Widget _createScreen(String? name) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => widget.nestedNavigation.value = name != "home");
|
||||
switch(name){
|
||||
case "home":
|
||||
return _homeScreen;
|
||||
@@ -87,7 +91,7 @@ class _InfoPageState extends State<InfoPage> {
|
||||
_createCardWidget(
|
||||
text: "Play on someone else's server",
|
||||
description: "If one of your friends is hosting a game server, click here",
|
||||
onClick: () => _navigatorKey.currentState?.pushNamed("else")
|
||||
onClick: () => widget.navigatorKey.currentState?.pushNamed("else")
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
@@ -97,7 +101,7 @@ class _InfoPageState extends State<InfoPage> {
|
||||
_createCardWidget(
|
||||
text: "Host your own server",
|
||||
description: "If you want to create your own server to invite your friends or to play around by yourself, click here",
|
||||
onClick: () => _navigatorKey.currentState?.pushNamed("own")
|
||||
onClick: () => widget.navigatorKey.currentState?.pushNamed("own")
|
||||
)
|
||||
]
|
||||
);
|
||||
216
lib/src/ui/page/launcher_page.dart
Normal file
216
lib/src/ui/page/launcher_page.dart
Normal file
@@ -0,0 +1,216 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../model/update_status.dart';
|
||||
import '../../util/checks.dart';
|
||||
import '../../util/reboot.dart';
|
||||
import '../controller/update_controller.dart';
|
||||
|
||||
class LauncherPage extends StatefulWidget {
|
||||
const LauncherPage(
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<LauncherPage> createState() => _LauncherPageState();
|
||||
}
|
||||
|
||||
class _LauncherPageState extends State<LauncherPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if(_gameController.updateStatus() == UpdateStatus.waiting) {
|
||||
_startUpdater();
|
||||
_setupBuildWarning();
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setupBuildWarning() {
|
||||
void onCancelWarning() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
showSnackbar(context, const Snackbar(content: Text("Download cancelled")));
|
||||
_buildController.cancelledDownload(false);
|
||||
});
|
||||
_buildController.cancelledDownload.listen((value) => value ? onCancelWarning() : {});
|
||||
}
|
||||
|
||||
Future<void> _startUpdater() async {
|
||||
if(!_settingsController.autoUpdate()){
|
||||
_gameController.updateStatus.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.updateStatus.value = UpdateStatus.started;
|
||||
try {
|
||||
updateTime = await downloadRebootDll(_settingsController.updateUrl.text, updateTime);
|
||||
_gameController.updateStatus.value = UpdateStatus.success;
|
||||
}catch(_) {
|
||||
_gameController.updateStatus.value = UpdateStatus.error;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => !_settingsController.autoUpdate() || _gameController.updateStatus().isDone() ? _homePage : _updateScreen);
|
||||
|
||||
Widget get _homePage => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _gameController.updateStatus() == UpdateStatus.error ? _updateError : const SizedBox(),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SizedBox(height: _gameController.updateStatus() == UpdateStatus.error ? 16.0 : 0.0),
|
||||
),
|
||||
SettingTile(
|
||||
title: "Credentials",
|
||||
subtitle: "Your in-game login credentials",
|
||||
expandedContentSpacing: 0,
|
||||
expandedContent: [
|
||||
SettingTile(
|
||||
title: "Username",
|
||||
subtitle: "The username that other players will see when you are in game",
|
||||
isChild: true,
|
||||
content: TextFormBox(
|
||||
placeholder: "Username",
|
||||
controller: _gameController.username,
|
||||
autovalidateMode: AutovalidateMode.always
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
title: "Password",
|
||||
subtitle: "The password of your account, only used if the backend requires it",
|
||||
isChild: true,
|
||||
content: Obx(() => TextFormBox(
|
||||
placeholder: "Password",
|
||||
controller: _gameController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_gameController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) => _showPasswordTrailing.value = text.isNotEmpty,
|
||||
suffix: Button(
|
||||
onPressed: () => _gameController.showPassword.value = !_gameController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_gameController.showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
))
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Matchmaking host",
|
||||
subtitle: "Enter the IP address of the game server hosting the match",
|
||||
content: TextFormBox(
|
||||
placeholder: "IP:PORT",
|
||||
controller: _settingsController.matchmakingIp,
|
||||
validator: checkMatchmaking,
|
||||
autovalidateMode: AutovalidateMode.always
|
||||
),
|
||||
expandedContent: [
|
||||
SettingTile(
|
||||
title: "Browse available servers",
|
||||
subtitle: "Discover new game servers that fit your play-style",
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(Uri.parse("https://google.com/search?q=One+Day+This+Will+Be+Ready")),
|
||||
child: const Text("Browse")
|
||||
),
|
||||
isChild: true
|
||||
)
|
||||
]
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Version",
|
||||
subtitle: "Select the version of Fortnite you want to play",
|
||||
content: const VersionSelector(),
|
||||
expandedContent: [
|
||||
SettingTile(
|
||||
title: "Add a version from this PC's local storage",
|
||||
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
||||
content: Button(
|
||||
onPressed: () => VersionSelector.openAddDialog(context),
|
||||
child: const Text("Add build"),
|
||||
),
|
||||
isChild: true
|
||||
),
|
||||
SettingTile(
|
||||
title: "Download any version from the cloud",
|
||||
subtitle: "A curated list of supported versions by Project Reboot",
|
||||
content: Button(
|
||||
onPressed: () => VersionSelector.openDownloadDialog(context),
|
||||
child: const Text("Download"),
|
||||
),
|
||||
isChild: true
|
||||
)
|
||||
]
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
const LaunchButton(
|
||||
host: false
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _updateScreen => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _updateError => MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => _startUpdater(),
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
|
||||
severity: InfoBarSeverity.info
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/host_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/port_input.dart';
|
||||
import 'package:reboot_launcher/src/widget/server/server_button.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../model/server_type.dart';
|
||||
import '../widget/shared/setting_tile.dart';
|
||||
|
||||
class ServerPage extends StatelessWidget {
|
||||
@@ -33,7 +34,7 @@ class ServerPage extends StatelessWidget {
|
||||
title: "Host",
|
||||
subtitle: "Enter the host of the backend server",
|
||||
content: TextFormBox(
|
||||
placeholder: "host",
|
||||
placeholder: "Host",
|
||||
controller: _serverController.host,
|
||||
enabled: _isRemote
|
||||
)
|
||||
@@ -42,10 +43,10 @@ class ServerPage extends StatelessWidget {
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Host",
|
||||
title: "Port",
|
||||
subtitle: "Enter the port of the backend server",
|
||||
content: TextFormBox(
|
||||
placeholder: "host",
|
||||
placeholder: "Port",
|
||||
controller: _serverController.port,
|
||||
enabled: _isRemote
|
||||
)
|
||||
@@ -54,24 +55,32 @@ class ServerPage extends StatelessWidget {
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Type",
|
||||
subtitle: "Select the type of backend to use",
|
||||
content: ServerTypeSelector()
|
||||
title: "Type",
|
||||
subtitle: "Select the type of backend to use",
|
||||
content: ServerTypeSelector()
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SettingTile(
|
||||
title: "Login automatically",
|
||||
subtitle: "Choose whether the game client should login automatically using random credentials",
|
||||
SettingTile(
|
||||
title: "Detached",
|
||||
subtitle: "Choose whether the backend should be started as a separate process, useful for debugging",
|
||||
contentWidth: null,
|
||||
content: Obx(() => ToggleSwitch(
|
||||
checked: _serverController.loginAutomatically(),
|
||||
onChanged: (value) => _serverController.loginAutomatically.value = value
|
||||
checked: _serverController.detached(),
|
||||
onChanged: (value) => _serverController.detached.value = value
|
||||
))
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Server files",
|
||||
subtitle: "The location where the backend is stored",
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(serverDirectory.uri),
|
||||
child: const Text("Open")
|
||||
)
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
const ServerButton()
|
||||
@@ -1,13 +1,16 @@
|
||||
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../util/checks.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
import '../../util/os.dart';
|
||||
import '../../util/selector.dart';
|
||||
import '../dialog/dialog.dart';
|
||||
import '../widget/shared/setting_tile.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
@@ -60,7 +63,7 @@ class SettingsPage extends StatelessWidget {
|
||||
title: "Custom launch arguments",
|
||||
subtitle: "Enter additional arguments to use when launching the game",
|
||||
content: TextFormBox(
|
||||
placeholder: "args",
|
||||
placeholder: "Arguments...",
|
||||
controller: _gameController.customLaunchArgs,
|
||||
)
|
||||
),
|
||||
@@ -71,10 +74,51 @@ class SettingsPage extends StatelessWidget {
|
||||
title: "Create a bug report",
|
||||
subtitle: "Help me fix bugs by reporting them",
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(Uri.parse("https://discord.com/channels/998020695223193670/1031262639457828910")),
|
||||
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues/new/choose")),
|
||||
child: const Text("Report a bug"),
|
||||
)
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Reset settings",
|
||||
subtitle: "Resets the launcher's settings to their default values",
|
||||
content: Button(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "Do you want to reset all settings to their default values? This action is irreversible",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Close",
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
text: "Reset",
|
||||
onTap: () {
|
||||
_settingsController.reset();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
child: const Text("Reset"),
|
||||
)
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Version status",
|
||||
subtitle: "Current version: 7.0",
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(installationDirectory.uri),
|
||||
child: const Text("Show Files"),
|
||||
)
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -87,7 +131,7 @@ class SettingsPage extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
placeholder: "path",
|
||||
placeholder: "Path",
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
autovalidateMode: AutovalidateMode.always
|
||||
@@ -99,7 +143,10 @@ class SettingsPage extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: () { },
|
||||
onPressed: () async {
|
||||
var selected = await compute(openFilePicker, "dll");
|
||||
controller.text = selected ?? controller.text;
|
||||
},
|
||||
child: const Icon(FluentIcons.open_folder_horizontal),
|
||||
),
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
|
||||
class BuildSelector extends StatefulWidget {
|
||||
@@ -1,36 +1,36 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/game_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/game_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/injector.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:reboot_launcher/src/../main.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||
|
||||
import '../../util/process.dart';
|
||||
import '../../../util/process.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
const LaunchButton(
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
final bool host;
|
||||
|
||||
const LaunchButton({Key? key, required this.host}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LaunchButton> createState() => _LaunchButtonState();
|
||||
@@ -51,17 +51,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
];
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
File? _logFile;
|
||||
final File _logFile = File("${assetsDirectory.path}\\logs\\game.log");
|
||||
bool _fail = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
loadBinary("game.txt", true)
|
||||
.then((value) => _logFile = value);
|
||||
super.initState();
|
||||
}
|
||||
Future? _executor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Align(
|
||||
@@ -74,26 +69,34 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_gameController.started() ? "Close fortnite" : "Launch fortnite"
|
||||
_hasStarted ? _stopMessage : _startMessage
|
||||
),
|
||||
),
|
||||
onPressed: () => _start(_gameController.type()),
|
||||
onPressed: () => _executor = _start()
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
void _start(GameType type) async {
|
||||
if (_gameController.started()) {
|
||||
_onStop(type);
|
||||
bool get _hasStarted => widget.host ? _hostingController.started() : _gameController.started();
|
||||
|
||||
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started;
|
||||
|
||||
String get _startMessage => widget.host ? "Start hosting" : "Launch fortnite";
|
||||
|
||||
String get _stopMessage => widget.host ? "Stop hosting" : "Close fortnite";
|
||||
|
||||
Future<void> _start() async {
|
||||
if (_hasStarted) {
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.started.value = true;
|
||||
_setStarted(widget.host, true);
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
if(_serverController.type() != ServerType.local){
|
||||
showMessage("Missing username");
|
||||
_onStop(type);
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,31 +105,29 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
if (_gameController.selectedVersion == null) {
|
||||
showMessage("No version is selected");
|
||||
_onStop(type);
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var element in Injectable.values) {
|
||||
if(await _getDllPath(element, type) == null) {
|
||||
if(await _getDllPath(element, widget.host) == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_fail = false;
|
||||
await _resetLogFile();
|
||||
|
||||
var version = _gameController.selectedVersion!;
|
||||
var gamePath = version.executable?.path;
|
||||
if(gamePath == null){
|
||||
showMissingBuildError(version);
|
||||
_onStop(type);
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = _serverController.started() || await _serverController.toggle();
|
||||
var result = _serverController.started() || await _serverController.toggle(true);
|
||||
if(!result){
|
||||
_onStop(type);
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,28 +135,34 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await compute(patchHeadless, version.executable!);
|
||||
|
||||
var automaticallyStartedServer = await _startMatchMakingServer();
|
||||
await _startGameProcesses(version, type, automaticallyStartedServer);
|
||||
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
|
||||
|
||||
if(type == GameType.headlessServer){
|
||||
if(widget.host){
|
||||
await _showServerLaunchingWarning();
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
_closeDialogIfOpen(false);
|
||||
showCorruptedBuildError(type != GameType.client, exception, stacktrace);
|
||||
_onStop(type);
|
||||
showCorruptedBuildError(widget.host, exception, stacktrace);
|
||||
_onStop(widget.host);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startGameProcesses(FortniteVersion version, GameType type, bool hasChildServer) async {
|
||||
Future<void> _startGameProcesses(FortniteVersion version, bool host, bool hasChildServer) async {
|
||||
_setStarted(host, true);
|
||||
var launcherProcess = await _createLauncherProcess(version);
|
||||
var eacProcess = await _createEacProcess(version);
|
||||
var gameProcess = await _createGameProcess(version.executable!.path, type);
|
||||
_gameController.gameInstancesMap[type] = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
|
||||
_injectOrShowError(Injectable.cranium, type);
|
||||
var gameProcess = await _createGameProcess(version.executable!.path, host);
|
||||
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
|
||||
if(host){
|
||||
_hostingController.instance = instance;
|
||||
}else{
|
||||
_gameController.instance = instance;
|
||||
}
|
||||
_injectOrShowError(Injectable.sslBypass, host);
|
||||
}
|
||||
|
||||
Future<bool> _startMatchMakingServer() async {
|
||||
if(_gameController.type() != GameType.client){
|
||||
if(widget.host){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -164,33 +171,32 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!_gameController.autostartGameServer()){
|
||||
return false;
|
||||
}
|
||||
|
||||
var version = _gameController.selectedVersion!;
|
||||
await _startGameProcesses(
|
||||
version,
|
||||
GameType.headlessServer,
|
||||
false
|
||||
);
|
||||
await _startGameProcesses(version, true, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<Process> _createGameProcess(String gamePath, GameType type) async {
|
||||
var gameArgs = createRebootArgs(_gameController.username.text, type, _gameController.customLaunchArgs.text);
|
||||
Future<Process> _createGameProcess(String gamePath, bool host) async {
|
||||
var gameArgs = createRebootArgs(_safeUsername, host, _gameController.customLaunchArgs.text);
|
||||
var gameProcess = await Process.start(gamePath, gameArgs);
|
||||
gameProcess
|
||||
..exitCode.then((_) => _onEnd(type))
|
||||
..outLines.forEach((line) => _onGameOutput(line, type))
|
||||
..errLines.forEach((line) => _onGameOutput(line, type));
|
||||
..exitCode.then((_) => _onEnd())
|
||||
..outLines.forEach((line) => _onGameOutput(line, host))
|
||||
..errLines.forEach((line) => _onGameOutput(line, host));
|
||||
return gameProcess;
|
||||
}
|
||||
|
||||
Future<void> _resetLogFile() async {
|
||||
if(_logFile != null && await _logFile!.exists()){
|
||||
await _logFile!.delete();
|
||||
String get _safeUsername {
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
return kDefaultPlayerName;
|
||||
}
|
||||
|
||||
var username = _gameController.username.text.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
|
||||
if(username.isEmpty){
|
||||
return kDefaultPlayerName;
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
|
||||
@@ -215,13 +221,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return eacProcess;
|
||||
}
|
||||
|
||||
void _onEnd(GameType type) {
|
||||
void _onEnd() {
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_closeDialogIfOpen(false);
|
||||
_onStop(type);
|
||||
_onStop(widget.host);
|
||||
}
|
||||
|
||||
void _closeDialogIfOpen(bool success) {
|
||||
@@ -238,7 +244,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => ProgressDialog(
|
||||
text: "Launching headless server...",
|
||||
onStop: () =>_onEnd(_gameController.type())
|
||||
onStop: () =>_onEnd()
|
||||
)
|
||||
) ?? false;
|
||||
|
||||
@@ -246,16 +252,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
_onStop(_gameController.type());
|
||||
_onStop(widget.host);
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, GameType type) {
|
||||
if(_logFile != null){
|
||||
_logFile!.writeAsString("$line\n", mode: FileMode.append);
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool host) {
|
||||
_logFile.createSync(recursive: true);
|
||||
_logFile.writeAsString("$line\n", mode: FileMode.append);
|
||||
if (line.contains(_shutdownLine)) {
|
||||
_onStop(type);
|
||||
_onStop(host);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -265,8 +269,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
showCorruptedBuildError(type != GameType.client);
|
||||
_onStop(type);
|
||||
showCorruptedBuildError(host);
|
||||
_onStop(host);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,71 +281,76 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_showTokenError(type);
|
||||
_showTokenError(host);
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Region ")){
|
||||
if(type == GameType.client){
|
||||
_injectOrShowError(Injectable.console, type);
|
||||
if(!host){
|
||||
_injectOrShowError(Injectable.console, host);
|
||||
}else {
|
||||
_injectOrShowError(Injectable.reboot, type)
|
||||
_injectOrShowError(Injectable.reboot, host)
|
||||
.then((value) => _closeDialogIfOpen(true));
|
||||
}
|
||||
|
||||
_injectOrShowError(Injectable.memoryFix, type);
|
||||
_gameController.currentGameInstance?.tokenError = false;
|
||||
_injectOrShowError(Injectable.memoryFix, host);
|
||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
||||
instance?.tokenError = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showTokenError(GameType type) async {
|
||||
Future<void> _showTokenError(bool host) async {
|
||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
||||
if(_serverController.type() != ServerType.embedded) {
|
||||
showTokenErrorUnfixable();
|
||||
_gameController.currentGameInstance?.tokenError = true;
|
||||
instance?.tokenError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenError = _gameController.currentGameInstance?.tokenError;
|
||||
_gameController.currentGameInstance?.tokenError = true;
|
||||
await _serverController.restart();
|
||||
var tokenError = instance?.tokenError;
|
||||
instance?.tokenError = true;
|
||||
await _serverController.restart(true);
|
||||
if (tokenError == true) {
|
||||
showTokenErrorCouldNotFix();
|
||||
return;
|
||||
}
|
||||
|
||||
showTokenErrorFixable();
|
||||
_onStop(type);
|
||||
_start(type);
|
||||
_onStop(host);
|
||||
_start();
|
||||
}
|
||||
|
||||
void _onStop(GameType? type) {
|
||||
if(type == null){
|
||||
return;
|
||||
void _onStop(bool host) async {
|
||||
if(_executor != null){
|
||||
await _executor;
|
||||
}
|
||||
|
||||
var value = _gameController.gameInstancesMap[type];
|
||||
if(value != null){
|
||||
if(value.hasChildServer){
|
||||
_onStop(GameType.headlessServer);
|
||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
||||
if(instance != null){
|
||||
if(instance.hasChildServer){
|
||||
_onStop(true);
|
||||
}
|
||||
|
||||
value.kill();
|
||||
_gameController.gameInstancesMap.remove(type);
|
||||
instance.kill();
|
||||
if(host){
|
||||
_hostingController.instance = null;
|
||||
}else {
|
||||
_gameController.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(type == _gameController.type()) {
|
||||
_gameController.started.value = false;
|
||||
}
|
||||
_setStarted(host, false);
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(Injectable injectable, GameType type) async {
|
||||
var gameProcess = _gameController.gameInstancesMap[type]?.gameProcess;
|
||||
if (gameProcess == null) {
|
||||
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
|
||||
var instance = hosting ? _hostingController.instance : _gameController.instance;
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var dllPath = await _getDllPath(injectable, type);
|
||||
var gameProcess = instance.gameProcess;
|
||||
var dllPath = await _getDllPath(injectable, hosting);
|
||||
if(dllPath == null) {
|
||||
return;
|
||||
}
|
||||
@@ -349,21 +358,21 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await injectDll(gameProcess.pid, dllPath.path);
|
||||
} catch (exception) {
|
||||
showMessage("Cannot inject $injectable.dll: $exception");
|
||||
_onStop(type);
|
||||
_onStop(hosting);
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllPath(Injectable injectable, GameType type) async {
|
||||
Future<File?> _getDllPath(Injectable injectable, bool hosting) async {
|
||||
Future<File> getPath(Injectable injectable) async {
|
||||
switch(injectable){
|
||||
case Injectable.reboot:
|
||||
return File(_settingsController.rebootDll.text);
|
||||
case Injectable.console:
|
||||
return File(_settingsController.consoleDll.text);
|
||||
case Injectable.cranium:
|
||||
case Injectable.sslBypass:
|
||||
return File(_settingsController.authDll.text);
|
||||
case Injectable.memoryFix:
|
||||
return await loadBinary("leakv2.dll", true);
|
||||
return File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,16 +381,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return dllPath;
|
||||
}
|
||||
|
||||
await _downloadMissingDll(injectable);
|
||||
if(dllPath.existsSync()) {
|
||||
return dllPath;
|
||||
}
|
||||
|
||||
_onDllFail(dllPath, type);
|
||||
_onDllFail(dllPath, hosting);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onDllFail(File dllPath, GameType type) {
|
||||
void _onDllFail(File dllPath, bool hosting) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(_fail){
|
||||
return;
|
||||
@@ -390,23 +394,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showMissingDllError(path.basename(dllPath.path));
|
||||
_onStop(type);
|
||||
_onStop(hosting);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _downloadMissingDll(Injectable injectable) async {
|
||||
if(injectable != Injectable.reboot){
|
||||
await loadBinary("$injectable.dll", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await downloadRebootDll(rebootDownloadUrl, 0);
|
||||
}
|
||||
}
|
||||
|
||||
enum Injectable {
|
||||
console,
|
||||
cranium,
|
||||
sslBypass,
|
||||
reboot,
|
||||
memoryFix
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
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/ui/controller/game_controller.dart';
|
||||
|
||||
class VersionNameInput extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
@@ -4,15 +4,15 @@ import 'dart:io';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.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/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/dialog/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import '../shared/file_selector.dart';
|
||||
|
||||
@@ -60,10 +60,9 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
.toList();
|
||||
|
||||
MenuFlyoutItem _createDefaultVersionItem() => MenuFlyoutItem(
|
||||
text: const SizedBox(
|
||||
width: double.infinity, child: Text("No versions available. Add it using the buttons on the right.")),
|
||||
trailing: const Expanded(child: SizedBox()),
|
||||
onPressed: () {});
|
||||
text: const Text("Please create or download a version"),
|
||||
onPressed: () {}
|
||||
);
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: _createOptionsMenu(
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
|
||||
class ServerButton extends StatefulWidget {
|
||||
@@ -26,7 +26,7 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
alignment: Alignment.center,
|
||||
child: Text(_buttonText),
|
||||
),
|
||||
onPressed: () => _serverController.toggle()
|
||||
onPressed: () => _serverController.toggle(false)
|
||||
),
|
||||
)),
|
||||
),
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
|
||||
class ServerTypeSelector extends StatelessWidget {
|
||||
@@ -4,7 +4,7 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/util/selector.dart';
|
||||
|
||||
@@ -16,6 +16,7 @@ class FileSelector extends StatefulWidget {
|
||||
final String? Function(String?) validator;
|
||||
final AutovalidateMode? validatorMode;
|
||||
final String? extension;
|
||||
final String? label;
|
||||
final bool folder;
|
||||
|
||||
const FileSelector(
|
||||
@@ -24,6 +25,7 @@ class FileSelector extends StatefulWidget {
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.folder,
|
||||
this.label,
|
||||
this.extension,
|
||||
this.validatorMode,
|
||||
this.allowNavigator = true,
|
||||
@@ -40,30 +42,35 @@ class _FileSelectorState extends State<FileSelector> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
return widget.label != null ? InfoLabel(
|
||||
label: widget.label!,
|
||||
child: _buildBody,
|
||||
) : _buildBody;
|
||||
}
|
||||
|
||||
Widget get _buildBody => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
controller: widget.controller,
|
||||
placeholder: widget.placeholder,
|
||||
validator: widget.validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
||||
)
|
||||
),
|
||||
if (widget.allowNavigator)
|
||||
const SizedBox(width: 16.0),
|
||||
if (widget.allowNavigator)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
if (widget.allowNavigator)
|
||||
const SizedBox(width: 16.0),
|
||||
if (widget.allowNavigator)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
void _onPressed() {
|
||||
if(_selecting){
|
||||
90
lib/src/ui/widget/shared/setting_tile.dart
Normal file
90
lib/src/ui/widget/shared/setting_tile.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/fluent_card.dart';
|
||||
|
||||
class SettingTile extends StatefulWidget {
|
||||
static const double kDefaultContentWidth = 200.0;
|
||||
static const double kDefaultSpacing = 8.0;
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget? content;
|
||||
final double? contentWidth;
|
||||
final List<Widget>? expandedContent;
|
||||
final double expandedContentSpacing;
|
||||
final bool isChild;
|
||||
|
||||
const SettingTile(
|
||||
{Key? key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.content,
|
||||
this.contentWidth = kDefaultContentWidth,
|
||||
this.expandedContentSpacing = kDefaultSpacing,
|
||||
this.expandedContent,
|
||||
this.isChild = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<SettingTile> createState() => _SettingTileState();
|
||||
}
|
||||
|
||||
class _SettingTileState extends State<SettingTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if(widget.expandedContent == null){
|
||||
return _contentCard;
|
||||
}
|
||||
|
||||
return Mica(
|
||||
elevation: 1,
|
||||
child: Expander(
|
||||
initiallyExpanded: true,
|
||||
contentBackgroundColor: FluentTheme.of(context).menuColor,
|
||||
headerShape: (open) => const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
|
||||
),
|
||||
header: _header,
|
||||
headerHeight: 72,
|
||||
trailing: _trailing,
|
||||
content: _content
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _content {
|
||||
var contents = widget.expandedContent!;
|
||||
var items = List.generate(contents.length * 2, (index) => index % 2 == 0 ? contents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing));
|
||||
return Column(
|
||||
children: items
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _trailing => SizedBox(
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
);
|
||||
|
||||
Widget get _header => ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle)
|
||||
);
|
||||
|
||||
Widget get _contentCard {
|
||||
if (widget.isChild) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: _contentCardBody
|
||||
);
|
||||
}
|
||||
|
||||
return FluentCard(
|
||||
child: _contentCardBody,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _contentCardBody => ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle),
|
||||
trailing: _trailing
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
import 'package:reboot_launcher/src/util/time.dart';
|
||||
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:unrar_file/unrar_file.dart';
|
||||
|
||||
import 'os.dart';
|
||||
|
||||
final _manifestSourceUrl = Uri.parse(
|
||||
"https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md");
|
||||
|
||||
final Uri _manifestSourceUrl = Uri.parse(
|
||||
"https://github.com/simplyblk/Fortnitebuilds/blob/main/README.md");
|
||||
|
||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
var response = await http.get(_manifestSourceUrl);
|
||||
@@ -19,45 +21,65 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
}
|
||||
|
||||
var document = parse(response.body);
|
||||
var table = document.querySelector("table");
|
||||
if (table == null) {
|
||||
throw Exception("Missing data table");
|
||||
}
|
||||
|
||||
var elements = document.getElementsByTagName("table")
|
||||
.map((element) => element.querySelector("tbody"))
|
||||
.expand((element) => element?.getElementsByTagName("tr") ?? [])
|
||||
.toList();
|
||||
var results = <FortniteBuild>[];
|
||||
for (var tableEntry in table.querySelectorAll("tbody > tr")) {
|
||||
for (var tableEntry in elements) {
|
||||
var children = tableEntry.querySelectorAll("td");
|
||||
|
||||
var name = children[0].text;
|
||||
var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-"));
|
||||
var version = parser
|
||||
.tryParse(minifiedName.replaceFirst("", ""));
|
||||
var version = parser.tryParse(children[0].text);
|
||||
if (version == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var link = children[2].firstChild!.attributes["href"]!;
|
||||
var link = children[3].firstChild?.attributes?["href"];
|
||||
if (link == null || link == "N/A") {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.add(FortniteBuild(version: version, link: link));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<Process> downloadManifestBuild(
|
||||
String manifestUrl, String destination, Function(double, String) onProgress) async {
|
||||
var log = await loadBinary("download.txt", true);
|
||||
await log.create();
|
||||
Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function() onRar) async {
|
||||
var outputDir = await destination.createTemp("build");
|
||||
try {
|
||||
destination.createSync(recursive: true);
|
||||
var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1);
|
||||
var extension = path.extension(fileName);
|
||||
var tempFile = File("${outputDir.path}//$fileName");
|
||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
var client = http.Client();
|
||||
var response = await client.send(
|
||||
http.Request("GET", Uri.parse(archiveUrl)));
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var buildExe = await loadBinary("build.exe", true);
|
||||
var process = await Process.start(buildExe.path, [manifestUrl, destination]);
|
||||
|
||||
log.writeAsString("Starting download of: $manifestUrl\n", mode: FileMode.append);
|
||||
process.errLines
|
||||
.where((message) => message.contains("%"))
|
||||
.forEach((message) {
|
||||
log.writeAsString("$message\n", mode: FileMode.append);
|
||||
onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1));
|
||||
});
|
||||
|
||||
return process;
|
||||
}
|
||||
var length = response.contentLength!;
|
||||
var received = 0;
|
||||
var sink = tempFile.openWrite();
|
||||
await response.stream.map((s) {
|
||||
received += s.length;
|
||||
var now = DateTime.now();
|
||||
var eta = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch;
|
||||
onProgress((received / length) * 100, toETA(eta));
|
||||
return s;
|
||||
}).pipe(sink);
|
||||
onRar();
|
||||
if(extension.toLowerCase() == ".zip"){
|
||||
await extractFileToDisk(tempFile.path, destination.path);
|
||||
}else if(extension.toLowerCase() == ".rar") {
|
||||
await UnrarFile.extract_rar(tempFile.path, destination.path);
|
||||
} else {
|
||||
throw Exception("Unknown file extension: $extension");
|
||||
}
|
||||
} catch(message) {
|
||||
throw Exception("Cannot download build: $message");
|
||||
}finally {
|
||||
outputDir.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../page/home_page.dart';
|
||||
import '../dialog/dialog.dart';
|
||||
import '../../../main.dart';
|
||||
import '../ui/dialog/dialog.dart';
|
||||
|
||||
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
if(exception == null){
|
||||
@@ -13,7 +12,7 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
@@ -21,5 +20,5 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => framework ? "An error was thrown by Flutter: $exception" : "An uncaught error was thrown: $exception"
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
@@ -19,32 +19,6 @@ bool get isWin11 {
|
||||
return intBuild != null && intBuild > 22000;
|
||||
}
|
||||
|
||||
Future<File> loadBinary(String binary, bool safe) async{
|
||||
var safeBinary = File("${safeBinariesDirectory.path}\\$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) =>
|
||||
File("${internalAssetsDirectory.path}\\binaries\\$binary");
|
||||
|
||||
Future<void> resetWinNat() async {
|
||||
var binary = await loadBinary("winnat.bat", true);
|
||||
await runElevated(binary.path, "");
|
||||
}
|
||||
|
||||
Future<bool> runElevated(String executable, String args) async {
|
||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||
@@ -57,57 +31,30 @@ Future<bool> runElevated(String executable, String args) async {
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
Directory get internalAssetsDirectory =>
|
||||
Directory("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets");
|
||||
Directory get installationDirectory =>
|
||||
File(Platform.resolvedExecutable).parent;
|
||||
|
||||
Directory get logsDirectory =>
|
||||
Directory("${installationDirectory.path}\\logs");
|
||||
|
||||
Directory get assetsDirectory =>
|
||||
Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
|
||||
Directory get tempDirectory =>
|
||||
Directory("${Platform.environment["Temp"]}");
|
||||
Directory(Platform.environment["Temp"]!);
|
||||
|
||||
Directory get safeBinariesDirectory =>
|
||||
Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher");
|
||||
|
||||
Directory get embeddedBackendDirectory =>
|
||||
Directory("${safeBinariesDirectory.path}\\backend-lawin");
|
||||
|
||||
File loadEmbedded(String file) {
|
||||
var safeBinary = File("${embeddedBackendDirectory.path}\\$file");
|
||||
if(safeBinary.existsSync()){
|
||||
return safeBinary;
|
||||
Future<bool> delete(FileSystemEntity file) async {
|
||||
try {
|
||||
await file.delete(recursive: true);
|
||||
return true;
|
||||
}catch(_){
|
||||
return Future.delayed(const Duration(seconds: 5)).then((value) async {
|
||||
try {
|
||||
await file.delete(recursive: true);
|
||||
return true;
|
||||
}catch(_){
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
safeBinary.parent.createSync(recursive: true);
|
||||
var internal = File("${internalAssetsDirectory.path}\\$file");
|
||||
if(internal.existsSync()) {
|
||||
internal.copySync(safeBinary.path);
|
||||
}
|
||||
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
Directory loadEmbeddedDirectory(String directory) {
|
||||
var safeBinary = Directory("${embeddedBackendDirectory.path}\\$directory");
|
||||
safeBinary.parent.createSync(recursive: true);
|
||||
var internal = Directory("${internalAssetsDirectory.path}\\$directory");
|
||||
_copyFolder(internal, safeBinary);
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
void _copyFolder(Directory dir1, Directory dir2) {
|
||||
if(!dir1.existsSync()){
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dir2.existsSync()) {
|
||||
dir2.createSync(recursive: true);
|
||||
}
|
||||
|
||||
dir1.listSync().forEach((element) {
|
||||
var newPath = "${dir2.path}/${path.basename(element.path)}";
|
||||
if (element is File) {
|
||||
var newFile = File(newPath);
|
||||
newFile.writeAsBytesSync(element.readAsBytesSync());
|
||||
} else if (element is Directory) {
|
||||
_copyFolder(element, Directory(newPath));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,53 +4,40 @@ import 'package:archive/archive_io.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/model/reboot_download.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
const String rebootDownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/main/Release.zip";
|
||||
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
|
||||
|
||||
Future<RebootDownload> downloadRebootDll(String url, int? lastUpdateMs) async {
|
||||
Directory? outputDir;
|
||||
File? tempZip;
|
||||
try {
|
||||
var now = DateTime.now();
|
||||
var oldRebootDll = await loadBinary("reboot.dll", true);
|
||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
var exists = await oldRebootDll.exists();
|
||||
if (lastUpdate != null &&
|
||||
now.difference(lastUpdate).inHours <= 24 &&
|
||||
await oldRebootDll.exists()) {
|
||||
return RebootDownload(lastUpdateMs!);
|
||||
}
|
||||
|
||||
var response = await http.get(Uri.parse(rebootDownloadUrl));
|
||||
var tempZip = await loadBinary("reboot.zip", true);
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
|
||||
var outputDir = await safeBinariesDirectory.createTemp("reboot_out");
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
|
||||
var rebootDll = File(outputDir
|
||||
.listSync()
|
||||
.firstWhere((element) => path.extension(element.path) == ".dll")
|
||||
.path);
|
||||
|
||||
if (!exists ||
|
||||
sha1.convert(await oldRebootDll.readAsBytes()) !=
|
||||
sha1.convert(await rebootDll.readAsBytes())) {
|
||||
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
||||
}
|
||||
|
||||
return RebootDownload(now.millisecondsSinceEpoch);
|
||||
} catch (error, stackTrace) {
|
||||
return RebootDownload(-1, error, stackTrace);
|
||||
} finally {
|
||||
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
|
||||
Directory? outputDir;
|
||||
try {
|
||||
outputDir?.delete(recursive: true);
|
||||
tempZip?.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
var now = DateTime.now();
|
||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
var exists = await rebootDllFile.exists();
|
||||
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) {
|
||||
return lastUpdateMs!;
|
||||
}
|
||||
|
||||
var response = await http.get(Uri.parse(rebootDownloadUrl));
|
||||
outputDir = await installationDirectory.createTemp("reboot_out");
|
||||
var tempZip = File("${outputDir.path}\\reboot.zip");
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
var rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
|
||||
if (!exists || sha1.convert(await rebootDllFile.readAsBytes()) != sha1.convert(await rebootDll.readAsBytes())) {
|
||||
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes());
|
||||
}
|
||||
|
||||
return now.millisecondsSinceEpoch;
|
||||
}catch(message) {
|
||||
throw Exception("Cannot download reboot.zip, invalid zip: $message");
|
||||
}finally{
|
||||
if(outputDir != null) {
|
||||
delete(outputDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:ini/ini.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
|
||||
final serverLogFile = File("${logsDirectory.path}\\server.log");
|
||||
final serverDirectory = Directory("${assetsDirectory.path}\\lawin");
|
||||
final serverExeFile = File("${serverDirectory.path}\\lawinserver-win.exe");
|
||||
|
||||
Future<void> writeMatchmakingIp(String text) async {
|
||||
var file = File("${embeddedBackendDirectory.path}\\Config\\config.ini");
|
||||
var file = File("${assetsDirectory.path}\\lawin\\Config\\config.ini");
|
||||
if(!file.existsSync()){
|
||||
return;
|
||||
}
|
||||
@@ -29,24 +31,25 @@ Future<void> writeMatchmakingIp(String text) async {
|
||||
file.writeAsStringSync(config.toString());
|
||||
}
|
||||
|
||||
Future<void> startServer() async {
|
||||
if(!embeddedBackendDirectory.existsSync()){
|
||||
var serverZip = await loadBinary("server.zip", true);
|
||||
await extractFileToDisk(serverZip.path, embeddedBackendDirectory.path);
|
||||
}
|
||||
|
||||
Future<void> startServer(bool detached) async {
|
||||
var process = await Process.start(
|
||||
"${embeddedBackendDirectory.path}\\lawinserver-win.exe",
|
||||
serverExeFile.path,
|
||||
[],
|
||||
workingDirectory: embeddedBackendDirectory.path
|
||||
workingDirectory: serverDirectory.path,
|
||||
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
|
||||
);
|
||||
process.outLines.forEach((element) => serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
||||
process.errLines.forEach((element) => serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
||||
if(!detached) {
|
||||
serverLogFile.createSync(recursive: true);
|
||||
process.outLines.forEach((element) =>
|
||||
serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
||||
process.errLines.forEach((element) =>
|
||||
serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopServer() async {
|
||||
var releaseBat = await loadBinary("kill_both_ports.bat", false);
|
||||
await Process.run(releaseBat.path, []);
|
||||
await freeLawinPort();
|
||||
await freeMatchmakerPort();
|
||||
}
|
||||
|
||||
Future<bool> isLawinPortFree() async {
|
||||
@@ -64,24 +67,23 @@ Future<bool> isMatchmakerPortFree() async {
|
||||
}
|
||||
|
||||
Future<void> freeLawinPort() async {
|
||||
var releaseBat = await loadBinary("kill_lawin_port.bat", false);
|
||||
var result = await Process.run(releaseBat.path, []);
|
||||
if(result.exitCode == 1){
|
||||
await runElevated(releaseBat.path, "");
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_lawin.bat");
|
||||
await Process.run(releaseBat.path, []);
|
||||
}
|
||||
|
||||
Future<void> freeMatchmakerPort() async {
|
||||
var releaseBat = await loadBinary("kill_matchmaker_port.bat", false);
|
||||
var result = await Process.run(releaseBat.path, []);
|
||||
if(result.exitCode == 1){
|
||||
await runElevated(releaseBat.path, "");
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_matchmaker.bat");
|
||||
await Process.run(releaseBat.path, []);
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, GameType type, String additionalArgs) {
|
||||
Future<void> resetWinNat() async {
|
||||
var binary = File("${serverDirectory.path}\\winnat.bat");
|
||||
await runElevated(binary.path, "");
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, bool host, String additionalArgs) {
|
||||
username = username.isEmpty ? kDefaultPlayerName : username;
|
||||
username = host ? "$username${Random().nextInt(1000)}" : username;
|
||||
var args = [
|
||||
"-epicapp=Fortnite",
|
||||
"-epicenv=Prod",
|
||||
@@ -91,18 +93,13 @@ List<String> createRebootArgs(String username, GameType type, String additionalA
|
||||
"-nobe",
|
||||
"-fromfl=eac",
|
||||
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ"
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||
"-AUTH_LOGIN=$username@projectreboot.dev",
|
||||
"-AUTH_PASSWORD=Rebooted",
|
||||
"-AUTH_TYPE=epic"
|
||||
];
|
||||
|
||||
if(username.isNotEmpty){
|
||||
args.addAll([
|
||||
"-AUTH_LOGIN=${username.replaceAll(RegExp("[^A-Za-z0-9]"), "")}@projectreboot.dev",
|
||||
"-AUTH_PASSWORD=Rebooted",
|
||||
"-AUTH_TYPE=epic"
|
||||
]);
|
||||
}
|
||||
|
||||
if(type == GameType.headlessServer){
|
||||
if(host){
|
||||
args.addAll([
|
||||
"-nullrhi",
|
||||
"-nosplash",
|
||||
@@ -205,5 +202,5 @@ enum ServerResultType {
|
||||
canStart,
|
||||
alreadyStarted,
|
||||
unknownError,
|
||||
stopped,
|
||||
stopped
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
String toETA(int milliseconds){
|
||||
var duration = Duration(milliseconds: milliseconds);
|
||||
String toETA(double milliseconds){
|
||||
var roundedMilliseconds = milliseconds.toInt();
|
||||
var duration = Duration(milliseconds: roundedMilliseconds);
|
||||
return "${duration.inHours.toString().padLeft(2, "0")}:"
|
||||
"${duration.inMinutes.remainder(60).toString().padLeft(2, "0")}:"
|
||||
"${duration.inSeconds.remainder(60).toString().padLeft(2, "0")}";
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
|
||||
|
||||
class GameTypeSelector extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
GameTypeSelector({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => DropDownButton(
|
||||
leading: Text(_gameController.type.value.name),
|
||||
items: GameType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()
|
||||
));
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createItem(GameType type) => MenuFlyoutItem(
|
||||
text: Text(type.name),
|
||||
onPressed: () {
|
||||
_gameController.type(type);
|
||||
_gameController.started.value = _gameController.currentGameInstance != null;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
|
||||
|
||||
class HostInput extends StatelessWidget {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
HostInput({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The hostname of the backend server",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Host",
|
||||
placeholder: "Type the backend server's hostname",
|
||||
controller: _serverController.host,
|
||||
enabled: _serverController.type.value == ServerType.remote
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
|
||||
|
||||
|
||||
class PortInput extends StatelessWidget {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
PortInput({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The port of the backend server",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Port",
|
||||
placeholder: "Type the backend server's port",
|
||||
controller: _serverController.port,
|
||||
enabled: _serverController.type.value == ServerType.remote
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/util/selector.dart';
|
||||
|
||||
class RebootUpdaterInput extends StatefulWidget {
|
||||
const RebootUpdaterInput({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RebootUpdaterInput> createState() => _RebootUpdaterInputState();
|
||||
}
|
||||
|
||||
class _RebootUpdaterInputState extends State<RebootUpdaterInput> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final RxBool _valid = RxBool(true);
|
||||
late String? Function(String?) validator;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
validator = (value) {
|
||||
var result = value != null && Uri.tryParse(value) != null;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _valid.value = result);
|
||||
return result ? null : "Invalid URL";
|
||||
};
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoLabel(
|
||||
label: "Reboot Updater",
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() => Expanded(
|
||||
child: TextFormBox(
|
||||
controller: _settingsController.updateUrl,
|
||||
placeholder: "Type the URL of the reboot updater",
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
enabled: _settingsController.autoUpdate.value
|
||||
)
|
||||
)),
|
||||
const SizedBox(width: 16.0),
|
||||
Tooltip(
|
||||
message: _settingsController.autoUpdate.value ? "Disable automatic updates" : "Enable automatic updates",
|
||||
child: Obx(() => Button(
|
||||
onPressed: () => _settingsController.autoUpdate.value = !_settingsController.autoUpdate.value,
|
||||
child: Icon(_settingsController.autoUpdate.value ? FluentIcons.disable_updates : FluentIcons.refresh)
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/fluent_card.dart';
|
||||
|
||||
class SettingTile extends StatefulWidget {
|
||||
static const double kDefaultContentWidth = 200.0;
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget? content;
|
||||
final double? contentWidth;
|
||||
final List<Widget>? expandedContent;
|
||||
|
||||
const SettingTile(
|
||||
{Key? key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.content,
|
||||
this.contentWidth = kDefaultContentWidth,
|
||||
this.expandedContent})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<SettingTile> createState() => _SettingTileState();
|
||||
}
|
||||
|
||||
class _SettingTileState extends State<SettingTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if(widget.expandedContent == null){
|
||||
return _contentCard;
|
||||
}
|
||||
|
||||
return Mica(
|
||||
elevation: 1,
|
||||
child: Expander(
|
||||
initiallyExpanded: true,
|
||||
contentBackgroundColor: FluentTheme.of(context).menuColor,
|
||||
headerShape: (open) => const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
|
||||
),
|
||||
header: ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle)
|
||||
),
|
||||
headerHeight: 72,
|
||||
trailing: SizedBox(
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
),
|
||||
content: Column(
|
||||
children: widget.expandedContent!
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _contentCard => FluentCard(
|
||||
child: ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle),
|
||||
trailing: SizedBox(
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
|
||||
class SmartSwitch extends StatefulWidget {
|
||||
final String? label;
|
||||
final bool enabled;
|
||||
final Function()? onDisabledPress;
|
||||
final Rx<bool> value;
|
||||
|
||||
const SmartSwitch(
|
||||
{Key? key,
|
||||
required this.value,
|
||||
this.label,
|
||||
this.enabled = true,
|
||||
this.onDisabledPress})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<SmartSwitch> createState() => _SmartSwitchState();
|
||||
}
|
||||
|
||||
class _SmartSwitchState extends State<SmartSwitch> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.label == null ? _createSwitch() : _createLabel();
|
||||
}
|
||||
|
||||
InfoLabel _createLabel() {
|
||||
return InfoLabel(
|
||||
label: widget.label!,
|
||||
child: _createSwitch()
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createSwitch() {
|
||||
return Obx(() => ToggleSwitch(
|
||||
checked: widget.value.value,
|
||||
onChanged: _onChanged
|
||||
)
|
||||
);
|
||||
}
|
||||
void _onChanged(bool checked) {
|
||||
if (!widget.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => widget.value.value = checked);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user