Refactored GUI

This commit is contained in:
Alessandro Autiero
2025-08-10 19:43:57 +01:00
parent 52abf5eb95
commit 4ea73d17c7
75 changed files with 2020 additions and 2011 deletions

View File

@@ -0,0 +1,89 @@
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/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/message/backend.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import '../messenger/info_bar.dart';
class BackendButton extends StatefulWidget {
const BackendButton({Key? key}) : super(key: key);
@override
State<BackendButton> createState() => _BackendButtonState();
}
class _BackendButtonState extends State<BackendButton> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final StreamController<void> _textController = StreamController.broadcast();
late final void Function() _listener = () => _textController.add(null);
@override
void initState() {
_backendController.port.addListener(_listener);
super.initState();
}
@override
void dispose() {
_backendController.port.removeListener(_listener);
_textController.close();
super.dispose();
}
@override
Widget build(BuildContext context) => Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
height: 48,
width: double.infinity,
child: Button(
child: Align(
alignment: Alignment.center,
child: StreamBuilder(
stream: _textController.stream,
builder: (context, snapshot) => Obx(() => Text(_buttonText))
),
),
onPressed: () => _backendController.toggle(
eventHandler: (type, event) {
_backendController.started.value = event.type.isStart && !event.type.isError;
if(event.type == AuthBackendResultType.startedImplementation) {
_backendController.implementation = event.implementation;
}
return onBackendResult(type, event);
},
errorHandler: (error) {
if(_backendController.started.value) {
_backendController.stop();
_gameController.instance.value?.kill();
_hostingController.instance.value?.kill();
onBackendError(error);
}
}
)
)
)
);
String get _buttonText {
if(_backendController.type.value == AuthBackendType.local && _backendController.port.text.trim() == kDefaultBackendPort.toString()){
return translations.checkServer;
}
if(_backendController.started.value){
return translations.stopServer;
}
return translations.startServer;
}
}

View File

@@ -1,6 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/os.dart';
typedef FileSelectorValidator = String? Function(String?);

View File

