mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
9.0.4
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class AuthenticatorController extends ServerController {
|
||||
AuthenticatorController() : super();
|
||||
|
||||
@override
|
||||
String get controllerName => translations.authenticatorName.toLowerCase();
|
||||
|
||||
@override
|
||||
String get storageName => "authenticator";
|
||||
|
||||
@override
|
||||
String get defaultHost => kDefaultAuthenticatorHost;
|
||||
|
||||
@override
|
||||
int get defaultPort => kDefaultAuthenticatorPort;
|
||||
|
||||
@override
|
||||
Future<bool> get isPortFree => isAuthenticatorPortFree();
|
||||
|
||||
@override
|
||||
Future<bool> freePort() => freeAuthenticatorPort();
|
||||
|
||||
@override
|
||||
RebootPageType get pageType => RebootPageType.authenticator;
|
||||
|
||||
@override
|
||||
Future<Win32Process> startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value);
|
||||
|
||||
@override
|
||||
Future<Uri?> pingServer(String host, int port) => pingAuthenticator(host, port);
|
||||
}
|
||||
43
gui/lib/src/controller/backend_controller.dart
Normal file
43
gui/lib/src/controller/backend_controller.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class BackendController extends ServerController {
|
||||
late RxBool detached;
|
||||
|
||||
BackendController() : super() {
|
||||
detached = RxBool(storage.read("detached") ?? false);
|
||||
detached.listen((value) => storage.write("detached", value));
|
||||
}
|
||||
|
||||
@override
|
||||
String get controllerName => translations.backendName.toLowerCase();
|
||||
|
||||
@override
|
||||
String get storageName => "backend";
|
||||
|
||||
@override
|
||||
String get defaultHost => kDefaultBackendHost;
|
||||
|
||||
@override
|
||||
int get defaultPort => kDefaultBackendPort;
|
||||
|
||||
@override
|
||||
Future<bool> get isPortFree => isBackendPortFree();
|
||||
|
||||
@override
|
||||
Future<bool> freePort() => freeBackendPort();
|
||||
|
||||
@override
|
||||
RebootPageType get pageType => RebootPageType.backend;
|
||||
|
||||
@override
|
||||
Future<Process> startEmbeddedInternal() => startEmbeddedBackend(detached.value);
|
||||
|
||||
@override
|
||||
Future<Uri?> pingServer(String host, int port) => pingBackend(host, port);
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
|
||||
class GameController extends GetxController {
|
||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||
|
||||
late final GetStorage _storage;
|
||||
late final TextEditingController username;
|
||||
late final TextEditingController password;
|
||||
@@ -15,6 +20,7 @@ class GameController extends GetxController {
|
||||
late final Rxn<FortniteVersion> _selectedVersion;
|
||||
late final RxBool started;
|
||||
late final Rxn<GameInstance> instance;
|
||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||
|
||||
GameController() {
|
||||
_storage = GetStorage("game");
|
||||
@@ -34,12 +40,42 @@ class GameController extends GetxController {
|
||||
username.addListener(() => _storage.write("username", username.text));
|
||||
password = TextEditingController(text: _storage.read("password") ?? "");
|
||||
password.addListener(() => _storage.write("password", password.text));
|
||||
customLaunchArgs =
|
||||
TextEditingController(text: _storage.read("custom_launch_args") ?? "");
|
||||
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? "");
|
||||
customLaunchArgs.addListener(() =>
|
||||
_storage.write("custom_launch_args", customLaunchArgs.text));
|
||||
started = RxBool(false);
|
||||
instance = Rxn();
|
||||
consoleKey = Rx(_readConsoleKey());
|
||||
_writeConsoleKey(consoleKey.value);
|
||||
consoleKey.listen((newValue) {
|
||||
_storage.write("console_key", newValue.usbHidUsage);
|
||||
_writeConsoleKey(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
PhysicalKeyboardKey _readConsoleKey() {
|
||||
final consoleKeyValue = _storage.read("console_key");
|
||||
if(consoleKeyValue == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
|
||||
if(consoleKeyNumber == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
|
||||
if(!consoleKey.isUnrealEngineKey) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
|
||||
return consoleKey;
|
||||
}
|
||||
|
||||
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
||||
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
|
||||
await defaultInput.parent.create(recursive: true);
|
||||
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
|
||||
@@ -14,9 +14,9 @@ class HostingController extends GetxController {
|
||||
late final RxBool showPassword;
|
||||
late final RxBool discoverable;
|
||||
late final RxBool headless;
|
||||
late final RxBool virtualDesktop;
|
||||
late final RxBool started;
|
||||
late final RxBool published;
|
||||
late final RxBool automaticServer;
|
||||
late final Rxn<GameInstance> instance;
|
||||
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||
|
||||
@@ -34,12 +34,12 @@ class HostingController extends GetxController {
|
||||
discoverable.listen((value) => _storage.write("discoverable", value));
|
||||
headless = RxBool(_storage.read("headless") ?? true);
|
||||
headless.listen((value) => _storage.write("headless", value));
|
||||
virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true);
|
||||
virtualDesktop.listen((value) => _storage.write("virtual_desktop", value));
|
||||
started = RxBool(false);
|
||||
published = RxBool(false);
|
||||
showPassword = RxBool(false);
|
||||
instance = Rxn();
|
||||
automaticServer = RxBool(_storage.read("auto") ?? true);
|
||||
automaticServer.listen((value) => _storage.write("auto", value));
|
||||
final supabase = Supabase.instance.client;
|
||||
servers = Rxn();
|
||||
supabase.from("hosting")
|
||||
@@ -60,6 +60,8 @@ class HostingController extends GetxController {
|
||||
discoverable.value = false;
|
||||
started.value = false;
|
||||
instance.value = null;
|
||||
headless.value = true;
|
||||
virtualDesktop.value = true;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? findServerById(String uuid) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
@@ -57,7 +59,7 @@ class MatchmakerController extends ServerController {
|
||||
RebootPageType get pageType => RebootPageType.matchmaker;
|
||||
|
||||
@override
|
||||
Future<Win32Process> startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value);
|
||||
Future<Process> startEmbeddedInternal() => startEmbeddedMatchmaker();
|
||||
|
||||
@override
|
||||
Future<Uri?> pingServer(String host, int port) => pingMatchmaker(host, port);
|
||||
|
||||
@@ -15,7 +15,6 @@ abstract class ServerController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final Semaphore semaphore;
|
||||
late RxBool started;
|
||||
late RxBool detached;
|
||||
StreamSubscription? worker;
|
||||
HttpServer? localServer;
|
||||
HttpServer? remoteServer;
|
||||
@@ -40,8 +39,6 @@ abstract class ServerController extends GetxController {
|
||||
port = TextEditingController(text: _readPort());
|
||||
port.addListener(() =>
|
||||
storage.write("${type.value.name}_port", port.text));
|
||||
detached = RxBool(storage.read("detached") ?? false);
|
||||
detached.listen((value) => storage.write("detached", value));
|
||||
semaphore = Semaphore();
|
||||
}
|
||||
|
||||
@@ -64,7 +61,7 @@ abstract class ServerController extends GetxController {
|
||||
Future<bool> freePort();
|
||||
|
||||
@protected
|
||||
Future<Win32Process> startEmbeddedInternal();
|
||||
Future<Process> startEmbeddedInternal();
|
||||
|
||||
void reset() async {
|
||||
type.value = ServerType.values.elementAt(0);
|
||||
@@ -100,7 +97,7 @@ abstract class ServerController extends GetxController {
|
||||
yield ServerResult(ServerResultType.starting);
|
||||
}else {
|
||||
started.value = false;
|
||||
if(portData != defaultPort) {
|
||||
if(portData != defaultPort.toString()) {
|
||||
yield ServerResult(ServerResultType.starting);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +121,7 @@ abstract class ServerController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((type() != ServerType.local || portData != defaultPort) && await isPortTaken) {
|
||||
if ((type() != ServerType.local || portData != defaultPort.toString()) && await isPortTaken) {
|
||||
yield ServerResult(ServerResultType.freeingPort);
|
||||
final result = await freePort();
|
||||
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
||||
@@ -138,12 +135,6 @@ abstract class ServerController extends GetxController {
|
||||
case ServerType.embedded:
|
||||
final process = await startEmbeddedInternal();
|
||||
final processPid = process.pid;
|
||||
if(processPid == null) {
|
||||
yield ServerResult(ServerResultType.startError);
|
||||
started.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
watchProcess(processPid).then((value) {
|
||||
if(started()) {
|
||||
started.value = false;
|
||||
@@ -159,11 +150,11 @@ abstract class ServerController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
remoteServer = await startRemoteAuthenticatorProxy(uriResult);
|
||||
remoteServer = await startRemoteBackendProxy(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
if(portData != defaultPort) {
|
||||
localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$port"));
|
||||
if(portData != defaultPort.toString()) {
|
||||
localServer = await startRemoteBackendProxy(Uri.parse("http://$defaultHost:$portData"));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -10,7 +10,7 @@ class SettingsController extends GetxController {
|
||||
late final String originalDll;
|
||||
late final TextEditingController gameServerDll;
|
||||
late final TextEditingController unrealEngineConsoleDll;
|
||||
late final TextEditingController authenticatorDll;
|
||||
late final TextEditingController backendDll;
|
||||
late final TextEditingController memoryLeakDll;
|
||||
late final TextEditingController gameServerPort;
|
||||
late final RxBool firstRun;
|
||||
@@ -25,7 +25,7 @@ class SettingsController extends GetxController {
|
||||
_storage = GetStorage("settings");
|
||||
gameServerDll = _createController("game_server", "reboot.dll");
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
|
||||
authenticatorDll = _createController("authenticator", "cobalt.dll");
|
||||
backendDll = _createController("backend", "cobalt.dll");
|
||||
memoryLeakDll = _createController("memory_leak", "memoryleak.dll");
|
||||
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
|
||||
@@ -33,8 +33,8 @@ class SettingsController extends GetxController {
|
||||
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
firstRun = RxBool(_storage.read("first_run") ?? true);
|
||||
firstRun.listen((value) => _storage.write("first_run", value));
|
||||
firstRun = RxBool(_storage.read("first_run_new1") ?? true);
|
||||
firstRun.listen((value) => _storage.write("first_run_new1", value));
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage.write("theme", value.index));
|
||||
language = RxString(_storage.read("language") ?? currentLocale);
|
||||
@@ -62,7 +62,7 @@ class SettingsController extends GetxController {
|
||||
void reset(){
|
||||
gameServerDll.text = _controllerDefaultPath("reboot.dll");
|
||||
unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll");
|
||||
authenticatorDll.text = _controllerDefaultPath("cobalt.dll");
|
||||
backendDll.text = _controllerDefaultPath("cobalt.dll");
|
||||
gameServerPort.text = kDefaultGameServerPort;
|
||||
firstRun.value = true;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,15 @@ class UpdateController {
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController url;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future? _updater;
|
||||
|
||||
UpdateController() {
|
||||
_storage = GetStorage("update");
|
||||
timestamp = RxnInt(_storage.read("ts"));
|
||||
timestamp.listen((value) => _storage.write("ts", value));
|
||||
var timerIndex = _storage.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.day : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage.write("timer", value.index));
|
||||
url = TextEditingController(text: _storage.read("update_url") ?? kRebootDownloadUrl);
|
||||
url.addListener(() => _storage.write("update_url", url.text));
|
||||
@@ -25,6 +27,16 @@ class UpdateController {
|
||||
}
|
||||
|
||||
Future<void> update([bool force = false]) async {
|
||||
if(_updater != null) {
|
||||
return await _updater;
|
||||
}
|
||||
|
||||
final result = _update(force);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<void> _update([bool force = false]) async {
|
||||
try {
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
@@ -36,25 +48,27 @@ class UpdateController {
|
||||
return;
|
||||
}
|
||||
|
||||
showInfoBar(
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
showInfoBar(
|
||||
infoBarEntry?.close();
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}catch(message) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showInfoBar(
|
||||
translations.downloadDllError(error.toString()),
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
@@ -62,6 +76,8 @@ class UpdateController {
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +1,72 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
const infoBarLongDuration = Duration(seconds: 4);
|
||||
const infoBarShortDuration = Duration(seconds: 2);
|
||||
const _height = 64.0;
|
||||
|
||||
Semaphore _semaphore = Semaphore();
|
||||
HashMap<int, _OverlayEntry> _overlays = HashMap();
|
||||
|
||||
void restoreMessage(int pageIndex, int lastIndex) {
|
||||
removeMessageByPage(lastIndex);
|
||||
final entry = _overlays[pageIndex];
|
||||
if(entry == null) {
|
||||
return;
|
||||
InfoBarEntry showInfoBar(dynamic text, {
|
||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
void Function()? onDismissed,
|
||||
Widget? action
|
||||
}) {
|
||||
final overlay = _buildOverlay(text, action, loading, severity);
|
||||
final overlayEntry = InfoBarEntry(overlay: overlay, onDismissed: onDismissed);
|
||||
if(duration != null) {
|
||||
Future.delayed(duration)
|
||||
.then((_) => WidgetsBinding.instance.addPostFrameCallback((timeStamp) => overlayEntry.close()));
|
||||
}
|
||||
|
||||
Overlay.of(pageKey.currentContext!).insert(entry.overlay);
|
||||
return overlayEntry;
|
||||
}
|
||||
|
||||
OverlayEntry showInfoBar(dynamic text,
|
||||
{InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
void Function()? onDismissed,
|
||||
Widget? action}) {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
removeMessageByPage(pageIndex.value);
|
||||
final overlay = OverlayEntry(
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: hasPageButton ? 72.0 : 16.0
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Mica(
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
Overlay.of(pageKey.currentContext!).insert(overlay);
|
||||
_overlays[pageIndex.value] = _OverlayEntry(
|
||||
overlay: overlay,
|
||||
onDismissed: onDismissed
|
||||
);
|
||||
if(duration != null) {
|
||||
Future.delayed(duration).then((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentOverlay = _overlays[pageIndex.value];
|
||||
if(currentOverlay == overlay) {
|
||||
if(overlay.mounted) {
|
||||
overlay.remove();
|
||||
}
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => SizedBox(
|
||||
width: double.infinity,
|
||||
height: _height,
|
||||
child: Mica(
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_overlays.remove(pageIndex.value);
|
||||
currentOverlay?.onDismissed?.call();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return overlay;
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
void removeMessageByPage(int index) {
|
||||
final lastOverlay = _overlays[index];
|
||||
if(lastOverlay != null) {
|
||||
try {
|
||||
lastOverlay.overlay.remove();
|
||||
}catch(_) {
|
||||
// Do not use .isMounted
|
||||
// This is intended behaviour
|
||||
}finally {
|
||||
_overlays.remove(index);
|
||||
lastOverlay.onDismissed?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _OverlayEntry {
|
||||
final OverlayEntry overlay;
|
||||
class InfoBarEntry {
|
||||
final Widget overlay;
|
||||
final void Function()? onDismissed;
|
||||
|
||||
_OverlayEntry({required this.overlay, required this.onDismissed});
|
||||
InfoBarEntry({required this.overlay, required this.onDismissed}) {
|
||||
final context = pageKey.currentContext;
|
||||
if(context != null) {
|
||||
infoBarAreaKey.currentState?.insertChild(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
bool close() {
|
||||
final result = infoBarAreaKey.currentState?.removeChild(overlay) ?? false;
|
||||
if(result) {
|
||||
onDismissed?.call();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -20,133 +20,116 @@ import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
extension ServerControllerDialog on ServerController {
|
||||
Future<bool> toggleInteractive([bool showSuccessMessage = true]) async {
|
||||
var stream = toggle();
|
||||
var completer = Completer<bool>();
|
||||
Future<bool> toggleInteractive() async {
|
||||
final stream = toggle();
|
||||
final completer = Completer<bool>();
|
||||
InfoBarEntry? entry;
|
||||
worker = stream.listen((event) {
|
||||
print(event.type);
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
showInfoBar(
|
||||
translations.startingServer(controllerName),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
break;
|
||||
case ServerResultType.startSuccess:
|
||||
if(showSuccessMessage) {
|
||||
showInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer(controllerName) : translations.startedServer(controllerName),
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
}
|
||||
completer.complete(true);
|
||||
break;
|
||||
case ServerResultType.startError:
|
||||
showInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError, controllerName) : translations.startServerError(event.error ?? translations.unknownError, controllerName),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
break;
|
||||
case ServerResultType.stopping:
|
||||
showInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
break;
|
||||
case ServerResultType.stopSuccess:
|
||||
if(showSuccessMessage) {
|
||||
showInfoBar(
|
||||
translations.stoppedServer(controllerName),
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
}
|
||||
completer.complete(true);
|
||||
break;
|
||||
case ServerResultType.stopError:
|
||||
showInfoBar(
|
||||
translations.stopServerError(
|
||||
event.error ?? translations.unknownError, controllerName),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
break;
|
||||
case ServerResultType.missingHostError:
|
||||
showInfoBar(
|
||||
translations.missingHostNameError(controllerName),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
case ServerResultType.missingPortError:
|
||||
showInfoBar(
|
||||
translations.missingPortError(controllerName),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
case ServerResultType.illegalPortError:
|
||||
showInfoBar(
|
||||
translations.illegalPortError(controllerName),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
case ServerResultType.freeingPort:
|
||||
showInfoBar(
|
||||
translations.freeingPort(defaultPort),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
break;
|
||||
case ServerResultType.freePortSuccess:
|
||||
showInfoBar(
|
||||
translations.freedPort(defaultPort),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
break;
|
||||
case ServerResultType.freePortError:
|
||||
showInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError, controllerName),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
break;
|
||||
case ServerResultType.pingingRemote:
|
||||
if(started.value) {
|
||||
showInfoBar(
|
||||
translations.pingingRemoteServer(controllerName),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ServerResultType.pingingLocal:
|
||||
showInfoBar(
|
||||
translations.pingingLocalServer(controllerName, type().name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
break;
|
||||
case ServerResultType.pingError:
|
||||
showInfoBar(
|
||||
translations.pingError(controllerName, type().name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
entry?.close();
|
||||
entry = _handeEvent(event);
|
||||
if(event.type.isError) {
|
||||
completer.complete(false);
|
||||
}else if(event.type.isSuccess) {
|
||||
completer.complete(true);
|
||||
}
|
||||
});
|
||||
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
InfoBarEntry _handeEvent(ServerResult event) {
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
return showInfoBar(
|
||||
translations.startingServer(controllerName),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.startSuccess:
|
||||
return showInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer(controllerName) : translations.startedServer(controllerName),
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.startError:
|
||||
return showInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError, controllerName) : translations.startServerError(event.error ?? translations.unknownError, controllerName),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.stopping:
|
||||
return showInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.stopSuccess:
|
||||
return showInfoBar(
|
||||
translations.stoppedServer(controllerName),
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.stopError:
|
||||
return showInfoBar(
|
||||
translations.stopServerError(
|
||||
event.error ?? translations.unknownError, controllerName),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.missingHostError:
|
||||
return showInfoBar(
|
||||
translations.missingHostNameError(controllerName),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.missingPortError:
|
||||
return showInfoBar(
|
||||
translations.missingPortError(controllerName),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.illegalPortError:
|
||||
return showInfoBar(
|
||||
translations.illegalPortError(controllerName),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.freeingPort:
|
||||
return showInfoBar(
|
||||
translations.freeingPort(defaultPort),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.freePortSuccess:
|
||||
return showInfoBar(
|
||||
translations.freedPort(defaultPort),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
case ServerResultType.freePortError:
|
||||
return showInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError, controllerName),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.pingingRemote:
|
||||
return showInfoBar(
|
||||
translations.pingingRemoteServer(controllerName),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingingLocal:
|
||||
return showInfoBar(
|
||||
translations.pingingLocalServer(controllerName, type().name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingError:
|
||||
return showInfoBar(
|
||||
translations.pingError(controllerName, type().name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Semaphore _publishingSemaphore = Semaphore();
|
||||
@@ -158,7 +141,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
|
||||
}
|
||||
|
||||
Future<void> joinServer(String uuid, Map<String, dynamic> entry) async {
|
||||
var id = entry["id"];
|
||||
final id = entry["id"];
|
||||
if(uuid == id) {
|
||||
showInfoBar(
|
||||
translations.joinSelfServer,
|
||||
@@ -168,13 +151,13 @@ extension MatchmakerControllerExtension on MatchmakerController {
|
||||
return;
|
||||
}
|
||||
|
||||
var hashedPassword = entry["password"];
|
||||
var hasPassword = hashedPassword != null;
|
||||
var embedded = type.value == ServerType.embedded;
|
||||
var author = entry["author"];
|
||||
var encryptedIp = entry["ip"];
|
||||
final hashedPassword = entry["password"];
|
||||
final hasPassword = hashedPassword != null;
|
||||
final embedded = type.value == ServerType.embedded;
|
||||
final author = entry["author"];
|
||||
final encryptedIp = entry["ip"];
|
||||
if(!hasPassword) {
|
||||
var valid = await _isServerValid(encryptedIp);
|
||||
final valid = await _isServerValid(encryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
@@ -183,7 +166,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmPassword = await _askForPassword();
|
||||
final confirmPassword = await _askForPassword();
|
||||
if(confirmPassword == null) {
|
||||
return;
|
||||
}
|
||||
@@ -197,8 +180,8 @@ extension MatchmakerControllerExtension on MatchmakerController {
|
||||
return;
|
||||
}
|
||||
|
||||
var decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
||||
var valid = await _isServerValid(decryptedIp);
|
||||
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
||||
final valid = await _isServerValid(decryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
@@ -207,7 +190,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
|
||||
}
|
||||
|
||||
Future<bool> _isServerValid(String address) async {
|
||||
var result = await pingGameServer(address);
|
||||
final result = await pingGameServer(address);
|
||||
if(result) {
|
||||
return true;
|
||||
}
|
||||
@@ -304,9 +287,9 @@ extension HostingControllerExtension on HostingController {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
var supabase = Supabase.instance.client;
|
||||
var hosts = supabase.from("hosting");
|
||||
var payload = {
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting");
|
||||
final payload = {
|
||||
'name': name.text,
|
||||
'description': description.text,
|
||||
'author': author,
|
||||
|
||||
@@ -2,7 +2,7 @@ enum RebootPageType {
|
||||
play,
|
||||
host,
|
||||
browser,
|
||||
authenticator,
|
||||
backend,
|
||||
matchmaker,
|
||||
info,
|
||||
settings
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
||||
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/src/controller/authenticator_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/server_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/implementation/data.dart';
|
||||
|
||||
class AuthenticatorPage extends RebootPage {
|
||||
const AuthenticatorPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.authenticatorName;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/authenticator.png";
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.authenticator;
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => pageName == null;
|
||||
|
||||
@override
|
||||
RebootPageState<AuthenticatorPage> createState() => _AuthenticatorPageState();
|
||||
}
|
||||
|
||||
class _AuthenticatorPageState extends RebootPageState<AuthenticatorPage> {
|
||||
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_type,
|
||||
_hostName,
|
||||
_port,
|
||||
_detached,
|
||||
_installationDirectory,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
Widget get _hostName => Obx(() {
|
||||
if(_authenticatorController.type.value != ServerType.remote) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.authenticatorConfigurationHostName),
|
||||
subtitle: Text(translations.authenticatorConfigurationHostDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.authenticatorConfigurationHostName,
|
||||
controller: _authenticatorController.host
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _port => Obx(() {
|
||||
if(_authenticatorController.type.value == ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
fluentUi.FluentIcons.number_field
|
||||
),
|
||||
title: Text(translations.authenticatorConfigurationPortName),
|
||||
subtitle: Text(translations.authenticatorConfigurationPortDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.authenticatorConfigurationPortName,
|
||||
controller: _authenticatorController.port,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _detached => Obx(() {
|
||||
if(_authenticatorController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.developer_board_24_regular
|
||||
),
|
||||
title: Text(translations.authenticatorConfigurationDetachedName),
|
||||
subtitle: Text(translations.authenticatorConfigurationDetachedDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_authenticatorController.detached.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _authenticatorController.detached(),
|
||||
onChanged: (value) => _authenticatorController.detached.value = value
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.authenticatorResetDefaultsName),
|
||||
subtitle: Text(translations.authenticatorResetDefaultsDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_authenticatorController.reset),
|
||||
child: Text(translations.authenticatorResetDefaultsContent),
|
||||
)
|
||||
);
|
||||
|
||||
Widget get _installationDirectory => Obx(() {
|
||||
if(_authenticatorController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
),
|
||||
title: Text(translations.authenticatorInstallationDirectoryName),
|
||||
subtitle: Text(translations.authenticatorInstallationDirectoryDescription),
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(authenticatorDirectory.uri),
|
||||
child: Text(translations.authenticatorInstallationDirectoryContent)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _type => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.password_24_regular
|
||||
),
|
||||
title: Text(translations.authenticatorTypeName),
|
||||
subtitle: Text(translations.authenticatorTypeDescription),
|
||||
content: const ServerTypeSelector(
|
||||
authenticator: true
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
Widget get button => const ServerButton(
|
||||
authenticator: true
|
||||
);
|
||||
}
|
||||
214
gui/lib/src/page/implementation/backend_page.dart
Normal file
214
gui/lib/src/page/implementation/backend_page.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
||||
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/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/server_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/implementation/data.dart';
|
||||
|
||||
class BackendPage extends RebootPage {
|
||||
const BackendPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.backendName;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/backend.png";
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.backend;
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => pageName == null;
|
||||
|
||||
@override
|
||||
RebootPageState<BackendPage> createState() => _BackendPageState();
|
||||
}
|
||||
|
||||
class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
InfoBarEntry? _infoBarEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
|
||||
if(_infoBarEntry == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(keyEvent.physicalKey.isUnrealEngineKey) {
|
||||
_gameController.consoleKey.value = keyEvent.physicalKey;
|
||||
}
|
||||
|
||||
_infoBarEntry?.close();
|
||||
_infoBarEntry = null;
|
||||
return true;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_type,
|
||||
_hostName,
|
||||
_port,
|
||||
_detached,
|
||||
_unrealEngineConsoleKey,
|
||||
_installationDirectory,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
Widget get _hostName => Obx(() {
|
||||
if(_backendController.type.value != ServerType.remote) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.backendConfigurationHostName),
|
||||
subtitle: Text(translations.backendConfigurationHostDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.backendConfigurationHostName,
|
||||
controller: _backendController.host
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _port => Obx(() {
|
||||
if(_backendController.type.value == ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
fluentUi.FluentIcons.number_field
|
||||
),
|
||||
title: Text(translations.backendConfigurationPortName),
|
||||
subtitle: Text(translations.backendConfigurationPortDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.backendConfigurationPortName,
|
||||
controller: _backendController.port,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _detached => Obx(() {
|
||||
if(_backendController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.developer_board_24_regular
|
||||
),
|
||||
title: Text(translations.backendConfigurationDetachedName),
|
||||
subtitle: Text(translations.backendConfigurationDetachedDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_backendController.detached.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _unrealEngineConsoleKey => Obx(() {
|
||||
if(_backendController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.key_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientConsoleKeyName),
|
||||
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
||||
contentWidth: null,
|
||||
content: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.backendResetDefaultsName),
|
||||
subtitle: Text(translations.backendResetDefaultsDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_backendController.reset),
|
||||
child: Text(translations.backendResetDefaultsContent),
|
||||
)
|
||||
);
|
||||
|
||||
Widget get _installationDirectory => Obx(() {
|
||||
if(_backendController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
),
|
||||
title: Text(translations.backendInstallationDirectoryName),
|
||||
subtitle: Text(translations.backendInstallationDirectoryDescription),
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(backendDirectory.uri),
|
||||
child: Text(translations.backendInstallationDirectoryContent)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _type => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.password_24_regular
|
||||
),
|
||||
title: Text(translations.backendTypeName),
|
||||
subtitle: Text(translations.backendTypeDescription),
|
||||
content: const ServerTypeSelector(
|
||||
backend: true
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
Widget get button => const ServerButton(
|
||||
backend: true
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
@@ -10,7 +11,6 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
||||
@@ -18,6 +18,7 @@ import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||
import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@@ -45,13 +46,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
var lastValue = pageIndex.value;
|
||||
pageIndex.listen((value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
restoreMessage(value, lastValue);
|
||||
lastValue = value;
|
||||
});
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_updateController.update();
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
@@ -61,6 +55,11 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
exit(0); // Force closing
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchFocusNode.dispose();
|
||||
@@ -80,6 +79,46 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
_focused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowDocked() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMinimize() {
|
||||
_focused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMove() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowRestore() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUndocked() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUnmaximize() {
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResized() {
|
||||
_settingsController.saveWindowSize(appWindow.size);
|
||||
@@ -140,7 +179,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(),
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
@@ -190,43 +229,47 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
onVerticalDragStart: (_) => appWindow.startDragging()
|
||||
);
|
||||
|
||||
Widget get _autoSuggestBox => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
Widget get _autoSuggestBox => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
enabled: !firstRun,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
|
||||
final pageIcon = SizedBox.square(
|
||||
@@ -369,32 +412,40 @@ class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
widget.controller.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: widget.body ?? const SizedBox.shrink()
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
widget.controller.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: widget.body ?? const SizedBox.shrink()
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -408,6 +459,9 @@ class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin
|
||||
}
|
||||
|
||||
class _CustomPane extends NavigationPaneWidget {
|
||||
final SettingsController settingsController;
|
||||
_CustomPane(this.settingsController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, NavigationPaneWidgetData data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -444,33 +498,36 @@ class _CustomPane extends NavigationPaneWidget {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = data.pane.items[index] as PaneItem;
|
||||
return HoverButton(
|
||||
onPressed: () => data.pane.onChanged?.call(index),
|
||||
builder: (context, states) => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
return Obx(() {
|
||||
final firstRun = settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
onPressed: firstRun ? null : () => data.pane.onChanged?.call(index),
|
||||
builder: (context, states) => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
data.pane.indicator ?? const SizedBox.shrink(),
|
||||
item.icon,
|
||||
const SizedBox(width: 12.0),
|
||||
item.title ?? const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
data.pane.indicator ?? const SizedBox.shrink(),
|
||||
item.icon,
|
||||
const SizedBox(width: 12.0),
|
||||
item.title ?? const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/util/tutorial.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
|
||||
class InfoPage extends RebootPage {
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
@@ -27,62 +29,193 @@ class InfoPage extends RebootPage {
|
||||
}
|
||||
|
||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
_documentation,
|
||||
_discord,
|
||||
_youtubeTutorial,
|
||||
_reportBug
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
RxInt _counter = RxInt(180);
|
||||
|
||||
static final List<InfoTile> _infoTiles = [
|
||||
InfoTile(
|
||||
title: Text("What is Project Reboot?"),
|
||||
content: Text(
|
||||
"Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.\n"
|
||||
"The project was started on Discord by Milxnor, while the launcher is developed by Auties00.\n"
|
||||
"Both are open source on GitHub, anyone can easily contribute or audit the code!"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("What is a Fortnite game server?"),
|
||||
content: Text(
|
||||
"If you have ever played Minecraft multiplayer, you might know that the servers you join are hosted on a computer running a program, called Minecraft Game Server.\n"
|
||||
"While the Minecraft Game server is written by the creators of Minecraft, Mojang, Epic Games doesn't provide an equivalent for Fortnite.\n"
|
||||
"By exploiting the Fortnite internals, though, it's possible to create a game server just like in Minecraft: this is in easy terms what Project Reboot does.\n"
|
||||
"Some Fortnite versions support running this game server in the background without rendering the game(\"headless\"), while others still require the full game to be open.\n"
|
||||
"Just like in Minecraft, you need a game client to play the game and one to host the server.\n"
|
||||
"By default, a game server is automatically started on your PC when you start a Fortnite version from the \"Play\" section in the launcher.\n"
|
||||
"If you want to play in another way, for example by joining a server hosted by one of your friends instead of running one yourself, you can checkout the \"Multiplayer\" section in the \"Play\" tab of the launcher."
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("Types of Fortnite game server"),
|
||||
content: Text(
|
||||
"Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called \"headless\" as the game is running, but you can't see it on your screen.\n"
|
||||
"If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the \"Configuration\" section in the \"Host\" tab of the launcher, you will see an instance of Fortnite open on your screen.\n"
|
||||
"For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the \"Configuration\" section in the \"Host\" tab of the launcher."
|
||||
"Just like in Minecraft, you need a game client to play the game and one to host the server."
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("How can others join my game server?"),
|
||||
content: Text(
|
||||
"For others to join your game server, port 7777 must be accessible on your PC.\n"
|
||||
"One option is to use a private VPN service like Hamachi or Radmin, but all of the players will need to download this software.\n"
|
||||
"The best solution is to use port forwarding:\n"
|
||||
"1. Set a static IP\n"
|
||||
" If you don't have already a static IP set, set one by following any tutorial on Google\n"
|
||||
"2. Log into your router's admin panel\n"
|
||||
" Usually this can be accessed on any web browser by going to http://192.168.1.1/\n"
|
||||
" You might need a username and a password to log in: refer to your router's manual for precise instructions\n"
|
||||
"3. Find the port forwarding section\n"
|
||||
" Once logged in into the admin panel, navigate to the port forwarding section of your router's settings\n"
|
||||
" This location may vary from router to router, but it's typically labelled as \"Port Forwarding,\" \"Port Mapping\" or \"Virtual Server\"\n"
|
||||
" Refer to your router's manual for precise instructions\n"
|
||||
"4. Add a port forwarding rule\n"
|
||||
" Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify:\n"
|
||||
" - Service Name: Choose a name for your port forwarding rule (e.g., \"Fortnite Game Server\")\n"
|
||||
" - Port Number: Enter 7777 for both the external and internal ports\n"
|
||||
" - Protocol: Select the UDP protocol\n"
|
||||
" - Internal IP Address: Enter the static IP address you set earlier\n"
|
||||
" - Enable: Make sure the port forwarding rule is enabled\n"
|
||||
"5. Save and apply the changes\n"
|
||||
" After configuring the port forwarding rule, save your changes and apply them\n"
|
||||
" This step may involve clicking a \"Save\" or \"Apply\" button on your router's web interface"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("What is a backend?"),
|
||||
content: Text(
|
||||
"A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.\n"
|
||||
"By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.\n"
|
||||
"If you are having any problems with the built in backend, enable the \"Detached\" option in the \"Backend\" tab of the Reboot Laucher to troubleshoot the issue."
|
||||
"LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend.\n"
|
||||
"Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user.\n"
|
||||
"You can run these alternatives either either on your PC or on a server by selecting respectively \"Local\" or \"Remote\" from the \"Type\" section in the \"Backend\" tab of the Reboot Launcher."
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("What is the Unreal Engine console?"),
|
||||
content: Text(
|
||||
"Many Fortnite versions don't support entering in game by clicking the \"Play\" button.\n"
|
||||
"Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1\n"
|
||||
"Keep in mind that the Unreal Engine console key is controlled by the backend, so this is true only if you are using the embedded backend: custom backends might use different keys.\n"
|
||||
"When using the embedded backend, you can customize the key used to open the console in the \"Backend\" tab of the Reboot Launcher."
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("What is a matchmaker?"),
|
||||
content: Text(
|
||||
"A matchmaker is a piece of software that emulates the Epic Games server responsible for putting you in game when you click the \"Play\" button in Fortnite.\n"
|
||||
"By default, the Reboot Launcher ships with a slightly customized version of Lawin's FortMatchmaker, an open source implementation available on Github.\n"
|
||||
"If you are having any problems with the built in matchmaker, enable the \"Detached\" option in the \"Matchmaker\" tab of the Reboot Launcher to troubleshoot the issue.\n"
|
||||
"Lawin's FortMatchmaker is an extremely basic implementation of a matchmaker: it takes the IP you configured in the \"Matchmaker\" tab, by default 127.0.0.1(your local machine) of the Reboot Launcher and send you with no wait to that game server.\n"
|
||||
"Unfortunately right now the play button still doesn't work on many Fortnite versions, you so might need to use the Unreal Engine console.\n"
|
||||
"Just like a backend, you can run a custom matchmaker, either on your PC or on a server with the appropriate configuration."
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("The backend is not working correctly"),
|
||||
content: Text(
|
||||
"To resolve this issue:\n"
|
||||
"- Check that your backend is working correctly from the \"Backend\" tab\n"
|
||||
"- If you are using a custom backend, try to use the embedded one\n"
|
||||
"- Try to run the backend as detached by enabling the \"Detached\" option in the \"Backend\" tab"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("The matchmaker is not working correctly"),
|
||||
content: Text(
|
||||
"To resolve this issue:\n"
|
||||
"- Check that your matchmaker is working correctly from the \"Matchmaker\" tab\n"
|
||||
"- If you are using a custom matchmaker, try to use the embedded one\n"
|
||||
"- Try to run the matchmaker as detached by enabling the \"Detached\" option in the \"Matchmaker\" tab"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("Why do I see two Fortnite versions opened on my PC?"),
|
||||
content: Text(
|
||||
"As explained in the \"What is a Fortnite game server?\" section, one instance of Fortnite is used to host the game server, while the other is used to let you play.\n"
|
||||
"The Fortnite instance used up by the game server is usually frozen, so it should be hard to use the wrong one to try to play.\n"
|
||||
"If you do not want to host a game server yourself, you can:\n"
|
||||
"1. Set a custom IP in the \"Matchmaker\" tab\n"
|
||||
"2. Set a custom matchmaker in the \"Matchmaker\" tab\n"
|
||||
"3. Disable the automatic game server from the \"Configuration\" section in the \"Host\" tab\n"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("I cannot open Fortnite because of an authentication error"),
|
||||
content: Text(
|
||||
"To resolve this issue:\n"
|
||||
"- Check that your backend is working correctly from the \"Backend\" tab\n"
|
||||
"- If you are using a custom backend, try to use the embedded one\n"
|
||||
"- Try to run the backend as detached by enabling the \"Detached\" option in the \"Backend\" tab"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("I cannot enter in a match when I'm in Fortnite"),
|
||||
content: Text(
|
||||
"As explained in the \"What is the Unreal Engine console?\" section, the \"Play\" button doesn't work in many Fortnite versions.\n"
|
||||
"Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("An error occurred while downloading a build (DioException)"),
|
||||
content: Text(
|
||||
"Unfortunately the servers that host the Fortnite builds are not reliable all the time so it might take a few tries, or downloading another version, to get started"
|
||||
)
|
||||
),
|
||||
InfoTile(
|
||||
title: Text("Failed to open descriptor file / Fortnite crash Reporter / Unreal Engine crash reporter"),
|
||||
content: Text(
|
||||
"Your version of Fortnite is corrupted, download it again from the launcher or use another build."
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
SettingTile get _reportBug => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.bug_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsBugReportName),
|
||||
subtitle: Text(translations.settingsUtilsBugReportSubtitle) ,
|
||||
content: Button(
|
||||
onPressed: openBugReport,
|
||||
child: Text(translations.settingsUtilsBugReportContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _youtubeTutorial => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.video_24_regular
|
||||
),
|
||||
title: Text(translations.infoVideoName),
|
||||
subtitle: Text(translations.infoVideoDescription),
|
||||
content: Button(
|
||||
onPressed: openYoutubeTutorial,
|
||||
child: Text(translations.infoVideoContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _discord => SettingTile(
|
||||
icon: Icon(
|
||||
Icons.discord_outlined
|
||||
),
|
||||
title: Text(translations.infoDiscordName),
|
||||
subtitle: Text(translations.infoDiscordDescription),
|
||||
content: Button(
|
||||
onPressed: openDiscordServer,
|
||||
child: Text(translations.infoDiscordContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _documentation => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.document_24_regular
|
||||
),
|
||||
title: Text(translations.infoDocumentationName),
|
||||
subtitle: Text(translations.infoDocumentationDescription),
|
||||
content: Button(
|
||||
onPressed: openTutorials,
|
||||
child: Text(translations.infoDocumentationContent)
|
||||
)
|
||||
);
|
||||
@override
|
||||
void initState() {
|
||||
if(_settingsController.firstRun.value) {
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_counter.value <= 0) {
|
||||
_settingsController.firstRun.value = false;
|
||||
timer.cancel();
|
||||
} else {
|
||||
_counter.value = _counter.value - 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
List<Widget> get settings => _infoTiles;
|
||||
|
||||
@override
|
||||
Widget? get button => Obx(() {
|
||||
if(!_settingsController.firstRun.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final totalSecondsLeft = _counter.value;
|
||||
final minutesLeft = totalSecondsLeft ~/ 60;
|
||||
final secondsLeft = totalSecondsLeft % 60;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: totalSecondsLeft <= 0 ? () => pageIndex.value = RebootPageType.play.index : null,
|
||||
child: Text(
|
||||
totalSecondsLeft <= 0 ? "I have read the instructions"
|
||||
: "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}"
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
|
||||
|
||||
@override
|
||||
Widget? get button => const ServerButton(
|
||||
authenticator: false
|
||||
backend: false
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -47,40 +47,10 @@ class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
|
||||
_hostName,
|
||||
_port,
|
||||
_gameServerAddress,
|
||||
_detached,
|
||||
_installationDirectory,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
Widget get _detached => Obx(() {
|
||||
if(_matchmakerController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.developer_board_24_regular
|
||||
),
|
||||
title: Text(translations.matchmakerConfigurationDetachedName),
|
||||
subtitle: Text(translations.matchmakerConfigurationDetachedDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_matchmakerController.detached.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _matchmakerController.detached.value,
|
||||
onChanged: (value) => _matchmakerController.detached.value = value
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _gameServerAddress => Obx(() {
|
||||
if(_matchmakerController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -123,7 +93,7 @@ class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
|
||||
});
|
||||
|
||||
Widget get _hostName => Obx(() {
|
||||
if(_matchmakerController.type.value == ServerType.remote) {
|
||||
if(_matchmakerController.type.value != ServerType.remote) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -147,7 +117,7 @@ class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
|
||||
title: Text(translations.matchmakerTypeName),
|
||||
subtitle: Text(translations.matchmakerTypeDescription),
|
||||
content: const ServerTypeSelector(
|
||||
authenticator: false
|
||||
backend: false
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
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/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
@@ -34,20 +40,9 @@ class PlayPage extends RebootPage {
|
||||
|
||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
late final RxBool _selfServer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_selfServer = RxBool(_isLocalPlay);
|
||||
_matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay);
|
||||
_hostingController.started.listen((_) => _selfServer.value = _isLocalPlay);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text)
|
||||
&& !_hostingController.started.value;
|
||||
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
@override
|
||||
Widget? get button => LaunchButton(
|
||||
startLabel: translations.launchFortnite,
|
||||
@@ -57,12 +52,60 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
_clientSettings,
|
||||
versionSelectSettingTile,
|
||||
_hostSettingTile,
|
||||
_browseServerTile,
|
||||
_matchmakerTile
|
||||
_multiplayer
|
||||
];
|
||||
|
||||
SettingTile get _multiplayer => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.people_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerName),
|
||||
subtitle: Text(translations.playGameServerDescription),
|
||||
children: [
|
||||
_hostSettingTile,
|
||||
_browseServerTile,
|
||||
_matchmakerTile,
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _clientSettings => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientName),
|
||||
subtitle: Text(translations.settingsClientDescription),
|
||||
children: [
|
||||
createFileSetting(
|
||||
title: translations.settingsClientConsoleName,
|
||||
description: translations.settingsClientConsoleDescription,
|
||||
controller: _settingsController.unrealEngineConsoleDll
|
||||
),
|
||||
createFileSetting(
|
||||
title: translations.settingsClientAuthName,
|
||||
description: translations.settingsClientAuthDescription,
|
||||
controller: _settingsController.backendDll
|
||||
),
|
||||
createFileSetting(
|
||||
title: translations.settingsClientMemoryName,
|
||||
description: translations.settingsClientMemoryDescription,
|
||||
controller: _settingsController.memoryLeakDll
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientArgsName),
|
||||
subtitle: Text(translations.settingsClientArgsDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsClientArgsPlaceholder,
|
||||
controller: _gameController.customLaunchArgs,
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _matchmakerTile => SettingTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = RebootPageType.matchmaker.index;
|
||||
|
||||
@@ -87,14 +87,8 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
);
|
||||
|
||||
Widget _buildPopulatedListBody(Set<Map<String, dynamic>> items) => ListView.builder(
|
||||
itemCount: items.length * 2,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
if(index % 2 != 0) {
|
||||
return const SizedBox(
|
||||
height: 8.0
|
||||
);
|
||||
}
|
||||
|
||||
var entry = items.elementAt(index ~/ 2);
|
||||
var hasPassword = entry["password"] != null;
|
||||
return SettingTile(
|
||||
|
||||
@@ -2,19 +2,27 @@ import 'package:clipboard/clipboard.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/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/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
||||
|
||||
import '../../util/checks.dart';
|
||||
|
||||
class HostPage extends RebootPage {
|
||||
const HostPage({Key? key}) : super(key: key);
|
||||
@@ -38,6 +46,8 @@ class HostPage extends RebootPage {
|
||||
class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||
|
||||
@@ -61,9 +71,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_gameServer,
|
||||
_information,
|
||||
_configuration,
|
||||
versionSelectSettingTile,
|
||||
_headless,
|
||||
_share,
|
||||
_resetDefaults
|
||||
];
|
||||
@@ -80,7 +90,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _gameServer => SettingTile(
|
||||
SettingTile get _information => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.info_24_regular
|
||||
),
|
||||
@@ -169,28 +179,108 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
]
|
||||
);
|
||||
|
||||
Widget get _headless => Obx(() => SettingTile(
|
||||
SettingTile get _configuration => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.window_console_20_regular
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.hostHeadlessName),
|
||||
subtitle: Text(translations.hostHeadlessDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_hostingController.headless.value ? translations.on : translations.off
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
fluentUi.FluentIcons.number_field
|
||||
),
|
||||
title: Text(translations.settingsServerPortName),
|
||||
subtitle: Text(translations.settingsServerPortDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerPortName,
|
||||
controller: _settingsController.gameServerPort,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
]
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _updateController.url,
|
||||
validator: checkUpdateUrl
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
_updateController.infoBarEntry?.close();
|
||||
_updateController.update(true);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.window_console_20_regular
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
title: Text(translations.hostHeadlessName),
|
||||
subtitle: Text(translations.hostHeadlessDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_hostingController.headless.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.headless.value,
|
||||
onChanged: (value) => _hostingController.headless.value = value
|
||||
),
|
||||
],
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.headless.value,
|
||||
onChanged: (value) => _hostingController.headless.value = value
|
||||
)),
|
||||
Obx(() => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_edit_24_regular
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Text(translations.hostVirtualDesktopName),
|
||||
subtitle: Text(translations.hostVirtualDesktopDescription),
|
||||
contentWidth: null,
|
||||
content: Row(
|
||||
children: [
|
||||
Text(
|
||||
_hostingController.virtualDesktop.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.virtualDesktop.value,
|
||||
onChanged: (value) => _hostingController.virtualDesktop.value = value
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _share => SettingTile(
|
||||
@@ -222,12 +312,15 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
subtitle: Text(translations.hostShareIpDescription),
|
||||
content: Button(
|
||||
onPressed: () async {
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
_showCopyingIp();
|
||||
var ip = await Ipify.ipv4();
|
||||
entry = _showCopyingIp();
|
||||
final ip = await Ipify.ipv4();
|
||||
entry.close();
|
||||
FlutterClipboard.controlC(ip);
|
||||
_showCopiedIp();
|
||||
}catch(error) {
|
||||
entry?.close();
|
||||
_showCannotCopyIp(error);
|
||||
}
|
||||
},
|
||||
@@ -257,7 +350,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
void _showCopyingIp() => showInfoBar(
|
||||
InfoBarEntry _showCopyingIp() => showInfoBar(
|
||||
translations.hostShareIpMessageLoading,
|
||||
loading: true,
|
||||
duration: null
|
||||
@@ -279,4 +372,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
String get text {
|
||||
if (this == UpdateTimer.never) {
|
||||
return translations.updateGameServerDllNever;
|
||||
}
|
||||
|
||||
return translations.updateGameServerDllEvery(name);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
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:flutter/foundation.dart';
|
||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/picker.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@@ -40,19 +38,52 @@ class SettingsPage extends RebootPage {
|
||||
}
|
||||
|
||||
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_clientSettings,
|
||||
_gameServerSettings,
|
||||
_launcherSettings,
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
),
|
||||
_installationDirectory
|
||||
];
|
||||
|
||||
@@ -68,173 +99,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _clientSettings => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientName),
|
||||
subtitle: Text(translations.settingsClientDescription),
|
||||
children: [
|
||||
_createFileSetting(
|
||||
title: translations.settingsClientConsoleName,
|
||||
description: translations.settingsClientConsoleDescription,
|
||||
controller: _settingsController.unrealEngineConsoleDll
|
||||
),
|
||||
_createFileSetting(
|
||||
title: translations.settingsClientAuthName,
|
||||
description: translations.settingsClientAuthDescription,
|
||||
controller: _settingsController.authenticatorDll
|
||||
),
|
||||
_createFileSetting(
|
||||
title: translations.settingsClientMemoryName,
|
||||
description: translations.settingsClientMemoryDescription,
|
||||
controller: _settingsController.memoryLeakDll
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.text_box_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsClientArgsName),
|
||||
subtitle: Text(translations.settingsClientArgsDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsClientArgsPlaceholder,
|
||||
controller: _gameController.customLaunchArgs,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _gameServerSettings => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.server_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
_createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
fluentUi.FluentIcons.number_field
|
||||
),
|
||||
title: Text(translations.settingsServerPortName),
|
||||
subtitle: Text(translations.settingsServerPortDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerPortName,
|
||||
controller: _settingsController.gameServerPort,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
]
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _updateController.url,
|
||||
validator: checkUpdateUrl
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
removeMessageByPage(6);
|
||||
_updateController.update(true);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.developer_board_24_regular
|
||||
),
|
||||
title: Text(translations.playAutomaticServerName),
|
||||
subtitle: Text(translations.playAutomaticServerDescription),
|
||||
contentWidth: null,
|
||||
content: Obx(() => Row(
|
||||
children: [
|
||||
Text(
|
||||
_hostingController.automaticServer.value ? translations.on : translations.off
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _hostingController.automaticServer.value,
|
||||
onChanged: (value) => _hostingController.automaticServer.value = value
|
||||
),
|
||||
],
|
||||
)),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _launcherSettings => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsName),
|
||||
subtitle: Text(translations.settingsUtilsSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
@@ -243,32 +107,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
SettingTile _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.document_24_regular
|
||||
),
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
content: FileSelector(
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
extension: "dll",
|
||||
folder: false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
String get text {
|
||||
if (this == UpdateTimer.never) {
|
||||
return translations.updateGameServerDllNever;
|
||||
}
|
||||
|
||||
return translations.updateGameServerDllEvery(name);
|
||||
}
|
||||
}
|
||||
|
||||
extension _ThemeModeExtension on ThemeMode {
|
||||
|
||||
@@ -2,15 +2,19 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/authenticator_page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/matchmaker_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||
|
||||
final StreamController<void> pagesController = StreamController.broadcast();
|
||||
bool hitBack = false;
|
||||
@@ -19,18 +23,24 @@ final List<RebootPage> pages = [
|
||||
const PlayPage(),
|
||||
const HostPage(),
|
||||
const BrowsePage(),
|
||||
const AuthenticatorPage(),
|
||||
const BackendPage(),
|
||||
const MatchmakerPage(),
|
||||
const InfoPage(),
|
||||
const SettingsPage()
|
||||
];
|
||||
|
||||
final RxInt pageIndex = RxInt(0);
|
||||
final RxInt pageIndex = _initialPageIndex;
|
||||
RxInt get _initialPageIndex {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
return RxInt(settingsController.firstRun.value ? RebootPageType.info.index : RebootPageType.play.index);
|
||||
}
|
||||
|
||||
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
||||
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
|
||||
|
||||
GlobalKey get pageKey => getPageKeyByIndex(pageIndex.value);
|
||||
|
||||
GlobalKey getPageKeyByIndex(int index) {
|
||||
@@ -72,4 +82,4 @@ void addSubPageToStack(String pageName) {
|
||||
appStack.add(identifier);
|
||||
_pagesStack[index]!.add(identifier);
|
||||
pagesController.add(null);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/controller/hosting_controller.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
final File _executable = File("${assetsDirectory.path}\\misc\\watch.exe");
|
||||
|
||||
extension GameInstanceWatcher on GameInstance {
|
||||
Future<void> startObserver() async {
|
||||
if(observerPid != null) {
|
||||
Process.killPid(observerPid!, ProcessSignal.sigabrt);
|
||||
}
|
||||
|
||||
final hostingController = Get.find<HostingController>();
|
||||
final gameController = Get.find<GameController>();
|
||||
watchProcess(gamePid).then((value) async {
|
||||
gameController.started.value = false;
|
||||
gameController.instance.value?.kill();
|
||||
if(_nestedHosting) {
|
||||
hostingController.started.value = false;
|
||||
hostingController.instance.value?.kill();
|
||||
await Supabase.instance.client.from("hosting")
|
||||
.delete()
|
||||
.match({'id': hostingController.uuid});
|
||||
}
|
||||
});
|
||||
|
||||
final process = await startProcess(
|
||||
executable: _executable,
|
||||
args: [
|
||||
hostingController.uuid,
|
||||
gamePid.toString(),
|
||||
launcherPid?.toString() ?? "-1",
|
||||
eacPid?.toString() ?? "-1",
|
||||
hosting.toString()
|
||||
],
|
||||
|
||||
);
|
||||
observerPid = process.pid;
|
||||
}
|
||||
|
||||
bool get _nestedHosting {
|
||||
GameInstance? child = this;
|
||||
while(child != null) {
|
||||
if(child.hosting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
child = child.child;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -9,33 +9,49 @@ import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
Future<void> downloadCriticalDllInteractive(String filePath) async {
|
||||
final Map<String, Future<void>> _operations = {};
|
||||
|
||||
Future<void> downloadCriticalDllInteractive(String filePath) {
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
return old;
|
||||
}
|
||||
|
||||
final newRun = _downloadCriticalDllInteractive(filePath);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<void> _downloadCriticalDllInteractive(String filePath) async {
|
||||
final fileName = path.basename(filePath).toLowerCase();
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
final fileName = path.basename(filePath).toLowerCase();
|
||||
if (fileName == "reboot.dll") {
|
||||
_updateController.update(true);
|
||||
await _updateController.update(true);
|
||||
return;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
|
||||
await showInfoBar(
|
||||
entry = showInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
await showInfoBar(
|
||||
entry.close();
|
||||
entry = await showInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}catch(message) {
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showInfoBar(
|
||||
translations.downloadDllError(error.toString()),
|
||||
translations.downloadDllError(fileName, error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
@@ -48,5 +64,7 @@ Future<void> downloadCriticalDllInteractive(String filePath) async {
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
202
gui/lib/src/util/keyboard.dart
Normal file
202
gui/lib/src/util/keyboard.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
extension UnrealEngineKeyboard on PhysicalKeyboardKey {
|
||||
static const Map<int, String> _keyboardKeyNames = <int, String>{
|
||||
0x0007003a: 'F1',
|
||||
0x0007003b: 'F2',
|
||||
0x0007003c: 'F3',
|
||||
0x0007003d: 'F4',
|
||||
0x0007003e: 'F5',
|
||||
0x0007003f: 'F6',
|
||||
0x00070040: 'F7',
|
||||
0x00070041: 'F8',
|
||||
0x00070042: 'F9',
|
||||
0x00070043: 'F10',
|
||||
0x00070044: 'F11',
|
||||
0x00070045: 'F12',
|
||||
|
||||
0x00070004: 'A',
|
||||
0x00070005: 'B',
|
||||
0x00070006: 'C',
|
||||
0x00070007: 'D',
|
||||
0x00070008: 'E',
|
||||
0x00070009: 'F',
|
||||
0x0007000a: 'G',
|
||||
0x0007000b: 'H',
|
||||
0x0007000c: 'I',
|
||||
0x0007000d: 'J',
|
||||
0x0007000e: 'K',
|
||||
0x0007000f: 'L',
|
||||
0x00070010: 'M',
|
||||
0x00070011: 'N',
|
||||
0x00070012: 'O',
|
||||
0x00070013: 'P',
|
||||
0x00070014: 'Q',
|
||||
0x00070015: 'R',
|
||||
0x00070016: 'S',
|
||||
0x00070017: 'T',
|
||||
0x00070018: 'U',
|
||||
0x00070019: 'V',
|
||||
0x0007001a: 'W',
|
||||
0x0007001b: 'X',
|
||||
0x0007001c: 'Y',
|
||||
0x0007001d: 'Z',
|
||||
|
||||
0x0007001e: 'one',
|
||||
0x0007001f: 'two',
|
||||
0x00070020: 'three',
|
||||
0x00070021: 'four',
|
||||
0x00070022: 'five',
|
||||
0x00070023: 'six',
|
||||
0x00070024: 'seven',
|
||||
0x00070025: 'eight',
|
||||
0x00070026: 'nine',
|
||||
0x00070027: 'zero',
|
||||
|
||||
0x00070028: 'Enter',
|
||||
0x00070029: 'Escape',
|
||||
0x0007002a: 'Backspace',
|
||||
0x0007002b: 'Tab',
|
||||
0x0007002c: 'SpaceBar',
|
||||
0x0007002d: 'Minus',
|
||||
0x0007002e: 'Equals',
|
||||
0x0007002f: 'LeftBracket',
|
||||
0x00070030: 'RightBracket',
|
||||
0x00070031: 'Backslash',
|
||||
0x00070033: 'Semicolon',
|
||||
0x00070034: 'Quote',
|
||||
0x00070036: 'Comma',
|
||||
0x00070037: 'Period',
|
||||
0x00070038: 'Slash',
|
||||
0x00070039: 'CapsLock',
|
||||
0x00070047: 'ScrollLock',
|
||||
0x00070048: 'Pause',
|
||||
0x00070049: 'Insert',
|
||||
0x0007004a: 'Home',
|
||||
0x0007004b: 'PageUp',
|
||||
0x0007004c: 'Delete',
|
||||
0x0007004d: 'End',
|
||||
0x0007004e: 'PageDown',
|
||||
0x00070053: 'NumLock',
|
||||
0x00070054: 'Divide',
|
||||
0x00070055: 'Multiply',
|
||||
0x00070056: 'Subtract',
|
||||
0x00070057: 'Add',
|
||||
0x00070058: 'Enter',
|
||||
0x00070059: 'NumPadOne',
|
||||
0x0007005a: 'NumPadTwo',
|
||||
0x0007005b: 'NumPadThree',
|
||||
0x0007005c: 'NumPadFour',
|
||||
0x0007005d: 'NumPadFive',
|
||||
0x0007005e: 'NumPadSix',
|
||||
0x0007005f: 'NumPadSeven',
|
||||
0x00070060: 'NumPadEight',
|
||||
0x00070061: 'NumPadNine',
|
||||
0x00070062: 'NumPadZero',
|
||||
0x00070063: 'Decimal',
|
||||
0x00070064: 'Backslash'
|
||||
};
|
||||
|
||||
static const Map<int, String> _keyboardKeyPrettyNames = <int, String>{
|
||||
0x0007003a: 'F1',
|
||||
0x0007003b: 'F2',
|
||||
0x0007003c: 'F3',
|
||||
0x0007003d: 'F4',
|
||||
0x0007003e: 'F5',
|
||||
0x0007003f: 'F6',
|
||||
0x00070040: 'F7',
|
||||
0x00070041: 'F8',
|
||||
0x00070042: 'F9',
|
||||
0x00070043: 'F10',
|
||||
0x00070044: 'F11',
|
||||
0x00070045: 'F12',
|
||||
|
||||
0x00070004: 'A',
|
||||
0x00070005: 'B',
|
||||
0x00070006: 'C',
|
||||
0x00070007: 'D',
|
||||
0x00070008: 'E',
|
||||
0x00070009: 'F',
|
||||
0x0007000a: 'G',
|
||||
0x0007000b: 'H',
|
||||
0x0007000c: 'I',
|
||||
0x0007000d: 'J',
|
||||
0x0007000e: 'K',
|
||||
0x0007000f: 'L',
|
||||
0x00070010: 'M',
|
||||
0x00070011: 'N',
|
||||
0x00070012: 'O',
|
||||
0x00070013: 'P',
|
||||
0x00070014: 'Q',
|
||||
0x00070015: 'R',
|
||||
0x00070016: 'S',
|
||||
0x00070017: 'T',
|
||||
0x00070018: 'U',
|
||||
0x00070019: 'V',
|
||||
0x0007001a: 'W',
|
||||
0x0007001b: 'X',
|
||||
0x0007001c: 'Y',
|
||||
0x0007001d: 'Z',
|
||||
|
||||
0x0007001e: '1',
|
||||
0x0007001f: '2',
|
||||
0x00070020: '3',
|
||||
0x00070021: '4',
|
||||
0x00070022: '5',
|
||||
0x00070023: '6',
|
||||
0x00070024: '7',
|
||||
0x00070025: '8',
|
||||
0x00070026: '9',
|
||||
0x00070027: '10',
|
||||
|
||||
0x00070028: 'ENTER',
|
||||
0x00070029: 'ESCAPE',
|
||||
0x0007002a: 'BACKSPACE',
|
||||
0x0007002b: 'TAB',
|
||||
0x0007002c: 'SPACEBAR',
|
||||
0x0007002d: 'MINUS',
|
||||
0x0007002e: 'EQUALS',
|
||||
0x0007002f: 'LEFTBRACKET',
|
||||
0x00070030: 'RIGHTBRACKET',
|
||||
0x00070031: 'BACKSLASH',
|
||||
0x00070033: 'SEMICOLON',
|
||||
0x00070034: 'QUOTE',
|
||||
0x00070036: 'COMMA',
|
||||
0x00070037: 'PERIOD',
|
||||
0x00070038: 'SLASH',
|
||||
0x00070039: 'CAPSLOCK',
|
||||
0x00070047: 'SCROLLLOCK',
|
||||
0x00070048: 'PAUSE',
|
||||
0x00070049: 'INSERT',
|
||||
0x0007004a: 'HOME',
|
||||
0x0007004b: 'PAGEUP',
|
||||
0x0007004c: 'DELETE',
|
||||
0x0007004d: 'END',
|
||||
0x0007004e: 'PAGEDOWN',
|
||||
0x00070053: 'NUMLOCK',
|
||||
0x00070054: 'DIVIDE',
|
||||
0x00070055: 'MULTIPLY',
|
||||
0x00070056: 'SUBTRACT',
|
||||
0x00070057: 'ADD',
|
||||
0x00070058: 'ENTER',
|
||||
0x00070059: '1',
|
||||
0x0007005a: '2',
|
||||
0x0007005b: '3',
|
||||
0x0007005c: '4',
|
||||
0x0007005d: '5',
|
||||
0x0007005e: '6',
|
||||
0x0007005f: '7',
|
||||
0x00070060: '8',
|
||||
0x00070061: '9',
|
||||
0x00070062: '0',
|
||||
0x00070063: 'DECIMAL',
|
||||
0x00070064: 'BACKSLASH'
|
||||
};
|
||||
|
||||
|
||||
String? get unrealEngineName => _keyboardKeyNames[this.usbHidUsage];
|
||||
|
||||
bool get isUnrealEngineKey => _keyboardKeyNames[this.usbHidUsage] != null;
|
||||
|
||||
String? get unrealEnginePrettyName => _keyboardKeyPrettyNames[this.usbHidUsage];
|
||||
}
|
||||
23
gui/lib/src/util/log.dart
Normal file
23
gui/lib/src/util/log.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final File _loggingFile = _createLoggingFile();
|
||||
final Semaphore _semaphore = Semaphore(1);
|
||||
|
||||
File _createLoggingFile() {
|
||||
final file = File("${logsDirectory.path}\\launcher.log");
|
||||
file.parent.createSync(recursive: true);
|
||||
if(file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
file.createSync();
|
||||
return file;
|
||||
}
|
||||
|
||||
void log(String message) async {
|
||||
await _semaphore.acquire();
|
||||
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
_semaphore.release();
|
||||
}
|
||||
@@ -6,22 +6,26 @@ import 'package:reboot_common/common.dart';
|
||||
const Duration _timeout = Duration(seconds: 2);
|
||||
|
||||
Future<bool> _pingGameServer(String hostname, int port) async {
|
||||
var socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
var dataToSend = utf8.encode(DateTime.now().toIso8601String());
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
await for (var event in socket) {
|
||||
switch(event) {
|
||||
case RawSocketEvent.read:
|
||||
return true;
|
||||
case RawSocketEvent.readClosed:
|
||||
case RawSocketEvent.closed:
|
||||
return false;
|
||||
case RawSocketEvent.write:
|
||||
break;
|
||||
RawDatagramSocket? socket;
|
||||
try {
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
final dataToSend = utf8.encode(DateTime.now().toIso8601String());
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
await for (var event in socket) {
|
||||
switch(event) {
|
||||
case RawSocketEvent.read:
|
||||
case RawSocketEvent.write:
|
||||
return true;
|
||||
case RawSocketEvent.readClosed:
|
||||
case RawSocketEvent.closed:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}finally {
|
||||
socket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> get _timeoutFuture => Future.delayed(_timeout).then((value) => false);
|
||||
|
||||
@@ -2,18 +2,399 @@ import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'dart:collection';
|
||||
import 'dart:ffi';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||
|
||||
bool get isWin11 {
|
||||
var result = _winBuildRegex.firstMatch(Platform.operatingSystemVersion)?.group(1);
|
||||
if(result == null){
|
||||
return false;
|
||||
int? get windowsBuild {
|
||||
final result = _winBuildRegex.firstMatch(Platform.operatingSystemVersion)?.group(1);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var intBuild = int.tryParse(result);
|
||||
return int.tryParse(result);
|
||||
}
|
||||
|
||||
bool get isWin11 {
|
||||
final intBuild = windowsBuild;
|
||||
return intBuild != null && intBuild > 22000;
|
||||
}
|
||||
|
||||
bool get isDarkMode
|
||||
=> SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
|
||||
bool get isDarkMode =>
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
|
||||
|
||||
class _ServiceProvider10 extends IUnknown {
|
||||
static const String _CLSID = "{C2F03A33-21F5-47FA-B4BB-156362A2F239}";
|
||||
static const String _IID = "{6D5140C1-7436-11CE-8034-00AA006009FA}";
|
||||
|
||||
_ServiceProvider10._internal(Pointer<COMObject> ptr) : super(ptr);
|
||||
|
||||
factory _ServiceProvider10.createInstance() =>
|
||||
_ServiceProvider10._internal(COMObject.createFromID(_CLSID, _IID));
|
||||
|
||||
Pointer<COMObject> queryService(String classId, String instanceId) {
|
||||
final result = calloc<COMObject>();
|
||||
final code = (ptr.ref.vtable + 3)
|
||||
.cast<
|
||||
Pointer<
|
||||
NativeFunction<
|
||||
HRESULT Function(Pointer, Pointer<GUID>, Pointer<GUID>,
|
||||
Pointer<COMObject>)>>>()
|
||||
.value
|
||||
.asFunction<
|
||||
int Function(Pointer, Pointer<GUID>, Pointer<GUID>,
|
||||
Pointer<COMObject>)>()(ptr.ref.lpVtbl,
|
||||
GUIDFromString(classId), GUIDFromString(instanceId), result);
|
||||
if (code != 0) {
|
||||
free(result);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
throw WindowsException(code);
|
||||
}
|
||||
|
||||
return IApplicationView._internal(result);
|
||||
}
|
||||
}
|
||||
|
||||
final class _Process extends Struct {
|
||||
@Uint32()
|
||||
external int pid;
|
||||
|
||||
@Uint32()
|
||||
external int HWnd;
|
||||
|
||||
static int _filter(int HWnd, int lParam) {
|
||||
final structure = Pointer.fromAddress(lParam).cast<_Process>();
|
||||
final pidPointer = calloc<Uint32>();
|
||||
GetWindowThreadProcessId(HWnd, pidPointer);
|
||||
final pid = pidPointer.value;
|
||||
free(pidPointer);
|
||||
if (pid != structure.ref.pid) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
structure.ref.HWnd = HWnd;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static int getHWndFromPid(int pid) {
|
||||
final result = calloc<_Process>();
|
||||
result.ref.pid = pid;
|
||||
EnumWindows(
|
||||
Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
|
||||
final HWnd = result.ref.HWnd;
|
||||
calloc.free(result);
|
||||
return HWnd;
|
||||
}
|
||||
}
|
||||
|
||||
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_APARTMENTTHREADED | 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();
|
||||
|
||||
void moveWindowToDesktop(int pid, IVirtualDesktop desktop) {
|
||||
final HWnd = _Process.getHWndFromPid(pid);
|
||||
final window = applicationViewCollection.getViewForHWnd(HWnd);
|
||||
windowManager.moveWindowToDesktop(window, desktop);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
Future<void> openYoutubeTutorial() => launchUrl(Uri.parse("https://www.youtube.com/watch?v=nrVE2RB0qa4"));
|
||||
|
||||
Future<void> openDiscordServer() => launchUrl(Uri.parse("https://discord.gg/reboot"));
|
||||
|
||||
Future<void> openTutorials() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale"));
|
||||
|
||||
Future<void> openPortTutorial() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"));
|
||||
|
||||
Future<void> openBugReport() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues"));
|
||||
22
gui/lib/src/widget/file_setting_tile.dart
Normal file
22
gui/lib/src/widget/file_setting_tile.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.document_24_regular
|
||||
),
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
content: FileSelector(
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
extension: "dll",
|
||||
folder: false
|
||||
)
|
||||
);
|
||||
@@ -4,25 +4,26 @@ import 'dart:io';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart' as messenger;
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/daemon.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/util/tutorial.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
final bool host;
|
||||
@@ -38,10 +39,13 @@ class LaunchButton extends StatefulWidget {
|
||||
class _LaunchButtonState extends State<LaunchButton> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
InfoBarEntry? _gameClientInfoBar;
|
||||
InfoBarEntry? _gameServerInfoBar;
|
||||
CancelableOperation? _operation;
|
||||
IVirtualDesktop? _virtualDesktop;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Align(
|
||||
@@ -51,7 +55,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
child: Obx(() => SizedBox(
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: () => _operation = CancelableOperation.fromFuture(_start()),
|
||||
onPressed: () => _operation = CancelableOperation.fromFuture(_toggle()),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(_hasStarted ? _stopMessage : _startMessage)
|
||||
@@ -69,8 +73,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
String get _stopMessage => widget.stopLabel ?? (widget.host ? translations.stopHosting : translations.stopGame);
|
||||
|
||||
Future<void> _start() async {
|
||||
Future<void> _toggle({bool forceGUI = false}) async {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
|
||||
if (_hasStarted) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(
|
||||
reason: _StopReason.normal
|
||||
);
|
||||
@@ -78,17 +84,24 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
if(_operation != null) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Already started, ignoring user action");
|
||||
return;
|
||||
}
|
||||
|
||||
if(_gameController.selectedVersion == null){
|
||||
final version = _gameController.selectedVersion;
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Version data: $version");
|
||||
if(version == null){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] No version selected");
|
||||
_onStop(
|
||||
reason: _StopReason.missingVersionError
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Setting started...");
|
||||
_setStarted(widget.host, true);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Set started");
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${_Injectable.values}");
|
||||
for (final injectable in _Injectable.values) {
|
||||
if(await _getDllFileOrStop(injectable, widget.host) == null) {
|
||||
return;
|
||||
@@ -96,9 +109,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
try {
|
||||
final version = _gameController.selectedVersion!;
|
||||
final executable = await version.executable;
|
||||
final executable = version.gameExecutable;
|
||||
if(executable == null){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] No executable found");
|
||||
_onStop(
|
||||
reason: _StopReason.missingExecutableError,
|
||||
error: version.location.path
|
||||
@@ -106,24 +119,38 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
final authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(false);
|
||||
if(!authenticatorResult){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
||||
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
|
||||
if(!backendResult){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||
_onStop(
|
||||
reason: _StopReason.authenticatorError
|
||||
reason: _StopReason.backendError
|
||||
);
|
||||
return;
|
||||
}
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Backend works");
|
||||
|
||||
final matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(false);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking matchmaker(port: ${_matchmakerController.type.value.name}, type: ${_matchmakerController.type.value.name})...");
|
||||
final matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive();
|
||||
if(!matchmakerResult){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start matchmaker");
|
||||
_onStop(
|
||||
reason: _StopReason.matchmakerError
|
||||
);
|
||||
return;
|
||||
}
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Matchmaker works");
|
||||
|
||||
final headless = !forceGUI && _hostingController.headless.value;
|
||||
final virtualDesktop = _hostingController.virtualDesktop.value;
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, headless, virtualDesktop, false);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
await _startGameProcesses(version, widget.host, headless, virtualDesktop, linkedHostingInstance);
|
||||
if(!widget.host) {
|
||||
_showLaunchingGameClientWidget();
|
||||
}
|
||||
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version);
|
||||
await _startGameProcesses(version, widget.host, linkedHostingInstance);
|
||||
if(linkedHostingInstance != null || widget.host){
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
@@ -136,40 +163,79 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version) async {
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||
if(widget.host){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||
return null;
|
||||
}
|
||||
|
||||
final matchmakingIp = _matchmakerController.gameServerAddress.text;
|
||||
if(!isLocalHost(matchmakingIp)) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The current IP($matchmakingIp) is not set to localhost");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_hostingController.started()){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!_hostingController.automaticServer()) {
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer();
|
||||
if(!response) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||
return null;
|
||||
}
|
||||
|
||||
final instance = await _startGameProcesses(version, true, null);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Starting implicit game server...");
|
||||
final instance = await _startGameProcesses(version, true, headless, virtualDesktop, null);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Started implicit game server...");
|
||||
_setStarted(true, true);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameInstance? linkedHosting) async {
|
||||
final launcherProcess = await _createPausedProcess(version.launcherExecutable);
|
||||
print("Created launcher process");
|
||||
final eacProcess = await _createPausedProcess(version.eacExecutable);
|
||||
print("Created eac process");
|
||||
final executable = await version.executable;
|
||||
final gameProcess = await _createGameProcess(executable!, host);
|
||||
Future<bool> _askForAutomaticGameServer() async {
|
||||
final result = await showAppDialog<bool>(
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. "
|
||||
"If you don't want to join another player's server, you should start a game server. This is necessary to be able to play: for more information check the Info tab in the launcher.",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Ignore"
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
text: "Start server",
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
)
|
||||
) ?? false;
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
|
||||
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
|
||||
|
||||
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 = host && headless ? await version.headlessGameExecutable : version.gameExecutable;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Using game path: ${executable?.path}");
|
||||
final gameProcess = await _createGameProcess(version, executable!, host, headless, virtualDesktop, linkedHosting);
|
||||
if(gameProcess == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||
final instance = GameInstance(
|
||||
versionName: version.name,
|
||||
gamePid: gameProcess,
|
||||
@@ -178,22 +244,24 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
hosting: host,
|
||||
child: linkedHosting
|
||||
);
|
||||
instance.startObserver();
|
||||
if(host){
|
||||
_hostingController.discardServer();
|
||||
_hostingController.instance.value = instance;
|
||||
}else{
|
||||
_gameController.instance.value = instance;
|
||||
}
|
||||
_injectOrShowError(_Injectable.sslBypass, host);
|
||||
await _injectOrShowError(_Injectable.sslBypassV2, host);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<int?> _createGameProcess(File executable, bool host) async {
|
||||
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async {
|
||||
if(!_hasStarted) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Discarding start game process request as the state is no longer started");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||
final gameArgs = createRebootArgs(
|
||||
_gameController.username.text,
|
||||
_gameController.password.text,
|
||||
@@ -201,40 +269,86 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_hostingController.headless.value,
|
||||
_gameController.customLaunchArgs.text
|
||||
);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generated game args: $gameArgs");
|
||||
final gameProcess = await startProcess(
|
||||
executable: executable,
|
||||
args: gameArgs,
|
||||
window: false
|
||||
wrapProcess: false,
|
||||
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||
);
|
||||
gameProcess.stdOutput.listen((line) => _onGameOutput(line, host, false));
|
||||
gameProcess.errorOutput.listen((line) => _onGameOutput(line, host, true));
|
||||
watchProcess(gameProcess.pid).then((_) => _onStop(reason: _StopReason.exitCode));
|
||||
gameProcess.stdOutput.listen((line) => _onGameOutput(line, host, virtualDesktop, false));
|
||||
gameProcess.stdError.listen((line) => _onGameOutput(line, host, virtualDesktop, true));
|
||||
watchProcess(gameProcess.pid).then((_) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance == null || !host || !headless || instance.launched) {
|
||||
_onStop(reason: _StopReason.exitCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if(widget.host) {
|
||||
await _onStop(reason: _StopReason.exitCode);
|
||||
_toggle(forceGUI: true);
|
||||
return;
|
||||
}
|
||||
|
||||
await _onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: true
|
||||
);
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, false, virtualDesktop, true);
|
||||
_gameController.instance.value?.child = linkedHostingInstance;
|
||||
if(linkedHostingInstance != null){
|
||||
_setStarted(true, true);
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
});
|
||||
if(host && !headless && virtualDesktop) {
|
||||
final name = version.name;
|
||||
final pid = gameProcess.pid;
|
||||
_moveProcessToVirtualDesktop(name, pid);
|
||||
}
|
||||
return gameProcess.pid;
|
||||
}
|
||||
|
||||
Future<int?> _createPausedProcess(File? file) async {
|
||||
Future<void> _moveProcessToVirtualDesktop(String versionName, int pid) async {
|
||||
try {
|
||||
final windowManager = VirtualDesktopManager.getInstance();
|
||||
_virtualDesktop = windowManager.createDesktop();
|
||||
windowManager.setDesktopName(_virtualDesktop!, "$versionName Server (Reboot Launcher)");
|
||||
Object? lastError;
|
||||
for(var i = 0; i < 10; i++) {
|
||||
try {
|
||||
windowManager.moveWindowToDesktop(pid, _virtualDesktop!);
|
||||
break;
|
||||
}catch(error) {
|
||||
lastError = error;
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
if(lastError != null) {
|
||||
log("[VIRTUAL_DESKTOP] Cannot move window: $lastError");
|
||||
}
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] Virtual desktop is not supported: $error");
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final process = await startProcess(
|
||||
executable: file,
|
||||
args: [],
|
||||
window: false,
|
||||
output: false
|
||||
wrapProcess: false,
|
||||
name: "${version.name}-${basenameWithoutExtension(file.path)}"
|
||||
);
|
||||
print("Started process: ${process.pid}");
|
||||
final pid = process.pid;
|
||||
suspend(pid);
|
||||
print("Suspended");
|
||||
return pid;
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool host, bool error) {
|
||||
if(kDebugMode) {
|
||||
print("${error ? '[ERROR]' : '[MESSAGE]'} $line");
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool host, bool virtualDesktop, bool error) async {
|
||||
if (line.contains(kShutdownLine)) {
|
||||
_onStop(
|
||||
reason: _StopReason.normal
|
||||
@@ -256,105 +370,154 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Region ")){
|
||||
if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
||||
await _injectOrShowError(_Injectable.memoryFix, host);
|
||||
if(!host){
|
||||
_injectOrShowError(_Injectable.console, host);
|
||||
await _injectOrShowError(_Injectable.console, host);
|
||||
_onGameClientInjected();
|
||||
}else {
|
||||
_injectOrShowError(_Injectable.reboot, host)
|
||||
.then((value) => _onGameServerInjected());
|
||||
await _injectOrShowError(_Injectable.reboot, host);
|
||||
_onGameServerInjected();
|
||||
}
|
||||
|
||||
_injectOrShowError(_Injectable.memoryFix, host);
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
instance?.launched = true;
|
||||
instance?.tokenError = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onGameServerInjected() async {
|
||||
final theme = FluentTheme.of(appKey.currentContext!);
|
||||
void _onGameClientInjected() {
|
||||
_gameClientInfoBar?.close();
|
||||
showInfoBar(
|
||||
translations.waitingForGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
final gameServerPort = _settingsController.gameServerPort.text;
|
||||
final localPingResult = await pingGameServer(
|
||||
"127.0.0.1:$gameServerPort",
|
||||
timeout: const Duration(minutes: 2)
|
||||
);
|
||||
if(!localPingResult) {
|
||||
showInfoBar(
|
||||
translations.gameServerStartWarning,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_matchmakerController.joinLocalHost();
|
||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||
if(!accessible) {
|
||||
showInfoBar(
|
||||
translations.gameServerStartLocalWarning,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _hostingController.publishServer(
|
||||
_gameController.username.text,
|
||||
_hostingController.instance.value!.versionName,
|
||||
);
|
||||
showInfoBar(
|
||||
translations.gameServerStarted,
|
||||
translations.gameClientStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||
showInfoBar(
|
||||
translations.checkingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
final publicIp = await Ipify.ipv4();
|
||||
final externalResult = await pingGameServer("$publicIp:$gameServerPort");
|
||||
if(externalResult) {
|
||||
return true;
|
||||
}
|
||||
Future<void> _onGameServerInjected() async {
|
||||
_gameServerInfoBar?.close();
|
||||
final theme = FluentTheme.of(appKey.currentContext!);
|
||||
try {
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
translations.waitingForGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
final gameServerPort = _settingsController.gameServerPort.text;
|
||||
final localPingResult = await pingGameServer(
|
||||
"127.0.0.1:$gameServerPort",
|
||||
timeout: const Duration(minutes: 2)
|
||||
);
|
||||
if (!localPingResult) {
|
||||
_gameServerInfoBar?.close();
|
||||
showInfoBar(
|
||||
translations.gameServerStartWarning,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final future = pingGameServer(
|
||||
"$publicIp:$gameServerPort",
|
||||
timeout: const Duration(days: 365)
|
||||
);
|
||||
showInfoBar(
|
||||
translations.checkGameServerFixMessage(gameServerPort),
|
||||
action: Button(
|
||||
onPressed: openPortTutorial,
|
||||
child: Text(translations.checkGameServerFixAction),
|
||||
),
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: null,
|
||||
loading: true
|
||||
);
|
||||
return await future;
|
||||
_matchmakerController.joinLocalHost();
|
||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||
if (!accessible) {
|
||||
showInfoBar(
|
||||
translations.gameServerStartLocalWarning,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _hostingController.publishServer(
|
||||
_gameController.username.text,
|
||||
_hostingController.instance.value!.versionName,
|
||||
);
|
||||
showInfoBar(
|
||||
translations.gameServerStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}finally {
|
||||
_gameServerInfoBar?.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||
host = host ?? widget.host;
|
||||
await _operation?.cancel();
|
||||
await _authenticatorController.worker?.cancel();
|
||||
await _matchmakerController.worker?.cancel();
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance != null){
|
||||
_onStop(
|
||||
reason: _StopReason.normal,
|
||||
host: true
|
||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||
try {
|
||||
_gameServerInfoBar?.close();
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
translations.checkingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
final publicIp = await Ipify.ipv4();
|
||||
final externalResult = await pingGameServer("$publicIp:$gameServerPort");
|
||||
if (externalResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_gameServerInfoBar?.close();
|
||||
final future = pingGameServer(
|
||||
"$publicIp:$gameServerPort",
|
||||
timeout: const Duration(days: 365)
|
||||
);
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
translations.checkGameServerFixMessage(gameServerPort),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
},
|
||||
child: Text(translations.checkGameServerFixAction),
|
||||
),
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: null,
|
||||
loading: true
|
||||
);
|
||||
return await future;
|
||||
}finally {
|
||||
_gameServerInfoBar?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||
if(_virtualDesktop != null) {
|
||||
try {
|
||||
final instance = VirtualDesktopManager.getInstance();
|
||||
instance.removeDesktop(_virtualDesktop!);
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] Cannot close virtual desktop: $error");
|
||||
}
|
||||
}
|
||||
|
||||
if(host == null) {
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
await _backendController.worker?.cancel();
|
||||
await _matchmakerController.worker?.cancel();
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
||||
if(host) {
|
||||
_hostingController.discardServer();
|
||||
}
|
||||
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance != null) {
|
||||
if(reason == _StopReason.normal) {
|
||||
instance.launched = true;
|
||||
}
|
||||
|
||||
instance.kill();
|
||||
final child = instance.child;
|
||||
if(child != null) {
|
||||
_onStop(
|
||||
reason: reason,
|
||||
host: child.hosting
|
||||
);
|
||||
}
|
||||
|
||||
if(host){
|
||||
_hostingController.instance.value = null;
|
||||
}else {
|
||||
@@ -363,16 +526,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
_setStarted(host, false);
|
||||
if(host){
|
||||
_hostingController.discardServer();
|
||||
}
|
||||
|
||||
if(reason == _StopReason.normal) {
|
||||
messenger.removeMessageByPage(_pageType.index);
|
||||
if(!reason.isError) {
|
||||
if(host) {
|
||||
_gameServerInfoBar?.close();
|
||||
}else {
|
||||
_gameClientInfoBar?.close();
|
||||
}
|
||||
}
|
||||
|
||||
switch(reason) {
|
||||
case _StopReason.authenticatorError:
|
||||
case _StopReason.backendError:
|
||||
case _StopReason.matchmakerError:
|
||||
case _StopReason.normal:
|
||||
break;
|
||||
@@ -430,24 +593,34 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
);
|
||||
break;
|
||||
}
|
||||
_operation = null;
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(_Injectable injectable, bool hosting) async {
|
||||
final instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if (instance == null) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] No instance found to inject ${injectable.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final gameProcess = instance.gamePid;
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
|
||||
final dllPath = await _getDllFileOrStop(injectable, hosting);
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
|
||||
if(dllPath == null) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedDllError,
|
||||
host: hosting
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
|
||||
await injectDll(gameProcess, dllPath.path);
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
|
||||
} catch (error, stackTrace) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Cannot inject ${injectable.name}: $error $stackTrace");
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedDllError,
|
||||
host: hosting,
|
||||
@@ -463,31 +636,40 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return _settingsController.gameServerDll.text;
|
||||
case _Injectable.console:
|
||||
return _settingsController.unrealEngineConsoleDll.text;
|
||||
case _Injectable.sslBypass:
|
||||
return _settingsController.authenticatorDll.text;
|
||||
case _Injectable.sslBypassV2:
|
||||
return _settingsController.backendDll.text;
|
||||
case _Injectable.memoryFix:
|
||||
return _settingsController.memoryLeakDll.text;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||
final path = _getDllPath(injectable);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
|
||||
final file = File(path);
|
||||
if(await file.exists()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||
return file;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
||||
await downloadCriticalDllInteractive(path);
|
||||
return null;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||
return _getDllFileOrStop(injectable, host);
|
||||
}
|
||||
|
||||
OverlayEntry _showLaunchingGameServerWidget() => showInfoBar(
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showInfoBar(
|
||||
translations.launchingHeadlessServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
RebootPageType get _pageType => widget.host ? RebootPageType.host : RebootPageType.play;
|
||||
InfoBarEntry _showLaunchingGameClientWidget() => _gameClientInfoBar = showInfoBar(
|
||||
translations.launchingGameClient,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
|
||||
enum _StopReason {
|
||||
@@ -496,15 +678,18 @@ enum _StopReason {
|
||||
missingExecutableError,
|
||||
corruptedVersionError,
|
||||
corruptedDllError,
|
||||
authenticatorError,
|
||||
backendError,
|
||||
matchmakerError,
|
||||
tokenError,
|
||||
unknownError, exitCode
|
||||
unknownError,
|
||||
exitCode;
|
||||
|
||||
bool get isError => name.contains("Error");
|
||||
}
|
||||
|
||||
enum _Injectable {
|
||||
console,
|
||||
sslBypass,
|
||||
sslBypassV2,
|
||||
reboot,
|
||||
memoryFix
|
||||
memoryFix,
|
||||
}
|
||||
|
||||
42
gui/lib/src/widget/info_bar_area.dart
Normal file
42
gui/lib/src/widget/info_bar_area.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
|
||||
class InfoBarArea extends StatefulWidget {
|
||||
const InfoBarArea({super.key});
|
||||
|
||||
@override
|
||||
State<InfoBarArea> createState() => InfoBarAreaState();
|
||||
}
|
||||
|
||||
class InfoBarAreaState extends State<InfoBarArea> {
|
||||
final Rx<List<Widget>> _children = Rx([]);
|
||||
|
||||
void insertChild(Widget child) {
|
||||
_children.value.add(child);
|
||||
_children.refresh();
|
||||
}
|
||||
|
||||
bool removeChild(Widget child) {
|
||||
final result = _children.value.remove(child);
|
||||
_children.refresh();
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: hasPageButton ? 72.0 : 16.0
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _children.value.map((child) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12.0
|
||||
),
|
||||
child: child
|
||||
)).toList(growable: false)
|
||||
),
|
||||
));
|
||||
}
|
||||
37
gui/lib/src/widget/info_tile.dart
Normal file
37
gui/lib/src/widget/info_tile.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class InfoTile extends StatelessWidget {
|
||||
final Key? expanderKey;
|
||||
final Text title;
|
||||
final Text content;
|
||||
|
||||
const InfoTile({
|
||||
this.expanderKey,
|
||||
required this.title,
|
||||
required this.content
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0
|
||||
),
|
||||
child: Expander(
|
||||
key: expanderKey,
|
||||
header: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
FluentIcons.info_24_regular
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
title
|
||||
],
|
||||
),
|
||||
content: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/profile.dart';
|
||||
|
||||
class ProfileWidget extends StatefulWidget {
|
||||
@@ -13,68 +14,72 @@ class ProfileWidget extends StatefulWidget {
|
||||
|
||||
class _ProfileWidgetState extends State<ProfileWidget> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => HoverButton(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
onPressed: () async {
|
||||
if(await showProfileForm(context)) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
builder: (context, states) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
states,
|
||||
transparentWhenNone: true,
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
onPressed: firstRun ? null : () async {
|
||||
if(await showProfileForm(context)) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
builder: (context, states) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle
|
||||
),
|
||||
child: Image.asset("assets/images/user.png")
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12.0,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_username,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
maxLines: 1
|
||||
),
|
||||
Text(
|
||||
_email,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w100
|
||||
),
|
||||
maxLines: 1
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle
|
||||
),
|
||||
child: Image.asset("assets/images/user.png")
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12.0,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_username,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
maxLines: 1
|
||||
),
|
||||
Text(
|
||||
_email,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w100
|
||||
),
|
||||
maxLines: 1
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
String get _username {
|
||||
var username = _gameController.username.text;
|
||||
|
||||
@@ -3,22 +3,22 @@ import 'dart:async';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerButton extends StatefulWidget {
|
||||
final bool authenticator;
|
||||
const ServerButton({Key? key, required this.authenticator}) : super(key: key);
|
||||
final bool backend;
|
||||
const ServerButton({Key? key, required this.backend}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ServerButton> createState() => _ServerButtonState();
|
||||
}
|
||||
|
||||
class _ServerButtonState extends State<ServerButton> {
|
||||
late final ServerController _controller = widget.authenticator ? Get.find<AuthenticatorController>() : Get.find<MatchmakerController>();
|
||||
late final ServerController _controller = widget.backend ? Get.find<BackendController>() : Get.find<MatchmakerController>();
|
||||
late final StreamController<void> _textController = StreamController.broadcast();
|
||||
late final void Function() _listener = () => _textController.add(null);
|
||||
|
||||
@@ -55,7 +55,7 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
);
|
||||
|
||||
String get _buttonText {
|
||||
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == _controller.defaultPort){
|
||||
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == _controller.defaultPort.toString()){
|
||||
return translations.checkServer(_controller.controllerName);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerTypeSelector extends StatefulWidget {
|
||||
final bool authenticator;
|
||||
final bool backend;
|
||||
|
||||
const ServerTypeSelector({Key? key, required this.authenticator})
|
||||
const ServerTypeSelector({Key? key, required this.backend})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -17,7 +17,7 @@ class ServerTypeSelector extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||
late final ServerController _controller = widget.authenticator ? Get.find<AuthenticatorController>() : Get.find<MatchmakerController>();
|
||||
late final ServerController _controller = widget.backend ? Get.find<BackendController>() : Get.find<MatchmakerController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class SettingTile extends StatefulWidget {
|
||||
class SettingTile extends StatelessWidget {
|
||||
static const double kDefaultContentWidth = 200.0;
|
||||
static const double kDefaultHeaderHeight = 72;
|
||||
|
||||
@@ -24,12 +24,7 @@ class SettingTile extends StatefulWidget {
|
||||
this.contentWidth = kDefaultContentWidth,
|
||||
this.children
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingTile> createState() => _SettingTileState();
|
||||
}
|
||||
|
||||
class _SettingTileState extends State<SettingTile> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -37,7 +32,7 @@ class _SettingTileState extends State<SettingTile> {
|
||||
bottom: 4.0
|
||||
),
|
||||
child: HoverButton(
|
||||
onPressed: _buildOnPressed(),
|
||||
onPressed: _buildOnPressed(context),
|
||||
builder: (context, states) => Container(
|
||||
height: 80,
|
||||
width: double.infinity,
|
||||
@@ -61,13 +56,13 @@ class _SettingTileState extends State<SettingTile> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
widget.icon,
|
||||
icon,
|
||||
const SizedBox(width: 16.0),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
widget.title == null ? _skeletonTitle : widget.title!,
|
||||
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
|
||||
title == null ? _skeletonTitle : title!,
|
||||
subtitle == null ? _skeletonSubtitle : subtitle!,
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -81,12 +76,12 @@ class _SettingTileState extends State<SettingTile> {
|
||||
);
|
||||
}
|
||||
|
||||
void Function()? _buildOnPressed() {
|
||||
if(widget.onPressed != null) {
|
||||
return widget.onPressed;
|
||||
void Function()? _buildOnPressed(BuildContext context) {
|
||||
if(onPressed != null) {
|
||||
return onPressed;
|
||||
}
|
||||
|
||||
final children = widget.children;
|
||||
final children = this.children;
|
||||
if (children == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -96,7 +91,7 @@ class _SettingTileState extends State<SettingTile> {
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: widget.title?.data
|
||||
name: title?.data
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
@@ -108,15 +103,15 @@ class _SettingTileState extends State<SettingTile> {
|
||||
}
|
||||
|
||||
Widget get _trailing {
|
||||
final hasContent = widget.content != null;
|
||||
final hasChildren = widget.children?.isNotEmpty == true;
|
||||
final hasListener = widget.onPressed != null;
|
||||
final hasContent = content != null;
|
||||
final hasChildren = children?.isNotEmpty == true;
|
||||
final hasListener = onPressed != null;
|
||||
if(hasContent && hasChildren) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
width: contentWidth,
|
||||
child: content
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Icon(
|
||||
@@ -128,8 +123,8 @@ class _SettingTileState extends State<SettingTile> {
|
||||
|
||||
if (hasContent) {
|
||||
return SizedBox(
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
width: contentWidth,
|
||||
child: content
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
|
||||
@@ -38,10 +38,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
if(_gameController.hasNoVersions) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return _createOptionsMenu(
|
||||
version: _gameController.selectedVersion,
|
||||
close: false,
|
||||
@@ -55,9 +51,20 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
);
|
||||
});
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) => _gameController.versions.value
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
final items = _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
items.add(MenuFlyoutItem(
|
||||
text: Text(translations.addLocalBuildContent),
|
||||
onPressed: VersionSelector.openAddDialog
|
||||
));
|
||||
items.add(MenuFlyoutItem(
|
||||
text: Text(translations.downloadBuildContent),
|
||||
onPressed: VersionSelector.openDownloadDialog
|
||||
));
|
||||
return items;
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: _createOptionsMenu(
|
||||
|
||||
@@ -7,55 +7,10 @@ import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
SettingTile get versionSelectSettingTile => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
title: Text(translations.manageVersionsName),
|
||||
subtitle: Text(translations.manageVersionsDescription),
|
||||
content: const VersionSelector(),
|
||||
children: [
|
||||
_selectVersionTile,
|
||||
_addLocalTile,
|
||||
_downloadTile
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _selectVersionTile => Obx(() {
|
||||
final gameController = Get.find<GameController>();
|
||||
if(gameController.hasNoVersions) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
title: Text(translations.selectFortniteName),
|
||||
subtitle: Text(translations.selectFortniteDescription),
|
||||
content: const VersionSelector()
|
||||
);
|
||||
});
|
||||
|
||||
SettingTile get _downloadTile => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_download_24_regular
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
title: Text(translations.downloadBuildName),
|
||||
subtitle: Text(translations.downloadBuildDescription),
|
||||
content: Button(
|
||||
onPressed: VersionSelector.openDownloadDialog,
|
||||
child: Text(translations.downloadBuildContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _addLocalTile => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_add_24_regular
|
||||
),
|
||||
title: Text(translations.addLocalBuildName),
|
||||
subtitle: Text(translations.addLocalBuildDescription),
|
||||
content: Button(
|
||||
onPressed: VersionSelector.openAddDialog,
|
||||
child: Text(translations.addLocalBuildContent)
|
||||
)
|
||||
title: Text(translations.selectFortniteName),
|
||||
subtitle: Text(translations.selectFortniteDescription),
|
||||
content: const VersionSelector()
|
||||
);
|
||||
Reference in New Issue
Block a user