10.0.8
@@ -2,13 +2,13 @@
|
||||
[OnlineSubsystemMcp.Xmpp]
|
||||
bUseSSL=false
|
||||
ServerAddr="ws://127.0.0.1"
|
||||
ServerPort=80
|
||||
ServerPort=8080
|
||||
|
||||
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
|
||||
[OnlineSubsystemMcp.Xmpp Prod]
|
||||
bUseSSL=false
|
||||
ServerAddr="ws://127.0.0.1"
|
||||
ServerPort=80
|
||||
ServerPort=8080
|
||||
|
||||
# Forces fortnite to use the v1 party system to support lawinserver xmpp
|
||||
[OnlineSubsystemMcp]
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 398 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 0 B |
@@ -63,10 +63,24 @@
|
||||
"favorite": false
|
||||
},
|
||||
{
|
||||
"accountId": "Player231",
|
||||
"accountId": "Player809",
|
||||
"status": "ACCEPTED",
|
||||
"direction": "OUTBOUND",
|
||||
"created": "2024-05-23T20:05:07.478Z",
|
||||
"created": "2024-05-31T19:09:47.089Z",
|
||||
"favorite": false
|
||||
},
|
||||
{
|
||||
"accountId": "Player153",
|
||||
"status": "ACCEPTED",
|
||||
"direction": "OUTBOUND",
|
||||
"created": "2024-05-31T19:50:04.738Z",
|
||||
"favorite": false
|
||||
},
|
||||
{
|
||||
"accountId": "Player724",
|
||||
"status": "ACCEPTED",
|
||||
"direction": "OUTBOUND",
|
||||
"created": "2024-06-24T20:15:48.062Z",
|
||||
"favorite": false
|
||||
}
|
||||
]
|
||||
@@ -82,13 +82,31 @@
|
||||
"created": "2024-05-23T19:36:22.635Z"
|
||||
},
|
||||
{
|
||||
"accountId": "Player231",
|
||||
"accountId": "Player809",
|
||||
"groups": [],
|
||||
"mutual": 0,
|
||||
"alias": "",
|
||||
"note": "",
|
||||
"favorite": false,
|
||||
"created": "2024-05-23T20:05:07.478Z"
|
||||
"created": "2024-05-31T19:09:47.089Z"
|
||||
},
|
||||
{
|
||||
"accountId": "Player153",
|
||||
"groups": [],
|
||||
"mutual": 0,
|
||||
"alias": "",
|
||||
"note": "",
|
||||
"favorite": false,
|
||||
"created": "2024-05-31T19:50:04.738Z"
|
||||
},
|
||||
{
|
||||
"accountId": "Player724",
|
||||
"groups": [],
|
||||
"mutual": 0,
|
||||
"alias": "",
|
||||
"note": "",
|
||||
"favorite": false,
|
||||
"created": "2024-06-24T20:15:48.062Z"
|
||||
}
|
||||
],
|
||||
"incoming": [],
|
||||
|
||||
@@ -128,10 +128,11 @@
|
||||
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
||||
"addLocalBuildName": "Add a version from this PC's local storage",
|
||||
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
|
||||
"addVersion": "New version",
|
||||
"addVersion": "Import",
|
||||
"downloadBuildName": "Download any version from the cloud",
|
||||
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
||||
"downloadBuildContent": "Download build",
|
||||
"downloadVersion": "Download",
|
||||
"cannotUpdateGameServer": "An error occurred while updating the game server: {error}",
|
||||
"launchFortnite": "Launch Fortnite",
|
||||
"closeFortnite": "Close Fortnite",
|
||||
@@ -146,9 +147,9 @@
|
||||
"defaultServerName": "Reboot Game Server",
|
||||
"defaultServerDescription": "Just another server",
|
||||
"downloadingDll": "Downloading {name} dll...",
|
||||
"dllAlreadyExists": "The {name} was already downloaded",
|
||||
"downloadDllSuccess": "The {name} dll was downloaded successfully",
|
||||
"downloadDllError": "An error occurred while downloading {name}: {error}",
|
||||
"downloadDllAntivirus": "The {name} dll was deleted: your antivirus({antivirus}) might have flagged it",
|
||||
"downloadDllRetry": "Retry",
|
||||
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
|
||||
"launchingGameServer": "Launching the game server...",
|
||||
@@ -212,6 +213,7 @@
|
||||
"selectBuild": "Select a fortnite version",
|
||||
"fetchingBuilds": "Fetching builds and disks...",
|
||||
"unknownError": "Unknown error",
|
||||
"unknown": "unknown",
|
||||
"downloadVersionError": "Cannot download version: {error}",
|
||||
"downloadedVersion": "The download was completed successfully!",
|
||||
"download": "Download",
|
||||
@@ -258,7 +260,8 @@
|
||||
"emptyURL": "Empty update URL",
|
||||
"missingVersionError": "Download or select a version before starting Fortnite",
|
||||
"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",
|
||||
"multipleExecutablesError": "There must be only one executable named {name} in the game directory",
|
||||
"corruptedVersionError": "Fortnite crashed while starting: either the game installation is corrupted or an injected dll({dlls}) tried to access memory illegally",
|
||||
"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})",
|
||||
@@ -282,9 +285,9 @@
|
||||
"infoVideoName": "Tutorial",
|
||||
"infoVideoDescription": "Show the tutorial again in the launcher",
|
||||
"infoVideoContent": "Start Tutorial",
|
||||
"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 and couldn't be reinstalled",
|
||||
"dllDeletedSecondaryAction": "Close",
|
||||
"dllDeletedPrimaryAction": "Try again",
|
||||
"dllDeletedPrimaryAction": "Disable Antivirus",
|
||||
"clickKey": "Waiting for a key to be registered",
|
||||
"settingsLogsName": "Export logs",
|
||||
"settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher",
|
||||
@@ -306,11 +309,8 @@
|
||||
"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",
|
||||
"gameServerTypeName": "Headless",
|
||||
"gameServerTypeDescription": "Disables game rendering to save resources",
|
||||
"localBuild": "This PC",
|
||||
"githubArchive": "Cloud archive",
|
||||
"all": "All",
|
||||
@@ -374,5 +374,10 @@
|
||||
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
|
||||
"gameResetDefaultsContent": "Reset",
|
||||
"selectFile": "Select a file",
|
||||
"reset": "Reset"
|
||||
"reset": "Reset",
|
||||
"importingVersion": "Looking for Fortnite game files...",
|
||||
"importedVersion": "Successfully imported version",
|
||||
"importVersionMissingShippingExeError": "Cannot import version: {name} should exist in the directory",
|
||||
"importVersionMultipleShippingExesError": "Cannot import version: only one {name} should exist in the directory",
|
||||
"importVersionUnsupportedVersionError": "This version of Fortnite is not supported by the launcher"
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'hosting_controller.dart';
|
||||
|
||||
class BackendController extends GetxController {
|
||||
static const String storageName = "v2_backend_storage";
|
||||
static const String storageName = "v3_backend_storage";
|
||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||
|
||||
late final GetStorage? _storage;
|
||||
@@ -162,6 +164,14 @@ class BackendController extends GetxController {
|
||||
detached: detached.value,
|
||||
onError: (errorMessage) {
|
||||
stop(interactive: false);
|
||||
Get.find<GameController>()
|
||||
.instance
|
||||
.value
|
||||
?.kill();
|
||||
Get.find<HostingController>()
|
||||
.instance
|
||||
.value
|
||||
?.kill();
|
||||
_showRebootInfoBar(
|
||||
translations.backendErrorMessage,
|
||||
severity: InfoBarSeverity.error,
|
||||
@@ -508,8 +518,7 @@ class BackendController extends GetxController {
|
||||
}else {
|
||||
FlutterClipboard.controlC(decryptedIp);
|
||||
}
|
||||
Get.find<GameController>()
|
||||
.selectedVersion = version;
|
||||
Get.find<GameController>().selectedVersion.value = version;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
|
||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||
duration: infoBarLongDuration,
|
||||
|
||||
@@ -9,11 +9,14 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
|
||||
class DllController extends GetxController {
|
||||
static const String storageName = "v2_dll_storage";
|
||||
static const String storageName = "v3_dll_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final TextEditingController customGameServerDll;
|
||||
@@ -27,6 +30,7 @@ class DllController extends GetxController {
|
||||
late final RxBool customGameServer;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Map<InjectableDll, StreamSubscription?> _subscriptions;
|
||||
|
||||
DllController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
@@ -39,15 +43,16 @@ class DllController extends GetxController {
|
||||
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));
|
||||
beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
|
||||
beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
|
||||
aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
|
||||
aboveS20Mirror.addListener(() => _storage?.write("after_s20_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));
|
||||
_subscriptions = {};
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, InjectableDll dll) {
|
||||
@@ -78,6 +83,7 @@ class DllController extends GetxController {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
_listenToFileEvents(InjectableDll.gameServer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -88,6 +94,7 @@ class DllController extends GetxController {
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
_listenToFileEvents(InjectableDll.gameServer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -116,6 +123,7 @@ class DllController extends GetxController {
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
_listenToFileEvents(InjectableDll.gameServer);
|
||||
return true;
|
||||
}catch(message) {
|
||||
infoBarEntry?.close();
|
||||
@@ -123,26 +131,29 @@ class DllController extends GetxController {
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
final completer = Completer<bool>();
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadDllError(error.toString(), "reboot.dll"),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(false),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
infoBarEntry?.close();
|
||||
updateGameServerDll(
|
||||
final result = updateGameServerDll(
|
||||
force: true,
|
||||
silent: silent
|
||||
);
|
||||
completer.complete(result);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
(File, bool) getInjectableData(Version version, InjectableDll dll) {
|
||||
(File, bool) getInjectableData(String version, InjectableDll dll) {
|
||||
final defaultPath = canonicalize(getDefaultDllPath(dll));
|
||||
switch(dll){
|
||||
case InjectableDll.gameServer:
|
||||
@@ -150,7 +161,7 @@ class DllController extends GetxController {
|
||||
return (File(customGameServerDll.text), true);
|
||||
}
|
||||
|
||||
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
||||
return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
||||
case InjectableDll.console:
|
||||
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
||||
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
||||
@@ -163,6 +174,14 @@ class DllController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isS20(String version) {
|
||||
try {
|
||||
return Version.parse(version).major >= 20;
|
||||
} on FormatException catch(_) {
|
||||
return version.trim().startsWith("20.");
|
||||
}
|
||||
}
|
||||
|
||||
TextEditingController getDllEditingController(InjectableDll dll) {
|
||||
switch(dll) {
|
||||
case InjectableDll.console:
|
||||
@@ -177,16 +196,16 @@ class DllController extends GetxController {
|
||||
}
|
||||
|
||||
String getDefaultDllPath(InjectableDll dll) {
|
||||
switch(dll) {
|
||||
case InjectableDll.console:
|
||||
return "${dllsDirectory.path}\\console.dll";
|
||||
case InjectableDll.auth:
|
||||
return "${dllsDirectory.path}\\cobalt.dll";
|
||||
case InjectableDll.gameServer:
|
||||
return "${dllsDirectory.path}\\reboot.dll";
|
||||
case InjectableDll.memoryLeak:
|
||||
return "${dllsDirectory.path}\\memory.dll";
|
||||
}
|
||||
switch(dll) {
|
||||
case InjectableDll.console:
|
||||
return "${dllsDirectory.path}\\console.dll";
|
||||
case InjectableDll.auth:
|
||||
return "${dllsDirectory.path}\\cobalt.dll";
|
||||
case InjectableDll.gameServer:
|
||||
return "${dllsDirectory.path}\\reboot.dll";
|
||||
case InjectableDll.memoryLeak:
|
||||
return "${dllsDirectory.path}\\memory.dll";
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
|
||||
@@ -198,77 +217,136 @@ class DllController extends GetxController {
|
||||
}
|
||||
|
||||
if(!force && File(filePath).existsSync()) {
|
||||
log("[DLL] File already exists");
|
||||
log("[DLL] $dll already exists");
|
||||
_listenToFileEvents(dll);
|
||||
return true;
|
||||
}
|
||||
|
||||
log("[DLL] Downloading $dll...");
|
||||
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
log("[DLL] Showing dialog while downloading $dll...");
|
||||
entry = showRebootInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}else {
|
||||
log("[DLL] Not showing dialog while downloading $dll...");
|
||||
}
|
||||
await downloadDependency(dll, filePath);
|
||||
final result = await downloadDependency(dll, filePath);
|
||||
if(!result) {
|
||||
entry?.close();
|
||||
showRebootInfoBar(
|
||||
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
log("[DLL] Downloaded $dll");
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
log("[DLL] Showing success dialog for $dll");
|
||||
entry = await showRebootInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}else {
|
||||
log("[DLL] Not showing success dialog for $dll");
|
||||
}
|
||||
_listenToFileEvents(dll);
|
||||
return true;
|
||||
}catch(message) {
|
||||
log("[DLL] Error: $message");
|
||||
log("[DLL] An error occurred while downloading $dll: $message");
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error =
|
||||
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
final completer = Completer<bool>();
|
||||
await showRebootInfoBar(
|
||||
translations.downloadDllError(error.toString(), dll.name),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
onDismissed: () => completer.complete(false),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await download(dll, filePath, silent: silent, force: force);
|
||||
completer.complete(null);
|
||||
final result = await download(dll, filePath, silent: silent, force: force);
|
||||
completer.complete(result);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
return false;
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
void guardFiles() {
|
||||
Future<void> downloadAndGuardDependencies() async {
|
||||
for(final injectable in InjectableDll.values) {
|
||||
final controller = getDllEditingController(injectable);
|
||||
final defaultPath = getDefaultDllPath(injectable);
|
||||
if (path.equals(controller.text, defaultPath)) {
|
||||
download(injectable, controller.text);
|
||||
}
|
||||
controller.addListener(() async {
|
||||
try {
|
||||
if (!path.equals(controller.text, defaultPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final filePath = controller.text;
|
||||
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
|
||||
if (path.equals(event.path, filePath)) {
|
||||
await download(injectable, filePath);
|
||||
}
|
||||
}
|
||||
} catch(_) {
|
||||
// Ignore
|
||||
}
|
||||
});
|
||||
if(path.equals(controller.text, defaultPath)) {
|
||||
await download(injectable, controller.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToFileEvents(InjectableDll injectable) {
|
||||
final controller = getDllEditingController(injectable);
|
||||
final defaultPath = getDefaultDllPath(injectable);
|
||||
|
||||
void onFileEvent(FileSystemEvent event, String filePath) {
|
||||
if (!path.equals(event.path, filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(path.equals(filePath, defaultPath)) {
|
||||
Get.find<GameController>()
|
||||
.instance
|
||||
.value
|
||||
?.kill();
|
||||
Get.find<HostingController>()
|
||||
.instance
|
||||
.value
|
||||
?.kill();
|
||||
showRebootInfoBar(
|
||||
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
|
||||
_updateInput(injectable);
|
||||
}
|
||||
|
||||
StreamSubscription subscribe(String filePath) => File(filePath)
|
||||
.parent
|
||||
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
|
||||
.listen((event) => onFileEvent(event, filePath));
|
||||
|
||||
controller.addListener(() {
|
||||
_subscriptions[injectable]?.cancel();
|
||||
_subscriptions[injectable] = subscribe(controller.text);
|
||||
});
|
||||
_subscriptions[injectable] = subscribe(controller.text);
|
||||
}
|
||||
|
||||
void _updateInput(InjectableDll injectable) {
|
||||
switch(injectable) {
|
||||
case InjectableDll.console:
|
||||
settingsConsoleDllInputKey.currentState?.validate();
|
||||
break;
|
||||
case InjectableDll.auth:
|
||||
settingsAuthDllInputKey.currentState?.validate();
|
||||
break;
|
||||
case InjectableDll.gameServer:
|
||||
settingsGameServerDllInputKey.currentState?.validate();
|
||||
break;
|
||||
case InjectableDll.memoryLeak:
|
||||
settingsMemoryDllInputKey.currentState?.validate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -8,14 +9,14 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
|
||||
class GameController extends GetxController {
|
||||
static const String storageName = "v2_game_storage";
|
||||
static const String storageName = "v3_game_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final TextEditingController username;
|
||||
late final TextEditingController password;
|
||||
late final TextEditingController customLaunchArgs;
|
||||
late final Rx<List<FortniteVersion>> versions;
|
||||
late final Rxn<FortniteVersion> _selectedVersion;
|
||||
late final Rxn<FortniteVersion> selectedVersion;
|
||||
late final RxBool started;
|
||||
late final Rxn<GameInstance> instance;
|
||||
|
||||
@@ -28,8 +29,8 @@ class GameController extends GetxController {
|
||||
versions = Rx(decodedVersions);
|
||||
versions.listen((data) => _saveVersions());
|
||||
final decodedSelectedVersionName = _storage?.read("version");
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
selectedVersion = Rxn(decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName));
|
||||
selectedVersion.listen((version) => _storage?.write("version", version?.name));
|
||||
username = TextEditingController(
|
||||
text: _storage?.read("username") ?? kDefaultPlayerName);
|
||||
username.addListener(() => _storage?.write("username", username.text));
|
||||
@@ -46,26 +47,27 @@ class GameController extends GetxController {
|
||||
password.text = "";
|
||||
customLaunchArgs.text = "";
|
||||
versions.value = [];
|
||||
_selectedVersion.value = null;
|
||||
selectedVersion.value = null;
|
||||
instance.value = null;
|
||||
}
|
||||
|
||||
FortniteVersion? getVersionByName(String name) {
|
||||
return versions.value.firstWhereOrNull((element) => element.content.toString() == name);
|
||||
name = name.trim();
|
||||
return versions.value.firstWhereOrNull((element) => element.name == name);
|
||||
}
|
||||
|
||||
void addVersion(FortniteVersion version) {
|
||||
var empty = versions.value.isEmpty;
|
||||
versions.update((val) => val?.add(version));
|
||||
if(empty){
|
||||
selectedVersion = version;
|
||||
}
|
||||
selectedVersion.value = version;
|
||||
}
|
||||
|
||||
void removeVersion(FortniteVersion version) {
|
||||
versions.update((val) => val?.remove(version));
|
||||
if (selectedVersion == version || hasNoVersions) {
|
||||
selectedVersion = null;
|
||||
final index = versions.value.indexOf(version);
|
||||
versions.update((val) => val?.removeAt(index));
|
||||
if(hasNoVersions) {
|
||||
selectedVersion.value = null;
|
||||
}else {
|
||||
selectedVersion.value = versions.value.elementAt(max(0, index - 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,14 +80,5 @@ class GameController extends GetxController {
|
||||
|
||||
bool get hasNoVersions => versions.value.isEmpty;
|
||||
|
||||
FortniteVersion? get selectedVersion => _selectedVersion();
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
_selectedVersion.value = version;
|
||||
_storage?.write("version", version?.content.toString());
|
||||
}
|
||||
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
versions.update((val) => function(version));
|
||||
}
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) => versions.update((val) => function(version));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:sync/semaphore.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class HostingController extends GetxController {
|
||||
static const String storageName = "v2_hosting_storage";
|
||||
static const String storageName = "v3_hosting_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final String uuid;
|
||||
@@ -26,7 +26,7 @@ class HostingController extends GetxController {
|
||||
late final FocusNode passwordFocusNode;
|
||||
late final RxBool showPassword;
|
||||
late final RxBool discoverable;
|
||||
late final Rx<GameServerType> type;
|
||||
late final RxBool headless;
|
||||
late final RxBool autoRestart;
|
||||
late final RxBool started;
|
||||
late final RxBool published;
|
||||
@@ -54,8 +54,8 @@ class HostingController extends GetxController {
|
||||
passwordFocusNode = FocusNode();
|
||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
||||
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
|
||||
type.listen((value) => _storage?.write("type", value.index));
|
||||
headless = RxBool(_storage?.read("headless") ?? true);
|
||||
headless.listen((value) => _storage?.write("headless", value));
|
||||
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
|
||||
autoRestart.listen((value) => _storage?.write("auto_restart", value));
|
||||
started = RxBool(false);
|
||||
@@ -165,7 +165,7 @@ class HostingController extends GetxController {
|
||||
showPassword.value = false;
|
||||
discoverable.value = false;
|
||||
instance.value = null;
|
||||
type.value = GameServerType.headless;
|
||||
headless.value = true;
|
||||
autoRestart.value = true;
|
||||
customLaunchArgs.text = "";
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
static const String storageName = "v2_settings_storage";
|
||||
static const String storageName = "v3_settings_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final RxString language;
|
||||
|
||||
@@ -126,7 +126,7 @@ class ProgressDialog extends AbstractDialog {
|
||||
header: InfoLabel(
|
||||
label: text,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()
|
||||
),
|
||||
|
||||
@@ -78,417 +78,6 @@ class _ServiceProvider10 extends IUnknown {
|
||||
}
|
||||
}
|
||||
|
||||
class IVirtualDesktop extends IUnknown {
|
||||
static const String _CLSID = "{3F07F4BE-B107-441A-AF0F-39D82529072C}";
|
||||
|
||||
IVirtualDesktop._internal(super.ptr);
|
||||
|
||||
String getName() {
|
||||
final result = calloc<HSTRING>();
|
||||
final code = (ptr.ref.vtable + 5)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
|
||||
.value
|
||||
.asFunction<
|
||||
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return _convertFromHString(result.value);
|
||||
}
|
||||
}
|
||||
|
||||
class IApplicationView extends IUnknown {
|
||||
// static const String _CLSID = "{372E1D3B-38D3-42E4-A15B-8AB2B178F513}";
|
||||
|
||||
IApplicationView._internal(super.ptr);
|
||||
}
|
||||
|
||||
class _IObjectArray extends IUnknown {
|
||||
_IObjectArray(super.ptr);
|
||||
|
||||
int getCount() {
|
||||
final result = calloc<Int32>();
|
||||
final code = (ptr.ref.vtable + 3)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
|
||||
.value
|
||||
.asFunction<
|
||||
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
Pointer<COMObject> getAt(int index, String guid) {
|
||||
final result = calloc<COMObject>();
|
||||
final code = (ptr.ref.vtable + 4)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(Pointer, Int32 index, Pointer<GUID>,
|
||||
Pointer<COMObject>)>>>()
|
||||
.value
|
||||
.asFunction<
|
||||
int Function(
|
||||
Pointer, int index, Pointer<GUID>, Pointer<COMObject>)>()(
|
||||
ptr.ref.lpVtbl, index, GUIDFromString(guid), result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
typedef _IObjectMapper<T> = T Function(Pointer<COMObject>);
|
||||
|
||||
class _IObjectArrayList<T> extends ListBase<T> {
|
||||
final _IObjectArray _array;
|
||||
final String _guid;
|
||||
final _IObjectMapper<T> _mapper;
|
||||
|
||||
_IObjectArrayList(
|
||||
{required _IObjectArray array,
|
||||
required String guid,
|
||||
required _IObjectMapper<T> mapper})
|
||||
: _array = array,
|
||||
_guid = guid,
|
||||
_mapper = mapper;
|
||||
|
||||
@override
|
||||
int get length => _array.getCount();
|
||||
|
||||
@override
|
||||
set length(int newLength) {
|
||||
throw UnsupportedError("Immutable list");
|
||||
}
|
||||
|
||||
@override
|
||||
T operator [](int index) => _mapper(_array.getAt(index, _guid));
|
||||
|
||||
@override
|
||||
void operator []=(int index, T value) {
|
||||
throw UnsupportedError("Immutable list");
|
||||
}
|
||||
}
|
||||
|
||||
class _IVirtualDesktopManagerInternal extends IUnknown {
|
||||
static const String _CLSID = "{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}";
|
||||
static const String _IID_WIN10 = "{F31574D6-B682-4CDC-BD56-1827860ABEC6}";
|
||||
static const String _IID_WIN_21H2 = "{B2F925B9-5A0F-4D2E-9F4D-2B1507593C10}";
|
||||
static const String _IID_WIN_23H2 = "{A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E}";
|
||||
static const String _IID_WIN_23H2_3085 = "{53F5CA0B-158F-4124-900C-057158060B27}";
|
||||
|
||||
_IVirtualDesktopManagerInternal._internal(super.ptr);
|
||||
|
||||
int getDesktopsCount() {
|
||||
final result = calloc<Int32>();
|
||||
final code = (ptr.ref.vtable + 3)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
|
||||
.value
|
||||
.asFunction<
|
||||
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
List<IVirtualDesktop> getDesktops() {
|
||||
final result = calloc<COMObject>();
|
||||
final code = (ptr.ref.vtable + 7)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
|
||||
ptr.ref.lpVtbl, result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
final array = _IObjectArray(result);
|
||||
return _IObjectArrayList(
|
||||
array: array,
|
||||
guid: IVirtualDesktop._CLSID,
|
||||
mapper: (comObject) => IVirtualDesktop._internal(comObject));
|
||||
}
|
||||
|
||||
void moveWindowToDesktop(IApplicationView view, IVirtualDesktop desktop) {
|
||||
final code = (ptr.ref.vtable + 4)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
Int32 Function(Pointer, COMObject, COMObject)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
|
||||
ptr.ref.lpVtbl, view.ptr.ref, desktop.ptr.ref);
|
||||
if (code != 0) {
|
||||
throw WindowsException(code, message: "Cannot move window");
|
||||
}
|
||||
}
|
||||
|
||||
IVirtualDesktop createDesktop() {
|
||||
final result = calloc<COMObject>();
|
||||
final code = (ptr.ref.vtable + 10)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
|
||||
ptr.ref.lpVtbl, result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return IVirtualDesktop._internal(result);
|
||||
}
|
||||
|
||||
void removeDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback) {
|
||||
final code = (ptr.ref.vtable + 12)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(Pointer, COMObject, COMObject)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
|
||||
ptr.ref.lpVtbl, desktop.ptr.ref, fallback.ptr.ref);
|
||||
if (code != 0) {
|
||||
throw WindowsException(code);
|
||||
}
|
||||
}
|
||||
|
||||
void setDesktopName(IVirtualDesktop desktop, String newName) {
|
||||
final code =
|
||||
(ptr.ref.vtable + 15)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, COMObject, int)>()(
|
||||
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
|
||||
if (code != 0) {
|
||||
throw WindowsException(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IApplicationViewCollection extends IUnknown {
|
||||
static const String _CLSID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
|
||||
static const String _IID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
|
||||
|
||||
_IApplicationViewCollection._internal(super.ptr);
|
||||
|
||||
IApplicationView? getViewForHWnd(int HWnd) {
|
||||
final result = calloc<COMObject>();
|
||||
final code =
|
||||
(ptr.ref.vtable + 6)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(
|
||||
Pointer, IntPtr, Pointer<COMObject>)>>>()
|
||||
.value
|
||||
.asFunction<int Function(Pointer, int, Pointer<COMObject>)>()(
|
||||
ptr.ref.lpVtbl, HWnd, result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
return null;
|
||||
}
|
||||
|
||||
return IApplicationView._internal(result);
|
||||
}
|
||||
}
|
||||
|
||||
final class Win32Process extends Struct {
|
||||
@Uint32()
|
||||
external int pid;
|
||||
|
||||
@Uint32()
|
||||
external int HWndLength;
|
||||
|
||||
external Pointer<Uint32> HWnd;
|
||||
|
||||
external Pointer<Utf16> excluded;
|
||||
}
|
||||
|
||||
int _filter(int HWnd, int lParam) {
|
||||
final structure = Pointer.fromAddress(lParam).cast<Win32Process>();
|
||||
if(structure.ref.excluded != nullptr) {
|
||||
final excludedWindowName = structure.ref.excluded.toDartString();
|
||||
final windowNameLength = GetWindowTextLength(HWnd);
|
||||
if(windowNameLength > 0) {
|
||||
final windowNamePointer = calloc<Uint16>(windowNameLength + 1).cast<Utf16>();
|
||||
GetWindowText(HWnd, windowNamePointer, windowNameLength);
|
||||
final windowName = windowNamePointer.toDartString(length: windowNameLength);
|
||||
if(windowName.toLowerCase().contains(excludedWindowName.toLowerCase())) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final pidPointer = calloc<Uint32>();
|
||||
GetWindowThreadProcessId(HWnd, pidPointer);
|
||||
final pid = pidPointer.value;
|
||||
if (pid == structure.ref.pid) {
|
||||
final length = structure.ref.HWndLength;
|
||||
final newLength = length + 1;
|
||||
final ptr = malloc.allocate<Uint32>(sizeOf<Uint32>() * newLength);
|
||||
final list = structure.ref.HWnd.asTypedList(length);
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
(ptr + i).value = list[i];
|
||||
}
|
||||
ptr[list.length] = HWnd;
|
||||
structure.ref.HWndLength = newLength;
|
||||
free(structure.ref.HWnd);
|
||||
structure.ref.HWnd = ptr;
|
||||
}
|
||||
|
||||
free(pidPointer);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
List<int> _getHWnds(int pid, String? excludedWindowName) {
|
||||
final result = calloc<Win32Process>();
|
||||
result.ref.pid = pid;
|
||||
if(excludedWindowName != null) {
|
||||
result.ref.excluded = excludedWindowName.toNativeUtf16();
|
||||
}
|
||||
|
||||
EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
|
||||
final length = result.ref.HWndLength;
|
||||
final HWndsPointer = result.ref.HWnd;
|
||||
if(HWndsPointer == nullptr) {
|
||||
calloc.free(result);
|
||||
return [];
|
||||
}
|
||||
|
||||
final HWnds = HWndsPointer.asTypedList(length)
|
||||
.toList(growable: false);
|
||||
calloc.free(result);
|
||||
return HWnds;
|
||||
}
|
||||
|
||||
class VirtualDesktopManager {
|
||||
static VirtualDesktopManager? _instance;
|
||||
|
||||
final _IVirtualDesktopManagerInternal windowManager;
|
||||
final _IApplicationViewCollection applicationViewCollection;
|
||||
|
||||
VirtualDesktopManager._internal(this.windowManager, this.applicationViewCollection);
|
||||
|
||||
factory VirtualDesktopManager.getInstance() {
|
||||
if (_instance != null) {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
final hr = CoInitializeEx(
|
||||
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
|
||||
if (FAILED(hr)) {
|
||||
throw WindowsException(hr);
|
||||
}
|
||||
|
||||
final shell = _ServiceProvider10.createInstance();
|
||||
final windowManager = _createWindowManager(shell);
|
||||
final applicationViewCollection = _IApplicationViewCollection._internal(
|
||||
shell.queryService(_IApplicationViewCollection._CLSID,
|
||||
_IApplicationViewCollection._IID));
|
||||
return _instance =
|
||||
VirtualDesktopManager._internal(windowManager, applicationViewCollection);
|
||||
}
|
||||
|
||||
static _IVirtualDesktopManagerInternal _createWindowManager(_ServiceProvider10 shell) {
|
||||
final build = windowsBuild;
|
||||
if(build == null || build < 19044) {
|
||||
return _IVirtualDesktopManagerInternal._internal(
|
||||
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
|
||||
_IVirtualDesktopManagerInternal._IID_WIN10));
|
||||
}else if(build >= 19044 && build < 22631) {
|
||||
return _IVirtualDesktopManagerInternal._internal(
|
||||
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
|
||||
_IVirtualDesktopManagerInternal._IID_WIN_21H2));
|
||||
}else if(build >= 22631 && build < 22631) {
|
||||
return _IVirtualDesktopManagerInternal._internal(
|
||||
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
|
||||
_IVirtualDesktopManagerInternal._IID_WIN_23H2));
|
||||
}else {
|
||||
return _IVirtualDesktopManagerInternal._internal(
|
||||
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
|
||||
_IVirtualDesktopManagerInternal._IID_WIN_23H2_3085));
|
||||
}
|
||||
}
|
||||
|
||||
int getDesktopsCount() => windowManager.getDesktopsCount();
|
||||
|
||||
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
|
||||
|
||||
Future<bool> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1), int remainingPolls = 10, String? excludedWindowName}) async {
|
||||
for(final hWND in _getHWnds(pid, excludedWindowName)) {
|
||||
final window = applicationViewCollection.getViewForHWnd(hWND);
|
||||
if(window != null) {
|
||||
windowManager.moveWindowToDesktop(window, desktop);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(remainingPolls <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Future.delayed(pollTime);
|
||||
return await moveWindowToDesktop(
|
||||
pid,
|
||||
desktop,
|
||||
pollTime: pollTime,
|
||||
remainingPolls: remainingPolls - 1
|
||||
);
|
||||
}
|
||||
|
||||
IVirtualDesktop createDesktop() => windowManager.createDesktop();
|
||||
|
||||
void removeDesktop(IVirtualDesktop desktop, [IVirtualDesktop? fallback]) {
|
||||
fallback ??= getDesktops().first;
|
||||
return windowManager.removeDesktop(desktop, fallback);
|
||||
}
|
||||
|
||||
void setDesktopName(IVirtualDesktop desktop, String newName) =>
|
||||
windowManager.setDesktopName(desktop, newName);
|
||||
}
|
||||
|
||||
String _convertFromHString(int hstring) =>
|
||||
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
|
||||
|
||||
int _convertToHString(String string) {
|
||||
final hString = calloc<HSTRING>();
|
||||
final stringPtr = string.toNativeUtf16();
|
||||
try {
|
||||
final hr = WindowsCreateString(stringPtr, string.length, hString);
|
||||
if (FAILED(hr)) throw WindowsException(hr);
|
||||
return hString.value;
|
||||
} finally {
|
||||
free(stringPtr);
|
||||
free(hString);
|
||||
}
|
||||
}
|
||||
|
||||
extension WindowManagerExtension on WindowManager {
|
||||
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
|
||||
}
|
||||
|
||||
@@ -19,17 +19,4 @@ void loadTranslations(BuildContext context) {
|
||||
_init = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
String get currentLocale => Intl.getCurrentLocale().split("_")[0];
|
||||
@@ -1,28 +1,35 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
typedef FileSelectorValidator = String? Function(String?);
|
||||
|
||||
class FileSelector extends StatefulWidget {
|
||||
final String placeholder;
|
||||
final String windowTitle;
|
||||
final bool allowNavigator;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?) validator;
|
||||
final FileSelectorValidator? validator;
|
||||
final AutovalidateMode? validatorMode;
|
||||
final Key? validatorKey;
|
||||
final String? extension;
|
||||
final String? label;
|
||||
final bool folder;
|
||||
final void Function(String)? onSelected;
|
||||
|
||||
const FileSelector(
|
||||
{required this.placeholder,
|
||||
required this.windowTitle,
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.folder,
|
||||
required this.allowNavigator,
|
||||
this.validator,
|
||||
this.validatorKey,
|
||||
this.label,
|
||||
this.extension,
|
||||
this.validatorMode,
|
||||
this.onSelected,
|
||||
Key? key})
|
||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||
super(key: key);
|
||||
@@ -47,6 +54,7 @@ class _FileSelectorState extends State<FileSelector> {
|
||||
placeholder: widget.placeholder,
|
||||
validator: widget.validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction,
|
||||
key: widget.validatorKey,
|
||||
suffix: !widget.allowNavigator ? null : Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
@@ -72,6 +80,10 @@ class _FileSelectorState extends State<FileSelector> {
|
||||
}
|
||||
|
||||
void _updateText(String? value) {
|
||||
if(value != null) {
|
||||
widget.onSelected?.call(value);
|
||||
}
|
||||
|
||||
var text = value ?? widget.controller.text;
|
||||
widget.controller.text = value ?? widget.controller.text;
|
||||
widget.controller.selection = TextSelection.collapsed(offset: text.length);
|
||||
|
||||
@@ -13,10 +13,14 @@ import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||
const double _kButtonDimensions = 30;
|
||||
const double _kButtonSpacing = 8;
|
||||
|
||||
// FIXME: If the user clicks on the reset button, the text field checker won't be called
|
||||
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
|
||||
final obx = RxString(controller.text);
|
||||
controller.addListener(() => obx.value = controller.text);
|
||||
SettingTile createFileSetting({
|
||||
required GlobalKey<TextFormBoxState> key,
|
||||
required String title,
|
||||
required String description,
|
||||
required TextEditingController controller,
|
||||
required void Function() onReset
|
||||
}) {
|
||||
final obx = RxnString();
|
||||
final selecting = RxBool(false);
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
@@ -32,17 +36,23 @@ SettingTile createFileSetting({required String title, required String descriptio
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: _checkDll,
|
||||
validator: (text) {
|
||||
final result = _checkDll(text);
|
||||
print("Called validator: $result");
|
||||
obx.value = result;
|
||||
return result;
|
||||
},
|
||||
extension: "dll",
|
||||
folder: false,
|
||||
validatorMode: AutovalidateMode.always,
|
||||
allowNavigator: false,
|
||||
validatorKey: key
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _kButtonSpacing),
|
||||
Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
||||
bottom: obx.value == null ? 0.0 : 20.0
|
||||
),
|
||||
child: Tooltip(
|
||||
message: translations.selectFile,
|
||||
@@ -63,7 +73,7 @@ SettingTile createFileSetting({required String title, required String descriptio
|
||||
const SizedBox(width: _kButtonSpacing),
|
||||
Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
||||
bottom: obx.value == null ? 0.0 : 20.0
|
||||
),
|
||||
child: Tooltip(
|
||||
message: translations.reset,
|
||||
@@ -109,7 +119,9 @@ String? _checkDll(String? text) {
|
||||
}
|
||||
|
||||
final file = File(text);
|
||||
if (!file.existsSync()) {
|
||||
try {
|
||||
file.readAsBytesSync();
|
||||
}catch(_) {
|
||||
return translations.dllDoesNotExist;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@@ -45,7 +44,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
InfoBarEntry? _gameServerInfoBar;
|
||||
CancelableOperation? _operation;
|
||||
Completer? _pingOperation;
|
||||
IVirtualDesktop? _virtualDesktop;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Align(
|
||||
@@ -73,17 +71,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if (host ? _hostingController.started() : _gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(
|
||||
reason: _StopReason.normal
|
||||
reason: _StopReason.normal,
|
||||
host: host
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final version = _gameController.selectedVersion;
|
||||
final version = _gameController.selectedVersion.value;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
|
||||
if(version == null){
|
||||
log("[${host ? 'HOST' : 'GAME'}] No version selected");
|
||||
_onStop(
|
||||
reason: _StopReason.missingVersionError
|
||||
reason: _StopReason.missingVersionError,
|
||||
host: host
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -93,37 +93,28 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
||||
for (final injectable in InjectableDll.values) {
|
||||
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
|
||||
if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final executable = await version.shippingExecutable;
|
||||
if(executable == null){
|
||||
log("[${host ? 'HOST' : 'GAME'}] No executable found");
|
||||
_onStop(
|
||||
reason: _StopReason.missingExecutableError,
|
||||
error: version.location.path
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
||||
final backendResult = _backendController.started() || await _backendController.toggle();
|
||||
if(!backendResult){
|
||||
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||
_onStop(
|
||||
reason: _StopReason.backendError
|
||||
reason: _StopReason.backendError,
|
||||
host: host
|
||||
);
|
||||
return;
|
||||
}
|
||||
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
||||
final serverType = _hostingController.type.value;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
||||
final headless = _hostingController.headless.value;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
||||
final result = await _startGameProcesses(version, host, headless, linkedHostingInstance);
|
||||
final started = host ? _hostingController.started() : _gameController.started();
|
||||
if(!started) {
|
||||
result?.kill();
|
||||
@@ -131,7 +122,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
|
||||
_showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null);
|
||||
}else {
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
@@ -139,18 +130,20 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedVersionError,
|
||||
error: exception.toString(),
|
||||
stackTrace: stackTrace
|
||||
stackTrace: stackTrace,
|
||||
host: host
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onStop(
|
||||
reason: _StopReason.unknownError,
|
||||
error: exception.toString(),
|
||||
stackTrace: stackTrace
|
||||
stackTrace: stackTrace,
|
||||
host: host
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, GameServerType hostType, bool forceLinkedHosting) async {
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool forceLinkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||
if(host){
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||
@@ -174,7 +167,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
|
||||
final instance = await _startGameProcesses(version, true, hostType, null);
|
||||
final instance = await _startGameProcesses(version, true, headless, null);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
|
||||
_setStarted(true, true);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
||||
@@ -184,7 +177,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
Future<bool> _askForAutomaticGameServer(bool host) async {
|
||||
if (host ? !_hostingController.started() : !_gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(reason: _StopReason.normal);
|
||||
_onStop(reason: _StopReason.normal, host: host);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -208,19 +201,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
|
||||
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started paused launcher: $launcherProcess");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting paused eac...");
|
||||
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started paused eac: $eacProcess");
|
||||
final executable = await version.shippingExecutable;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Using game path: ${executable?.path}");
|
||||
final gameProcess = await _createGameProcess(version, executable!, host, hostType, linkedHosting);
|
||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
|
||||
final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
|
||||
final eacProcess = await _createPausedProcess(version, host, kEacExe);
|
||||
final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
|
||||
if(gameProcess == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
|
||||
return null;
|
||||
@@ -228,11 +212,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||
final instance = GameInstance(
|
||||
version: version.content,
|
||||
version: version.gameVersion,
|
||||
gamePid: gameProcess,
|
||||
launcherPid: launcherProcess,
|
||||
eacPid: eacProcess,
|
||||
serverType: host ? hostType : null,
|
||||
headless: host && headless,
|
||||
child: linkedHosting
|
||||
);
|
||||
if(host){
|
||||
@@ -246,22 +230,44 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
|
||||
Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
|
||||
final shippingExecutables = await findFiles(version.location, kShippingExe);
|
||||
if(shippingExecutables.isEmpty){
|
||||
log("[${host ? 'HOST' : 'GAME'}] No game executable found");
|
||||
_onStop(
|
||||
reason: _StopReason.missingExecutableError,
|
||||
error: kShippingExe,
|
||||
host: host
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if(shippingExecutables.length != 1) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Too many game executables found");
|
||||
_onStop(
|
||||
reason: _StopReason.multipleExecutablesError,
|
||||
error: kShippingExe,
|
||||
host: host
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||
final gameArgs = createRebootArgs(
|
||||
host ? _hostingController.accountUsername.text : _gameController.username.text,
|
||||
host ? _hostingController.accountPassword.text : _gameController.password.text,
|
||||
host,
|
||||
hostType,
|
||||
headless,
|
||||
false,
|
||||
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
|
||||
);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
|
||||
final gameProcess = await startProcess(
|
||||
executable: executable,
|
||||
executable: shippingExecutables.first,
|
||||
args: gameArgs,
|
||||
useTempBatch: false,
|
||||
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
|
||||
name: "${version.gameVersion}-${host ? 'HOST' : 'GAME'}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
@@ -272,26 +278,26 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
handleGameOutput(
|
||||
line: line,
|
||||
host: host,
|
||||
onShutdown: () => _onStop(reason: _StopReason.normal),
|
||||
onTokenError: () => _onStop(reason: _StopReason.tokenError),
|
||||
onShutdown: () => _onStop(reason: _StopReason.normal, host: host),
|
||||
onTokenError: () => _onStop(reason: _StopReason.tokenError, host: host),
|
||||
onBuildCorrupted: () {
|
||||
if(instance == null) {
|
||||
return;
|
||||
}else if(!instance.launched) {
|
||||
_onStop(reason: _StopReason.corruptedVersionError);
|
||||
_onStop(reason: _StopReason.corruptedVersionError, host: host);
|
||||
}else {
|
||||
_onStop(reason: _StopReason.crash);
|
||||
_onStop(reason: _StopReason.crash, host: host);
|
||||
}
|
||||
},
|
||||
onLoggedIn: () =>_onLoggedIn(host),
|
||||
onMatchEnd: () => _onMatchEnd(version),
|
||||
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
|
||||
onMatchEnd: () => _onMatchEnd(version)
|
||||
);
|
||||
}
|
||||
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
|
||||
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
||||
gameProcess.exitCode.then((_) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
instance?.killed = true;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
|
||||
_onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
@@ -301,60 +307,37 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return gameProcess.pid;
|
||||
}
|
||||
|
||||
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async {
|
||||
if (file == null) {
|
||||
Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
|
||||
final executables = await findFiles(version.location, executableName);
|
||||
if(executables.isEmpty){
|
||||
return null;
|
||||
}
|
||||
|
||||
if(executables.length != 1) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Too many $executableName found: $executables");
|
||||
_onStop(
|
||||
reason: _StopReason.multipleExecutablesError,
|
||||
error: executableName,
|
||||
host: host
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final process = await startProcess(
|
||||
executable: file,
|
||||
executable: executables.first,
|
||||
useTempBatch: false,
|
||||
name: "${version.content}-${basenameWithoutExtension(file.path)}",
|
||||
name: "${version.gameVersion}-${basenameWithoutExtension(executables.first.path)}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started paused $executableName: $process");
|
||||
final pid = process.pid;
|
||||
suspend(pid);
|
||||
return pid;
|
||||
}
|
||||
|
||||
Future<void> _onDisplayAttached(bool host, GameServerType type, FortniteVersion version) async {
|
||||
if(host && type == GameServerType.virtualWindow) {
|
||||
final hostingInstance = _hostingController.instance.value;
|
||||
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
|
||||
hostingInstance.movedToVirtualDesktop = true;
|
||||
try {
|
||||
final windowManager = VirtualDesktopManager.getInstance();
|
||||
_virtualDesktop = windowManager.createDesktop();
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
|
||||
var success = false;
|
||||
try {
|
||||
success = await windowManager.moveWindowToDesktop(
|
||||
hostingInstance.gamePid,
|
||||
_virtualDesktop!,
|
||||
excludedWindowName: "Reboot"
|
||||
);
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] $error");
|
||||
success = false;
|
||||
}
|
||||
if(!success) {
|
||||
try {
|
||||
windowManager.removeDesktop(_virtualDesktop!);
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] $error");
|
||||
}finally {
|
||||
_virtualDesktop = null;
|
||||
}
|
||||
}
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] $error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onMatchEnd(FortniteVersion version) {
|
||||
if(_hostingController.autoRestart.value) {
|
||||
final notification = LocalNotification(
|
||||
@@ -397,7 +380,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(instance != null && !instance.launched) {
|
||||
instance.launched = true;
|
||||
instance.tokenError = false;
|
||||
await _injectOrShowError(InjectableDll.memoryLeak, host);
|
||||
if(_isChapterOne(instance.version)) {
|
||||
await _injectOrShowError(InjectableDll.memoryLeak, host);
|
||||
}
|
||||
if(!host){
|
||||
await _injectOrShowError(InjectableDll.console, host);
|
||||
_onGameClientInjected();
|
||||
@@ -412,6 +397,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isChapterOne(String version) {
|
||||
try {
|
||||
return Version.parse(version).major < 10;
|
||||
} on FormatException catch(_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void _onGameClientInjected() {
|
||||
_gameClientInfoBar?.close();
|
||||
showRebootInfoBar(
|
||||
@@ -515,21 +508,20 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||
if(host == null) {
|
||||
Future<void> _onStop({required _StopReason reason, required bool host, String? error, StackTrace? stackTrace}) async {
|
||||
if(host) {
|
||||
try {
|
||||
_pingOperation?.complete(false);
|
||||
}catch(_) {
|
||||
} catch (_) {
|
||||
// Ignore: might be running, don't bother checking
|
||||
} finally {
|
||||
_pingOperation = null;
|
||||
}
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
_backendController.stop(interactive: false);
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
|
||||
if(host){
|
||||
@@ -538,15 +530,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_gameController.instance.value = null;
|
||||
}
|
||||
|
||||
if(_virtualDesktop != null) {
|
||||
try {
|
||||
final instance = VirtualDesktopManager.getInstance();
|
||||
instance.removeDesktop(_virtualDesktop!);
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] Cannot close virtual desktop: $error");
|
||||
}
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
|
||||
if(host) {
|
||||
@@ -562,7 +545,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(child != null) {
|
||||
await _onStop(
|
||||
reason: reason,
|
||||
host: child.serverType != null
|
||||
host: host
|
||||
);
|
||||
}
|
||||
|
||||
@@ -594,10 +577,18 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.multipleExecutablesError:
|
||||
showRebootInfoBar(
|
||||
translations.multipleExecutablesError(error ?? translations.unknown),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.exitCode:
|
||||
if(instance != null && !instance.launched) {
|
||||
final injectedDlls = instance.injectedDlls;
|
||||
showRebootInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
@@ -630,8 +621,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
break;
|
||||
case _StopReason.tokenError:
|
||||
_backendController.stop(interactive: false);
|
||||
final injectedDlls = instance?.injectedDlls;
|
||||
showRebootInfoBar(
|
||||
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
|
||||
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
action: Button(
|
||||
@@ -669,17 +661,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
|
||||
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
|
||||
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");
|
||||
_onStop(
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
host: hosting
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable
|
||||
.name}...");
|
||||
await injectDll(gameProcess, dllPath);
|
||||
instance.injectedDlls.add(injectable);
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
|
||||
@@ -694,13 +682,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
|
||||
Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||
final (file, customDll) = _dllController.getInjectableData(version, injectable);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
||||
if(await file.exists()) {
|
||||
try {
|
||||
await file.readAsBytes();
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||
return file;
|
||||
}catch(_) {
|
||||
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
|
||||
@@ -709,14 +700,20 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_onStop(
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
host: host
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
||||
await _dllController.download(injectable, file.path, force: true);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||
return _getDllFileOrStop(version, injectable, host, true);
|
||||
final result = await _dllController.download(injectable, file.path, force: true);
|
||||
if(result) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded critical dll");
|
||||
return file;
|
||||
}
|
||||
|
||||
_onStop(reason: _StopReason.normal, host: host);
|
||||
return null;
|
||||
}
|
||||
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
||||
@@ -725,7 +722,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
duration: null
|
||||
);
|
||||
|
||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
|
||||
return _gameClientInfoBar = showRebootInfoBar(
|
||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
||||
loading: true,
|
||||
@@ -743,9 +740,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
onPressed: () async {
|
||||
_backendController.joinLocalhost();
|
||||
if(!_hostingController.started.value) {
|
||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
|
||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
|
||||
_gameClientInfoBar?.close();
|
||||
_showLaunchingGameClientWidget(version, hostType, true);
|
||||
_showLaunchingGameClientWidget(version, headless, true);
|
||||
}
|
||||
},
|
||||
child: Text(translations.startGameServer),
|
||||
@@ -760,6 +757,7 @@ enum _StopReason {
|
||||
normal,
|
||||
missingVersionError,
|
||||
missingExecutableError,
|
||||
multipleExecutablesError,
|
||||
corruptedVersionError,
|
||||
missingCustomDllError,
|
||||
corruptedDllError,
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||
Future<void> showDllDeletedDialog() => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.dllDeletedTitle,
|
||||
buttons: [
|
||||
@@ -15,7 +15,7 @@ Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||
text: translations.dllDeletedPrimaryAction,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onConfirm();
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -134,7 +134,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
dllsDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
_dllController.guardFiles();
|
||||
_dllController.downloadAndGuardDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,12 +5,10 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||
import 'package:reboot_launcher/src/widget/message/data.dart';
|
||||
@@ -19,7 +17,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||
@@ -78,7 +76,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_information,
|
||||
buildVersionSelector(
|
||||
VersionSelector.buildTile(
|
||||
key: hostVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
@@ -171,7 +169,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
contentWidth: null,
|
||||
content: Obx(() => Row(
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
Obx(() => Text(
|
||||
_hostingController.discoverable.value ? translations.on : translations.off
|
||||
)),
|
||||
const SizedBox(
|
||||
@@ -214,15 +212,21 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.gameServerTypeName),
|
||||
subtitle: Text(translations.gameServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_hostingController.type.value.translatedName),
|
||||
items: GameServerType.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _hostingController.type.value = entry
|
||||
)).toList()
|
||||
)),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
_hostingController.headless.value ? translations.on : translations.off
|
||||
)),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
Obx(() => ToggleSwitch(
|
||||
checked: _hostingController.headless.value,
|
||||
onChanged: (value) => _hostingController.headless.value = value
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||
|
||||
@@ -46,7 +46,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
buildVersionSelector(
|
||||
VersionSelector.buildTile(
|
||||
key: gameVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
|
||||
@@ -17,6 +17,11 @@ import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
|
||||
final GlobalKey<TextFormBoxState> settingsAuthDllInputKey = GlobalKey();
|
||||
final GlobalKey<TextFormBoxState> settingsMemoryDllInputKey = GlobalKey();
|
||||
final GlobalKey<TextFormBoxState> settingsGameServerDllInputKey = GlobalKey();
|
||||
|
||||
class SettingsPage extends RebootPage {
|
||||
const SettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -60,33 +65,39 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
subtitle: Text(translations.settingsClientDescription),
|
||||
children: [
|
||||
createFileSetting(
|
||||
key: settingsConsoleDllInputKey,
|
||||
title: translations.settingsClientConsoleName,
|
||||
description: translations.settingsClientConsoleDescription,
|
||||
controller: _dllController.unrealEngineConsoleDll,
|
||||
onReset: () {
|
||||
onReset: () async {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.console);
|
||||
_dllController.unrealEngineConsoleDll.text = path;
|
||||
_dllController.download(InjectableDll.console, path, force: true);
|
||||
await _dllController.download(InjectableDll.console, path, force: true);
|
||||
settingsConsoleDllInputKey.currentState?.validate();
|
||||
}
|
||||
),
|
||||
createFileSetting(
|
||||
key: settingsAuthDllInputKey,
|
||||
title: translations.settingsClientAuthName,
|
||||
description: translations.settingsClientAuthDescription,
|
||||
controller: _dllController.backendDll,
|
||||
onReset: () {
|
||||
onReset: () async {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
|
||||
_dllController.backendDll.text = path;
|
||||
_dllController.download(InjectableDll.auth, path, force: true);
|
||||
await _dllController.download(InjectableDll.auth, path, force: true);
|
||||
settingsAuthDllInputKey.currentState?.validate();
|
||||
}
|
||||
),
|
||||
createFileSetting(
|
||||
key: settingsMemoryDllInputKey,
|
||||
title: translations.settingsClientMemoryName,
|
||||
description: translations.settingsClientMemoryDescription,
|
||||
controller: _dllController.memoryLeakDll,
|
||||
onReset: () {
|
||||
onReset: () async {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
|
||||
_dllController.memoryLeakDll.text = path;
|
||||
_dllController.download(InjectableDll.memoryLeak, path, force: true);
|
||||
await _dllController.download(InjectableDll.memoryLeak, path, force: true);
|
||||
settingsAuthDllInputKey.currentState?.validate();
|
||||
}
|
||||
),
|
||||
_internalFilesServerType,
|
||||
@@ -142,9 +153,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.beforeS20Mirror,
|
||||
onChanged: _scheduleMirrorDownload
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.beforeS20Mirror,
|
||||
onChanged: _scheduleMirrorDownload
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
@@ -181,13 +192,15 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
);
|
||||
}else {
|
||||
return createFileSetting(
|
||||
key: settingsGameServerDllInputKey,
|
||||
title: translations.settingsOldServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _dllController.customGameServerDll,
|
||||
onReset: () {
|
||||
onReset: () async {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
|
||||
_dllController.customGameServerDll.text = path;
|
||||
_dllController.download(InjectableDll.gameServer, path);
|
||||
await _dllController.download(InjectableDll.gameServer, path);
|
||||
settingsGameServerDllInputKey.currentState?.validate();
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -224,9 +237,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.aboveS20Mirror,
|
||||
onChanged: _scheduleMirrorDownload
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.aboveS20Mirror,
|
||||
onChanged: _scheduleMirrorDownload
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
@@ -337,7 +350,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
|
||||
SettingTile get _installationDirectory => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
|
||||
@@ -14,22 +14,22 @@ import 'package:reboot_launcher/src/util/types.dart';
|
||||
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddVersionDialog extends StatefulWidget {
|
||||
class DownloadVersionDialog extends StatefulWidget {
|
||||
final bool closable;
|
||||
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
|
||||
const DownloadVersionDialog({Key? key, required this.closable}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddVersionDialog> createState() => _AddVersionDialogState();
|
||||
State<DownloadVersionDialog> createState() => _DownloadVersionDialogState();
|
||||
}
|
||||
|
||||
class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
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();
|
||||
@@ -46,6 +46,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_pathController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
@@ -63,10 +64,10 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case _DownloadStatus.form:
|
||||
return Obx(() => FormDialog(
|
||||
content: _buildFormBody(downloadableBuilds),
|
||||
return FormDialog(
|
||||
content: _formBody,
|
||||
buttons: _formButtons
|
||||
));
|
||||
);
|
||||
case _DownloadStatus.downloading:
|
||||
case _DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
@@ -86,7 +87,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
);
|
||||
case _DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
@@ -96,7 +97,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
if(widget.closable)
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
|
||||
text: translations.download,
|
||||
type: widget.closable ? ButtonType.primary : ButtonType.only,
|
||||
color: FluentTheme.of(context).accentColor,
|
||||
onTap: () => _startDownload(context),
|
||||
@@ -120,13 +121,6 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
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) {
|
||||
@@ -161,16 +155,22 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
final name = _nameController.text.trim();
|
||||
final location = Directory(_pathController.text);
|
||||
final files = await findFiles(location, kShippingExe);
|
||||
if(files.length == 1) {
|
||||
await patchHeadless(files.first);
|
||||
}
|
||||
|
||||
_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)
|
||||
)));
|
||||
final version = FortniteVersion(
|
||||
name: name,
|
||||
gameVersion: build.gameVersion,
|
||||
location: location
|
||||
);
|
||||
_gameController.addVersion(version);
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
@@ -269,25 +269,32 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
return translations.downloading;
|
||||
}
|
||||
|
||||
Widget _buildFormBody(List<FortniteBuild> builds) {
|
||||
return Column(
|
||||
Widget get _formBody => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSourceSelector(),
|
||||
InfoLabel(
|
||||
label: translations.versionName,
|
||||
child: TextFormBox(
|
||||
controller: _nameController,
|
||||
validator: _checkVersionName,
|
||||
placeholder: translations.versionNameLabel,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
_buildBuildSelector(builds),
|
||||
_buildSelector,
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
||||
placeholder: translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
||||
validator: _checkDownloadDestination,
|
||||
folder: true,
|
||||
allowNavigator: true
|
||||
),
|
||||
@@ -297,20 +304,14 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? _checkGameFolder(text) {
|
||||
String? _checkVersionName(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
final directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
if(_gameController.getVersionByName(text) != null) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -324,44 +325,46 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
|
||||
Widget get _buildSelector => 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();
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: downloadableBuilds.where((build) => build.available)
|
||||
.map((element) => _buildBuildItem(element))
|
||||
.toList(),
|
||||
value: _build.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
),
|
||||
if(formContext.hasError)
|
||||
const SizedBox(height: 4.0),
|
||||
if(formContext.hasError)
|
||||
Text(
|
||||
formContext.errorText ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
|
||||
),
|
||||
|
||||
_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
|
||||
),
|
||||
],
|
||||
)
|
||||
SizedBox(
|
||||
height: formContext.hasError ? 8.0 : 16.0
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -370,40 +373,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
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)
|
||||
child: Text(element.gameVersion)
|
||||
);
|
||||
|
||||
|
||||
@@ -415,21 +390,21 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_source.value != _BuildSource.local && _build.value?.available != true) {
|
||||
if(_build.value?.available != true) {
|
||||
_build.value = null;
|
||||
}
|
||||
|
||||
final disks = WindowsDisk.available();
|
||||
if(_source.value != _BuildSource.local && disks.isNotEmpty) {
|
||||
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
final build = _build.value;
|
||||
if(build != null) {
|
||||
_nameController.text = build.gameVersion;
|
||||
_nameController.selection = TextSelection.collapsed(offset: build.gameVersion.length);
|
||||
final disks = WindowsDisk.available();
|
||||
if(disks.isNotEmpty) {
|
||||
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
|
||||
final pathText = "${bestDisk.path}FortniteBuilds\\${build.gameVersion}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
}
|
||||
|
||||
_formKey.currentState?.validate();
|
||||
@@ -443,18 +418,3 @@ enum _DownloadStatus {
|
||||
error,
|
||||
done
|
||||
}
|
||||
|
||||
enum _BuildSource {
|
||||
local,
|
||||
githubArchive;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _BuildSource.local:
|
||||
return translations.localBuild;
|
||||
case _BuildSource.githubArchive:
|
||||
return translations.githubArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
234
gui/lib/src/widget/version/import_version.dart
Normal file
@@ -0,0 +1,234 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.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/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:version/version.dart';
|
||||
|
||||
class ImportVersionDialog extends StatefulWidget {
|
||||
final FortniteVersion? version;
|
||||
final bool closable;
|
||||
const ImportVersionDialog({Key? key, required this.version, required this.closable}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ImportVersionDialog> createState() => _ImportVersionDialogState();
|
||||
}
|
||||
|
||||
class _ImportVersionDialogState extends State<ImportVersionDialog> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final Rx<_ImportState> _validator = Rx(_ImportState.inputData);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final version = widget.version;
|
||||
if(version != null) {
|
||||
_nameController.text = version.name;
|
||||
_nameController.selection = TextSelection.collapsed(offset: version.name.length);
|
||||
_pathController.text = version.location.path;
|
||||
_pathController.selection = TextSelection.collapsed(offset: version.location.path.length);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_pathController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_validator.value) {
|
||||
case _ImportState.inputData:
|
||||
return FormDialog(
|
||||
content: _importBody,
|
||||
buttons: _importButtons
|
||||
);
|
||||
case _ImportState.validating:
|
||||
return ProgressDialog(
|
||||
text: translations.importingVersion
|
||||
);
|
||||
case _ImportState.success:
|
||||
return InfoDialog(
|
||||
text: translations.importedVersion
|
||||
);
|
||||
case _ImportState.missingShippingExeError:
|
||||
return InfoDialog(
|
||||
text: translations.importVersionMissingShippingExeError(kShippingExe)
|
||||
);
|
||||
case _ImportState.multipleShippingExesError:
|
||||
return InfoDialog(
|
||||
text: translations.importVersionMultipleShippingExesError(kShippingExe)
|
||||
);
|
||||
case _ImportState.unsupportedVersionError:
|
||||
return InfoDialog(
|
||||
text: translations.importVersionUnsupportedVersionError
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
Widget get _importBody => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoLabel(
|
||||
label: translations.versionName,
|
||||
child: TextFormBox(
|
||||
controller: _nameController,
|
||||
validator: _checkVersionName,
|
||||
placeholder: translations.versionNameLabel,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: translations.gameFolderPlaceholder,
|
||||
windowTitle: translations.gameFolderPlaceWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: _checkGamePath,
|
||||
validatorMode: AutovalidateMode.onUserInteraction,
|
||||
folder: true,
|
||||
allowNavigator: true,
|
||||
onSelected: (selected) {
|
||||
var name = path.basename(selected);
|
||||
if(_gameController.getVersionByName(name) != null) {
|
||||
var counter = 1;
|
||||
while(_gameController.getVersionByName("$name-$counter") != null) {
|
||||
counter++;
|
||||
}
|
||||
name = "$name-$counter";
|
||||
}
|
||||
_nameController.text = name;
|
||||
_nameController.selection = TextSelection.collapsed(offset: name.length);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
List<DialogButton> get _importButtons => [
|
||||
if(widget.closable)
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: translations.saveLocalVersion,
|
||||
type: widget.closable ? ButtonType.primary : ButtonType.only,
|
||||
color: FluentTheme.of(context).accentColor,
|
||||
onTap: _importVersion,
|
||||
)
|
||||
];
|
||||
|
||||
void _importVersion() async {
|
||||
final topResult = _formKey.currentState?.validate();
|
||||
if(topResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
_validator.value = _ImportState.validating;
|
||||
final name = _nameController.text.trim();
|
||||
final directory = Directory(_pathController.text.trim());
|
||||
|
||||
final files = await Future.wait([
|
||||
Future.delayed(const Duration(seconds: 1)).then((_) => <File>[]),
|
||||
findFiles(directory, kShippingExe).then((files) async {
|
||||
if(files.length == 1) {
|
||||
await patchHeadless(files.first);
|
||||
}
|
||||
return files;
|
||||
})
|
||||
]).then((values) => values.expand((entry) => entry).toList());
|
||||
|
||||
if (files.isEmpty) {
|
||||
_validator.value = _ImportState.missingShippingExeError;
|
||||
return;
|
||||
}
|
||||
|
||||
if(files.length != 1) {
|
||||
_validator.value = _ImportState.multipleShippingExesError;
|
||||
return;
|
||||
}
|
||||
|
||||
final gameVersion = await extractGameVersion(files.first.path, path.basename(directory.path));
|
||||
try {
|
||||
if(Version.parse(gameVersion) >= kMaxAllowedVersion) {
|
||||
_validator.value = _ImportState.unsupportedVersionError;
|
||||
return;
|
||||
}
|
||||
}catch(_) {
|
||||
|
||||
}
|
||||
|
||||
if(widget.version == null) {
|
||||
final version = FortniteVersion(
|
||||
name: name,
|
||||
gameVersion: gameVersion,
|
||||
location: files.first.parent
|
||||
);
|
||||
_gameController.addVersion(version);
|
||||
}else {
|
||||
widget.version?.name = name;
|
||||
widget.version?.gameVersion = gameVersion;
|
||||
widget.version?.location = files.first.parent;
|
||||
}
|
||||
_validator.value = _ImportState.success;
|
||||
}
|
||||
|
||||
String? _checkVersionName(String? text) {
|
||||
final version = widget.version;
|
||||
if(version != null && version.name == text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
if(_gameController.getVersionByName(text) != null) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _checkGamePath(String? input) {
|
||||
if(input == null || input.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
final directory = Directory(input);
|
||||
if(!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
enum _ImportState {
|
||||
inputData,
|
||||
validating,
|
||||
success,
|
||||
missingShippingExeError,
|
||||
multipleShippingExesError,
|
||||
unsupportedVersionError
|
||||
}
|
||||
|
||||
@@ -8,15 +8,46 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/widget/message/version.dart';
|
||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/download_version.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/import_version.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
const VersionSelector({Key? key}) : super(key: key);
|
||||
|
||||
static SettingTile buildTile({
|
||||
required GlobalKey<OverlayTargetState> key
|
||||
}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
title: Text(translations.selectFortniteName),
|
||||
subtitle: Text(translations.selectFortniteDescription),
|
||||
contentWidth: null,
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: SettingTile.kDefaultContentWidth,
|
||||
),
|
||||
child: OverlayTarget(
|
||||
key: key,
|
||||
child: const VersionSelector(),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
static Future<void> openImportDialog(FortniteVersion? version) => showRebootDialog<bool>(
|
||||
builder: (context) => ImportVersionDialog(
|
||||
version: version,
|
||||
closable: true,
|
||||
),
|
||||
dismissWithEsc: true
|
||||
);
|
||||
|
||||
static Future<void> openDownloadDialog() => showRebootDialog<bool>(
|
||||
builder: (context) => AddVersionDialog(
|
||||
builder: (context) => DownloadVersionDialog(
|
||||
closable: true,
|
||||
),
|
||||
dismissWithEsc: true
|
||||
@@ -34,7 +65,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
return _createOptionsMenu(
|
||||
version: _gameController.selectedVersion,
|
||||
version: _gameController.selectedVersion.value,
|
||||
close: false,
|
||||
child: FlyoutTarget(
|
||||
controller: _flyoutController,
|
||||
@@ -42,7 +73,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_gameController.selectedVersion?.content.toString() ?? translations.selectVersion,
|
||||
_gameController.selectedVersion.value?.name ?? translations.selectVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -65,7 +96,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
var result = await _flyoutController.showFlyout<_ContextualOption?>(
|
||||
builder: (context) => MenuFlyout(
|
||||
items: _ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.map((entry) => _createOption(entry))
|
||||
.toList()
|
||||
)
|
||||
);
|
||||
@@ -76,7 +107,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
final items = _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.map((version) => _createVersionItem(version))
|
||||
.toList();
|
||||
items.add(MenuFlyoutItem(
|
||||
trailing: Padding(
|
||||
@@ -87,12 +118,23 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
),
|
||||
),
|
||||
text: Text(translations.addVersion),
|
||||
onPressed: () => VersionSelector.openImportDialog(null)
|
||||
));
|
||||
items.add(MenuFlyoutItem(
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
FluentIcons.arrow_download_24_regular,
|
||||
size: 14
|
||||
),
|
||||
),
|
||||
text: Text(translations.downloadVersion),
|
||||
onPressed: VersionSelector.openDownloadDialog
|
||||
));
|
||||
return items;
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
MenuFlyoutItem _createVersionItem(FortniteVersion version) => MenuFlyoutItem(
|
||||
text: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||
@@ -101,7 +143,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
|
||||
await _openVersionOptions(version);
|
||||
},
|
||||
child: Text(version.content.toString())
|
||||
child: Text(version.name)
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () => _openVersionOptions(version),
|
||||
@@ -109,14 +151,14 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
FluentIcons.more_vertical_24_regular
|
||||
)
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version
|
||||
onPressed: () => _gameController.selectedVersion.value = version
|
||||
);
|
||||
|
||||
Future<void> _openVersionOptions(FortniteVersion version) async {
|
||||
final result = await _flyoutController.showFlyout<_ContextualOption?>(
|
||||
builder: (context) => MenuFlyout(
|
||||
items: _ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.map((entry) => _createOption(entry))
|
||||
.toList()
|
||||
),
|
||||
barrierDismissible: true,
|
||||
@@ -139,8 +181,19 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
launchUrl(version.location.uri)
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
break;
|
||||
case _ContextualOption.modify:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
await VersionSelector.openImportDialog(version);
|
||||
break;
|
||||
case _ContextualOption.delete:
|
||||
final result = await _openDeleteDialog(context, version) ?? false;
|
||||
final result = await _openDeleteDialog(version) ?? false;
|
||||
if(!mounted || !result){
|
||||
return;
|
||||
}
|
||||
@@ -160,7 +213,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createOption(BuildContext context, _ContextualOption entry) {
|
||||
MenuFlyoutItem _createOption(_ContextualOption entry) {
|
||||
return MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => Navigator.of(context).pop(entry)
|
||||
@@ -168,11 +221,15 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
bool _onExplorerError() {
|
||||
showRebootInfoBar(translations.missingVersion);
|
||||
showRebootInfoBar(
|
||||
translations.missingVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
|
||||
Future<bool?> _openDeleteDialog(FortniteVersion version) {
|
||||
return showRebootDialog<bool>(
|
||||
builder: (context) => ContentDialog(
|
||||
content: Column(
|
||||
@@ -209,18 +266,21 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
GameController get gameController => _gameController;
|
||||
}
|
||||
|
||||
enum _ContextualOption {
|
||||
openExplorer,
|
||||
delete;
|
||||
modify,
|
||||
delete
|
||||
}
|
||||
|
||||
extension _ContextualOptionExtension on _ContextualOption {
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _ContextualOption.openExplorer:
|
||||
return translations.openInExplorer;
|
||||
case _ContextualOption.delete:
|
||||
return translations.delete;
|
||||
}
|
||||
return this == _ContextualOption.openExplorer ? translations.openInExplorer
|
||||
: this == _ContextualOption.modify ? translations.modify
|
||||
: translations.delete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||
|
||||
SettingTile buildVersionSelector({
|
||||
required GlobalKey<OverlayTargetState> key
|
||||
}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
title: Text(translations.selectFortniteName),
|
||||
subtitle: Text(translations.selectFortniteDescription),
|
||||
contentWidth: null,
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: SettingTile.kDefaultContentWidth,
|
||||
),
|
||||
child: OverlayTarget(
|
||||
key: key,
|
||||
child: const VersionSelector(),
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
name: reboot_launcher
|
||||
description: Graphical User Interface for Project Reboot
|
||||
version: "10.0.7"
|
||||
version: "10.0.8"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
@@ -33,9 +33,6 @@ dependencies:
|
||||
# Window management
|
||||
window_manager: ^0.4.2
|
||||
|
||||
# Extract zip archives (for example the reboot.zip)
|
||||
archive: ^3.6.1
|
||||
|
||||
# Cryptographic functions
|
||||
bcrypt: ^1.1.3
|
||||
pointycastle: ^3.9.1
|
||||
@@ -49,6 +46,8 @@ dependencies:
|
||||
get: ^4.6.6
|
||||
|
||||
# Native utilities
|
||||
archive: ^3.6.1
|
||||
watcher: ^1.1.1
|
||||
clipboard: ^0.1.3
|
||||
app_links: ^6.3.2
|
||||
windows_taskbar: ^1.1.2
|
||||
|
||||
@@ -55,6 +55,7 @@ begin
|
||||
' Allow DLL injection',
|
||||
' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server',
|
||||
'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' +
|
||||
'If you are using another AntiVirus, it might be necessary to add an exclusion manually. ' +
|
||||
'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' +
|
||||
'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' +
|
||||
'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.',
|
||||
|
||||