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...");
|
stdout.writeln("Launching game...");
|
||||||
var executable = version.shippingExecutable;
|
var executable = version.gameExecutable;
|
||||||
if(executable == null){
|
if(executable == null){
|
||||||
throw Exception("Missing game executable at: ${version.location.path}");
|
throw Exception("Missing game executable at: ${version.location.path}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Future<void> startGame() async {
|
|||||||
await _startLauncherProcess(version);
|
await _startLauncherProcess(version);
|
||||||
await _startEacProcess(version);
|
await _startEacProcess(version);
|
||||||
|
|
||||||
var executable = await version.shippingExecutable;
|
var executable = await version.gameExecutable;
|
||||||
if (executable == null) {
|
if (executable == null) {
|
||||||
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
|
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...");
|
stdout.writeln("Starting backend server...");
|
||||||
switch(type){
|
switch(type){
|
||||||
case ServerType.local:
|
case ServerType.local:
|
||||||
final result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
|
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
|
||||||
if(result == null){
|
if(result == null){
|
||||||
throw Exception("Local backend server is not running");
|
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/server_type.dart';
|
||||||
export 'package:reboot_common/src/model/update_status.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/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/model/dll.dart';
|
||||||
export 'package:reboot_common/src/util/backend.dart';
|
export 'package:reboot_common/src/util/backend.dart';
|
||||||
export 'package:reboot_common/src/util/build.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/network.dart';
|
||||||
export 'package:reboot_common/src/util/patcher.dart';
|
export 'package:reboot_common/src/util/patcher.dart';
|
||||||
export 'package:reboot_common/src/util/path.dart';
|
export 'package:reboot_common/src/util/path.dart';
|
||||||
export 'package:reboot_common/src/util/process.dart';
|
export 'package:reboot_common/src/util/process.dart';
|
||||||
export 'package:reboot_common/src/util/log.dart';
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
const String kDefaultBackendHost = "127.0.0.1";
|
const String kDefaultBackendHost = "127.0.0.1";
|
||||||
const int kDefaultBackendPort = 3551;
|
const int kDefaultBackendPort = 3551;
|
||||||
const int kDefaultXmppPort = 80;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
const String kDefaultPlayerName = "Player";
|
const String kDefaultPlayerName = "Player";
|
||||||
const String kDefaultHostName = "Host";
|
|
||||||
const String kDefaultGameServerHost = "127.0.0.1";
|
const String kDefaultGameServerHost = "127.0.0.1";
|
||||||
const String kDefaultGameServerPort = "7777";
|
const String kDefaultGameServerPort = "7777";
|
||||||
const String kInitializedLine = "Game Engine Initialized";
|
const String kInitializedLine = "Game Engine Initialized";
|
||||||
@@ -12,7 +11,7 @@ const List<String> kCorruptedBuildErrors = [
|
|||||||
"Critical error",
|
"Critical error",
|
||||||
"when 0 bytes remain",
|
"when 0 bytes remain",
|
||||||
"Pak chunk signature verification failed!",
|
"Pak chunk signature verification failed!",
|
||||||
"LogWindows:Error: Fatal error!"
|
"Couldn't find pak signature file"
|
||||||
];
|
];
|
||||||
const List<String> kCannotConnectErrors = [
|
const List<String> kCannotConnectErrors = [
|
||||||
"port 3551 failed: Connection refused",
|
"port 3551 failed: Connection refused",
|
||||||
|
|||||||
@@ -5,50 +5,38 @@ import 'package:path/path.dart' as path;
|
|||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
extension FortniteVersionExtension on FortniteVersion {
|
extension FortniteVersionExtension on FortniteVersion {
|
||||||
static String _marker = "FortniteClient.mod";
|
static File? findExecutable(Directory directory, String name) {
|
||||||
|
|
||||||
static File? findFile(Directory directory, String name) {
|
|
||||||
try{
|
try{
|
||||||
for(final child in directory.listSync()) {
|
final result = directory.listSync(recursive: true)
|
||||||
if(child is Directory) {
|
.firstWhere((element) => path.basename(element.path) == name);
|
||||||
if(!path.basename(child.path).startsWith("\.")) {
|
return File(result.path);
|
||||||
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;
|
|
||||||
}catch(_){
|
}catch(_){
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> get shippingExecutable async {
|
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||||
final result = findFile(location, "FortniteClient-Win64-Shipping.exe");
|
|
||||||
if(result == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final marker = findFile(location, _marker);
|
Future<File?> get headlessGameExecutable async {
|
||||||
if(marker != null) {
|
final result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
|
||||||
|
if(result != null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Isolate.run(() => patchHeadless(result));
|
final original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||||
await File("${location.path}\\$_marker").create();
|
if(original == null) {
|
||||||
return result;
|
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 {
|
enum InjectableDll {
|
||||||
console,
|
console,
|
||||||
starfall,
|
cobalt,
|
||||||
reboot,
|
reboot,
|
||||||
}
|
memory
|
||||||
|
|
||||||
extension InjectableDllVersionAware on InjectableDll {
|
|
||||||
bool get isVersionDependent => this == InjectableDll.reboot;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:version/version.dart';
|
|
||||||
|
|
||||||
class FortniteBuild {
|
class FortniteBuild {
|
||||||
final Version version;
|
final String identifier;
|
||||||
|
final String version;
|
||||||
final String link;
|
final String link;
|
||||||
final bool available;
|
|
||||||
|
|
||||||
FortniteBuild({
|
FortniteBuild({
|
||||||
|
required this.identifier,
|
||||||
required this.version,
|
required this.version,
|
||||||
required this.link,
|
required this.link
|
||||||
required this.available
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class FortniteBuildDownloadProgress {
|
class FortniteBuildDownloadProgress {
|
||||||
final double progress;
|
final double progress;
|
||||||
final int? timeLeft;
|
final int? minutesLeft;
|
||||||
final bool extracting;
|
final bool extracting;
|
||||||
final int speed;
|
|
||||||
|
|
||||||
FortniteBuildDownloadProgress({
|
FortniteBuildDownloadProgress({
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.extracting,
|
required this.extracting,
|
||||||
required this.timeLeft,
|
this.minutesLeft,
|
||||||
required this.speed
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 'dart:io';
|
||||||
|
|
||||||
import 'package:version/version.dart';
|
|
||||||
|
|
||||||
class FortniteVersion {
|
class FortniteVersion {
|
||||||
Version content;
|
String name;
|
||||||
Directory location;
|
Directory location;
|
||||||
|
|
||||||
FortniteVersion.fromJson(json)
|
FortniteVersion.fromJson(json)
|
||||||
: content = Version.parse(json["content"]),
|
: name = json["name"],
|
||||||
location = Directory(json["location"]);
|
location = Directory(json["location"]);
|
||||||
|
|
||||||
FortniteVersion({required this.content, required this.location});
|
FortniteVersion({required this.name, required this.location});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'content': content.toString(),
|
'name': name,
|
||||||
'location': location.path
|
'location': location.path
|
||||||
};
|
};
|
||||||
|
}
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => other is FortniteVersion && this.content == other.content;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,42 +1,30 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:version/version.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class GameInstance {
|
class GameInstance {
|
||||||
final Version version;
|
final String versionName;
|
||||||
final int gamePid;
|
final int gamePid;
|
||||||
final int? launcherPid;
|
final int? launcherPid;
|
||||||
final int? eacPid;
|
final int? eacPid;
|
||||||
final List<InjectableDll> injectedDlls;
|
final List<InjectableDll> injectedDlls;
|
||||||
final GameServerType? serverType;
|
bool hosting;
|
||||||
bool launched;
|
bool launched;
|
||||||
bool movedToVirtualDesktop;
|
bool movedToVirtualDesktop;
|
||||||
bool tokenError;
|
bool tokenError;
|
||||||
bool killed;
|
|
||||||
GameInstance? child;
|
GameInstance? child;
|
||||||
|
|
||||||
GameInstance({
|
GameInstance({
|
||||||
required this.version,
|
required this.versionName,
|
||||||
required this.gamePid,
|
required this.gamePid,
|
||||||
required this.launcherPid,
|
required this.launcherPid,
|
||||||
required this.eacPid,
|
required this.eacPid,
|
||||||
required this.serverType,
|
required this.hosting,
|
||||||
required this.child
|
required this.child
|
||||||
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
}): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
||||||
|
|
||||||
void kill() {
|
void kill() {
|
||||||
GameInstance? child = this;
|
|
||||||
while(child != null) {
|
|
||||||
child._kill();
|
|
||||||
child = child.child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _kill() {
|
|
||||||
launched = true;
|
|
||||||
killed = true;
|
|
||||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||||
if(launcherPid != null) {
|
if(launcherPid != null) {
|
||||||
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
|
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
|
||||||
@@ -45,10 +33,17 @@ class GameInstance {
|
|||||||
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
enum GameServerType {
|
bool get nestedHosting {
|
||||||
headless,
|
GameInstance? child = this;
|
||||||
virtualWindow,
|
while(child != null) {
|
||||||
window
|
if(child.hosting) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
child = child.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,6 @@ class ServerResult {
|
|||||||
final StackTrace? stackTrace;
|
final StackTrace? stackTrace;
|
||||||
|
|
||||||
ServerResult(this.type, {this.error, this.stackTrace});
|
ServerResult(this.type, {this.error, this.stackTrace});
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'ServerResult{type: $type, error: $error, stackTrace: $stackTrace}';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ServerResultType {
|
enum ServerResultType {
|
||||||
|
|||||||
@@ -15,19 +15,10 @@ final Semaphore _semaphore = Semaphore();
|
|||||||
String? _lastIp;
|
String? _lastIp;
|
||||||
String? _lastPort;
|
String? _lastPort;
|
||||||
|
|
||||||
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
|
Future<Process> startEmbeddedBackend(bool detached) async => startProcess(
|
||||||
final process = await startProcess(
|
|
||||||
executable: backendStartExecutable,
|
executable: backendStartExecutable,
|
||||||
window: detached,
|
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);
|
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 {
|
Future<bool> freeBackendPort() async {
|
||||||
await killProcessByPort(kDefaultBackendPort);
|
await killProcessByPort(kDefaultBackendPort);
|
||||||
await killProcessByPort(kDefaultXmppPort);
|
|
||||||
final standardResult = await isBackendPortFree();
|
final standardResult = await isBackendPortFree();
|
||||||
if(standardResult) {
|
if(standardResult) {
|
||||||
return true;
|
return true;
|
||||||
@@ -45,24 +35,21 @@ Future<bool> freeBackendPort() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||||
final hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
var hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||||
final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||||
try{
|
try{
|
||||||
final uri = Uri(
|
var uri = Uri(
|
||||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||||
host: hostName,
|
host: hostName,
|
||||||
port: port,
|
port: port,
|
||||||
path: "unknown"
|
path: "unknown"
|
||||||
);
|
);
|
||||||
log("[BACKEND] Pinging $uri...");
|
var client = HttpClient()
|
||||||
final client = HttpClient()
|
..connectionTimeout = const Duration(seconds: 5);
|
||||||
..connectionTimeout = const Duration(seconds: 10);
|
var request = await client.getUrl(uri);
|
||||||
final request = await client.getUrl(uri);
|
var response = await request.close();
|
||||||
await request.close().timeout(const Duration(seconds: 10));
|
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
|
||||||
log("[BACKEND] Ping successful");
|
}catch(_){
|
||||||
return uri;
|
|
||||||
}catch(error){
|
|
||||||
log("[BACKEND] Cannot ping backend: $error");
|
|
||||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,16 +59,16 @@ Stream<String?> watchMatchmakingIp() async* {
|
|||||||
return;
|
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 {
|
yield* observer.where((event) => event.path == matchmakerConfigFile.path).asyncMap((event) async {
|
||||||
try {
|
try {
|
||||||
final config = Config.fromString(await matchmakerConfigFile.readAsString());
|
var config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||||
final ip = config.get("GameServer", "ip");
|
var ip = config.get("GameServer", "ip");
|
||||||
if(ip == null) {
|
if(ip == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final port = config.get("GameServer", "port");
|
var port = config.get("GameServer", "port");
|
||||||
if(port == null) {
|
if(port == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -102,14 +89,14 @@ Stream<String?> watchMatchmakingIp() async* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeMatchmakingIp(String text) async {
|
Future<void> writeMatchmakingIp(String text) async {
|
||||||
final exists = await matchmakerConfigFile.exists();
|
var exists = await matchmakerConfigFile.exists();
|
||||||
if(!exists) {
|
if(!exists) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_semaphore.acquire();
|
_semaphore.acquire();
|
||||||
final splitIndex = text.indexOf(":");
|
var splitIndex = text.indexOf(":");
|
||||||
final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
||||||
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
|
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
|
||||||
if(port.isBlank) {
|
if(port.isBlank) {
|
||||||
port = kDefaultGameServerPort;
|
port = kDefaultGameServerPort;
|
||||||
@@ -117,7 +104,7 @@ Future<void> writeMatchmakingIp(String text) async {
|
|||||||
|
|
||||||
_lastIp = ip;
|
_lastIp = ip;
|
||||||
_lastPort = port;
|
_lastPort = port;
|
||||||
final config = Config.fromString(await matchmakerConfigFile.readAsString());
|
var config = Config.fromString(await matchmakerConfigFile.readAsString());
|
||||||
config.set("GameServer", "ip", ip);
|
config.set("GameServer", "ip", ip);
|
||||||
config.set("GameServer", "port", port);
|
config.set("GameServer", "port", port);
|
||||||
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
||||||
|
|||||||
@@ -3,243 +3,136 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio/io.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_common/src/extension/types.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";
|
const String kStopBuildDownloadSignal = "kill";
|
||||||
|
|
||||||
final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json");
|
final Dio _dio = _buildDioInstance();
|
||||||
final int _ariaPort = 6800;
|
Dio _buildDioInstance() {
|
||||||
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
|
final dio = Dio();
|
||||||
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
|
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter;
|
||||||
final String _ariaSecret = "RebootLauncher";
|
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*)?))%\$");
|
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 {
|
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) {
|
if (response.statusCode != 200) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonDecode(response.body)
|
final data = jsonDecode(response.data ?? "{}");
|
||||||
.map((entry) {
|
var results = <FortniteBuild>[];
|
||||||
try {
|
for(final entry in data.entries) {
|
||||||
final fileUrl = entry as String;
|
results.add(FortniteBuild(
|
||||||
final fileName = Uri.parse(fileUrl).pathSegments.last;
|
identifier: entry.key,
|
||||||
final fileNameWithoutExtension = path.basenameWithoutExtension(fileName);
|
version: "${entry.value["title"]} (${entry.key})",
|
||||||
return FortniteBuild(
|
link: entry.value["url"]
|
||||||
version: Version.parse(fileNameWithoutExtension),
|
));
|
||||||
link: entry,
|
}
|
||||||
available: true
|
return results;
|
||||||
);
|
|
||||||
}catch(_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.whereType<FortniteBuild>()
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
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 {
|
try {
|
||||||
final stopped = _setupLifecycle(options);
|
final stopped = _setupLifecycle(options);
|
||||||
await outputFile.parent.create(recursive: true);
|
final outputDir = Directory("${options.destination.path}\\.build");
|
||||||
|
await outputDir.create(recursive: true);
|
||||||
final downloadItemCompleter = Completer<File>();
|
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||||
|
final extension = path.extension(fileName);
|
||||||
await _startAriaServer();
|
final tempFile = File("${outputDir.path}\\$fileName");
|
||||||
final downloadId = await _startAriaDownload(options, outputFile);
|
if(await tempFile.exists()) {
|
||||||
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
|
await tempFile.delete(recursive: true);
|
||||||
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 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) {
|
}catch(error) {
|
||||||
_onError(error, options);
|
_onError(error, options);
|
||||||
}finally {
|
|
||||||
delete(outputFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startAriaServer() async {
|
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
|
||||||
final running = await _isAriaRunning();
|
var received = byteStart ?? 0;
|
||||||
if(running) {
|
try {
|
||||||
await killProcessByPort(_ariaPort);
|
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(statusCode == 403 || statusCode == 503) {
|
||||||
if(!aria2c.existsSync()) {
|
throw _deniedConnectionError;
|
||||||
throw "Missing aria2c.exe";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await startProcess(
|
if(statusCode == 404) {
|
||||||
executable: aria2c,
|
throw _unavailableError;
|
||||||
args: [
|
}
|
||||||
"--max-connection-per-server=${Platform.numberOfProcessors}",
|
|
||||||
"--split=${Platform.numberOfProcessors}",
|
throw _genericError;
|
||||||
"--enable-rpc",
|
},
|
||||||
"--rpc-listen-all=true",
|
headers: byteStart == null || byteStart <= 0 ? {
|
||||||
"--rpc-allow-origin-all",
|
"Cookie": "_c_t_c=1"
|
||||||
"--rpc-secret=$_ariaSecret",
|
} : {
|
||||||
"--rpc-listen-port=$_ariaPort"
|
"Cookie": "_c_t_c=1",
|
||||||
],
|
"Range": "bytes=${byteStart}-"
|
||||||
window: false
|
},
|
||||||
);
|
)
|
||||||
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
|
);
|
||||||
if(await _isAriaRunning()) {
|
}catch(error) {
|
||||||
|
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
|
||||||
|
_onError(error, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
}
|
|
||||||
throw "cannot start download server (timeout exceeded)";
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _isAriaRunning() async {
|
await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
|
||||||
|
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
Process? process;
|
Process? process;
|
||||||
switch (extension.toLowerCase()) {
|
switch (extension.toLowerCase()) {
|
||||||
case ".zip":
|
case ".zip":
|
||||||
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
|
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
|
||||||
if(!sevenZip.existsSync()) {
|
if(!sevenZip.existsSync()) {
|
||||||
throw "Missing 7zip.exe";
|
throw "Corrupted installation: missing 7zip.exe";
|
||||||
}
|
}
|
||||||
|
|
||||||
process = await startProcess(
|
process = await startProcess(
|
||||||
@@ -254,15 +147,10 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
);
|
);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
process.stdOutput.listen((data) {
|
process.stdOutput.listen((data) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
if(data.toLowerCase().contains("everything is ok")) {
|
if(data.toLowerCase().contains("everything is ok")) {
|
||||||
completed = true;
|
completed = true;
|
||||||
_onProgress(
|
_onProgress(startTime, now, 100, true, options);
|
||||||
options.port,
|
|
||||||
100,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
process?.kill(ProcessSignal.sigabrt);
|
process?.kill(ProcessSignal.sigabrt);
|
||||||
return;
|
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();
|
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
|
||||||
_onProgress(
|
_onProgress(startTime, now, percentage, true, options);
|
||||||
options.port,
|
|
||||||
percentage,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
process.stdError.listen((data) {
|
process.stdError.listen((data) {
|
||||||
if(!data.isBlank) {
|
if(!data.isBlank) {
|
||||||
@@ -295,7 +177,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
case ".rar":
|
case ".rar":
|
||||||
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
||||||
if(!winrar.existsSync()) {
|
if(!winrar.existsSync()) {
|
||||||
throw "Missing winrar.exe";
|
throw "Corrupted installation: missing winrar.exe";
|
||||||
}
|
}
|
||||||
|
|
||||||
process = await startProcess(
|
process = await startProcess(
|
||||||
@@ -310,16 +192,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
);
|
);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
process.stdOutput.listen((data) {
|
process.stdOutput.listen((data) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
||||||
if(data == "All OK") {
|
if(data == "All OK") {
|
||||||
completed = true;
|
completed = true;
|
||||||
_onProgress(
|
_onProgress(startTime, now, 100, true, options);
|
||||||
options.port,
|
|
||||||
100,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
process?.kill(ProcessSignal.sigabrt);
|
process?.kill(ProcessSignal.sigabrt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -330,13 +207,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
}
|
}
|
||||||
|
|
||||||
final percentage = int.parse(element).toDouble();
|
final percentage = int.parse(element).toDouble();
|
||||||
_onProgress(
|
_onProgress(startTime, now, percentage, true, options);
|
||||||
options.port,
|
|
||||||
percentage,
|
|
||||||
0,
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
process.stdError.listen((data) {
|
process.stdError.listen((data) {
|
||||||
if(!data.isBlank) {
|
if(!data.isBlank) {
|
||||||
@@ -354,25 +225,23 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Future.any([stopped.future, process.exitCode]);
|
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) {
|
if(percentage == 0) {
|
||||||
port.send(FortniteBuildDownloadProgress(
|
options.port.send(FortniteBuildDownloadProgress(
|
||||||
progress: percentage,
|
progress: percentage,
|
||||||
extracting: extracting,
|
extracting: extracting
|
||||||
timeLeft: null,
|
|
||||||
speed: speed
|
|
||||||
));
|
));
|
||||||
return;
|
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,
|
progress: percentage,
|
||||||
extracting: extracting,
|
extracting: extracting,
|
||||||
timeLeft: minutesLeft,
|
minutesLeft: minutesLeft
|
||||||
speed: speed
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,5 +261,4 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
|||||||
});
|
});
|
||||||
options.port.send(lifecyclePort.sendPort);
|
options.port.send(lifecyclePort.sendPort);
|
||||||
return stopped;
|
return stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,21 +6,19 @@ import 'package:path/path.dart' as path;
|
|||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
bool _watcher = false;
|
bool _watcher = false;
|
||||||
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
|
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
|
||||||
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
|
const String kRebootDownloadUrl =
|
||||||
const String kRebootBelowS20DownloadUrl =
|
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
|
||||||
"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";
|
|
||||||
|
|
||||||
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
||||||
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||||
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
|
final exists = await rebootDllFile.exists();
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> downloadCriticalDll(String name, String outputPath) async {
|
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"));
|
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
|
||||||
if(response.statusCode != 200) {
|
if(response.statusCode != 200) {
|
||||||
throw Exception("Cannot download $name: status code ${response.statusCode}");
|
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);
|
await output.writeAsBytes(response.bodyBytes, flush: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> downloadRebootDll(File file, String url) async {
|
Future<int> downloadRebootDll(String url) async {
|
||||||
Directory? outputDir;
|
Directory? outputDir;
|
||||||
|
final now = DateTime.now();
|
||||||
try {
|
try {
|
||||||
final response = await http.get(Uri.parse(url));
|
final response = await http.get(Uri.parse(url));
|
||||||
if(response.statusCode != 200) {
|
if(response.statusCode != 200) {
|
||||||
@@ -44,7 +43,8 @@ Future<void> downloadRebootDll(File file, String url) async {
|
|||||||
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
|
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
|
||||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||||
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").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{
|
} finally{
|
||||||
if(outputDir != null) {
|
if(outputDir != null) {
|
||||||
delete(outputDir);
|
delete(outputDir);
|
||||||
@@ -64,7 +64,7 @@ Stream<String> watchDlls() async* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_watcher = true;
|
_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")) {
|
if (event.path.endsWith(".dll")) {
|
||||||
yield event.path;
|
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
|
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([
|
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
|
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 =>
|
Future<bool> patchHeadless(File file) async =>
|
||||||
await _patch(file, _originalHeadless, _patchedHeadless);
|
_patch(file, _originalHeadless, _patchedHeadless);
|
||||||
|
|
||||||
Future<bool> patchMatchmaking(File file) async =>
|
Future<bool> patchMatchmaking(File file) async =>
|
||||||
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
|
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");
|
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 readOffset = 0;
|
||||||
var patchOffset = -1;
|
var patchOffset = -1;
|
||||||
var patchCount = 0;
|
var patchCount = 0;
|
||||||
while(readOffset < source.length){
|
while(readOffset < length){
|
||||||
if(source[readOffset] == original[patchCount]){
|
if(read[readOffset] == original[patchCount]){
|
||||||
if(patchOffset == -1) {
|
if(patchOffset == -1) {
|
||||||
patchOffset = readOffset;
|
patchOffset = readOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(readOffset - patchOffset + 1 == original.length) {
|
if(++patchCount == original.length) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
patchCount++;
|
|
||||||
}else {
|
}else {
|
||||||
patchOffset = -1;
|
patchOffset = -1;
|
||||||
patchCount = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readOffset++;
|
readOffset++;
|
||||||
@@ -58,10 +55,10 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for(var i = 0; i < patched.length; i++) {
|
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;
|
return true;
|
||||||
}catch(_){
|
}catch(_){
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Directory get installationDirectory =>
|
|||||||
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
|
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
|
||||||
|
|
||||||
Directory get assetsDirectory {
|
Directory get assetsDirectory {
|
||||||
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||||
if(directory.existsSync()) {
|
if(directory.existsSync()) {
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,9 @@ Directory get assetsDirectory {
|
|||||||
return installationDirectory;
|
return installationDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Directory get logsDirectory =>
|
||||||
|
Directory("${installationDirectory.path}\\logs");
|
||||||
|
|
||||||
Directory get settingsDirectory =>
|
Directory get settingsDirectory =>
|
||||||
Directory("${installationDirectory.path}\\settings");
|
Directory("${installationDirectory.path}\\settings");
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
// ignore_for_file: non_constant_identifier_names
|
// ignore_for_file: non_constant_identifier_names
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:sync/semaphore.dart';
|
||||||
import 'package:win32/win32.dart';
|
import 'package:win32/win32.dart';
|
||||||
|
|
||||||
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
||||||
@@ -95,53 +96,61 @@ Future<bool> startElevatedProcess({required String executable, required String a
|
|||||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||||
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
|
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
|
||||||
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
|
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
||||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
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 {
|
Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async {
|
||||||
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
|
|
||||||
final argsOrEmpty = args ?? [];
|
final argsOrEmpty = args ?? [];
|
||||||
final workingDirectory = _getWorkingDirectory(executable);
|
if(wrapProcess) {
|
||||||
if(useTempBatch) {
|
|
||||||
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
|
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(" ")}';
|
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
|
||||||
await tempScriptFile.writeAsString(command, flush: true);
|
await tempScriptFile.writeAsString(command, flush: true);
|
||||||
final process = await Process.start(
|
final process = await Process.start(
|
||||||
tempScriptFile.path,
|
tempScriptFile.path,
|
||||||
[],
|
[],
|
||||||
workingDirectory: workingDirectory,
|
workingDirectory: executable.parent.path,
|
||||||
environment: environment,
|
|
||||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||||
runInShell: window
|
runInShell: window
|
||||||
);
|
);
|
||||||
return _ExtendedProcess(process, true);
|
return _withLogger(name, executable, process, window);
|
||||||
}
|
}
|
||||||
|
|
||||||
final process = await Process.start(
|
final process = await Process.start(
|
||||||
executable.path,
|
executable.path,
|
||||||
args ?? [],
|
args ?? [],
|
||||||
workingDirectory: workingDirectory,
|
workingDirectory: executable.parent.path,
|
||||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||||
runInShell: window
|
runInShell: window
|
||||||
);
|
);
|
||||||
return _ExtendedProcess(process, true);
|
return _withLogger(name, executable, process, window);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _getWorkingDirectory(File executable) {
|
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) {
|
||||||
try {
|
final extendedProcess = _ExtendedProcess(process, true);
|
||||||
log("[PROCESS] Calculating working directory for $executable");
|
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log");
|
||||||
final workingDirectory = executable.parent.resolveSymbolicLinksSync();
|
loggingFile.parent.createSync(recursive: true);
|
||||||
log("[PROCESS] Using working directory: $workingDirectory");
|
if(loggingFile.existsSync()) {
|
||||||
return workingDirectory;
|
loggingFile.deleteSync();
|
||||||
}catch(error) {
|
|
||||||
log("[PROCESS] Cannot infer working directory: $error");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
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');
|
int Function(int hWnd)>('NtSuspendProcess');
|
||||||
|
|
||||||
bool suspend(int pid) {
|
bool suspend(int pid) {
|
||||||
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||||
try {
|
final result = _NtSuspendProcess(processHandle);
|
||||||
return _NtSuspendProcess(processHandle) == 0;
|
CloseHandle(processHandle);
|
||||||
} finally {
|
return result == 0;
|
||||||
CloseHandle(processHandle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool resume(int pid) {
|
bool resume(int pid) {
|
||||||
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||||
try {
|
final result = _NtResumeProcess(processHandle);
|
||||||
return _NtResumeProcess(processHandle) == 0;
|
CloseHandle(processHandle);
|
||||||
} finally {
|
return result == 0;
|
||||||
CloseHandle(processHandle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _watchProcess(int pid) {
|
||||||
Future<void> watchProcess(int pid) => Isolate.run(() {
|
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||||
final processHandle = OpenProcess(FILE_ACCESS_RIGHTS.SYNCHRONIZE, FALSE, pid);
|
|
||||||
if (processHandle == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
WaitForSingleObject(processHandle, INFINITE);
|
WaitForSingleObject(processHandle, INFINITE);
|
||||||
}finally {
|
}finally {
|
||||||
CloseHandle(processHandle);
|
CloseHandle(processHandle);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
|
Future<bool> watchProcess(int pid) async {
|
||||||
log("[PROCESS] Generating reboot args");
|
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) {
|
if(password.isEmpty) {
|
||||||
username = '${_parseUsername(username, host)}@projectreboot.dev';
|
username = '${_parseUsername(username, host)}@projectreboot.dev';
|
||||||
}
|
}
|
||||||
|
|
||||||
password = password.isNotEmpty ? password : "Rebooted";
|
password = password.isNotEmpty ? password : "Rebooted";
|
||||||
final args = LinkedHashMap<String, String>(
|
final args = [
|
||||||
equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
|
"-epicapp=Fortnite",
|
||||||
hashCode: (a) => a.toUpperCase().hashCode
|
"-epicenv=Prod",
|
||||||
);
|
"-epiclocale=en-us",
|
||||||
args.addAll({
|
"-epicportal",
|
||||||
"-epicapp": "Fortnite",
|
"-skippatchcheck",
|
||||||
"-epicenv": "Prod",
|
"-nobe",
|
||||||
"-epiclocale": "en-us",
|
"-fromfl=eac",
|
||||||
"-epicportal": "",
|
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
||||||
"-skippatchcheck": "",
|
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||||
"-nobe": "",
|
"-AUTH_LOGIN=$username",
|
||||||
"-fromfl": "eac",
|
"-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}",
|
||||||
"-fltoken": "3db3ba5dcbd2e16703f3978d",
|
"-AUTH_TYPE=epic"
|
||||||
"-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
];
|
||||||
"-AUTH_LOGIN": username,
|
|
||||||
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
|
|
||||||
"-AUTH_TYPE": "epic"
|
|
||||||
});
|
|
||||||
|
|
||||||
if(logging) {
|
if(host && headless){
|
||||||
args["-log"] = "";
|
args.addAll([
|
||||||
|
"-nullrhi",
|
||||||
|
"-nosplash",
|
||||||
|
"-nosound",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(host) {
|
if(additionalArgs.isNotEmpty){
|
||||||
args["-nosplash"] = "";
|
args.addAll(additionalArgs.split(" "));
|
||||||
args["-nosound"] = "";
|
|
||||||
if(hostType == GameServerType.headless){
|
|
||||||
args["-nullrhi"] = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log("[PROCESS] Default args: $args");
|
return 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleGameOutput({
|
void handleGameOutput({
|
||||||
@@ -250,22 +249,16 @@ void handleGameOutput({
|
|||||||
required void Function() onBuildCorrupted,
|
required void Function() onBuildCorrupted,
|
||||||
}) {
|
}) {
|
||||||
if (line.contains(kShutdownLine)) {
|
if (line.contains(kShutdownLine)) {
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
|
|
||||||
onShutdown();
|
onShutdown();
|
||||||
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
|
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
|
|
||||||
onBuildCorrupted();
|
onBuildCorrupted();
|
||||||
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
|
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
|
|
||||||
onTokenError();
|
onTokenError();
|
||||||
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
|
|
||||||
onLoggedIn();
|
onLoggedIn();
|
||||||
}else if(line.contains(kGameFinishedLine) && host) {
|
}else if(line.contains(kGameFinishedLine) && host) {
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
|
|
||||||
onMatchEnd();
|
onMatchEnd();
|
||||||
}else if(line.contains(kDisplayInitializedLine) && host) {
|
}else if(line.contains(kDisplayInitializedLine) && host) {
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
|
|
||||||
onDisplayAttached();
|
onDisplayAttached();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,14 +291,7 @@ final class _ExtendedProcess implements Process {
|
|||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> get exitCode {
|
Future<int> get exitCode => _delegate.exitCode;
|
||||||
try {
|
|
||||||
return _delegate.exitCode;
|
|
||||||
}catch(_) {
|
|
||||||
return watchProcess(_delegate.pid)
|
|
||||||
.then((_) => -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ environment:
|
|||||||
sdk: ">=3.0.0 <=4.0.0"
|
sdk: ">=3.0.0 <=4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
win32: ^5.5.4
|
dio: ^5.3.2
|
||||||
ffi: ^2.1.3
|
win32: 3.0.0
|
||||||
path: ^1.9.0
|
ffi: ^2.1.0
|
||||||
http: ^1.2.2
|
path: ^1.8.3
|
||||||
crypto: ^3.0.5
|
http: ^1.1.0
|
||||||
archive: ^3.6.1
|
crypto: ^3.0.2
|
||||||
|
archive: ^3.3.7
|
||||||
ini: ^2.1.0
|
ini: ^2.1.0
|
||||||
shelf_proxy: ^1.0.2
|
shelf_proxy: ^1.0.2
|
||||||
sync: ^0.3.0
|
sync: ^0.3.0
|
||||||
uuid: ^4.5.1
|
uuid: ^3.0.6
|
||||||
shelf_web_socket: ^2.0.0
|
shelf_web_socket: ^2.0.0
|
||||||
version: ^3.0.2
|
|
||||||
|
|
||||||
dev_dependencies:
|
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
|
Launcher for project reboot
|
||||||
|
|
||||||
Welcome to the **Reboot Launcher**!
|
|
||||||
This is a GUI application developed as part of the **Reboot Project**.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Running the Project
|
This project is a starting point for a Flutter application.
|
||||||
To launch the project in development mode, simply run:
|
|
||||||
```
|
|
||||||
flutter run
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building the Project
|
A few resources to get you started if this is your first Flutter project:
|
||||||
To create a production-ready build, use:
|
|
||||||
```
|
|
||||||
flutter build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Packaging the Project
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
To package the application for distribution, run:
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
```
|
|
||||||
package.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
## Requirements
|
For help getting started with Flutter development, view the
|
||||||
- [Flutter SDK](https://flutter.dev/docs/get-started/install)
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
- Supported operating systems: Windows
|
samples, guidance on mobile development, and a full API reference.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
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",
|
"hostGameServerPasswordDescription": "The password of your game server, if you need one",
|
||||||
"hostGameServerDiscoverableName": "Discoverable",
|
"hostGameServerDiscoverableName": "Discoverable",
|
||||||
"hostGameServerDiscoverableDescription": "Make your server available to other players on the server browser",
|
"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",
|
"hostAutomaticRestartName": "Automatic restart",
|
||||||
"hostAutomaticRestartDescription": "Automatically restarts your game server when the match ends",
|
"hostAutomaticRestartDescription": "Automatically restarts your game server when the match ends",
|
||||||
"hostShareName": "Share",
|
"hostShareName": "Share",
|
||||||
@@ -76,10 +80,10 @@
|
|||||||
"playGameServerCustomContent": "Enter IP",
|
"playGameServerCustomContent": "Enter IP",
|
||||||
"settingsName": "Settings",
|
"settingsName": "Settings",
|
||||||
"settingsClientName": "Internal files",
|
"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",
|
"settingsClientOptionsName": "Options",
|
||||||
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
|
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
|
||||||
"settingsClientConsoleName": "Unreal engine patcher",
|
"settingsClientConsoleName": "Unreal engine console",
|
||||||
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
|
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
|
||||||
"settingsClientConsoleKeyName": "Unreal engine console key",
|
"settingsClientConsoleKeyName": "Unreal engine console key",
|
||||||
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
|
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
|
||||||
@@ -88,25 +92,24 @@
|
|||||||
"settingsClientMemoryName": "Memory patcher",
|
"settingsClientMemoryName": "Memory patcher",
|
||||||
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
|
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
|
||||||
"settingsClientArgsName": "Custom launch arguments",
|
"settingsClientArgsName": "Custom launch arguments",
|
||||||
"settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
|
"settingsClientArgsDescription": "Additional arguments to use when launching the game",
|
||||||
"settingsClientArgsPlaceholder": "Arguments...",
|
"settingsClientArgsPlaceholder": "Arguments...",
|
||||||
"settingsServerName": "Internal files",
|
"settingsServerName": "Internal files",
|
||||||
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
||||||
"settingsServerOptionsName": "Options",
|
"settingsServerOptionsName": "Options",
|
||||||
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
||||||
"settingsServerTypeName": "Game server type",
|
"settingsServerTypeName": "Type",
|
||||||
"settingsServerTypeDescription": "The type of game server to inject",
|
"settingsServerTypeDescription": "The type of game server to inject",
|
||||||
"settingsServerTypeEmbeddedName": "Embedded",
|
"settingsServerTypeEmbeddedName": "Embedded",
|
||||||
"settingsServerTypeCustomName": "Custom",
|
"settingsServerTypeCustomName": "Custom",
|
||||||
"settingsOldServerFileName": "Game server",
|
"settingsServerFileName": "Implementation",
|
||||||
"settingsServerFileDescription": "The file injected to create the game server",
|
"settingsServerFileDescription": "The file injected to create the game server",
|
||||||
"settingsServerPortName": "Port",
|
"settingsServerPortName": "Port",
|
||||||
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
|
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
|
||||||
"settingsServerOldMirrorName": "Update mirror (Before season 20)",
|
"settingsServerMirrorName": "Update mirror",
|
||||||
"settingsServerNewMirrorName": "Update mirror (Season 20 and above)",
|
|
||||||
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
||||||
"settingsServerMirrorPlaceholder": "mirror",
|
"settingsServerMirrorPlaceholder": "mirror",
|
||||||
"settingsServerTimerName": "Game server updater",
|
"settingsServerTimerName": "Update timer",
|
||||||
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
|
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
|
||||||
"settingsUtilsName": "Launcher",
|
"settingsUtilsName": "Launcher",
|
||||||
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
||||||
@@ -119,7 +122,9 @@
|
|||||||
"settingsUtilsResetDefaultsName": "Reset settings",
|
"settingsUtilsResetDefaultsName": "Reset settings",
|
||||||
"settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values",
|
"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",
|
"settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
|
||||||
|
"settingsUtilsResetDefaultsContent": "Reset",
|
||||||
"settingsUtilsDialogSecondaryAction": "Close",
|
"settingsUtilsDialogSecondaryAction": "Close",
|
||||||
|
"settingsUtilsDialogPrimaryAction": "Reset",
|
||||||
"selectFortniteName": "Fortnite version",
|
"selectFortniteName": "Fortnite version",
|
||||||
"selectFortniteDescription": "Select the version of Fortnite you want to use",
|
"selectFortniteDescription": "Select the version of Fortnite you want to use",
|
||||||
"manageVersionsName": "Manage versions",
|
"manageVersionsName": "Manage versions",
|
||||||
@@ -128,7 +133,7 @@
|
|||||||
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
||||||
"addLocalBuildName": "Add a version from this PC's local storage",
|
"addLocalBuildName": "Add a version from this PC's local storage",
|
||||||
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
|
"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",
|
"downloadBuildName": "Download any version from the cloud",
|
||||||
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
||||||
"downloadBuildContent": "Download build",
|
"downloadBuildContent": "Download build",
|
||||||
@@ -146,17 +151,13 @@
|
|||||||
"defaultServerName": "Reboot Game Server",
|
"defaultServerName": "Reboot Game Server",
|
||||||
"defaultServerDescription": "Just another server",
|
"defaultServerDescription": "Just another server",
|
||||||
"downloadingDll": "Downloading {name} dll...",
|
"downloadingDll": "Downloading {name} dll...",
|
||||||
"dllAlreadyExists": "The {name} was already downloaded",
|
|
||||||
"downloadDllSuccess": "The {name} dll was downloaded successfully",
|
"downloadDllSuccess": "The {name} dll was downloaded successfully",
|
||||||
"downloadDllError": "An error occurred while downloading {name}: {error}",
|
"downloadDllError": "An error occurred while downloading {name}: {error}",
|
||||||
"downloadDllRetry": "Retry",
|
"downloadDllRetry": "Retry",
|
||||||
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
|
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
|
||||||
"launchingGameServer": "Launching the game server...",
|
"launchingHeadlessServer": "Launching the game server...",
|
||||||
"launchingGameClientOnly": "Launching the game client without a server...",
|
"launchingGameClient": "Launching the game client...",
|
||||||
"launchingGameClientAndServer": "Launching the game client and server...",
|
|
||||||
"startGameServer": "Start a game server",
|
|
||||||
"usernameOrEmail": "Username/Email",
|
"usernameOrEmail": "Username/Email",
|
||||||
"invalidEmail": "Invalid email",
|
|
||||||
"usernameOrEmailPlaceholder": "Type your username or email",
|
"usernameOrEmailPlaceholder": "Type your username or email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"passwordPlaceholder": "Type your password, if you want to use one",
|
"passwordPlaceholder": "Type your password, if you want to use one",
|
||||||
@@ -170,16 +171,16 @@
|
|||||||
"stoppingServer": "Stopping the backend...",
|
"stoppingServer": "Stopping the backend...",
|
||||||
"stoppedServer": "The backend was stopped successfully",
|
"stoppedServer": "The backend was stopped successfully",
|
||||||
"stopServerError": "An error occurred while stopping the backend: {error}",
|
"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",
|
"missingPortError": "Missing port in the backend configuration",
|
||||||
"illegalPortError": "Invalid port in the backend configuration",
|
"illegalPortError": "Invalid port in the backend configuration",
|
||||||
"freeingPort": "Freeing the backend port...",
|
"freeingPort": "Freeing the backend port...",
|
||||||
"freedPort": "The backend port was freed successfully",
|
"freedPort": "The backend port was freed successfully",
|
||||||
"freePortError": "An error occurred while freeing the backend port: {error}",
|
"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",
|
"pingError": "Cannot ping the {type} backend",
|
||||||
"joinSelfServer": "You can't join your own server",
|
"joinSelfServer": "You can't join your own server",
|
||||||
"cannotJoinServerVersion": "You can't join this server: download Fortnite {version}",
|
|
||||||
"wrongServerPassword": "Wrong password: please try again",
|
"wrongServerPassword": "Wrong password: please try again",
|
||||||
"offlineServer": "This server isn't online right now: please try again later",
|
"offlineServer": "This server isn't online right now: please try again later",
|
||||||
"serverPassword": "Password",
|
"serverPassword": "Password",
|
||||||
@@ -195,12 +196,12 @@
|
|||||||
"deleteVersionCancel": "Keep",
|
"deleteVersionCancel": "Keep",
|
||||||
"deleteVersionConfirm": "Delete",
|
"deleteVersionConfirm": "Delete",
|
||||||
"versionName": "Name",
|
"versionName": "Name",
|
||||||
"versionNameLabel": "Type the version name",
|
"versionNameLabel": "Type the new version name",
|
||||||
"newVersionNameConfirm": "Save",
|
"newVersionNameConfirm": "Save",
|
||||||
"newVersionNameLabel": "Type the version name",
|
"newVersionNameLabel": "Type the new version name",
|
||||||
"gameFolderTitle": "Game directory",
|
"gameFolderTitle": "Game folder",
|
||||||
"gameFolderPlaceholder": "Type the game directory",
|
"gameFolderPlaceholder": "Type the new game folder",
|
||||||
"gameFolderPlaceWindowTitle": "Select game directory",
|
"gameFolderPlaceWindowTitle": "Select game folder",
|
||||||
"gameFolderLabel": "Path",
|
"gameFolderLabel": "Path",
|
||||||
"openInExplorer": "Open in explorer",
|
"openInExplorer": "Open in explorer",
|
||||||
"modify": "Modify",
|
"modify": "Modify",
|
||||||
@@ -216,8 +217,6 @@
|
|||||||
"downloadedVersion": "The download was completed successfully!",
|
"downloadedVersion": "The download was completed successfully!",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloading": "Downloading...",
|
"downloading": "Downloading...",
|
||||||
"allocatingSpace": "Allocating disk space...",
|
|
||||||
"startingDownload": "Starting download...",
|
|
||||||
"extracting": "Extracting...",
|
"extracting": "Extracting...",
|
||||||
"buildProgress": "{progress}%",
|
"buildProgress": "{progress}%",
|
||||||
"buildInstallationDirectory": "Installation directory",
|
"buildInstallationDirectory": "Installation directory",
|
||||||
@@ -225,7 +224,7 @@
|
|||||||
"buildInstallationDirectoryWindowTitle": "Select installation directory",
|
"buildInstallationDirectoryWindowTitle": "Select installation directory",
|
||||||
"timeLeft": "Time left: {timeLeft, plural, =0{less than a minute} =1{about {timeLeft} minute} other{about {timeLeft} minutes}}",
|
"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",
|
"localBuildsWarning": "Local builds are not guaranteed to work",
|
||||||
"saveLocalVersion": "Add",
|
"saveLocalVersion": "Save",
|
||||||
"embedded": "Embedded",
|
"embedded": "Embedded",
|
||||||
"remote": "Remote",
|
"remote": "Remote",
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
@@ -249,7 +248,7 @@
|
|||||||
"versionAlreadyExists": "This version already exists",
|
"versionAlreadyExists": "This version already exists",
|
||||||
"emptyGamePath": "Empty game path",
|
"emptyGamePath": "Empty game path",
|
||||||
"directoryDoesNotExist": "Directory doesn't exist",
|
"directoryDoesNotExist": "Directory doesn't exist",
|
||||||
"missingShippingExe": "Invalid game path: missing Fortnite executable",
|
"missingShippingExe": "Invalid game path: missing FortniteClient-Win64-Shipping",
|
||||||
"invalidDownloadPath": "Invalid download path",
|
"invalidDownloadPath": "Invalid download path",
|
||||||
"invalidDllPath": "Invalid dll path",
|
"invalidDllPath": "Invalid dll path",
|
||||||
"dllDoesNotExist": "The file doesn't exist",
|
"dllDoesNotExist": "The file doesn't exist",
|
||||||
@@ -261,10 +260,9 @@
|
|||||||
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
|
"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",
|
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version",
|
||||||
"corruptedDllError": "Cannot inject dll: {error}",
|
"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})",
|
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
|
||||||
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
|
"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",
|
"serverNoLongerAvailableUnnamed": "The previous server is no longer available",
|
||||||
"noServerFound": "No server found: invalid or expired link",
|
"noServerFound": "No server found: invalid or expired link",
|
||||||
"settingsUtilsThemeName": "Theme",
|
"settingsUtilsThemeName": "Theme",
|
||||||
@@ -274,6 +272,8 @@
|
|||||||
"system": "System",
|
"system": "System",
|
||||||
"settingsUtilsLanguageName": "Language",
|
"settingsUtilsLanguageName": "Language",
|
||||||
"settingsUtilsLanguageDescription": "Select the language to use inside the launcher",
|
"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",
|
"infoDocumentationName": "Documentation",
|
||||||
"infoDocumentationDescription": "Read some tutorials on how to use Reboot",
|
"infoDocumentationDescription": "Read some tutorials on how to use Reboot",
|
||||||
"infoDocumentationContent": "Open GitHub",
|
"infoDocumentationContent": "Open GitHub",
|
||||||
@@ -281,8 +281,8 @@
|
|||||||
"infoDiscordDescription": "Join the discord server to receive help",
|
"infoDiscordDescription": "Join the discord server to receive help",
|
||||||
"infoDiscordContent": "Open Discord",
|
"infoDiscordContent": "Open Discord",
|
||||||
"infoVideoName": "Tutorial",
|
"infoVideoName": "Tutorial",
|
||||||
"infoVideoDescription": "Show the tutorial again in the launcher",
|
"infoVideoDescription": "Watch a tutorial to understand how to use the launcher",
|
||||||
"infoVideoContent": "Start Tutorial",
|
"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",
|
"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",
|
"dllDeletedSecondaryAction": "Close",
|
||||||
"dllDeletedPrimaryAction": "Try again",
|
"dllDeletedPrimaryAction": "Try again",
|
||||||
@@ -298,82 +298,5 @@
|
|||||||
"updateAvailableAction": "Download",
|
"updateAvailableAction": "Download",
|
||||||
"gameServerEnd": "The match ended",
|
"gameServerEnd": "The match ended",
|
||||||
"gameServerRestart": "The server will restart in {timeInSeconds} seconds",
|
"gameServerRestart": "The server will restart in {timeInSeconds} seconds",
|
||||||
"gameServerShutdown": "The server will shutdown 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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
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:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||||
import 'package:flutter_localized_locales/flutter_localized_locales.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:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.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/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_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/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/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/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:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
import 'package:url_protocol/url_protocol.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:win32/win32.dart';
|
||||||
|
|
||||||
const double kDefaultWindowWidth = 1164;
|
const double kDefaultWindowWidth = 1164;
|
||||||
const double kDefaultWindowHeight = 864;
|
const double kDefaultWindowHeight = 864;
|
||||||
@@ -34,8 +45,8 @@ bool appWithNoStorage = false;
|
|||||||
void main() {
|
void main() {
|
||||||
log("[APP] Called");
|
log("[APP] Called");
|
||||||
runZonedGuarded(
|
runZonedGuarded(
|
||||||
() => _startApp(),
|
() => _startApp(),
|
||||||
(error, stack) => onError(error, stack, false),
|
(error, stack) => onError(error, stack, false),
|
||||||
zoneSpecification: ZoneSpecification(
|
zoneSpecification: ZoneSpecification(
|
||||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||||
)
|
)
|
||||||
@@ -43,7 +54,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startApp() async {
|
Future<void> _startApp() async {
|
||||||
_overrideHttpCertificate();
|
|
||||||
final errors = <Object>[];
|
final errors = <Object>[];
|
||||||
try {
|
try {
|
||||||
log("[APP] Starting application");
|
log("[APP] Starting application");
|
||||||
@@ -62,6 +72,11 @@ Future<void> _startApp() async {
|
|||||||
errors.add(notificationsError);
|
errors.add(notificationsError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final tilesError = InfoPage.initInfoTiles();
|
||||||
|
if(tilesError != null) {
|
||||||
|
errors.add(tilesError);
|
||||||
|
}
|
||||||
|
|
||||||
final versionError = await _initVersion();
|
final versionError = await _initVersion();
|
||||||
if(versionError != null) {
|
if(versionError != null) {
|
||||||
errors.add(versionError);
|
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 {
|
Future<Object?> _initNotifications() async {
|
||||||
try {
|
try {
|
||||||
await localNotifier.setup(
|
await localNotifier.setup(
|
||||||
@@ -145,55 +148,54 @@ Future<Object?> _initVersion() async {
|
|||||||
|
|
||||||
Future<Object?> _initUrlHandler() async {
|
Future<Object?> _initUrlHandler() async {
|
||||||
try {
|
try {
|
||||||
registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
|
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
|
||||||
return null;
|
return null;
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initWindow() async {
|
void _initWindow() => doWhenWindowReady(() async {
|
||||||
try {
|
try {
|
||||||
await SystemTheme.accentColor.load();
|
await SystemTheme.accentColor.load();
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
await Window.initialize();
|
await Window.initialize();
|
||||||
var settingsController = Get.find<SettingsController>();
|
var settingsController = Get.find<SettingsController>();
|
||||||
var size = Size(settingsController.width, settingsController.height);
|
var size = Size(settingsController.width, settingsController.height);
|
||||||
await windowManager.setSize(size);
|
appWindow.size = size;
|
||||||
var offsetX = settingsController.offsetX;
|
var offsetX = settingsController.offsetX;
|
||||||
var offsetY = settingsController.offsetY;
|
var offsetY = settingsController.offsetY;
|
||||||
if(offsetX != null && offsetY != null) {
|
if(offsetX != null && offsetY != null){
|
||||||
final position = Offset(
|
appWindow.position = Offset(
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY
|
offsetY
|
||||||
);
|
);
|
||||||
await windowManager.setPosition(position);
|
|
||||||
}else {
|
}else {
|
||||||
await windowManager.setAlignment(Alignment.center);
|
appWindow.alignment = Alignment.center;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isWin11) {
|
if(isWin11) {
|
||||||
await Window.setEffect(
|
await Window.setEffect(
|
||||||
effect: WindowEffect.acrylic,
|
effect: WindowEffect.acrylic,
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
dark: isDarkMode
|
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}catch(error, stackTrace) {
|
}catch(error, stackTrace) {
|
||||||
onError(error, stackTrace, false);
|
onError(error, stackTrace, false);
|
||||||
}finally {
|
}finally {
|
||||||
windowManager.show();
|
appWindow.show();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
Future<List<Object>> _initStorage() async {
|
Future<List<Object>> _initStorage() async {
|
||||||
final errors = <Object>[];
|
final errors = <Object>[];
|
||||||
try {
|
try {
|
||||||
await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
|
await GetStorage("game", settingsDirectory.path).initStorage;
|
||||||
await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
|
await GetStorage("backend", settingsDirectory.path).initStorage;
|
||||||
await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
|
await GetStorage("update", settingsDirectory.path).initStorage;
|
||||||
await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
|
await GetStorage("settings", settingsDirectory.path).initStorage;
|
||||||
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
|
await GetStorage("hosting", settingsDirectory.path).initStorage;
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
appWithNoStorage = true;
|
appWithNoStorage = true;
|
||||||
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
|
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 {
|
try {
|
||||||
final controller = HostingController();
|
Get.put(BuildController());
|
||||||
Get.put(controller);
|
}catch(error) {
|
||||||
controller.discardServer();
|
errors.add(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Get.put(HostingController());
|
||||||
|
}catch(error) {
|
||||||
|
errors.add(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Get.put(UpdateController());
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
errors.add(error);
|
errors.add(error);
|
||||||
}
|
}
|
||||||
@@ -225,12 +237,6 @@ Future<List<Object>> _initStorage() async {
|
|||||||
errors.add(error);
|
errors.add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
Get.put(DllController());
|
|
||||||
}catch(error) {
|
|
||||||
errors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,39 +2,33 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
|
||||||
|
|
||||||
class BackendController extends GetxController {
|
class BackendController extends GetxController {
|
||||||
static const String storageName = "v2_backend_storage";
|
late final GetStorage? storage;
|
||||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
|
||||||
late final TextEditingController host;
|
late final TextEditingController host;
|
||||||
late final TextEditingController port;
|
late final TextEditingController port;
|
||||||
late final Rx<ServerType> type;
|
late final Rx<ServerType> type;
|
||||||
late final TextEditingController gameServerAddress;
|
late final TextEditingController gameServerAddress;
|
||||||
late final FocusNode gameServerAddressFocusNode;
|
late final FocusNode gameServerAddressFocusNode;
|
||||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
late final RxnString gameServerOwner;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool detached;
|
late final RxBool detached;
|
||||||
StreamSubscription? worker;
|
StreamSubscription? worker;
|
||||||
int? embeddedProcessPid;
|
|
||||||
HttpServer? localServer;
|
HttpServer? localServer;
|
||||||
HttpServer? remoteServer;
|
HttpServer? remoteServer;
|
||||||
|
|
||||||
BackendController() {
|
BackendController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
storage = appWithNoStorage ? null : GetStorage("backend");
|
||||||
started = RxBool(false);
|
started = RxBool(false);
|
||||||
type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
|
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
|
||||||
type.listen((value) {
|
type.listen((value) {
|
||||||
host.text = _readHost();
|
host.text = _readHost();
|
||||||
port.text = _readPort();
|
port.text = _readPort();
|
||||||
_storage?.write("type", value.index);
|
storage?.write("type", value.index);
|
||||||
if (!started.value) {
|
if (!started.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,14 +37,13 @@ class BackendController extends GetxController {
|
|||||||
});
|
});
|
||||||
host = TextEditingController(text: _readHost());
|
host = TextEditingController(text: _readHost());
|
||||||
host.addListener(() =>
|
host.addListener(() =>
|
||||||
_storage?.write("${type.value.name}_host", host.text));
|
storage?.write("${type.value.name}_host", host.text));
|
||||||
port = TextEditingController(text: _readPort());
|
port = TextEditingController(text: _readPort());
|
||||||
port.addListener(() =>
|
port.addListener(() =>
|
||||||
_storage?.write("${type.value.name}_port", port.text));
|
storage?.write("${type.value.name}_port", port.text));
|
||||||
detached = RxBool(_storage?.read("detached") ?? false);
|
detached = RxBool(storage?.read("detached") ?? false);
|
||||||
detached.listen((value) => _storage?.write("detached", value));
|
detached.listen((value) => storage?.write("detached", value));
|
||||||
final address = _storage?.read("game_server_address");
|
gameServerAddress = TextEditingController(text: storage?.read("game_server_address") ?? "127.0.0.1");
|
||||||
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
|
|
||||||
var lastValue = gameServerAddress.text;
|
var lastValue = gameServerAddress.text;
|
||||||
writeMatchmakingIp(lastValue);
|
writeMatchmakingIp(lastValue);
|
||||||
gameServerAddress.addListener(() {
|
gameServerAddress.addListener(() {
|
||||||
@@ -61,7 +54,7 @@ class BackendController extends GetxController {
|
|||||||
|
|
||||||
lastValue = newValue;
|
lastValue = newValue;
|
||||||
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
||||||
_storage?.write("game_server_address", newValue);
|
storage?.write("game_server_address", newValue);
|
||||||
writeMatchmakingIp(newValue);
|
writeMatchmakingIp(newValue);
|
||||||
});
|
});
|
||||||
watchMatchmakingIp().listen((event) {
|
watchMatchmakingIp().listen((event) {
|
||||||
@@ -70,59 +63,24 @@ class BackendController extends GetxController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
gameServerAddressFocusNode = FocusNode();
|
gameServerAddressFocusNode = FocusNode();
|
||||||
consoleKey = Rx(_readConsoleKey());
|
gameServerOwner = RxnString(storage?.read("game_server_owner"));
|
||||||
_writeConsoleKey(consoleKey.value);
|
gameServerOwner.listen((value) => storage?.write("game_server_owner", 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() async {
|
void reset() async {
|
||||||
type.value = ServerType.values.elementAt(0);
|
type.value = ServerType.values.elementAt(0);
|
||||||
for (final type in ServerType.values) {
|
for (final type in ServerType.values) {
|
||||||
_storage?.write("${type.name}_host", null);
|
storage?.write("${type.name}_host", null);
|
||||||
_storage?.write("${type.name}_port", null);
|
storage?.write("${type.name}_port", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
|
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
|
||||||
port.text = kDefaultBackendPort.toString();
|
port.text = kDefaultBackendPort.toString();
|
||||||
gameServerAddress.text = "127.0.0.1";
|
|
||||||
consoleKey.value = _kDefaultConsoleKey;
|
|
||||||
detached.value = false;
|
detached.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _readHost() {
|
String _readHost() {
|
||||||
String? value = _storage?.read("${type.value.name}_host");
|
String? value = storage?.read("${type.value.name}_host");
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -135,20 +93,24 @@ class BackendController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _readPort() =>
|
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 {
|
try {
|
||||||
if(started.value) {
|
if(started.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverType = type.value;
|
|
||||||
final hostData = this.host.text.trim();
|
final hostData = this.host.text.trim();
|
||||||
final portData = this.port.text.trim();
|
final portData = this.port.text.trim();
|
||||||
started.value = true;
|
if(type() != ServerType.local) {
|
||||||
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
|
started.value = true;
|
||||||
yield ServerResult(ServerResultType.starting);
|
yield ServerResult(ServerResultType.starting);
|
||||||
|
}else {
|
||||||
|
started.value = false;
|
||||||
|
if(portData != kDefaultBackendPort.toString()) {
|
||||||
|
yield ServerResult(ServerResultType.starting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostData.isEmpty) {
|
if (hostData.isEmpty) {
|
||||||
@@ -170,7 +132,7 @@ class BackendController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
||||||
yield ServerResult(ServerResultType.freeingPort);
|
yield ServerResult(ServerResultType.freeingPort);
|
||||||
final result = await freeBackendPort();
|
final result = await freeBackendPort();
|
||||||
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
||||||
@@ -180,21 +142,15 @@ class BackendController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(serverType){
|
switch(type()){
|
||||||
case ServerType.embedded:
|
case ServerType.embedded:
|
||||||
final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
|
final process = await startEmbeddedBackend(detached.value);
|
||||||
if(started.value) {
|
final processPid = process.pid;
|
||||||
|
watchProcess(processPid).then((value) {
|
||||||
|
if(started()) {
|
||||||
started.value = false;
|
started.value = false;
|
||||||
onError(errorMessage);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
watchProcess(process.pid).then((_) {
|
|
||||||
if(started.value) {
|
|
||||||
started.value = false;
|
|
||||||
onExit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
embeddedProcessPid = process.pid;
|
|
||||||
break;
|
break;
|
||||||
case ServerType.remote:
|
case ServerType.remote:
|
||||||
yield ServerResult(ServerResultType.pingingRemote);
|
yield ServerResult(ServerResultType.pingingRemote);
|
||||||
@@ -208,20 +164,8 @@ class BackendController extends GetxController {
|
|||||||
remoteServer = await startRemoteBackendProxy(uriResult);
|
remoteServer = await startRemoteBackendProxy(uriResult);
|
||||||
break;
|
break;
|
||||||
case ServerType.local:
|
case ServerType.local:
|
||||||
if(portNumber != kDefaultBackendPort) {
|
if(portData != kDefaultBackendPort.toString()) {
|
||||||
yield ServerResult(ServerResultType.pingingLocal);
|
|
||||||
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
|
|
||||||
if(uriResult == null) {
|
|
||||||
yield ServerResult(ServerResultType.pingError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
|
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;
|
break;
|
||||||
@@ -260,11 +204,7 @@ class BackendController extends GetxController {
|
|||||||
try{
|
try{
|
||||||
switch(type()){
|
switch(type()){
|
||||||
case ServerType.embedded:
|
case ServerType.embedded:
|
||||||
final embeddedProcessPid = this.embeddedProcessPid;
|
killProcessByPort(kDefaultBackendPort);
|
||||||
if(embeddedProcessPid != null) {
|
|
||||||
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
|
|
||||||
this.embeddedProcessPid = null;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case ServerType.remote:
|
case ServerType.remote:
|
||||||
await remoteServer?.close(force: true);
|
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()) {
|
if(started()) {
|
||||||
yield* stop();
|
yield* stop();
|
||||||
}else {
|
}else {
|
||||||
yield* start(
|
yield* start();
|
||||||
onExit: onExit,
|
|
||||||
onError: onError
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.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 {
|
class GameController extends GetxController {
|
||||||
static const String storageName = "v2_game_storage";
|
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final TextEditingController username;
|
late final TextEditingController username;
|
||||||
@@ -18,9 +22,10 @@ class GameController extends GetxController {
|
|||||||
late final Rxn<FortniteVersion> _selectedVersion;
|
late final Rxn<FortniteVersion> _selectedVersion;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final Rxn<GameInstance> instance;
|
late final Rxn<GameInstance> instance;
|
||||||
|
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||||
|
|
||||||
GameController() {
|
GameController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage("game");
|
||||||
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
||||||
final decodedVersions = decodedVersionsJson
|
final decodedVersions = decodedVersionsJson
|
||||||
.map((entry) => FortniteVersion.fromJson(entry))
|
.map((entry) => FortniteVersion.fromJson(entry))
|
||||||
@@ -28,7 +33,8 @@ class GameController extends GetxController {
|
|||||||
versions = Rx(decodedVersions);
|
versions = Rx(decodedVersions);
|
||||||
versions.listen((data) => _saveVersions());
|
versions.listen((data) => _saveVersions());
|
||||||
final decodedSelectedVersionName = _storage?.read("version");
|
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);
|
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||||
username = TextEditingController(
|
username = TextEditingController(
|
||||||
text: _storage?.read("username") ?? kDefaultPlayerName);
|
text: _storage?.read("username") ?? kDefaultPlayerName);
|
||||||
@@ -36,9 +42,41 @@ class GameController extends GetxController {
|
|||||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||||
password.addListener(() => _storage?.write("password", password.text));
|
password.addListener(() => _storage?.write("password", password.text));
|
||||||
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
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);
|
started = RxBool(false);
|
||||||
instance = Rxn();
|
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() {
|
void reset() {
|
||||||
@@ -46,12 +84,11 @@ class GameController extends GetxController {
|
|||||||
password.text = "";
|
password.text = "";
|
||||||
customLaunchArgs.text = "";
|
customLaunchArgs.text = "";
|
||||||
versions.value = [];
|
versions.value = [];
|
||||||
_selectedVersion.value = null;
|
|
||||||
instance.value = null;
|
instance.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteVersion? getVersionByName(String name) {
|
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) {
|
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) {
|
void removeVersion(FortniteVersion version) {
|
||||||
versions.update((val) => val?.remove(version));
|
versions.update((val) => val?.remove(version));
|
||||||
if (selectedVersion == version || hasNoVersions) {
|
if (selectedVersion?.name == version.name || hasNoVersions) {
|
||||||
selectedVersion = null;
|
selectedVersion = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +125,7 @@ class GameController extends GetxController {
|
|||||||
|
|
||||||
set selectedVersion(FortniteVersion? version) {
|
set selectedVersion(FortniteVersion? version) {
|
||||||
_selectedVersion.value = version;
|
_selectedVersion.value = version;
|
||||||
_storage?.write("version", version?.content.toString());
|
_storage?.write("version", version?.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:sync/semaphore.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class HostingController extends GetxController {
|
class HostingController extends GetxController {
|
||||||
static const String storageName = "v2_hosting_storage";
|
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final String uuid;
|
late final String uuid;
|
||||||
late final TextEditingController accountUsername;
|
|
||||||
late final TextEditingController accountPassword;
|
|
||||||
late final TextEditingController name;
|
late final TextEditingController name;
|
||||||
late final FocusNode nameFocusNode;
|
|
||||||
late final TextEditingController description;
|
late final TextEditingController description;
|
||||||
late final FocusNode descriptionFocusNode;
|
|
||||||
late final TextEditingController password;
|
late final TextEditingController password;
|
||||||
late final FocusNode passwordFocusNode;
|
|
||||||
late final RxBool showPassword;
|
late final RxBool showPassword;
|
||||||
late final RxBool discoverable;
|
late final RxBool discoverable;
|
||||||
late final Rx<GameServerType> type;
|
late final RxBool headless;
|
||||||
|
late final RxBool virtualDesktop;
|
||||||
late final RxBool autoRestart;
|
late final RxBool autoRestart;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool published;
|
late final RxBool published;
|
||||||
late final Rxn<GameInstance> instance;
|
late final Rxn<GameInstance> instance;
|
||||||
late final Rxn<Set<FortniteServer>> servers;
|
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||||
late final TextEditingController customLaunchArgs;
|
|
||||||
late final Semaphore _semaphore;
|
|
||||||
|
|
||||||
HostingController() {
|
HostingController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage("hosting");
|
||||||
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
||||||
_storage?.write("uuid", uuid);
|
_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 = TextEditingController(text: _storage?.read("name"));
|
||||||
name.addListener(() => _storage?.write("name", name.text));
|
name.addListener(() => _storage?.write("name", name.text));
|
||||||
description = TextEditingController(text: _storage?.read("description"));
|
description = TextEditingController(text: _storage?.read("description"));
|
||||||
description.addListener(() => _storage?.write("description", description.text));
|
description.addListener(() => _storage?.write("description", description.text));
|
||||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||||
password.addListener(() => _storage?.write("password", password.text));
|
password.addListener(() => _storage?.write("password", password.text));
|
||||||
nameFocusNode = FocusNode();
|
|
||||||
descriptionFocusNode = FocusNode();
|
|
||||||
passwordFocusNode = FocusNode();
|
|
||||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
||||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
discoverable.listen((value) => _storage?.write("discoverable", value));
|
||||||
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
|
headless = RxBool(_storage?.read("headless") ?? true);
|
||||||
type.listen((value) => _storage?.write("type", value.index));
|
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 = RxBool(_storage?.read("auto_restart") ?? true);
|
||||||
autoRestart.listen((value) => _storage?.write("auto_restart", value));
|
autoRestart.listen((value) => _storage?.write("auto_restart", value));
|
||||||
started = RxBool(false);
|
started = RxBool(false);
|
||||||
published = RxBool(false);
|
published = RxBool(false);
|
||||||
showPassword = RxBool(false);
|
showPassword = RxBool(false);
|
||||||
instance = Rxn();
|
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;
|
final supabase = Supabase.instance.client;
|
||||||
supabase.from("hosting_v2")
|
servers = Rxn();
|
||||||
|
supabase.from("hosting")
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
|
.map((event) => _parseValidServers(event))
|
||||||
.listen(
|
.listen((event) {
|
||||||
_onNewServer,
|
servers.value = event;
|
||||||
onError: (error) async {
|
published.value = event.any((element) => element["id"] == uuid);
|
||||||
log("[SUPABASE] Error: ${error}");
|
});
|
||||||
await Future.delayed(Duration(seconds: attempt * 5));
|
|
||||||
_listenServers(attempt + 1);
|
|
||||||
},
|
|
||||||
cancelOnError: true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNewServer(Set<FortniteServer> event) {
|
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
accountUsername.text = kDefaultHostName;
|
|
||||||
accountPassword.text = "";
|
|
||||||
name.text = "";
|
name.text = "";
|
||||||
description.text = "";
|
description.text = "";
|
||||||
showPassword.value = false;
|
showPassword.value = false;
|
||||||
discoverable.value = false;
|
discoverable.value = false;
|
||||||
|
started.value = false;
|
||||||
instance.value = null;
|
instance.value = null;
|
||||||
type.value = GameServerType.headless;
|
headless.value = true;
|
||||||
autoRestart.value = true;
|
virtualDesktop.value = true;
|
||||||
customLaunchArgs.text = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteServer? findServerById(String uuid) {
|
Map<String, dynamic>? findServerById(String uuid) {
|
||||||
try {
|
try {
|
||||||
return servers.value?.firstWhere((element) => element.id == uuid);
|
return servers.value?.firstWhere((element) => element["id"] == uuid);
|
||||||
} on StateError catch(_) {
|
} on StateError catch(_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +1,71 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.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: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 {
|
class SettingsController extends GetxController {
|
||||||
static const String storageName = "v2_settings_storage";
|
late final GetStorage _storage;
|
||||||
|
late final String originalDll;
|
||||||
late final GetStorage? _storage;
|
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 RxString language;
|
||||||
late final Rx<ThemeMode> themeMode;
|
late final Rx<ThemeMode> themeMode;
|
||||||
late final RxBool firstRun;
|
|
||||||
late double width;
|
late double width;
|
||||||
late double height;
|
late double height;
|
||||||
late double? offsetX;
|
late double? offsetX;
|
||||||
late double? offsetY;
|
late double? offsetY;
|
||||||
|
|
||||||
SettingsController() {
|
SettingsController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = GetStorage("settings");
|
||||||
width = _storage?.read("width") ?? kDefaultWindowWidth;
|
gameServerDll = _createController("game_server", "reboot.dll");
|
||||||
height = _storage?.read("height") ?? kDefaultWindowHeight;
|
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
|
||||||
offsetX = _storage?.read("offset_x");
|
backendDll = _createController("backend", "cobalt.dll");
|
||||||
offsetY = _storage?.read("offset_y");
|
memoryLeakDll = _createController("memory_leak", "memory.dll");
|
||||||
themeMode = Rx(ThemeMode.values.elementAt(_storage?.read("theme") ?? 0));
|
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
|
||||||
themeMode.listen((value) => _storage?.write("theme", value.index));
|
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
|
||||||
language = RxString(_storage?.read("language") ?? currentLocale);
|
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||||
language.listen((value) => _storage?.write("language", value));
|
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||||
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
|
offsetX = _storage.read("offset_x");
|
||||||
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
|
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) {
|
void saveWindowSize(Size size) {
|
||||||
_storage?.write("width", size.width);
|
_storage.write("width", size.width);
|
||||||
_storage?.write("height", size.height);
|
_storage.write("height", size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveWindowOffset(Offset position) {
|
void saveWindowOffset(Offset position) {
|
||||||
offsetX = position.dx;
|
offsetX = position.dx;
|
||||||
offsetY = position.dy;
|
offsetY = position.dy;
|
||||||
_storage?.write("offset_x", offsetX);
|
_storage.write("offset_x", offsetX);
|
||||||
_storage?.write("offset_y", offsetY);
|
_storage.write("offset_y", offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notifyLauncherUpdate() async {
|
void reset(){
|
||||||
if (appVersion == null) {
|
gameServerDll.text = _controllerDefaultPath("reboot.dll");
|
||||||
return;
|
unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll");
|
||||||
}
|
backendDll.text = _controllerDefaultPath("cobalt.dll");
|
||||||
|
gameServerPort.text = kDefaultGameServerPort;
|
||||||
final pubspec = await _getPubspecYaml();
|
firstRun.value = true;
|
||||||
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"));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _getPubspecYaml() async {
|
String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
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:clipboard/clipboard.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
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/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
|
import 'dialog_button.dart';
|
||||||
|
|
||||||
bool inDialog = false;
|
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;
|
inDialog = true;
|
||||||
pagesController.add(null);
|
pagesController.add(null);
|
||||||
try {
|
try {
|
||||||
return await fluent.showDialog(
|
return await fluent.showDialog(
|
||||||
context: appNavigatorKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
dismissWithEsc: dismissWithEsc,
|
|
||||||
builder: builder
|
builder: builder
|
||||||
);
|
);
|
||||||
}finally {
|
}finally {
|
||||||
@@ -57,7 +58,7 @@ class FormDialog extends AbstractDialog {
|
|||||||
return Form(
|
return Form(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||||
return GenericDialog(
|
return GenericDialog(
|
||||||
header: content,
|
header: content,
|
||||||
buttons: parsed
|
buttons: parsed
|
||||||
@@ -116,9 +117,8 @@ class InfoDialog extends AbstractDialog {
|
|||||||
class ProgressDialog extends AbstractDialog {
|
class ProgressDialog extends AbstractDialog {
|
||||||
final String text;
|
final String text;
|
||||||
final Function()? onStop;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -132,12 +132,11 @@ class ProgressDialog extends AbstractDialog {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
buttons: [
|
buttons: [
|
||||||
if(showButton)
|
DialogButton(
|
||||||
DialogButton(
|
text: translations.defaultDialogSecondaryAction,
|
||||||
text: translations.defaultDialogSecondaryAction,
|
type: ButtonType.only,
|
||||||
type: ButtonType.only,
|
onTap: onStop
|
||||||
onTap: onStop
|
)
|
||||||
)
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -240,7 +239,7 @@ class ErrorDialog extends AbstractDialog {
|
|||||||
type: type,
|
type: type,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
FlutterClipboard.controlC("$error\n$stackTrace");
|
FlutterClipboard.controlC("$error\n$stackTrace");
|
||||||
showRebootInfoBar(translations.copyErrorDialogSuccess);
|
showInfoBar(translations.copyErrorDialogSuccess);
|
||||||
onClick();
|
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 infoBarShortDuration = Duration(seconds: 2);
|
||||||
const _height = 64.0;
|
const _height = 64.0;
|
||||||
|
|
||||||
InfoBarEntry showRebootInfoBar(dynamic text, {
|
InfoBarEntry showInfoBar(dynamic text, {
|
||||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||||
bool loading = false,
|
bool loading = false,
|
||||||
Duration? duration = infoBarShortDuration,
|
Duration? duration = infoBarShortDuration,
|
||||||
@@ -21,39 +21,33 @@ InfoBarEntry showRebootInfoBar(dynamic text, {
|
|||||||
return overlayEntry;
|
return overlayEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
|
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => SizedBox(
|
||||||
constraints: BoxConstraints(
|
width: double.infinity,
|
||||||
minHeight: _height
|
height: _height,
|
||||||
),
|
|
||||||
child: Mica(
|
child: Mica(
|
||||||
elevation: 1,
|
child: InfoBar(
|
||||||
child: InfoBar(
|
title: Row(
|
||||||
title: Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
if(text is Widget)
|
||||||
children: [
|
text,
|
||||||
Expanded(
|
if(text is String)
|
||||||
child: text is Widget ? text : Text(text)
|
Text(text),
|
||||||
),
|
if(action != null)
|
||||||
if(action != null)
|
action
|
||||||
action
|
],
|
||||||
],
|
),
|
||||||
),
|
isLong: false,
|
||||||
isLong: false,
|
isIconVisible: true,
|
||||||
isIconVisible: true,
|
content: SizedBox(
|
||||||
content: SizedBox(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
child: loading ? const Padding(
|
||||||
child: loading ? const Padding(
|
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
|
||||||
padding: const EdgeInsets.only(
|
child: ProgressBar(),
|
||||||
top: 8.0,
|
) : const SizedBox()
|
||||||
bottom: 2.0,
|
),
|
||||||
right: 6.0
|
severity: severity
|
||||||
),
|
),
|
||||||
child: ProgressBar(),
|
|
||||||
) : const SizedBox()
|
|
||||||
),
|
|
||||||
severity: severity
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
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';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
Future<void> showResetDialog(Function() onConfirm) => showAppDialog(
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
text: translations.resetDefaultsDialogTitle,
|
text: translations.resetDefaultsDialogTitle,
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
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';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
Future<void> showDllDeletedDialog(Function() onConfirm) => showAppDialog(
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
text: translations.dllDeletedTitle,
|
text: translations.dllDeletedTitle,
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
|
import '../../util/log.dart';
|
||||||
|
|
||||||
|
|
||||||
String? lastError;
|
String? lastError;
|
||||||
|
|
||||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||||
@@ -18,15 +21,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastError = exception.toString();
|
lastError = exception.toString();
|
||||||
if(inDialog){
|
var route = ModalRoute.of(pageKey.currentContext!);
|
||||||
final context = pageKey.currentContext;
|
if(route != null && !route.isCurrent){
|
||||||
if(context != null) {
|
Navigator.of(pageKey.currentContext!).pop(false);
|
||||||
Navigator.of(context).pop(false);
|
|
||||||
inDialog = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showAppDialog(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
exception: exception,
|
exception: exception,
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import 'package:email_validator/email_validator.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/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';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final showPassword = RxBool(false);
|
|
||||||
final oldUsername = username.text;
|
Future<bool> showProfileForm(BuildContext context) async{
|
||||||
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
var showPassword = RxBool(false);
|
||||||
final oldPassword = password.text;
|
var oldUsername = _gameController.username.text;
|
||||||
final result = await showRebootDialog<bool?>(
|
var showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||||
|
var oldPassword = _gameController.password.text;
|
||||||
|
var result = await showAppDialog<bool?>(
|
||||||
builder: (context) => Obx(() => FormDialog(
|
builder: (context) => Obx(() => FormDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -22,18 +24,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
|||||||
label: translations.usernameOrEmail,
|
label: translations.usernameOrEmail,
|
||||||
child: TextFormBox(
|
child: TextFormBox(
|
||||||
placeholder: translations.usernameOrEmailPlaceholder,
|
placeholder: translations.usernameOrEmailPlaceholder,
|
||||||
validator: (text) {
|
controller: _gameController.username,
|
||||||
if(password.text.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(EmailValidator.validate(username.text)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations.invalidEmail;
|
|
||||||
},
|
|
||||||
controller: username,
|
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
enableSuggestions: true,
|
enableSuggestions: true,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -45,7 +36,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
|||||||
label: translations.password,
|
label: translations.password,
|
||||||
child: TextFormBox(
|
child: TextFormBox(
|
||||||
placeholder: translations.passwordPlaceholder,
|
placeholder: translations.passwordPlaceholder,
|
||||||
controller: password,
|
controller: _gameController.password,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
obscureText: !showPassword.value,
|
obscureText: !showPassword.value,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
@@ -54,8 +45,8 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
|||||||
suffix: Button(
|
suffix: Button(
|
||||||
onPressed: () => showPassword.value = !showPassword.value,
|
onPressed: () => showPassword.value = !showPassword.value,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
shape: ButtonState.all(const CircleBorder()),
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
backgroundColor: ButtonState.all(Colors.transparent)
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||||
@@ -85,7 +76,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
username.text = oldUsername;
|
_gameController.username.text = oldUsername;
|
||||||
password.text = oldPassword;
|
_gameController.password.text = oldPassword;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1,54 +1,28 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.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/dialog/abstract/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.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 {
|
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 {
|
Future<bool> toggleInteractive() async {
|
||||||
cancelInteractive();
|
final stream = toggle();
|
||||||
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 completer = Completer<bool>();
|
final completer = Completer<bool>();
|
||||||
InfoBarEntry? entry;
|
InfoBarEntry? entry;
|
||||||
worker = stream.listen((event) {
|
worker = stream.listen((event) {
|
||||||
@@ -65,103 +39,108 @@ extension ServerControllerDialog on BackendController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InfoBarEntry _handeEvent(ServerResult event) {
|
InfoBarEntry _handeEvent(ServerResult event) {
|
||||||
log("[BACKEND] Handling event: $event");
|
switch (event.type) {
|
||||||
switch (event.type) {
|
|
||||||
case ServerResultType.starting:
|
case ServerResultType.starting:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.startingServer,
|
translations.startingServer,
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.startSuccess:
|
case ServerResultType.startSuccess:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
);
|
);
|
||||||
case ServerResultType.startError:
|
case ServerResultType.startError:
|
||||||
print(event.stackTrace);
|
print(event.stackTrace);
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.stopping:
|
case ServerResultType.stopping:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.stoppingServer,
|
translations.stoppingServer,
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.stopSuccess:
|
case ServerResultType.stopSuccess:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.stoppedServer,
|
translations.stoppedServer,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
);
|
);
|
||||||
case ServerResultType.stopError:
|
case ServerResultType.stopError:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.stopServerError(event.error ?? translations.unknownError),
|
translations.stopServerError(event.error ?? translations.unknownError),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.missingHostError:
|
case ServerResultType.missingHostError:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.missingHostNameError,
|
translations.missingHostNameError,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
case ServerResultType.missingPortError:
|
case ServerResultType.missingPortError:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.missingPortError,
|
translations.missingPortError,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
case ServerResultType.illegalPortError:
|
case ServerResultType.illegalPortError:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.illegalPortError,
|
translations.illegalPortError,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
case ServerResultType.freeingPort:
|
case ServerResultType.freeingPort:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.freeingPort,
|
translations.freeingPort,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.freePortSuccess:
|
case ServerResultType.freePortSuccess:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.freedPort,
|
translations.freedPort,
|
||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
duration: infoBarShortDuration
|
duration: infoBarShortDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.freePortError:
|
case ServerResultType.freePortError:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.freePortError(event.error ?? translations.unknownError),
|
translations.freePortError(event.error ?? translations.unknownError),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.pingingRemote:
|
case ServerResultType.pingingRemote:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.pingingServer(ServerType.remote.name),
|
translations.pingingRemoteServer,
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.pingingLocal:
|
case ServerResultType.pingingLocal:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.pingingServer(type.value.name),
|
translations.pingingLocalServer(type.value.name),
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.pingError:
|
case ServerResultType.pingError:
|
||||||
return _showRebootInfoBar(
|
return showInfoBar(
|
||||||
translations.pingError(type.value.name),
|
translations.pingError(type.value.name),
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
|
void joinLocalHost() {
|
||||||
if(!kDebugMode && uuid == server.id) {
|
gameServerAddress.text = kDefaultGameServerHost;
|
||||||
_showRebootInfoBar(
|
gameServerOwner.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> joinServer(String uuid, Map<String, dynamic> entry) async {
|
||||||
|
final id = entry["id"];
|
||||||
|
if(uuid == id) {
|
||||||
|
showInfoBar(
|
||||||
translations.joinSelfServer,
|
translations.joinSelfServer,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
@@ -169,29 +148,18 @@ extension ServerControllerDialog on BackendController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final gameController = Get.find<GameController>();
|
final hashedPassword = entry["password"];
|
||||||
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 hasPassword = hashedPassword != null;
|
final hasPassword = hashedPassword != null;
|
||||||
final embedded = type.value == ServerType.embedded;
|
final embedded = type.value == ServerType.embedded;
|
||||||
final author = server.author;
|
final author = entry["author"];
|
||||||
final encryptedIp = server.ip;
|
final encryptedIp = entry["ip"];
|
||||||
if(!hasPassword) {
|
if(!hasPassword) {
|
||||||
final valid = await _isServerValid(encryptedIp);
|
final valid = await _isServerValid(encryptedIp);
|
||||||
if(!valid) {
|
if(!valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSuccess(gameController, embedded, encryptedIp, author, version);
|
_onSuccess(embedded, encryptedIp, author);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +169,7 @@ extension ServerControllerDialog on BackendController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||||
_showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.wrongServerPassword,
|
translations.wrongServerPassword,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
@@ -215,7 +183,7 @@ extension ServerControllerDialog on BackendController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSuccess(gameController, embedded, decryptedIp, author, version);
|
_onSuccess(embedded, decryptedIp, author);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _isServerValid(String address) async {
|
Future<bool> _isServerValid(String address) async {
|
||||||
@@ -224,7 +192,7 @@ extension ServerControllerDialog on BackendController {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.offlineServer,
|
translations.offlineServer,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
@@ -236,7 +204,7 @@ extension ServerControllerDialog on BackendController {
|
|||||||
final confirmPasswordController = TextEditingController();
|
final confirmPasswordController = TextEditingController();
|
||||||
final showPassword = RxBool(false);
|
final showPassword = RxBool(false);
|
||||||
final showPasswordTrailing = RxBool(false);
|
final showPasswordTrailing = RxBool(false);
|
||||||
return await showRebootDialog<String?>(
|
return await showAppDialog<String?>(
|
||||||
builder: (context) => FormDialog(
|
builder: (context) => FormDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -257,8 +225,8 @@ extension ServerControllerDialog on BackendController {
|
|||||||
suffix: !showPasswordTrailing.value ? null : Button(
|
suffix: !showPasswordTrailing.value ? null : Button(
|
||||||
onPressed: () => showPassword.value = !showPassword.value,
|
onPressed: () => showPassword.value = !showPassword.value,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
shape: ButtonState.all(const CircleBorder()),
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
backgroundColor: ButtonState.all(Colors.transparent)
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
|
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) {
|
if(embedded) {
|
||||||
gameServerAddress.text = decryptedIp;
|
gameServerAddress.text = decryptedIp;
|
||||||
pageIndex.value = RebootPageType.play.index;
|
gameServerOwner.value = author;
|
||||||
|
pageIndex.value = 0;
|
||||||
}else {
|
}else {
|
||||||
FlutterClipboard.controlC(decryptedIp);
|
FlutterClipboard.controlC(decryptedIp);
|
||||||
}
|
}
|
||||||
controller.selectedVersion = version;
|
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
|
|
||||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
InfoBarEntry _showRebootInfoBar(dynamic text, {
|
extension HostingControllerExtension on HostingController {
|
||||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
Future<void> publishServer(String author, String version) async {
|
||||||
bool loading = false,
|
try {
|
||||||
Duration? duration = infoBarShortDuration,
|
_publishingSemaphore.acquire();
|
||||||
void Function()? onDismissed,
|
if(published.value) {
|
||||||
Widget? action
|
return;
|
||||||
}) {
|
}
|
||||||
final result = showRebootInfoBar(
|
|
||||||
text,
|
final passwordText = password.text;
|
||||||
severity: severity,
|
final hasPassword = passwordText.isNotEmpty;
|
||||||
loading: loading,
|
var ip = await Ipify.ipv4();
|
||||||
duration: duration,
|
if(hasPassword) {
|
||||||
onDismissed: onDismissed,
|
ip = aes256Encrypt(ip, passwordText);
|
||||||
action: action
|
}
|
||||||
);
|
|
||||||
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
|
final supabase = Supabase.instance.client;
|
||||||
_infoBars.add(result);
|
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: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/page/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
|
|
||||||
abstract class RebootPage extends StatefulWidget {
|
abstract class RebootPage extends StatefulWidget {
|
||||||
const RebootPage({super.key});
|
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> {
|
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
var buttonWidget = button;
|
var buttonWidget = button;
|
||||||
if(buttonWidget == null) {
|
if(buttonWidget == null) {
|
||||||
return Column(
|
return _listView;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildFirstLaunchInfo(),
|
|
||||||
Expanded(
|
|
||||||
child: _listView
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
_buildFirstLaunchInfo(),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: _listView,
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _listView,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: 1000
|
|
||||||
),
|
|
||||||
child: buttonWidget
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
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(
|
ListView get _listView => ListView.builder(
|
||||||
itemCount: settings.length,
|
itemCount: settings.length,
|
||||||
itemBuilder: (context, index) => settings[index],
|
itemBuilder: (context, index) => settings[index],
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.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/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/keyboard.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:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
import '../../dialog/implementation/data.dart';
|
||||||
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
|
|
||||||
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
|
||||||
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
|
||||||
|
|
||||||
class BackendPage extends RebootPage {
|
class BackendPage extends RebootPage {
|
||||||
const BackendPage({Key? key}) : super(key: key);
|
const BackendPage({Key? key}) : super(key: key);
|
||||||
@@ -42,10 +38,11 @@ class BackendPage extends RebootPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BackendPageState extends RebootPageState<BackendPage> {
|
class _BackendPageState extends RebootPageState<BackendPage> {
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
|
||||||
InfoBarEntry? _infoBarEntry;
|
InfoBarEntry? _infoBarEntry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
|
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
|
||||||
@@ -54,7 +51,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(keyEvent.physicalKey.isUnrealEngineKey) {
|
if(keyEvent.physicalKey.isUnrealEngineKey) {
|
||||||
_backendController.consoleKey.value = keyEvent.physicalKey;
|
_gameController.consoleKey.value = keyEvent.physicalKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
_infoBarEntry?.close();
|
_infoBarEntry?.close();
|
||||||
@@ -63,7 +60,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Widget> get settings => [
|
List<Widget> get settings => [
|
||||||
_type,
|
_type,
|
||||||
@@ -87,13 +84,10 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.matchmakerConfigurationAddressName),
|
title: Text(translations.matchmakerConfigurationAddressName),
|
||||||
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
||||||
content: OverlayTarget(
|
content: TextFormBox(
|
||||||
key: backendGameServerAddressOverlayTargetKey,
|
placeholder: translations.matchmakerConfigurationAddressName,
|
||||||
child: TextFormBox(
|
controller: _backendController.gameServerAddress,
|
||||||
placeholder: translations.matchmakerConfigurationAddressName,
|
focusNode: _backendController.gameServerAddressFocusNode
|
||||||
controller: _backendController.gameServerAddress,
|
|
||||||
focusNode: _backendController.gameServerAddressFocusNode
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -152,24 +146,21 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
Obx(() => Text(
|
Text(
|
||||||
_backendController.detached.value ? translations.on : translations.off
|
_backendController.detached.value ? translations.on : translations.off
|
||||||
)),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16.0
|
width: 16.0
|
||||||
),
|
),
|
||||||
OverlayTarget(
|
ToggleSwitch(
|
||||||
key: backendDetachedOverlayTargetKey,
|
checked: _backendController.detached(),
|
||||||
child: ToggleSwitch(
|
onChanged: (value) => _backendController.detached.value = value
|
||||||
checked: _backendController.detached(),
|
|
||||||
onChanged: (value) => _backendController.detached.value = value
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget get _unrealEngineConsoleKey => Obx(() {
|
Widget get _unrealEngineConsoleKey => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.embedded) {
|
if(_backendController.type.value != ServerType.embedded) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -182,18 +173,14 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
title: Text(translations.settingsClientConsoleKeyName),
|
title: Text(translations.settingsClientConsoleKeyName),
|
||||||
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: OverlayTarget(
|
content: Button(
|
||||||
key: backendUnrealEngineOverlayTargetKey,
|
onPressed: () {
|
||||||
child: Button(
|
_infoBarEntry = showInfoBar(
|
||||||
onPressed: () {
|
translations.clickKey,
|
||||||
_infoBarEntry = showRebootInfoBar(
|
loading: true
|
||||||
translations.clickKey,
|
);
|
||||||
loading: true,
|
},
|
||||||
duration: null
|
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(_backendController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -234,9 +221,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.backendTypeName),
|
title: Text(translations.backendTypeName),
|
||||||
subtitle: Text(translations.backendTypeDescription),
|
subtitle: Text(translations.backendTypeDescription),
|
||||||
content: ServerTypeSelector(
|
content: const ServerTypeSelector()
|
||||||
overlayKey: backendTypeOverlayTargetKey
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@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 'dart:ui';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart' show MaterialPage;
|
import 'package:flutter/material.dart' show MaterialPage;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.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/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
|
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/server.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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.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/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.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/info_bar_area.dart';
|
||||||
import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
||||||
import 'package:version/version.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
static const double kDefaultPadding = 12.0;
|
|
||||||
static const double kTitleBarHeight = 32;
|
|
||||||
|
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,15 +37,16 @@ class HomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
|
static const double _kDefaultPadding = 12.0;
|
||||||
|
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final UpdateController _updateController = Get.find<UpdateController>();
|
||||||
final GlobalKey _searchKey = GlobalKey();
|
final GlobalKey _searchKey = GlobalKey();
|
||||||
final FocusNode _searchFocusNode = FocusNode();
|
final FocusNode _searchFocusNode = FocusNode();
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final RxBool _focused = RxBool(true);
|
final RxBool _focused = RxBool(true);
|
||||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
@@ -59,7 +55,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
_syncPageViewWithNavigator();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_checkUpdates();
|
_checkUpdates();
|
||||||
_initAppLink();
|
_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 {
|
void _initAppLink() async {
|
||||||
final appLinks = AppLinks();
|
final appLinks = AppLinks();
|
||||||
final initialUrl = await appLinks.getInitialLink();
|
final initialUrl = await appLinks.getInitialLink();
|
||||||
@@ -93,9 +76,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
final uuid = uri.host;
|
final uuid = uri.host;
|
||||||
final server = _hostingController.findServerById(uuid);
|
final server = _hostingController.findServerById(uuid);
|
||||||
if(server != null) {
|
if(server != null) {
|
||||||
_backendController.joinServerInteractive(_hostingController.uuid, server);
|
_backendController.joinServer(_hostingController.uuid, server);
|
||||||
}else {
|
}else {
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.noServerFound,
|
translations.noServerFound,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
@@ -110,14 +93,15 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await pingGameServer(address);
|
var result = await pingGameServer(address);
|
||||||
if(result) {
|
if(result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_backendController.joinLocalhost();
|
var oldOwner = _backendController.gameServerOwner.value;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
_backendController.joinLocalHost();
|
||||||
translations.serverNoLongerAvailableUnnamed,
|
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||||
|
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||||
severity: InfoBarSeverity.warning,
|
severity: InfoBarSeverity.warning,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
));
|
));
|
||||||
@@ -128,43 +112,27 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _checkUpdates() {
|
void _checkUpdates() {
|
||||||
_settingsController.notifyLauncherUpdate();
|
_updateController.notifyLauncherUpdate();
|
||||||
|
|
||||||
if(!dllsDirectory.existsSync()) {
|
if(!dllsDirectory.existsSync()) {
|
||||||
dllsDirectory.createSync(recursive: true);
|
dllsDirectory.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final dummy = Version.parse("1");
|
|
||||||
final dummyS20 = Version.parse("20");
|
|
||||||
for(final injectable in InjectableDll.values) {
|
for(final injectable in InjectableDll.values) {
|
||||||
_downloadDll(dummy, injectable);
|
downloadCriticalDllInteractive(
|
||||||
if(injectable.isVersionDependent) {
|
injectable.path,
|
||||||
_downloadDll(dummyS20, injectable);
|
silent: true
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
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
|
@override
|
||||||
void onWindowClose() async {
|
void onWindowClose() {
|
||||||
try {
|
exit(0); // Force closing
|
||||||
await _hostingController.discardServer();
|
|
||||||
}catch(error) {
|
|
||||||
log("[HOSTING] Cannot discard server: $error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -183,7 +151,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowBlur() {
|
void onWindowBlur() {
|
||||||
_focused.value = !_focused.value;
|
_focused.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -228,18 +196,14 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowResized() {
|
void onWindowResized() {
|
||||||
|
_settingsController.saveWindowSize(appWindow.size);
|
||||||
_focused.value = true;
|
_focused.value = true;
|
||||||
windowManager.getSize().then((size) {
|
|
||||||
_settingsController.saveWindowSize(size);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowMoved() {
|
void onWindowMoved() {
|
||||||
|
_settingsController.saveWindowOffset(appWindow.position);
|
||||||
_focused.value = true;
|
_focused.value = true;
|
||||||
windowManager.getPosition().then((position) {
|
|
||||||
_settingsController.saveWindowOffset(position);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -252,365 +216,136 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
_focused.value = true;
|
_focused.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowEvent(String eventName) {
|
|
||||||
if(eventName != "move") {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
_settingsController.language.value;
|
_settingsController.language.value;
|
||||||
loadTranslations(context);
|
loadTranslations(context);
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
return Container(
|
return NavigationPaneTheme(
|
||||||
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
data: NavigationPaneThemeData(
|
||||||
child: Column(
|
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
child: NavigationView(
|
||||||
SizedBox(
|
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||||
height: HomePage.kTitleBarHeight,
|
padding: _kDefaultPadding,
|
||||||
child: Row(
|
controller: pagesController,
|
||||||
children: [
|
body: body
|
||||||
_backButton,
|
),
|
||||||
Expanded(child: _draggableArea),
|
appBar: NavigationAppBar(
|
||||||
WindowTitleBar(focused: _focused())
|
height: 32,
|
||||||
],
|
title: _draggableArea,
|
||||||
)
|
actions: WindowTitleBar(focused: _focused()),
|
||||||
),
|
leading: _backButton,
|
||||||
Expanded(
|
automaticallyImplyLeading: false,
|
||||||
child: Navigator(
|
),
|
||||||
key: appNavigatorKey,
|
pane: NavigationPane(
|
||||||
onPopPage: (page, data) => false,
|
selected: pageIndex.value,
|
||||||
pages: [
|
onChanged: (index) {
|
||||||
MaterialPage(
|
final lastPageIndex = pageIndex.value;
|
||||||
child: Overlay(
|
if(lastPageIndex != index) {
|
||||||
key: appOverlayKey,
|
pageIndex.value = index;
|
||||||
initialEntries: [
|
}else if(pageStack.isNotEmpty) {
|
||||||
OverlayEntry(
|
Navigator.of(pageKey.currentContext!).pop();
|
||||||
maintainState: true,
|
final element = pageStack.removeLast();
|
||||||
builder: (context) => Row(
|
appStack.remove(element);
|
||||||
children: [
|
pagesController.add(null);
|
||||||
_buildLateralView(),
|
}
|
||||||
_buildBody()
|
},
|
||||||
],
|
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(
|
Widget get _backButton => StreamBuilder(
|
||||||
stream: pagesController.stream,
|
stream: pagesController.stream,
|
||||||
builder: (context, _) => Button(
|
builder: (context, _) => Button(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
|
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
|
||||||
vertical: 12.0,
|
backgroundColor: ButtonState.all(Colors.transparent),
|
||||||
horizontal: 16.0
|
shape: ButtonState.all(Border())
|
||||||
)),
|
),
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||||
shape: WidgetStateProperty.all(Border())
|
if(inDialog) {
|
||||||
),
|
Navigator.of(appKey.currentContext!).pop();
|
||||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
}else {
|
||||||
if(inDialog) {
|
final lastPage = appStack.removeLast();
|
||||||
Navigator.of(appNavigatorKey.currentContext!).pop();
|
pageStack.remove(lastPage);
|
||||||
}else {
|
if (lastPage is int) {
|
||||||
final lastPage = appStack.removeLast();
|
hitBack = true;
|
||||||
pageStack.remove(lastPage);
|
pageIndex.value = lastPage;
|
||||||
if (lastPage is int) {
|
} else {
|
||||||
hitBack = true;
|
Navigator.of(pageKey.currentContext!).pop();
|
||||||
pageIndex.value = lastPage;
|
}
|
||||||
} else {
|
|
||||||
Navigator.of(pageKey.currentContext!).pop();
|
|
||||||
}
|
}
|
||||||
}
|
pagesController.add(null);
|
||||||
pagesController.add(null);
|
},
|
||||||
},
|
child: const Icon(FluentIcons.back, size: 12.0),
|
||||||
child: const Icon(FluentIcons.back, size: 12.0),
|
)
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
GestureDetector get _draggableArea => GestureDetector(
|
GestureDetector get _draggableArea => GestureDetector(
|
||||||
onDoubleTap: windowManager.maximizeOrRestore,
|
onDoubleTap: appWindow.maximizeOrRestore,
|
||||||
onHorizontalDragStart: (_) => windowManager.startDragging(),
|
onHorizontalDragStart: (_) => appWindow.startDragging(),
|
||||||
onVerticalDragStart: (_) => windowManager.startDragging()
|
onVerticalDragStart: (_) => appWindow.startDragging()
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget get _autoSuggestBox => Padding(
|
Widget get _autoSuggestBox => Obx(() {
|
||||||
padding: const EdgeInsets.symmetric(
|
final firstRun = _settingsController.firstRun.value;
|
||||||
horizontal: 16.0,
|
return Padding(
|
||||||
vertical: 8.0
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 16.0,
|
||||||
child: AutoSuggestBox<PageSuggestion>(
|
vertical: 8.0
|
||||||
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
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
items: _suggestedItems,
|
child: AutoSuggestBox<PageSuggestion>(
|
||||||
autofocus: true,
|
key: _searchKey,
|
||||||
trailingIcon: IgnorePointer(
|
controller: _searchController,
|
||||||
child: IconButton(
|
enabled: !firstRun,
|
||||||
onPressed: () {},
|
placeholder: translations.find,
|
||||||
icon: Transform.flip(
|
focusNode: _searchFocusNode,
|
||||||
flipX: true,
|
selectionHeightStyle: BoxHeightStyle.max,
|
||||||
child: const Icon(FluentIcons.search)
|
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) {
|
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
|
||||||
final pageIcon = SizedBox.square(
|
final pageIcon = SizedBox.square(
|
||||||
@@ -629,6 +364,266 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
));
|
));
|
||||||
return results;
|
return results;
|
||||||
}).toList();
|
}).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 {
|
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:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.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/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||||
|
|
||||||
class InfoPage extends RebootPage {
|
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);
|
const InfoPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,52 +67,53 @@ class InfoPage extends RebootPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
static const String _kDiscordInviteUrl = "https://discord.gg/reboot";
|
RxInt _counter = RxInt(kDebugMode ? 0 : 180);
|
||||||
|
late bool _showButton;
|
||||||
@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)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@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:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:get/get.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/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/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/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/game_start_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
|
||||||
|
|
||||||
class PlayPage extends RebootPage {
|
class PlayPage extends RebootPage {
|
||||||
const PlayPage({Key? key}) : super(key: key);
|
const PlayPage({Key? key}) : super(key: key);
|
||||||
@@ -33,8 +34,10 @@ class PlayPage extends RebootPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? get button => LaunchButton(
|
Widget? get button => LaunchButton(
|
||||||
startLabel: translations.launchFortnite,
|
startLabel: translations.launchFortnite,
|
||||||
@@ -44,19 +47,56 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<SettingTile> get settings => [
|
List<SettingTile> get settings => [
|
||||||
buildVersionSelector(
|
versionSelectSettingTile,
|
||||||
key: gameVersionOverlayTargetKey
|
|
||||||
),
|
|
||||||
_options,
|
_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(
|
SettingTile get _options => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.options_24_regular
|
FluentIcons.options_24_regular
|
||||||
),
|
),
|
||||||
title: Text(translations.settingsClientOptionsName),
|
title: Text(translations.settingsServerOptionsName),
|
||||||
subtitle: Text(translations.settingsClientOptionsDescription),
|
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||||
children: [
|
children: [
|
||||||
SettingTile(
|
SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -72,17 +112,33 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
SettingTile get _resetDefaults => SettingTile(
|
SettingTile get _matchmakerTile => SettingTile(
|
||||||
icon: Icon(
|
onPressed: () {
|
||||||
FluentIcons.arrow_reset_24_regular
|
pageIndex.value = RebootPageType.backend.index;
|
||||||
),
|
WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus());
|
||||||
title: Text(translations.gameResetDefaultsName),
|
},
|
||||||
subtitle: Text(translations.gameResetDefaultsDescription),
|
icon: Icon(
|
||||||
content: Button(
|
FluentIcons.globe_24_regular
|
||||||
onPressed: () => showResetDialog(() {
|
),
|
||||||
_gameController.reset();
|
title: Text(translations.playGameServerCustomName),
|
||||||
}),
|
subtitle: Text(translations.playGameServerCustomDescription),
|
||||||
child: Text(translations.gameResetDefaultsContent),
|
);
|
||||||
)
|
|
||||||
|
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:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.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/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_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/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.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/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
import '../../util/checks.dart';
|
||||||
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();
|
|
||||||
|
|
||||||
class HostPage extends RebootPage {
|
class HostPage extends RebootPage {
|
||||||
const HostPage({Key? key}) : super(key: key);
|
const HostPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => translations.hostName;
|
String get name => "Host";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get iconAsset => "assets/images/host.png";
|
String get iconAsset => "assets/images/host.png";
|
||||||
@@ -53,7 +47,8 @@ class HostPage extends RebootPage {
|
|||||||
class _HostingPageState extends RebootPageState<HostPage> {
|
class _HostingPageState extends RebootPageState<HostPage> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
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);
|
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||||
|
|
||||||
@@ -72,31 +67,39 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get button => LaunchButton(
|
Widget get button => LaunchButton(
|
||||||
host: true,
|
host: true,
|
||||||
startLabel: translations.startHosting,
|
startLabel: translations.startHosting,
|
||||||
stopLabel: translations.stopHosting
|
stopLabel: translations.stopHosting
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Widget> get settings => [
|
List<Widget> get settings => [
|
||||||
_information,
|
_information,
|
||||||
buildVersionSelector(
|
versionSelectSettingTile,
|
||||||
key: hostVersionOverlayTargetKey
|
|
||||||
),
|
|
||||||
_options,
|
_options,
|
||||||
|
_internalFiles,
|
||||||
_share,
|
_share,
|
||||||
_resetDefaults
|
_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(
|
SettingTile get _information => SettingTile(
|
||||||
key: hostInfoTileKey,
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.info_24_regular
|
FluentIcons.info_24_regular
|
||||||
),
|
),
|
||||||
title: Text(translations.hostGameServerName),
|
title: Text(translations.hostGameServerName),
|
||||||
subtitle: Text(translations.hostGameServerDescription),
|
subtitle: Text(translations.hostGameServerDescription),
|
||||||
overlayKey: hostInfoOverlayTargetKey,
|
|
||||||
children: [
|
children: [
|
||||||
SettingTile(
|
SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -104,14 +107,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.hostGameServerNameName),
|
title: Text(translations.hostGameServerNameName),
|
||||||
subtitle: Text(translations.hostGameServerNameDescription),
|
subtitle: Text(translations.hostGameServerNameDescription),
|
||||||
content: OverlayTarget(
|
content: TextFormBox(
|
||||||
key: hostInfoNameOverlayTargetKey,
|
placeholder: translations.hostGameServerNameName,
|
||||||
child: TextFormBox(
|
controller: _hostingController.name,
|
||||||
placeholder: translations.hostGameServerNameName,
|
onChanged: (_) => _updateServer()
|
||||||
controller: _hostingController.name,
|
|
||||||
focusNode: _hostingController.nameFocusNode,
|
|
||||||
onChanged: (_) => _updateServer()
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
@@ -120,14 +119,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.hostGameServerDescriptionName),
|
title: Text(translations.hostGameServerDescriptionName),
|
||||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||||
content: OverlayTarget(
|
content: TextFormBox(
|
||||||
key: hostInfoDescriptionOverlayTargetKey,
|
placeholder: translations.hostGameServerDescriptionName,
|
||||||
child: TextFormBox(
|
controller: _hostingController.description,
|
||||||
placeholder: translations.hostGameServerDescriptionName,
|
onChanged: (_) => _updateServer()
|
||||||
controller: _hostingController.description,
|
|
||||||
focusNode: _hostingController.descriptionFocusNode,
|
|
||||||
onChanged: (_) => _updateServer()
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
@@ -136,32 +131,28 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.hostGameServerPasswordName),
|
title: Text(translations.hostGameServerPasswordName),
|
||||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||||
content: Obx(() => OverlayTarget(
|
content: Obx(() => TextFormBox(
|
||||||
key: hostInfoPasswordOverlayTargetKey,
|
placeholder: translations.hostGameServerPasswordName,
|
||||||
child: TextFormBox(
|
controller: _hostingController.password,
|
||||||
placeholder: translations.hostGameServerPasswordName,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
controller: _hostingController.password,
|
obscureText: !_hostingController.showPassword.value,
|
||||||
focusNode: _hostingController.passwordFocusNode,
|
enableSuggestions: false,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autocorrect: false,
|
||||||
obscureText: !_hostingController.showPassword.value,
|
onChanged: (text) {
|
||||||
enableSuggestions: false,
|
_showPasswordTrailing.value = text.isNotEmpty;
|
||||||
autocorrect: false,
|
_updateServer();
|
||||||
onChanged: (text) {
|
},
|
||||||
_showPasswordTrailing.value = text.isNotEmpty;
|
suffix: Button(
|
||||||
_updateServer();
|
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||||
},
|
style: ButtonStyle(
|
||||||
suffix: Button(
|
shape: ButtonState.all(const CircleBorder()),
|
||||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
backgroundColor: ButtonState.all(Colors.transparent)
|
||||||
style: ButtonStyle(
|
),
|
||||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
child: Icon(
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||||
),
|
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||||
child: Icon(
|
),
|
||||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
)
|
||||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
))
|
))
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
@@ -173,9 +164,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Obx(() => Row(
|
content: Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
Obx(() => Text(
|
Text(
|
||||||
_hostingController.discoverable.value ? translations.on : translations.off
|
_hostingController.discoverable.value ? translations.on : translations.off
|
||||||
)),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16.0
|
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(
|
SettingTile get _options => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.options_24_regular
|
FluentIcons.options_24_regular
|
||||||
@@ -199,34 +289,51 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
title: Text(translations.settingsServerOptionsName),
|
title: Text(translations.settingsServerOptionsName),
|
||||||
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||||
children: [
|
children: [
|
||||||
SettingTile(
|
Obx(() => SettingTile(
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.options_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsClientArgsName),
|
|
||||||
subtitle: Text(translations.settingsClientArgsDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.settingsClientArgsPlaceholder,
|
|
||||||
controller: _hostingController.customLaunchArgs,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.window_console_20_regular
|
FluentIcons.window_console_20_regular
|
||||||
),
|
),
|
||||||
title: Text(translations.gameServerTypeName),
|
title: Text(translations.hostHeadlessName),
|
||||||
subtitle: Text(translations.gameServerTypeDescription),
|
subtitle: Text(translations.hostHeadlessDescription),
|
||||||
content: Obx(() => DropDownButton(
|
contentWidth: null,
|
||||||
onOpen: () => inDialog = true,
|
content: Row(
|
||||||
onClose: () => inDialog = false,
|
children: [
|
||||||
leading: Text(_hostingController.type.value.translatedName),
|
Text(
|
||||||
items: GameServerType.values.map((entry) => MenuFlyoutItem(
|
_hostingController.headless.value ? translations.on : translations.off
|
||||||
text: Text(entry.translatedName),
|
),
|
||||||
onPressed: () => _hostingController.type.value = entry
|
const SizedBox(
|
||||||
)).toList()
|
width: 16.0
|
||||||
)),
|
),
|
||||||
),
|
ToggleSwitch(
|
||||||
SettingTile(
|
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(
|
icon: Icon(
|
||||||
FluentIcons.arrow_reset_24_regular
|
FluentIcons.arrow_reset_24_regular
|
||||||
),
|
),
|
||||||
@@ -235,19 +342,19 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
Obx(() => Text(
|
Text(
|
||||||
_hostingController.autoRestart.value ? translations.on : translations.off
|
_hostingController.autoRestart.value ? translations.on : translations.off
|
||||||
)),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16.0
|
width: 16.0
|
||||||
),
|
),
|
||||||
Obx(() => ToggleSwitch(
|
ToggleSwitch(
|
||||||
checked: _hostingController.autoRestart.value,
|
checked: _hostingController.autoRestart.value,
|
||||||
onChanged: (value) => _hostingController.autoRestart.value = value
|
onChanged: (value) => _hostingController.autoRestart.value = value
|
||||||
)),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
)),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
fluentUi.FluentIcons.number_field
|
fluentUi.FluentIcons.number_field
|
||||||
@@ -257,14 +364,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
contentWidth: 64,
|
contentWidth: 64,
|
||||||
content: TextFormBox(
|
content: TextFormBox(
|
||||||
placeholder: translations.settingsServerPortName,
|
placeholder: translations.settingsServerPortName,
|
||||||
controller: _dllController.gameServerPort,
|
controller: _settingsController.gameServerPort,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly
|
FilteringTextInputFormatter.digitsOnly
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -274,7 +381,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.hostShareName),
|
title: Text(translations.hostShareName),
|
||||||
subtitle: Text(translations.hostShareDescription),
|
subtitle: Text(translations.hostShareDescription),
|
||||||
overlayKey: hostShareOverlayTargetKey,
|
|
||||||
children: [
|
children: [
|
||||||
SettingTile(
|
SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -304,7 +410,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
final ip = await Ipify.ipv4();
|
final ip = await Ipify.ipv4();
|
||||||
entry.close();
|
entry.close();
|
||||||
FlutterClipboard.controlC(ip);
|
FlutterClipboard.controlC(ip);
|
||||||
_showCopiedIp();
|
_showCopiedIp();
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
entry?.close();
|
entry?.close();
|
||||||
_showCannotCopyIp(error);
|
_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 {
|
Future<void> _updateServer() async {
|
||||||
if(!_hostingController.published()) {
|
if(!_hostingController.published()) {
|
||||||
return;
|
return;
|
||||||
@@ -338,39 +429,49 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_hostingController.publishServer(
|
_hostingController.publishServer(
|
||||||
_hostingController.accountUsername.text,
|
_gameController.username.text,
|
||||||
_hostingController.instance.value!.version.toString()
|
_hostingController.instance.value!.versionName
|
||||||
);
|
);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
_showCannotUpdateGameServer(error);
|
_showCannotUpdateGameServer(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCopiedLink() => showRebootInfoBar(
|
void _showCopiedLink() => showInfoBar(
|
||||||
translations.hostShareLinkMessageSuccess,
|
translations.hostShareLinkMessageSuccess,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
);
|
);
|
||||||
|
|
||||||
InfoBarEntry _showCopyingIp() => showRebootInfoBar(
|
InfoBarEntry _showCopyingIp() => showInfoBar(
|
||||||
translations.hostShareIpMessageLoading,
|
translations.hostShareIpMessageLoading,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
|
|
||||||
void _showCopiedIp() => showRebootInfoBar(
|
void _showCopiedIp() => showInfoBar(
|
||||||
translations.hostShareIpMessageSuccess,
|
translations.hostShareIpMessageSuccess,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
);
|
);
|
||||||
|
|
||||||
void _showCannotCopyIp(Object error) => showRebootInfoBar(
|
void _showCannotCopyIp(Object error) => showInfoBar(
|
||||||
translations.hostShareIpMessageError(error.toString()),
|
translations.hostShareIpMessageError(error.toString()),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
|
|
||||||
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar(
|
void _showCannotUpdateGameServer(Object error) => showInfoBar(
|
||||||
translations.cannotUpdateGameServer(error.toString()),
|
translations.cannotUpdateGameServer(error.toString()),
|
||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
duration: infoBarLongDuration
|
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:flutter_localized_locales/flutter_localized_locales.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.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/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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.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:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@@ -35,287 +34,58 @@ class SettingsPage extends RebootPage {
|
|||||||
|
|
||||||
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? get button => null;
|
Widget? get button => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Widget> get settings => [
|
List<Widget> get settings => [
|
||||||
_language,
|
SettingTile(
|
||||||
_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(
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.timer_24_regular
|
FluentIcons.local_language_24_regular
|
||||||
),
|
),
|
||||||
title: Text(translations.settingsServerTimerName),
|
title: Text(translations.settingsUtilsLanguageName),
|
||||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||||
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
|
||||||
content: Obx(() => DropDownButton(
|
content: Obx(() => DropDownButton(
|
||||||
onOpen: () => inDialog = true,
|
onOpen: () => inDialog = true,
|
||||||
onClose: () => inDialog = false,
|
onClose: () => inDialog = false,
|
||||||
leading: Text(_dllController.timer.value.text),
|
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||||
text: Text(entry.text),
|
text: Text(_getLocaleName(locale.languageCode)),
|
||||||
onPressed: () {
|
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||||
_dllController.timer.value = entry;
|
|
||||||
_dllController.infoBarEntry?.close();
|
|
||||||
_dllController.updateGameServerDll(
|
|
||||||
force: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)).toList()
|
)).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(
|
SettingTile get _installationDirectory => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.folder_24_regular
|
FluentIcons.folder_24_regular
|
||||||
@@ -327,6 +97,15 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|||||||
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
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 {
|
extension _ThemeModeExtension on ThemeMode {
|
||||||
@@ -340,14 +119,4 @@ extension _ThemeModeExtension on ThemeMode {
|
|||||||
return translations.light;
|
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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.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/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/info_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/play_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/page/implementation/settings_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||||
|
|
||||||
@@ -26,15 +26,15 @@ final List<RebootPage> pages = [
|
|||||||
const SettingsPage()
|
const SettingsPage()
|
||||||
];
|
];
|
||||||
|
|
||||||
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
|
final RxInt pageIndex = _initialPageIndex;
|
||||||
|
RxInt get _initialPageIndex {
|
||||||
final RxInt pageIndex = RxInt(RebootPageType.play.index);
|
final settingsController = Get.find<SettingsController>();
|
||||||
|
return RxInt(settingsController.firstRun.value ? RebootPageType.info.index : RebootPageType.play.index);
|
||||||
|
}
|
||||||
|
|
||||||
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey();
|
final GlobalKey appKey = GlobalKey();
|
||||||
|
|
||||||
final GlobalKey<OverlayState> appOverlayKey = GlobalKey();
|
|
||||||
|
|
||||||
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
|
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
|
||||||
|
|
||||||
@@ -79,8 +79,4 @@ void addSubPageToStack(String pageName) {
|
|||||||
appStack.add(identifier);
|
appStack.add(identifier);
|
||||||
_pagesStack[index]!.add(identifier);
|
_pagesStack[index]!.add(identifier);
|
||||||
pagesController.add(null);
|
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:reboot_common/common.dart';
|
||||||
import 'package:sync/semaphore.dart';
|
import 'package:sync/semaphore.dart';
|
||||||
|
|
||||||
final File launcherLogFile = _createLoggingFile();
|
final File _loggingFile = _createLoggingFile();
|
||||||
final Semaphore _semaphore = Semaphore(1);
|
final Semaphore _semaphore = Semaphore(1);
|
||||||
|
|
||||||
File _createLoggingFile() {
|
File _createLoggingFile() {
|
||||||
final file = File("${installationDirectory.path}\\launcher.log");
|
final file = File("${logsDirectory.path}\\launcher.log");
|
||||||
file.parent.createSync(recursive: true);
|
file.parent.createSync(recursive: true);
|
||||||
if(file.existsSync()) {
|
if(file.existsSync()) {
|
||||||
file.deleteSync();
|
file.deleteSync();
|
||||||
@@ -20,9 +20,9 @@ void log(String message) async {
|
|||||||
try {
|
try {
|
||||||
await _semaphore.acquire();
|
await _semaphore.acquire();
|
||||||
print(message);
|
print(message);
|
||||||
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
print("[LOGGER_ERROR] An error occurred while logging: $error");
|
print(error);
|
||||||
}finally {
|
}finally {
|
||||||
_semaphore.release();
|
_semaphore.release();
|
||||||
}
|
}
|
||||||
@@ -1,69 +1,57 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:reboot_common/common.dart';
|
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) {
|
Future<bool> _pingGameServer(String hostname, int port) async {
|
||||||
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");
|
|
||||||
RawDatagramSocket? socket;
|
RawDatagramSocket? socket;
|
||||||
try {
|
try {
|
||||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||||
await for (final event in socket) {
|
final dataToSend = utf8.encode(DateTime.now().toIso8601String());
|
||||||
log("[MATCHMAKER] Event: $event");
|
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||||
|
await for (var event in socket) {
|
||||||
switch(event) {
|
switch(event) {
|
||||||
case RawSocketEvent.read:
|
case RawSocketEvent.read:
|
||||||
log("[MATCHMAKER] Success");
|
|
||||||
return true;
|
|
||||||
case RawSocketEvent.write:
|
case RawSocketEvent.write:
|
||||||
log("[MATCHMAKER] Sending data");
|
return true;
|
||||||
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
|
|
||||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
|
||||||
case RawSocketEvent.readClosed:
|
case RawSocketEvent.readClosed:
|
||||||
case RawSocketEvent.closed:
|
case RawSocketEvent.closed:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}catch(error) {
|
|
||||||
log("[MATCHMAKER] Error: $error");
|
|
||||||
return false;
|
return false;
|
||||||
}finally {
|
}finally {
|
||||||
socket?.close();
|
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 'dart:io';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:win32/win32.dart';
|
import 'package:win32/win32.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||||
|
|
||||||
@@ -25,25 +23,6 @@ bool get isWin11 {
|
|||||||
return intBuild != null && intBuild > 22000;
|
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 =>
|
bool get isDarkMode =>
|
||||||
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
|
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
|
||||||
|
|
||||||
@@ -97,7 +76,7 @@ class IVirtualDesktop extends IUnknown {
|
|||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _convertFromHString(result.value);
|
return convertFromHString(result.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +263,7 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, COMObject, int)>()(
|
.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) {
|
if (code != 0) {
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
}
|
}
|
||||||
@@ -373,7 +352,7 @@ List<int> _getHWnds(int pid, String? excludedWindowName) {
|
|||||||
result.ref.excluded = excludedWindowName.toNativeUtf16();
|
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 length = result.ref.HWndLength;
|
||||||
final HWndsPointer = result.ref.HWnd;
|
final HWndsPointer = result.ref.HWnd;
|
||||||
if(HWndsPointer == nullptr) {
|
if(HWndsPointer == nullptr) {
|
||||||
@@ -401,7 +380,7 @@ class VirtualDesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final hr = CoInitializeEx(
|
final hr = CoInitializeEx(
|
||||||
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
|
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
throw WindowsException(hr);
|
throw WindowsException(hr);
|
||||||
}
|
}
|
||||||
@@ -472,23 +451,3 @@ class VirtualDesktopManager {
|
|||||||
void setDesktopName(IVirtualDesktop desktop, String newName) =>
|
void setDesktopName(IVirtualDesktop desktop, String newName) =>
|
||||||
windowManager.setDesktopName(desktop, 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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
|
|
||||||
AppLocalizations? _translations;
|
AppLocalizations? _translations;
|
||||||
bool _init = false;
|
bool _init = false;
|
||||||
@@ -20,16 +19,3 @@ void loadTranslations(BuildContext context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get currentLocale => Intl.getCurrentLocale().split("_")[0];
|
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;
|
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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.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 {
|
class FileSelector extends StatefulWidget {
|
||||||
final String placeholder;
|
final String placeholder;
|
||||||
@@ -19,10 +19,10 @@ class FileSelector extends StatefulWidget {
|
|||||||
required this.controller,
|
required this.controller,
|
||||||
required this.validator,
|
required this.validator,
|
||||||
required this.folder,
|
required this.folder,
|
||||||
required this.allowNavigator,
|
|
||||||
this.label,
|
this.label,
|
||||||
this.extension,
|
this.extension,
|
||||||
this.validatorMode,
|
this.validatorMode,
|
||||||
|
this.allowNavigator = true,
|
||||||
Key? key})
|
Key? key})
|
||||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||||
super(key: key);
|
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:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:reboot_launcher/src/util/checks.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
|
|
||||||
const double _kButtonDimensions = 30;
|
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
|
||||||
const double _kButtonSpacing = 8;
|
icon: Icon(
|
||||||
|
FluentIcons.document_24_regular
|
||||||
// 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}) {
|
title: Text(title),
|
||||||
final obx = RxString(controller.text);
|
subtitle: Text(description),
|
||||||
controller.addListener(() => obx.value = controller.text);
|
content: FileSelector(
|
||||||
final selecting = RxBool(false);
|
placeholder: translations.selectPathPlaceholder,
|
||||||
return SettingTile(
|
windowTitle: translations.selectPathWindowTitle,
|
||||||
icon: Icon(
|
controller: controller,
|
||||||
FluentIcons.document_24_regular
|
validator: checkDll,
|
||||||
),
|
extension: "dll",
|
||||||
title: Text(title),
|
folder: false
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -4,24 +4,26 @@ import 'dart:io';
|
|||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
import 'package:dart_ipify/dart_ipify.dart';
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:local_notifier/local_notifier.dart';
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.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/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_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/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/server.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/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/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.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 {
|
class LaunchButton extends StatefulWidget {
|
||||||
final bool host;
|
final bool host;
|
||||||
@@ -40,12 +42,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
|
||||||
InfoBarEntry? _gameClientInfoBar;
|
InfoBarEntry? _gameClientInfoBar;
|
||||||
InfoBarEntry? _gameServerInfoBar;
|
InfoBarEntry? _gameServerInfoBar;
|
||||||
CancelableOperation? _operation;
|
CancelableOperation? _operation;
|
||||||
Completer? _pingOperation;
|
|
||||||
IVirtualDesktop? _virtualDesktop;
|
IVirtualDesktop? _virtualDesktop;
|
||||||
|
|
||||||
@override
|
@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;
|
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;
|
host ??= widget.host;
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Toggling state");
|
log("[${host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
|
||||||
if (host ? _hostingController.started() : _gameController.started()) {
|
if (host ? _hostingController.started() : _gameController.started()) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||||
_onStop(
|
_onStop(
|
||||||
@@ -94,13 +94,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
||||||
for (final injectable in InjectableDll.values) {
|
for (final injectable in InjectableDll.values) {
|
||||||
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
|
if(await _getDllFileOrStop(injectable, host) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final executable = await version.shippingExecutable;
|
final executable = version.gameExecutable;
|
||||||
if(executable == null){
|
if(executable == null){
|
||||||
log("[${host ? 'HOST' : 'GAME'}] No executable found");
|
log("[${host ? 'HOST' : 'GAME'}] No executable found");
|
||||||
_onStop(
|
_onStop(
|
||||||
@@ -120,28 +120,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
||||||
final serverType = _hostingController.type.value;
|
final headless = !forceGUI && _hostingController.headless.value;
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
final virtualDesktop = _hostingController.virtualDesktop.value;
|
||||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
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");
|
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||||
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
await _startGameProcesses(version, host, headless, virtualDesktop, linkedHostingInstance);
|
||||||
final started = host ? _hostingController.started() : _gameController.started();
|
if(!host) {
|
||||||
if(!started) {
|
_showLaunchingGameClientWidget();
|
||||||
result?.kill();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!host) {
|
if(linkedHostingInstance != null || host){
|
||||||
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
|
|
||||||
}else {
|
|
||||||
_showLaunchingGameServerWidget();
|
_showLaunchingGameServerWidget();
|
||||||
}
|
}
|
||||||
} on ProcessException catch (exception, stackTrace) {
|
|
||||||
_onStop(
|
|
||||||
reason: _StopReason.corruptedVersionError,
|
|
||||||
error: exception.toString(),
|
|
||||||
stackTrace: stackTrace
|
|
||||||
);
|
|
||||||
} catch (exception, stackTrace) {
|
} catch (exception, stackTrace) {
|
||||||
_onStop(
|
_onStop(
|
||||||
reason: _StopReason.unknownError,
|
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...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||||
if(host){
|
if(host){
|
||||||
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||||
return null;
|
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");
|
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -168,38 +159,33 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = forceLinkedHosting || await _askForAutomaticGameServer(host);
|
final response = forceLinkedHosting || await _askForAutomaticGameServer();
|
||||||
if(!response) {
|
if(!response) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
|
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...");
|
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
|
||||||
_setStarted(true, true);
|
_setStarted(true, true);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _askForAutomaticGameServer(bool host) async {
|
Future<bool> _askForAutomaticGameServer() async {
|
||||||
if (host ? !_hostingController.started() : !_gameController.started()) {
|
final result = await showAppDialog<bool>(
|
||||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
|
||||||
_onStop(reason: _StopReason.normal);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = await showRebootDialog<bool>(
|
|
||||||
builder: (context) => InfoDialog(
|
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: [
|
buttons: [
|
||||||
DialogButton(
|
DialogButton(
|
||||||
type: ButtonType.secondary,
|
type: ButtonType.secondary,
|
||||||
text: translations.automaticGameServerDialogIgnore
|
text: "Ignore"
|
||||||
),
|
),
|
||||||
DialogButton(
|
DialogButton(
|
||||||
type: ButtonType.primary,
|
type: ButtonType.primary,
|
||||||
text: translations.automaticGameServerDialogStart,
|
text: "Start server",
|
||||||
onTap: () => Navigator.of(context).pop(true),
|
onTap: () => Navigator.of(context).pop(true),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -209,7 +195,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return result;
|
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 game process...");
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
|
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
|
||||||
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
|
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
|
||||||
@@ -219,9 +205,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
|
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
|
||||||
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Started paused eac: $eacProcess");
|
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}");
|
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) {
|
if(gameProcess == null) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
|
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
|
||||||
return null;
|
return null;
|
||||||
@@ -229,11 +215,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||||
final instance = GameInstance(
|
final instance = GameInstance(
|
||||||
version: version.content,
|
versionName: version.name,
|
||||||
gamePid: gameProcess,
|
gamePid: gameProcess,
|
||||||
launcherPid: launcherProcess,
|
launcherPid: launcherProcess,
|
||||||
eacPid: eacProcess,
|
eacPid: eacProcess,
|
||||||
serverType: host ? hostType : null,
|
hosting: host,
|
||||||
child: linkedHosting
|
child: linkedHosting
|
||||||
);
|
);
|
||||||
if(host){
|
if(host){
|
||||||
@@ -242,61 +228,68 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}else{
|
}else{
|
||||||
_gameController.instance.value = instance;
|
_gameController.instance.value = instance;
|
||||||
}
|
}
|
||||||
await _injectOrShowError(InjectableDll.starfall, host);
|
await _injectOrShowError(InjectableDll.cobalt, host);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
||||||
return 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...");
|
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||||
final gameArgs = createRebootArgs(
|
final gameArgs = createRebootArgs(
|
||||||
host ? _hostingController.accountUsername.text : _gameController.username.text,
|
_gameController.username.text,
|
||||||
host ? _hostingController.accountPassword.text :_gameController.password.text,
|
_gameController.password.text,
|
||||||
host,
|
host,
|
||||||
hostType,
|
_hostingController.headless.value,
|
||||||
false,
|
""
|
||||||
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
|
|
||||||
);
|
);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
|
log("[${host ? 'HOST' : 'GAME'}] Generated game args: $gameArgs");
|
||||||
final gameProcess = await startProcess(
|
final gameProcess = await startProcess(
|
||||||
executable: executable,
|
executable: executable,
|
||||||
args: gameArgs,
|
args: gameArgs,
|
||||||
useTempBatch: false,
|
wrapProcess: false,
|
||||||
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
|
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||||
environment: {
|
|
||||||
"OPENSSL_ia32cap": "~0x20000000"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
|
||||||
void onGameOutput(String line, bool error) {
|
void onGameOutput(String line, bool error) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
|
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
|
||||||
|
|
||||||
handleGameOutput(
|
handleGameOutput(
|
||||||
line: line,
|
line: line,
|
||||||
host: host,
|
host: host,
|
||||||
onShutdown: () => _onStop(reason: _StopReason.normal),
|
onShutdown: () => _onStop(reason: _StopReason.normal),
|
||||||
onTokenError: () => _onStop(reason: _StopReason.tokenError),
|
onTokenError: () => _onStop(reason: _StopReason.tokenError),
|
||||||
onBuildCorrupted: () {
|
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError),
|
||||||
if(instance == null) {
|
|
||||||
return;
|
|
||||||
}else if(!instance.launched) {
|
|
||||||
_onStop(reason: _StopReason.corruptedVersionError);
|
|
||||||
}else {
|
|
||||||
_onStop(reason: _StopReason.crash);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLoggedIn: () =>_onLoggedIn(host),
|
onLoggedIn: () =>_onLoggedIn(host),
|
||||||
onMatchEnd: () => _onMatchEnd(version),
|
onMatchEnd: () => _onMatchEnd(version, virtualDesktop),
|
||||||
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
|
onDisplayAttached: () => _onDisplayAttached(headless, virtualDesktop, version)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
|
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
|
||||||
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
||||||
gameProcess.exitCode.then((_) async {
|
gameProcess.exitCode.then((_) async {
|
||||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
|
if(instance == null) {
|
||||||
_onStop(
|
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,
|
reason: _StopReason.exitCode,
|
||||||
host: host
|
host: true
|
||||||
|
);
|
||||||
|
await _toggle(
|
||||||
|
forceGUI: true,
|
||||||
|
host: true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return gameProcess.pid;
|
return gameProcess.pid;
|
||||||
@@ -309,26 +302,23 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
final process = await startProcess(
|
final process = await startProcess(
|
||||||
executable: file,
|
executable: file,
|
||||||
useTempBatch: false,
|
wrapProcess: false,
|
||||||
name: "${version.content}-${basenameWithoutExtension(file.path)}",
|
name: "${version.name}-${basenameWithoutExtension(file.path)}"
|
||||||
environment: {
|
|
||||||
"OPENSSL_ia32cap": "~0x20000000"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
final pid = process.pid;
|
final pid = process.pid;
|
||||||
suspend(pid);
|
suspend(pid);
|
||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDisplayAttached(bool host, GameServerType type, FortniteVersion version) async {
|
Future<void> _onDisplayAttached(bool headless, bool virtualDesktop, FortniteVersion version) async {
|
||||||
if(host && type == GameServerType.virtualWindow) {
|
if(!headless && virtualDesktop) {
|
||||||
final hostingInstance = _hostingController.instance.value;
|
final hostingInstance = _hostingController.instance.value;
|
||||||
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
|
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
|
||||||
hostingInstance.movedToVirtualDesktop = true;
|
hostingInstance.movedToVirtualDesktop = true;
|
||||||
try {
|
try {
|
||||||
final windowManager = VirtualDesktopManager.getInstance();
|
final windowManager = VirtualDesktopManager.getInstance();
|
||||||
_virtualDesktop = windowManager.createDesktop();
|
_virtualDesktop = windowManager.createDesktop();
|
||||||
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
|
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
|
||||||
var success = false;
|
var success = false;
|
||||||
try {
|
try {
|
||||||
success = await windowManager.moveWindowToDesktop(
|
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) {
|
if(_hostingController.autoRestart.value) {
|
||||||
final notification = LocalNotification(
|
final notification = LocalNotification(
|
||||||
title: translations.gameServerEnd,
|
title: translations.gameServerEnd,
|
||||||
@@ -398,11 +388,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
if(instance != null && !instance.launched) {
|
if(instance != null && !instance.launched) {
|
||||||
instance.launched = true;
|
instance.launched = true;
|
||||||
instance.tokenError = false;
|
instance.tokenError = false;
|
||||||
|
await _injectOrShowError(InjectableDll.memory, host);
|
||||||
if(!host){
|
if(!host){
|
||||||
await _injectOrShowError(InjectableDll.console, host);
|
await _injectOrShowError(InjectableDll.console, host);
|
||||||
_onGameClientInjected();
|
_onGameClientInjected();
|
||||||
}else {
|
}else {
|
||||||
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
|
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
|
||||||
if(gameServerPort != null) {
|
if(gameServerPort != null) {
|
||||||
await killProcessByPort(gameServerPort);
|
await killProcessByPort(gameServerPort);
|
||||||
}
|
}
|
||||||
@@ -414,7 +405,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
void _onGameClientInjected() {
|
void _onGameClientInjected() {
|
||||||
_gameClientInfoBar?.close();
|
_gameClientInfoBar?.close();
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.gameClientStarted,
|
translations.gameClientStarted,
|
||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
@@ -422,39 +413,32 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onGameServerInjected() async {
|
Future<void> _onGameServerInjected() async {
|
||||||
if(_gameServerInfoBar != null) {
|
_gameServerInfoBar?.close();
|
||||||
_gameServerInfoBar?.close();
|
final theme = FluentTheme.of(appKey.currentContext!);
|
||||||
}else {
|
|
||||||
_gameClientInfoBar?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
|
|
||||||
try {
|
try {
|
||||||
_gameServerInfoBar = showRebootInfoBar(
|
_gameServerInfoBar = showInfoBar(
|
||||||
translations.waitingForGameServer,
|
translations.waitingForGameServer,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
final gameServerPort = _dllController.gameServerPort.text;
|
final gameServerPort = _settingsController.gameServerPort.text;
|
||||||
final pingOperation = pingGameServerOrTimeout(
|
final localPingResult = await pingGameServer(
|
||||||
"127.0.0.1:$gameServerPort",
|
"127.0.0.1:$gameServerPort",
|
||||||
const Duration(minutes: 2)
|
timeout: const Duration(minutes: 2)
|
||||||
);
|
);
|
||||||
this._pingOperation = pingOperation;
|
|
||||||
final localPingResult = await pingOperation.future;
|
|
||||||
_gameServerInfoBar?.close();
|
_gameServerInfoBar?.close();
|
||||||
if (!localPingResult) {
|
if (!localPingResult) {
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.gameServerStartWarning,
|
translations.gameServerStartWarning,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_backendController.joinLocalhost();
|
_backendController.joinLocalHost();
|
||||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||||
if (!accessible) {
|
if (!accessible) {
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.gameServerStartLocalWarning,
|
translations.gameServerStartLocalWarning,
|
||||||
severity: InfoBarSeverity.warning,
|
severity: InfoBarSeverity.warning,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
@@ -463,10 +447,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _hostingController.publishServer(
|
await _hostingController.publishServer(
|
||||||
_hostingController.accountUsername.text,
|
_gameController.username.text,
|
||||||
_hostingController.instance.value!.version.toString(),
|
_hostingController.instance.value!.versionName,
|
||||||
);
|
);
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.gameServerStarted,
|
translations.gameServerStarted,
|
||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
@@ -478,36 +462,35 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||||
try {
|
try {
|
||||||
_gameServerInfoBar = showRebootInfoBar(
|
_gameServerInfoBar = showInfoBar(
|
||||||
translations.checkingGameServer,
|
translations.checkingGameServer,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
final publicIp = await Ipify.ipv4();
|
final publicIp = await Ipify.ipv4();
|
||||||
final available = await pingGameServer("$publicIp:$gameServerPort");
|
final externalResult = await pingGameServer("$publicIp:$gameServerPort");
|
||||||
if(available) {
|
if (externalResult) {
|
||||||
_gameServerInfoBar?.close();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pingOperation = pingGameServerOrTimeout(
|
_gameServerInfoBar?.close();
|
||||||
|
final future = pingGameServer(
|
||||||
"$publicIp:$gameServerPort",
|
"$publicIp:$gameServerPort",
|
||||||
const Duration(days: 365)
|
timeout: const Duration(days: 365)
|
||||||
);
|
);
|
||||||
this._pingOperation = pingOperation;
|
_gameServerInfoBar = showInfoBar(
|
||||||
_gameServerInfoBar = showRebootInfoBar(
|
|
||||||
translations.checkGameServerFixMessage(gameServerPort),
|
translations.checkGameServerFixMessage(gameServerPort),
|
||||||
action: Button(
|
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),
|
child: Text(translations.checkGameServerFixAction),
|
||||||
),
|
),
|
||||||
severity: InfoBarSeverity.warning,
|
severity: InfoBarSeverity.warning,
|
||||||
duration: null,
|
duration: null,
|
||||||
loading: true
|
loading: true
|
||||||
);
|
);
|
||||||
final result = await pingOperation.future;
|
return await future;
|
||||||
_gameServerInfoBar?.close();
|
|
||||||
return result;
|
|
||||||
}finally {
|
}finally {
|
||||||
_gameServerInfoBar?.close();
|
_gameServerInfoBar?.close();
|
||||||
}
|
}
|
||||||
@@ -515,21 +498,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||||
if(host == null) {
|
if(host == null) {
|
||||||
try {
|
|
||||||
_pingOperation?.complete(false);
|
|
||||||
}catch(_) {
|
|
||||||
// Ignore: might be running, don't bother checking
|
|
||||||
} finally {
|
|
||||||
_pingOperation = null;
|
|
||||||
}
|
|
||||||
await _operation?.cancel();
|
await _operation?.cancel();
|
||||||
_operation = null;
|
_operation = null;
|
||||||
_backendController.cancelInteractive();
|
await _backendController.worker?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
host = host ?? widget.host;
|
host = host ?? widget.host;
|
||||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
|
|
||||||
if(host){
|
if(host){
|
||||||
_hostingController.instance.value = null;
|
_hostingController.instance.value = null;
|
||||||
}else {
|
}else {
|
||||||
@@ -551,17 +526,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
_hostingController.discardServer();
|
_hostingController.discardServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(reason == _StopReason.normal) {
|
if(instance != null) {
|
||||||
instance?.launched = true;
|
if(reason == _StopReason.normal) {
|
||||||
}
|
instance.launched = true;
|
||||||
|
}
|
||||||
|
|
||||||
instance?.kill();
|
instance.kill();
|
||||||
final child = instance?.child;
|
final child = instance.child;
|
||||||
if(child != null) {
|
if(child != null) {
|
||||||
await _onStop(
|
await _onStop(
|
||||||
reason: reason,
|
reason: reason,
|
||||||
host: child.serverType != null
|
host: child.hosting
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setStarted(host, false);
|
_setStarted(host, false);
|
||||||
@@ -579,14 +556,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
case _StopReason.normal:
|
case _StopReason.normal:
|
||||||
break;
|
break;
|
||||||
case _StopReason.missingVersionError:
|
case _StopReason.missingVersionError:
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.missingVersionError,
|
translations.missingVersionError,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.missingExecutableError:
|
case _StopReason.missingExecutableError:
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.missingExecutableError,
|
translations.missingExecutableError,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
@@ -594,7 +571,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
break;
|
break;
|
||||||
case _StopReason.exitCode:
|
case _StopReason.exitCode:
|
||||||
if(instance != null && !instance.launched) {
|
if(instance != null && !instance.launched) {
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.corruptedVersionError,
|
translations.corruptedVersionError,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
@@ -602,51 +579,28 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case _StopReason.corruptedVersionError:
|
case _StopReason.corruptedVersionError:
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.corruptedVersionError,
|
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),
|
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.missingCustomDllError:
|
case _StopReason.corruptedDllError:
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.missingCustomDllError(error!),
|
translations.corruptedDllError(error!),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.tokenError:
|
case _StopReason.tokenError:
|
||||||
_backendController.stop();
|
showInfoBar(
|
||||||
showRebootInfoBar(
|
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
|
||||||
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"),
|
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.unknownError:
|
case _StopReason.unknownError:
|
||||||
showRebootInfoBar(
|
showInfoBar(
|
||||||
translations.unknownFortniteError(error ?? translations.unknownError),
|
translations.unknownFortniteError(error ?? translations.unknownError),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
@@ -665,13 +619,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
try {
|
try {
|
||||||
final gameProcess = instance.gamePid;
|
final gameProcess = instance.gamePid;
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
|
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");
|
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
|
||||||
if(dllPath == null) {
|
if(dllPath == null) {
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
||||||
_onStop(
|
_onStop(
|
||||||
reason: _StopReason.missingCustomDllError,
|
reason: _StopReason.corruptedDllError,
|
||||||
error: injectable.name,
|
|
||||||
host: hosting
|
host: hosting
|
||||||
);
|
);
|
||||||
return;
|
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}...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||||
final (file, customDll) = _dllController.getInjectableData(version, injectable);
|
final path = injectable.path;
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
|
||||||
|
final file = File(path);
|
||||||
if(await file.exists()) {
|
if(await file.exists()) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||||
return file;
|
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...");
|
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...");
|
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||||
return _getDllFileOrStop(version, injectable, host, true);
|
return _getDllFileOrStop(injectable, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showInfoBar(
|
||||||
translations.launchingGameServer,
|
translations.launchingHeadlessServer,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
|
|
||||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
InfoBarEntry _showLaunchingGameClientWidget() => _gameClientInfoBar = showInfoBar(
|
||||||
return _gameClientInfoBar = showRebootInfoBar(
|
translations.launchingGameClient,
|
||||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
loading: true,
|
||||||
loading: true,
|
duration: null
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _StopReason {
|
enum _StopReason {
|
||||||
@@ -759,14 +679,12 @@ enum _StopReason {
|
|||||||
missingVersionError,
|
missingVersionError,
|
||||||
missingExecutableError,
|
missingExecutableError,
|
||||||
corruptedVersionError,
|
corruptedVersionError,
|
||||||
missingCustomDllError,
|
|
||||||
corruptedDllError,
|
corruptedDllError,
|
||||||
backendError,
|
backendError,
|
||||||
matchmakerError,
|
matchmakerError,
|
||||||
tokenError,
|
tokenError,
|
||||||
unknownError,
|
unknownError,
|
||||||
exitCode,
|
exitCode;
|
||||||
crash;
|
|
||||||
|
|
||||||
bool get isError => name.contains("Error");
|
bool get isError => name.contains("Error");
|
||||||
}
|
}
|
||||||
@@ -24,22 +24,19 @@ class InfoBarAreaState extends State<InfoBarArea> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => StreamBuilder(
|
Widget build(BuildContext context) => Obx(() => Padding(
|
||||||
stream: pagesController.stream,
|
padding: EdgeInsets.only(
|
||||||
builder: (context, _) => Obx(() => Padding(
|
bottom: hasPageButton ? 72.0 : 16.0
|
||||||
padding: EdgeInsets.only(
|
),
|
||||||
bottom: hasPageButton ? 72.0 : 16.0
|
child: Column(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
children: _children.value.map((child) => Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: EdgeInsets.only(
|
||||||
children: _children.value.map((child) => Padding(
|
top: 12.0
|
||||||
padding: EdgeInsets.only(
|
),
|
||||||
top: 12.0
|
child: child
|
||||||
),
|
)).toList(growable: false)
|
||||||
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