@@ -13,10 +13,12 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/message/backend.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
@@ -39,6 +41,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final DllController _dllController = Get.find<DllController>();
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar;
@@ -91,8 +94,8 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Setting started...");
_setStarted(host, true);
log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) {
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${GameDll.values}");
for (final injectable in GameDll.values) {
if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
return;
}
@@ -100,7 +103,23 @@ class _LaunchButtonState extends State<LaunchButton> {
try {
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggle();
final backendResult = _backendController.started() || await _backendController.toggle(
eventHandler: (type, event) {
_backendController.started.value = event.type.isStart && !event.type.isError;
if(event.type == AuthBackendResultType.startedImplementation) {
_backendController.implementation = event.implementation;
}
return onBackendResult(type, event);
},
errorHandler: (error) {
if(_backendController.started.value) {
_backendController.stop();
_gameController.instance.value?.kill();
_hostingController.instance.value?.kill();
onBackendError(error);
}
}
);
if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop(
@@ -154,14 +173,14 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool forceLinkedHosting) async {
Future<GameInstance?> _startMatchMakingServer(GameVersion version, bool host, bool headless, bool forceLinkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
if(host){
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
return null;
}
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
if(!forceLinkedHosting && _backendController.type.value == AuthBackendType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
return null;
}
@@ -212,7 +231,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return result;
}
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
Future<GameInstance?> _startGameProcesses(GameVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
final eacProcess = await _createPausedProcess(version, host, kEacExe);
final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
@@ -232,17 +251,17 @@ class _LaunchButtonState extends State<LaunchButton> {
child: linkedHosting
);
if(host){
_hostingController.discardServer();
_serverBrowserController.removeServer(_hostingController.uuid);
_hostingController.instance.value = instance;
}else{
_gameController.instance.value = instance;
}
await _injectOrShowError(InjectableDll.auth, host);
await _injectOrShowError(GameDll.auth, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance;
}
Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
Future<int?> _createGameProcess(GameVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
try {
log("[${host ? 'HOST' : 'GAME'}] Deleting $kGFSDKAftermathLibDll...");
@@ -335,7 +354,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return gameProcess.pid;
}
Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
Future<int?> _createPausedProcess(GameVersion version, bool host, String executableName) async {
log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
final executables = await findFiles(version.location, executableName);
if(executables.isEmpty){
@@ -366,7 +385,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return pid;
}
void _onMatchEnd(FortniteVersion version) {
void _onMatchEnd(GameVersion version) {
if(_hostingController.autoRestart.value) {
final notification = LocalNotification(
title: translations.gameServerEnd,
@@ -409,17 +428,17 @@ class _LaunchButtonState extends State<LaunchButton> {
instance.launched = true;
instance.tokenError = false;
if(_isChapterOne(instance.version)) {
await _injectOrShowError(InjectableDll.memoryLeak, host);
await _injectOrShowError(GameDll.memoryLeak, host);
}
if(!host){
await _injectOrShowError(InjectableDll.console, host);
await _injectOrShowError(GameDll.console, host);
_onGameClientInjected();
}else {
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
if(gameServerPort != null) {
await killProcessByPort(gameServerPort);
}
await _injectOrShowError(InjectableDll.gameServer, host);
await _injectOrShowError(GameDll.gameServer, host);
_onGameServerInjected();
}
}
@@ -462,7 +481,8 @@ class _LaunchButtonState extends State<LaunchButton> {
}
return;
}
_backendController.joinLocalhost();
_backendController.gameServerAddress.text = kDefaultGameServerHost;
final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) {
showRebootInfoBar(
@@ -477,10 +497,8 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
await _hostingController.publishServer(
_hostingController.accountUsername.text,
_hostingController.instance.value!.version.toString(),
);
final serverBrowserEntry = await _hostingController.createServerBrowserEntry();
await _serverBrowserController.addServer(serverBrowserEntry);
showRebootInfoBar(
translations.gameServerStarted,
severity: InfoBarSeverity.success,
@@ -599,7 +617,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
if(host) {
_hostingController.discardServer();
_serverBrowserController.removeServer(_hostingController.uuid);
}
if(reason == _StopReason.normal) {
@@ -692,7 +710,7 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.tokenError:
_backendController.stop(interactive: false);
_backendController.stop();
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar(
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
@@ -729,7 +747,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<void> _injectOrShowError(InjectableDll injectable, bool hosting) async {
Future<void> _injectOrShowError(GameDll 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}");
@@ -762,7 +780,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
Future<File?> _getDllFileOrStop(String version, GameDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _dllController.getInjectableData(version, injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
@@ -802,7 +820,7 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
InfoBarEntry _showLaunchingGameClientWidget(GameVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
@@ -818,7 +836,8 @@ class _LaunchButtonState extends State<LaunchButton> {
),
child: Button(
onPressed: () async {
_backendController.joinLocalhost();
_backendController.gameServerAddress.text = kDefaultGameServerHost;
if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
_gameClientInfoBar?.close();

View File

@@ -2,9 +2,9 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/util/translations.dart';
class ServerTypeSelector extends StatefulWidget {
final Key overlayKey;
@@ -15,7 +15,7 @@ class ServerTypeSelector extends StatefulWidget {
}
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
late final BackendController _controller = Get.find<BackendController>();
late final BackendController _backendController = Get.find<BackendController>();
@override
Widget build(BuildContext context) {
@@ -24,27 +24,27 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_controller.type.value.label),
items: ServerType.values
leading: Text(_backendController.type.value.label),
items: AuthBackendType.values
.map((type) => _createItem(type))
.toList()
),
));
}
MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem(
MenuFlyoutItem _createItem(AuthBackendType type) => MenuFlyoutItem(
text: Text(type.label),
onPressed: () async {
await _controller.stop(interactive: false);
_controller.type.value = type;
await _backendController.stop();
_backendController.type.value = type;
}
);
}
extension _ServerTypeExtension on ServerType {
extension _ServerTypeExtension on AuthBackendType {
String get label {
return this == ServerType.embedded ? translations.embedded
: this == ServerType.remote ? translations.remote
return this == AuthBackendType.embedded ? translations.embedded
: this == AuthBackendType.remote ? translations.remote
: translations.local;
}
}

View File

@@ -6,13 +6,13 @@ import 'package:flutter/gestures.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/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/download_version.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version/import_version.dart';
import 'package:reboot_launcher/src/message/download_version.dart';
import 'package:reboot_launcher/src/message/import_version.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget {
@@ -38,7 +38,7 @@ class VersionSelector extends StatefulWidget {
)
);
static Future<void> openImportDialog(FortniteVersion? version) => showRebootDialog<bool>(
static Future<void> openImportDialog(GameVersion? version) => showRebootDialog<bool>(
builder: (context) => ImportVersionDialog(
version: version,
closable: true,
@@ -83,7 +83,7 @@ class _VersionSelectorState extends State<VersionSelector> {
);
});
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener(
Widget _createOptionsMenu({required GameVersion? version, required bool close, required Widget child}) => Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
return;
@@ -134,7 +134,7 @@ class _VersionSelectorState extends State<VersionSelector> {
return items;
}
MenuFlyoutItem _createVersionItem(FortniteVersion version) => MenuFlyoutItem(
MenuFlyoutItem _createVersionItem(GameVersion version) => MenuFlyoutItem(
text: Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
@@ -154,7 +154,7 @@ class _VersionSelectorState extends State<VersionSelector> {
onPressed: () => _gameController.selectedVersion.value = version
);
Future<void> _openVersionOptions(FortniteVersion version) async {
Future<void> _openVersionOptions(GameVersion version) async {
final result = await _flyoutController.showFlyout<_ContextualOption?>(
builder: (context) => MenuFlyout(
items: _ContextualOption.values
@@ -167,7 +167,7 @@ class _VersionSelectorState extends State<VersionSelector> {
_handleResult(result, version, true);
}
void _handleResult(_ContextualOption? result, FortniteVersion version, bool close) async {
void _handleResult(_ContextualOption? result, GameVersion version, bool close) async {
if(!mounted){
return;
}
@@ -229,7 +229,7 @@ class _VersionSelectorState extends State<VersionSelector> {
return false;
}
Future<bool?> _openDeleteDialog(FortniteVersion version) {
Future<bool?> _openDeleteDialog(GameVersion version) {
return showRebootDialog<bool>(
builder: (context) => ContentDialog(
content: Column(

View File

@@ -1,27 +1,16 @@
import 'dart:async';
import 'dart:io';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'hosting_controller.dart';
typedef BackendInteractiveEventHandler = InfoBarEntry? Function(AuthBackendType, AuthBackendResult);
class BackendController extends GetxController {
static const String storageName = "v3_backend_storage";
@@ -30,20 +19,20 @@ class BackendController extends GetxController {
late final GetStorage? _storage;
late final TextEditingController host;
late final TextEditingController port;
late final Rx<ServerType> type;
late final Rx<AuthBackendType> type;
late final TextEditingController gameServerAddress;
late final FocusNode gameServerAddressFocusNode;
late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started;
late final RxBool detached;
late final List<InfoBarEntry> _infoBars;
AuthBackendImplementation? implementation;
StreamSubscription? _worker;
ServerImplementation? _implementation;
InfoBarEntry? _interactiveEntry;
BackendController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
started = RxBool(false);
type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
type = Rx(AuthBackendType.values.elementAt(_storage?.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
port.text = _readPort();
@@ -58,9 +47,9 @@ class BackendController extends GetxController {
detached = RxBool(_storage?.read("detached") ?? false);
detached.listen((value) => _storage?.write("detached", value));
final address = _storage?.read("game_server_address");
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? kDefaultBackendHost : address);
var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue);
writeAuthBackendMatchmakingIp(lastValue);
gameServerAddress.addListener(() {
var newValue = gameServerAddress.text;
if(newValue.trim().toLowerCase() == lastValue.trim().toLowerCase()) {
@@ -70,7 +59,7 @@ class BackendController extends GetxController {
lastValue = newValue;
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
_storage?.write("game_server_address", newValue);
writeMatchmakingIp(newValue);
writeAuthBackendMatchmakingIp(newValue);
});
watchMatchmakingIp().listen((event) {
if(event != null && gameServerAddress.text != event) {
@@ -101,7 +90,6 @@ class BackendController extends GetxController {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
_infoBars = [];
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
@@ -116,7 +104,7 @@ class BackendController extends GetxController {
return value;
}
if (type.value != ServerType.remote) {
if (type.value != AuthBackendType.remote) {
return kDefaultBackendHost;
}
@@ -125,71 +113,56 @@ class BackendController extends GetxController {
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
void joinLocalhost() {
gameServerAddress.text = kDefaultGameServerHost;
}
void reset() async {
type.value = ServerType.values.elementAt(0);
for (final type in ServerType.values) {
type.value = AuthBackendType.values.elementAt(0);
for (final type in AuthBackendType.values) {
_storage?.write("${type.name}_host", null);
_storage?.write("${type.name}_port", null);
}
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
host.text = type.value != AuthBackendType.remote ? kDefaultBackendHost : "";
port.text = kDefaultBackendPort.toString();
gameServerAddress.text = "127.0.0.1";
gameServerAddress.text = kDefaultBackendHost;
consoleKey.value = _kDefaultConsoleKey;
detached.value = false;
}
Future<bool> toggle() {
Future<bool> toggle({
BackendInteractiveEventHandler? eventHandler,
BackendErrorHandler? errorHandler
}) {
if(started.value) {
return stop(interactive: true);
return stop(
eventHandler: eventHandler
);
}else {
return start(interactive: true);
return start(
eventHandler: eventHandler,
errorHandler: errorHandler
);
}
}
Future<bool> start({required bool interactive}) async {
Future<bool> start({
BackendInteractiveEventHandler? eventHandler,
BackendErrorHandler? errorHandler
}) async {
if(started.value) {
return true;
}
_cancel();
final stream = startBackend(
final stream = startAuthBackend(
type: type.value,
host: host.text,
port: port.text,
detached: detached.value,
onError: (errorMessage) {
if(started.value) {
stop(interactive: false);
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
_showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
}
}
onError: errorHandler
);
final completer = Completer<bool>();
InfoBarEntry? entry;
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event, interactive);
_interactiveEntry?.close();
_interactiveEntry = eventHandler?.call(type.value, event);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
@@ -199,21 +172,22 @@ class BackendController extends GetxController {
return await completer.future;
}
Future<bool> stop({required bool interactive}) async {
Future<bool> stop({
BackendInteractiveEventHandler? eventHandler
}) async {
if(!started.value) {
return true;
}
_cancel();
final stream = stopBackend(
final stream = stopAuthBackend(
type: type.value,
implementation: _implementation
implementation: implementation
);
final completer = Completer<bool>();
InfoBarEntry? entry;
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event, interactive);
_interactiveEntry?.close();
_interactiveEntry = eventHandler?.call(type.value, event);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
@@ -225,334 +199,6 @@ class BackendController extends GetxController {
void _cancel() {
_worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear();
}
InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})");
started.value = event.type.isStart && !event.type.isError;
switch (event.type) {
case ServerResultType.starting:
if(interactive) {
return _showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startSuccess:
if(interactive) {
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.startError:
if(interactive) {
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.stopping:
if(interactive) {
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.stopSuccess:
if(interactive) {
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.stopError:
if(interactive) {
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.startMissingHostError:
if(interactive) {
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startMissingPortError:
if(interactive) {
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startIllegalPortError:
if(interactive) {
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startFreeingPort:
if(interactive) {
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startFreePortSuccess:
if(interactive) {
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}else {
return null;
}
case ServerResultType.startFreePortError:
if(interactive) {
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.startPingingRemote:
if(interactive) {
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startPingingLocal:
if(interactive) {
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startPingError:
if(interactive) {
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startedImplementation:
_implementation = event.implementation;
return null;
}
}
Future<void> joinServer(String uuid, FortniteServer server) async {
if(!kDebugMode && uuid == server.id) {
_showRebootInfoBar(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final version = Get.find<GameController>()
.getVersionByGame(server.version.toString());
if(version == null) {
_showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final hashedPassword = server.password;
final hasPassword = hashedPassword != null;
final embedded = type.value == ServerType.embedded;
final author = server.author;
final encryptedIp = server.ip;
if(!hasPassword) {
final valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
_showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, decryptedIp, author, version);
}
Future<bool> _isServerValid(String address) async {
final result = await pingGameServer(address);
if(result) {
return true;
}
_showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
);
}
void _onServerJoined(bool embedded, String decryptedIp, String author, FortniteVersion version) {
if(embedded) {
gameServerAddress.text = decryptedIp;
pageIndex.value = RebootPageType.play.index;
}else {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>().selectedVersion.value = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
severity: InfoBarSeverity.success
));
}
InfoBarEntry _showRebootInfoBar(dynamic text, {
InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false,
Duration? duration = infoBarShortDuration,
void Function()? onDismissed,
Widget? action
}) {
final result = showRebootInfoBar(
text,
severity: severity,
loading: loading,
duration: duration,
onDismissed: onDismissed,
action: action
);
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
_infoBars.add(result);
}
return result;
}
Future<void> restart() async {
if(started.value) {
await stop(interactive: false);
await start(interactive: true);
}
_interactiveEntry?.close();
}
}

View File

@@ -4,16 +4,13 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:path/path.dart' as path;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:version/version.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
class DllController extends GetxController {
static const String storageName = "v3_dll_storage";
@@ -24,25 +21,20 @@ class DllController extends GetxController {
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer;
late final TextEditingController beforeS20Mirror;
late final TextEditingController aboveS20Mirror;
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Map<InjectableDll, StreamSubscription?> _subscriptions;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
customGameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
customGameServerDll = _createController("game_server", GameDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", GameDll.console);
backendDll = _createController("backend", GameDll.auth);
memoryLeakDll = _createController("memory_leak", GameDll.memoryLeak);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
@@ -52,24 +44,22 @@ class DllController extends GetxController {
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
_subscriptions = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
TextEditingController _createController(String key, GameDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
}
void resetGame() {
customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.auth);
customGameServerDll.text = getDefaultDllPath(GameDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(GameDll.console);
backendDll.text = getDefaultDllPath(GameDll.auth);
}
void resetServer() {
gameServerPort.text = kDefaultGameServerPort;
timer.value = UpdateTimer.hour;
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
status.value = UpdateStatus.waiting;
@@ -83,18 +73,15 @@ class DllController extends GetxController {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -134,7 +121,6 @@ class DllController extends GetxController {
duration: infoBarShortDuration
);
}
_listenToFileEvents(InjectableDll.gameServer);
return true;
}catch(message) {
infoBarEntry?.close();
@@ -164,22 +150,22 @@ class DllController extends GetxController {
}
}
(File, bool) getInjectableData(String version, InjectableDll dll) {
(File, bool) getInjectableData(String version, GameDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.gameServer:
case GameDll.gameServer:
if(customGameServer.value) {
return (File(customGameServerDll.text), true);
}
return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
case InjectableDll.console:
case GameDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.auth:
case GameDll.auth:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memoryLeak:
case GameDll.memoryLeak:
final memoryFile = File(memoryLeakDll.text);
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
}
@@ -193,43 +179,42 @@ class DllController extends GetxController {
}
}
TextEditingController getDllEditingController(InjectableDll dll) {
TextEditingController getDllEditingController(GameDll dll) {
switch(dll) {
case InjectableDll.console:
case GameDll.console:
return unrealEngineConsoleDll;
case InjectableDll.auth:
case GameDll.auth:
return backendDll;
case InjectableDll.gameServer:
case GameDll.gameServer:
return customGameServerDll;
case InjectableDll.memoryLeak:
case GameDll.memoryLeak:
return memoryLeakDll;
}
}
String getDefaultDllPath(InjectableDll dll) {
String getDefaultDllPath(GameDll dll) {
switch(dll) {
case InjectableDll.console:
case GameDll.console:
return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
case GameDll.auth:
return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer:
case GameDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
case GameDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
}
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
Future<bool> download(GameDll dll, String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry;
try {
if (dll == InjectableDll.gameServer) {
if (dll == GameDll.gameServer) {
return await updateGameServerDll(silent: silent);
}
if(!force && File(filePath).existsSync()) {
log("[DLL] $dll already exists");
_listenToFileEvents(dll);
return true;
}
@@ -267,7 +252,6 @@ class DllController extends GetxController {
}else {
log("[DLL] Not showing success dialog for $dll");
}
_listenToFileEvents(dll);
return true;
}catch(message) {
log("[DLL] An error occurred while downloading $dll: $message");
@@ -294,7 +278,7 @@ class DllController extends GetxController {
}
Future<void> downloadAndGuardDependencies() async {
for(final injectable in InjectableDll.values) {
for(final injectable in GameDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
@@ -303,76 +287,11 @@ class DllController extends GetxController {
}
}
}
void _listenToFileEvents(InjectableDll injectable) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
void onFileEvent(FileSystemEvent event, String filePath) {
if (!path.equals(event.path, filePath)) {
return;
}
if(path.equals(filePath, defaultPath)) {
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
_updateInput(injectable);
}
StreamSubscription subscribe(String filePath) => File(filePath)
.parent
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
.listen((event) => onFileEvent(event, filePath));
controller.addListener(() {
_subscriptions[injectable]?.cancel();
_subscriptions[injectable] = subscribe(controller.text);
});
_subscriptions[injectable] = subscribe(controller.text);
}
void _updateInput(InjectableDll injectable) {
switch(injectable) {
case InjectableDll.console:
settingsConsoleDllInputKey.currentState?.validate();
break;
case InjectableDll.auth:
settingsAuthDllInputKey.currentState?.validate();
break;
case InjectableDll.gameServer:
settingsGameServerDllInputKey.currentState?.validate();
break;
case InjectableDll.memoryLeak:
settingsMemoryDllInputKey.currentState?.validate();
break;
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
enum UpdateStatus {
waiting,
started,
success,
error
}

View File

@@ -16,8 +16,8 @@ class GameController extends GetxController {
late final TextEditingController username;
late final TextEditingController password;
late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> selectedVersion;
late final Rx<List<GameVersion>> versions;
late final Rxn<GameVersion> selectedVersion;
late final RxBool started;
late final Rxn<GameInstance> instance;
@@ -25,7 +25,7 @@ class GameController extends GetxController {
_storage = appWithNoStorage ? null : GetStorage(storageName);
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
.map((entry) => GameVersion.fromJson(entry))
.toList();
versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions());
@@ -52,12 +52,12 @@ class GameController extends GetxController {
instance.value = null;
}
FortniteVersion? getVersionByName(String name) {
GameVersion? getVersionByName(String name) {
name = name.trim();
return versions.value.firstWhereOrNull((element) => element.name == name);
}
FortniteVersion? getVersionByGame(String gameVersion) {
GameVersion? getVersionByGame(String gameVersion) {
gameVersion = gameVersion.trim();
final parsedGameVersion = Version.parse(gameVersion);
return versions.value.firstWhereOrNull((element) {
@@ -72,12 +72,12 @@ class GameController extends GetxController {
});
}
void addVersion(FortniteVersion version) {
void addVersion(GameVersion version) {
versions.update((val) => val?.add(version));
selectedVersion.value = version;
}
void removeVersion(FortniteVersion version) {
void removeVersion(GameVersion version) {
final index = versions.value.indexOf(version);
versions.update((val) => val?.removeAt(index));
if(hasNoVersions) {
@@ -96,5 +96,5 @@ class GameController extends GetxController {
bool get hasNoVersions => versions.value.isEmpty;
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) => versions.update((val) => function(version));
void updateVersion(GameVersion version, Function(GameVersion) function) => versions.update((val) => function(version));
}

View File

@@ -1,16 +1,13 @@
import 'dart:convert';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart';
import '../util/cryptography.dart';
class HostingController extends GetxController {
static const String storageName = "v3_hosting_storage";
@@ -25,15 +22,12 @@ class HostingController extends GetxController {
late final TextEditingController password;
late final FocusNode passwordFocusNode;
late final RxBool showPassword;
late final RxBool discoverable;
late final RxBool headless;
late final RxBool autoRestart;
late final RxBool started;
late final RxBool published;
late final Rxn<GameInstance> instance;
late final Rxn<Set<FortniteServer>> servers;
late final TextEditingController customLaunchArgs;
late final Semaphore _semaphore;
HostingController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -52,8 +46,6 @@ class HostingController extends GetxController {
nameFocusNode = FocusNode();
descriptionFocusNode = FocusNode();
passwordFocusNode = FocusNode();
discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage?.write("discoverable", value));
headless = RxBool(_storage?.read("headless") ?? true);
headless.listen((value) => _storage?.write("headless", value));
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
@@ -62,119 +54,38 @@ class HostingController extends GetxController {
published = RxBool(false);
showPassword = RxBool(false);
instance = Rxn();
servers = Rxn();
_listenServers();
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_semaphore = Semaphore();
}
void _listenServers([int attempt = 0]) {
log("[SUPABASE] Listening...");
final supabase = Supabase.instance.client;
supabase.from("hosting_v2")
.stream(primaryKey: ['id'])
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
.listen(
_onNewServer,
onError: (error) async {
log("[SUPABASE] Error: ${error}");
await Future.delayed(Duration(seconds: attempt * 5));
_listenServers(attempt + 1);
},
cancelOnError: true
Future<ServerBrowserEntry> createServerBrowserEntry() async {
final passwordText = password.text;
final hasPassword = passwordText.isNotEmpty;
var ip = await Ipify.ipv4();
if(hasPassword) {
ip = aes256Encrypt(ip, passwordText);
}
return ServerBrowserEntry(
id: uuid,
name: name.text,
description: description.text,
author: accountUsername.text,
ip: ip,
version: instance.value!.version.toString(),
password: hasPassword ? hashPassword(passwordText) : "",
timestamp: DateTime.now()
);
}
void _onNewServer(Set<FortniteServer> event) {
log("[SUPABASE] New event: ${event}");
servers.value = event;
published.value = event.any((element) => element.id == uuid);
}
Future<void> publishServer(String author, String version) async {
try {
_semaphore.acquire();
log("[SERVER] Publishing server...");
if(published.value) {
log("[SERVER] Already published");
return;
}
final passwordText = password.text;
final hasPassword = passwordText.isNotEmpty;
var ip = await Ipify.ipv4();
if(hasPassword) {
ip = aes256Encrypt(ip, passwordText);
}
final supabase = Supabase.instance.client;
final hosts = supabase.from("hosting_v2");
final payload = FortniteServer(
id: uuid,
name: name.text,
description: description.text,
author: author,
ip: ip,
version: version,
password: hasPassword ? hashPassword(passwordText) : null,
timestamp: DateTime.now(),
discoverable: discoverable.value
).toJson();
log("[SERVER] Payload: ${jsonEncode(payload)}");
if(published()) {
await hosts.update(payload)
.eq("id", uuid);
}else {
await hosts.insert(payload);
}
published.value = true;
log("[SERVER] Published");
}catch(error) {
log("[SERVER] Cannot publish server: $error");
published.value = false;
}finally {
_semaphore.release();
}
}
Future<void> discardServer() async {
try {
_semaphore.acquire();
log("[SERVER] Discarding server...");
final supabase = Supabase.instance.client;
await supabase.from("hosting_v2")
.delete()
.match({'id': uuid});
servers.value?.removeWhere((element) => element.id == uuid);
log("[SERVER] Discarded server");
}catch(error) {
log("[SERVER] Cannot discard server: $error");
}finally {
published.value = false;
_semaphore.release();
}
}
void reset() {
accountUsername.text = kDefaultHostName;
accountPassword.text = "";
name.text = "";
description.text = "";
showPassword.value = false;
discoverable.value = false;
instance.value = null;
headless.value = true;
autoRestart.value = true;
customLaunchArgs.text = "";
}
FortniteServer? findServerById(String uuid) {
try {
return servers.value?.firstWhere((element) => element.id == uuid);
} on StateError catch(_) {
return null;
}
}
}

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
final class ServerBrowserController extends GetxController {
static const String _url = "ws://192.99.216.42:8080";
final Rxn<List<ServerBrowserEntry>> servers;
final Map<String, ServerBrowserEntry> _entries;
final ServerBrowserClient _client;
final Semaphore _semaphore;
ServerBrowserController() :
servers = Rxn(),
_entries = {},
_client = ServerBrowserClient(serverUrl: _url)..connect(), // The client should always be connected
_semaphore = Semaphore() {
addEventsListener((data) {
switch(data) {
case ServerBrowserStateEvent():
break;
case ServerBrowserAddEvent():
for(final entry in data.entries) {
_entries[entry.id] = entry;
}
_updateServers();
break;
case ServerBrowserRemoveEvent():
for(final entry in data.entries) {
_entries.remove(entry);
}
_updateServers();
break;
case ServerBrowserErrorEvent():
break;
}
});
}
void _updateServers() {
servers.value = servers.value == null
? _entries.values.toList(growable: false)
: [...?servers.value, ..._entries.values];
}
Future<void> addServer(ServerBrowserEntry entry) async {
try {
_semaphore.acquire();
await _client.addEntry(entry);
} finally {
_semaphore.release();
}
}
Future<void> removeServer(String uuid) async {
try {
_semaphore.acquire();
await _client.removeEntry(uuid);
} finally {
_semaphore.release();
}
}
StreamSubscription<ServerBrowserEvent> addEventsListener(void Function(ServerBrowserEvent) onData) {
return _client.addListener(onData);
}
ServerBrowserEntry? getServerById(String uuid) {
return _entries[uuid];
}
}

View File

@@ -6,8 +6,8 @@ import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
@@ -49,50 +49,4 @@ class SettingsController extends GetxController {
_storage?.write("offset_x", offsetX);
_storage?.write("offset_y", offsetY);
}
Future<void> notifyLauncherUpdate() async {
if (appVersion == null) {
return;
}
final pubspec = await _getPubspecYaml();
if (pubspec == null) {
return;
}
final latestVersion = Version.parse(pubspec["version"]);
if (latestVersion <= appVersion) {
return;
}
late InfoBarEntry infoBar;
infoBar = showRebootInfoBar(
translations.updateAvailable(latestVersion.toString()),
duration: null,
severity: InfoBarSeverity.warning,
action: Button(
child: Text(translations.updateAvailableAction),
onPressed: () {
infoBar.close();
launchUrl(Uri.parse(
"https://github.com/Auties00/reboot_launcher/releases"));
},
)
);
}
Future<dynamic> _getPubspecYaml() async {
try {
final pubspecResponse = await http.get(Uri.parse(
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) {
return null;
}
return loadYaml(pubspecResponse.body);
} catch (error) {
log("[UPDATER] Cannot check for updates: $error");
return null;
}
}
}

View File

@@ -0,0 +1,116 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
InfoBarEntry? onBackendResult(AuthBackendType type, AuthBackendResult event) {
switch (event.type) {
case AuthBackendResultType.starting:
return showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.startSuccess:
return showRebootInfoBar(
type == AuthBackendType.local
? translations.checkedServer
: translations.startedServer,
severity: InfoBarSeverity.success
);
case AuthBackendResultType.startError:
return showRebootInfoBar(
type == AuthBackendType.local
? translations.localServerError(event.error ?? translations.unknownError)
: translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case AuthBackendResultType.stopping:
return showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.stopSuccess:
return showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
case AuthBackendResultType.stopError:
return showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case AuthBackendResultType.startMissingHostError:
return showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startMissingPortError:
return showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startIllegalPortError:
return showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startFreeingPort:
return showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
case AuthBackendResultType.startFreePortSuccess:
return showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
case AuthBackendResultType.startFreePortError:
return showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case AuthBackendResultType.startPingingRemote:
return showRebootInfoBar(
translations.pingingServer(AuthBackendType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.startPingingLocal:
return showRebootInfoBar(
translations.pingingServer(type.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.startPingError:
return showRebootInfoBar(
translations.pingError(type.name),
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startedImplementation:
return null;
}
}
void onBackendError(Object error) {
showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
}

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
builder: (context) => InfoDialog(

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
Future<void> showDllDeletedDialog() => showRebootDialog(
builder: (context) => InfoDialog(

View File

@@ -6,11 +6,11 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/extensions.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:reboot_launcher/src/button/file_selector.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:windows_taskbar/windows_taskbar.dart';
@@ -30,19 +30,13 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
final Rxn<FortniteBuild> _build = Rxn();
final Rxn<GameBuild> _build = Rxn();
final RxnInt _timeLeft = RxnInt();
final Rxn<double> _progress = Rxn();
final RxInt _speed = RxInt(0);
SendPort? _downloadPort;
Object? _error;
StackTrace? _stackTrace;
@override
void initState() {
super.initState();
}
@override
void dispose() {
@@ -133,33 +127,33 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
_status.value = _DownloadStatus.downloading;
final communicationPort = ReceivePort();
communicationPort.listen((message) {
if(message is FortniteBuildDownloadProgress) {
if(message is GameBuildDownloadProgress) {
_onProgress(build, message);
}else if(message is SendPort) {
_downloadPort = message;
}else {
_onDownloadError(message, null);
_onDownloadError(message);
}
});
final options = FortniteBuildDownloadOptions(
final options = GameBuildDownloadOptions(
build,
Directory(_pathController.text),
communicationPort.sendPort
);
final errorPort = ReceivePort();
errorPort.listen((message) => _onDownloadError(message, null));
errorPort.listen((message) => _onDownloadError(message));
await Isolate.spawn(
downloadArchiveBuild,
options,
onError: errorPort.sendPort,
errorsAreFatal: true
);
} catch (exception, stackTrace) {
_onDownloadError(exception, stackTrace);
} catch (exception) {
_onDownloadError(exception);
}
}
Future<void> _onDownloadComplete(FortniteBuild build) async {
Future<void> _onDownloadComplete(GameBuild build) async {
if (!mounted) {
return;
}
@@ -174,7 +168,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
_status.value = _DownloadStatus.done;
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
final version = FortniteVersion(
final version = GameVersion(
name: name,
gameVersion: build.gameVersion,
location: location
@@ -182,7 +176,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
_gameController.addVersion(version);
}
void _onDownloadError(Object? error, StackTrace? stackTrace) {
void _onDownloadError(Object? error) {
_cancelDownload();
if (!mounted) {
return;
@@ -191,10 +185,9 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
_status.value = _DownloadStatus.error;
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
_error = error;
_stackTrace = stackTrace;
}
void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
void _onProgress(GameBuild build, GameBuildDownloadProgress message) {
if (!mounted) {
return;
}
@@ -282,6 +275,8 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSelector,
InfoLabel(
label: translations.versionName,
child: TextFormBox(
@@ -296,8 +291,6 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
height: 16.0
),
_buildSelector,
FileSelector(
label: translations.gameFolderTitle,
placeholder: translations.buildInstallationDirectoryPlaceholder,
@@ -336,13 +329,13 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
Widget get _buildSelector => InfoLabel(
label: translations.build,
child: FormField<FortniteBuild?>(
child: FormField<GameBuild?>(
key: _formFieldKey,
validator: (data) => _checkBuild(data),
builder: (formContext) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ComboBox<FortniteBuild>(
ComboBox<GameBuild>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: downloadableBuilds.where((build) => build.available)
@@ -377,7 +370,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
)
);
String? _checkBuild(FortniteBuild? data) {
String? _checkBuild(GameBuild? data) {
if(data == null) {
return translations.selectBuild;
}
@@ -385,7 +378,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
return null;
}
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
ComboBoxItem<GameBuild> _buildBuildItem(GameBuild element) => ComboBoxItem<GameBuild>(
value: element,
child: Text(element.gameVersion)
);

View File

@@ -1,19 +1,22 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
String? lastError;
void onError(Object exception, StackTrace? stackTrace, bool framework) {
log("[ERROR_HANDLER] Called");
log("[ERROR] $exception");
log("[STACKTRACE] $stackTrace");
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
log("[ERROR_HANDLER] Not mounted");
return;
}
if(lastError == exception.toString()){
log("[ERROR_HANDLER] Duplicate");
return;
}

View File

@@ -2,16 +2,16 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/button/file_selector.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:version/version.dart';
class ImportVersionDialog extends StatefulWidget {
final FortniteVersion? version;
final GameVersion? version;
final bool closable;
const ImportVersionDialog({Key? key, required this.version, required this.closable}) : super(key: key);
@@ -175,7 +175,7 @@ class _ImportVersionDialogState extends State<ImportVersionDialog> {
}
if(widget.version == null) {
final version = FortniteVersion(
final version = GameVersion(
name: name,
gameVersion: gameVersion,
location: shippingExes.first.parent

View File

@@ -5,16 +5,16 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/profile.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
import 'package:reboot_launcher/src/widget/page/home_page.dart';
import 'package:reboot_launcher/src/widget/page/host_page.dart';
import 'package:reboot_launcher/src/widget/page/play_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
import 'package:reboot_launcher/src/message/profile.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/backend_page.dart';
import 'package:reboot_launcher/src/pager/pager.dart';
import 'package:reboot_launcher/src/page/host_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/page/play_page.dart';
import 'package:reboot_launcher/src/button/version_selector.dart';
void startOnboarding() {
final gameController = Get.find<GameController>();
@@ -36,7 +36,7 @@ void startOnboarding() {
}
void _promptPlayPage() {
pageIndex.value = RebootPageType.play.index;
pageIndex.value = PageType.play.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptPlayPageText,
actionBuilder: (context, onClose) => _buildActionButton(
@@ -72,7 +72,7 @@ void _promptPlayVersion() {
}
void _promptServerBrowserPage() {
pageIndex.value = RebootPageType.browser.index;
pageIndex.value = PageType.browser.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptServerBrowserPageText,
actionBuilder: (context, onClose) => _buildActionButton(
@@ -87,7 +87,7 @@ void _promptServerBrowserPage() {
}
void _promptHostAccount() {
pageIndex.value = RebootPageType.host.index;
pageIndex.value = PageType.host.index;
profileOverlayKey.currentState!.showOverlay(
text: translations.hostAccountText,
offset: Offset(27.5, 17.5),
@@ -118,7 +118,6 @@ void _promptHostPage() {
void _promptHostInfo() {
final hostingController = Get.find<HostingController>();
hostInfoOverlayTargetKey.currentState!.showOverlay(
text: translations.promptHostInfoText,
offset: Offset(-10, 2.5),
@@ -130,7 +129,6 @@ void _promptHostInfo() {
themed: false,
onTap: () {
onClose();
hostingController.discoverable.value = false;
_promptHostVersion();
}
),
@@ -140,7 +138,6 @@ void _promptHostInfo() {
label: translations.promptHostInfoActionLabelConfigure,
onTap: () {
onClose();
hostingController.discoverable.value = true;
hostInfoTileKey.currentState!.openNestedPage();
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
}
@@ -202,7 +199,7 @@ void _promptHostInformationPassword() {
onTap: () {
onClose();
Navigator.of(hostInfoTileKey.currentContext!).pop();
pageStack.removeLast();
currentPageStack.removeLast();
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
}
)
@@ -234,7 +231,7 @@ void _promptHostShare() {
label: translations.promptHostShareActionLabel,
onTap: () {
onClose();
backendController.type.value = ServerType.embedded;
backendController.type.value = AuthBackendType.embedded;
_promptBackendPage();
}
)
@@ -243,7 +240,7 @@ void _promptHostShare() {
void _promptBackendPage() {
pageIndex.value = RebootPageType.backend.index;
pageIndex.value = PageType.backend.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptBackendPageText,
actionBuilder: (context, onClose) => _buildActionButton(
@@ -322,7 +319,7 @@ void _promptBackendDetached() {
}
void _promptInfoTab() {
pageIndex.value = RebootPageType.info.index;
pageIndex.value = PageType.info.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptInfoTabText,
actionBuilder: (context, onClose) => _buildActionButton(
@@ -337,7 +334,7 @@ void _promptInfoTab() {
}
void _promptSettingsTab() {
pageIndex.value = RebootPageType.settings.index;
pageIndex.value = PageType.settings.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptSettingsTabText,
actionBuilder: (context, onClose) => _buildActionButton(

View File

@@ -2,8 +2,8 @@ import 'package:email_validator/email_validator.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
final showPassword = RxBool(false);

View File

@@ -1,9 +1,10 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'info_bar.dart';
bool inDialog = false;

View File

@@ -1,6 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/rendering.dart';
import 'package:reboot_launcher/src/widget/page/home_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
typedef WidgetBuilder = Widget Function(BuildContext, void Function());

View File

@@ -5,16 +5,16 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/server/server_start_button.dart';
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/message/data.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/button/backend_start_button.dart';
import 'package:reboot_launcher/src/button/server_type_selector.dart';
import 'package:url_launcher/url_launcher.dart';
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
@@ -22,7 +22,7 @@ final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = G
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
class BackendPage extends RebootPage {
class BackendPage extends AbstractPage {
const BackendPage({Key? key}) : super(key: key);
@override
@@ -32,16 +32,16 @@ class BackendPage extends RebootPage {
String get iconAsset => "assets/images/backend.png";
@override
RebootPageType get type => RebootPageType.backend;
PageType get type => PageType.backend;
@override
bool hasButton(String? pageName) => pageName == null;
@override
RebootPageState<BackendPage> createState() => _BackendPageState();
AbstractPageState<BackendPage> createState() => _BackendPageState();
}
class _BackendPageState extends RebootPageState<BackendPage> {
class _BackendPageState extends AbstractPageState<BackendPage> {
final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry;
@@ -77,7 +77,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
];
Widget get _gameServerAddress => Obx(() {
if(_backendController.type.value != ServerType.embedded) {
if(_backendController.type.value != AuthBackendType.embedded) {
return const SizedBox.shrink();
}
@@ -99,7 +99,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
});
Widget get _hostName => Obx(() {
if(_backendController.type.value != ServerType.remote) {
if(_backendController.type.value != AuthBackendType.remote) {
return const SizedBox.shrink();
}
@@ -117,7 +117,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
});
Widget get _port => Obx(() {
if(_backendController.type.value == ServerType.embedded) {
if(_backendController.type.value == AuthBackendType.embedded) {
return const SizedBox.shrink();
}
@@ -139,7 +139,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
});
Widget get _detached => Obx(() {
if(_backendController.type.value != ServerType.embedded) {
if(_backendController.type.value != AuthBackendType.embedded) {
return const SizedBox.shrink();
}
@@ -162,12 +162,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
key: backendDetachedOverlayTargetKey,
child: ToggleSwitch(
checked: _backendController.detached(),
onChanged: (value) async {
_backendController.detached.value = value;
if(_backendController.started.value) {
await _backendController.restart();
}
}
onChanged: (value) async => _backendController.detached.value = value
),
),
],
@@ -176,7 +171,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
});
Widget get _unrealEngineConsoleKey => Obx(() {
if(_backendController.type.value != ServerType.embedded) {
if(_backendController.type.value != AuthBackendType.embedded) {
return const SizedBox.shrink();
}
@@ -216,7 +211,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
);
Widget get _installationDirectory => Obx(() {
if(_backendController.type.value != ServerType.embedded) {
if(_backendController.type.value != AuthBackendType.embedded) {
return const SizedBox.shrink();
}
@@ -245,5 +240,5 @@ class _BackendPageState extends RebootPageState<BackendPage> {
);
@override
Widget get button => const ServerButton();
Widget get button => const BackendButton();
}

View File

@@ -0,0 +1,559 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentIcons show FluentIcons;
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/extensions.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
class BrowsePage extends AbstractPage {
const BrowsePage({Key? key}) : super(key: key);
@override
String get name => translations.browserName;
@override
PageType get type => PageType.browser;
@override
String get iconAsset => "assets/images/server_browser.png";
@override
bool hasButton(String? pageName) => false;
@override
AbstractPageState<BrowsePage> createState() => _BrowsePageState();
}
class _BrowsePageState extends AbstractPageState<BrowsePage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
final TextEditingController _filterController = TextEditingController();
final StreamController<String> _filterControllerStream = StreamController.broadcast();
final Rx<_Filter> _filter = Rx(_Filter.all);
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_initAppLink();
});
super.initState();
}
void _initAppLink() async {
final appLinks = AppLinks();
final initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_onAppLink(initialUrl);
}
appLinks.uriLinkStream.listen(_onAppLink);
}
void _onAppLink(Uri uri) {
final uuid = uri.host;
final server = _serverBrowserController.getServerById(uuid);
if(server != null) {
_joinServer(_hostingController.uuid, server);
}else {
showRebootInfoBar(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() {
final servers = _serverBrowserController.servers.value;
return servers?.isEmpty == true
? _noServers
: _buildPageBody(servers);
});
}
Widget get _noServers => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
translations.noServersAvailableTitle,
style: FluentTheme.of(context).typography.titleLarge,
),
Text(
translations.noServersAvailableSubtitle,
style: FluentTheme.of(context).typography.body
),
],
);
Widget _buildPageBody(List<ServerBrowserEntry>? data) => StreamBuilder(
stream: _filterControllerStream.stream,
builder: (context, filterSnapshot) {
final items = data
?.where((entry) => _isValidItem(entry, filterSnapshot.data))
.toList(growable: false);
return Column(
children: [
_searchBar,
const SizedBox(
height: 24,
),
Row(
children: [
_buildFilter(context),
const SizedBox(
width: 16.0
),
_buildSort(context),
],
),
const SizedBox(
height: 24,
),
Expanded(
child: _buildPopulatedListBody(items)
),
],
);
}
);
Widget _buildSort(BuildContext context) => Row(
children: [
Icon(
fluentIcons.FluentIcons.arrow_sort_24_regular,
color: FluentTheme.of(context).resources.textFillColorDisabled
),
const SizedBox(width: 4.0),
Text(
"Sort by: ",
style: TextStyle(
color: FluentTheme.of(context).resources.textFillColorDisabled
),
),
const SizedBox(width: 4.0),
Obx(() => SizedBox(
width: 230,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_sort.value.translatedName,
textAlign: TextAlign.start
),
title: const Spacer(),
items: _Sort.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _sort.value = entry
)).toList()
),
))
],
);
Row _buildFilter(BuildContext context) {
return Row(
children: [
Icon(
fluentIcons.FluentIcons.filter_24_regular,
color: FluentTheme.of(context).resources.textFillColorDisabled
),
const SizedBox(width: 4.0),
Text(
"Filter by: ",
style: TextStyle(
color: FluentTheme.of(context).resources.textFillColorDisabled
),
),
const SizedBox(width: 4.0),
Obx(() => SizedBox(
width: 125,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_filter.value.translatedName,
textAlign: TextAlign.start
),
title: const Spacer(),
items: _Filter.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _filter.value = entry
)).toList()
),
))
],
);
}
Widget _buildPopulatedListBody(List<ServerBrowserEntry>? items) => Obx(() {
final filter = _filter.value;
final sorted = items?.where((element) {
switch(filter) {
case _Filter.all:
return true;
case _Filter.accessible:
return element.password.isNotEmpty;
case _Filter.playable:
return _gameController.getVersionByGame(element.version) != null;
}
}).toList();
final sort = _sort.value;
sorted?.sort((first, second) {
switch(sort) {
case _Sort.timeAscending:
return first.timestamp.compareTo(second.timestamp);
case _Sort.timeDescending:
return second.timestamp.compareTo(first.timestamp);
case _Sort.nameAscending:
return first.name.compareTo(second.name);
case _Sort.nameDescending:
return second.name.compareTo(first.name);
}
});
if(sorted?.isEmpty == true) {
return _noServersByQuery;
}
return ListView.builder(
itemCount: sorted?.length,
physics: sorted == null ? const NeverScrollableScrollPhysics() : const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
final entry = sorted?.elementAt(index);
if (entry == null) {
return const SettingTile();
} else {
final hasPassword = entry.password.isNotEmpty;
return SettingTile(
icon: Icon(
hasPassword ? FluentIcons.lock : FluentIcons.globe
),
title: Text(
"${_formatName(entry)}${entry.author}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
subtitle: Text(
"${_formatDescription(entry)}${_formatVersion(entry)}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
content: Button(
onPressed: () => _joinServer(_hostingController.uuid, entry),
child: Text(
_backendController.type.value == AuthBackendType.embedded
? translations.joinServer
: translations.copyIp),
)
);
}
}
);
});
Widget get _noServersByQuery => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
translations.noServersAvailableByQueryTitle,
style: FluentTheme.of(context).typography.titleLarge,
),
Text(
translations.noServersAvailableByQuerySubtitle,
style: FluentTheme.of(context).typography.body
),
],
);
bool _isValidItem(ServerBrowserEntry entry, String? filter) =>
filter == null || filter.isEmpty || _filterServer(entry, filter);
bool _filterServer(ServerBrowserEntry element, String filter) {
filter = filter.toLowerCase();
final uri = Uri.tryParse(filter);
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
return true;
}
return element.id.toLowerCase().contains(filter.toLowerCase())
|| element.name.toLowerCase().contains(filter)
|| element.author.toLowerCase().contains(filter)
|| element.description.toLowerCase().contains(filter);
}
Widget get _searchBar => Align(
alignment: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 350
),
child: TextBox(
placeholder: translations.findServer,
controller: _filterController,
autofocus: true,
onChanged: (value) => _filterControllerStream.add(value),
suffix: _searchBarIcon,
),
),
);
Widget get _searchBarIcon => Button(
onPressed: _filterController.text.isEmpty ? null : () {
_filterController.clear();
_filterControllerStream.add("");
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
),
child: _searchBarIconData
);
Widget get _searchBarIconData {
final color = FluentTheme.of(context).resources.textFillColorPrimary;
if (_filterController.text.isNotEmpty) {
return Icon(
FluentIcons.clear,
size: 8.0,
color: color
);
}
return Transform.flip(
flipX: true,
child: Icon(
FluentIcons.search,
size: 12.0,
color: color
),
);
}
String _formatName(ServerBrowserEntry server) {
final result = server.name;
return result.isEmpty ? translations.defaultServerName : result;
}
String _formatDescription(ServerBrowserEntry server) {
final result = server.description;
return result.isEmpty ? translations.defaultServerDescription : result;
}
String _formatVersion(ServerBrowserEntry server) => "Fortnite ${server.version.toString()}";
Future<void> _joinServer(String uuid, ServerBrowserEntry server) async {
if(!kDebugMode && uuid == server.id) {
showRebootInfoBar(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final version = _gameController.getVersionByGame(server.version.toString());
if(version == null) {
showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final hashedPassword = server.password;
final embedded = _backendController.type.value == AuthBackendType.embedded;
final author = server.author;
final encryptedIp = server.ip;
if(hashedPassword.isEmpty) {
final valid = await _isServerValid(server.name, encryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(server.name, decryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, decryptedIp, author, version);
}
Future<bool> _isServerValid(String name, String address) async {
final loadingBar = showRebootInfoBar(
translations.joiningServer(name),
duration: infoBarLongDuration,
loading: true,
severity: InfoBarSeverity.info
);
final result = await pingGameServer(address)
.withMinimumDuration(const Duration(seconds: 1));
loadingBar.close();
if(result) {
return true;
}
showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? fluentIcons.FluentIcons.eye_off_24_regular : fluentIcons.FluentIcons.eye_24_regular
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
);
}
void _onServerJoined(bool embedded, String decryptedIp, String author, GameVersion version) {
if(embedded) {
_backendController.gameServerAddress.text = decryptedIp;
pageIndex.value = PageType.play.index;
}else {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>().selectedVersion.value = version;
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
severity: InfoBarSeverity.success
));
}
@override
Widget? get button => null;
@override
List<Widget> get settings => [];
}
enum _Filter {
all,
accessible,
playable;
String get translatedName {
switch(this) {
case _Filter.all:
return translations.all;
case _Filter.accessible:
return translations.accessible;
case _Filter.playable:
return translations.playable;
}
}
}
enum _Sort {
timeAscending,
timeDescending,
nameAscending,
nameDescending;
String get translatedName {
switch(this) {
case _Sort.timeAscending:
return translations.timeAscending;
case _Sort.timeDescending:
return translations.timeDescending;
case _Sort.nameAscending:
return translations.nameAscending;
case _Sort.nameDescending:
return translations.nameDescending;
}
}
}

View File

@@ -7,17 +7,17 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/message/data.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/button/game_start_button.dart';
import 'package:reboot_launcher/src/button/version_selector.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
@@ -27,7 +27,7 @@ final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
class HostPage extends RebootPage {
class HostPage extends AbstractPage {
const HostPage({Key? key}) : super(key: key);
@override
@@ -37,17 +37,17 @@ class HostPage extends RebootPage {
String get iconAsset => "assets/images/host.png";
@override
RebootPageType get type => RebootPageType.host;
PageType get type => PageType.host;
@override
bool hasButton(String? pageName) => pageName == null;
@override
RebootPageState<HostPage> createState() => _HostingPageState();
AbstractPageState<HostPage> createState() => _HostingPageState();
}
class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>();
class _HostingPageState extends AbstractPageState<HostPage> {
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
final HostingController _hostingController = Get.find<HostingController>();
final DllController _dllController = Get.find<DllController>();
@@ -159,31 +159,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
)
),
))
),
SettingTile(
icon: Icon(
FluentIcons.eye_24_regular
),
title: Text(translations.hostGameServerDiscoverableName),
subtitle: Text(translations.hostGameServerDiscoverableDescription),
contentWidth: null,
content: Obx(() => Row(
children: [
Obx(() => Text(
_hostingController.discoverable.value ? translations.on : translations.off
)),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _hostingController.discoverable(),
onChanged: (value) async {
_hostingController.discoverable.value = value;
await _updateServer();
}
),
],
))
)
]
);
@@ -339,15 +314,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
}
try {
_hostingController.publishServer(
_hostingController.accountUsername.text,
_hostingController.instance.value!.version.toString()
);
final server = await _hostingController.createServerBrowserEntry();
_serverBrowserController.addServer(server);
} catch(error) {
_showCannotUpdateGameServer(error);
}
}
void _showCopiedLink() => showRebootInfoBar(
translations.hostShareLinkMessageSuccess,
severity: InfoBarSeverity.success

View File

@@ -1,18 +1,18 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/widget/message/onboard.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/message/onboard.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:url_launcher/url_launcher_string.dart';
class InfoPage extends RebootPage {
class InfoPage extends AbstractPage {
const InfoPage({Key? key}) : super(key: key);
@override
RebootPageState<InfoPage> createState() => _InfoPageState();
AbstractPageState<InfoPage> createState() => _InfoPageState();
@override
String get name => translations.infoName;
@@ -24,10 +24,10 @@ class InfoPage extends RebootPage {
bool hasButton(String? routeName) => false;
@override
RebootPageType get type => RebootPageType.info;
PageType get type => PageType.info;
}
class _InfoPageState extends RebootPageState<InfoPage> {
class _InfoPageState extends AbstractPageState<InfoPage> {
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
static const String _kDiscordInviteUrl = "https://discord.gg/rebootmp";

View File

@@ -3,21 +3,21 @@ import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
import 'package:reboot_launcher/src/widget/page/browser_page.dart';
import 'package:reboot_launcher/src/widget/page/host_page.dart';
import 'package:reboot_launcher/src/widget/page/info_page.dart';
import 'package:reboot_launcher/src/widget/page/play_page.dart';
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
import 'package:reboot_launcher/src/page/backend_page.dart';
import 'package:reboot_launcher/src/page/browser_page.dart';
import 'package:reboot_launcher/src/page/host_page.dart';
import 'package:reboot_launcher/src/page/info_page.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/page/play_page.dart';
import 'package:reboot_launcher/src/page/settings_page.dart';
import 'package:reboot_launcher/src/messenger/info_bar_area.dart';
final StreamController<void> pagesController = StreamController.broadcast();
bool hitBack = false;
final List<RebootPage> pages = [
final List<AbstractPage> pages = [
const PlayPage(),
const HostPage(),
const BrowsePage(),
@@ -28,7 +28,7 @@ final List<RebootPage> pages = [
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
final RxInt pageIndex = RxInt(RebootPageType.play.index);
final RxInt pageIndex = RxInt(PageType.play.index);
final HashMap<int, GlobalKey> _pageKeys = HashMap();
@@ -51,7 +51,9 @@ GlobalKey getPageKeyByIndex(int index) {
return result;
}
bool get hasPageButton => pages[pageIndex.value].hasButton(pageStack.lastOrNull);
bool get hasPageButton => currentPage.hasButton(currentPageStack.lastOrNull);
AbstractPage get currentPage => pages[pageIndex.value];
final Queue<Object?> appStack = _createAppStack();
Queue _createAppStack() {
@@ -71,13 +73,12 @@ Queue _createAppStack() {
final Map<int, Queue<String>> _pagesStack = Map.fromEntries(List.generate(pages.length, (index) => MapEntry(index, Queue<String>())));
Queue<String> get pageStack => _pagesStack[pageIndex.value]!;
Queue<String> get currentPageStack => _pagesStack[pageIndex.value]!;
void addSubPageToStack(String pageName) {
void addSubPageToCurrent(String pageName) {
final index = pageIndex.value;
final identifier = "${index}_$pageName";
appStack.add(identifier);
_pagesStack[index]!.add(identifier);
appStack.add(pageName);
_pagesStack[index]!.add(pageName);
pagesController.add(null);
}

View File

@@ -3,22 +3,22 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/message/data.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/button/game_start_button.dart';
import 'package:reboot_launcher/src/button/version_selector.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
class PlayPage extends RebootPage {
class PlayPage extends AbstractPage {
const PlayPage({Key? key}) : super(key: key);
@override
RebootPageState<PlayPage> createState() => _PlayPageState();
AbstractPageState<PlayPage> createState() => _PlayPageState();
@override
bool hasButton(String? pageName) => pageName == null;
@@ -30,10 +30,10 @@ class PlayPage extends RebootPage {
String get iconAsset => "assets/images/play.png";
@override
RebootPageType get type => RebootPageType.play;
PageType get type => PageType.play;
}
class _PlayPageState extends RebootPageState<PlayPage> {
class _PlayPageState extends AbstractPageState<PlayPage> {
final GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();

View File

@@ -1,6 +1,5 @@
import 'dart:math';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
@@ -9,12 +8,12 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/l10n/reboot_localizations.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/tile/file_setting_tile.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:url_launcher/url_launcher.dart';
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
@@ -22,7 +21,7 @@ final GlobalKey<TextFormBoxState> settingsAuthDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsMemoryDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsGameServerDllInputKey = GlobalKey();
class SettingsPage extends RebootPage {
class SettingsPage extends AbstractPage {
const SettingsPage({Key? key}) : super(key: key);
@override
@@ -32,16 +31,16 @@ class SettingsPage extends RebootPage {
String get iconAsset => "assets/images/settings.png";
@override
RebootPageType get type => RebootPageType.settings;
PageType get type => PageType.settings;
@override
bool hasButton(String? pageName) => false;
@override
RebootPageState<SettingsPage> createState() => _SettingsPageState();
AbstractPageState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends RebootPageState<SettingsPage> {
class _SettingsPageState extends AbstractPageState<SettingsPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
int? _downloadFromMirrorId;
@@ -70,9 +69,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
final path = _dllController.getDefaultDllPath(GameDll.console);
_dllController.unrealEngineConsoleDll.text = path;
await _dllController.download(InjectableDll.console, path, force: true);
await _dllController.download(GameDll.console, path, force: true);
settingsConsoleDllInputKey.currentState?.validate();
}
),
@@ -82,9 +81,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
final path = _dllController.getDefaultDllPath(GameDll.auth);
_dllController.backendDll.text = path;
await _dllController.download(InjectableDll.auth, path, force: true);
await _dllController.download(GameDll.auth, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
@@ -94,14 +93,24 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
final path = _dllController.getDefaultDllPath(GameDll.memoryLeak);
_dllController.memoryLeakDll.text = path;
await _dllController.download(InjectableDll.memoryLeak, path, force: true);
await _dllController.download(GameDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
_gameServer
],
);
SettingTile get _gameServer => SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(translations.settingsServerName),
subtitle: Text(translations.settingsServerSubtitle),
children: [
_internalFilesServerType,
_internalFilesUpdateTimer,
_internalFilesServerSource,
_internalFilesNewServerSource,
],
@@ -197,9 +206,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
description: translations.settingsServerFileDescription,
controller: _dllController.customGameServerDll,
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
final path = _dllController.getDefaultDllPath(GameDll.gameServer);
_dllController.customGameServerDll.text = path;
await _dllController.download(InjectableDll.gameServer, path);
await _dllController.download(GameDll.gameServer, path);
settingsGameServerDllInputKey.currentState?.validate();
}
);
@@ -279,35 +288,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
}
});
Widget get _internalFilesUpdateTimer => Obx(() {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.updateGameServerDll(
force: true
);
}
)).toList()
))
);
});
SettingTile get _language => SettingTile(
icon: Icon(
FluentIcons.local_language_24_regular
@@ -375,14 +355,4 @@ extension _ThemeModeExtension on ThemeMode {
return translations.light;
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

@@ -1,28 +1,29 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/widget/message/onboard.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/message/onboard.dart';
abstract class RebootPage extends StatefulWidget {
const RebootPage({super.key});
abstract class AbstractPage extends StatefulWidget {
const AbstractPage({super.key});
String get name;
String get iconAsset;
RebootPageType get type;
PageType get type;
int get index => type.index;
bool hasButton(String? pageName);
@override
RebootPageState createState();
AbstractPageState createState();
}
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
abstract class AbstractPageState<T extends AbstractPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
final SettingsController _settingsController = Get.find<SettingsController>();
@override

View File

@@ -5,9 +5,11 @@ class PageSuggestion {
final int pageIndex;
final String? routeName;
PageSuggestion({required this.name,
PageSuggestion({
required this.name,
required this.description,
this.content,
required this.pageIndex,
this.routeName});
this.routeName
});
}

View File

@@ -1,4 +1,4 @@
enum RebootPageType {
enum PageType {
play,
host,
browser,

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage;
@@ -12,37 +11,39 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/dll.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/pager/page_suggestion.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/fluent/profile_tile.dart';
import 'package:version/version.dart';
import 'package:reboot_launcher/src/tile/profile_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/updater.dart';
import 'package:reboot_launcher/src/messenger/info_bar_area.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
const double _kDefaultPadding = 12.0;
class HomePage extends StatefulWidget {
static const double kDefaultPadding = 12.0;
class RebootPager extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
const RebootPager({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
State<RebootPager> createState() => _RebootPagerState();
}
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
class _RebootPagerState extends State<RebootPager> with WindowListener, AutomaticKeepAliveClientMixin {
final BackendController _backendController = Get.find<BackendController>();
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
final GlobalKey _searchKey = GlobalKey();
@@ -61,7 +62,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
_syncPageViewWithNavigator();
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkUpdates();
_initAppLink();
_checkGameServer();
});
}
@@ -79,30 +79,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
});
}
void _initAppLink() async {
final appLinks = AppLinks();
final initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_joinServer(initialUrl);
}
appLinks.uriLinkStream.listen(_joinServer);
}
void _joinServer(Uri uri) {
final uuid = uri.host;
final server = _hostingController.findServerById(uuid);
if(server != null) {
_backendController.joinServer(_hostingController.uuid, server);
}else {
showRebootInfoBar(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
}
Future<void> _checkGameServer() async {
try {
final address = _backendController.gameServerAddress.text;
@@ -115,7 +91,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
return;
}
_backendController.joinLocalhost();
_backendController.gameServerAddress.text = kDefaultGameServerHost;
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
translations.serverNoLongerAvailableUnnamed,
severity: InfoBarSeverity.warning,
@@ -128,7 +104,23 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}
void _checkUpdates() {
_settingsController.notifyLauncherUpdate();
checkLauncherUpdate(
onUpdate: (latestVersion) {
late InfoBarEntry infoBar;
infoBar = showRebootInfoBar(
translations.updateAvailable(latestVersion.toString()),
duration: null,
severity: InfoBarSeverity.warning,
action: Button(
child: Text(translations.updateAvailableAction),
onPressed: () {
infoBar.close();
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
},
)
);
}
);
if(!dllsDirectory.existsSync()) {
dllsDirectory.createSync(recursive: true);
@@ -146,15 +138,13 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}
try {
await _hostingController.discardServer();
await _serverBrowserController.removeServer(_hostingController.uuid);
}catch(error) {
log("[HOSTING] Cannot discard server on exit: $error");
}
try {
if(_backendController.started.value) {
await _backendController.toggle();
}
await _backendController.stop();
}catch(error) {
log("[BACKEND] Cannot stop backend on exit: $error");
}
@@ -300,10 +290,10 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
Widget _buildBody() => Expanded(
child: Padding(
padding: EdgeInsets.only(
left: HomePage.kDefaultPadding,
right: HomePage.kDefaultPadding * 2,
top: HomePage.kDefaultPadding,
bottom: HomePage.kDefaultPadding * 2
left: _kDefaultPadding,
right: _kDefaultPadding * 2,
top: _kDefaultPadding,
bottom: _kDefaultPadding * 2
),
child: Column(
children: [
@@ -339,6 +329,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
Widget _buildBodyContent() => PageView.builder(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
itemBuilder: (context, index) => Navigator(
onPopPage: (page, data) => true,
@@ -347,8 +338,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
onChanged: (routeName) {
if(routeName != null) {
pageIndex.refresh();
addSubPageToStack(routeName);
pagesController.add(null);
addSubPageToCurrent(routeName);
}
}
)
@@ -375,12 +365,22 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
stream: pagesController.stream,
builder: (context, _) {
final elements = <TextSpan>[];
elements.add(_buildBodyHeaderRootPage(inactiveColor));
for(var i = pageStack.length - 1; i >= 0; i--) {
var innerPage = pageStack.elementAt(i);
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
final subPagesLength = currentPageStack.length;
final pagesLength = subPagesLength + 1;
elements.add(_buildBodyHeaderNestedPage(
name: currentPage.name,
index: 0,
length: pagesLength,
inactiveColor: inactiveColor
));
for(var index = 0; index < subPagesLength; index++) {
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor));
elements.add(_buildBodyHeaderNestedPage(
name: currentPageStack.elementAt(index),
index: index + 1,
length: pagesLength,
inactiveColor: inactiveColor
));
}
return Text.rich(
@@ -397,26 +397,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
}
TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan(
text: pages[pageIndex.value].name,
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
if(inDialog) {
return;
}
for(var i = 0; i < pageStack.length; i++) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
}
pagesController.add(null);
}) : null,
style: TextStyle(
color: pageStack.isNotEmpty ? inactiveColor : null
)
);
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
text: " > ",
style: TextStyle(
@@ -424,24 +404,33 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
)
);
TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan(
text: nestedPageName,
recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
TextSpan _buildBodyHeaderNestedPage({
required String name,
required int index,
required int length,
required Color inactiveColor
}) {
final last = index == length - 1;
return TextSpan(
text: name,
recognizer: last ? null : (TapGestureRecognizer()..onTap = () {
if(inDialog) {
return;
}
for(var j = 0; j < nestedPageIndex - 1; j++) {
var pops = length - 1 - index;
while(pops-- > 0) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
final element = currentPageStack.removeLast();
appStack.remove(element);
}
pagesController.add(null);
}),
style: TextStyle(
color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor
color: last ? null : inactiveColor
)
);
}
Widget _buildLateralView() => SizedBox(
width: 310,
@@ -479,7 +468,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
)
);
Widget _buildNavigationItem(RebootPage page) {
Widget _buildNavigationItem(AbstractPage page) {
final index = page.type.index;
return OverlayTarget(
key: getOverlayTargetKeyByPage(index),
@@ -488,9 +477,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
final lastPageIndex = pageIndex.value;
if(lastPageIndex != index) {
pageIndex.value = index;
}else if(pageStack.isNotEmpty) {
}else if(currentPageStack.isNotEmpty) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
final element = currentPageStack.removeLast();
appStack.remove(element);
pagesController.add(null);
}

View File

@@ -7,8 +7,8 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/button/file_selector.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8;

View File

@@ -19,7 +19,7 @@ class InfoTile extends StatelessWidget {
),
child: Expander(
key: expanderKey,
header: Row(
header: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(

View File

@@ -3,9 +3,9 @@ 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/pager/page_type.dart';
import 'package:reboot_launcher/src/message/profile.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/profile.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
class ProfileWidget extends StatefulWidget {
@@ -113,6 +113,6 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return "$username@projectreboot.dev".toLowerCase();
}
TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username;
TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password;
TextEditingController get _username => pageIndex.value == PageType.host.index ? _hostingController.accountUsername : _gameController.username;
TextEditingController get _password => pageIndex.value == PageType.host.index ? _hostingController.accountPassword : _gameController.password;
}

View File

@@ -9,7 +9,7 @@ class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0;
final void Function()? onPressed;
final Icon icon;
final Icon? icon;
final Text? title;
final Text? subtitle;
final Widget? content;
@@ -19,10 +19,10 @@ class SettingTile extends StatefulWidget {
const SettingTile({
super.key,
this.icon,
this.title,
this.subtitle,
this.onPressed,
required this.icon,
required this.title,
required this.subtitle,
this.content,
this.contentWidth = kDefaultContentWidth,
this.overlayKey,
@@ -62,6 +62,10 @@ class SettingTileState extends State<SettingTile> {
);
Card _buildBody() {
final icon = widget.icon;
final title = widget.title;
final subtitle = widget.subtitle;
final isSkeleton = icon == null || title == null || subtitle == null;
return Card(
borderRadius: const BorderRadius.all(
Radius.circular(6.0)
@@ -76,10 +80,10 @@ class SettingTileState extends State<SettingTile> {
if(widget.overlayKey != null)
OverlayTarget(
key: widget.overlayKey,
child: widget.icon,
child: isSkeleton ? _skeletonIcon : icon,
)
else
widget.icon,
isSkeleton ? _skeletonIcon : icon,
const SizedBox(width: 16.0),
@@ -87,12 +91,14 @@ class SettingTileState extends State<SettingTile> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.title == null ? _skeletonTitle : widget.title!,
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
isSkeleton ? _skeletonTitle : title,
isSkeleton ? _skeletonSubtitle : subtitle
],
),
),
const SizedBox(width: 16.0),
_trailing
],
),
@@ -100,6 +106,12 @@ class SettingTileState extends State<SettingTile> {
);
}
SkeletonAvatar get _skeletonIcon => const SkeletonAvatar(style: SkeletonAvatarStyle(
width: 30,
height: 30,
shape: BoxShape.circle
));
void Function()? _buildOnPressed() {
if(widget.onPressed != null) {
return widget.onPressed;

View File

@@ -16,4 +16,14 @@ extension StringExtension on String {
return substring(index + leading.length);
}
}
extension FutureExtension<T> on Future<T> {
Future<T> withMinimumDuration(Duration duration) async {
final result = await Future.wait([
Future.delayed(duration),
this
]);
return result.last;
}
}

View File

@@ -1,4 +1,3 @@
import 'dart:collection';
import 'dart:ffi';
import 'dart:io';

View File

@@ -0,0 +1,40 @@
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:version/version.dart';
import 'package:http/http.dart' as http;
import 'package:yaml/yaml.dart';
Future<void> checkLauncherUpdate({
required void Function(Version) onUpdate
}) async {
if (appVersion == null) {
return;
}
final pubspec = await _getPubspecYaml();
if (pubspec == null) {
return;
}
final latestVersion = Version.parse(pubspec["version"]);
if (latestVersion <= appVersion) {
return;
}
onUpdate(latestVersion);
}
Future<dynamic> _getPubspecYaml() async {
try {
final pubspecResponse = await http.get(Uri.parse(
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) {
return null;
}
return loadYaml(pubspecResponse.body);
} catch (error) {
log("[UPDATER] Cannot check for updates: $error");
return null;
}
}

View File

@@ -1,365 +0,0 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons;
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
class BrowsePage extends RebootPage {
const BrowsePage({Key? key}) : super(key: key);
@override
String get name => translations.browserName;
@override
RebootPageType get type => RebootPageType.browser;
@override
String get iconAsset => "assets/images/server_browser.png";
@override
bool hasButton(String? pageName) => false;
@override
RebootPageState<BrowsePage> createState() => _BrowsePageState();
}
class _BrowsePageState extends RebootPageState<BrowsePage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final TextEditingController _filterController = TextEditingController();
final StreamController<String> _filterControllerStream = StreamController.broadcast();
final Rx<_Filter> _filter = Rx(_Filter.all);
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() {
final data = _hostingController.servers.value
?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable)
.toSet();
if(data == null || data.isEmpty == true) {
return _noServers;
}
return _buildPageBody(data);
});
}
Widget get _noServers => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
translations.noServersAvailableTitle,
style: FluentTheme.of(context).typography.titleLarge,
),
Text(
translations.noServersAvailableSubtitle,
style: FluentTheme.of(context).typography.body
),
],
);
Widget _buildPageBody(Set<FortniteServer> data) => StreamBuilder(
stream: _filterControllerStream.stream,
builder: (context, filterSnapshot) {
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
return Column(
children: [
_searchBar,
const SizedBox(
height: 24,
),
Row(
children: [
_buildFilter(context),
const SizedBox(
width: 16.0
),
_buildSort(context),
],
),
const SizedBox(
height: 24,
),
Expanded(
child: _buildPopulatedListBody(items)
),
],
);
}
);
Widget _buildSort(BuildContext context) => Row(
children: [
Icon(
fluentUiIcons.FluentIcons.arrow_sort_24_regular,
color: FluentTheme.of(context).resources.textFillColorDisabled
),
const SizedBox(width: 4.0),
Text(
"Sort by: ",
style: TextStyle(
color: FluentTheme.of(context).resources.textFillColorDisabled
),
),
const SizedBox(width: 4.0),
Obx(() => SizedBox(
width: 230,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_sort.value.translatedName,
textAlign: TextAlign.start
),
title: const Spacer(),
items: _Sort.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _sort.value = entry
)).toList()
),
))
],
);
Row _buildFilter(BuildContext context) {
return Row(
children: [
Icon(
fluentUiIcons.FluentIcons.filter_24_regular,
color: FluentTheme.of(context).resources.textFillColorDisabled
),
const SizedBox(width: 4.0),
Text(
"Filter by: ",
style: TextStyle(
color: FluentTheme.of(context).resources.textFillColorDisabled
),
),
const SizedBox(width: 4.0),
Obx(() => SizedBox(
width: 125,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_filter.value.translatedName,
textAlign: TextAlign.start
),
title: const Spacer(),
items: _Filter.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _filter.value = entry
)).toList()
),
))
],
);
}
Widget _buildPopulatedListBody(Set<FortniteServer> items) => Obx(() {
final filter = _filter.value;
final sorted = items.where((element) {
switch(filter) {
case _Filter.all:
return true;
case _Filter.accessible:
return element.password == null;
case _Filter.playable:
return _gameController.getVersionByGame(element.version) != null;
}
}).toList();
final sort = _sort.value;
sorted.sort((first, second) {
switch(sort) {
case _Sort.timeAscending:
return first.timestamp.compareTo(second.timestamp);
case _Sort.timeDescending:
return second.timestamp.compareTo(first.timestamp);
case _Sort.nameAscending:
return first.name.compareTo(second.name);
case _Sort.nameDescending:
return second.name.compareTo(first.name);
}
});
if(sorted.isEmpty) {
return _noServersByQuery;
}
return ListView.builder(
itemCount: sorted.length,
itemBuilder: (context, index) {
final entry = sorted.elementAt(index);
final hasPassword = entry.password != null;
return SettingTile(
icon: Icon(
hasPassword ? FluentIcons.lock : FluentIcons.globe
),
title: Text(
"${_formatName(entry)}${entry.author}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
subtitle: Text(
"${_formatDescription(entry)}${_formatVersion(entry)}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
content: Button(
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
)
);
}
);
});
Widget get _noServersByQuery => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
translations.noServersAvailableByQueryTitle,
style: FluentTheme.of(context).typography.titleLarge,
),
Text(
translations.noServersAvailableByQuerySubtitle,
style: FluentTheme.of(context).typography.body
),
],
);
bool _isValidItem(FortniteServer entry, String? filter) =>
filter == null || filter.isEmpty || _filterServer(entry, filter);
bool _filterServer(FortniteServer element, String filter) {
filter = filter.toLowerCase();
final uri = Uri.tryParse(filter);
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
return true;
}
return element.id.toLowerCase().contains(filter.toLowerCase())
|| element.name.toLowerCase().contains(filter)
|| element.author.toLowerCase().contains(filter)
|| element.description.toLowerCase().contains(filter);
}
Widget get _searchBar => Align(
alignment: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 350
),
child: TextBox(
placeholder: translations.findServer,
controller: _filterController,
autofocus: true,
onChanged: (value) => _filterControllerStream.add(value),
suffix: _searchBarIcon,
),
),
);
Widget get _searchBarIcon => Button(
onPressed: _filterController.text.isEmpty ? null : () {
_filterController.clear();
_filterControllerStream.add("");
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
),
child: _searchBarIconData
);
Widget get _searchBarIconData {
final color = FluentTheme.of(context).resources.textFillColorPrimary;
if (_filterController.text.isNotEmpty) {
return Icon(
FluentIcons.clear,
size: 8.0,
color: color
);
}
return Transform.flip(
flipX: true,
child: Icon(
FluentIcons.search,
size: 12.0,
color: color
),
);
}
String _formatName(FortniteServer server) {
final result = server.name;
return result.isEmpty ? translations.defaultServerName : result;
}
String _formatDescription(FortniteServer server) {
final result = server.description;
return result.isEmpty ? translations.defaultServerDescription : result;
}
String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}";
@override
Widget? get button => null;
@override
List<Widget> get settings => [];
}
enum _Filter {
all,
accessible,
playable;
String get translatedName {
switch(this) {
case _Filter.all:
return translations.all;
case _Filter.accessible:
return translations.accessible;
case _Filter.playable:
return translations.playable;
}
}
}
enum _Sort {
timeAscending,
timeDescending,
nameAscending,
nameDescending;
String get translatedName {
switch(this) {
case _Sort.timeAscending:
return translations.timeAscending;
case _Sort.timeDescending:
return translations.timeDescending;
case _Sort.nameAscending:
return translations.nameAscending;
case _Sort.nameDescending:
return translations.nameDescending;
}
}
}

View File

@@ -1,64 +0,0 @@
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/backend_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart';
class ServerButton extends StatefulWidget {
const ServerButton({Key? key}) : super(key: key);
@override
State<ServerButton> createState() => _ServerButtonState();
}
class _ServerButtonState extends State<ServerButton> {
late final BackendController _controller = Get.find<BackendController>();
late final StreamController<void> _textController = StreamController.broadcast();
late final void Function() _listener = () => _textController.add(null);
@override
void initState() {
_controller.port.addListener(_listener);
super.initState();
}
@override
void dispose() {
_controller.port.removeListener(_listener);
_textController.close();
super.dispose();
}
@override
Widget build(BuildContext context) => Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
height: 48,
width: double.infinity,
child: Button(
child: Align(
alignment: Alignment.center,
child: StreamBuilder(
stream: _textController.stream,
builder: (context, snapshot) => Obx(() => Text(_buttonText))
),
),
onPressed: () => _controller.toggle()
)
)
);
String get _buttonText {
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == kDefaultBackendPort.toString()){
return translations.checkServer;
}
if(_controller.started.value){
return translations.stopServer;
}
return translations.startServer;
}
}