mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
Compare commits
1 Commits
_onLoggedI
...
9.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a775e2f3f |
@@ -1,4 +0,0 @@
|
||||
# Backend
|
||||
Fork of LawinV1
|
||||
Awaiting rewrite in Dart
|
||||
Use build.bat to generate the executable
|
||||
@@ -51,7 +51,7 @@ void main(List<String> args) async {
|
||||
}
|
||||
|
||||
stdout.writeln("Launching game...");
|
||||
var executable = version.shippingExecutable;
|
||||
var executable = version.gameExecutable;
|
||||
if(executable == null){
|
||||
throw Exception("Missing game executable at: ${version.location.path}");
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ Future<void> startGame() async {
|
||||
await _startLauncherProcess(version);
|
||||
await _startEacProcess(version);
|
||||
|
||||
var executable = await version.shippingExecutable;
|
||||
var executable = await version.gameExecutable;
|
||||
if (executable == null) {
|
||||
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Future<bool> startServerCli(String? host, int? port, ServerType type) async {
|
||||
stdout.writeln("Starting backend server...");
|
||||
switch(type){
|
||||
case ServerType.local:
|
||||
final result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
|
||||
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
|
||||
if(result == null){
|
||||
throw Exception("Local backend server is not running");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ export 'package:reboot_common/src/model/server_result.dart';
|
||||
export 'package:reboot_common/src/model/server_type.dart';
|
||||
export 'package:reboot_common/src/model/update_status.dart';
|
||||
export 'package:reboot_common/src/model/update_timer.dart';
|
||||
export 'package:reboot_common/src/model/fortnite_server.dart';
|
||||
export 'package:reboot_common/src/model/dll.dart';
|
||||
export 'package:reboot_common/src/util/backend.dart';
|
||||
export 'package:reboot_common/src/util/build.dart';
|
||||
@@ -18,5 +17,4 @@ export 'package:reboot_common/src/util/dll.dart';
|
||||
export 'package:reboot_common/src/util/network.dart';
|
||||
export 'package:reboot_common/src/util/patcher.dart';
|
||||
export 'package:reboot_common/src/util/path.dart';
|
||||
export 'package:reboot_common/src/util/process.dart';
|
||||
export 'package:reboot_common/src/util/log.dart';
|
||||
export 'package:reboot_common/src/util/process.dart';
|
||||
@@ -1,3 +1,2 @@
|
||||
const String kDefaultBackendHost = "127.0.0.1";
|
||||
const int kDefaultBackendPort = 3551;
|
||||
const int kDefaultXmppPort = 80;
|
||||
const int kDefaultBackendPort = 3551;
|
||||
@@ -1,5 +1,4 @@
|
||||
const String kDefaultPlayerName = "Player";
|
||||
const String kDefaultHostName = "Host";
|
||||
const String kDefaultGameServerHost = "127.0.0.1";
|
||||
const String kDefaultGameServerPort = "7777";
|
||||
const String kInitializedLine = "Game Engine Initialized";
|
||||
@@ -12,7 +11,7 @@ const List<String> kCorruptedBuildErrors = [
|
||||
"Critical error",
|
||||
"when 0 bytes remain",
|
||||
"Pak chunk signature verification failed!",
|
||||
"LogWindows:Error: Fatal error!"
|
||||
"Couldn't find pak signature file"
|
||||
];
|
||||
const List<String> kCannotConnectErrors = [
|
||||
"port 3551 failed: Connection refused",
|
||||
|
||||
@@ -5,50 +5,38 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
extension FortniteVersionExtension on FortniteVersion {
|
||||
static String _marker = "FortniteClient.mod";
|
||||
|
||||
static File? findFile(Directory directory, String name) {
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
try{
|
||||
for(final child in directory.listSync()) {
|
||||
if(child is Directory) {
|
||||
if(!path.basename(child.path).startsWith("\.")) {
|
||||
final result = findFile(child, name);
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}else if(child is File) {
|
||||
if(path.basename(child.path) == name) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
final result = directory.listSync(recursive: true)
|
||||
.firstWhere((element) => path.basename(element.path) == name);
|
||||
return File(result.path);
|
||||
}catch(_){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> get shippingExecutable async {
|
||||
final result = findFile(location, "FortniteClient-Win64-Shipping.exe");
|
||||
if(result == null) {
|
||||
return null;
|
||||
}
|
||||
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
|
||||
final marker = findFile(location, _marker);
|
||||
if(marker != null) {
|
||||
Future<File?> get headlessGameExecutable async {
|
||||
final result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
await Isolate.run(() => patchHeadless(result));
|
||||
await File("${location.path}\\$_marker").create();
|
||||
return result;
|
||||
final original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
if(original == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
|
||||
await original.copy(output.path);
|
||||
await Isolate.run(() => patchHeadless(output));
|
||||
return output;
|
||||
}
|
||||
|
||||
File? get launcherExecutable => findFile(location, "FortniteLauncher.exe");
|
||||
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
|
||||
|
||||
File? get eacExecutable => findFile(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
|
||||
File? get splashBitmap => findFile(location, "Splash.bmp");
|
||||
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
enum InjectableDll {
|
||||
console,
|
||||
starfall,
|
||||
cobalt,
|
||||
reboot,
|
||||
}
|
||||
|
||||
extension InjectableDllVersionAware on InjectableDll {
|
||||
bool get isVersionDependent => this == InjectableDll.reboot;
|
||||
memory
|
||||
}
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class FortniteBuild {
|
||||
final Version version;
|
||||
final String identifier;
|
||||
final String version;
|
||||
final String link;
|
||||
final bool available;
|
||||
|
||||
FortniteBuild({
|
||||
required this.identifier,
|
||||
required this.version,
|
||||
required this.link,
|
||||
required this.available
|
||||
required this.link
|
||||
});
|
||||
}
|
||||
|
||||
class FortniteBuildDownloadProgress {
|
||||
final double progress;
|
||||
final int? timeLeft;
|
||||
final int? minutesLeft;
|
||||
final bool extracting;
|
||||
final int speed;
|
||||
|
||||
FortniteBuildDownloadProgress({
|
||||
required this.progress,
|
||||
required this.extracting,
|
||||
required this.timeLeft,
|
||||
required this.speed
|
||||
this.minutesLeft,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
class FortniteServer {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String author;
|
||||
final String ip;
|
||||
final String version;
|
||||
final String? password;
|
||||
final DateTime timestamp;
|
||||
final bool discoverable;
|
||||
|
||||
FortniteServer({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.author,
|
||||
required this.ip,
|
||||
required this.version,
|
||||
required this.password,
|
||||
required this.timestamp,
|
||||
required this.discoverable
|
||||
});
|
||||
|
||||
factory FortniteServer.fromJson(json) => FortniteServer(
|
||||
id: json["id"],
|
||||
name: json["name"],
|
||||
description: json["description"],
|
||||
author: json["author"],
|
||||
ip: json["ip"],
|
||||
version: json["version"],
|
||||
password: json["password"],
|
||||
timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now(),
|
||||
discoverable: json["discoverable"] ?? false
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"author": author,
|
||||
"ip": ip,
|
||||
"version": version,
|
||||
"password": password,
|
||||
"timestamp": timestamp.toString(),
|
||||
"discoverable": discoverable
|
||||
};
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class FortniteVersion {
|
||||
Version content;
|
||||
String name;
|
||||
Directory location;
|
||||
|
||||
FortniteVersion.fromJson(json)
|
||||
: content = Version.parse(json["content"]),
|
||||
: name = json["name"],
|
||||
location = Directory(json["location"]);
|
||||
|
||||
FortniteVersion({required this.content, required this.location});
|
||||
FortniteVersion({required this.name, required this.location});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'content': content.toString(),
|
||||
'name': name,
|
||||
'location': location.path
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is FortniteVersion && this.content == other.content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,30 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
|
||||
class GameInstance {
|
||||
final Version version;
|
||||
final String versionName;
|
||||
final int gamePid;
|
||||
final int? launcherPid;
|
||||
final int? eacPid;
|
||||
final List<InjectableDll> injectedDlls;
|
||||
final GameServerType? serverType;
|
||||
bool hosting;
|
||||
bool launched;
|
||||
bool movedToVirtualDesktop;
|
||||
bool tokenError;
|
||||
bool killed;
|
||||
GameInstance? child;
|
||||
|
||||
GameInstance({
|
||||
required this.version,
|
||||
required this.versionName,
|
||||
required this.gamePid,
|
||||
required this.launcherPid,
|
||||
required this.eacPid,
|
||||
required this.serverType,
|
||||
required this.hosting,
|
||||
required this.child
|
||||
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
||||
}): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
||||
|
||||
void kill() {
|
||||
GameInstance? child = this;
|
||||
while(child != null) {
|
||||
child._kill();
|
||||
child = child.child;
|
||||
}
|
||||
}
|
||||
|
||||
void _kill() {
|
||||
launched = true;
|
||||
killed = true;
|
||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||
if(launcherPid != null) {
|
||||
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
|
||||
@@ -45,10 +33,17 @@ class GameInstance {
|
||||
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GameServerType {
|
||||
headless,
|
||||
virtualWindow,
|
||||
window
|
||||
bool get nestedHosting {
|
||||
GameInstance? child = this;
|
||||
while(child != null) {
|
||||
if(child.hosting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
child = child.child;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,6 @@ class ServerResult {
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
ServerResult(this.type, {this.error, this.stackTrace});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ServerResult{type: $type, error: $error, stackTrace: $stackTrace}';
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerResultType {
|
||||
|
||||
@@ -15,19 +15,10 @@ final Semaphore _semaphore = Semaphore();
|
||||
String? _lastIp;
|
||||
String? _lastPort;
|
||||
|
||||
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
|
||||
final process = await startProcess(
|
||||
Future<Process> startEmbeddedBackend(bool detached) async => startProcess(
|
||||
executable: backendStartExecutable,
|
||||
window: detached,
|
||||
);
|
||||
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
|
||||
process.stdError.listen((error) {
|
||||
log("[BACKEND] Error: $error");
|
||||
onError?.call(error);
|
||||
});
|
||||
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
|
||||
return process;
|
||||
}
|
||||
);
|
||||
|
||||
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
|
||||
|
||||
@@ -35,7 +26,6 @@ Future<bool> isBackendPortFree() async => await pingBackend(kDefaultBackendHost,
|
||||
|
||||
Future<bool> freeBackendPort() async {
|
||||
await killProcessByPort(kDefaultBackendPort);
|
||||
await killProcessByPort(kDefaultXmppPort);
|
||||
final standardResult = await isBackendPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
@@ -45,24 +35,21 @@ Future<bool> freeBackendPort() async {
|
||||
}
|
||||
|
||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||
final hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
var hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
try{
|
||||
final uri = Uri(
|
||||
var uri = Uri(
|
||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||
host: hostName,
|
||||
port: port,
|
||||
path: "unknown"
|
||||
);
|
||||
log("[BACKEND] Pinging $uri...");
|
||||
final client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 10);
|
||||
final request = await client.getUrl(uri);
|
||||
await request.close().timeout(const Duration(seconds: 10));
|
||||
log("[BACKEND] Ping successful");
|
||||
return uri;
|
||||
}catch(error){
|
||||
log("[BACKEND] Cannot ping backend: $error");
|
||||
var client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 5);
|
||||
var request = await client.getUrl(uri);
|
||||
var response = await request.close();
|
||||
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
|
||||
}catch(_){
|
||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
||||
}
|
||||
}
|
||||
@@ -72,16 +59,16 @@ Stream<String?> watchMatchmakingIp() async* {
|
||||
return;
|
||||
}
|
||||
|
||||
final observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
|
||||
var observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
|
||||
yield* observer.where((event) => event.path == matchmakerConfigFile.path).asyncMap((event) async {
|
||||
try {
|
||||
final config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
final ip = config.get("GameServer", "ip");
|
||||
var config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
var ip = config.get("GameServer", "ip");
|
||||
if(ip == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final port = config.get("GameServer", "port");
|
||||
var port = config.get("GameServer", "port");
|
||||
if(port == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -102,14 +89,14 @@ Stream<String?> watchMatchmakingIp() async* {
|
||||
}
|
||||
|
||||
Future<void> writeMatchmakingIp(String text) async {
|
||||
final exists = await matchmakerConfigFile.exists();
|
||||
var exists = await matchmakerConfigFile.exists();
|
||||
if(!exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
_semaphore.acquire();
|
||||
final splitIndex = text.indexOf(":");
|
||||
final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
||||
var splitIndex = text.indexOf(":");
|
||||
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
||||
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
|
||||
if(port.isBlank) {
|
||||
port = kDefaultGameServerPort;
|
||||
@@ -117,7 +104,7 @@ Future<void> writeMatchmakingIp(String text) async {
|
||||
|
||||
_lastIp = ip;
|
||||
_lastPort = port;
|
||||
final config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
var config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||
config.set("GameServer", "ip", ip);
|
||||
config.set("GameServer", "port", port);
|
||||
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
||||
|
||||
@@ -3,243 +3,136 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_common/src/extension/types.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
const String kStopBuildDownloadSignal = "kill";
|
||||
|
||||
final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json");
|
||||
final int _ariaPort = 6800;
|
||||
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
|
||||
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
|
||||
final String _ariaSecret = "RebootLauncher";
|
||||
final Dio _dio = _buildDioInstance();
|
||||
Dio _buildDioInstance() {
|
||||
final dio = Dio();
|
||||
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter;
|
||||
httpClientAdapter.createHttpClient = () {
|
||||
final client = HttpClient();
|
||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||
return client;
|
||||
};
|
||||
return dio;
|
||||
}
|
||||
|
||||
final String _archiveSourceUrl = "http://185.203.216.3/versions.json";
|
||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
|
||||
const String _unavailableError = "The build downloader is not available right now";
|
||||
const String _genericError = "The build downloader is not working correctly";
|
||||
const int _maxErrors = 100;
|
||||
|
||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
final response = await http.get(_archiveSourceUrl);
|
||||
final response = await _dio.get<String>(
|
||||
_archiveSourceUrl,
|
||||
options: Options(
|
||||
responseType: ResponseType.plain
|
||||
)
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return jsonDecode(response.body)
|
||||
.map((entry) {
|
||||
try {
|
||||
final fileUrl = entry as String;
|
||||
final fileName = Uri.parse(fileUrl).pathSegments.last;
|
||||
final fileNameWithoutExtension = path.basenameWithoutExtension(fileName);
|
||||
return FortniteBuild(
|
||||
version: Version.parse(fileNameWithoutExtension),
|
||||
link: entry,
|
||||
available: true
|
||||
);
|
||||
}catch(_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<FortniteBuild>()
|
||||
.toList();
|
||||
final data = jsonDecode(response.data ?? "{}");
|
||||
var results = <FortniteBuild>[];
|
||||
for(final entry in data.entries) {
|
||||
results.add(FortniteBuild(
|
||||
identifier: entry.key,
|
||||
version: "${entry.value["title"]} (${entry.key})",
|
||||
link: entry.value["url"]
|
||||
));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
||||
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||
final outputFile = File("${options.destination.path}\\.build\\$fileName");
|
||||
try {
|
||||
final stopped = _setupLifecycle(options);
|
||||
await outputFile.parent.create(recursive: true);
|
||||
|
||||
final downloadItemCompleter = Completer<File>();
|
||||
|
||||
await _startAriaServer();
|
||||
final downloadId = await _startAriaDownload(options, outputFile);
|
||||
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
|
||||
try {
|
||||
final statusRequestId = Uuid().toString().replaceAll("-", "");
|
||||
final statusRequest = {
|
||||
"jsonrcp": "2.0",
|
||||
"id": statusRequestId,
|
||||
"method": "aria2.tellStatus",
|
||||
"params": [
|
||||
"token:${_ariaSecret}",
|
||||
downloadId
|
||||
]
|
||||
};
|
||||
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
|
||||
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
|
||||
if(statusResponseJson == null) {
|
||||
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final result = statusResponseJson["result"];
|
||||
final files = result["files"] as List?;
|
||||
if(files == null || files.isEmpty) {
|
||||
downloadItemCompleter.completeError("Download aborted");
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final error = result["errorCode"];
|
||||
if(error != null) {
|
||||
final errorCode = int.tryParse(error);
|
||||
if(errorCode == 0) {
|
||||
final path = File(files[0]["path"]);
|
||||
downloadItemCompleter.complete(path);
|
||||
}else if(errorCode == 3) {
|
||||
downloadItemCompleter.completeError("This build is not available yet");
|
||||
}else {
|
||||
final errorMessage = result["errorMessage"];
|
||||
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
|
||||
}
|
||||
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final speed = int.parse(result["downloadSpeed"] ?? "0");
|
||||
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
|
||||
final totalLength = int.parse(files[0]["length"] ?? "0");
|
||||
|
||||
final percentage = completedLength * 100 / totalLength;
|
||||
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
|
||||
_onProgress(
|
||||
options.port,
|
||||
percentage,
|
||||
speed,
|
||||
minutesLeft,
|
||||
false
|
||||
);
|
||||
}catch(error) {
|
||||
throw "Invalid download status (${error})";
|
||||
}
|
||||
});
|
||||
|
||||
await Future.any([stopped.future, downloadItemCompleter.future]);
|
||||
if(!stopped.isCompleted) {
|
||||
final extension = path.extension(fileName);
|
||||
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
|
||||
}else {
|
||||
await _stopAriaDownload(downloadId);
|
||||
final outputDir = Directory("${options.destination.path}\\.build");
|
||||
await outputDir.create(recursive: true);
|
||||
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||
final extension = path.extension(fileName);
|
||||
final tempFile = File("${outputDir.path}\\$fileName");
|
||||
if(await tempFile.exists()) {
|
||||
await tempFile.delete(recursive: true);
|
||||
}
|
||||
|
||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final response = _downloadArchive(options, tempFile, startTime);
|
||||
await Future.any([stopped.future, response]);
|
||||
if(!stopped.isCompleted) {
|
||||
await _extractArchive(stopped, extension, tempFile, options);
|
||||
}
|
||||
|
||||
delete(outputDir);
|
||||
}catch(error) {
|
||||
_onError(error, options);
|
||||
}finally {
|
||||
delete(outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startAriaServer() async {
|
||||
final running = await _isAriaRunning();
|
||||
if(running) {
|
||||
await killProcessByPort(_ariaPort);
|
||||
}
|
||||
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
|
||||
var received = byteStart ?? 0;
|
||||
try {
|
||||
await _dio.download(
|
||||
options.build.link,
|
||||
tempFile.path,
|
||||
onReceiveProgress: (data, length) {
|
||||
received = data;
|
||||
final percentage = (received / length) * 100;
|
||||
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options);
|
||||
},
|
||||
deleteOnError: false,
|
||||
options: Options(
|
||||
validateStatus: (statusCode) {
|
||||
if(statusCode == 200) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
|
||||
if(!aria2c.existsSync()) {
|
||||
throw "Missing aria2c.exe";
|
||||
}
|
||||
if(statusCode == 403 || statusCode == 503) {
|
||||
throw _deniedConnectionError;
|
||||
}
|
||||
|
||||
await startProcess(
|
||||
executable: aria2c,
|
||||
args: [
|
||||
"--max-connection-per-server=${Platform.numberOfProcessors}",
|
||||
"--split=${Platform.numberOfProcessors}",
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all=true",
|
||||
"--rpc-allow-origin-all",
|
||||
"--rpc-secret=$_ariaSecret",
|
||||
"--rpc-listen-port=$_ariaPort"
|
||||
],
|
||||
window: false
|
||||
);
|
||||
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
|
||||
if(await _isAriaRunning()) {
|
||||
if(statusCode == 404) {
|
||||
throw _unavailableError;
|
||||
}
|
||||
|
||||
throw _genericError;
|
||||
},
|
||||
headers: byteStart == null || byteStart <= 0 ? {
|
||||
"Cookie": "_c_t_c=1"
|
||||
} : {
|
||||
"Cookie": "_c_t_c=1",
|
||||
"Range": "bytes=${byteStart}-"
|
||||
},
|
||||
)
|
||||
);
|
||||
}catch(error) {
|
||||
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
|
||||
_onError(error, options);
|
||||
return;
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
throw "cannot start download server (timeout exceeded)";
|
||||
}
|
||||
|
||||
Future<bool> _isAriaRunning() async {
|
||||
try {
|
||||
final statusRequestId = Uuid().toString().replaceAll("-", "");
|
||||
final statusRequest = {
|
||||
"jsonrcp": "2.0",
|
||||
"id": statusRequestId,
|
||||
"method": "aria2.getVersion",
|
||||
"params": [
|
||||
"token:${_ariaSecret}"
|
||||
]
|
||||
};
|
||||
await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
|
||||
return true;
|
||||
}catch(_) {
|
||||
return false;
|
||||
await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
|
||||
http.Response? addDownloadResponse;
|
||||
try {
|
||||
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
|
||||
final addDownloadRequest = {
|
||||
"jsonrcp": "2.0",
|
||||
"id": addDownloadRequestId,
|
||||
"method": "aria2.addUri",
|
||||
"params": [
|
||||
"token:${_ariaSecret}",
|
||||
[options.build.link],
|
||||
{
|
||||
"dir": outputFile.parent.path,
|
||||
"out": path.basename(outputFile.path)
|
||||
}
|
||||
]
|
||||
};
|
||||
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
|
||||
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
|
||||
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
|
||||
if(downloadId == null) {
|
||||
throw "Start failed (${addDownloadResponse.body})";
|
||||
}
|
||||
|
||||
return downloadId;
|
||||
}catch(error) {
|
||||
throw "Start failed (${addDownloadResponse?.body ?? error})";
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopAriaDownload(String downloadId) async {
|
||||
try {
|
||||
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
|
||||
final addDownloadRequest = {
|
||||
"jsonrcp": "2.0",
|
||||
"id": addDownloadRequestId,
|
||||
"method": "aria2.forceRemove",
|
||||
"params": [
|
||||
"token:${_ariaSecret}",
|
||||
downloadId
|
||||
]
|
||||
};
|
||||
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
|
||||
}catch(error) {
|
||||
throw "Stop failed (${error})";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
|
||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
Process? process;
|
||||
switch (extension.toLowerCase()) {
|
||||
case ".zip":
|
||||
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
|
||||
if(!sevenZip.existsSync()) {
|
||||
throw "Missing 7zip.exe";
|
||||
throw "Corrupted installation: missing 7zip.exe";
|
||||
}
|
||||
|
||||
process = await startProcess(
|
||||
@@ -254,15 +147,10 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
);
|
||||
var completed = false;
|
||||
process.stdOutput.listen((data) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if(data.toLowerCase().contains("everything is ok")) {
|
||||
completed = true;
|
||||
_onProgress(
|
||||
options.port,
|
||||
100,
|
||||
0,
|
||||
-1,
|
||||
true
|
||||
);
|
||||
_onProgress(startTime, now, 100, true, options);
|
||||
process?.kill(ProcessSignal.sigabrt);
|
||||
return;
|
||||
}
|
||||
@@ -273,13 +161,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
}
|
||||
|
||||
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
|
||||
_onProgress(
|
||||
options.port,
|
||||
percentage,
|
||||
0,
|
||||
-1,
|
||||
true
|
||||
);
|
||||
_onProgress(startTime, now, percentage, true, options);
|
||||
});
|
||||
process.stdError.listen((data) {
|
||||
if(!data.isBlank) {
|
||||
@@ -295,7 +177,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
case ".rar":
|
||||
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
||||
if(!winrar.existsSync()) {
|
||||
throw "Missing winrar.exe";
|
||||
throw "Corrupted installation: missing winrar.exe";
|
||||
}
|
||||
|
||||
process = await startProcess(
|
||||
@@ -310,16 +192,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
);
|
||||
var completed = false;
|
||||
process.stdOutput.listen((data) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
||||
if(data == "All OK") {
|
||||
completed = true;
|
||||
_onProgress(
|
||||
options.port,
|
||||
100,
|
||||
0,
|
||||
-1,
|
||||
true
|
||||
);
|
||||
_onProgress(startTime, now, 100, true, options);
|
||||
process?.kill(ProcessSignal.sigabrt);
|
||||
return;
|
||||
}
|
||||
@@ -330,13 +207,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
}
|
||||
|
||||
final percentage = int.parse(element).toDouble();
|
||||
_onProgress(
|
||||
options.port,
|
||||
percentage,
|
||||
0,
|
||||
-1,
|
||||
true
|
||||
);
|
||||
_onProgress(startTime, now, percentage, true, options);
|
||||
});
|
||||
process.stdError.listen((data) {
|
||||
if(!data.isBlank) {
|
||||
@@ -354,25 +225,23 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
}
|
||||
|
||||
await Future.any([stopped.future, process.exitCode]);
|
||||
process.kill(ProcessSignal.sigabrt);
|
||||
}
|
||||
|
||||
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
|
||||
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
||||
if(percentage == 0) {
|
||||
port.send(FortniteBuildDownloadProgress(
|
||||
options.port.send(FortniteBuildDownloadProgress(
|
||||
progress: percentage,
|
||||
extracting: extracting,
|
||||
timeLeft: null,
|
||||
speed: speed
|
||||
extracting: extracting
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
port.send(FortniteBuildDownloadProgress(
|
||||
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now;
|
||||
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
|
||||
options.port.send(FortniteBuildDownloadProgress(
|
||||
progress: percentage,
|
||||
extracting: extracting,
|
||||
timeLeft: minutesLeft,
|
||||
speed: speed
|
||||
minutesLeft: minutesLeft
|
||||
));
|
||||
}
|
||||
|
||||
@@ -392,5 +261,4 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
||||
});
|
||||
options.port.send(lifecyclePort.sendPort);
|
||||
return stopped;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,21 +6,19 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
bool _watcher = false;
|
||||
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
|
||||
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
|
||||
const String kRebootBelowS20DownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
|
||||
const String kRebootAboveS20DownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
|
||||
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
|
||||
const String kRebootDownloadUrl =
|
||||
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
|
||||
|
||||
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
||||
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
|
||||
final exists = await rebootDllFile.exists();
|
||||
final now = DateTime.now();
|
||||
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
||||
}
|
||||
|
||||
Future<void> downloadCriticalDll(String name, String outputPath) async {
|
||||
print("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name");
|
||||
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
|
||||
if(response.statusCode != 200) {
|
||||
throw Exception("Cannot download $name: status code ${response.statusCode}");
|
||||
@@ -31,8 +29,9 @@ Future<void> downloadCriticalDll(String name, String outputPath) async {
|
||||
await output.writeAsBytes(response.bodyBytes, flush: true);
|
||||
}
|
||||
|
||||
Future<void> downloadRebootDll(File file, String url) async {
|
||||
Future<int> downloadRebootDll(String url) async {
|
||||
Directory? outputDir;
|
||||
final now = DateTime.now();
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if(response.statusCode != 200) {
|
||||
@@ -44,7 +43,8 @@ Future<void> downloadRebootDll(File file, String url) async {
|
||||
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
|
||||
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
|
||||
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
} finally{
|
||||
if(outputDir != null) {
|
||||
delete(outputDir);
|
||||
@@ -64,7 +64,7 @@ Stream<String> watchDlls() async* {
|
||||
}
|
||||
|
||||
_watcher = true;
|
||||
await for(final event in dllsDirectory.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
|
||||
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
|
||||
if (event.path.endsWith(".dll")) {
|
||||
yield event.path;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ final Uint8List _patchedHeadless = Uint8List.fromList([
|
||||
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
|
||||
]);
|
||||
|
||||
// Not used right now
|
||||
final Uint8List _originalMatchmaking = Uint8List.fromList([
|
||||
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
|
||||
]);
|
||||
@@ -19,7 +18,7 @@ final Uint8List _patchedMatchmaking = Uint8List.fromList([
|
||||
]);
|
||||
|
||||
Future<bool> patchHeadless(File file) async =>
|
||||
await _patch(file, _originalHeadless, _patchedHeadless);
|
||||
_patch(file, _originalHeadless, _patchedHeadless);
|
||||
|
||||
Future<bool> patchMatchmaking(File file) async =>
|
||||
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
|
||||
@@ -30,24 +29,22 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
||||
throw Exception("Cannot mutate length of binary file");
|
||||
}
|
||||
|
||||
final source = await file.readAsBytes();
|
||||
final read = await file.readAsBytes();
|
||||
final length = await file.length();
|
||||
var readOffset = 0;
|
||||
var patchOffset = -1;
|
||||
var patchCount = 0;
|
||||
while(readOffset < source.length){
|
||||
if(source[readOffset] == original[patchCount]){
|
||||
while(readOffset < length){
|
||||
if(read[readOffset] == original[patchCount]){
|
||||
if(patchOffset == -1) {
|
||||
patchOffset = readOffset;
|
||||
}
|
||||
|
||||
if(readOffset - patchOffset + 1 == original.length) {
|
||||
if(++patchCount == original.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
patchCount++;
|
||||
}else {
|
||||
patchOffset = -1;
|
||||
patchCount = 0;
|
||||
}
|
||||
|
||||
readOffset++;
|
||||
@@ -58,10 +55,10 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
||||
}
|
||||
|
||||
for(var i = 0; i < patched.length; i++) {
|
||||
source[patchOffset + i] = patched[i];
|
||||
read[patchOffset + i] = patched[i];
|
||||
}
|
||||
|
||||
await file.writeAsBytes(source, flush: true);
|
||||
await file.writeAsBytes(read, flush: true);
|
||||
return true;
|
||||
}catch(_){
|
||||
return false;
|
||||
|
||||
@@ -6,7 +6,7 @@ Directory get installationDirectory =>
|
||||
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
|
||||
|
||||
Directory get assetsDirectory {
|
||||
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
if(directory.existsSync()) {
|
||||
return directory;
|
||||
}
|
||||
@@ -14,6 +14,9 @@ Directory get assetsDirectory {
|
||||
return installationDirectory;
|
||||
}
|
||||
|
||||
Directory get logsDirectory =>
|
||||
Directory("${installationDirectory.path}\\logs");
|
||||
|
||||
Directory get settingsDirectory =>
|
||||
Directory("${installationDirectory.path}\\settings");
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
||||
@@ -95,53 +96,61 @@ Future<bool> startElevatedProcess({required String executable, required String a
|
||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
|
||||
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
|
||||
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
|
||||
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||
return ShellExecuteEx(shellInput) == 1;
|
||||
var shellResult = ShellExecuteEx(shellInput);
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
|
||||
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
|
||||
Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async {
|
||||
final argsOrEmpty = args ?? [];
|
||||
final workingDirectory = _getWorkingDirectory(executable);
|
||||
if(useTempBatch) {
|
||||
if(wrapProcess) {
|
||||
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
|
||||
final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
|
||||
final tempScriptFile = File("${tempScriptDirectory.path}/process.bat");
|
||||
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
|
||||
await tempScriptFile.writeAsString(command, flush: true);
|
||||
final process = await Process.start(
|
||||
tempScriptFile.path,
|
||||
[],
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
workingDirectory: executable.parent.path,
|
||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||
runInShell: window
|
||||
);
|
||||
return _ExtendedProcess(process, true);
|
||||
return _withLogger(name, executable, process, window);
|
||||
}
|
||||
|
||||
final process = await Process.start(
|
||||
executable.path,
|
||||
args ?? [],
|
||||
workingDirectory: workingDirectory,
|
||||
workingDirectory: executable.parent.path,
|
||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||
runInShell: window
|
||||
);
|
||||
return _ExtendedProcess(process, true);
|
||||
return _withLogger(name, executable, process, window);
|
||||
}
|
||||
|
||||
String? _getWorkingDirectory(File executable) {
|
||||
try {
|
||||
log("[PROCESS] Calculating working directory for $executable");
|
||||
final workingDirectory = executable.parent.resolveSymbolicLinksSync();
|
||||
log("[PROCESS] Using working directory: $workingDirectory");
|
||||
return workingDirectory;
|
||||
}catch(error) {
|
||||
log("[PROCESS] Cannot infer working directory: $error");
|
||||
return null;
|
||||
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) {
|
||||
final extendedProcess = _ExtendedProcess(process, true);
|
||||
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log");
|
||||
loggingFile.parent.createSync(recursive: true);
|
||||
if(loggingFile.existsSync()) {
|
||||
loggingFile.deleteSync();
|
||||
}
|
||||
|
||||
final semaphore = Semaphore(1);
|
||||
void logEvent(String event) async {
|
||||
await semaphore.acquire();
|
||||
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
|
||||
semaphore.release();
|
||||
}
|
||||
extendedProcess.stdOutput.listen(logEvent);
|
||||
extendedProcess.stdError.listen(logEvent);
|
||||
if(!window) {
|
||||
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
|
||||
}
|
||||
return extendedProcess;
|
||||
}
|
||||
|
||||
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
||||
@@ -151,92 +160,82 @@ final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
||||
int Function(int hWnd)>('NtSuspendProcess');
|
||||
|
||||
bool suspend(int pid) {
|
||||
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||
try {
|
||||
return _NtSuspendProcess(processHandle) == 0;
|
||||
} finally {
|
||||
CloseHandle(processHandle);
|
||||
}
|
||||
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||
final result = _NtSuspendProcess(processHandle);
|
||||
CloseHandle(processHandle);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
bool resume(int pid) {
|
||||
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||
try {
|
||||
return _NtResumeProcess(processHandle) == 0;
|
||||
} finally {
|
||||
CloseHandle(processHandle);
|
||||
}
|
||||
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||
final result = _NtResumeProcess(processHandle);
|
||||
CloseHandle(processHandle);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
|
||||
Future<void> watchProcess(int pid) => Isolate.run(() {
|
||||
final processHandle = OpenProcess(FILE_ACCESS_RIGHTS.SYNCHRONIZE, FALSE, pid);
|
||||
if (processHandle == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void _watchProcess(int pid) {
|
||||
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
try {
|
||||
WaitForSingleObject(processHandle, INFINITE);
|
||||
}finally {
|
||||
CloseHandle(processHandle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
|
||||
log("[PROCESS] Generating reboot args");
|
||||
Future<bool> watchProcess(int pid) async {
|
||||
var completer = Completer<bool>();
|
||||
var exitPort = ReceivePort();
|
||||
exitPort.listen((_) {
|
||||
if(!completer.isCompleted) {
|
||||
completer.complete(true);
|
||||
}
|
||||
});
|
||||
var errorPort = ReceivePort();
|
||||
errorPort.listen((_) => completer.complete(false));
|
||||
await Isolate.spawn(
|
||||
_watchProcess,
|
||||
pid,
|
||||
onExit: exitPort.sendPort,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, String password, bool host, bool headless, String additionalArgs) {
|
||||
if(password.isEmpty) {
|
||||
username = '${_parseUsername(username, host)}@projectreboot.dev';
|
||||
}
|
||||
|
||||
password = password.isNotEmpty ? password : "Rebooted";
|
||||
final args = LinkedHashMap<String, String>(
|
||||
equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
|
||||
hashCode: (a) => a.toUpperCase().hashCode
|
||||
);
|
||||
args.addAll({
|
||||
"-epicapp": "Fortnite",
|
||||
"-epicenv": "Prod",
|
||||
"-epiclocale": "en-us",
|
||||
"-epicportal": "",
|
||||
"-skippatchcheck": "",
|
||||
"-nobe": "",
|
||||
"-fromfl": "eac",
|
||||
"-fltoken": "3db3ba5dcbd2e16703f3978d",
|
||||
"-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||
"-AUTH_LOGIN": username,
|
||||
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
|
||||
"-AUTH_TYPE": "epic"
|
||||
});
|
||||
final args = [
|
||||
"-epicapp=Fortnite",
|
||||
"-epicenv=Prod",
|
||||
"-epiclocale=en-us",
|
||||
"-epicportal",
|
||||
"-skippatchcheck",
|
||||
"-nobe",
|
||||
"-fromfl=eac",
|
||||
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||
"-AUTH_LOGIN=$username",
|
||||
"-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}",
|
||||
"-AUTH_TYPE=epic"
|
||||
];
|
||||
|
||||
if(logging) {
|
||||
args["-log"] = "";
|
||||
if(host && headless){
|
||||
args.addAll([
|
||||
"-nullrhi",
|
||||
"-nosplash",
|
||||
"-nosound",
|
||||
]);
|
||||
}
|
||||
|
||||
if(host) {
|
||||
args["-nosplash"] = "";
|
||||
args["-nosound"] = "";
|
||||
if(hostType == GameServerType.headless){
|
||||
args["-nullrhi"] = "";
|
||||
}
|
||||
if(additionalArgs.isNotEmpty){
|
||||
args.addAll(additionalArgs.split(" "));
|
||||
}
|
||||
|
||||
log("[PROCESS] Default args: $args");
|
||||
log("[PROCESS] Adding custom args: $additionalArgs");
|
||||
for(final additionalArg in additionalArgs.split(" ")) {
|
||||
log("[PROCESS] Processing custom arg: $additionalArg");
|
||||
final separatorIndex = additionalArg.indexOf("=");
|
||||
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
|
||||
log("[PROCESS] Custom arg key: $argName");
|
||||
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
|
||||
log("[PROCESS] Custom arg value: $argValue");
|
||||
args[argName] = argValue;
|
||||
log("[PROCESS] Updated args: $args");
|
||||
}
|
||||
|
||||
log("[PROCESS] Final args result: $args");
|
||||
return args.entries
|
||||
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
|
||||
.toList();
|
||||
return args;
|
||||
}
|
||||
|
||||
void handleGameOutput({
|
||||
@@ -250,22 +249,16 @@ void handleGameOutput({
|
||||
required void Function() onBuildCorrupted,
|
||||
}) {
|
||||
if (line.contains(kShutdownLine)) {
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
|
||||
onShutdown();
|
||||
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
|
||||
onBuildCorrupted();
|
||||
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
|
||||
onTokenError();
|
||||
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
|
||||
onLoggedIn();
|
||||
}else if(line.contains(kGameFinishedLine) && host) {
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
|
||||
onMatchEnd();
|
||||
}else if(line.contains(kDisplayInitializedLine) && host) {
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
|
||||
onDisplayAttached();
|
||||
}
|
||||
}
|
||||
@@ -298,14 +291,7 @@ final class _ExtendedProcess implements Process {
|
||||
|
||||
|
||||
@override
|
||||
Future<int> get exitCode {
|
||||
try {
|
||||
return _delegate.exitCode;
|
||||
}catch(_) {
|
||||
return watchProcess(_delegate.pid)
|
||||
.then((_) => -1);
|
||||
}
|
||||
}
|
||||
Future<int> get exitCode => _delegate.exitCode;
|
||||
|
||||
@override
|
||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
||||
|
||||
@@ -7,18 +7,18 @@ environment:
|
||||
sdk: ">=3.0.0 <=4.0.0"
|
||||
|
||||
dependencies:
|
||||
win32: ^5.5.4
|
||||
ffi: ^2.1.3
|
||||
path: ^1.9.0
|
||||
http: ^1.2.2
|
||||
crypto: ^3.0.5
|
||||
archive: ^3.6.1
|
||||
dio: ^5.3.2
|
||||
win32: 3.0.0
|
||||
ffi: ^2.1.0
|
||||
path: ^1.8.3
|
||||
http: ^1.1.0
|
||||
crypto: ^3.0.2
|
||||
archive: ^3.3.7
|
||||
ini: ^2.1.0
|
||||
shelf_proxy: ^1.0.2
|
||||
sync: ^0.3.0
|
||||
uuid: ^4.5.1
|
||||
uuid: ^3.0.6
|
||||
shelf_web_socket: ^2.0.0
|
||||
version: ^3.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^2.0.1
|
||||
@@ -1,37 +0,0 @@
|
||||
# How can I make my server accessible to other players?
|
||||
|
||||
### 1. Set a static IP
|
||||
|
||||
Set a static IP on the PC hosting the game server and copy it for later:
|
||||
|
||||
- [Windows 11](https://pureinfotech.com/set-static-ip-address-windows-11/)
|
||||
- [Windows 10](https://pureinfotech.com/set-static-ip-address-windows-10/)
|
||||
|
||||
|
||||
### 2. Log into Your Router
|
||||
|
||||
You'll need to access your router's web interface at 192.168.1.1.
|
||||
You might need a username and a password to log in: refer to your router's manual for precise instructions.
|
||||
|
||||
### 3. Find the Port Forwarding Section
|
||||
|
||||
Once logged in, navigate to the port forwarding section of your router's settings.
|
||||
This location may vary from router to router, but it's typically labelled as "Port Forwarding," "Port Mapping," or "Virtual Server."
|
||||
Refer to your router's manual for precise instructions.
|
||||
|
||||
### 4. Add a Port Forwarding Rule
|
||||
|
||||
Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify:
|
||||
|
||||
- **Service Name:** Choose a name for your port forwarding rule (e.g., "Fortnite Game Server").
|
||||
- **Port Number:** Enter 7777 for both the external and internal ports.
|
||||
- **Protocol:** Select the UDP protocol.
|
||||
- **Internal IP Address:** Enter the static IP address you set earlier.
|
||||
- **Enable:** Make sure the port forwarding rule is enabled.
|
||||
|
||||
### 5. Save and Apply the Changes
|
||||
|
||||
After configuring the port forwarding rule, save your changes and apply them.
|
||||
This step may involve clicking a "Save" or "Apply" button on your router's web interface.
|
||||
|
||||
### 6. Try hosting a game!
|
||||
@@ -1,39 +1,16 @@
|
||||
# reboot_launcher
|
||||
|
||||
# Reboot Launcher
|
||||
|
||||
Welcome to the **Reboot Launcher**!
|
||||
This is a GUI application developed as part of the **Reboot Project**.
|
||||
Launcher for project reboot
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Running the Project
|
||||
To launch the project in development mode, simply run:
|
||||
```
|
||||
flutter run
|
||||
```
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
### Building the Project
|
||||
To create a production-ready build, use:
|
||||
```
|
||||
flutter build
|
||||
```
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
### Packaging the Project
|
||||
To package the application for distribution, run:
|
||||
```
|
||||
package.bat
|
||||
```
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
## Requirements
|
||||
- [Flutter SDK](https://flutter.dev/docs/get-started/install)
|
||||
- Supported operating systems: Windows
|
||||
|
||||
## Other platforms
|
||||
|
||||
Native support for these platforms is not currently planned, but Linux support is a priority for the 10.0 release cycle
|
||||
|
||||
- [Linux Tutorial using Proton](https://www.reddit.com/r/linux_gaming/comments/1fwa4l8/guide_running_a_fortnite_private_server_to_play/)
|
||||
- No tutorials are available for MacOS(got lost when the Reboot discord was banned), but it's possible to run Reboot using a compatibility layer
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome! Feel free to open an issue or submit a pull request.
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
2
gui/assets/build/stop.bat
Normal file
2
gui/assets/build/stop.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
taskkill /f /im winrar.exe
|
||||
taskkill /f /im tar.exe
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 522 B |
3
gui/assets/info/en/1. What is Project Reboot
Normal file
3
gui/assets/info/en/1. What is Project Reboot
Normal file
@@ -0,0 +1,3 @@
|
||||
Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.
|
||||
The project was started on Discord by Milxnor, while the launcher is developed by Auties00.
|
||||
Both are open source on GitHub, anyone can easily contribute or audit the code!"
|
||||
1
gui/assets/info/en/10. Corrupted build
Normal file
1
gui/assets/info/en/10. Corrupted build
Normal file
@@ -0,0 +1 @@
|
||||
Your version of Fortnite is corrupted, download it again from the launcher, or use another build.
|
||||
@@ -0,0 +1,3 @@
|
||||
Support for LawinV2 is available in the launcher.
|
||||
To use the backend, select a local or remote backend from the "Backend" tab in the launcher.
|
||||
To use the credentials, click on the avatar on the top left of the launcher and enter your email and password
|
||||
7
gui/assets/info/en/2. What is a Fortnite game server
Normal file
7
gui/assets/info/en/2. What is a Fortnite game server
Normal file
@@ -0,0 +1,7 @@
|
||||
If you have ever played Minecraft multiplayer, you might know that the servers you join are hosted on a computer running a program, called Minecraft Game Server.
|
||||
While the Minecraft Game server is written by the creators of Minecraft, Mojang, Epic Games doesn't provide an equivalent for Fortnite.
|
||||
By exploiting the Fortnite internals, though, it's possible to create a game server just like in Minecraft: this is in easy terms what Project Reboot does.
|
||||
Some Fortnite versions support running this game server in the background without rendering the game("headless"), while others still require the full game to be open.
|
||||
Just like in Minecraft, you need a game client to play the game and one to host the server.
|
||||
By default, a game server is automatically started on your PC when you start a Fortnite version from the "Play" section in the launcher.
|
||||
If you want to play in another way, for example by joining a server hosted by one of your friends instead of running one yourself, you can checkout the "Multiplayer" section in the "Play" tab of the launcher.
|
||||
4
gui/assets/info/en/3. Types of Fortnite game server
Normal file
4
gui/assets/info/en/3. Types of Fortnite game server
Normal file
@@ -0,0 +1,4 @@
|
||||
Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called "headless" as the game is running, but you can't see it on your screen.
|
||||
If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the "Configuration" section in the "Host" tab of the launcher, you will see an instance of Fortnite open on your screen.
|
||||
For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the "Configuration" section in the "Host" tab of the launcher.
|
||||
Just like in Minecraft, you need a game client to play the game and one to host the server.
|
||||
22
gui/assets/info/en/4. How can others join my game server
Normal file
22
gui/assets/info/en/4. How can others join my game server
Normal file
@@ -0,0 +1,22 @@
|
||||
For others to join your game server, port 7777 must be accessible on your PC.
|
||||
One option is to use a private VPN service like Hamachi or Radmin, but all of the players will need to download this software.
|
||||
The best solution is to use port forwarding:
|
||||
1. Set a static IP
|
||||
If you don't have already a static IP set, set one by following any tutorial on Google
|
||||
2. Log into your router's admin panel
|
||||
Usually this can be accessed on any web browser by going to http://192.168.1.1/
|
||||
You might need a username and a password to log in: refer to your router's manual for precise instructions
|
||||
3. Find the port forwarding section
|
||||
Once logged in into the admin panel, navigate to the port forwarding section of your router's settings
|
||||
This location may vary from router to router, but it's typically labelled as "Port Forwarding," "Port Mapping" or "Virtual Server"
|
||||
Refer to your router's manual for precise instructions
|
||||
4. Add a port forwarding rule
|
||||
Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify:
|
||||
- Service Name: Choose a name for your port forwarding rule (e.g., "Fortnite Game Server")
|
||||
- Port Number: Enter 7777 for both the external and internal ports
|
||||
- Protocol: Select the UDP protocol
|
||||
- Internal IP Address: Enter the static IP address you set earlier
|
||||
- Enable: Make sure the port forwarding rule is enabled
|
||||
5. Save and apply the changes
|
||||
After configuring the port forwarding rule, save your changes and apply them
|
||||
This step may involve clicking a "Save" or "Apply" button on your router's web interface
|
||||
6
gui/assets/info/en/5. What is a backend
Normal file
6
gui/assets/info/en/5. What is a backend
Normal file
@@ -0,0 +1,6 @@
|
||||
A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.
|
||||
By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.
|
||||
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue.
|
||||
LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend.
|
||||
Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user.
|
||||
You can run these alternatives either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.
|
||||
4
gui/assets/info/en/6. What is the Unreal Engine console
Normal file
4
gui/assets/info/en/6. What is the Unreal Engine console
Normal file
@@ -0,0 +1,4 @@
|
||||
Many Fortnite versions don't support entering in game by clicking the \"Play\" button.
|
||||
Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1
|
||||
Keep in mind that the Unreal Engine console key is controlled by the backend, so this is true only if you are using the embedded backend: custom backends might use different keys.
|
||||
When using the embedded backend, you can customize the key used to open the console in the \"Backend\" tab of the Reboot Launcher.
|
||||
@@ -0,0 +1,4 @@
|
||||
To resolve this issue:
|
||||
- Check that your backend is working correctly from the "Backend" tab
|
||||
- If you are using a custom backend, try to use the embedded one
|
||||
- Try to run the backend as detached by enabling the "Detached" option in the "Backend" tab
|
||||
@@ -0,0 +1,2 @@
|
||||
As explained in the "What is a Fortnite game server?" section, one instance of Fortnite is used to host the game server, while the other is used to let you play.
|
||||
The Fortnite instance used up by the game server is usually frozen, so it should be hard to use the wrong one to try to play.
|
||||
@@ -0,0 +1,2 @@
|
||||
As explained in the "What is the Unreal Engine console?" section, the "Play" button doesn't work in many Fortnite versions.
|
||||
Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1
|
||||
735
gui/dependencies/InnoDependencyInstaller/CodeDependencies.iss
Normal file
735
gui/dependencies/InnoDependencyInstaller/CodeDependencies.iss
Normal file
@@ -0,0 +1,735 @@
|
||||
[Code]
|
||||
// https://github.com/DomGries/InnoDependencyInstaller
|
||||
|
||||
// types and variables
|
||||
type
|
||||
TDependency_Entry = record
|
||||
Filename: String;
|
||||
Parameters: String;
|
||||
Title: String;
|
||||
URL: String;
|
||||
Checksum: String;
|
||||
ForceSuccess: Boolean;
|
||||
RestartAfter: Boolean;
|
||||
end;
|
||||
|
||||
var
|
||||
Dependency_Memo: String;
|
||||
Dependency_List: array of TDependency_Entry;
|
||||
Dependency_NeedToRestart, Dependency_ForceX86: Boolean;
|
||||
Dependency_DownloadPage: TDownloadWizardPage;
|
||||
|
||||
procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean);
|
||||
var
|
||||
Dependency: TDependency_Entry;
|
||||
DependencyCount: Integer;
|
||||
begin
|
||||
Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title;
|
||||
|
||||
Dependency.Filename := Filename;
|
||||
Dependency.Parameters := Parameters;
|
||||
Dependency.Title := Title;
|
||||
|
||||
if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin
|
||||
Dependency.URL := '';
|
||||
end else begin
|
||||
Dependency.URL := URL;
|
||||
end;
|
||||
|
||||
Dependency.Checksum := Checksum;
|
||||
Dependency.ForceSuccess := ForceSuccess;
|
||||
Dependency.RestartAfter := RestartAfter;
|
||||
|
||||
DependencyCount := GetArrayLength(Dependency_List);
|
||||
SetArrayLength(Dependency_List, DependencyCount + 1);
|
||||
Dependency_List[DependencyCount] := Dependency;
|
||||
end;
|
||||
|
||||
<event('InitializeWizard')>
|
||||
procedure Dependency_InitializeWizard;
|
||||
begin
|
||||
Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
|
||||
end;
|
||||
|
||||
<event('PrepareToInstall')>
|
||||
function Dependency_PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
DependencyCount, DependencyIndex, ResultCode: Integer;
|
||||
Retry: Boolean;
|
||||
TempValue: String;
|
||||
begin
|
||||
DependencyCount := GetArrayLength(Dependency_List);
|
||||
|
||||
if DependencyCount > 0 then begin
|
||||
Dependency_DownloadPage.Show;
|
||||
|
||||
for DependencyIndex := 0 to DependencyCount - 1 do begin
|
||||
if Dependency_List[DependencyIndex].URL <> '' then begin
|
||||
Dependency_DownloadPage.Clear;
|
||||
Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum);
|
||||
|
||||
Retry := True;
|
||||
while Retry do begin
|
||||
Retry := False;
|
||||
|
||||
try
|
||||
Dependency_DownloadPage.Download;
|
||||
except
|
||||
if Dependency_DownloadPage.AbortedByUser then begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
DependencyIndex := DependencyCount;
|
||||
end else begin
|
||||
case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
|
||||
IDABORT: begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
DependencyIndex := DependencyCount;
|
||||
end;
|
||||
IDRETRY: begin
|
||||
Retry := True;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Result = '' then begin
|
||||
for DependencyIndex := 0 to DependencyCount - 1 do begin
|
||||
Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, '');
|
||||
Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1);
|
||||
|
||||
while True do begin
|
||||
ResultCode := 0;
|
||||
#ifdef Dependency_CustomExecute
|
||||
if {#Dependency_CustomExecute}(ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, ResultCode) then begin
|
||||
#else
|
||||
if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin
|
||||
#endif
|
||||
if Dependency_List[DependencyIndex].RestartAfter then begin
|
||||
if DependencyIndex = DependencyCount - 1 then begin
|
||||
Dependency_NeedToRestart := True;
|
||||
end else begin
|
||||
NeedsRestart := True;
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
end;
|
||||
break;
|
||||
end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0)
|
||||
break;
|
||||
end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641)
|
||||
NeedsRestart := True;
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
break;
|
||||
end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010)
|
||||
Dependency_NeedToRestart := True;
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
|
||||
case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependency_List[DependencyIndex].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
|
||||
IDABORT: begin
|
||||
Result := Dependency_List[DependencyIndex].Title;
|
||||
break;
|
||||
end;
|
||||
IDIGNORE: begin
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
if Result <> '' then begin
|
||||
break;
|
||||
end;
|
||||
end;
|
||||
|
||||
if NeedsRestart then begin
|
||||
TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"';
|
||||
if WizardNoIcons then begin
|
||||
TempValue := TempValue + ' /NOICONS';
|
||||
end;
|
||||
RegWriteStringValue(HKA, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', '{#SetupSetting("AppName")}', TempValue);
|
||||
end;
|
||||
end;
|
||||
|
||||
Dependency_DownloadPage.Hide;
|
||||
end;
|
||||
end;
|
||||
|
||||
#ifndef Dependency_NoUpdateReadyMemo
|
||||
<event('UpdateReadyMemo')>
|
||||
#endif
|
||||
function Dependency_UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
|
||||
begin
|
||||
Result := '';
|
||||
if MemoUserInfoInfo <> '' then begin
|
||||
Result := Result + MemoUserInfoInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoDirInfo <> '' then begin
|
||||
Result := Result + MemoDirInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoTypeInfo <> '' then begin
|
||||
Result := Result + MemoTypeInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoComponentsInfo <> '' then begin
|
||||
Result := Result + MemoComponentsInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoGroupInfo <> '' then begin
|
||||
Result := Result + MemoGroupInfo + Newline + NewLine;
|
||||
end;
|
||||
if MemoTasksInfo <> '' then begin
|
||||
Result := Result + MemoTasksInfo;
|
||||
end;
|
||||
|
||||
if Dependency_Memo <> '' then begin
|
||||
if MemoTasksInfo = '' then begin
|
||||
Result := Result + SetupMessage(msgReadyMemoTasks);
|
||||
end;
|
||||
Result := Result + FmtMessage(Dependency_Memo, [Space]);
|
||||
end;
|
||||
end;
|
||||
|
||||
<event('NeedRestart')>
|
||||
function Dependency_NeedRestart: Boolean;
|
||||
begin
|
||||
Result := Dependency_NeedToRestart;
|
||||
end;
|
||||
|
||||
function Dependency_IsX64: Boolean;
|
||||
begin
|
||||
Result := not Dependency_ForceX86 and Is64BitInstallMode;
|
||||
end;
|
||||
|
||||
function Dependency_String(const x86, x64: String): String;
|
||||
begin
|
||||
if Dependency_IsX64 then begin
|
||||
Result := x64;
|
||||
end else begin
|
||||
Result := x86;
|
||||
end;
|
||||
end;
|
||||
|
||||
function Dependency_ArchSuffix: String;
|
||||
begin
|
||||
Result := Dependency_String('', '_x64');
|
||||
end;
|
||||
|
||||
function Dependency_ArchTitle: String;
|
||||
begin
|
||||
Result := Dependency_String(' (x86)', ' (x64)');
|
||||
end;
|
||||
|
||||
function Dependency_IsNetCoreInstalled(const Version: String): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
// source code: https://github.com/dotnet/deployment-tools/tree/main/src/clickonce/native/projects/NetCoreCheck
|
||||
if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe') then begin
|
||||
ExtractTemporaryFile('netcorecheck' + Dependency_ArchSuffix + '.exe');
|
||||
end;
|
||||
Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet35;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net35-sp1
|
||||
if not IsDotNetInstalled(net35, 1) then begin
|
||||
Dependency_Add('dotnetfx35.exe',
|
||||
'/lang:enu /passive /norestart',
|
||||
'.NET Framework 3.5 Service Pack 1',
|
||||
'https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet40;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net40
|
||||
if not IsDotNetInstalled(net4full, 0) then begin
|
||||
Dependency_Add('dotNetFx40_Full_setup.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.0',
|
||||
'https://download.microsoft.com/download/1/B/E/1BE39E79-7E39-46A3-96FF-047F95396215/dotNetFx40_Full_setup.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet45;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net452
|
||||
if not IsDotNetInstalled(net452, 0) then begin
|
||||
Dependency_Add('dotnetfx45.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.5.2',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=397707',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet46;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net462
|
||||
if not IsDotNetInstalled(net462, 0) then begin
|
||||
Dependency_Add('dotnetfx46.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.6.2',
|
||||
'https://go.microsoft.com/fwlink/?linkid=780596',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet47;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net472
|
||||
if not IsDotNetInstalled(net472, 0) then begin
|
||||
Dependency_Add('dotnetfx47.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.7.2',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=863262',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet48;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net48
|
||||
if not IsDotNetInstalled(net48, 0) then begin
|
||||
Dependency_Add('dotnetfx48.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.8',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=2085155',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet481;
|
||||
var
|
||||
Version: Cardinal;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-framework/net481
|
||||
if not RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Version) or (Version < 533320) then begin
|
||||
Dependency_Add('dotnetfx481.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Framework 4.8.1',
|
||||
'https://go.microsoft.com/fwlink/?LinkId=2203304',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddNetCore31;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-core/3.1
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 3.1.32') then begin
|
||||
Dependency_Add('netcore31' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Core Runtime 3.1.32' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/de4b3438-24a2-4d1d-a845-97355cf97b71/515abb880478b49f7c1bced8fbf07b16/dotnet-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/476eba79-f17f-49c8-a213-0f24a22cd026/37c02de81ff5b76ac57a5427462395f1/dotnet-runtime-3.1.32-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddNetCore31Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-core/3.1
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 3.1.32') then begin
|
||||
Dependency_Add('netcore31asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 3.1.32' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/63b482d2-04b2-4dd4-baaf-d1e78de80738/40321091c872f4e77337b68fc61a5a07/aspnetcore-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/98910750-2644-472c-ab2b-17f315ccb953/c2a4c223ee11e2eec7d13744e7a45547/aspnetcore-runtime-3.1.32-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddNetCore31Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet-core/3.1
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 3.1.32') then begin
|
||||
Dependency_Add('netcore31desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 3.1.32' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3f353d2c-0431-48c5-bdf6-fbbe8f901bb5/542a4af07c1df5136a98a1c2df6f3d62/windowsdesktop-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b92958c6-ae36-4efa-aafe-569fced953a5/1654639ef3b20eb576174c1cc200f33a/windowsdesktop-runtime-3.1.32-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet50;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/5.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 5.0.17') then begin
|
||||
Dependency_Add('dotnet50' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Runtime 5.0.17' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/54683c13-6b04-4d7d-b4d4-1f055b50ea43/e99048e2840d57040e8312058853a5b9/dotnet-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a0832b5a-6900-442b-af79-6ffddddd6ba4/e2df0b25dd851ee0b38a86947dd0e42e/dotnet-runtime-5.0.17-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet50Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/5.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 5.0.17') then begin
|
||||
Dependency_Add('dotnet50asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 5.0.17' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/4bfa247d-321d-4b29-a34b-62320849059b/8df7a17d9aad4044efe9b5b1c423e82c/aspnetcore-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3789ec90-2717-424f-8b9c-3adbbcea6c16/2085cc5ff077b8789ff938015392e406/aspnetcore-runtime-5.0.17-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet50Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/5.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 5.0.17') then begin
|
||||
Dependency_Add('dotnet50desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 5.0.17' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b6fe5f2a-95f4-46f1-9824-f5994f10bc69/db5ec9b47ec877b5276f83a185fdb6a0/windowsdesktop-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3aa4e942-42cd-4bf5-afe7-fc23bd9c69c5/64da54c8864e473c19a7d3de15790418/windowsdesktop-runtime-5.0.17-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet60;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 6.0.20') then begin
|
||||
Dependency_Add('dotnet60' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Runtime 6.0.20' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3be5ee3a-c171-4cd2-ab98-00ca5c11eb8c/6fd31294b0c6c670ab5c060592935203/dotnet-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3cfb6d2a-afbe-4ae7-8e5b-776f350654cc/6e8d858a60fe15381f3c84d8ca66c4a7/dotnet-runtime-6.0.20-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet60Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 6.0.20') then begin
|
||||
Dependency_Add('dotnet60asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 6.0.20' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0e37c76c-53b4-4eea-8f5c-6ad2f8d5fe3c/88a8620329ced1aee271992a5b56d236/aspnetcore-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/be9f67fd-60af-45b1-9bca-a7bcc0e86e7e/6a750f7d7432937b3999bb4c5325062a/aspnetcore-runtime-6.0.20-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet60Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 6.0.20') then begin
|
||||
Dependency_Add('dotnet60desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 6.0.20' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0413b619-3eb2-4178-a78e-8d1aafab1a01/5247f08ea3c13849b68074a2142fbf31/windowsdesktop-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1146f414-17c7-4184-8b10-1addfa5315e4/39db5573efb029130add485566320d74/windowsdesktop-runtime-6.0.20-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet70;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/7.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 7.0.9') then begin
|
||||
Dependency_Add('dotnet70' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Runtime 7.0.9' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/305a85f5-2b0d-459b-b2ea-caf71b98d25d/805edc610efa49432e5e268bbba4eacb/dotnet-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/73058888-02a4-4f6d-b3cd-845531c2d7d0/a785e54b7f12046c00714b2ba759e173/dotnet-runtime-7.0.9-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet70Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/7.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 7.0.9') then begin
|
||||
Dependency_Add('dotnet70asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 7.0.9' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/6ec3b357-31df-4b18-948f-4979a5b4b99f/fdeec71fc7f0f34ecfa0cb8b2b897da0/aspnetcore-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/edd9c9b1-0c49-4297-9197-9392b2462318/d06fedaefb256d801ce94ade76af3ad9/aspnetcore-runtime-7.0.9-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet70Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/7.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 7.0.9') then begin
|
||||
Dependency_Add('dotnet70desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 7.0.9' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/139b19d0-2d39-48ce-b59a-aec437509c20/ea6a2711eec53660c3b14d78b9fb2963/windowsdesktop-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/7727acb3-25ca-473b-a392-75afeb33cab7/f11f0477fd2fcfbb3111881377d0c9bb/windowsdesktop-runtime-7.0.9-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure Dependency_AddDotNet80;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 8.0.3') then begin
|
||||
Dependency_Add('dotnet80' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Runtime 8.0.3' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c8d7a77c-5647-4e38-9ed8-edf82328497d/56130e071ac13c3660b0f3a0d60914c7/dotnet-runtime-8.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/961dfc84-ea72-48a2-b3f4-b82cefc34580/6ac50b6bf244a2c5481ad705a92cf843/dotnet-runtime-8.0.3-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet80Asp;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 8.0.3') then begin
|
||||
Dependency_Add('dotnet80asp' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'ASP.NET Core Runtime 8.0.3' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/e1efd12b-9598-4b70-ad83-496563ae3f7c/da67696e4232886f52d50bb8ecda5ab1/aspnetcore-runtime-8.0.3-win-x86.zip', 'https://download.visualstudio.microsoft.com/download/pr/e91876a9-1760-42cb-a6f4-97c57e9cca52/b433fcf4768929539f17e1908cb315bf/aspnetcore-runtime-8.0.3-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDotNet80Desktop;
|
||||
begin
|
||||
// https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 8.0.3') then begin
|
||||
Dependency_Add('dotnet80desktop' + Dependency_ArchSuffix + '.exe',
|
||||
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
|
||||
'.NET Desktop Runtime 8.0.3' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c629f243-5125-4751-a5ff-e78fa45646b1/85777e3e3f58f863d884fd4b8a1453f2/windowsdesktop-runtime-8.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/51bc18ac-0594-412d-bd63-18ece4c91ac4/90b47b97c3bfe40a833791b166697e67/windowsdesktop-runtime-8.0.3-win-x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2005;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=26347
|
||||
if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin
|
||||
Dependency_Add('vcredist2005' + Dependency_ArchSuffix + '.exe',
|
||||
'/q',
|
||||
'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2008;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=26368
|
||||
if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin
|
||||
Dependency_Add('vcredist2008' + Dependency_ArchSuffix + '.exe',
|
||||
'/q',
|
||||
'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2010;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=26999
|
||||
if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin
|
||||
Dependency_Add('vcredist2010' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2012;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=30679
|
||||
if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin
|
||||
Dependency_Add('vcredist2012' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2013;
|
||||
begin
|
||||
// https://support.microsoft.com/en-us/help/4032938
|
||||
if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin
|
||||
Dependency_Add('vcredist2013' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddVC2015To2022;
|
||||
begin
|
||||
// https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
|
||||
if not IsMsiProductInstalled(Dependency_String('{65E5BD06-6392-3027-8C26-853107D3CF1A}', '{36F68A90-239C-34DF-B58C-64B30153CE35}'), PackVersionComponents(14, 30, 30704, 0)) then begin
|
||||
Dependency_Add('vcredist2022' + Dependency_ArchSuffix + '.exe',
|
||||
'/passive /norestart',
|
||||
'Visual C++ 2015-2022 Redistributable' + Dependency_ArchTitle,
|
||||
Dependency_String('https://aka.ms/vs/17/release/vc_redist.x86.exe', 'https://aka.ms/vs/17/release/vc_redist.x64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddDirectX;
|
||||
begin
|
||||
#ifdef Dependency_Files_DirectX
|
||||
ExtractTemporaryFile('dxwebsetup.exe');
|
||||
#endif
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=35
|
||||
Dependency_Add('dxwebsetup.exe',
|
||||
'/q',
|
||||
'DirectX Runtime',
|
||||
'https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe',
|
||||
'', True, False);
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2008Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=30438
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(10, 50, 4000, 0)) < 0) then begin
|
||||
Dependency_Add('sql2008express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2008 R2 Service Pack 2 Express',
|
||||
Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2012Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=56042
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(11, 0, 7001, 0)) < 0) then begin
|
||||
Dependency_Add('sql2012express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2012 Service Pack 4 Express',
|
||||
Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2014Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=57473
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(12, 0, 6024, 0)) < 0) then begin
|
||||
Dependency_Add('sql2014express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2014 Service Pack 3 Express',
|
||||
Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2016Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=103447
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(13, 0, 6404, 1)) < 0) then begin
|
||||
Dependency_Add('sql2016express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2016 Service Pack 3 Express',
|
||||
'https://download.microsoft.com/download/f/a/8/fa83d147-63d1-449c-b22d-5fef9bd5bb46/SQLServer2016-SSEI-Expr.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2017Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=55994
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 0, 0, 0)) < 0) then begin
|
||||
Dependency_Add('sql2017express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2017 Express',
|
||||
'https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2019Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=101064
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(15, 0, 0, 0)) < 0) then begin
|
||||
Dependency_Add('sql2019express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2019 Express',
|
||||
'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddSql2022Express;
|
||||
var
|
||||
Version: String;
|
||||
PackedVersion: Int64;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=104781
|
||||
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(16, 0, 1000, 6)) < 0) then begin
|
||||
Dependency_Add('sql2022express' + Dependency_ArchSuffix + '.exe',
|
||||
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
|
||||
'SQL Server 2022 Express',
|
||||
'https://go.microsoft.com/fwlink/p/?linkid=2216019',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddWebView2;
|
||||
begin
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/webview2
|
||||
if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin
|
||||
Dependency_Add('MicrosoftEdgeWebview2Setup.exe',
|
||||
'/silent /install',
|
||||
'WebView2 Runtime',
|
||||
'https://go.microsoft.com/fwlink/p/?LinkId=2124703',
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddAccessDatabaseEngine2010;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=13255
|
||||
if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\14.0\Access Connectivity Engine\Engines\ACE') then begin
|
||||
Dependency_Add('AccessDatabaseEngine2010' + Dependency_ArchSuffix + '.exe',
|
||||
'/quiet',
|
||||
'Microsoft Access Database Engine 2010' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine.exe', 'https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure Dependency_AddAccessDatabaseEngine2016;
|
||||
begin
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=54920
|
||||
if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\16.0\Access Connectivity Engine\Engines\ACE') then begin
|
||||
Dependency_Add('AccessDatabaseEngine2016' + Dependency_ArchSuffix + '.exe',
|
||||
'/quiet',
|
||||
'Microsoft Access Database Engine 2016' + Dependency_ArchTitle,
|
||||
Dependency_String('https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine.exe', 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe'),
|
||||
'', False, False);
|
||||
end;
|
||||
end;
|
||||
|
||||
[Files]
|
||||
#ifdef Dependency_Path_NetCoreCheck
|
||||
; download netcorecheck.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x86
|
||||
; download netcorecheck_x64.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x64
|
||||
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck.exe"; Flags: dontcopy noencryption
|
||||
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck_x64.exe"; Flags: dontcopy noencryption
|
||||
#endif
|
||||
|
||||
#ifdef Dependency_Path_DirectX
|
||||
Source: "{#Dependency_Path_DirectX}dxwebsetup.exe"; Flags: dontcopy noencryption
|
||||
#endif
|
||||
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck.exe
Normal file
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck.exe
Normal file
Binary file not shown.
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck_x64.exe
Normal file
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck_x64.exe
Normal file
Binary file not shown.
BIN
gui/dependencies/dlls/cobalt.dll
Normal file
BIN
gui/dependencies/dlls/cobalt.dll
Normal file
Binary file not shown.
BIN
gui/dependencies/dlls/memory.dll
Normal file
BIN
gui/dependencies/dlls/memory.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -31,6 +31,10 @@
|
||||
"hostGameServerPasswordDescription": "The password of your game server, if you need one",
|
||||
"hostGameServerDiscoverableName": "Discoverable",
|
||||
"hostGameServerDiscoverableDescription": "Make your server available to other players on the server browser",
|
||||
"hostHeadlessName": "Headless",
|
||||
"hostHeadlessDescription": "Runs Fortnite without graphics to optimize resources usage, may not work for old seasons",
|
||||
"hostVirtualDesktopName": "Virtual desktop",
|
||||
"hostVirtualDesktopDescription": "Runs Fortnite in a virtual desktop if headless is not supported",
|
||||
"hostAutomaticRestartName": "Automatic restart",
|
||||
"hostAutomaticRestartDescription": "Automatically restarts your game server when the match ends",
|
||||
"hostShareName": "Share",
|
||||
@@ -76,10 +80,10 @@
|
||||
"playGameServerCustomContent": "Enter IP",
|
||||
"settingsName": "Settings",
|
||||
"settingsClientName": "Internal files",
|
||||
"settingsClientDescription": "Configure the internal files used by the launcher",
|
||||
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite",
|
||||
"settingsClientOptionsName": "Options",
|
||||
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
|
||||
"settingsClientConsoleName": "Unreal engine patcher",
|
||||
"settingsClientConsoleName": "Unreal engine console",
|
||||
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
|
||||
"settingsClientConsoleKeyName": "Unreal engine console key",
|
||||
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
|
||||
@@ -88,25 +92,24 @@
|
||||
"settingsClientMemoryName": "Memory patcher",
|
||||
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
|
||||
"settingsClientArgsName": "Custom launch arguments",
|
||||
"settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
|
||||
"settingsClientArgsDescription": "Additional arguments to use when launching the game",
|
||||
"settingsClientArgsPlaceholder": "Arguments...",
|
||||
"settingsServerName": "Internal files",
|
||||
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
||||
"settingsServerOptionsName": "Options",
|
||||
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
||||
"settingsServerTypeName": "Game server type",
|
||||
"settingsServerTypeName": "Type",
|
||||
"settingsServerTypeDescription": "The type of game server to inject",
|
||||
"settingsServerTypeEmbeddedName": "Embedded",
|
||||
"settingsServerTypeCustomName": "Custom",
|
||||
"settingsOldServerFileName": "Game server",
|
||||
"settingsServerFileName": "Implementation",
|
||||
"settingsServerFileDescription": "The file injected to create the game server",
|
||||
"settingsServerPortName": "Port",
|
||||
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
|
||||
"settingsServerOldMirrorName": "Update mirror (Before season 20)",
|
||||
"settingsServerNewMirrorName": "Update mirror (Season 20 and above)",
|
||||
"settingsServerMirrorName": "Update mirror",
|
||||
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
||||
"settingsServerMirrorPlaceholder": "mirror",
|
||||
"settingsServerTimerName": "Game server updater",
|
||||
"settingsServerTimerName": "Update timer",
|
||||
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
|
||||
"settingsUtilsName": "Launcher",
|
||||
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
||||
@@ -119,7 +122,9 @@
|
||||
"settingsUtilsResetDefaultsName": "Reset settings",
|
||||
"settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values",
|
||||
"settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
|
||||
"settingsUtilsResetDefaultsContent": "Reset",
|
||||
"settingsUtilsDialogSecondaryAction": "Close",
|
||||
"settingsUtilsDialogPrimaryAction": "Reset",
|
||||
"selectFortniteName": "Fortnite version",
|
||||
"selectFortniteDescription": "Select the version of Fortnite you want to use",
|
||||
"manageVersionsName": "Manage versions",
|
||||
@@ -128,7 +133,7 @@
|
||||
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
||||
"addLocalBuildName": "Add a version from this PC's local storage",
|
||||
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
|
||||
"addVersion": "Add version",
|
||||
"addLocalBuildContent": "Add local build",
|
||||
"downloadBuildName": "Download any version from the cloud",
|
||||
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
||||
"downloadBuildContent": "Download build",
|
||||
@@ -146,17 +151,13 @@
|
||||
"defaultServerName": "Reboot Game Server",
|
||||
"defaultServerDescription": "Just another server",
|
||||
"downloadingDll": "Downloading {name} dll...",
|
||||
"dllAlreadyExists": "The {name} was already downloaded",
|
||||
"downloadDllSuccess": "The {name} dll was downloaded successfully",
|
||||
"downloadDllError": "An error occurred while downloading {name}: {error}",
|
||||
"downloadDllRetry": "Retry",
|
||||
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
|
||||
"launchingGameServer": "Launching the game server...",
|
||||
"launchingGameClientOnly": "Launching the game client without a server...",
|
||||
"launchingGameClientAndServer": "Launching the game client and server...",
|
||||
"startGameServer": "Start a game server",
|
||||
"launchingHeadlessServer": "Launching the game server...",
|
||||
"launchingGameClient": "Launching the game client...",
|
||||
"usernameOrEmail": "Username/Email",
|
||||
"invalidEmail": "Invalid email",
|
||||
"usernameOrEmailPlaceholder": "Type your username or email",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Type your password, if you want to use one",
|
||||
@@ -170,16 +171,16 @@
|
||||
"stoppingServer": "Stopping the backend...",
|
||||
"stoppedServer": "The backend was stopped successfully",
|
||||
"stopServerError": "An error occurred while stopping the backend: {error}",
|
||||
"missingHostNameError": "Missing hostname in the backend configuration",
|
||||
"missingHostNameError": "Missing hostname in the {name} configuration",
|
||||
"missingPortError": "Missing port in the backend configuration",
|
||||
"illegalPortError": "Invalid port in the backend configuration",
|
||||
"freeingPort": "Freeing the backend port...",
|
||||
"freedPort": "The backend port was freed successfully",
|
||||
"freePortError": "An error occurred while freeing the backend port: {error}",
|
||||
"pingingServer": "Pinging the {type} backend...",
|
||||
"pingingRemoteServer": "Pinging the remote backend...",
|
||||
"pingingLocalServer": "Pinging the {type} backend...",
|
||||
"pingError": "Cannot ping the {type} backend",
|
||||
"joinSelfServer": "You can't join your own server",
|
||||
"cannotJoinServerVersion": "You can't join this server: download Fortnite {version}",
|
||||
"wrongServerPassword": "Wrong password: please try again",
|
||||
"offlineServer": "This server isn't online right now: please try again later",
|
||||
"serverPassword": "Password",
|
||||
@@ -195,12 +196,12 @@
|
||||
"deleteVersionCancel": "Keep",
|
||||
"deleteVersionConfirm": "Delete",
|
||||
"versionName": "Name",
|
||||
"versionNameLabel": "Type the version name",
|
||||
"versionNameLabel": "Type the new version name",
|
||||
"newVersionNameConfirm": "Save",
|
||||
"newVersionNameLabel": "Type the version name",
|
||||
"gameFolderTitle": "Game directory",
|
||||
"gameFolderPlaceholder": "Type the game directory",
|
||||
"gameFolderPlaceWindowTitle": "Select game directory",
|
||||
"newVersionNameLabel": "Type the new version name",
|
||||
"gameFolderTitle": "Game folder",
|
||||
"gameFolderPlaceholder": "Type the new game folder",
|
||||
"gameFolderPlaceWindowTitle": "Select game folder",
|
||||
"gameFolderLabel": "Path",
|
||||
"openInExplorer": "Open in explorer",
|
||||
"modify": "Modify",
|
||||
@@ -216,8 +217,6 @@
|
||||
"downloadedVersion": "The download was completed successfully!",
|
||||
"download": "Download",
|
||||
"downloading": "Downloading...",
|
||||
"allocatingSpace": "Allocating disk space...",
|
||||
"startingDownload": "Starting download...",
|
||||
"extracting": "Extracting...",
|
||||
"buildProgress": "{progress}%",
|
||||
"buildInstallationDirectory": "Installation directory",
|
||||
@@ -225,7 +224,7 @@
|
||||
"buildInstallationDirectoryWindowTitle": "Select installation directory",
|
||||
"timeLeft": "Time left: {timeLeft, plural, =0{less than a minute} =1{about {timeLeft} minute} other{about {timeLeft} minutes}}",
|
||||
"localBuildsWarning": "Local builds are not guaranteed to work",
|
||||
"saveLocalVersion": "Add",
|
||||
"saveLocalVersion": "Save",
|
||||
"embedded": "Embedded",
|
||||
"remote": "Remote",
|
||||
"local": "Local",
|
||||
@@ -249,7 +248,7 @@
|
||||
"versionAlreadyExists": "This version already exists",
|
||||
"emptyGamePath": "Empty game path",
|
||||
"directoryDoesNotExist": "Directory doesn't exist",
|
||||
"missingShippingExe": "Invalid game path: missing Fortnite executable",
|
||||
"missingShippingExe": "Invalid game path: missing FortniteClient-Win64-Shipping",
|
||||
"invalidDownloadPath": "Invalid download path",
|
||||
"invalidDllPath": "Invalid dll path",
|
||||
"dllDoesNotExist": "The file doesn't exist",
|
||||
@@ -261,10 +260,9 @@
|
||||
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
|
||||
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version",
|
||||
"corruptedDllError": "Cannot inject dll: {error}",
|
||||
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
|
||||
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
|
||||
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
|
||||
"fortniteCrashError": "The {name} crashed after being launched",
|
||||
"serverNoLongerAvailable": "{owner}'s server is no longer available",
|
||||
"serverNoLongerAvailableUnnamed": "The previous server is no longer available",
|
||||
"noServerFound": "No server found: invalid or expired link",
|
||||
"settingsUtilsThemeName": "Theme",
|
||||
@@ -274,6 +272,8 @@
|
||||
"system": "System",
|
||||
"settingsUtilsLanguageName": "Language",
|
||||
"settingsUtilsLanguageDescription": "Select the language to use inside the launcher",
|
||||
"playAutomaticServerName": "Embedded game server",
|
||||
"playAutomaticServerDescription": "Whether a game server should be started automatically if none was configured",
|
||||
"infoDocumentationName": "Documentation",
|
||||
"infoDocumentationDescription": "Read some tutorials on how to use Reboot",
|
||||
"infoDocumentationContent": "Open GitHub",
|
||||
@@ -281,8 +281,8 @@
|
||||
"infoDiscordDescription": "Join the discord server to receive help",
|
||||
"infoDiscordContent": "Open Discord",
|
||||
"infoVideoName": "Tutorial",
|
||||
"infoVideoDescription": "Show the tutorial again in the launcher",
|
||||
"infoVideoContent": "Start Tutorial",
|
||||
"infoVideoDescription": "Watch a tutorial to understand how to use the launcher",
|
||||
"infoVideoContent": "Open YouTube",
|
||||
"dllDeletedTitle": "A critical dll was deleted. If you didn't delete it, your Antivirus probably flagged it. This is a false positive: please disable your Antivirus and try again",
|
||||
"dllDeletedSecondaryAction": "Close",
|
||||
"dllDeletedPrimaryAction": "Try again",
|
||||
@@ -298,82 +298,5 @@
|
||||
"updateAvailableAction": "Download",
|
||||
"gameServerEnd": "The match ended",
|
||||
"gameServerRestart": "The server will restart in {timeInSeconds} seconds",
|
||||
"gameServerShutdown": "The server will shutdown in {timeInSeconds} seconds",
|
||||
"quiz": "Quiz",
|
||||
"startQuiz": "I have read the instructions",
|
||||
"checkQuiz": "Check answers",
|
||||
"quizFailed": "You got a score of {right}/{total}: you have {tries} left",
|
||||
"quizSuccess": "You got all the questions right: thanks for reading the instructions!",
|
||||
"quizZeroTriesLeft": "zero tries",
|
||||
"quizOneTryLeft": "one try",
|
||||
"quizTwoTriesLeft": "two tries",
|
||||
"gameServerTypeName": "Type",
|
||||
"gameServerTypeDescription": "The type of game server to use",
|
||||
"gameServerTypeHeadless": "Background process",
|
||||
"gameServerTypeVirtualWindow": "Virtual window",
|
||||
"gameServerTypeWindow": "Normal window",
|
||||
"localBuild": "This PC",
|
||||
"githubArchive": "Cloud archive",
|
||||
"all": "All",
|
||||
"accessible": "Accessible",
|
||||
"playable": "Playable",
|
||||
"timeDescending": "Time (from newest to oldest)",
|
||||
"timeAscending": "Time (from oldest to newest)",
|
||||
"nameAscending": "Name (from A to Z)",
|
||||
"nameDescending": "Name (from Z to A)",
|
||||
"none": "none",
|
||||
"openLog": "Open log",
|
||||
"backendProcessError": "The backend shut down unexpectedly",
|
||||
"backendErrorMessage": "The backend reported an unexpected error",
|
||||
"welcomeTitle": "Welcome to Reboot Launcher",
|
||||
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
|
||||
"hostAccountText": "The host tab shows different credentials compared to the play tab.\nIf you are advanced user, you can set a different email and password\nhere if the backend you are using needs authentication.",
|
||||
"hostAccountAction": "I understand",
|
||||
"welcomeAction": "Take the tour",
|
||||
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
|
||||
"startOnboardingActionLabel": "Let's do it",
|
||||
"promptPlayPageText": "The Play tab is used to launch the version of Fortnite you want.\nBefore playing, you'll need to host or join a game server.\nYou will learn how to later.",
|
||||
"promptPlayPageActionLabel": "Next",
|
||||
"promptPlayVersionText": "Here you can download or import any Fortnite version\nAdd at least one to start using the launcher",
|
||||
"promptPlayVersionActionLabelHasBuilds": "Next",
|
||||
"promptPlayVersionActionLabelNoBuilds": "Let's do it",
|
||||
"promptServerBrowserPageText": "The Server Browser tab is used to find game servers hosted by other players\nServers can be free to join or password protected based on the settings set by the owner",
|
||||
"promptServerBrowserPageActionLabel": "Next",
|
||||
"promptHostPageText": "The Host tab is used to host a game server.\nWhen you usually play Fortnite, you connect to an Epic Games' game server.\nTo play using Reboot, you'll need to host the game server yourself, or join someone else's.\nOtherwise, you will be sent back to the lobby when trying to join a game.",
|
||||
"promptHostPageActionLabel": "Next",
|
||||
"promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't want other players to join your server, you can skip this part",
|
||||
"promptHostInfoActionLabelSkip": "Skip",
|
||||
"promptHostInfoActionLabelConfigure": "Configure",
|
||||
"promptHostInformationText": "Choose the name for your server",
|
||||
"promptHostInformationActionLabel": "Next",
|
||||
"promptHostInformationDescriptionText": "Choose the description for your server",
|
||||
"promptHostInformationDescriptionActionLabel": "Next",
|
||||
"promptHostInformationPasswordText": "Set a password for your server, if you need one",
|
||||
"promptHostInformationPasswordActionLabel": "Next",
|
||||
"promptHostVersionText": "You can select the version of Fortnite to host here.\nThese are synchronized with the Play tab.",
|
||||
"promptHostVersionActionLabel": "Next",
|
||||
"promptHostShareText": "If you don't want to use the server browser, other players can join\nyou server by using your Reboot Launcher link or your public IP.",
|
||||
"promptHostShareActionLabel": "Next",
|
||||
"promptBackendPageText": "The Backend tab is used for authentication and queuing.\nWhen you usually play Fortnite, you connect to an Epic Games' backend.\nTo play using Reboot, you'll need to host the backend yourself, or join someone else's.\nIf the backend doesn't work correctly, an authentication error will be displayed.",
|
||||
"promptBackendPageActionLabel": "Next",
|
||||
"promptBackendTypePageText": "By default, an embedded LawinV1 backend is started.\nIf you want to run another backend on your PC, like\nLawinV2 or Momentum, select Local. If you want to join,\na backend on someone else's PC, select Remote.",
|
||||
"promptBackendTypePageActionLabel": "Next",
|
||||
"promptBackendGameServerAddressText": "When you are using an embedded backend, you can type\nhere the IP of the game server you want to join. When\nyou click Join in the Server Browser, this field will be\nautocompleted. If you are not using an embedded backend,\nyou will need to set the IP manually in your backend configuration.",
|
||||
"promptBackendGameServerAddressActionLabel": "Next",
|
||||
"promptBackendUnrealEngineKeyText": "For some Fortnite versions, the PLAY button doesn't work: when this happens,\nyou need to click this Key to open the Unreal Engine console and type: open IP.\nSo for example if you want to join your own server you can type: open 127.0.0.1.\nIf you don't know, 127.0.0.1 is the IP of your local machine. If you are not using\nthe embedded backend, you'll need to set the Unreal Engine key in its configuration.",
|
||||
"promptBackendUnrealEngineKeyActionLabel": "Next",
|
||||
"promptBackendDetachedText": "If you get an authentication error when trying to log into Fortnite,\nswitch to embedded backend and enable this option to debug the backend.\nIf you can't fix the error, report a bug on Discord.",
|
||||
"promptBackendDetachedActionLabel": "Next",
|
||||
"promptInfoTabText": "The Info tab contains useful links to report bugs and receive support",
|
||||
"promptInfoTabActionLabel": "Next",
|
||||
"promptSettingsTabText": "The Settings tab contains options to customize the launcher",
|
||||
"promptSettingsTabActionLabel": "Done",
|
||||
"automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!",
|
||||
"automaticGameServerDialogIgnore": "Ignore",
|
||||
"automaticGameServerDialogStart": "Start server",
|
||||
"gameResetDefaultsName": "Reset",
|
||||
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
|
||||
"gameResetDefaultsContent": "Reset",
|
||||
"selectFile": "Select a file",
|
||||
"reset": "Reset"
|
||||
"gameServerShutdown": "The server will shutdown in {timeInSeconds} seconds"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
@@ -11,18 +14,26 @@ import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/error.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/error.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/url_protocol.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:url_protocol/url_protocol.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
const double kDefaultWindowWidth = 1164;
|
||||
const double kDefaultWindowHeight = 864;
|
||||
@@ -34,8 +45,8 @@ bool appWithNoStorage = false;
|
||||
void main() {
|
||||
log("[APP] Called");
|
||||
runZonedGuarded(
|
||||
() => _startApp(),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
() => _startApp(),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
)
|
||||
@@ -43,7 +54,6 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> _startApp() async {
|
||||
_overrideHttpCertificate();
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
log("[APP] Starting application");
|
||||
@@ -62,6 +72,11 @@ Future<void> _startApp() async {
|
||||
errors.add(notificationsError);
|
||||
}
|
||||
|
||||
final tilesError = InfoPage.initInfoTiles();
|
||||
if(tilesError != null) {
|
||||
errors.add(tilesError);
|
||||
}
|
||||
|
||||
final versionError = await _initVersion();
|
||||
if(versionError != null) {
|
||||
errors.add(versionError);
|
||||
@@ -88,18 +103,6 @@ Future<void> _startApp() async {
|
||||
}
|
||||
}
|
||||
|
||||
class _MyHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context){
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
|
||||
}
|
||||
}
|
||||
|
||||
void _overrideHttpCertificate() {
|
||||
HttpOverrides.global = _MyHttpOverrides(); // Not safe, but necessary
|
||||
}
|
||||
|
||||
Future<Object?> _initNotifications() async {
|
||||
try {
|
||||
await localNotifier.setup(
|
||||
@@ -145,55 +148,54 @@ Future<Object?> _initVersion() async {
|
||||
|
||||
Future<Object?> _initUrlHandler() async {
|
||||
try {
|
||||
registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
|
||||
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initWindow() async {
|
||||
void _initWindow() => doWhenWindowReady(() async {
|
||||
try {
|
||||
await SystemTheme.accentColor.load();
|
||||
await windowManager.ensureInitialized();
|
||||
await Window.initialize();
|
||||
var settingsController = Get.find<SettingsController>();
|
||||
var size = Size(settingsController.width, settingsController.height);
|
||||
await windowManager.setSize(size);
|
||||
appWindow.size = size;
|
||||
var offsetX = settingsController.offsetX;
|
||||
var offsetY = settingsController.offsetY;
|
||||
if(offsetX != null && offsetY != null) {
|
||||
final position = Offset(
|
||||
if(offsetX != null && offsetY != null){
|
||||
appWindow.position = Offset(
|
||||
offsetX,
|
||||
offsetY
|
||||
);
|
||||
await windowManager.setPosition(position);
|
||||
}else {
|
||||
await windowManager.setAlignment(Alignment.center);
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
if(isWin11) {
|
||||
await Window.setEffect(
|
||||
effect: WindowEffect.acrylic,
|
||||
color: Colors.transparent,
|
||||
dark: isDarkMode
|
||||
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
|
||||
);
|
||||
}
|
||||
}catch(error, stackTrace) {
|
||||
onError(error, stackTrace, false);
|
||||
}finally {
|
||||
windowManager.show();
|
||||
appWindow.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Future<List<Object>> _initStorage() async {
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
|
||||
await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
|
||||
await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
|
||||
await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
|
||||
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
|
||||
await GetStorage("game", settingsDirectory.path).initStorage;
|
||||
await GetStorage("backend", settingsDirectory.path).initStorage;
|
||||
await GetStorage("update", settingsDirectory.path).initStorage;
|
||||
await GetStorage("settings", settingsDirectory.path).initStorage;
|
||||
await GetStorage("hosting", settingsDirectory.path).initStorage;
|
||||
}catch(error) {
|
||||
appWithNoStorage = true;
|
||||
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
|
||||
@@ -212,9 +214,19 @@ Future<List<Object>> _initStorage() async {
|
||||
}
|
||||
|
||||
try {
|
||||
final controller = HostingController();
|
||||
Get.put(controller);
|
||||
controller.discardServer();
|
||||
Get.put(BuildController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(HostingController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(UpdateController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
@@ -225,12 +237,6 @@ Future<List<Object>> _initStorage() async {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(DllController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -2,39 +2,33 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
|
||||
class BackendController extends GetxController {
|
||||
static const String storageName = "v2_backend_storage";
|
||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final GetStorage? storage;
|
||||
late final TextEditingController host;
|
||||
late final TextEditingController port;
|
||||
late final Rx<ServerType> type;
|
||||
late final TextEditingController gameServerAddress;
|
||||
late final FocusNode gameServerAddressFocusNode;
|
||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||
late final RxnString gameServerOwner;
|
||||
late final RxBool started;
|
||||
late final RxBool detached;
|
||||
StreamSubscription? worker;
|
||||
int? embeddedProcessPid;
|
||||
HttpServer? localServer;
|
||||
HttpServer? remoteServer;
|
||||
|
||||
BackendController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
storage = appWithNoStorage ? null : GetStorage("backend");
|
||||
started = RxBool(false);
|
||||
type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
|
||||
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
|
||||
type.listen((value) {
|
||||
host.text = _readHost();
|
||||
port.text = _readPort();
|
||||
_storage?.write("type", value.index);
|
||||
storage?.write("type", value.index);
|
||||
if (!started.value) {
|
||||
return;
|
||||
}
|
||||
@@ -43,14 +37,13 @@ class BackendController extends GetxController {
|
||||
});
|
||||
host = TextEditingController(text: _readHost());
|
||||
host.addListener(() =>
|
||||
_storage?.write("${type.value.name}_host", host.text));
|
||||
storage?.write("${type.value.name}_host", host.text));
|
||||
port = TextEditingController(text: _readPort());
|
||||
port.addListener(() =>
|
||||
_storage?.write("${type.value.name}_port", port.text));
|
||||
detached = RxBool(_storage?.read("detached") ?? false);
|
||||
detached.listen((value) => _storage?.write("detached", value));
|
||||
final address = _storage?.read("game_server_address");
|
||||
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
|
||||
storage?.write("${type.value.name}_port", port.text));
|
||||
detached = RxBool(storage?.read("detached") ?? false);
|
||||
detached.listen((value) => storage?.write("detached", value));
|
||||
gameServerAddress = TextEditingController(text: storage?.read("game_server_address") ?? "127.0.0.1");
|
||||
var lastValue = gameServerAddress.text;
|
||||
writeMatchmakingIp(lastValue);
|
||||
gameServerAddress.addListener(() {
|
||||
@@ -61,7 +54,7 @@ class BackendController extends GetxController {
|
||||
|
||||
lastValue = newValue;
|
||||
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
||||
_storage?.write("game_server_address", newValue);
|
||||
storage?.write("game_server_address", newValue);
|
||||
writeMatchmakingIp(newValue);
|
||||
});
|
||||
watchMatchmakingIp().listen((event) {
|
||||
@@ -70,59 +63,24 @@ class BackendController extends GetxController {
|
||||
}
|
||||
});
|
||||
gameServerAddressFocusNode = FocusNode();
|
||||
consoleKey = Rx(_readConsoleKey());
|
||||
_writeConsoleKey(consoleKey.value);
|
||||
consoleKey.listen((newValue) {
|
||||
_storage?.write("console_key", newValue.usbHidUsage);
|
||||
_writeConsoleKey(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
PhysicalKeyboardKey _readConsoleKey() {
|
||||
final consoleKeyValue = _storage?.read("console_key");
|
||||
if(consoleKeyValue == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
|
||||
if(consoleKeyNumber == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
|
||||
if(!consoleKey.isUnrealEngineKey) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
return consoleKey;
|
||||
}
|
||||
|
||||
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
||||
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
|
||||
await defaultInput.parent.create(recursive: true);
|
||||
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
|
||||
}
|
||||
|
||||
void joinLocalhost() {
|
||||
gameServerAddress.text = kDefaultGameServerHost;
|
||||
gameServerOwner = RxnString(storage?.read("game_server_owner"));
|
||||
gameServerOwner.listen((value) => storage?.write("game_server_owner", value));
|
||||
}
|
||||
|
||||
void reset() async {
|
||||
type.value = ServerType.values.elementAt(0);
|
||||
for (final type in ServerType.values) {
|
||||
_storage?.write("${type.name}_host", null);
|
||||
_storage?.write("${type.name}_port", null);
|
||||
storage?.write("${type.name}_host", null);
|
||||
storage?.write("${type.name}_port", null);
|
||||
}
|
||||
|
||||
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
|
||||
port.text = kDefaultBackendPort.toString();
|
||||
gameServerAddress.text = "127.0.0.1";
|
||||
consoleKey.value = _kDefaultConsoleKey;
|
||||
detached.value = false;
|
||||
}
|
||||
|
||||
String _readHost() {
|
||||
String? value = _storage?.read("${type.value.name}_host");
|
||||
String? value = storage?.read("${type.value.name}_host");
|
||||
if (value != null && value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
@@ -135,20 +93,24 @@ class BackendController extends GetxController {
|
||||
}
|
||||
|
||||
String _readPort() =>
|
||||
_storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||
storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||
|
||||
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* {
|
||||
Stream<ServerResult> start() async* {
|
||||
try {
|
||||
if(started.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final serverType = type.value;
|
||||
final hostData = this.host.text.trim();
|
||||
final portData = this.port.text.trim();
|
||||
started.value = true;
|
||||
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
|
||||
if(type() != ServerType.local) {
|
||||
started.value = true;
|
||||
yield ServerResult(ServerResultType.starting);
|
||||
}else {
|
||||
started.value = false;
|
||||
if(portData != kDefaultBackendPort.toString()) {
|
||||
yield ServerResult(ServerResultType.starting);
|
||||
}
|
||||
}
|
||||
|
||||
if (hostData.isEmpty) {
|
||||
@@ -170,7 +132,7 @@ class BackendController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
||||
if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
||||
yield ServerResult(ServerResultType.freeingPort);
|
||||
final result = await freeBackendPort();
|
||||
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
||||
@@ -180,21 +142,15 @@ class BackendController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
switch(serverType){
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
|
||||
if(started.value) {
|
||||
final process = await startEmbeddedBackend(detached.value);
|
||||
final processPid = process.pid;
|
||||
watchProcess(processPid).then((value) {
|
||||
if(started()) {
|
||||
started.value = false;
|
||||
onError(errorMessage);
|
||||
}
|
||||
});
|
||||
watchProcess(process.pid).then((_) {
|
||||
if(started.value) {
|
||||
started.value = false;
|
||||
onExit();
|
||||
}
|
||||
});
|
||||
embeddedProcessPid = process.pid;
|
||||
break;
|
||||
case ServerType.remote:
|
||||
yield ServerResult(ServerResultType.pingingRemote);
|
||||
@@ -208,20 +164,8 @@ class BackendController extends GetxController {
|
||||
remoteServer = await startRemoteBackendProxy(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
if(portNumber != kDefaultBackendPort) {
|
||||
yield ServerResult(ServerResultType.pingingLocal);
|
||||
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
|
||||
if(uriResult == null) {
|
||||
yield ServerResult(ServerResultType.pingError);
|
||||
started.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(portData != kDefaultBackendPort.toString()) {
|
||||
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
|
||||
}else {
|
||||
// If the local server is running on port 3551 there is no reverse proxy running
|
||||
// We only need to check if everything is working
|
||||
started.value = false;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -260,11 +204,7 @@ class BackendController extends GetxController {
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
final embeddedProcessPid = this.embeddedProcessPid;
|
||||
if(embeddedProcessPid != null) {
|
||||
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
|
||||
this.embeddedProcessPid = null;
|
||||
}
|
||||
killProcessByPort(kDefaultBackendPort);
|
||||
break;
|
||||
case ServerType.remote:
|
||||
await remoteServer?.close(force: true);
|
||||
@@ -286,14 +226,11 @@ class BackendController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Stream<ServerResult> toggle({required void Function() onExit, required void Function(String) onError}) async* {
|
||||
Stream<ServerResult> toggle() async* {
|
||||
if(started()) {
|
||||
yield* stop();
|
||||
}else {
|
||||
yield* start(
|
||||
onExit: onExit,
|
||||
onError: onError
|
||||
);
|
||||
yield* start();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
gui/lib/src/controller/build_controller.dart
Normal file
22
gui/lib/src/controller/build_controller.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
class BuildController extends GetxController {
|
||||
List<FortniteBuild>? _builds;
|
||||
Rxn<FortniteBuild> _selectedBuild;
|
||||
|
||||
BuildController() : _selectedBuild = Rxn();
|
||||
|
||||
List<FortniteBuild>? get builds => _builds;
|
||||
|
||||
FortniteBuild? get selectedBuild => _selectedBuild.value;
|
||||
|
||||
set selectedBuild(FortniteBuild? value) {
|
||||
_selectedBuild.value = value;
|
||||
}
|
||||
|
||||
set builds(List<FortniteBuild>? builds) {
|
||||
_builds = builds;
|
||||
_selectedBuild.value = builds?.firstOrNull;
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class DllController extends GetxController {
|
||||
static const String storageName = "v2_dll_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController gameServerDll;
|
||||
late final TextEditingController unrealEngineConsoleDll;
|
||||
late final TextEditingController backendDll;
|
||||
late final TextEditingController gameServerPort;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController beforeS20Mirror;
|
||||
late final TextEditingController aboveS20Mirror;
|
||||
late final RxBool customGameServer;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future<bool>? _updater;
|
||||
|
||||
DllController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
gameServerDll = _createController("game_server", InjectableDll.reboot);
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
||||
backendDll = _createController("backend", InjectableDll.starfall);
|
||||
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
||||
final timerIndex = _storage?.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage?.write("timer", value.index));
|
||||
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl);
|
||||
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
|
||||
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
|
||||
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
|
||||
status = Rx(UpdateStatus.waiting);
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, InjectableDll dll) {
|
||||
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
|
||||
controller.addListener(() => _storage?.write(key, controller.text));
|
||||
return controller;
|
||||
}
|
||||
|
||||
void resetGame() {
|
||||
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
|
||||
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
|
||||
backendDll.text = getDefaultDllPath(InjectableDll.starfall);
|
||||
}
|
||||
|
||||
void resetServer() {
|
||||
gameServerPort.text = kDefaultGameServerPort;
|
||||
timer.value = UpdateTimer.hour;
|
||||
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
|
||||
status.value = UpdateStatus.waiting;
|
||||
customGameServer.value = false;
|
||||
timestamp.value = null;
|
||||
updateGameServerDll();
|
||||
}
|
||||
|
||||
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater!;
|
||||
}
|
||||
|
||||
final result = _updateGameServerDll(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<bool> _updateGameServerDll(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
return true;
|
||||
}
|
||||
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
hours: timer.value.hours,
|
||||
force: force
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!silent) {
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await Future.wait(
|
||||
[
|
||||
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text),
|
||||
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text),
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
],
|
||||
eagerError: false
|
||||
);
|
||||
timestamp.value = DateTime.now().millisecondsSinceEpoch;
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
if(!silent) {
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}catch(message) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadDllError(error.toString(), "reboot.dll"),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
infoBarEntry?.close();
|
||||
updateGameServerDll(
|
||||
force: true,
|
||||
silent: silent
|
||||
);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
(File, bool) getInjectableData(Version version, InjectableDll dll) {
|
||||
final defaultPath = canonicalize(getDefaultDllPath(dll));
|
||||
switch(dll){
|
||||
case InjectableDll.reboot:
|
||||
if(customGameServer.value) {
|
||||
return (File(gameServerDll.text), true);
|
||||
}
|
||||
|
||||
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
||||
case InjectableDll.console:
|
||||
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
||||
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
||||
case InjectableDll.starfall:
|
||||
final backendFile = File(backendDll.text);
|
||||
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
String getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
|
||||
|
||||
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false, bool force = false}) async {
|
||||
log("[DLL] Asking for $filePath(silent: $silent)");
|
||||
final fileName = basename(filePath).toLowerCase();
|
||||
log("[DLL] File name: $fileName");
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName.contains("reboot")) {
|
||||
log("[DLL] Downloading reboot.dll...");
|
||||
return await updateGameServerDll(
|
||||
silent: silent
|
||||
);
|
||||
}
|
||||
|
||||
if(!force && File(filePath).existsSync()) {
|
||||
log("[DLL] File already exists");
|
||||
return true;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
entry = showRebootInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
entry = await showRebootInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}catch(message) {
|
||||
log("[DLL] Error: $message");
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error =
|
||||
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showRebootInfoBar(
|
||||
translations.downloadDllError(error.toString(), fileName),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await downloadCriticalDllInteractive(filePath);
|
||||
completer.complete(null);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
int get hours {
|
||||
switch(this) {
|
||||
case UpdateTimer.never:
|
||||
return -1;
|
||||
case UpdateTimer.hour:
|
||||
return 1;
|
||||
case UpdateTimer.day:
|
||||
return 24;
|
||||
case UpdateTimer.week:
|
||||
return 24 * 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
|
||||
class GameController extends GetxController {
|
||||
static const String storageName = "v2_game_storage";
|
||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final TextEditingController username;
|
||||
@@ -18,9 +22,10 @@ class GameController extends GetxController {
|
||||
late final Rxn<FortniteVersion> _selectedVersion;
|
||||
late final RxBool started;
|
||||
late final Rxn<GameInstance> instance;
|
||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||
|
||||
GameController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
_storage = appWithNoStorage ? null : GetStorage("game");
|
||||
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
||||
final decodedVersions = decodedVersionsJson
|
||||
.map((entry) => FortniteVersion.fromJson(entry))
|
||||
@@ -28,7 +33,8 @@ class GameController extends GetxController {
|
||||
versions = Rx(decodedVersions);
|
||||
versions.listen((data) => _saveVersions());
|
||||
final decodedSelectedVersionName = _storage?.read("version");
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName);
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((
|
||||
element) => element.name == decodedSelectedVersionName);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
username = TextEditingController(
|
||||
text: _storage?.read("username") ?? kDefaultPlayerName);
|
||||
@@ -36,9 +42,41 @@ class GameController extends GetxController {
|
||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||
password.addListener(() => _storage?.write("password", password.text));
|
||||
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
||||
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
|
||||
customLaunchArgs.addListener(() =>
|
||||
_storage?.write("custom_launch_args", customLaunchArgs.text));
|
||||
started = RxBool(false);
|
||||
instance = Rxn();
|
||||
consoleKey = Rx(_readConsoleKey());
|
||||
_writeConsoleKey(consoleKey.value);
|
||||
consoleKey.listen((newValue) {
|
||||
_storage?.write("console_key", newValue.usbHidUsage);
|
||||
_writeConsoleKey(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
PhysicalKeyboardKey _readConsoleKey() {
|
||||
final consoleKeyValue = _storage?.read("console_key");
|
||||
if(consoleKeyValue == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
|
||||
if(consoleKeyNumber == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
|
||||
if(!consoleKey.isUnrealEngineKey) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
return consoleKey;
|
||||
}
|
||||
|
||||
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
||||
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
|
||||
await defaultInput.parent.create(recursive: true);
|
||||
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
@@ -46,12 +84,11 @@ class GameController extends GetxController {
|
||||
password.text = "";
|
||||
customLaunchArgs.text = "";
|
||||
versions.value = [];
|
||||
_selectedVersion.value = null;
|
||||
instance.value = null;
|
||||
}
|
||||
|
||||
FortniteVersion? getVersionByName(String name) {
|
||||
return versions.value.firstWhereOrNull((element) => element.content.toString() == name);
|
||||
return versions.value.firstWhereOrNull((element) => element.name == name);
|
||||
}
|
||||
|
||||
void addVersion(FortniteVersion version) {
|
||||
@@ -62,9 +99,15 @@ class GameController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
FortniteVersion removeVersionByName(String versionName) {
|
||||
var version = versions.value.firstWhere((element) => element.name == versionName);
|
||||
removeVersion(version);
|
||||
return version;
|
||||
}
|
||||
|
||||
void removeVersion(FortniteVersion version) {
|
||||
versions.update((val) => val?.remove(version));
|
||||
if (selectedVersion == version || hasNoVersions) {
|
||||
if (selectedVersion?.name == version.name || hasNoVersions) {
|
||||
selectedVersion = null;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +125,7 @@ class GameController extends GetxController {
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
_selectedVersion.value = version;
|
||||
_storage?.write("version", version?.content.toString());
|
||||
_storage?.write("version", version?.name);
|
||||
}
|
||||
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
|
||||
@@ -1,178 +1,76 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class HostingController extends GetxController {
|
||||
static const String storageName = "v2_hosting_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final String uuid;
|
||||
late final TextEditingController accountUsername;
|
||||
late final TextEditingController accountPassword;
|
||||
late final TextEditingController name;
|
||||
late final FocusNode nameFocusNode;
|
||||
late final TextEditingController description;
|
||||
late final FocusNode descriptionFocusNode;
|
||||
late final TextEditingController password;
|
||||
late final FocusNode passwordFocusNode;
|
||||
late final RxBool showPassword;
|
||||
late final RxBool discoverable;
|
||||
late final Rx<GameServerType> type;
|
||||
late final RxBool headless;
|
||||
late final RxBool virtualDesktop;
|
||||
late final RxBool autoRestart;
|
||||
late final RxBool started;
|
||||
late final RxBool published;
|
||||
late final Rxn<GameInstance> instance;
|
||||
late final Rxn<Set<FortniteServer>> servers;
|
||||
late final TextEditingController customLaunchArgs;
|
||||
late final Semaphore _semaphore;
|
||||
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||
|
||||
HostingController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
_storage = appWithNoStorage ? null : GetStorage("hosting");
|
||||
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
||||
_storage?.write("uuid", uuid);
|
||||
accountUsername = TextEditingController(text: _storage?.read("account_username") ?? kDefaultHostName);
|
||||
accountUsername.addListener(() => _storage?.write("account_username", accountUsername.text));
|
||||
accountPassword = TextEditingController(text: _storage?.read("account_password") ?? "");
|
||||
accountPassword.addListener(() => _storage?.write("account_password", password.text));
|
||||
name = TextEditingController(text: _storage?.read("name"));
|
||||
name.addListener(() => _storage?.write("name", name.text));
|
||||
description = TextEditingController(text: _storage?.read("description"));
|
||||
description.addListener(() => _storage?.write("description", description.text));
|
||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||
password.addListener(() => _storage?.write("password", password.text));
|
||||
nameFocusNode = FocusNode();
|
||||
descriptionFocusNode = FocusNode();
|
||||
passwordFocusNode = FocusNode();
|
||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
||||
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
|
||||
type.listen((value) => _storage?.write("type", value.index));
|
||||
headless = RxBool(_storage?.read("headless") ?? true);
|
||||
headless.listen((value) => _storage?.write("headless", value));
|
||||
virtualDesktop = RxBool(_storage?.read("virtual_desktop") ?? true);
|
||||
virtualDesktop.listen((value) => _storage?.write("virtual_desktop", value));
|
||||
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
|
||||
autoRestart.listen((value) => _storage?.write("auto_restart", value));
|
||||
started = RxBool(false);
|
||||
published = RxBool(false);
|
||||
showPassword = RxBool(false);
|
||||
instance = Rxn();
|
||||
servers = Rxn();
|
||||
_listenServers();
|
||||
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
||||
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
|
||||
_semaphore = Semaphore();
|
||||
}
|
||||
|
||||
void _listenServers([int attempt = 0]) {
|
||||
log("[SUPABASE] Listening...");
|
||||
final supabase = Supabase.instance.client;
|
||||
supabase.from("hosting_v2")
|
||||
servers = Rxn();
|
||||
supabase.from("hosting")
|
||||
.stream(primaryKey: ['id'])
|
||||
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
|
||||
.listen(
|
||||
_onNewServer,
|
||||
onError: (error) async {
|
||||
log("[SUPABASE] Error: ${error}");
|
||||
await Future.delayed(Duration(seconds: attempt * 5));
|
||||
_listenServers(attempt + 1);
|
||||
},
|
||||
cancelOnError: true
|
||||
);
|
||||
.map((event) => _parseValidServers(event))
|
||||
.listen((event) {
|
||||
servers.value = event;
|
||||
published.value = event.any((element) => element["id"] == uuid);
|
||||
});
|
||||
}
|
||||
|
||||
void _onNewServer(Set<FortniteServer> event) {
|
||||
log("[SUPABASE] New event: ${event}");
|
||||
servers.value = event;
|
||||
published.value = event.any((element) => element.id == uuid);
|
||||
}
|
||||
|
||||
Future<void> publishServer(String author, String version) async {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
log("[SERVER] Publishing server...");
|
||||
if(published.value) {
|
||||
log("[SERVER] Already published");
|
||||
return;
|
||||
}
|
||||
|
||||
final passwordText = password.text;
|
||||
final hasPassword = passwordText.isNotEmpty;
|
||||
var ip = await Ipify.ipv4();
|
||||
if(hasPassword) {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting_v2");
|
||||
final payload = FortniteServer(
|
||||
id: uuid,
|
||||
name: name.text,
|
||||
description: description.text,
|
||||
author: author,
|
||||
ip: ip,
|
||||
version: version,
|
||||
password: hasPassword ? hashPassword(passwordText) : null,
|
||||
timestamp: DateTime.now(),
|
||||
discoverable: discoverable.value
|
||||
).toJson();
|
||||
log("[SERVER] Payload: ${jsonEncode(payload)}");
|
||||
if(published()) {
|
||||
await hosts.update(payload)
|
||||
.eq("id", uuid);
|
||||
}else {
|
||||
await hosts.insert(payload);
|
||||
}
|
||||
|
||||
published.value = true;
|
||||
log("[SERVER] Published");
|
||||
}catch(error) {
|
||||
log("[SERVER] Cannot publish server: $error");
|
||||
published.value = false;
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardServer() async {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
log("[SERVER] Discarding server...");
|
||||
final supabase = Supabase.instance.client;
|
||||
await supabase.from("hosting_v2")
|
||||
.delete()
|
||||
.match({'id': uuid});
|
||||
servers.value?.removeWhere((element) => element.id == uuid);
|
||||
log("[SERVER] Discarded server");
|
||||
}catch(error) {
|
||||
log("[SERVER] Cannot discard server: $error");
|
||||
}finally {
|
||||
published.value = false;
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet();
|
||||
|
||||
void reset() {
|
||||
accountUsername.text = kDefaultHostName;
|
||||
accountPassword.text = "";
|
||||
name.text = "";
|
||||
description.text = "";
|
||||
showPassword.value = false;
|
||||
discoverable.value = false;
|
||||
started.value = false;
|
||||
instance.value = null;
|
||||
type.value = GameServerType.headless;
|
||||
autoRestart.value = true;
|
||||
customLaunchArgs.text = "";
|
||||
headless.value = true;
|
||||
virtualDesktop.value = true;
|
||||
}
|
||||
|
||||
FortniteServer? findServerById(String uuid) {
|
||||
Map<String, dynamic>? findServerById(String uuid) {
|
||||
try {
|
||||
return servers.value?.firstWhere((element) => element.id == uuid);
|
||||
return servers.value?.firstWhere((element) => element["id"] == uuid);
|
||||
} on StateError catch(_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,98 +1,71 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
static const String storageName = "v2_settings_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final GetStorage _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController gameServerDll;
|
||||
late final TextEditingController unrealEngineConsoleDll;
|
||||
late final TextEditingController backendDll;
|
||||
late final TextEditingController memoryLeakDll;
|
||||
late final TextEditingController gameServerPort;
|
||||
late final RxBool firstRun;
|
||||
late final RxString language;
|
||||
late final Rx<ThemeMode> themeMode;
|
||||
late final RxBool firstRun;
|
||||
late double width;
|
||||
late double height;
|
||||
late double? offsetX;
|
||||
late double? offsetY;
|
||||
|
||||
SettingsController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
width = _storage?.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage?.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage?.read("offset_x");
|
||||
offsetY = _storage?.read("offset_y");
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage?.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage?.write("theme", value.index));
|
||||
language = RxString(_storage?.read("language") ?? currentLocale);
|
||||
language.listen((value) => _storage?.write("language", value));
|
||||
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
|
||||
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
|
||||
_storage = GetStorage("settings");
|
||||
gameServerDll = _createController("game_server", "reboot.dll");
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
|
||||
backendDll = _createController("backend", "cobalt.dll");
|
||||
memoryLeakDll = _createController("memory_leak", "memory.dll");
|
||||
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
|
||||
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
firstRun = RxBool(_storage.read("first_run_new1") ?? true);
|
||||
firstRun.listen((value) => _storage.write("first_run_new1", value));
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage.write("theme", value.index));
|
||||
language = RxString(_storage.read("language") ?? currentLocale);
|
||||
language.listen((value) => _storage.write("language", value));
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, String name) {
|
||||
var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name));
|
||||
controller.addListener(() => _storage.write(key, controller.text));
|
||||
return controller;
|
||||
}
|
||||
|
||||
void saveWindowSize(Size size) {
|
||||
_storage?.write("width", size.width);
|
||||
_storage?.write("height", size.height);
|
||||
_storage.write("width", size.width);
|
||||
_storage.write("height", size.height);
|
||||
}
|
||||
|
||||
void saveWindowOffset(Offset position) {
|
||||
offsetX = position.dx;
|
||||
offsetY = position.dy;
|
||||
_storage?.write("offset_x", offsetX);
|
||||
_storage?.write("offset_y", offsetY);
|
||||
_storage.write("offset_x", offsetX);
|
||||
_storage.write("offset_y", offsetY);
|
||||
}
|
||||
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
if (appVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspec = await _getPubspecYaml();
|
||||
if (pubspec == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final latestVersion = Version.parse(pubspec["version"]);
|
||||
if (latestVersion <= appVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
late InfoBarEntry infoBar;
|
||||
infoBar = showRebootInfoBar(
|
||||
translations.updateAvailable(latestVersion.toString()),
|
||||
duration: null,
|
||||
severity: InfoBarSeverity.warning,
|
||||
action: Button(
|
||||
child: Text(translations.updateAvailableAction),
|
||||
onPressed: () {
|
||||
infoBar.close();
|
||||
launchUrl(Uri.parse(
|
||||
"https://github.com/Auties00/reboot_launcher/releases"));
|
||||
},
|
||||
)
|
||||
);
|
||||
void reset(){
|
||||
gameServerDll.text = _controllerDefaultPath("reboot.dll");
|
||||
unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll");
|
||||
backendDll.text = _controllerDefaultPath("cobalt.dll");
|
||||
gameServerPort.text = kDefaultGameServerPort;
|
||||
firstRun.value = true;
|
||||
}
|
||||
|
||||
Future<dynamic> _getPubspecYaml() async {
|
||||
try {
|
||||
final pubspecResponse = await http.get(Uri.parse(
|
||||
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||
if (pubspecResponse.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadYaml(pubspecResponse.body);
|
||||
} catch (error) {
|
||||
log("[UPDATER] Cannot check for updates: $error");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
|
||||
}
|
||||
|
||||
161
gui/lib/src/controller/update_controller.dart
Normal file
161
gui/lib/src/controller/update_controller.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class UpdateController {
|
||||
late final GetStorage? _storage;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController url;
|
||||
late final RxBool customGameServer;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future? _updater;
|
||||
|
||||
UpdateController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("update");
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
var timerIndex = _storage?.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage?.write("timer", value.index));
|
||||
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
|
||||
url.addListener(() => _storage?.write("update_url", url.text));
|
||||
status = Rx(UpdateStatus.waiting);
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
}
|
||||
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
if(appVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||
if(pubspecResponse.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspec = loadYaml(pubspecResponse.body);
|
||||
final latestVersion = Version.parse(pubspec["version"]);
|
||||
if(latestVersion <= appVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
late InfoBarEntry infoBar;
|
||||
infoBar = showInfoBar(
|
||||
translations.updateAvailable(latestVersion.toString()),
|
||||
duration: null,
|
||||
severity: InfoBarSeverity.warning,
|
||||
action: Button(
|
||||
child: Text(translations.updateAvailableAction),
|
||||
onPressed: () {
|
||||
infoBar.close();
|
||||
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateReboot({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater;
|
||||
}
|
||||
|
||||
final result = _updateReboot(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<void> _updateReboot(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
hours: timer.value.hours,
|
||||
force: force
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error =
|
||||
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(
|
||||
force: true,
|
||||
silent: silent
|
||||
),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
}
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
timestamp.value = null;
|
||||
timer.value = UpdateTimer.never;
|
||||
url.text = kRebootDownloadUrl;
|
||||
status.value = UpdateStatus.waiting;
|
||||
customGameServer.value = false;
|
||||
updateReboot();
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
int get hours {
|
||||
switch(this) {
|
||||
case UpdateTimer.never:
|
||||
return -1;
|
||||
case UpdateTimer.hour:
|
||||
return 1;
|
||||
case UpdateTimer.day:
|
||||
return 24;
|
||||
case UpdateTimer.week:
|
||||
return 24 * 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import 'dialog_button.dart';
|
||||
|
||||
bool inDialog = false;
|
||||
|
||||
Future<T?> showRebootDialog<T extends Object?>({required WidgetBuilder builder, bool dismissWithEsc = true}) async {
|
||||
Future<T?> showAppDialog<T extends Object?>({required WidgetBuilder builder}) async {
|
||||
inDialog = true;
|
||||
pagesController.add(null);
|
||||
try {
|
||||
return await fluent.showDialog(
|
||||
context: appNavigatorKey.currentContext!,
|
||||
context: appKey.currentContext!,
|
||||
useRootNavigator: false,
|
||||
dismissWithEsc: dismissWithEsc,
|
||||
builder: builder
|
||||
);
|
||||
}finally {
|
||||
@@ -57,7 +58,7 @@ class FormDialog extends AbstractDialog {
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
return GenericDialog(
|
||||
header: content,
|
||||
buttons: parsed
|
||||
@@ -116,9 +117,8 @@ class InfoDialog extends AbstractDialog {
|
||||
class ProgressDialog extends AbstractDialog {
|
||||
final String text;
|
||||
final Function()? onStop;
|
||||
final bool showButton;
|
||||
|
||||
const ProgressDialog({required this.text, this.onStop, this.showButton = true, Key? key}) : super(key: key);
|
||||
const ProgressDialog({required this.text, this.onStop, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -132,12 +132,11 @@ class ProgressDialog extends AbstractDialog {
|
||||
),
|
||||
),
|
||||
buttons: [
|
||||
if(showButton)
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -240,7 +239,7 @@ class ErrorDialog extends AbstractDialog {
|
||||
type: type,
|
||||
onTap: () async {
|
||||
FlutterClipboard.controlC("$error\n$stackTrace");
|
||||
showRebootInfoBar(translations.copyErrorDialogSuccess);
|
||||
showInfoBar(translations.copyErrorDialogSuccess);
|
||||
onClick();
|
||||
},
|
||||
);
|
||||
@@ -263,62 +262,4 @@ class ErrorDialog extends AbstractDialog {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
final Color? color;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
this.color,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton => Button(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
|
||||
),
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
|
||||
Widget get _secondaryButton => Button(
|
||||
style: widget.color != null ? ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(widget.color!)
|
||||
) : null,
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
56
gui/lib/src/dialog/abstract/dialog_button.dart
Normal file
56
gui/lib/src/dialog/abstract/dialog_button.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton {
|
||||
return Button(
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _secondaryButton {
|
||||
return Button(
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4);
|
||||
const infoBarShortDuration = Duration(seconds: 2);
|
||||
const _height = 64.0;
|
||||
|
||||
InfoBarEntry showRebootInfoBar(dynamic text, {
|
||||
InfoBarEntry showInfoBar(dynamic text, {
|
||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
@@ -21,39 +21,33 @@ InfoBarEntry showRebootInfoBar(dynamic text, {
|
||||
return overlayEntry;
|
||||
}
|
||||
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: _height
|
||||
),
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => SizedBox(
|
||||
width: double.infinity,
|
||||
height: _height,
|
||||
child: Mica(
|
||||
elevation: 1,
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: text is Widget ? text : Text(text)
|
||||
),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 2.0,
|
||||
right: 6.0
|
||||
),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
)
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
||||
Future<void> showResetDialog(Function() onConfirm) => showAppDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.resetDefaultsDialogTitle,
|
||||
buttons: [
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showAppDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.dllDeletedTitle,
|
||||
buttons: [
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import '../../util/log.dart';
|
||||
|
||||
|
||||
String? lastError;
|
||||
|
||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
@@ -18,15 +21,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
}
|
||||
|
||||
lastError = exception.toString();
|
||||
if(inDialog){
|
||||
final context = pageKey.currentContext;
|
||||
if(context != null) {
|
||||
Navigator.of(context).pop(false);
|
||||
inDialog = false;
|
||||
}
|
||||
var route = ModalRoute.of(pageKey.currentContext!);
|
||||
if(route != null && !route.isCurrent){
|
||||
Navigator.of(pageKey.currentContext!).pop(false);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showAppDialog(
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: exception,
|
||||
@@ -1,17 +1,19 @@
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
|
||||
final showPassword = RxBool(false);
|
||||
final oldUsername = username.text;
|
||||
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
final oldPassword = password.text;
|
||||
final result = await showRebootDialog<bool?>(
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
Future<bool> showProfileForm(BuildContext context) async{
|
||||
var showPassword = RxBool(false);
|
||||
var oldUsername = _gameController.username.text;
|
||||
var showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
var oldPassword = _gameController.password.text;
|
||||
var result = await showAppDialog<bool?>(
|
||||
builder: (context) => Obx(() => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -22,18 +24,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
||||
label: translations.usernameOrEmail,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.usernameOrEmailPlaceholder,
|
||||
validator: (text) {
|
||||
if(password.text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(EmailValidator.validate(username.text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return translations.invalidEmail;
|
||||
},
|
||||
controller: username,
|
||||
controller: _gameController.username,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
enableSuggestions: true,
|
||||
autofocus: true,
|
||||
@@ -45,7 +36,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
||||
label: translations.password,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.passwordPlaceholder,
|
||||
controller: password,
|
||||
controller: _gameController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !showPassword.value,
|
||||
enableSuggestions: false,
|
||||
@@ -54,8 +45,8 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
||||
suffix: Button(
|
||||
onPressed: () => showPassword.value = !showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||
@@ -85,7 +76,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
||||
return true;
|
||||
}
|
||||
|
||||
username.text = oldUsername;
|
||||
password.text = oldPassword;
|
||||
_gameController.username.text = oldUsername;
|
||||
_gameController.password.text = oldPassword;
|
||||
return false;
|
||||
}
|
||||
@@ -1,54 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final List<InfoBarEntry> _infoBars = [];
|
||||
final Semaphore _publishingSemaphore = Semaphore();
|
||||
|
||||
extension ServerControllerDialog on BackendController {
|
||||
void cancelInteractive() {
|
||||
worker?.cancel(); // Do not await or it will hang
|
||||
_infoBars.forEach((infoBar) => infoBar.close());
|
||||
_infoBars.clear();
|
||||
}
|
||||
|
||||
Future<bool> toggleInteractive() async {
|
||||
cancelInteractive();
|
||||
final stream = toggle(
|
||||
onExit: () {
|
||||
cancelInteractive();
|
||||
_showRebootInfoBar(
|
||||
translations.backendProcessError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
},
|
||||
onError: (errorMessage) {
|
||||
cancelInteractive();
|
||||
_showRebootInfoBar(
|
||||
translations.backendErrorMessage,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
action: Button(
|
||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||
child: Text(translations.openLog),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
final stream = toggle();
|
||||
final completer = Completer<bool>();
|
||||
InfoBarEntry? entry;
|
||||
worker = stream.listen((event) {
|
||||
@@ -65,103 +39,108 @@ extension ServerControllerDialog on BackendController {
|
||||
}
|
||||
|
||||
InfoBarEntry _handeEvent(ServerResult event) {
|
||||
log("[BACKEND] Handling event: $event");
|
||||
switch (event.type) {
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.startingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.startSuccess:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.startError:
|
||||
print(event.stackTrace);
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.stopping:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.stopSuccess:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.stoppedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.stopError:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.stopServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.missingHostError:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.missingHostNameError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.missingPortError:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.missingPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.illegalPortError:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.illegalPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.freeingPort:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.freeingPort,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.freePortSuccess:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.freedPort,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
case ServerResultType.freePortError:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.pingingRemote:
|
||||
return _showRebootInfoBar(
|
||||
translations.pingingServer(ServerType.remote.name),
|
||||
return showInfoBar(
|
||||
translations.pingingRemoteServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingingLocal:
|
||||
return _showRebootInfoBar(
|
||||
translations.pingingServer(type.value.name),
|
||||
return showInfoBar(
|
||||
translations.pingingLocalServer(type.value.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingError:
|
||||
return _showRebootInfoBar(
|
||||
return showInfoBar(
|
||||
translations.pingError(type.value.name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
|
||||
if(!kDebugMode && uuid == server.id) {
|
||||
_showRebootInfoBar(
|
||||
void joinLocalHost() {
|
||||
gameServerAddress.text = kDefaultGameServerHost;
|
||||
gameServerOwner.value = null;
|
||||
}
|
||||
|
||||
Future<void> joinServer(String uuid, Map<String, dynamic> entry) async {
|
||||
final id = entry["id"];
|
||||
if(uuid == id) {
|
||||
showInfoBar(
|
||||
translations.joinSelfServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -169,29 +148,18 @@ extension ServerControllerDialog on BackendController {
|
||||
return;
|
||||
}
|
||||
|
||||
final gameController = Get.find<GameController>();
|
||||
final version = gameController.getVersionByName(server.version.toString());
|
||||
if(version == null) {
|
||||
_showRebootInfoBar(
|
||||
translations.cannotJoinServerVersion(server.version.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final hashedPassword = server.password;
|
||||
final hashedPassword = entry["password"];
|
||||
final hasPassword = hashedPassword != null;
|
||||
final embedded = type.value == ServerType.embedded;
|
||||
final author = server.author;
|
||||
final encryptedIp = server.ip;
|
||||
final author = entry["author"];
|
||||
final encryptedIp = entry["ip"];
|
||||
if(!hasPassword) {
|
||||
final valid = await _isServerValid(encryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(gameController, embedded, encryptedIp, author, version);
|
||||
_onSuccess(embedded, encryptedIp, author);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,7 +169,7 @@ extension ServerControllerDialog on BackendController {
|
||||
}
|
||||
|
||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||
_showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.wrongServerPassword,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -215,7 +183,7 @@ extension ServerControllerDialog on BackendController {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(gameController, embedded, decryptedIp, author, version);
|
||||
_onSuccess(embedded, decryptedIp, author);
|
||||
}
|
||||
|
||||
Future<bool> _isServerValid(String address) async {
|
||||
@@ -224,7 +192,7 @@ extension ServerControllerDialog on BackendController {
|
||||
return true;
|
||||
}
|
||||
|
||||
_showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.offlineServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -236,7 +204,7 @@ extension ServerControllerDialog on BackendController {
|
||||
final confirmPasswordController = TextEditingController();
|
||||
final showPassword = RxBool(false);
|
||||
final showPasswordTrailing = RxBool(false);
|
||||
return await showRebootDialog<String?>(
|
||||
return await showAppDialog<String?>(
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -257,8 +225,8 @@ extension ServerControllerDialog on BackendController {
|
||||
suffix: !showPasswordTrailing.value ? null : Button(
|
||||
onPressed: () => showPassword.value = !showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
|
||||
@@ -285,39 +253,80 @@ extension ServerControllerDialog on BackendController {
|
||||
);
|
||||
}
|
||||
|
||||
void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
||||
void _onSuccess(bool embedded, String decryptedIp, String author) {
|
||||
if(embedded) {
|
||||
gameServerAddress.text = decryptedIp;
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
gameServerOwner.value = author;
|
||||
pageIndex.value = 0;
|
||||
}else {
|
||||
FlutterClipboard.controlC(decryptedIp);
|
||||
}
|
||||
controller.selectedVersion = version;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.success
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
InfoBarEntry _showRebootInfoBar(dynamic text, {
|
||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
void Function()? onDismissed,
|
||||
Widget? action
|
||||
}) {
|
||||
final result = showRebootInfoBar(
|
||||
text,
|
||||
severity: severity,
|
||||
loading: loading,
|
||||
duration: duration,
|
||||
onDismissed: onDismissed,
|
||||
action: action
|
||||
);
|
||||
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
|
||||
_infoBars.add(result);
|
||||
extension HostingControllerExtension on HostingController {
|
||||
Future<void> publishServer(String author, String version) async {
|
||||
try {
|
||||
_publishingSemaphore.acquire();
|
||||
if(published.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final passwordText = password.text;
|
||||
final hasPassword = passwordText.isNotEmpty;
|
||||
var ip = await Ipify.ipv4();
|
||||
if(hasPassword) {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting");
|
||||
final payload = {
|
||||
'name': name.text,
|
||||
'description': description.text,
|
||||
'author': author,
|
||||
'ip': ip,
|
||||
'version': version,
|
||||
'password': hasPassword ? hashPassword(passwordText) : null,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'discoverable': discoverable.value
|
||||
};
|
||||
if(published()) {
|
||||
await hosts.update(payload).eq("id", uuid);
|
||||
}else {
|
||||
payload["id"] = uuid;
|
||||
await hosts.insert(payload);
|
||||
}
|
||||
|
||||
published.value = true;
|
||||
}catch(error) {
|
||||
published.value = false;
|
||||
}finally {
|
||||
_publishingSemaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardServer() async {
|
||||
try {
|
||||
_publishingSemaphore.acquire();
|
||||
if(!published.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
await supabase.from("hosting")
|
||||
.delete()
|
||||
.match({'id': uuid});
|
||||
published.value = false;
|
||||
}catch(_) {
|
||||
published.value = true;
|
||||
}finally {
|
||||
_publishingSemaphore.release();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
|
||||
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
||||
|
||||
class OverlayTarget extends StatefulWidget {
|
||||
final Widget child;
|
||||
const OverlayTarget({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<OverlayTarget> createState() => OverlayTargetState();
|
||||
|
||||
OverlayTargetState of(BuildContext context) => context.findAncestorStateOfType<OverlayTargetState>()!;
|
||||
}
|
||||
|
||||
class OverlayTargetState extends State<OverlayTarget> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
|
||||
void showOverlay({
|
||||
required String text,
|
||||
required WidgetBuilder actionBuilder,
|
||||
Offset offset = Offset.zero,
|
||||
bool ignoreTargetPointers = true,
|
||||
AttachMode attachMode = AttachMode.start
|
||||
}) {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final color = FluentTheme.of(context).acrylicBackgroundColor;
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: _AbsorbPointer(
|
||||
exclusion: ignoreTargetPointers ? null : renderBox
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
left: position.dx - (attachMode != AttachMode.start ? renderBox.size.width : 0) + offset.dx,
|
||||
top: position.dy + (renderBox.size.height / 2) + offset.dy,
|
||||
child: CustomPaint(
|
||||
painter: _CallOutShape(color, attachMode != AttachMode.start),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(text),
|
||||
const SizedBox(height: 12.0),
|
||||
actionBuilder(context, () => entry.remove())
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
appOverlayKey.currentState?.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachMode {
|
||||
start,
|
||||
middle,
|
||||
end;
|
||||
}
|
||||
|
||||
// Harder than one would think
|
||||
class _CallOutShape extends CustomPainter {
|
||||
final Color color;
|
||||
final bool end;
|
||||
_CallOutShape(this.color, this.end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final fillPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final borderPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.25
|
||||
..color = Colors.white;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(10, 0);
|
||||
if(!end) {
|
||||
path.lineTo(12.5, 0);
|
||||
path.lineTo(20, -12.5);
|
||||
path.lineTo(27.5, 0);
|
||||
}else {
|
||||
path.lineTo(size.width - 27.5, 0);
|
||||
path.lineTo(size.width - 20, -12.5);
|
||||
path.lineTo(size.width - 12.5, 0);
|
||||
}
|
||||
|
||||
path.lineTo(size.width - 10, 0);
|
||||
path.arcToPoint(Offset(size.width, 10), radius: Radius.circular(10));
|
||||
path.lineTo(size.width, size.height - 10);
|
||||
path.arcToPoint(Offset(size.width - 10, size.height), radius: Radius.circular(10));
|
||||
path.lineTo(10, size.height);
|
||||
path.arcToPoint(Offset(0, size.height - 10), radius: Radius.circular(10));
|
||||
path.lineTo(0, 10);
|
||||
path.arcToPoint(Offset(10, 0), radius: Radius.circular(10));
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, borderPaint);
|
||||
canvas.drawShadow(path, color, 1, true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _AbsorbPointer extends SingleChildRenderObjectWidget {
|
||||
final RenderBox? exclusion;
|
||||
const _AbsorbPointer({
|
||||
required this.exclusion
|
||||
});
|
||||
|
||||
@override
|
||||
_RenderAbsorbPointer createRenderObject(BuildContext context) => _RenderAbsorbPointer(
|
||||
exclusion: exclusion
|
||||
);
|
||||
}
|
||||
|
||||
class _RenderAbsorbPointer extends RenderProxyBox {
|
||||
final RenderBox? exclusion;
|
||||
_RenderAbsorbPointer({
|
||||
required this.exclusion,
|
||||
RenderBox? child
|
||||
}) : super(child);
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, { required Offset position }) {
|
||||
final exclusion = this.exclusion;
|
||||
if(exclusion == null) {
|
||||
return size.contains(position);
|
||||
}
|
||||
|
||||
// 32 is the height of the title bar (need this offset as the overlay area doesn't include it)
|
||||
// Not an optimal solution but it works (calculating it is kind of complicated)
|
||||
position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight);
|
||||
final exclusionPosition = exclusion.localToGlobal(Offset.zero);
|
||||
final exclusionSize = Rect.fromLTRB(
|
||||
exclusionPosition.dx,
|
||||
exclusionPosition.dy,
|
||||
exclusionPosition.dx + exclusion.size.width,
|
||||
exclusionPosition.dy + exclusion.size.height
|
||||
);
|
||||
return !exclusionSize.contains(position);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) => super.visitChildrenForSemantics(visitor);
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.isBlockingUserActions = true;
|
||||
}
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
void startOnboarding() {
|
||||
final gameController = Get.find<GameController>();
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
settingsController.firstRun.value = false;
|
||||
profileOverlayKey.currentState!.showOverlay(
|
||||
text: translations.startOnboardingText,
|
||||
offset: Offset(27.5, 17.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.startOnboardingActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
await showProfileForm(context, gameController.username, gameController.password);
|
||||
_promptPlayPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayPage() {
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptPlayPageActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
_promptPlayVersion();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayVersion() {
|
||||
final gameController = Get.find<GameController>();
|
||||
final hasBuilds = gameController.versions.value.isNotEmpty;
|
||||
gameVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayVersionText,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: hasBuilds ? translations.promptPlayVersionActionLabelHasBuilds : translations.promptPlayVersionActionLabelNoBuilds,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
if(!hasBuilds) {
|
||||
await VersionSelector.openDownloadDialog();
|
||||
}
|
||||
_promptServerBrowserPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptServerBrowserPage() {
|
||||
pageIndex.value = RebootPageType.browser.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptServerBrowserPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptServerBrowserPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostAccount();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostAccount() {
|
||||
pageIndex.value = RebootPageType.host.index;
|
||||
profileOverlayKey.currentState!.showOverlay(
|
||||
text: translations.hostAccountText,
|
||||
offset: Offset(27.5, 17.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.hostAccountAction,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
_promptHostPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostPage() {
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInfo();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptHostInfo() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostInfoOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInfoText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => Row(
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelSkip,
|
||||
themed: false,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = false;
|
||||
_promptHostVersion();
|
||||
}
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelConfigure,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = true;
|
||||
hostInfoTileKey.currentState!.openNestedPage();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformation() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.nameFocusNode.requestFocus();
|
||||
hostInfoNameOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationDescription();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationDescription() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.descriptionFocusNode.requestFocus();
|
||||
hostInfoDescriptionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationDescriptionText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(70, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationDescriptionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationPassword();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationPassword() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.passwordFocusNode.requestFocus();
|
||||
hostInfoPasswordOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationPasswordText,
|
||||
ignoreTargetPointers: false,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationPasswordActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
Navigator.of(hostInfoTileKey.currentContext!).pop();
|
||||
pageStack.removeLast();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostVersion() {
|
||||
hostVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostVersionText,
|
||||
attachMode: AttachMode.end,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostVersionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostShare();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostShare() {
|
||||
final backendController = Get.find<BackendController>();
|
||||
hostShareOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostShareText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostShareActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
backendController.type.value = ServerType.embedded;
|
||||
_promptBackendPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptBackendPage() {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendTypePage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendTypePage() {
|
||||
backendTypeOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendTypePageText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendTypePageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendGameServerAddress();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendGameServerAddress() {
|
||||
backendGameServerAddressOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendGameServerAddressText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendGameServerAddressActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendUnrealEngineKey();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendUnrealEngineKey() {
|
||||
backendUnrealEngineOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendUnrealEngineKeyText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-465, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendUnrealEngineKeyActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendDetached();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendDetached() {
|
||||
backendDetachedOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendDetachedText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-410, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendDetachedActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptInfoTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptInfoTab() {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptInfoTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptInfoTabActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptSettingsTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptSettingsTab() {
|
||||
pageIndex.value = RebootPageType.settings.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptSettingsTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptSettingsTabActionLabel,
|
||||
onTap: onClose
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
bool themed = true,
|
||||
required void Function() onTap,
|
||||
}) => Button(
|
||||
style: themed ? ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
|
||||
) : null,
|
||||
child: Text(label),
|
||||
onPressed: onTap
|
||||
);
|
||||
@@ -1,492 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/util/types.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddVersionDialog extends StatefulWidget {
|
||||
final bool closable;
|
||||
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddVersionDialog> createState() => _AddVersionDialogState();
|
||||
}
|
||||
|
||||
class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
|
||||
|
||||
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
|
||||
final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive);
|
||||
final Rxn<FortniteBuild> _build = Rxn();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
final RxInt _speed = RxInt(0);
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future<List<FortniteBuild>> _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = compute(fetchBuilds, null);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case _DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
final data = snapshot.data;
|
||||
if (data == null) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
showButton: widget.closable,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() => FormDialog(
|
||||
content: _buildFormBody(data),
|
||||
buttons: _formButtons
|
||||
));
|
||||
}
|
||||
);
|
||||
case _DownloadStatus.downloading:
|
||||
case _DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case _DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) {
|
||||
var error = exception.toString();
|
||||
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
|
||||
error = error.toLowerCase();
|
||||
return translations.downloadVersionError(error);
|
||||
}
|
||||
);
|
||||
case _DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
if(widget.closable)
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
|
||||
type: widget.closable ? ButtonType.primary : ButtonType.only,
|
||||
color: FluentTheme.of(context).accentColor,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final topResult = _formKey.currentState?.validate();
|
||||
if(topResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fieldResult = _formFieldKey.currentState?.validate();
|
||||
if(fieldResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final source = _source.value;
|
||||
if(source == _BuildSource.local) {
|
||||
Navigator.of(context).pop();
|
||||
_addFortniteVersion(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(build, message);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete(FortniteBuild build) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_addFortniteVersion(build);
|
||||
}
|
||||
|
||||
void _addFortniteVersion(FortniteBuild build) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
content: build.version,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(message.progress >= 100 && message.extracting) {
|
||||
_onDownloadComplete(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = message.extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
|
||||
if(message.progress >= 0) {
|
||||
WindowsTaskbar.setProgress(message.progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = message.timeLeft;
|
||||
_progress.value = message.progress;
|
||||
_speed.value = message.speed;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_statusText,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
if(_progress.value != null && !_isAllocatingDiskSpace)
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
if(_progress.value != null && !_isAllocatingDiskSpace)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _isAllocatingDiskSpace ? null : _progress.value?.toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String get _statusText {
|
||||
if (_status.value != _DownloadStatus.downloading) {
|
||||
return translations.extracting;
|
||||
}
|
||||
|
||||
if (_progress.value == null) {
|
||||
return translations.startingDownload;
|
||||
}
|
||||
|
||||
if (_speed.value == 0) {
|
||||
return translations.allocatingSpace;
|
||||
}
|
||||
|
||||
return translations.downloading;
|
||||
}
|
||||
|
||||
bool get _isAllocatingDiskSpace => _status.value == _DownloadStatus.downloading && _speed.value == 0;
|
||||
|
||||
Widget _buildFormBody(List<FortniteBuild> builds) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSourceSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
_buildBuildSelector(builds),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
||||
folder: true,
|
||||
allowNavigator: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? _checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
final directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
|
||||
label: translations.build,
|
||||
child: FormField<FortniteBuild?>(
|
||||
key: _formFieldKey,
|
||||
validator: (data) => _checkBuild(data),
|
||||
builder: (formContext) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(),
|
||||
value: _build.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_build.value = value;
|
||||
formContext.didChange(value);
|
||||
formContext.validate();
|
||||
_updateFormDefaults();
|
||||
}
|
||||
),
|
||||
if(formContext.hasError)
|
||||
const SizedBox(height: 4.0),
|
||||
if(formContext.hasError)
|
||||
Text(
|
||||
formContext.errorText ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: formContext.hasError ? 8.0 : 16.0
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
String? _checkBuild(FortniteBuild? data) {
|
||||
if(data == null) {
|
||||
return translations.selectBuild;
|
||||
}
|
||||
|
||||
final versions = _gameController.versions.value;
|
||||
if (versions.any((element) => data.version == element.content)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
Widget _buildSourceSelector() => InfoLabel(
|
||||
label: translations.source,
|
||||
child: ComboBox<_BuildSource>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(),
|
||||
value: _source.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_source.value = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>(
|
||||
value: element,
|
||||
child: Text(element.translatedName)
|
||||
);
|
||||
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: translations.stopLoadingDialogAction,
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_source.value != _BuildSource.local && _build.value?.available != true) {
|
||||
_build.value = null;
|
||||
}
|
||||
|
||||
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) {
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
}
|
||||
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum _DownloadStatus {
|
||||
form,
|
||||
downloading,
|
||||
extracting,
|
||||
error,
|
||||
done
|
||||
}
|
||||
|
||||
enum _BuildSource {
|
||||
local,
|
||||
githubArchive;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _BuildSource.local:
|
||||
return translations.localBuild;
|
||||
case _BuildSource.githubArchive:
|
||||
return translations.githubArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
abstract class RebootPage extends StatefulWidget {
|
||||
const RebootPage({super.key});
|
||||
@@ -23,79 +19,32 @@ abstract class RebootPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
var buttonWidget = button;
|
||||
if(buttonWidget == null) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFirstLaunchInfo(),
|
||||
Expanded(
|
||||
child: _listView
|
||||
)
|
||||
],
|
||||
);
|
||||
return _listView;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFirstLaunchInfo(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _listView,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: buttonWidget
|
||||
)
|
||||
],
|
||||
),
|
||||
child: _listView,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: buttonWidget
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstLaunchInfo() => Obx(() {
|
||||
if(!_settingsController.firstRun.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.welcomeTitle),
|
||||
severity: InfoBarSeverity.warning,
|
||||
isLong: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(translations.welcomeDescription)
|
||||
),
|
||||
action: Button(
|
||||
child: Text(translations.welcomeAction),
|
||||
onPressed: () => startOnboarding(),
|
||||
),
|
||||
onClose: () => _settingsController.firstRun.value = false
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
ListView get _listView => ListView.builder(
|
||||
itemCount: settings.length,
|
||||
itemBuilder: (context, index) => settings[index],
|
||||
|
||||
@@ -5,9 +5,8 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
@@ -17,10 +16,7 @@ import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
||||
import '../../dialog/implementation/data.dart';
|
||||
|
||||
class BackendPage extends RebootPage {
|
||||
const BackendPage({Key? key}) : super(key: key);
|
||||
@@ -42,10 +38,11 @@ class BackendPage extends RebootPage {
|
||||
}
|
||||
|
||||
class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
InfoBarEntry? _infoBarEntry;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
|
||||
@@ -54,7 +51,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
}
|
||||
|
||||
if(keyEvent.physicalKey.isUnrealEngineKey) {
|
||||
_backendController.consoleKey.value = keyEvent.physicalKey;
|
||||
_gameController.consoleKey.value = keyEvent.physicalKey;
|
||||
}
|
||||
|
||||
_infoBarEntry?.close();
|
||||
@@ -63,7 +60,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_type,
|
||||
@@ -87,13 +84,10 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.matchmakerConfigurationAddressName),
|
||||
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
||||
content: OverlayTarget(
|
||||
key: backendGameServerAddressOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -152,24 +146,21 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
Text(
|
||||
_backendController.detached.value ? translations.on : translations.off
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
OverlayTarget(
|
||||
key: backendDetachedOverlayTargetKey,
|
||||
child: ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Widget get _unrealEngineConsoleKey => Obx(() {
|
||||
if(_backendController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -182,18 +173,14 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
title: Text(translations.settingsClientConsoleKeyName),
|
||||
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
||||
contentWidth: null,
|
||||
content: OverlayTarget(
|
||||
key: backendUnrealEngineOverlayTargetKey,
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showRebootInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
},
|
||||
child: Text(_backendController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
),
|
||||
content: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -234,9 +221,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.backendTypeName),
|
||||
subtitle: Text(translations.backendTypeDescription),
|
||||
content: ServerTypeSelector(
|
||||
overlayKey: backendTypeOverlayTargetKey
|
||||
)
|
||||
content: const ServerTypeSelector()
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
final Rx<_Filter> _filter = Rx(_Filter.all);
|
||||
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
final data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable)
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<FortniteServer> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildFilter(context),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
_buildSort(context),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildSort(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.arrow_sort_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Sort by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 230,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_sort.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Sort.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _sort.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
|
||||
Row _buildFilter(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.filter_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Filter by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 125,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_filter.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Filter.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _filter.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopulatedListBody(Set<FortniteServer> items) => Obx(() {
|
||||
final filter = _filter.value;
|
||||
final sorted = items.where((element) {
|
||||
switch(filter) {
|
||||
case _Filter.all:
|
||||
return true;
|
||||
case _Filter.accessible:
|
||||
return element.password == null;
|
||||
case _Filter.playable:
|
||||
return _gameController.getVersionByName(element.version) != null;
|
||||
}
|
||||
}).toList();
|
||||
final sort = _sort.value;
|
||||
sorted.sort((first, second) {
|
||||
switch(sort) {
|
||||
case _Sort.timeAscending:
|
||||
return first.timestamp.compareTo(second.timestamp);
|
||||
case _Sort.timeDescending:
|
||||
return second.timestamp.compareTo(first.timestamp);
|
||||
case _Sort.nameAscending:
|
||||
return first.name.compareTo(second.name);
|
||||
case _Sort.nameDescending:
|
||||
return second.name.compareTo(first.name);
|
||||
}
|
||||
});
|
||||
if(sorted.isEmpty) {
|
||||
return _noServersByQuery;
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = sorted.elementAt(index);
|
||||
final hasPassword = entry.password != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry.author}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
|
||||
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(FortniteServer entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(FortniteServer element, String filter) {
|
||||
filter = filter.toLowerCase();
|
||||
|
||||
final uri = Uri.tryParse(filter);
|
||||
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return element.id.toLowerCase().contains(filter.toLowerCase())
|
||||
|| element.name.toLowerCase().contains(filter)
|
||||
|| element.author.toLowerCase().contains(filter)
|
||||
|| element.description.toLowerCase().contains(filter);
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||
shape: WidgetStateProperty.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
final color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(FortniteServer server) {
|
||||
final result = server.name;
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(FortniteServer server) {
|
||||
final result = server.description;
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}";
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
|
||||
enum _Filter {
|
||||
all,
|
||||
accessible,
|
||||
playable;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Filter.all:
|
||||
return translations.all;
|
||||
case _Filter.accessible:
|
||||
return translations.accessible;
|
||||
case _Filter.playable:
|
||||
return translations.playable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _Sort {
|
||||
timeAscending,
|
||||
timeDescending,
|
||||
nameAscending,
|
||||
nameDescending;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Sort.timeAscending:
|
||||
return translations.timeAscending;
|
||||
case _Sort.timeDescending:
|
||||
return translations.timeDescending;
|
||||
case _Sort.nameAscending:
|
||||
return translations.nameAscending;
|
||||
case _Sort.nameDescending:
|
||||
return translations.nameDescending;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,38 +3,33 @@ import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' show MaterialPage;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||
import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
static const double kDefaultPadding = 12.0;
|
||||
static const double kTitleBarHeight = 32;
|
||||
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -42,15 +37,16 @@ class HomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
static const double _kDefaultPadding = 12.0;
|
||||
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final DllController _dllController = Get.find<DllController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -59,7 +55,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
void initState() {
|
||||
super.initState();
|
||||
windowManager.addListener(this);
|
||||
_syncPageViewWithNavigator();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkUpdates();
|
||||
_initAppLink();
|
||||
@@ -67,18 +62,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
});
|
||||
}
|
||||
|
||||
void _syncPageViewWithNavigator() {
|
||||
var lastPage = pageIndex.value;
|
||||
pageIndex.listen((index) {
|
||||
if(index == lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
}
|
||||
|
||||
void _initAppLink() async {
|
||||
final appLinks = AppLinks();
|
||||
final initialUrl = await appLinks.getInitialLink();
|
||||
@@ -93,9 +76,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
final uuid = uri.host;
|
||||
final server = _hostingController.findServerById(uuid);
|
||||
if(server != null) {
|
||||
_backendController.joinServerInteractive(_hostingController.uuid, server);
|
||||
_backendController.joinServer(_hostingController.uuid, server);
|
||||
}else {
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.noServerFound,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -110,14 +93,15 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await pingGameServer(address);
|
||||
var result = await pingGameServer(address);
|
||||
if(result) {
|
||||
return;
|
||||
}
|
||||
|
||||
_backendController.joinLocalhost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
translations.serverNoLongerAvailableUnnamed,
|
||||
var oldOwner = _backendController.gameServerOwner.value;
|
||||
_backendController.joinLocalHost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
));
|
||||
@@ -128,43 +112,27 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
}
|
||||
|
||||
void _checkUpdates() {
|
||||
_settingsController.notifyLauncherUpdate();
|
||||
_updateController.notifyLauncherUpdate();
|
||||
|
||||
if(!dllsDirectory.existsSync()) {
|
||||
dllsDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
final dummy = Version.parse("1");
|
||||
final dummyS20 = Version.parse("20");
|
||||
for(final injectable in InjectableDll.values) {
|
||||
_downloadDll(dummy, injectable);
|
||||
if(injectable.isVersionDependent) {
|
||||
_downloadDll(dummyS20, injectable);
|
||||
}
|
||||
downloadCriticalDllInteractive(
|
||||
injectable.path,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
_dllController.downloadCriticalDllInteractive(filePath);
|
||||
downloadCriticalDllInteractive(filePath);
|
||||
}));
|
||||
}
|
||||
|
||||
void _downloadDll(Version version, InjectableDll injectable) {
|
||||
final (file, custom) = _dllController.getInjectableData(version, injectable);
|
||||
if(!custom) {
|
||||
_dllController.downloadCriticalDllInteractive(
|
||||
file.path,
|
||||
silent: false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
try {
|
||||
await _hostingController.discardServer();
|
||||
}catch(error) {
|
||||
log("[HOSTING] Cannot discard server: $error");
|
||||
}
|
||||
void onWindowClose() {
|
||||
exit(0); // Force closing
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -183,7 +151,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
_focused.value = !_focused.value;
|
||||
_focused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -228,18 +196,14 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
@override
|
||||
void onWindowResized() {
|
||||
_settingsController.saveWindowSize(appWindow.size);
|
||||
_focused.value = true;
|
||||
windowManager.getSize().then((size) {
|
||||
_settingsController.saveWindowSize(size);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMoved() {
|
||||
_settingsController.saveWindowOffset(appWindow.position);
|
||||
_focused.value = true;
|
||||
windowManager.getPosition().then((position) {
|
||||
_settingsController.saveWindowOffset(position);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -252,365 +216,136 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
if(eventName != "move") {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
_settingsController.language.value;
|
||||
loadTranslations(context);
|
||||
return Obx(() {
|
||||
return Container(
|
||||
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: HomePage.kTitleBarHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
_backButton,
|
||||
Expanded(child: _draggableArea),
|
||||
WindowTitleBar(focused: _focused())
|
||||
],
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appNavigatorKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Overlay(
|
||||
key: appOverlayKey,
|
||||
initialEntries: [
|
||||
OverlayEntry(
|
||||
maintainState: true,
|
||||
builder: (context) => Row(
|
||||
children: [
|
||||
_buildLateralView(),
|
||||
_buildBody()
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
return NavigationPaneTheme(
|
||||
data: NavigationPaneThemeData(
|
||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
),
|
||||
child: NavigationView(
|
||||
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||
padding: _kDefaultPadding,
|
||||
controller: pagesController,
|
||||
body: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
height: 32,
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: pageIndex.value,
|
||||
onChanged: (index) {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeOut,
|
||||
indicatorSize: 3.25
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
contentShape: const RoundedRectangleBorder(),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBody() => Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: HomePage.kDefaultPadding,
|
||||
right: HomePage.kDefaultPadding * 2,
|
||||
top: HomePage.kDefaultPadding,
|
||||
bottom: HomePage.kDefaultPadding * 2
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildBodyHeader(),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
_buildBodyContent(),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildBodyContent() => PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
pagesController.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: pages[index]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
);
|
||||
|
||||
Widget _buildBodyHeader() {
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(_buildBodyHeaderRootPage(inactiveColor));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
|
||||
elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan(
|
||||
text: pages[pageIndex.value].name,
|
||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < pageStack.length; i++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
|
||||
pagesController.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
);
|
||||
|
||||
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
);
|
||||
|
||||
TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan(
|
||||
text: nestedPageName,
|
||||
recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var j = 0; j < nestedPageIndex - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
pagesController.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
);
|
||||
|
||||
Widget _buildLateralView() => SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(() {
|
||||
pageIndex.value;
|
||||
return ProfileWidget(
|
||||
overlayKey: profileOverlayKey
|
||||
);
|
||||
}),
|
||||
_autoSuggestBox,
|
||||
const SizedBox(height: 12.0),
|
||||
_buildNavigationTrail()
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildNavigationTrail() => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
primary: true,
|
||||
itemCount: pages.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) => _buildNavigationItem(pages[index]),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
Widget _buildNavigationItem(RebootPage page) {
|
||||
final index = page.type.index;
|
||||
return OverlayTarget(
|
||||
key: getOverlayTargetKeyByPage(index),
|
||||
child: HoverButton(
|
||||
onPressed: () {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
builder: (context, states) => Obx(() => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
pageIndex.value == index ? {WidgetState.hovered} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Text(page.name)
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _backButton => StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) => Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 16.0
|
||||
)),
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||
shape: WidgetStateProperty.all(Border())
|
||||
),
|
||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||
if(inDialog) {
|
||||
Navigator.of(appNavigatorKey.currentContext!).pop();
|
||||
}else {
|
||||
final lastPage = appStack.removeLast();
|
||||
pageStack.remove(lastPage);
|
||||
if (lastPage is int) {
|
||||
hitBack = true;
|
||||
pageIndex.value = lastPage;
|
||||
} else {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
style: ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||
if(inDialog) {
|
||||
Navigator.of(appKey.currentContext!).pop();
|
||||
}else {
|
||||
final lastPage = appStack.removeLast();
|
||||
pageStack.remove(lastPage);
|
||||
if (lastPage is int) {
|
||||
hitBack = true;
|
||||
pageIndex.value = lastPage;
|
||||
} else {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
pagesController.add(null);
|
||||
},
|
||||
child: const Icon(FluentIcons.back, size: 12.0),
|
||||
)
|
||||
pagesController.add(null);
|
||||
},
|
||||
child: const Icon(FluentIcons.back, size: 12.0),
|
||||
)
|
||||
);
|
||||
|
||||
GestureDetector get _draggableArea => GestureDetector(
|
||||
onDoubleTap: windowManager.maximizeOrRestore,
|
||||
onHorizontalDragStart: (_) => windowManager.startDragging(),
|
||||
onVerticalDragStart: (_) => windowManager.startDragging()
|
||||
onDoubleTap: appWindow.maximizeOrRestore,
|
||||
onHorizontalDragStart: (_) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (_) => appWindow.startDragging()
|
||||
);
|
||||
|
||||
Widget get _autoSuggestBox => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
Widget get _autoSuggestBox => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
enabled: !firstRun,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
|
||||
final pageIcon = SizedBox.square(
|
||||
@@ -629,6 +364,266 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
));
|
||||
return results;
|
||||
}).toList();
|
||||
|
||||
List<NavigationPaneItem> get _items => pages.map((page) => _createItem(page)).toList();
|
||||
|
||||
NavigationPaneItem _createItem(RebootPage page) => PaneItem(
|
||||
title: Text(page.name),
|
||||
icon: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
body: page
|
||||
);
|
||||
}
|
||||
|
||||
class _PaneBody extends StatefulWidget {
|
||||
const _PaneBody({
|
||||
required this.padding,
|
||||
required this.controller,
|
||||
required this.body
|
||||
});
|
||||
|
||||
final double padding;
|
||||
final StreamController<void> controller;
|
||||
final Widget? body;
|
||||
|
||||
@override
|
||||
State<_PaneBody> createState() => _PaneBodyState();
|
||||
}
|
||||
|
||||
class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final PageController _pageController = PageController(keepPage: true);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
pageIndex.listen((index) => _pageController.jumpToPage(index));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.padding,
|
||||
right: widget.padding * 2,
|
||||
top: widget.padding,
|
||||
bottom: widget.padding * 2
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: widget.controller.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(TextSpan(
|
||||
text: pages[pageIndex.value].name,
|
||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
||||
for(var i = 0; i < pageStack.length; i++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
|
||||
widget.controller.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
));
|
||||
elements.add(TextSpan(
|
||||
text: innerPage,
|
||||
recognizer: i == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
for(var j = 0; j < i - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
widget.controller.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: i == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
widget.controller.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: widget.body ?? const SizedBox.shrink()
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomPane extends NavigationPaneWidget {
|
||||
final SettingsController settingsController;
|
||||
_CustomPane(this.settingsController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, NavigationPaneWidgetData data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
data.appBar,
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
data.pane.header ?? const SizedBox.shrink(),
|
||||
data.pane.autoSuggestBox ?? const SizedBox.shrink(),
|
||||
const SizedBox(height: 12.0),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: data.scrollController,
|
||||
child: ListView.separated(
|
||||
controller: data.scrollController,
|
||||
itemCount: data.pane.items.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = data.pane.items[index] as PaneItem;
|
||||
return Obx(() {
|
||||
final firstRun = settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
onPressed: firstRun ? null : () => data.pane.onChanged?.call(index),
|
||||
builder: (context, states) => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
data.pane.indicator ?? const SizedBox.shrink(),
|
||||
item.icon,
|
||||
const SizedBox(width: 12.0),
|
||||
item.title ?? const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: data.content
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _NestedPageObserver extends NavigatorObserver {
|
||||
|
||||
@@ -1,14 +1,53 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
|
||||
class InfoPage extends RebootPage {
|
||||
static late final List<InfoTile> _infoTiles;
|
||||
static Object? initInfoTiles() {
|
||||
try {
|
||||
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
|
||||
final map = SplayTreeMap<int, InfoTile>();
|
||||
for(final entry in directory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
map[index] = InfoTile(
|
||||
title: Text(questionName),
|
||||
content: Text(entry.readAsStringSync())
|
||||
);
|
||||
}
|
||||
}
|
||||
_infoTiles = map.values.toList(growable: false);
|
||||
return null;
|
||||
}catch(error) {
|
||||
_infoTiles = [];
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -28,52 +67,53 @@ class InfoPage extends RebootPage {
|
||||
}
|
||||
|
||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
||||
static const String _kDiscordInviteUrl = "https://discord.gg/reboot";
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
_discord,
|
||||
_tutorial,
|
||||
_reportBug
|
||||
];
|
||||
|
||||
SettingTile get _reportBug => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.bug_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsBugReportName),
|
||||
subtitle: Text(translations.settingsUtilsBugReportSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kReportBugUrl),
|
||||
child: Text(translations.settingsUtilsBugReportContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _tutorial => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.chat_help_24_regular
|
||||
),
|
||||
title: Text(translations.infoVideoName),
|
||||
subtitle: Text(translations.infoVideoDescription),
|
||||
content: Button(
|
||||
onPressed: () => startOnboarding(),
|
||||
child: Text(translations.infoVideoContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _discord => SettingTile(
|
||||
icon: Icon(
|
||||
Icons.discord_outlined
|
||||
),
|
||||
title: Text(translations.infoDiscordName),
|
||||
subtitle: Text(translations.infoDiscordDescription),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kDiscordInviteUrl),
|
||||
child: Text(translations.infoDiscordContent)
|
||||
)
|
||||
);
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
RxInt _counter = RxInt(kDebugMode ? 0 : 180);
|
||||
late bool _showButton;
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
void initState() {
|
||||
_showButton = _settingsController.firstRun.value;
|
||||
if(_settingsController.firstRun.value) {
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_counter.value <= 0) {
|
||||
_settingsController.firstRun.value = false;
|
||||
timer.cancel();
|
||||
} else {
|
||||
_counter.value = _counter.value - 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> get settings => InfoPage._infoTiles;
|
||||
|
||||
@override
|
||||
Widget? get button {
|
||||
if(!_showButton) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final totalSecondsLeft = _counter.value;
|
||||
final minutesLeft = totalSecondsLeft ~/ 60;
|
||||
final secondsLeft = totalSecondsLeft % 60;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: totalSecondsLeft <= 0 ? () {
|
||||
_showButton = false;
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
} : null,
|
||||
child: Text(
|
||||
totalSecondsLeft <= 0 ? "I have read the instructions"
|
||||
: "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}"
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||
|
||||
class PlayPage extends RebootPage {
|
||||
const PlayPage({Key? key}) : super(key: key);
|
||||
@@ -33,8 +34,10 @@ class PlayPage extends RebootPage {
|
||||
}
|
||||
|
||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
@override
|
||||
Widget? get button => LaunchButton(
|
||||
startLabel: translations.launchFortnite,
|
||||
@@ -44,19 +47,56 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
buildVersionSelector(
|
||||
key: gameVersionOverlayTargetKey
|
||||
),
|
||||
versionSelectSettingTile,
|
||||
_options,
|
||||
_resetDefaults
|
||||
_internalFiles,
|
||||
_multiplayer
|
||||
];
|
||||
|
||||
SettingTile get _multiplayer => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.people_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerName),
|
||||
subtitle: Text(translations.playGameServerDescription),
|
||||
children: [
|
||||
_hostSettingTile,
|
||||
_browseServerTile,
|
||||
_matchmakerTile,
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientName),
|
||||
subtitle: Text(translations.settingsClientDescription),
|
||||
children: [
|
||||
createFileSetting(
|
||||
title: translations.settingsClientConsoleName,
|
||||
description: translations.settingsClientConsoleDescription,
|
||||
controller: _settingsController.unrealEngineConsoleDll
|
||||
),
|
||||
createFileSetting(
|
||||
title: translations.settingsClientAuthName,
|
||||
description: translations.settingsClientAuthDescription,
|
||||
controller: _settingsController.backendDll
|
||||
),
|
||||
createFileSetting(
|
||||
title: translations.settingsClientMemoryName,
|
||||
description: translations.settingsClientMemoryDescription,
|
||||
controller: _settingsController.memoryLeakDll
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _options => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientOptionsName),
|
||||
subtitle: Text(translations.settingsClientOptionsDescription),
|
||||
title: Text(translations.settingsServerOptionsName),
|
||||
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -72,17 +112,33 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.gameResetDefaultsName),
|
||||
subtitle: Text(translations.gameResetDefaultsDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(() {
|
||||
_gameController.reset();
|
||||
}),
|
||||
child: Text(translations.gameResetDefaultsContent),
|
||||
)
|
||||
SettingTile get _matchmakerTile => SettingTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus());
|
||||
},
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerCustomName),
|
||||
subtitle: Text(translations.playGameServerCustomDescription),
|
||||
);
|
||||
|
||||
SettingTile get _browseServerTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.browser.index,
|
||||
icon: Icon(
|
||||
FluentIcons.search_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerBrowserName),
|
||||
subtitle: Text(translations.playGameServerBrowserDescription)
|
||||
);
|
||||
|
||||
SettingTile get _hostSettingTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.host.index,
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerHostName),
|
||||
subtitle: Text(translations.playGameServerHostDescription),
|
||||
);
|
||||
}
|
||||
237
gui/lib/src/page/implementation/server_browser_page.dart
Normal file
237
gui/lib/src/page/implementation/server_browser_page.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
var data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry["id"] != _hostingController.uuid) && entry["discoverable"])
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<Map<String, dynamic>> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: items.isEmpty ? _noServersByQuery : _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildPopulatedListBody(Set<Map<String, dynamic>> items) => ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var entry = items.elementAt(index ~/ 2);
|
||||
var hasPassword = entry["password"] != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry["author"]}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(Map<String, dynamic> entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(Map<String, dynamic> element, String filter) {
|
||||
String? id = element["id"];
|
||||
if(id?.toLowerCase().contains(filter.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var uri = Uri.tryParse(filter);
|
||||
if(uri != null
|
||||
&& uri.host.isNotEmpty
|
||||
&& id?.toLowerCase().contains(uri.host.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? name = element["name"];
|
||||
if(name?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? author = element["author"];
|
||||
if(author?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? description = element["description"];
|
||||
if(description?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
var color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(Map<String, dynamic> entry) {
|
||||
String result = entry['name'];
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(Map<String, dynamic> entry) {
|
||||
String result = entry['description'];
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(Map<String, dynamic> entry) {
|
||||
var version = entry['version'];
|
||||
var versionSplit = version.indexOf("-");
|
||||
var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
|
||||
String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion;
|
||||
if(result.toLowerCase().startsWith("fortnite ")) {
|
||||
result = result.substring(0, 10);
|
||||
}
|
||||
|
||||
return "Fortnite $result";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
@@ -7,14 +7,14 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -23,19 +23,13 @@ import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
|
||||
import '../../util/checks.dart';
|
||||
|
||||
class HostPage extends RebootPage {
|
||||
const HostPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.hostName;
|
||||
String get name => "Host";
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/host.png";
|
||||
@@ -53,7 +47,8 @@ class HostPage extends RebootPage {
|
||||
class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final DllController _dllController = Get.find<DllController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||
|
||||
@@ -72,31 +67,39 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
|
||||
@override
|
||||
Widget get button => LaunchButton(
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
);
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_information,
|
||||
buildVersionSelector(
|
||||
key: hostVersionOverlayTargetKey
|
||||
),
|
||||
versionSelectSettingTile,
|
||||
_options,
|
||||
_internalFiles,
|
||||
_share,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _information => SettingTile(
|
||||
key: hostInfoTileKey,
|
||||
icon: Icon(
|
||||
FluentIcons.info_24_regular
|
||||
),
|
||||
title: Text(translations.hostGameServerName),
|
||||
subtitle: Text(translations.hostGameServerDescription),
|
||||
overlayKey: hostInfoOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -104,14 +107,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerNameName),
|
||||
subtitle: Text(translations.hostGameServerNameDescription),
|
||||
content: OverlayTarget(
|
||||
key: hostInfoNameOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
focusNode: _hostingController.nameFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
onChanged: (_) => _updateServer()
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -120,14 +119,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerDescriptionName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: OverlayTarget(
|
||||
key: hostInfoDescriptionOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
focusNode: _hostingController.descriptionFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
onChanged: (_) => _updateServer()
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -136,32 +131,28 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerPasswordName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: Obx(() => OverlayTarget(
|
||||
key: hostInfoPasswordOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
focusNode: _hostingController.passwordFocusNode,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
),
|
||||
content: Obx(() => TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
@@ -173,9 +164,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
contentWidth: null,
|
||||
content: Obx(() => Row(
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
Text(
|
||||
_hostingController.discoverable.value ? translations.on : translations.off
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
@@ -192,6 +183,105 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _updateController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateController.customGameServer.value = entry.key;
|
||||
_updateController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _updateController.url,
|
||||
validator: checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
_updateController.infoBarEntry?.close();
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _options => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
@@ -199,34 +289,51 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
title: Text(translations.settingsServerOptionsName),
|
||||
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientArgsName),
|
||||
subtitle: Text(translations.settingsClientArgsDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsClientArgsPlaceholder,
|
||||
controller: _hostingController.customLaunchArgs,
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
Obx(() => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.window_console_20_regular
|
||||
),
|
||||
title: Text(translations.gameServerTypeName),
|
||||
subtitle: Text(translations.gameServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_hostingController.type.value.translatedName),
|
||||
items: GameServerType.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _hostingController.type.value = entry
|
||||
)).toList()
|
||||
)),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(translations.hostHeadlessName),
|
||||
subtitle: Text(translations.hostHeadlessDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_hostingController.headless.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.headless.value,
|
||||
onChanged: (value) => _hostingController.headless.value = value
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
Obx(() => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_edit_24_regular
|
||||
),
|
||||
title: Text(translations.hostVirtualDesktopName),
|
||||
subtitle: Text(translations.hostVirtualDesktopDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_hostingController.virtualDesktop.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.virtualDesktop.value,
|
||||
onChanged: (value) => _hostingController.virtualDesktop.value = value
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
Obx(() => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
@@ -235,19 +342,19 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
Text(
|
||||
_hostingController.autoRestart.value ? translations.on : translations.off
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
Obx(() => ToggleSwitch(
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.autoRestart.value,
|
||||
onChanged: (value) => _hostingController.autoRestart.value = value
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
fluentUi.FluentIcons.number_field
|
||||
@@ -257,14 +364,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
contentWidth: 64,
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerPortName,
|
||||
controller: _dllController.gameServerPort,
|
||||
controller: _settingsController.gameServerPort,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -274,7 +381,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostShareName),
|
||||
subtitle: Text(translations.hostShareDescription),
|
||||
overlayKey: hostShareOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -304,7 +410,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final ip = await Ipify.ipv4();
|
||||
entry.close();
|
||||
FlutterClipboard.controlC(ip);
|
||||
_showCopiedIp();
|
||||
_showCopiedIp();
|
||||
}catch(error) {
|
||||
entry?.close();
|
||||
_showCannotCopyIp(error);
|
||||
@@ -316,21 +422,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(() {
|
||||
_hostingController.reset();
|
||||
_dllController.resetServer();
|
||||
}),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
Future<void> _updateServer() async {
|
||||
if(!_hostingController.published()) {
|
||||
return;
|
||||
@@ -338,39 +429,49 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
|
||||
try {
|
||||
_hostingController.publishServer(
|
||||
_hostingController.accountUsername.text,
|
||||
_hostingController.instance.value!.version.toString()
|
||||
_gameController.username.text,
|
||||
_hostingController.instance.value!.versionName
|
||||
);
|
||||
} catch(error) {
|
||||
_showCannotUpdateGameServer(error);
|
||||
}
|
||||
}
|
||||
|
||||
void _showCopiedLink() => showRebootInfoBar(
|
||||
void _showCopiedLink() => showInfoBar(
|
||||
translations.hostShareLinkMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
InfoBarEntry _showCopyingIp() => showRebootInfoBar(
|
||||
InfoBarEntry _showCopyingIp() => showInfoBar(
|
||||
translations.hostShareIpMessageLoading,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
void _showCopiedIp() => showRebootInfoBar(
|
||||
void _showCopiedIp() => showInfoBar(
|
||||
translations.hostShareIpMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
void _showCannotCopyIp(Object error) => showRebootInfoBar(
|
||||
void _showCannotCopyIp(Object error) => showInfoBar(
|
||||
translations.hostShareIpMessageError(error.toString()),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
|
||||
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar(
|
||||
void _showCannotUpdateGameServer(Object error) => showInfoBar(
|
||||
translations.cannotUpdateGameServer(error.toString()),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
String get text {
|
||||
if (this == UpdateTimer.never) {
|
||||
return translations.updateGameServerDllNever;
|
||||
}
|
||||
|
||||
return translations.updateGameServerDllEvery(name);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,12 @@ import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@@ -35,287 +34,58 @@ class SettingsPage extends RebootPage {
|
||||
|
||||
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final DllController _dllController = Get.find<DllController>();
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_language,
|
||||
_theme,
|
||||
_internalFiles,
|
||||
_installationDirectory,
|
||||
];
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientName),
|
||||
subtitle: Text(translations.settingsClientDescription),
|
||||
children: [
|
||||
createFileSetting(
|
||||
title: translations.settingsClientConsoleName,
|
||||
description: translations.settingsClientConsoleDescription,
|
||||
controller: _dllController.unrealEngineConsoleDll,
|
||||
onReset: () {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.console);
|
||||
_dllController.unrealEngineConsoleDll.text = path;
|
||||
_dllController.downloadCriticalDllInteractive(path, force: true);
|
||||
}
|
||||
),
|
||||
createFileSetting(
|
||||
title: translations.settingsClientAuthName,
|
||||
description: translations.settingsClientAuthDescription,
|
||||
controller: _dllController.backendDll,
|
||||
onReset: () {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.starfall);
|
||||
_dllController.backendDll.text = path;
|
||||
_dllController.downloadCriticalDllInteractive(path, force: true);
|
||||
}
|
||||
),
|
||||
_internalFilesServerType,
|
||||
_internalFilesUpdateTimer,
|
||||
_internalFilesServerSource,
|
||||
_internalFilesNewServerSource,
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _internalFilesServerType => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.games_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _dllController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_dllController.customGameServer.value = entry.key;
|
||||
_dllController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_dllController.updateGameServerDll(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
Widget get _internalFilesServerSource => Obx(() {
|
||||
if(!_dllController.customGameServer.value) {
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerOldMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.beforeS20Mirror,
|
||||
onChanged: (value) {
|
||||
if(Uri.tryParse(value) != null) {
|
||||
_dllController.updateGameServerDll(force: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||
),
|
||||
onPressed: () => _dllController.updateGameServerDll(force: true),
|
||||
child: SizedBox.square(
|
||||
dimension: 30,
|
||||
child: Icon(
|
||||
FluentIcons.arrow_download_24_regular
|
||||
),
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||
),
|
||||
onPressed: () {
|
||||
_dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||
_dllController.updateGameServerDll(force: true);
|
||||
},
|
||||
child: SizedBox.square(
|
||||
dimension: 30,
|
||||
child: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}else {
|
||||
return createFileSetting(
|
||||
title: translations.settingsOldServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _dllController.gameServerDll,
|
||||
onReset: () {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
|
||||
_dllController.gameServerDll.text = path;
|
||||
_dllController.downloadCriticalDllInteractive(path);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Widget get _internalFilesNewServerSource => Obx(() {
|
||||
if(!_dllController.customGameServer.value) {
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerNewMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.aboveS20Mirror,
|
||||
onChanged: (value) {
|
||||
if(Uri.tryParse(value) != null) {
|
||||
_dllController.updateGameServerDll(force: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||
),
|
||||
onPressed: () => _dllController.updateGameServerDll(force: true),
|
||||
child: SizedBox.square(
|
||||
dimension: 30,
|
||||
child: Icon(
|
||||
FluentIcons.arrow_download_24_regular
|
||||
),
|
||||
)
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||
),
|
||||
onPressed: () {
|
||||
_dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||
_dllController.updateGameServerDll(force: true);
|
||||
},
|
||||
child: SizedBox.square(
|
||||
dimension: 30,
|
||||
child: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}else {
|
||||
return const SizedBox();
|
||||
}
|
||||
});
|
||||
|
||||
Widget get _internalFilesUpdateTimer => Obx(() {
|
||||
if(_dllController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_dllController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_dllController.timer.value = entry;
|
||||
_dllController.infoBarEntry?.close();
|
||||
_dllController.updateGameServerDll(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
});
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
),
|
||||
_installationDirectory
|
||||
];
|
||||
|
||||
SettingTile get _language => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
SettingTile get _theme => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
SettingTile get _installationDirectory => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
@@ -327,6 +97,15 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
||||
)
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
extension _ThemeModeExtension on ThemeMode {
|
||||
@@ -340,14 +119,4 @@ extension _ThemeModeExtension on ThemeMode {
|
||||
return translations.light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
String get text {
|
||||
if (this == UpdateTimer.never) {
|
||||
return translations.updateGameServerDllNever;
|
||||
}
|
||||
|
||||
return translations.updateGameServerDllEvery(name);
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@ import 'dart:collection';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||
|
||||
@@ -26,15 +26,15 @@ final List<RebootPage> pages = [
|
||||
const SettingsPage()
|
||||
];
|
||||
|
||||
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
|
||||
|
||||
final RxInt pageIndex = RxInt(RebootPageType.play.index);
|
||||
final RxInt pageIndex = _initialPageIndex;
|
||||
RxInt get _initialPageIndex {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
return RxInt(settingsController.firstRun.value ? RebootPageType.info.index : RebootPageType.play.index);
|
||||
}
|
||||
|
||||
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
||||
|
||||
final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey();
|
||||
|
||||
final GlobalKey<OverlayState> appOverlayKey = GlobalKey();
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
|
||||
|
||||
@@ -79,8 +79,4 @@ void addSubPageToStack(String pageName) {
|
||||
appStack.add(identifier);
|
||||
_pagesStack[index]!.add(identifier);
|
||||
pagesController.add(null);
|
||||
}
|
||||
|
||||
GlobalKey<OverlayTargetState> getOverlayTargetKeyByPage(int pageIndex) => _flyoutPageControllers[pageIndex];
|
||||
|
||||
GlobalKey<OverlayTargetState> get pageOverlayTargetKey => _flyoutPageControllers[pageIndex.value];
|
||||
}
|
||||
86
gui/lib/src/util/checks.dart
Normal file
86
gui/lib/src/util/checks.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
String? checkVersion(String? text, List<FortniteVersion> versions) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
if (versions.any((element) => element.name == text)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkChangeVersion(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
var directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkDll(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDllPath;
|
||||
}
|
||||
|
||||
if (!File(text).existsSync()) {
|
||||
return translations.dllDoesNotExist;
|
||||
}
|
||||
|
||||
if (!text.endsWith(".dll")) {
|
||||
return translations.invalidDllExtension;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkMatchmaking(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyHostname;
|
||||
}
|
||||
|
||||
var ipParts = text.split(":");
|
||||
if(ipParts.length > 2){
|
||||
return translations.hostnameFormat;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyURL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
107
gui/lib/src/util/dll.dart
Normal file
107
gui/lib/src/util/dll.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final Map<String, Future<void>> _operations = {};
|
||||
|
||||
Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
return old;
|
||||
}
|
||||
|
||||
final newRun = _downloadCriticalDllInteractive(filePath, silent);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
|
||||
final fileName = path.basename(filePath).toLowerCase();
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName == "reboot.dll") {
|
||||
await _updateController.updateReboot(
|
||||
silent: silent
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(File(filePath).existsSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
entry = showInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
entry = await showInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showInfoBar(
|
||||
translations.downloadDllError(fileName, error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await downloadCriticalDllInteractive(filePath);
|
||||
completer.complete(null);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
}
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
extension InjectableDllExtension on InjectableDll {
|
||||
String get path {
|
||||
final SettingsController settingsController = Get.find<SettingsController>();
|
||||
switch(this){
|
||||
case InjectableDll.reboot:
|
||||
if(_updateController.customGameServer.value) {
|
||||
final file = File(settingsController.gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return rebootDllFile.path;
|
||||
case InjectableDll.console:
|
||||
return settingsController.unrealEngineConsoleDll.text;
|
||||
case InjectableDll.cobalt:
|
||||
return settingsController.backendDll.text;
|
||||
case InjectableDll.memory:
|
||||
return settingsController.memoryLeakDll.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import 'dart:io';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final File launcherLogFile = _createLoggingFile();
|
||||
final File _loggingFile = _createLoggingFile();
|
||||
final Semaphore _semaphore = Semaphore(1);
|
||||
|
||||
File _createLoggingFile() {
|
||||
final file = File("${installationDirectory.path}\\launcher.log");
|
||||
final file = File("${logsDirectory.path}\\launcher.log");
|
||||
file.parent.createSync(recursive: true);
|
||||
if(file.existsSync()) {
|
||||
file.deleteSync();
|
||||
@@ -20,9 +20,9 @@ void log(String message) async {
|
||||
try {
|
||||
await _semaphore.acquire();
|
||||
print(message);
|
||||
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
}catch(error) {
|
||||
print("[LOGGER_ERROR] An error occurred while logging: $error");
|
||||
print(error);
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
@@ -1,69 +1,57 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
const Duration _timeout = Duration(seconds: 5);
|
||||
const Duration _timeout = Duration(seconds: 2);
|
||||
|
||||
Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
|
||||
final completer = Completer<bool>();
|
||||
final start = DateTime.now();
|
||||
(() async {
|
||||
while (!completer.isCompleted && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds) {
|
||||
final result = await pingGameServer(address);
|
||||
if(result) {
|
||||
completer.complete(true);
|
||||
}else {
|
||||
await Future.delayed(_timeout);
|
||||
}
|
||||
}
|
||||
if(!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
}
|
||||
})();
|
||||
return completer;
|
||||
}
|
||||
|
||||
Future<bool> pingGameServer(String address) async {
|
||||
final split = address.split(":");
|
||||
var hostname = split[0];
|
||||
if(isLocalHost(hostname)) {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
|
||||
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||
return await _ping(hostname, port)
|
||||
.timeout(_timeout, onTimeout: () => false);
|
||||
}
|
||||
|
||||
|
||||
Future<bool> _ping(String hostname, int port) async {
|
||||
log("[MATCHMAKER] Pinging $hostname:$port");
|
||||
Future<bool> _pingGameServer(String hostname, int port) async {
|
||||
RawDatagramSocket? socket;
|
||||
try {
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
await for (final event in socket) {
|
||||
log("[MATCHMAKER] Event: $event");
|
||||
final dataToSend = utf8.encode(DateTime.now().toIso8601String());
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
await for (var event in socket) {
|
||||
switch(event) {
|
||||
case RawSocketEvent.read:
|
||||
log("[MATCHMAKER] Success");
|
||||
return true;
|
||||
case RawSocketEvent.write:
|
||||
log("[MATCHMAKER] Sending data");
|
||||
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
return true;
|
||||
case RawSocketEvent.readClosed:
|
||||
case RawSocketEvent.closed:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}catch(error) {
|
||||
log("[MATCHMAKER] Error: $error");
|
||||
return false;
|
||||
}finally {
|
||||
socket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> get _timeoutFuture => Future.delayed(_timeout).then((value) => false);
|
||||
|
||||
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
|
||||
var start = DateTime.now();
|
||||
var firstTime = true;
|
||||
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
|
||||
var split = address.split(":");
|
||||
var hostname = split[0];
|
||||
if(isLocalHost(hostname)) {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
|
||||
var port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||
var result = await Future.any([_timeoutFuture, _pingGameServer(hostname, port)]);
|
||||
if(result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(firstTime) {
|
||||
firstTime = false;
|
||||
}else {
|
||||
await Future.delayed(_timeout);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -3,11 +3,9 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||
|
||||
@@ -25,25 +23,6 @@ bool get isWin11 {
|
||||
return intBuild != null && intBuild > 22000;
|
||||
}
|
||||
|
||||
Future<String?> openFolderPicker(String title) async {
|
||||
FilePicker.platform = FilePickerWindows();
|
||||
return await FilePicker.platform.getDirectoryPath(dialogTitle: title);
|
||||
}
|
||||
|
||||
Future<String?> openFilePicker(String extension) async {
|
||||
FilePicker.platform = FilePickerWindows();
|
||||
var result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowMultiple: false,
|
||||
allowedExtensions: [extension]
|
||||
);
|
||||
if(result == null || result.files.isEmpty){
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.files.first.path;
|
||||
}
|
||||
|
||||
bool get isDarkMode =>
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
|
||||
|
||||
@@ -97,7 +76,7 @@ class IVirtualDesktop extends IUnknown {
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return _convertFromHString(result.value);
|
||||
return convertFromHString(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +263,7 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
||||
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, COMObject, int)>()(
|
||||
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
|
||||
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
|
||||
if (code != 0) {
|
||||
throw WindowsException(code);
|
||||
}
|
||||
@@ -373,7 +352,7 @@ List<int> _getHWnds(int pid, String? excludedWindowName) {
|
||||
result.ref.excluded = excludedWindowName.toNativeUtf16();
|
||||
}
|
||||
|
||||
EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
|
||||
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
|
||||
final length = result.ref.HWndLength;
|
||||
final HWndsPointer = result.ref.HWnd;
|
||||
if(HWndsPointer == nullptr) {
|
||||
@@ -401,7 +380,7 @@ class VirtualDesktopManager {
|
||||
}
|
||||
|
||||
final hr = CoInitializeEx(
|
||||
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
|
||||
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
if (FAILED(hr)) {
|
||||
throw WindowsException(hr);
|
||||
}
|
||||
@@ -472,23 +451,3 @@ class VirtualDesktopManager {
|
||||
void setDesktopName(IVirtualDesktop desktop, String newName) =>
|
||||
windowManager.setDesktopName(desktop, newName);
|
||||
}
|
||||
|
||||
String _convertFromHString(int hstring) =>
|
||||
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
|
||||
|
||||
int _convertToHString(String string) {
|
||||
final hString = calloc<HSTRING>();
|
||||
final stringPtr = string.toNativeUtf16();
|
||||
try {
|
||||
final hr = WindowsCreateString(stringPtr, string.length, hString);
|
||||
if (FAILED(hr)) throw WindowsException(hr);
|
||||
return hString.value;
|
||||
} finally {
|
||||
free(stringPtr);
|
||||
free(hString);
|
||||
}
|
||||
}
|
||||
|
||||
extension WindowManagerExtension on WindowManager {
|
||||
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
|
||||
}
|
||||
17
gui/lib/src/util/picker.dart
Normal file
17
gui/lib/src/util/picker.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
Future<String?> openFolderPicker(String title) async =>
|
||||
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
|
||||
|
||||
Future<String?> openFilePicker(String extension) async {
|
||||
var result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowMultiple: false,
|
||||
allowedExtensions: [extension]
|
||||
);
|
||||
if(result == null || result.files.isEmpty){
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.files.first.path;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
AppLocalizations? _translations;
|
||||
bool _init = false;
|
||||
@@ -20,16 +19,3 @@ void loadTranslations(BuildContext context) {
|
||||
}
|
||||
|
||||
String get currentLocale => Intl.getCurrentLocale().split("_")[0];
|
||||
|
||||
extension GameServerTypeExtension on GameServerType {
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case GameServerType.headless:
|
||||
return translations.gameServerTypeHeadless;
|
||||
case GameServerType.virtualWindow:
|
||||
return translations.gameServerTypeVirtualWindow;
|
||||
case GameServerType.window:
|
||||
return translations.gameServerTypeWindow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,4 @@ extension IterableExtension<E> on Iterable<E> {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExtension on String {
|
||||
String? after(String leading) {
|
||||
final index = indexOf(leading);
|
||||
if(index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substring(index + leading.length);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
final _hive = HKEY_CURRENT_USER;
|
||||
|
||||
void registerUrlProtocol(String scheme, {String? executable, List<String>? arguments}) {
|
||||
final prefix = _regPrefix(scheme);
|
||||
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
|
||||
final args = _getArguments(arguments).map((a) => _sanitize(a));
|
||||
final cmd =
|
||||
'${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
|
||||
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
|
||||
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
|
||||
_regCreateStringKey(_hive, prefix + '\\shell\\open\\command', '', cmd);
|
||||
}
|
||||
|
||||
void unregisterUrlProtocol(String scheme) {
|
||||
final txtKey = TEXT(_regPrefix(scheme));
|
||||
try {
|
||||
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
|
||||
} finally {
|
||||
free(txtKey);
|
||||
}
|
||||
}
|
||||
|
||||
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
|
||||
|
||||
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
|
||||
final txtKey = TEXT(key);
|
||||
final txtValue = TEXT(valueName);
|
||||
final txtData = TEXT(data);
|
||||
try {
|
||||
return RegSetKeyValue(
|
||||
hKey,
|
||||
txtKey,
|
||||
txtValue,
|
||||
REG_VALUE_TYPE.REG_SZ,
|
||||
txtData,
|
||||
txtData.length * 2 + 2,
|
||||
);
|
||||
} finally {
|
||||
free(txtKey);
|
||||
free(txtValue);
|
||||
free(txtData);
|
||||
}
|
||||
}
|
||||
|
||||
String _sanitize(String value) {
|
||||
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
|
||||
return '"$value"';
|
||||
}
|
||||
|
||||
List<String> _getArguments(List<String>? arguments) {
|
||||
if (arguments == null) return ['%s'];
|
||||
|
||||
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
|
||||
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
103
gui/lib/src/widget/add_local_version.dart
Normal file
103
gui/lib/src/widget/add_local_version.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_name_input.dart';
|
||||
|
||||
class AddLocalVersion extends StatefulWidget {
|
||||
const AddLocalVersion({Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<AddLocalVersion> createState() => _AddLocalVersionState();
|
||||
}
|
||||
|
||||
class _AddLocalVersionState extends State<AddLocalVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _gamePathController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_gamePathController.addListener(() async {
|
||||
var file = Directory(_gamePathController.text);
|
||||
if(await file.exists()) {
|
||||
if(_nameController.text.isEmpty) {
|
||||
var text = path.basename(_gamePathController.text);
|
||||
_nameController.text = text;
|
||||
_nameController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.localBuildsWarning),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: translations.gameFolderPlaceholder,
|
||||
windowTitle: translations.gameFolderPlaceWindowTitle,
|
||||
controller: _gamePathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.saveLocalVersion,
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_gamePathController.text)
|
||||
)));
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
345
gui/lib/src/widget/add_server_version.dart
Normal file
345
gui/lib/src/widget/add_server_version.dart
Normal file
@@ -0,0 +1,345 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.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/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_name_input.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
const AddServerVersion({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddServerVersion> createState() => _AddServerVersionState();
|
||||
}
|
||||
|
||||
class _AddServerVersionState extends State<AddServerVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final Rx<DownloadStatus> _status = Rx(DownloadStatus.form);
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return FormDialog(
|
||||
content: _formBody,
|
||||
buttons: _formButtons
|
||||
);
|
||||
}
|
||||
);
|
||||
case DownloadStatus.downloading:
|
||||
case DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
||||
);
|
||||
case DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: translations.download,
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final build = _buildController.selectedBuild;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(message.progress, message.minutesLeft, message.extracting);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(double progress, int? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_status.value == DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _formBody => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.buildInstallationDirectory,
|
||||
placeholder: translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildSelector() => InfoLabel(
|
||||
label: translations.build,
|
||||
child: Obx(() => ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _builds,
|
||||
value: _buildController.selectedBuild,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_buildController.selectedBuild = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
List<ComboBoxItem<FortniteBuild>> get _builds => _buildController.builds!
|
||||
.map((element) => _buildItem(element))
|
||||
.toList();
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: "Stop",
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_diskSpace.disks.isEmpty){
|
||||
return;
|
||||
}
|
||||
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _buildController.selectedBuild;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
final buildName = build.version.toString();
|
||||
_nameController.text = buildName;
|
||||
_nameController.selection = TextSelection.collapsed(offset: buildName.length);
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/picker.dart';
|
||||
|
||||
class FileSelector extends StatefulWidget {
|
||||
final String placeholder;
|
||||
@@ -19,10 +19,10 @@ class FileSelector extends StatefulWidget {
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.folder,
|
||||
required this.allowNavigator,
|
||||
this.label,
|
||||
this.extension,
|
||||
this.validatorMode,
|
||||
this.allowNavigator = true,
|
||||
Key? key})
|
||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||
super(key: key);
|
||||
|
||||
@@ -1,121 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
const double _kButtonDimensions = 30;
|
||||
const double _kButtonSpacing = 8;
|
||||
|
||||
// FIXME: If the user clicks on the reset button, the text field checker won't be called
|
||||
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
|
||||
final obx = RxString(controller.text);
|
||||
controller.addListener(() => obx.value = controller.text);
|
||||
final selecting = RxBool(false);
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.document_24_regular
|
||||
),
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
|
||||
content: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FileSelector(
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: _checkDll,
|
||||
extension: "dll",
|
||||
folder: false,
|
||||
validatorMode: AutovalidateMode.always,
|
||||
allowNavigator: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _kButtonSpacing),
|
||||
Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
||||
),
|
||||
child: Tooltip(
|
||||
message: translations.selectFile,
|
||||
child: Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||
),
|
||||
onPressed: () => _onPressed(selecting, controller),
|
||||
child: SizedBox.square(
|
||||
dimension: _kButtonDimensions,
|
||||
child: Icon(
|
||||
fluentIcons.FluentIcons.open_folder_horizontal
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(width: _kButtonSpacing),
|
||||
Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
||||
),
|
||||
child: Tooltip(
|
||||
message: translations.reset,
|
||||
child: Button(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||
),
|
||||
onPressed: onReset,
|
||||
child: SizedBox.square(
|
||||
dimension: _kButtonDimensions,
|
||||
child: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressed(RxBool selecting, TextEditingController controller) {
|
||||
if(selecting.value){
|
||||
return;
|
||||
}
|
||||
|
||||
selecting.value = true;
|
||||
compute(openFilePicker, "dll")
|
||||
.then((value) => _updateText(controller, value))
|
||||
.then((_) => selecting.value = false);
|
||||
}
|
||||
|
||||
void _updateText(TextEditingController controller, String? value) {
|
||||
final text = value ?? controller.text;
|
||||
controller.text = text;
|
||||
controller.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
|
||||
String? _checkDll(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDllPath;
|
||||
}
|
||||
|
||||
final file = File(text);
|
||||
if (!file.existsSync()) {
|
||||
return translations.dllDoesNotExist;
|
||||
}
|
||||
|
||||
if (!text.endsWith(".dll")) {
|
||||
return translations.invalidDllExtension;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.document_24_regular
|
||||
),
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
content: FileSelector(
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
extension: "dll",
|
||||
folder: false
|
||||
)
|
||||
);
|
||||
@@ -4,24 +4,26 @@ import 'dart:io';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
final bool host;
|
||||
@@ -40,12 +42,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final DllController _dllController = Get.find<DllController>();
|
||||
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
InfoBarEntry? _gameClientInfoBar;
|
||||
InfoBarEntry? _gameServerInfoBar;
|
||||
CancelableOperation? _operation;
|
||||
Completer? _pingOperation;
|
||||
IVirtualDesktop? _virtualDesktop;
|
||||
|
||||
@override
|
||||
@@ -68,9 +68,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started;
|
||||
|
||||
Future<void> _toggle({bool? host}) async {
|
||||
Future<void> _toggle({bool? host, bool forceGUI = false}) async {
|
||||
host ??= widget.host;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Toggling state");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
|
||||
if (host ? _hostingController.started() : _gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(
|
||||
@@ -94,13 +94,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
||||
for (final injectable in InjectableDll.values) {
|
||||
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
|
||||
if(await _getDllFileOrStop(injectable, host) == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final executable = await version.shippingExecutable;
|
||||
final executable = version.gameExecutable;
|
||||
if(executable == null){
|
||||
log("[${host ? 'HOST' : 'GAME'}] No executable found");
|
||||
_onStop(
|
||||
@@ -120,28 +120,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
||||
final serverType = _hostingController.type.value;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
||||
final headless = !forceGUI && _hostingController.headless.value;
|
||||
final virtualDesktop = _hostingController.virtualDesktop.value;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, virtualDesktop, false);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
||||
final started = host ? _hostingController.started() : _gameController.started();
|
||||
if(!started) {
|
||||
result?.kill();
|
||||
return;
|
||||
await _startGameProcesses(version, host, headless, virtualDesktop, linkedHostingInstance);
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget();
|
||||
}
|
||||
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
|
||||
}else {
|
||||
if(linkedHostingInstance != null || host){
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
} on ProcessException catch (exception, stackTrace) {
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedVersionError,
|
||||
error: exception.toString(),
|
||||
stackTrace: stackTrace
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onStop(
|
||||
reason: _StopReason.unknownError,
|
||||
@@ -151,14 +142,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, GameServerType hostType, bool forceLinkedHosting) async {
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||
if(host){
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||
if(_backendController.type.value != ServerType.embedded || !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||
return null;
|
||||
}
|
||||
@@ -168,38 +159,33 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return null;
|
||||
}
|
||||
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer(host);
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer();
|
||||
if(!response) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
|
||||
final instance = await _startGameProcesses(version, true, hostType, null);
|
||||
final instance = await _startGameProcesses(version, true, headless, virtualDesktop, null);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
|
||||
_setStarted(true, true);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<bool> _askForAutomaticGameServer(bool host) async {
|
||||
if (host ? !_hostingController.started() : !_gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(reason: _StopReason.normal);
|
||||
return false;
|
||||
}
|
||||
|
||||
final result = await showRebootDialog<bool>(
|
||||
Future<bool> _askForAutomaticGameServer() async {
|
||||
final result = await showAppDialog<bool>(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.automaticGameServerDialogContent,
|
||||
text: "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. "
|
||||
"If you don't want to join another player's server, you should start a game server. This is necessary to be able to play: for more information check the Info tab in the launcher.",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: translations.automaticGameServerDialogIgnore
|
||||
text: "Ignore"
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
text: translations.automaticGameServerDialogStart,
|
||||
text: "Start server",
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
@@ -209,7 +195,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
|
||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
|
||||
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
|
||||
@@ -219,9 +205,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started paused eac: $eacProcess");
|
||||
final executable = await version.shippingExecutable;
|
||||
final executable = host && headless ? await version.headlessGameExecutable : version.gameExecutable;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Using game path: ${executable?.path}");
|
||||
final gameProcess = await _createGameProcess(version, executable!, host, hostType, linkedHosting);
|
||||
final gameProcess = await _createGameProcess(version, executable!, host, headless, virtualDesktop, linkedHosting);
|
||||
if(gameProcess == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
|
||||
return null;
|
||||
@@ -229,11 +215,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||
final instance = GameInstance(
|
||||
version: version.content,
|
||||
versionName: version.name,
|
||||
gamePid: gameProcess,
|
||||
launcherPid: launcherProcess,
|
||||
eacPid: eacProcess,
|
||||
serverType: host ? hostType : null,
|
||||
hosting: host,
|
||||
child: linkedHosting
|
||||
);
|
||||
if(host){
|
||||
@@ -242,61 +228,68 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}else{
|
||||
_gameController.instance.value = instance;
|
||||
}
|
||||
await _injectOrShowError(InjectableDll.starfall, host);
|
||||
await _injectOrShowError(InjectableDll.cobalt, host);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
|
||||
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||
final gameArgs = createRebootArgs(
|
||||
host ? _hostingController.accountUsername.text : _gameController.username.text,
|
||||
host ? _hostingController.accountPassword.text :_gameController.password.text,
|
||||
_gameController.username.text,
|
||||
_gameController.password.text,
|
||||
host,
|
||||
hostType,
|
||||
false,
|
||||
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
|
||||
_hostingController.headless.value,
|
||||
""
|
||||
);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generated game args: $gameArgs");
|
||||
final gameProcess = await startProcess(
|
||||
executable: executable,
|
||||
args: gameArgs,
|
||||
useTempBatch: false,
|
||||
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
wrapProcess: false,
|
||||
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||
);
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
void onGameOutput(String line, bool error) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
|
||||
|
||||
handleGameOutput(
|
||||
line: line,
|
||||
host: host,
|
||||
onShutdown: () => _onStop(reason: _StopReason.normal),
|
||||
onTokenError: () => _onStop(reason: _StopReason.tokenError),
|
||||
onBuildCorrupted: () {
|
||||
if(instance == null) {
|
||||
return;
|
||||
}else if(!instance.launched) {
|
||||
_onStop(reason: _StopReason.corruptedVersionError);
|
||||
}else {
|
||||
_onStop(reason: _StopReason.crash);
|
||||
}
|
||||
},
|
||||
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError),
|
||||
onLoggedIn: () =>_onLoggedIn(host),
|
||||
onMatchEnd: () => _onMatchEnd(version),
|
||||
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
|
||||
onMatchEnd: () => _onMatchEnd(version, virtualDesktop),
|
||||
onDisplayAttached: () => _onDisplayAttached(headless, virtualDesktop, version)
|
||||
);
|
||||
}
|
||||
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
|
||||
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
||||
gameProcess.exitCode.then((_) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
|
||||
_onStop(
|
||||
if(instance == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!host || instance.launched) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): stop signal");
|
||||
_onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: host
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): restart signal");
|
||||
instance.launched = true;
|
||||
await _onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: host
|
||||
host: true
|
||||
);
|
||||
await _toggle(
|
||||
forceGUI: true,
|
||||
host: true
|
||||
);
|
||||
});
|
||||
return gameProcess.pid;
|
||||
@@ -309,26 +302,23 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
final process = await startProcess(
|
||||
executable: file,
|
||||
useTempBatch: false,
|
||||
name: "${version.content}-${basenameWithoutExtension(file.path)}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
wrapProcess: false,
|
||||
name: "${version.name}-${basenameWithoutExtension(file.path)}"
|
||||
);
|
||||
final pid = process.pid;
|
||||
suspend(pid);
|
||||
return pid;
|
||||
}
|
||||
|
||||
Future<void> _onDisplayAttached(bool host, GameServerType type, FortniteVersion version) async {
|
||||
if(host && type == GameServerType.virtualWindow) {
|
||||
Future<void> _onDisplayAttached(bool headless, bool virtualDesktop, FortniteVersion version) async {
|
||||
if(!headless && virtualDesktop) {
|
||||
final hostingInstance = _hostingController.instance.value;
|
||||
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
|
||||
hostingInstance.movedToVirtualDesktop = true;
|
||||
try {
|
||||
final windowManager = VirtualDesktopManager.getInstance();
|
||||
_virtualDesktop = windowManager.createDesktop();
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
|
||||
var success = false;
|
||||
try {
|
||||
success = await windowManager.moveWindowToDesktop(
|
||||
@@ -356,7 +346,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onMatchEnd(FortniteVersion version) {
|
||||
void _onMatchEnd(FortniteVersion version, bool virtualDesktop) {
|
||||
if(_hostingController.autoRestart.value) {
|
||||
final notification = LocalNotification(
|
||||
title: translations.gameServerEnd,
|
||||
@@ -398,11 +388,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(instance != null && !instance.launched) {
|
||||
instance.launched = true;
|
||||
instance.tokenError = false;
|
||||
await _injectOrShowError(InjectableDll.memory, host);
|
||||
if(!host){
|
||||
await _injectOrShowError(InjectableDll.console, host);
|
||||
_onGameClientInjected();
|
||||
}else {
|
||||
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
|
||||
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
|
||||
if(gameServerPort != null) {
|
||||
await killProcessByPort(gameServerPort);
|
||||
}
|
||||
@@ -414,7 +405,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onGameClientInjected() {
|
||||
_gameClientInfoBar?.close();
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.gameClientStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -422,39 +413,32 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
Future<void> _onGameServerInjected() async {
|
||||
if(_gameServerInfoBar != null) {
|
||||
_gameServerInfoBar?.close();
|
||||
}else {
|
||||
_gameClientInfoBar?.close();
|
||||
}
|
||||
|
||||
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
|
||||
_gameServerInfoBar?.close();
|
||||
final theme = FluentTheme.of(appKey.currentContext!);
|
||||
try {
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
translations.waitingForGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
final gameServerPort = _dllController.gameServerPort.text;
|
||||
final pingOperation = pingGameServerOrTimeout(
|
||||
final gameServerPort = _settingsController.gameServerPort.text;
|
||||
final localPingResult = await pingGameServer(
|
||||
"127.0.0.1:$gameServerPort",
|
||||
const Duration(minutes: 2)
|
||||
timeout: const Duration(minutes: 2)
|
||||
);
|
||||
this._pingOperation = pingOperation;
|
||||
final localPingResult = await pingOperation.future;
|
||||
_gameServerInfoBar?.close();
|
||||
if (!localPingResult) {
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.gameServerStartWarning,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
_backendController.joinLocalhost();
|
||||
_backendController.joinLocalHost();
|
||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||
if (!accessible) {
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.gameServerStartLocalWarning,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
@@ -463,10 +447,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
await _hostingController.publishServer(
|
||||
_hostingController.accountUsername.text,
|
||||
_hostingController.instance.value!.version.toString(),
|
||||
_gameController.username.text,
|
||||
_hostingController.instance.value!.versionName,
|
||||
);
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.gameServerStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -478,36 +462,35 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||
try {
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
translations.checkingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
final publicIp = await Ipify.ipv4();
|
||||
final available = await pingGameServer("$publicIp:$gameServerPort");
|
||||
if(available) {
|
||||
_gameServerInfoBar?.close();
|
||||
final externalResult = await pingGameServer("$publicIp:$gameServerPort");
|
||||
if (externalResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final pingOperation = pingGameServerOrTimeout(
|
||||
_gameServerInfoBar?.close();
|
||||
final future = pingGameServer(
|
||||
"$publicIp:$gameServerPort",
|
||||
const Duration(days: 365)
|
||||
timeout: const Duration(days: 365)
|
||||
);
|
||||
this._pingOperation = pingOperation;
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
translations.checkGameServerFixMessage(gameServerPort),
|
||||
action: Button(
|
||||
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
|
||||
onPressed: () async {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
},
|
||||
child: Text(translations.checkGameServerFixAction),
|
||||
),
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: null,
|
||||
loading: true
|
||||
);
|
||||
final result = await pingOperation.future;
|
||||
_gameServerInfoBar?.close();
|
||||
return result;
|
||||
return await future;
|
||||
}finally {
|
||||
_gameServerInfoBar?.close();
|
||||
}
|
||||
@@ -515,21 +498,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||
if(host == null) {
|
||||
try {
|
||||
_pingOperation?.complete(false);
|
||||
}catch(_) {
|
||||
// Ignore: might be running, don't bother checking
|
||||
} finally {
|
||||
_pingOperation = null;
|
||||
}
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
_backendController.cancelInteractive();
|
||||
await _backendController.worker?.cancel();
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
|
||||
if(host){
|
||||
_hostingController.instance.value = null;
|
||||
}else {
|
||||
@@ -551,17 +526,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_hostingController.discardServer();
|
||||
}
|
||||
|
||||
if(reason == _StopReason.normal) {
|
||||
instance?.launched = true;
|
||||
}
|
||||
if(instance != null) {
|
||||
if(reason == _StopReason.normal) {
|
||||
instance.launched = true;
|
||||
}
|
||||
|
||||
instance?.kill();
|
||||
final child = instance?.child;
|
||||
if(child != null) {
|
||||
await _onStop(
|
||||
reason: reason,
|
||||
host: child.serverType != null
|
||||
);
|
||||
instance.kill();
|
||||
final child = instance.child;
|
||||
if(child != null) {
|
||||
await _onStop(
|
||||
reason: reason,
|
||||
host: child.hosting
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_setStarted(host, false);
|
||||
@@ -579,14 +556,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
case _StopReason.normal:
|
||||
break;
|
||||
case _StopReason.missingVersionError:
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.missingVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.missingExecutableError:
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.missingExecutableError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -594,7 +571,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
break;
|
||||
case _StopReason.exitCode:
|
||||
if(instance != null && !instance.launched) {
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -602,51 +579,28 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
break;
|
||||
case _StopReason.corruptedVersionError:
|
||||
showRebootInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
action: Button(
|
||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||
child: Text(translations.openLog),
|
||||
)
|
||||
);
|
||||
break;
|
||||
case _StopReason.corruptedDllError:
|
||||
showRebootInfoBar(
|
||||
translations.corruptedDllError(error ?? translations.unknownError),
|
||||
showInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.missingCustomDllError:
|
||||
showRebootInfoBar(
|
||||
translations.missingCustomDllError(error!),
|
||||
case _StopReason.corruptedDllError:
|
||||
showInfoBar(
|
||||
translations.corruptedDllError(error!),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.tokenError:
|
||||
_backendController.stop();
|
||||
showRebootInfoBar(
|
||||
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
action: Button(
|
||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||
child: Text(translations.openLog),
|
||||
)
|
||||
);
|
||||
break;
|
||||
case _StopReason.crash:
|
||||
showRebootInfoBar(
|
||||
translations.fortniteCrashError(host ? "game server" : "client"),
|
||||
showInfoBar(
|
||||
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.unknownError:
|
||||
showRebootInfoBar(
|
||||
showInfoBar(
|
||||
translations.unknownFortniteError(error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -665,13 +619,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
try {
|
||||
final gameProcess = instance.gamePid;
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
|
||||
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
|
||||
final dllPath = await _getDllFileOrStop(injectable, hosting);
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
|
||||
if(dllPath == null) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
||||
_onStop(
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
reason: _StopReason.corruptedDllError,
|
||||
host: hosting
|
||||
);
|
||||
return;
|
||||
@@ -692,66 +645,33 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
|
||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||
final (file, customDll) = _dllController.getInjectableData(version, injectable);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
||||
final path = injectable.path;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
|
||||
final file = File(path);
|
||||
if(await file.exists()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||
return file;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
|
||||
if(customDll) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
|
||||
_onStop(
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
||||
await _dllController.downloadCriticalDllInteractive(file.path, force: true);
|
||||
await downloadCriticalDllInteractive(path);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||
return _getDllFileOrStop(version, injectable, host, true);
|
||||
return _getDllFileOrStop(injectable, host);
|
||||
}
|
||||
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
||||
translations.launchingGameServer,
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showInfoBar(
|
||||
translations.launchingHeadlessServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
||||
return _gameClientInfoBar = showRebootInfoBar(
|
||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
||||
loading: true,
|
||||
duration: null,
|
||||
action: Obx(() {
|
||||
if(_hostingController.started.value || linkedHosting) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 2.0
|
||||
),
|
||||
child: Button(
|
||||
onPressed: () async {
|
||||
_backendController.joinLocalhost();
|
||||
if(!_hostingController.started.value) {
|
||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
|
||||
_gameClientInfoBar?.close();
|
||||
_showLaunchingGameClientWidget(version, hostType, true);
|
||||
}
|
||||
},
|
||||
child: Text(translations.startGameServer),
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
InfoBarEntry _showLaunchingGameClientWidget() => _gameClientInfoBar = showInfoBar(
|
||||
translations.launchingGameClient,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
|
||||
enum _StopReason {
|
||||
@@ -759,14 +679,12 @@ enum _StopReason {
|
||||
missingVersionError,
|
||||
missingExecutableError,
|
||||
corruptedVersionError,
|
||||
missingCustomDllError,
|
||||
corruptedDllError,
|
||||
backendError,
|
||||
matchmakerError,
|
||||
tokenError,
|
||||
unknownError,
|
||||
exitCode,
|
||||
crash;
|
||||
exitCode;
|
||||
|
||||
bool get isError => name.contains("Error");
|
||||
}
|
||||
@@ -24,22 +24,19 @@ class InfoBarAreaState extends State<InfoBarArea> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) => Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: hasPageButton ? 72.0 : 16.0
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _children.value.map((child) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12.0
|
||||
),
|
||||
child: child
|
||||
)).toList(growable: false)
|
||||
),
|
||||
))
|
||||
);
|
||||
Widget build(BuildContext context) => Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: hasPageButton ? 72.0 : 16.0
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _children.value.map((child) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12.0
|
||||
),
|
||||
child: child
|
||||
)).toList(growable: false)
|
||||
),
|
||||
));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user