mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
Refactored GUI
This commit is contained in:
89
gui/lib/src/button/backend_start_button.dart
Normal file
89
gui/lib/src/button/backend_start_button.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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?);
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
gui/lib/src/controller/server_browser_controller.dart
Normal file
74
gui/lib/src/controller/server_browser_controller.dart
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
116
gui/lib/src/message/backend.dart
Normal file
116
gui/lib/src/message/backend.dart
Normal 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),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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(
|
||||
@@ -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)
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
559
gui/lib/src/page/browser_page.dart
Normal file
559
gui/lib/src/page/browser_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
enum RebootPageType {
|
||||
enum PageType {
|
||||
play,
|
||||
host,
|
||||
browser,
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -19,7 +19,7 @@ class InfoTile extends StatelessWidget {
|
||||
),
|
||||
child: Expander(
|
||||
key: expanderKey,
|
||||
header: Row(
|
||||
header: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
|
||||
40
gui/lib/src/util/updater.dart
Normal file
40
gui/lib/src/util/updater.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user