From 4ea73d17c797c7b63551a19e93752d0329ce44a8 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Sun, 10 Aug 2025 19:43:57 +0100 Subject: [PATCH] Refactored GUI --- common/lib/common.dart | 37 +- .../auth_backend_constants.dart} | 0 .../auth_backend_helper.dart} | 154 ++--- .../auth_backend_result.dart} | 20 +- .../auth_backend_type.dart} | 2 +- .../src/browser/server_browser_client.dart | 209 +++++++ .../server_browser_entry.dart} | 18 +- .../lib/src/browser/server_browser_event.dart | 30 + .../lib/src/browser/server_browser_state.dart | 5 + common/lib/src/constant/supabase.dart | 2 - common/lib/src/game/game_build.dart | 64 ++ .../game.dart => game/game_constants.dart} | 0 common/lib/src/game/game_dll.dart | 10 + .../game_downloader.dart} | 199 ++++--- .../src/{model => game}/game_instance.dart | 2 +- .../game.dart => game/game_metadata.dart} | 0 .../game_version.dart} | 6 +- common/lib/src/model/dll.dart | 10 - common/lib/src/model/fortnite_build.dart | 64 -- common/lib/src/model/update_status.dart | 6 - common/lib/src/model/update_timer.dart | 6 - common/lib/src/util/{log.dart => logger.dart} | 0 common/lib/src/util/os.dart | 19 +- gui/lib/l10n/reboot_en.arb | 7 +- gui/lib/main.dart | 324 ++++------ gui/lib/src/button/backend_start_button.dart | 89 +++ .../file => button}/file_selector.dart | 1 - .../game => button}/game_start_button.dart | 73 ++- .../server_type_selector.dart | 20 +- .../version => button}/version_selector.dart | 20 +- .../src/controller/backend_controller.dart | 436 ++------------ gui/lib/src/controller/dll_controller.dart | 147 ++--- gui/lib/src/controller/game_controller.dart | 16 +- .../src/controller/hosting_controller.dart | 125 +--- .../controller/server_browser_controller.dart | 74 +++ .../src/controller/settings_controller.dart | 48 +- gui/lib/src/message/backend.dart | 116 ++++ gui/lib/src/{widget => }/message/data.dart | 2 +- gui/lib/src/{widget => }/message/dll.dart | 2 +- .../version => message}/download_version.dart | 47 +- gui/lib/src/{widget => }/message/error.dart | 5 +- .../version => message}/import_version.dart | 10 +- gui/lib/src/{widget => }/message/onboard.dart | 37 +- gui/lib/src/{widget => }/message/profile.dart | 2 +- gui/lib/src/messenger/dialog.dart | 5 +- .../window => messenger}/info_bar_area.dart | 0 gui/lib/src/messenger/overlay.dart | 1 - .../src/{widget => }/page/backend_page.dart | 45 +- gui/lib/src/page/browser_page.dart | 559 ++++++++++++++++++ gui/lib/src/{widget => }/page/host_page.dart | 58 +- gui/lib/src/{widget => }/page/info_page.dart | 16 +- gui/lib/src/page/pages.dart | 35 +- gui/lib/src/{widget => }/page/play_page.dart | 22 +- .../src/{widget => }/page/settings_page.dart | 86 +-- .../page.dart => pager/abstract_page.dart} | 15 +- .../src/{page => pager}/page_suggestion.dart | 6 +- gui/lib/src/{page => pager}/page_type.dart | 2 +- .../page/home_page.dart => pager/pager.dart} | 163 +++-- .../file => tile}/file_setting_tile.dart | 4 +- .../{widget/window => tile}/info_tile.dart | 2 +- .../{widget/fluent => tile}/profile_tile.dart | 8 +- .../{widget/fluent => tile}/setting_tile.dart | 28 +- .../src/util/{types.dart => extensions.dart} | 10 + gui/lib/src/util/os.dart | 1 - gui/lib/src/util/updater.dart | 40 ++ gui/lib/src/widget/page/browser_page.dart | 365 ------------ .../widget/server/server_start_button.dart | 64 -- gui/pubspec.yaml | 1 - server_browser_backend/bin/main.dart | 2 +- .../lib/server_browser_backend.dart | 2 +- ...r_entry.dart => server_browser_entry.dart} | 8 +- .../lib/src/web_socket.dart | 12 +- server_browser_backend/package.bat | 1 + server_browser_backend/package.sh | 2 + server_browser_backend/test/test.dart | 4 +- 75 files changed, 2020 insertions(+), 2011 deletions(-) rename common/lib/src/{constant/backend.dart => backend/auth_backend_constants.dart} (100%) rename common/lib/src/{util/backend.dart => backend/auth_backend_helper.dart} (59%) rename common/lib/src/{model/server_result.dart => backend/auth_backend_result.dart} (51%) rename common/lib/src/{model/server_type.dart => backend/auth_backend_type.dart} (57%) create mode 100644 common/lib/src/browser/server_browser_client.dart rename common/lib/src/{model/fortnite_server.dart => browser/server_browser_entry.dart} (71%) create mode 100644 common/lib/src/browser/server_browser_event.dart create mode 100644 common/lib/src/browser/server_browser_state.dart delete mode 100644 common/lib/src/constant/supabase.dart create mode 100644 common/lib/src/game/game_build.dart rename common/lib/src/{constant/game.dart => game/game_constants.dart} (100%) create mode 100644 common/lib/src/game/game_dll.dart rename common/lib/src/{util/downloader.dart => game/game_downloader.dart} (62%) rename common/lib/src/{model => game}/game_instance.dart (96%) rename common/lib/src/{util/game.dart => game/game_metadata.dart} (100%) rename common/lib/src/{model/fortnite_version.dart => game/game_version.dart} (68%) delete mode 100644 common/lib/src/model/dll.dart delete mode 100644 common/lib/src/model/fortnite_build.dart delete mode 100644 common/lib/src/model/update_status.dart delete mode 100644 common/lib/src/model/update_timer.dart rename common/lib/src/util/{log.dart => logger.dart} (100%) create mode 100644 gui/lib/src/button/backend_start_button.dart rename gui/lib/src/{widget/file => button}/file_selector.dart (98%) rename gui/lib/src/{widget/game => button}/game_start_button.dart (91%) rename gui/lib/src/{widget/server => button}/server_type_selector.dart (68%) rename gui/lib/src/{widget/version => button}/version_selector.dart (91%) create mode 100644 gui/lib/src/controller/server_browser_controller.dart create mode 100644 gui/lib/src/message/backend.dart rename gui/lib/src/{widget => }/message/data.dart (100%) rename gui/lib/src/{widget => }/message/dll.dart (100%) rename gui/lib/src/{widget/version => message}/download_version.dart (91%) rename gui/lib/src/{widget => }/message/error.dart (90%) rename gui/lib/src/{widget/version => message}/import_version.dart (97%) rename gui/lib/src/{widget => }/message/onboard.dart (91%) rename gui/lib/src/{widget => }/message/profile.dart (100%) rename gui/lib/src/{widget/window => messenger}/info_bar_area.dart (100%) rename gui/lib/src/{widget => }/page/backend_page.dart (83%) create mode 100644 gui/lib/src/page/browser_page.dart rename gui/lib/src/{widget => }/page/host_page.dart (86%) rename gui/lib/src/{widget => }/page/info_page.dart (81%) rename gui/lib/src/{widget => }/page/play_page.dart (80%) rename gui/lib/src/{widget => }/page/settings_page.dart (82%) rename gui/lib/src/{page/page.dart => pager/abstract_page.dart} (86%) rename gui/lib/src/{page => pager}/page_suggestion.dart (76%) rename gui/lib/src/{page => pager}/page_type.dart (72%) rename gui/lib/src/{widget/page/home_page.dart => pager/pager.dart} (80%) rename gui/lib/src/{widget/file => tile}/file_setting_tile.dart (96%) rename gui/lib/src/{widget/window => tile}/info_tile.dart (97%) rename gui/lib/src/{widget/fluent => tile}/profile_tile.dart (89%) rename gui/lib/src/{widget/fluent => tile}/setting_tile.dart (87%) rename gui/lib/src/util/{types.dart => extensions.dart} (64%) create mode 100644 gui/lib/src/util/updater.dart delete mode 100644 gui/lib/src/widget/page/browser_page.dart delete mode 100644 gui/lib/src/widget/server/server_start_button.dart rename server_browser_backend/lib/src/{server_entry.dart => server_browser_entry.dart} (88%) create mode 100644 server_browser_backend/package.bat create mode 100644 server_browser_backend/package.sh diff --git a/common/lib/common.dart b/common/lib/common.dart index cc83a0e..2368e57 100644 --- a/common/lib/common.dart +++ b/common/lib/common.dart @@ -1,18 +1,21 @@ -export 'package:reboot_common/src/constant/backend.dart'; -export 'package:reboot_common/src/constant/game.dart'; -export 'package:reboot_common/src/constant/supabase.dart'; -export 'package:reboot_common/src/model/fortnite_build.dart'; -export 'package:reboot_common/src/model/fortnite_version.dart'; -export 'package:reboot_common/src/model/game_instance.dart'; -export 'package:reboot_common/src/model/server_result.dart'; -export 'package:reboot_common/src/model/server_type.dart'; -export 'package:reboot_common/src/model/update_status.dart'; -export 'package:reboot_common/src/model/update_timer.dart'; -export 'package:reboot_common/src/model/fortnite_server.dart'; -export 'package:reboot_common/src/model/dll.dart'; -export 'package:reboot_common/src/util/backend.dart'; -export 'package:reboot_common/src/util/downloader.dart'; +export 'package:reboot_common/src/backend/auth_backend_constants.dart'; +export 'package:reboot_common/src/backend/auth_backend_helper.dart'; +export 'package:reboot_common/src/backend/auth_backend_result.dart'; +export 'package:reboot_common/src/backend/auth_backend_type.dart'; + +export 'package:reboot_common/src/browser/server_browser_client.dart'; +export 'package:reboot_common/src/browser/server_browser_entry.dart'; +export 'package:reboot_common/src/browser/server_browser_event.dart'; +export 'package:reboot_common/src/browser/server_browser_state.dart'; + +export 'package:reboot_common/src/game/game_build.dart'; +export 'package:reboot_common/src/game/game_constants.dart'; +export 'package:reboot_common/src/game/game_dll.dart'; +export 'package:reboot_common/src/game/game_downloader.dart'; +export 'package:reboot_common/src/game/game_instance.dart'; +export 'package:reboot_common/src/game/game_metadata.dart'; +export 'package:reboot_common/src/game/game_version.dart'; + +export 'package:reboot_common/src/util/extensions.dart'; +export 'package:reboot_common/src/util/logger.dart'; export 'package:reboot_common/src/util/os.dart'; -export 'package:reboot_common/src/util/log.dart'; -export 'package:reboot_common/src/util/game.dart'; -export 'package:reboot_common/src/util/extensions.dart'; \ No newline at end of file diff --git a/common/lib/src/constant/backend.dart b/common/lib/src/backend/auth_backend_constants.dart similarity index 100% rename from common/lib/src/constant/backend.dart rename to common/lib/src/backend/auth_backend_constants.dart diff --git a/common/lib/src/util/backend.dart b/common/lib/src/backend/auth_backend_helper.dart similarity index 59% rename from common/lib/src/util/backend.dart rename to common/lib/src/backend/auth_backend_helper.dart index 21eda2a..f6e5015 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/backend/auth_backend_helper.dart @@ -15,93 +15,95 @@ final Semaphore _semaphore = Semaphore(); String? _lastIp; String? _lastPort; -Stream startBackend({ - required ServerType type, +typedef BackendErrorHandler = void Function(String); + +Stream startAuthBackend({ + required AuthBackendType type, required String host, required String port, required bool detached, - required void Function(String) onError + required BackendErrorHandler? onError }) async* { Process? process; HttpServer? server; try { host = host.trim(); port = port.trim(); - if(type != ServerType.local || port != kDefaultBackendPort.toString()) { - yield ServerResult(ServerResultType.starting); + if(type != AuthBackendType.local || port != kDefaultBackendPort.toString()) { + yield AuthBackendResult(AuthBackendResultType.starting); } if (host.isEmpty) { - yield ServerResult(ServerResultType.startMissingHostError); + yield AuthBackendResult(AuthBackendResultType.startMissingHostError); return; } if (port.isEmpty) { - yield ServerResult(ServerResultType.startMissingPortError); + yield AuthBackendResult(AuthBackendResultType.startMissingPortError); return; } final portNumber = int.tryParse(port); if (portNumber == null) { - yield ServerResult(ServerResultType.startIllegalPortError); + yield AuthBackendResult(AuthBackendResultType.startIllegalPortError); return; } - if ((type != ServerType.local || port != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { - yield ServerResult(ServerResultType.startFreeingPort); - final result = await freeBackendPort(); + if ((type != AuthBackendType.local || port != kDefaultBackendPort.toString()) && !(await isAuthBackendPortFree())) { + yield AuthBackendResult(AuthBackendResultType.startFreeingPort); + final result = await freeAuthBackendPort(); if(!result) { - yield ServerResult(ServerResultType.startFreePortError); + yield AuthBackendResult(AuthBackendResultType.startFreePortError); return; } - yield ServerResult(ServerResultType.startFreePortSuccess); + yield AuthBackendResult(AuthBackendResultType.startFreePortSuccess); } switch(type){ - case ServerType.embedded: - process = await startEmbeddedBackend(detached, onError: onError); - yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(process: process)); + case AuthBackendType.embedded: + process = await _startEmbedded(detached, onError: onError); + yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(process: process)); break; - case ServerType.remote: - yield ServerResult(ServerResultType.startPingingRemote); - final uriResult = await pingBackend(host, portNumber); + case AuthBackendType.remote: + yield AuthBackendResult(AuthBackendResultType.startPingingRemote); + final uriResult = await _ping(host, portNumber); if(uriResult == null) { - yield ServerResult(ServerResultType.startPingError); + yield AuthBackendResult(AuthBackendResultType.startPingError); return; } - server = await startRemoteBackendProxy(uriResult); - yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server)); + server = await _startRemote(uriResult); + yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(server: server)); break; - case ServerType.local: + case AuthBackendType.local: if(portNumber != kDefaultBackendPort) { - yield ServerResult(ServerResultType.startPingingLocal); - final uriResult = await pingBackend(kDefaultBackendHost, portNumber); + yield AuthBackendResult(AuthBackendResultType.startPingingLocal); + final uriResult = await _ping(kDefaultBackendHost, portNumber); if(uriResult == null) { - yield ServerResult(ServerResultType.startPingError); + yield AuthBackendResult(AuthBackendResultType.startPingError); return; } - server = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$port")); - yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server)); + server = await _startRemote(Uri.parse("http://$kDefaultBackendHost:$port")); + yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(server: server)); } break; } - yield ServerResult(ServerResultType.startPingingLocal); - final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort); + yield AuthBackendResult(AuthBackendResultType.startPingingLocal); + final uriResult = await _ping(kDefaultBackendHost, kDefaultBackendPort); if(uriResult == null) { - yield ServerResult(ServerResultType.startPingError); + yield AuthBackendResult(AuthBackendResultType.startPingError); process?.kill(ProcessSignal.sigterm); server?.close(force: true); return; } - yield ServerResult(ServerResultType.startSuccess); + yield AuthBackendResult(AuthBackendResultType.startSuccess); }catch(error, stackTrace) { - yield ServerResult( - ServerResultType.startError, + yield AuthBackendResult( + AuthBackendResultType.startError, error: error, stackTrace: stackTrace ); @@ -110,34 +112,7 @@ Stream startBackend({ } } -Stream stopBackend({required ServerType type, required ServerImplementation? implementation}) async* { - yield ServerResult(ServerResultType.stopping); - try{ - switch(type){ - case ServerType.embedded: - final process = implementation?.process; - if(process != null) { - Process.killPid(process.pid, ProcessSignal.sigterm); - } - break; - case ServerType.remote: - await implementation?.server?.close(force: true); - break; - case ServerType.local: - await implementation?.server?.close(force: true); - break; - } - yield ServerResult(ServerResultType.stopSuccess); - }catch(error, stackTrace){ - yield ServerResult( - ServerResultType.stopError, - error: error, - stackTrace: stackTrace - ); - } -} - -Future startEmbeddedBackend(bool detached, {void Function(String)? onError}) async { +Future _startEmbedded(bool detached, {BackendErrorHandler? onError}) async { final process = await startProcess( executable: backendStartExecutable, window: detached, @@ -164,22 +139,38 @@ Future startEmbeddedBackend(bool detached, {void Function(String)? onEr return process; } -Future startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort); +Future _startRemote(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort); -Future isBackendPortFree() async => await pingBackend(kDefaultBackendHost, kDefaultBackendPort) == null; - -Future freeBackendPort() async { - await killProcessByPort(kDefaultBackendPort); - await killProcessByPort(kDefaultXmppPort); - final standardResult = await isBackendPortFree(); - if(standardResult) { - return true; +Stream stopAuthBackend({required AuthBackendType type, required AuthBackendImplementation? implementation}) async* { + yield AuthBackendResult(AuthBackendResultType.stopping); + try{ + switch(type){ + case AuthBackendType.embedded: + final process = implementation?.process; + if(process != null) { + Process.killPid(process.pid, ProcessSignal.sigterm); + } + break; + case AuthBackendType.remote: + await implementation?.server?.close(force: true); + break; + case AuthBackendType.local: + await implementation?.server?.close(force: true); + break; + } + yield AuthBackendResult(AuthBackendResultType.stopSuccess); + }catch(error, stackTrace){ + yield AuthBackendResult( + AuthBackendResultType.stopError, + error: error, + stackTrace: stackTrace + ); } - - return false; } -Future pingBackend(String host, int port, [bool https=false]) async { +Future isAuthBackendPortFree() async => await _ping(kDefaultBackendHost, kDefaultBackendPort) == null; + +Future _ping(String host, int port, [bool https=false]) async { final hostName = host.replaceFirst("http://", "").replaceFirst("https://", ""); final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; try{ @@ -198,7 +189,7 @@ Future pingBackend(String host, int port, [bool https=false]) async { return uri; }catch(error) { log("[BACKEND] Cannot ping backend: $error"); - return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true); + return https || declaredScheme != null || isLocalHost(host) ? null : await _ping(host, port, true); } } @@ -236,7 +227,18 @@ Stream watchMatchmakingIp() async* { }); } -Future writeMatchmakingIp(String text) async { +Future freeAuthBackendPort() async { + await killProcessByPort(kDefaultBackendPort); + await killProcessByPort(kDefaultXmppPort); + final standardResult = await isAuthBackendPortFree(); + if(standardResult) { + return true; + } + + return false; +} + +Future writeAuthBackendMatchmakingIp(String text) async { final exists = await matchmakerConfigFile.exists(); if(!exists) { return; diff --git a/common/lib/src/model/server_result.dart b/common/lib/src/backend/auth_backend_result.dart similarity index 51% rename from common/lib/src/model/server_result.dart rename to common/lib/src/backend/auth_backend_result.dart index 8ca28d3..57ae0be 100644 --- a/common/lib/src/model/server_result.dart +++ b/common/lib/src/backend/auth_backend_result.dart @@ -1,12 +1,12 @@ import 'dart:io'; -class ServerResult { - final ServerResultType type; - final ServerImplementation? implementation; +class AuthBackendResult { + final AuthBackendResultType type; + final AuthBackendImplementation? implementation; final Object? error; final StackTrace? stackTrace; - ServerResult(this.type, {this.implementation, this.error, this.stackTrace}); + AuthBackendResult(this.type, {this.implementation, this.error, this.stackTrace}); @override String toString() { @@ -14,14 +14,14 @@ class ServerResult { } } -class ServerImplementation { +class AuthBackendImplementation { final Process? process; final HttpServer? server; - ServerImplementation({this.process, this.server}); + AuthBackendImplementation({this.process, this.server}); } -enum ServerResultType { +enum AuthBackendResultType { starting, startMissingHostError, startMissingPortError, @@ -39,9 +39,9 @@ enum ServerResultType { stopSuccess, stopError; - bool get isStart => name.contains("start"); + bool get isStart => name.startsWith("start"); - bool get isError => name.contains("Error"); + bool get isError => name.endsWith("Error"); - bool get isSuccess => this == ServerResultType.startSuccess || this == ServerResultType.stopSuccess; + bool get isSuccess => this == AuthBackendResultType.startSuccess || this == AuthBackendResultType.stopSuccess; } \ No newline at end of file diff --git a/common/lib/src/model/server_type.dart b/common/lib/src/backend/auth_backend_type.dart similarity index 57% rename from common/lib/src/model/server_type.dart rename to common/lib/src/backend/auth_backend_type.dart index 8018df2..7557880 100644 --- a/common/lib/src/model/server_type.dart +++ b/common/lib/src/backend/auth_backend_type.dart @@ -1,4 +1,4 @@ -enum ServerType { +enum AuthBackendType { embedded, remote, local diff --git a/common/lib/src/browser/server_browser_client.dart b/common/lib/src/browser/server_browser_client.dart new file mode 100644 index 0000000..130fb95 --- /dev/null +++ b/common/lib/src/browser/server_browser_client.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:reboot_common/common.dart'; + +class ServerBrowserClient { + static const String _pingEvent = 'ping'; + static const String _addEvent = 'add'; + static const String _removeEvent = 'remove'; + static const Duration _pingInterval = const Duration(seconds: 30); + static const Duration _reconnectDelay = const Duration(seconds: 10); + static const Duration _timeout = const Duration(seconds: 10); + + final String _serverUrl; + final StreamController _eventsController = StreamController.broadcast(); + + WebSocket? _socket; + Timer? _reconnectTimer; + Timer? _pingTimer; + Completer _pingCompleter = Completer(); + ServerBrowserState _state = ServerBrowserState.disconnected; + + ServerBrowserClient({required String serverUrl}) + : _serverUrl = serverUrl; + + Future connect() async { + if (_state != ServerBrowserState.disconnected) { + return; + } + + _setState(ServerBrowserState.connecting); + _reconnectTimer?.cancel(); + + try { + final socket = await WebSocket.connect(_serverUrl) + .timeout(_timeout); + _socket = socket; + _setState(ServerBrowserState.connected); + socket.listen( + _handleMessage, + onDone: () => _handleDisconnection(_state == ServerBrowserState.connected), + onError: (error) { + _eventsController.add(new ServerBrowserErrorEvent( + error: 'An unhandled error was thrown: $error' + )); + _handleDisconnection(true); + } + ); + _startHeartbeat(); + } catch (e) { + _eventsController.add(new ServerBrowserErrorEvent( + error: 'Cannot connect: $e' + )); + _handleDisconnection(true); + } + } + + void _handleMessage(dynamic data) { + try { + final message = jsonDecode(data); + final type = message['type']; + final payload = message['data']; + switch (type) { + case _pingEvent: + if(!_pingCompleter.isCompleted) { + _pingCompleter.complete(null); + } + break; + + case _addEvent: + if (payload is List) { + final entries = payload + .map((entry) => ServerBrowserEntry.fromJson(entry)) + .toList(growable: false); + _eventsController.add(new ServerBrowserAddEvent( + entries: entries, + )); + }else { + _eventsController.add(new ServerBrowserErrorEvent( + error: 'Invalid add event payload: ${payload?.runtimeType}' + )); + } + break; + + case _removeEvent: + if (payload is List) { + final entries = payload + .map((entry) => entry['id'] as String?) + .whereType() + .toList(growable: false); + _eventsController.add(new ServerBrowserRemoveEvent( + entries: entries + )); + }else { + _eventsController.add(new ServerBrowserErrorEvent( + error: 'Invalid remove event payload: ${payload?.runtimeType}' + )); + } + break; + + default: + _eventsController.add(new ServerBrowserErrorEvent( + error: 'Invalid event type: $type' + )); + break; + } + } catch (error) { + _eventsController.add(new ServerBrowserErrorEvent( + error: 'An error occurred while processing an event: $error' + )); + } + } + + void _handleDisconnection(bool reconnect) { + _setState(ServerBrowserState.disconnected); + _cleanup(); + if (reconnect) { + _reconnectTimer = Timer(_reconnectDelay, () => connect()); + } + } + + void _startHeartbeat() { + _pingTimer?.cancel(); + _pingTimer = Timer.periodic(_pingInterval, (timer) async { + final socket = _socket; + if(socket == null || _state != ServerBrowserState.connected) { + return; + } + + try { + socket.add(jsonEncode({'type': _pingEvent})); + await _pingCompleter.future + .timeout(_timeout); + _pingCompleter = Completer(); + } catch (error) { + _pingCompleter = Completer(); + _handleDisconnection(true); + } + }); + } + + void _cleanup() { + _socket?.close(); + _socket = null; + _pingTimer?.cancel(); + _pingTimer = null; + } + + void _setState(ServerBrowserState newState) { + if (_state != newState) { + _state = newState; + _eventsController.add(ServerBrowserStateEvent( + state: newState + )); + } + } + + Future addEntry(ServerBrowserEntry entry) async { + if (_state != ServerBrowserState.connected) { + return false; + } + + final socket = _socket; + if(socket == null) { + return false; + } + + final message = { + 'type': _addEvent, + 'data': entry.toJson() + }; + socket.add(jsonEncode(message)); + return true; + } + + Future removeEntry(String id) async { + if (_state != ServerBrowserState.connected) { + return false; + } + + final socket = _socket; + if(socket == null) { + return false; + } + + final message = { + 'type': _removeEvent, + 'data': id + }; + socket.add(jsonEncode(message)); + return true; + } + + StreamSubscription addListener(void Function(ServerBrowserEvent) onData) { + return _eventsController.stream.listen(onData); + } + + Future disconnect() async { + _reconnectTimer?.cancel(); + _cleanup(); + _setState(ServerBrowserState.disconnected); + } + + void dispose() { + disconnect(); + _eventsController.close(); + } +} \ No newline at end of file diff --git a/common/lib/src/model/fortnite_server.dart b/common/lib/src/browser/server_browser_entry.dart similarity index 71% rename from common/lib/src/model/fortnite_server.dart rename to common/lib/src/browser/server_browser_entry.dart index 4084e1c..43ca524 100644 --- a/common/lib/src/model/fortnite_server.dart +++ b/common/lib/src/browser/server_browser_entry.dart @@ -1,15 +1,14 @@ -class FortniteServer { +class ServerBrowserEntry { final String id; final String name; final String description; final String author; final String ip; final String version; - final String? password; + final String password; final DateTime timestamp; - final bool discoverable; - FortniteServer({ + ServerBrowserEntry({ required this.id, required this.name, required this.description, @@ -18,10 +17,9 @@ class FortniteServer { required this.version, required this.password, required this.timestamp, - required this.discoverable }); - factory FortniteServer.fromJson(json) => FortniteServer( + factory ServerBrowserEntry.fromJson(json) => ServerBrowserEntry( id: json["id"], name: json["name"], description: json["description"], @@ -29,8 +27,7 @@ class FortniteServer { ip: json["ip"], version: json["version"], password: json["password"], - timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now(), - discoverable: json["discoverable"] ?? false + timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now() ); Map toJson() => { @@ -41,7 +38,6 @@ class FortniteServer { "ip": ip, "version": version, "password": password, - "timestamp": timestamp.toString(), - "discoverable": discoverable + "timestamp": timestamp.toString() }; -} +} \ No newline at end of file diff --git a/common/lib/src/browser/server_browser_event.dart b/common/lib/src/browser/server_browser_event.dart new file mode 100644 index 0000000..a135f9a --- /dev/null +++ b/common/lib/src/browser/server_browser_event.dart @@ -0,0 +1,30 @@ +import 'package:reboot_common/common.dart'; +import 'package:reboot_common/src/browser/server_browser_state.dart'; + +sealed class ServerBrowserEvent { + +} + +final class ServerBrowserStateEvent extends ServerBrowserEvent { + final ServerBrowserState state; + + ServerBrowserStateEvent({required this.state}); +} + +final class ServerBrowserAddEvent extends ServerBrowserEvent { + final List entries; + + ServerBrowserAddEvent({required this.entries}); +} + +final class ServerBrowserRemoveEvent extends ServerBrowserEvent { + final List entries; + + ServerBrowserRemoveEvent({required this.entries}); +} + +final class ServerBrowserErrorEvent extends ServerBrowserEvent { + final String error; + + ServerBrowserErrorEvent({required this.error}); +} \ No newline at end of file diff --git a/common/lib/src/browser/server_browser_state.dart b/common/lib/src/browser/server_browser_state.dart new file mode 100644 index 0000000..a4de64f --- /dev/null +++ b/common/lib/src/browser/server_browser_state.dart @@ -0,0 +1,5 @@ +enum ServerBrowserState { + disconnected, + connecting, + connected +} \ No newline at end of file diff --git a/common/lib/src/constant/supabase.dart b/common/lib/src/constant/supabase.dart deleted file mode 100644 index 6e6378f..0000000 --- a/common/lib/src/constant/supabase.dart +++ /dev/null @@ -1,2 +0,0 @@ -const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co'; -const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M'; \ No newline at end of file diff --git a/common/lib/src/game/game_build.dart b/common/lib/src/game/game_build.dart new file mode 100644 index 0000000..7885623 --- /dev/null +++ b/common/lib/src/game/game_build.dart @@ -0,0 +1,64 @@ +import 'dart:io'; +import 'dart:isolate'; + +class GameBuild { + final String gameVersion; + final String link; + final bool available; + + GameBuild({ + required this.gameVersion, + required this.link, + required this.available + }); +} + +class GameBuildDownloadProgress { + final double progress; + final int? timeLeft; + final bool extracting; + final int speed; + + GameBuildDownloadProgress({ + required this.progress, + required this.extracting, + required this.timeLeft, + required this.speed + }); +} + +class GameBuildDownloadOptions { + GameBuild build; + Directory destination; + SendPort port; + + GameBuildDownloadOptions(this.build, this.destination, this.port); +} + +class GameBuildManifestChunk { + List chunksIds; + String file; + int fileSize; + + GameBuildManifestChunk._internal(this.chunksIds, this.file, this.fileSize); + + factory GameBuildManifestChunk.fromJson(json) => GameBuildManifestChunk._internal( + List.from(json["ChunksIds"] as List), + json["File"], + json["FileSize"] + ); +} + +class GameBuildManifestFile { + String name; + List chunks; + int size; + + GameBuildManifestFile._internal(this.name, this.chunks, this.size); + + factory GameBuildManifestFile.fromJson(json) => GameBuildManifestFile._internal( + json["Name"], + List.from(json["Chunks"].map((chunk) => GameBuildManifestChunk.fromJson(chunk))), + json["Size"] + ); +} diff --git a/common/lib/src/constant/game.dart b/common/lib/src/game/game_constants.dart similarity index 100% rename from common/lib/src/constant/game.dart rename to common/lib/src/game/game_constants.dart diff --git a/common/lib/src/game/game_dll.dart b/common/lib/src/game/game_dll.dart new file mode 100644 index 0000000..e3d9f03 --- /dev/null +++ b/common/lib/src/game/game_dll.dart @@ -0,0 +1,10 @@ +enum GameDll { + console, + auth, + gameServer, + memoryLeak +} + +extension InjectableDllVersionAware on GameDll { + bool get isVersionDependent => this == GameDll.gameServer; +} diff --git a/common/lib/src/util/downloader.dart b/common/lib/src/game/game_downloader.dart similarity index 62% rename from common/lib/src/util/downloader.dart rename to common/lib/src/game/game_downloader.dart index 5372981..33efc39 100644 --- a/common/lib/src/util/downloader.dart +++ b/common/lib/src/game/game_downloader.dart @@ -30,96 +30,97 @@ final int _ariaPort = 6800; final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc'); final Duration _ariaMaxSpawnTime = const Duration(seconds: 10); final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); -final List downloadableBuilds = [ - FortniteBuild(gameVersion: "1.7.2", link: "https://public.simplyblk.xyz/1.7.2.zip", available: true), - FortniteBuild(gameVersion: "1.8", link: "https://public.simplyblk.xyz/1.8.rar", available: true), - FortniteBuild(gameVersion: "1.8.1", link: "https://public.simplyblk.xyz/1.8.1.rar", available: true), - FortniteBuild(gameVersion: "1.8.2", link: "https://public.simplyblk.xyz/1.8.2.rar", available: true), - FortniteBuild(gameVersion: "1.9", link: "https://public.simplyblk.xyz/1.9.rar", available: true), - FortniteBuild(gameVersion: "1.9.1", link: "https://public.simplyblk.xyz/1.9.1.rar", available: true), - FortniteBuild(gameVersion: "1.10", link: "https://public.simplyblk.xyz/1.10.rar", available: true), - FortniteBuild(gameVersion: "1.11", link: "https://public.simplyblk.xyz/1.11.zip", available: true), - FortniteBuild(gameVersion: "2.1.0", link: "https://public.simplyblk.xyz/2.1.0.zip", available: true), - FortniteBuild(gameVersion: "2.2.0", link: "https://public.simplyblk.xyz/2.2.0.rar", available: true), - FortniteBuild(gameVersion: "2.3", link: "https://public.simplyblk.xyz/2.3.rar", available: true), - FortniteBuild(gameVersion: "2.4.0", link: "https://public.simplyblk.xyz/2.4.0.zip", available: true), - FortniteBuild(gameVersion: "2.4.2", link: "https://public.simplyblk.xyz/2.4.2.zip", available: true), - FortniteBuild(gameVersion: "2.5.0", link: "https://public.simplyblk.xyz/2.5.0.rar", available: true), - FortniteBuild(gameVersion: "3.0", link: "https://public.simplyblk.xyz/3.0.zip", available: true), - FortniteBuild(gameVersion: "3.1", link: "https://public.simplyblk.xyz/3.1.rar", available: true), - FortniteBuild(gameVersion: "3.1.1", link: "https://public.simplyblk.xyz/3.1.1.zip", available: true), - FortniteBuild(gameVersion: "3.2", link: "https://public.simplyblk.xyz/3.2.zip", available: true), - FortniteBuild(gameVersion: "3.3", link: "https://public.simplyblk.xyz/3.3.rar", available: true), - FortniteBuild(gameVersion: "3.5", link: "https://public.simplyblk.xyz/3.5.rar", available: true), - FortniteBuild(gameVersion: "3.6", link: "https://public.simplyblk.xyz/3.6.zip", available: true), - FortniteBuild(gameVersion: "4.0", link: "https://public.simplyblk.xyz/4.0.zip", available: true), - FortniteBuild(gameVersion: "4.1", link: "https://public.simplyblk.xyz/4.1.zip", available: true), - FortniteBuild(gameVersion: "4.2", link: "https://public.simplyblk.xyz/4.2.zip", available: true), - FortniteBuild(gameVersion: "4.4", link: "https://public.simplyblk.xyz/4.4.rar", available: true), - FortniteBuild(gameVersion: "4.5", link: "https://public.simplyblk.xyz/4.5.rar", available: true), - FortniteBuild(gameVersion: "5.00", link: "https://public.simplyblk.xyz/5.00.rar", available: true), - FortniteBuild(gameVersion: "5.0.1", link: "https://public.simplyblk.xyz/5.0.1.rar", available: true), - FortniteBuild(gameVersion: "5.10", link: "https://public.simplyblk.xyz/5.10.rar", available: true), - FortniteBuild(gameVersion: "5.21", link: "https://public.simplyblk.xyz/5.21.rar", available: true), - FortniteBuild(gameVersion: "5.30", link: "https://public.simplyblk.xyz/5.30.rar", available: true), - FortniteBuild(gameVersion: "5.40", link: "https://public.simplyblk.xyz/5.40.rar", available: true), - FortniteBuild(gameVersion: "6.00", link: "https://public.simplyblk.xyz/6.00.rar", available: true), - FortniteBuild(gameVersion: "6.01", link: "https://public.simplyblk.xyz/6.01.rar", available: true), - FortniteBuild(gameVersion: "6.1.1", link: "https://public.simplyblk.xyz/6.1.1.rar", available: true), - FortniteBuild(gameVersion: "6.02", link: "https://public.simplyblk.xyz/6.02.rar", available: true), - FortniteBuild(gameVersion: "6.2.1", link: "https://public.simplyblk.xyz/6.2.1.rar", available: true), - FortniteBuild(gameVersion: "6.10", link: "https://public.simplyblk.xyz/6.10.rar", available: true), - FortniteBuild(gameVersion: "6.10.1", link: "https://public.simplyblk.xyz/6.10.1.rar", available: true), - FortniteBuild(gameVersion: "6.10.2", link: "https://public.simplyblk.xyz/6.10.2.rar", available: true), - FortniteBuild(gameVersion: "6.21", link: "https://public.simplyblk.xyz/6.21.rar", available: true), - FortniteBuild(gameVersion: "6.22", link: "https://public.simplyblk.xyz/6.22.rar", available: true), - FortniteBuild(gameVersion: "6.30", link: "https://public.simplyblk.xyz/6.30.rar", available: true), - FortniteBuild(gameVersion: "6.31", link: "https://public.simplyblk.xyz/6.31.rar", available: true), - FortniteBuild(gameVersion: "7.00", link: "https://public.simplyblk.xyz/7.00.rar", available: true), - FortniteBuild(gameVersion: "7.10", link: "https://public.simplyblk.xyz/7.10.rar", available: true), - FortniteBuild(gameVersion: "7.20", link: "https://public.simplyblk.xyz/7.20.rar", available: true), - FortniteBuild(gameVersion: "7.30", link: "https://public.simplyblk.xyz/7.30.zip", available: true), - FortniteBuild(gameVersion: "7.40", link: "https://public.simplyblk.xyz/7.40.rar", available: true), - FortniteBuild(gameVersion: "8.00", link: "https://public.simplyblk.xyz/8.00.zip", available: true), - FortniteBuild(gameVersion: "8.20", link: "https://public.simplyblk.xyz/8.20.rar", available: true), - FortniteBuild(gameVersion: "8.30", link: "https://public.simplyblk.xyz/8.30.rar", available: true), - FortniteBuild(gameVersion: "8.40", link: "https://public.simplyblk.xyz/8.40.zip", available: true), - FortniteBuild(gameVersion: "8.50", link: "https://public.simplyblk.xyz/8.50.zip", available: true), - FortniteBuild(gameVersion: "8.51", link: "https://public.simplyblk.xyz/8.51.rar", available: true), - FortniteBuild(gameVersion: "9.00", link: "https://public.simplyblk.xyz/9.00.zip", available: true), - FortniteBuild(gameVersion: "9.01", link: "https://public.simplyblk.xyz/9.01.zip", available: true), - FortniteBuild(gameVersion: "9.10", link: "https://public.simplyblk.xyz/9.10.rar", available: true), - FortniteBuild(gameVersion: "9.21", link: "https://public.simplyblk.xyz/9.21.zip", available: true), - FortniteBuild(gameVersion: "9.30", link: "https://public.simplyblk.xyz/9.30.zip", available: true), - FortniteBuild(gameVersion: "9.40", link: "https://public.simplyblk.xyz/9.40.zip", available: true), - FortniteBuild(gameVersion: "9.41", link: "https://public.simplyblk.xyz/9.41.rar", available: true), - FortniteBuild(gameVersion: "10.00", link: "https://public.simplyblk.xyz/10.00.zip", available: true), - FortniteBuild(gameVersion: "10.10", link: "https://public.simplyblk.xyz/10.10.zip", available: true), - FortniteBuild(gameVersion: "10.20", link: "https://public.simplyblk.xyz/10.20.zip", available: true), - FortniteBuild(gameVersion: "10.31", link: "https://public.simplyblk.xyz/10.31.zip", available: true), - FortniteBuild(gameVersion: "10.40", link: "https://public.simplyblk.xyz/10.40.rar", available: true), - FortniteBuild(gameVersion: "11.00", link: "https://public.simplyblk.xyz/11.00.zip", available: true), - FortniteBuild(gameVersion: "11.31", link: "https://public.simplyblk.xyz/11.31.rar", available: true), - FortniteBuild(gameVersion: "12.00", link: "https://public.simplyblk.xyz/12.00.rar", available: true), - FortniteBuild(gameVersion: "12.21", link: "https://public.simplyblk.xyz/12.21.zip", available: true), - FortniteBuild(gameVersion: "12.50", link: "https://public.simplyblk.xyz/12.50.zip", available: true), - FortniteBuild(gameVersion: "12.61", link: "https://public.simplyblk.xyz/12.61.zip", available: true), - FortniteBuild(gameVersion: "13.00", link: "https://public.simplyblk.xyz/13.00.rar", available: true), - FortniteBuild(gameVersion: "13.40", link: "https://public.simplyblk.xyz/13.40.zip", available: true), - FortniteBuild(gameVersion: "14.00", link: "https://public.simplyblk.xyz/14.00.rar", available: true), - FortniteBuild(gameVersion: "14.40", link: "https://public.simplyblk.xyz/14.40.rar", available: true), - FortniteBuild(gameVersion: "14.60", link: "https://public.simplyblk.xyz/14.60.rar", available: true), - FortniteBuild(gameVersion: "15.30", link: "https://public.simplyblk.xyz/15.30.rar", available: true), - FortniteBuild(gameVersion: "16.40", link: "https://public.simplyblk.xyz/16.40.rar", available: true), - FortniteBuild(gameVersion: "17.30", link: "https://public.simplyblk.xyz/17.30.zip", available: true), - FortniteBuild(gameVersion: "17.50", link: "https://public.simplyblk.xyz/17.50.zip", available: true), - FortniteBuild(gameVersion: "18.40", link: "https://public.simplyblk.xyz/18.40.zip", available: true), - FortniteBuild(gameVersion: "19.10", link: "https://public.simplyblk.xyz/19.10.rar", available: true), - FortniteBuild(gameVersion: "20.40", link: "https://public.simplyblk.xyz/20.40.zip", available: true), +final List downloadableBuilds = [ + GameBuild(gameVersion: "1.7.2", link: "https://builds.rebootfn.org/1.7.2.zip", available: true), + GameBuild(gameVersion: "1.8", link: "https://builds.rebootfn.org/1.8.rar", available: true), + GameBuild(gameVersion: "1.8.1", link: "https://builds.rebootfn.org/1.8.1.rar", available: true), + GameBuild(gameVersion: "1.8.2", link: "https://builds.rebootfn.org/1.8.2.rar", available: true), + GameBuild(gameVersion: "1.9", link: "https://builds.rebootfn.org/1.9.rar", available: true), + GameBuild(gameVersion: "1.9.1", link: "https://builds.rebootfn.org/1.9.1.rar", available: true), + GameBuild(gameVersion: "1.10", link: "https://builds.rebootfn.org/1.10.rar", available: true), + GameBuild(gameVersion: "1.11", link: "https://builds.rebootfn.org/1.11.zip", available: true), + GameBuild(gameVersion: "2.1.0", link: "https://builds.rebootfn.org/2.1.0.zip", available: true), + GameBuild(gameVersion: "2.2.0", link: "https://builds.rebootfn.org/2.2.0.rar", available: true), + GameBuild(gameVersion: "2.3", link: "https://builds.rebootfn.org/2.3.rar", available: true), + GameBuild(gameVersion: "2.4.0", link: "https://builds.rebootfn.org/2.4.0.zip", available: true), + GameBuild(gameVersion: "2.4.2", link: "https://builds.rebootfn.org/2.4.2.zip", available: true), + GameBuild(gameVersion: "2.5.0", link: "https://builds.rebootfn.org/2.5.0.rar", available: true), + GameBuild(gameVersion: "3.0", link: "https://builds.rebootfn.org/3.0.zip", available: true), + GameBuild(gameVersion: "3.1", link: "https://builds.rebootfn.org/3.1.rar", available: true), + GameBuild(gameVersion: "3.1.1", link: "https://builds.rebootfn.org/3.1.1.zip", available: true), + GameBuild(gameVersion: "3.2", link: "https://builds.rebootfn.org/3.2.zip", available: true), + GameBuild(gameVersion: "3.3", link: "https://builds.rebootfn.org/3.3.rar", available: true), + GameBuild(gameVersion: "3.5", link: "https://builds.rebootfn.org/3.5.rar", available: true), + GameBuild(gameVersion: "3.6", link: "https://builds.rebootfn.org/3.6.zip", available: true), + GameBuild(gameVersion: "4.0", link: "https://builds.rebootfn.org/4.0.zip", available: true), + GameBuild(gameVersion: "4.1", link: "https://builds.rebootfn.org/4.1.zip", available: true), + GameBuild(gameVersion: "4.2", link: "https://builds.rebootfn.org/4.2.zip", available: true), + GameBuild(gameVersion: "4.4", link: "https://builds.rebootfn.org/4.4.rar", available: true), + GameBuild(gameVersion: "4.5", link: "https://builds.rebootfn.org/4.5.rar", available: true), + GameBuild(gameVersion: "5.00", link: "https://builds.rebootfn.org/5.00.rar", available: true), + GameBuild(gameVersion: "5.0.1", link: "https://builds.rebootfn.org/5.0.1.rar", available: true), + GameBuild(gameVersion: "5.10", link: "https://builds.rebootfn.org/5.10.rar", available: true), + GameBuild(gameVersion: "5.21", link: "https://builds.rebootfn.org/5.21.rar", available: true), + GameBuild(gameVersion: "5.30", link: "https://builds.rebootfn.org/5.30.rar", available: true), + GameBuild(gameVersion: "5.40", link: "https://builds.rebootfn.org/5.40.rar", available: true), + GameBuild(gameVersion: "6.00", link: "https://builds.rebootfn.org/6.00.rar", available: true), + GameBuild(gameVersion: "6.01", link: "https://builds.rebootfn.org/6.01.rar", available: true), + GameBuild(gameVersion: "6.1.1", link: "https://builds.rebootfn.org/6.1.1.rar", available: true), + GameBuild(gameVersion: "6.02", link: "https://builds.rebootfn.org/6.02.rar", available: true), + GameBuild(gameVersion: "6.2.1", link: "https://builds.rebootfn.org/6.2.1.rar", available: true), + GameBuild(gameVersion: "6.10", link: "https://builds.rebootfn.org/6.10.rar", available: true), + GameBuild(gameVersion: "6.10.1", link: "https://builds.rebootfn.org/6.10.1.rar", available: true), + GameBuild(gameVersion: "6.10.2", link: "https://builds.rebootfn.org/6.10.2.rar", available: true), + GameBuild(gameVersion: "6.21", link: "https://builds.rebootfn.org/6.21.rar", available: true), + GameBuild(gameVersion: "6.22", link: "https://builds.rebootfn.org/6.22.rar", available: true), + GameBuild(gameVersion: "6.30", link: "https://builds.rebootfn.org/6.30.rar", available: true), + GameBuild(gameVersion: "6.31", link: "https://builds.rebootfn.org/6.31.rar", available: true), + GameBuild(gameVersion: "7.00", link: "https://builds.rebootfn.org/7.00.rar", available: true), + GameBuild(gameVersion: "7.10", link: "https://builds.rebootfn.org/7.10.rar", available: true), + GameBuild(gameVersion: "7.20", link: "https://builds.rebootfn.org/7.20.rar", available: true), + GameBuild(gameVersion: "7.30", link: "https://builds.rebootfn.org/7.30.zip", available: true), + GameBuild(gameVersion: "7.40", link: "https://builds.rebootfn.org/7.40.rar", available: true), + GameBuild(gameVersion: "8.00", link: "https://builds.rebootfn.org/8.00.zip", available: true), + GameBuild(gameVersion: "8.20", link: "https://builds.rebootfn.org/8.20.rar", available: true), + GameBuild(gameVersion: "8.30", link: "https://builds.rebootfn.org/8.30.rar", available: true), + GameBuild(gameVersion: "8.40", link: "https://builds.rebootfn.org/8.40.zip", available: true), + GameBuild(gameVersion: "8.50", link: "https://builds.rebootfn.org/8.50.zip", available: true), + GameBuild(gameVersion: "8.51", link: "https://builds.rebootfn.org/8.51.rar", available: true), + GameBuild(gameVersion: "9.00", link: "https://builds.rebootfn.org/9.00.zip", available: true), + GameBuild(gameVersion: "9.01", link: "https://builds.rebootfn.org/9.01.zip", available: true), + GameBuild(gameVersion: "9.10", link: "https://builds.rebootfn.org/9.10.rar", available: true), + GameBuild(gameVersion: "9.21", link: "https://builds.rebootfn.org/9.21.zip", available: true), + GameBuild(gameVersion: "9.30", link: "https://builds.rebootfn.org/9.30.zip", available: true), + GameBuild(gameVersion: "9.40", link: "https://builds.rebootfn.org/9.40.zip", available: true), + GameBuild(gameVersion: "9.41", link: "https://builds.rebootfn.org/9.41.rar", available: true), + GameBuild(gameVersion: "10.00", link: "https://builds.rebootfn.org/10.00.zip", available: true), + GameBuild(gameVersion: "10.10", link: "https://builds.rebootfn.org/10.10.zip", available: true), + GameBuild(gameVersion: "10.20", link: "https://builds.rebootfn.org/10.20.zip", available: true), + GameBuild(gameVersion: "10.31", link: "https://builds.rebootfn.org/10.31.zip", available: true), + GameBuild(gameVersion: "10.40", link: "https://builds.rebootfn.org/10.40.rar", available: false), + GameBuild(gameVersion: "11.00", link: "https://builds.rebootfn.org/11.00.zip", available: false), + GameBuild(gameVersion: "11.31", link: "https://builds.rebootfn.org/11.31.rar", available: false), + GameBuild(gameVersion: "12.00", link: "https://builds.rebootfn.org/12.00.rar", available: false), + GameBuild(gameVersion: "12.21", link: "https://builds.rebootfn.org/12.21.zip", available: false), + GameBuild(gameVersion: "Fortnite 12.41", link: "https://builds.rebootfn.org/Fortnite%2012.41.zip", available: false), + GameBuild(gameVersion: "12.50", link: "https://builds.rebootfn.org/12.50.zip", available: false), + GameBuild(gameVersion: "12.61", link: "https://builds.rebootfn.org/12.61.zip", available: false), + GameBuild(gameVersion: "13.00", link: "https://builds.rebootfn.org/13.00.rar", available: false), + GameBuild(gameVersion: "13.40", link: "https://builds.rebootfn.org/13.40.zip", available: false), + GameBuild(gameVersion: "14.00", link: "https://builds.rebootfn.org/14.00.rar", available: false), + GameBuild(gameVersion: "14.40", link: "https://builds.rebootfn.org/14.40.rar", available: false), + GameBuild(gameVersion: "14.60", link: "https://builds.rebootfn.org/14.60.rar", available: false), + GameBuild(gameVersion: "15.30", link: "https://builds.rebootfn.org/15.30.rar", available: false), + GameBuild(gameVersion: "16.40", link: "https://builds.rebootfn.org/16.40.rar", available: false), + GameBuild(gameVersion: "17.30", link: "https://builds.rebootfn.org/17.30.zip", available: false), + GameBuild(gameVersion: "17.50", link: "https://builds.rebootfn.org/17.50.zip", available: false), + GameBuild(gameVersion: "18.40", link: "https://builds.rebootfn.org/18.40.zip", available: false), + GameBuild(gameVersion: "19.10", link: "https://builds.rebootfn.org/19.10.rar", available: false), + GameBuild(gameVersion: "20.40", link: "https://builds.rebootfn.org/20.40.zip", available: false) ]; -Future downloadArchiveBuild(FortniteBuildDownloadOptions options) async { +Future downloadArchiveBuild(GameBuildDownloadOptions options) async { final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1); final outputFile = File("${options.destination.path}\\.build\\$fileName"); Timer? timer; @@ -259,7 +260,7 @@ Future _isAriaRunning() async { } } -Future _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async { +Future _startAriaDownload(GameBuildDownloadOptions options, File outputFile) async { http.Response? addDownloadResponse; try { final addDownloadRequestId = Uuid().toString().replaceAll("-", ""); @@ -311,7 +312,7 @@ Future stopDownloadServer() async { } -Future _extractArchive(Completer stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async { +Future _extractArchive(Completer stopped, String extension, File tempFile, GameBuildDownloadOptions options) async { Process? process; switch (extension.toLowerCase()) { case ".zip": @@ -437,7 +438,7 @@ Future _extractArchive(Completer stopped, String extension, File void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) { if(percentage == 0) { - port.send(FortniteBuildDownloadProgress( + port.send(GameBuildDownloadProgress( progress: percentage, extracting: extracting, timeLeft: null, @@ -446,7 +447,7 @@ void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, b return; } - port.send(FortniteBuildDownloadProgress( + port.send(GameBuildDownloadProgress( progress: percentage, extracting: extracting, timeLeft: minutesLeft, @@ -454,13 +455,13 @@ void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, b )); } -void _onError(Object? error, FortniteBuildDownloadOptions options) { +void _onError(Object? error, GameBuildDownloadOptions options) { if(error != null) { options.port.send(error.toString()); } } -Completer _setupLifecycle(FortniteBuildDownloadOptions options) { +Completer _setupLifecycle(GameBuildDownloadOptions options) { var stopped = Completer(); var lifecyclePort = ReceivePort(); lifecyclePort.listen((message) { @@ -473,8 +474,6 @@ Completer _setupLifecycle(FortniteBuildDownloadOptions options) { return stopped; } - - Future hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async { final lastUpdate = await _getLastUpdate(lastUpdateMs); final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists(); @@ -482,16 +481,16 @@ Future hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours); } -Future downloadDependency(InjectableDll dll, String outputPath) async { +Future downloadDependency(GameDll dll, String outputPath) async { String? name; switch(dll) { - case InjectableDll.console: + case GameDll.console: name = "console.dll"; - case InjectableDll.auth: + case GameDll.auth: name = "cobalt.dll"; - case InjectableDll.memoryLeak: + case GameDll.memoryLeak: name = "memory.dll"; - case InjectableDll.gameServer: + case GameDll.gameServer: name = null; } if(name == null) { diff --git a/common/lib/src/model/game_instance.dart b/common/lib/src/game/game_instance.dart similarity index 96% rename from common/lib/src/model/game_instance.dart rename to common/lib/src/game/game_instance.dart index 42d6be0..0b943e3 100644 --- a/common/lib/src/model/game_instance.dart +++ b/common/lib/src/game/game_instance.dart @@ -9,7 +9,7 @@ class GameInstance { final int gamePid; final int? launcherPid; final int? eacPid; - final List injectedDlls; + final List injectedDlls; final bool headless; bool launched; bool tokenError; diff --git a/common/lib/src/util/game.dart b/common/lib/src/game/game_metadata.dart similarity index 100% rename from common/lib/src/util/game.dart rename to common/lib/src/game/game_metadata.dart diff --git a/common/lib/src/model/fortnite_version.dart b/common/lib/src/game/game_version.dart similarity index 68% rename from common/lib/src/model/fortnite_version.dart rename to common/lib/src/game/game_version.dart index f658ab7..5f81208 100644 --- a/common/lib/src/model/fortnite_version.dart +++ b/common/lib/src/game/game_version.dart @@ -1,16 +1,16 @@ import 'dart:io'; -class FortniteVersion { +class GameVersion { String name; String gameVersion; Directory location; - FortniteVersion.fromJson(json) + GameVersion.fromJson(json) : name = json["name"], gameVersion = json["gameVersion"], location = Directory(json["location"]); - FortniteVersion({required this.name, required this.gameVersion, required this.location}); + GameVersion({required this.name, required this.gameVersion, required this.location}); Map toJson() => { 'name': name, diff --git a/common/lib/src/model/dll.dart b/common/lib/src/model/dll.dart deleted file mode 100644 index d950879..0000000 --- a/common/lib/src/model/dll.dart +++ /dev/null @@ -1,10 +0,0 @@ -enum InjectableDll { - console, - auth, - gameServer, - memoryLeak -} - -extension InjectableDllVersionAware on InjectableDll { - bool get isVersionDependent => this == InjectableDll.gameServer; -} diff --git a/common/lib/src/model/fortnite_build.dart b/common/lib/src/model/fortnite_build.dart deleted file mode 100644 index 19277f1..0000000 --- a/common/lib/src/model/fortnite_build.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'dart:io'; -import 'dart:isolate'; - -class FortniteBuild { - final String gameVersion; - final String link; - final bool available; - - FortniteBuild({ - required this.gameVersion, - required this.link, - required this.available - }); -} - -class FortniteBuildDownloadProgress { - final double progress; - final int? timeLeft; - final bool extracting; - final int speed; - - FortniteBuildDownloadProgress({ - required this.progress, - required this.extracting, - required this.timeLeft, - required this.speed - }); -} - -class FortniteBuildDownloadOptions { - FortniteBuild build; - Directory destination; - SendPort port; - - FortniteBuildDownloadOptions(this.build, this.destination, this.port); -} - -class FortniteBuildManifestChunk { - List chunksIds; - String file; - int fileSize; - - FortniteBuildManifestChunk._internal(this.chunksIds, this.file, this.fileSize); - - factory FortniteBuildManifestChunk.fromJson(json) => FortniteBuildManifestChunk._internal( - List.from(json["ChunksIds"] as List), - json["File"], - json["FileSize"] - ); -} - -class FortniteBuildManifestFile { - String name; - List chunks; - int size; - - FortniteBuildManifestFile._internal(this.name, this.chunks, this.size); - - factory FortniteBuildManifestFile.fromJson(json) => FortniteBuildManifestFile._internal( - json["Name"], - List.from(json["Chunks"].map((chunk) => FortniteBuildManifestChunk.fromJson(chunk))), - json["Size"] - ); -} diff --git a/common/lib/src/model/update_status.dart b/common/lib/src/model/update_status.dart deleted file mode 100644 index 7686734..0000000 --- a/common/lib/src/model/update_status.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum UpdateStatus { - waiting, - started, - success, - error -} \ No newline at end of file diff --git a/common/lib/src/model/update_timer.dart b/common/lib/src/model/update_timer.dart deleted file mode 100644 index 88ccdac..0000000 --- a/common/lib/src/model/update_timer.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum UpdateTimer { - never, - hour, - day, - week -} \ No newline at end of file diff --git a/common/lib/src/util/log.dart b/common/lib/src/util/logger.dart similarity index 100% rename from common/lib/src/util/log.dart rename to common/lib/src/util/logger.dart diff --git a/common/lib/src/util/os.dart b/common/lib/src/util/os.dart index 3ff9eef..6563323 100644 --- a/common/lib/src/util/os.dart +++ b/common/lib/src/util/os.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'package:reboot_common/common.dart'; @@ -41,7 +40,7 @@ String? get antiVirusName { final hr = CoCreateInstance( rclsid, nullptr, - CLSCTX.CLSCTX_INPROC_SERVER, + CLSCTX_INPROC_SERVER, riid, pLoc.cast(), ); @@ -84,7 +83,7 @@ String? get antiVirusName { final hr3 = service.execQuery( wql, query, - WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_FORWARD_ONLY | WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_RETURN_IMMEDIATELY, + WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, nullptr, pEnumerator.cast(), ); @@ -125,7 +124,7 @@ String? get antiVirusName { calloc.free(propName); - if (SUCCEEDED(hr5) && vtProp.ref.vt == VARENUM.VT_BSTR) { + if (SUCCEEDED(hr5) && vtProp.ref.vt == VT_BSTR) { final bstr = vtProp.ref.bstrVal; result = bstr.toDartString(); } @@ -225,7 +224,7 @@ bool killProcessByPort(int port) { _TCP_TABLE_OWNER_PID_LISTENER, 0 ); - if (result == WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) { + if (result == ERROR_INSUFFICIENT_BUFFER) { calloc.free(pTcpTable); pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value); result = _getExtendedTcpTable( @@ -247,7 +246,7 @@ bool killProcessByPort(int port) { final pid = row.dwOwningPid; calloc.free(pTcpTable); calloc.free(dwSize); - final hProcess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE, FALSE, pid); + final hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); if (hProcess != NULL) { final result = TerminateProcess(hProcess, 0); CloseHandle(hProcess); @@ -345,8 +344,8 @@ Future startElevatedProcess({required String executable, required String a var shellInput = calloc(); shellInput.ref.lpFile = executable.toNativeUtf16(); shellInput.ref.lpParameters = args.toNativeUtf16(); - shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE; - shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED; + shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE; + shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; shellInput.ref.lpVerb = "runas".toNativeUtf16(); shellInput.ref.cbSize = sizeOf(); return ShellExecuteEx(shellInput) == 1; @@ -402,7 +401,7 @@ final _NtSuspendProcess = _ntdll.lookupFunction('NtSuspendProcess'); bool suspend(int pid) { - final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid); + final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); try { return _NtSuspendProcess(processHandle) == 0; } finally { @@ -411,7 +410,7 @@ bool suspend(int pid) { } bool resume(int pid) { - final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid); + final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); try { return _NtResumeProcess(processHandle) == 0; } finally { diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index 9983aa3..8d225e0 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -90,8 +90,8 @@ "settingsClientArgsName": "Custom launch arguments", "settingsClientArgsDescription": "Additional arguments to use when launching Fortnite", "settingsClientArgsPlaceholder": "Arguments...", - "settingsServerName": "Internal files", - "settingsServerSubtitle": "Configure the internal files used by the launcher for the game server", + "settingsServerName": "Game server", + "settingsServerSubtitle": "Creates the game server on top of a Fortnite instance", "settingsServerOptionsName": "Options", "settingsServerOptionsSubtitle": "Configure additional options for the game server", "settingsServerTypeName": "Game server type", @@ -182,6 +182,7 @@ "joinSelfServer": "You can't join your own server", "cannotJoinServerVersion": "You can't join this server: download Fortnite {version}", "wrongServerPassword": "Wrong password: please try again", + "joiningServer": "Joining {name}...", "offlineServer": "This server isn't online right now: please try again later", "serverPassword": "Password", "serverPasswordPlaceholder": "Type the server's password", @@ -340,7 +341,7 @@ "promptServerBrowserPageActionLabel": "Next", "promptHostPageText": "The Host tab is used to host a game server.\nWhen you usually play Fortnite, you connect to an Epic Games' game server.\nTo play using Reboot, you'll need to host the game server yourself, or join someone else's.\nOtherwise, you will be sent back to the lobby when trying to join a game.", "promptHostPageActionLabel": "Next", - "promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't want other players to join your server, you can skip this part", + "promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't to use the Server Browser, you can skip this part for now and come back later.", "promptHostInfoActionLabelSkip": "Skip", "promptHostInfoActionLabelConfigure": "Configure", "promptHostInformationText": "Choose the name for your server", diff --git a/gui/lib/main.dart b/gui/lib/main.dart index ec6b329..526ee01 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -13,12 +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/controller/server_browser_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/widget/message/error.dart'; -import 'package:reboot_launcher/src/widget/page/home_page.dart'; +import 'package:reboot_launcher/src/message/error.dart'; +import 'package:reboot_launcher/src/pager/pager.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/url_protocol.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; import 'package:version/version.dart'; import 'package:window_manager/window_manager.dart'; @@ -35,207 +35,140 @@ bool appWithNoStorage = false; void main() { log("[APP] Called"); runZonedGuarded( - () => _startApp(), - (error, stack) => onError(error, stack, false), + () => _startApp(), + (error, stack) => onError(error, stack, false), zoneSpecification: ZoneSpecification( handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) ) ); } +// If anything fails here, the app won't start +// Be extremely careful Future _startApp() async { - _overrideHttpCertificate(); - final errors = []; + final errors = []; + Future runCatching({ + required FutureOr Function() callable, + required String Function(Object) errorFormatter + }) async { + try { + return callable(); + }catch(error) { + errors.add(errorFormatter(error)); + return null; + } + } + + log("[APP] Starting application"); try { - log("[APP] Starting application"); - final pathError = await _initPath(); - if(pathError != null) { - errors.add(pathError); - } - - final databaseError = await _initDatabase(); - if(databaseError != null) { - errors.add(databaseError); - } - - final notificationsError = await _initNotifications(); - if(notificationsError != null) { - errors.add(notificationsError); - } - - final versionError = await _initVersion(); - if(versionError != null) { - errors.add(versionError); - } - - final storageErrors = await _initStorage(); - errors.addAll(storageErrors); - WidgetsFlutterBinding.ensureInitialized(); - - _initWindow(); - - final urlError = await _initUrlHandler(); - if(urlError != null) { - errors.add(urlError); - } - }catch(uncaughtError) { - errors.add(uncaughtError); - } finally{ + await runCatching( + callable: () => installationDirectory.create( + recursive: true + ), + errorFormatter: (error) => "Cannot create installation directory: $error" + ); + await runCatching( + callable: () => localNotifier.setup( + appName: 'Reboot Launcher', + shortcutPolicy: ShortcutPolicy.ignore + ), + errorFormatter: (error) => "Cannot create installation directory: $error" + ); + await runCatching( + callable: () async { + final packageInfo = await PackageInfo.fromPlatform(); + appVersion = Version.parse(packageInfo.version); + }, + errorFormatter: (error) => "Cannot parse version: $error" + ); + await runCatching( + callable: () async { + await GetStorage(GameController.storageName, settingsDirectory.path).initStorage; + await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage; + await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage; + await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage; + await GetStorage(DllController.storageName, settingsDirectory.path).initStorage; + }, + errorFormatter: (error) { + appWithNoStorage = true; + return "Cannot access storage: $error"; + } + ); + await runCatching( + callable: () => Get.put(GameController(), permanent: true), + errorFormatter: (error) => "Cannot create game controller: $error" + ); + await runCatching( + callable: () => Get.put(BackendController(), permanent: true), + errorFormatter: (error) => "Cannot create backend controller: $error" + ); + await runCatching( + callable: () => Get.put(HostingController(), permanent: true), + errorFormatter: (error) => "Cannot create backend controller: $error" + ); + await runCatching( + callable: () => Get.put(ServerBrowserController(), permanent: true), + errorFormatter: (error) => "Cannot create browser controller: $error" + ); + final settingsController = await runCatching( + callable: () => Get.put(SettingsController(), permanent: true), + errorFormatter: (error) => "Cannot create settings controller: $error" + ); + await runCatching( + callable: () => Get.put(DllController(), permanent: true), + errorFormatter: (error) => "Cannot create dll controller: $error" + ); + await runCatching( + callable: () async { + try { + await SystemTheme.accentColor.load(); + await windowManager.ensureInitialized(); + await Window.initialize(); + if(settingsController != null) { + final size = Size(settingsController.width, settingsController.height); + await windowManager.setSize(size); + final offsetX = settingsController.offsetX; + final offsetY = settingsController.offsetY; + if (offsetX != null && offsetY != null) { + final position = Offset( + offsetX, + offsetY + ); + await windowManager.setPosition(position); + } else { + await windowManager.setAlignment(Alignment.center); + } + } + await windowManager.setPreventClose(true); + await windowManager.setResizable(true); + if(isWin11) { + await Window.setEffect( + effect: WindowEffect.acrylic, + color: Colors.green, + dark: isDarkMode + ); + } + } finally { + windowManager.show(); + } + }, + errorFormatter: (error) => "Cannot configure window: $error" + ); + runCatching( + callable: () => registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']), + errorFormatter: (error) => "Cannot configure custom url scheme: $error" + ); + }catch(error) { + errors.add("Uncaught error: $error"); + }finally { log("[APP] Started applications with errors: $errors"); runApp(RebootApplication(errors: errors)); } } -class _MyHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context){ - return super.createHttpClient(context) - ..badCertificateCallback = ((X509Certificate cert, String host, int port) => true); - } -} - -void _overrideHttpCertificate() { - HttpOverrides.global = _MyHttpOverrides(); // Not safe, but necessary -} - -Future _initNotifications() async { - try { - await localNotifier.setup( - appName: 'Reboot Launcher', - shortcutPolicy: ShortcutPolicy.ignore - ); - return null; - }catch(error) { - return error; - } -} - -Future _initDatabase() async { - try { - await Supabase.initialize( - url: supabaseUrl, - anonKey: supabaseAnonKey - ); - return null; - }catch(error) { - return error; - } -} - -Future _initPath() async { - try { - await installationDirectory.create(recursive: true); - return null; - }catch(error) { - return error; - } -} - -Future _initVersion() async { - try { - final packageInfo = await PackageInfo.fromPlatform(); - appVersion = Version.parse(packageInfo.version); - return null; - }catch(error) { - return error; - } -} - -Future _initUrlHandler() async { - try { - registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']); - return null; - }catch(error) { - return error; - } -} - -Future _initWindow() async { - try { - await SystemTheme.accentColor.load(); - await windowManager.ensureInitialized(); - await Window.initialize(); - var settingsController = Get.find(); - var size = Size(settingsController.width, settingsController.height); - await windowManager.setSize(size); - var offsetX = settingsController.offsetX; - var offsetY = settingsController.offsetY; - if(offsetX != null && offsetY != null) { - final position = Offset( - offsetX, - offsetY - ); - await windowManager.setPosition(position); - }else { - await windowManager.setAlignment(Alignment.center); - } - await windowManager.setPreventClose(true); - await windowManager.setResizable(true); - if(isWin11) { - await Window.setEffect( - effect: WindowEffect.acrylic, - color: Colors.green, - dark: isDarkMode - ); - } - }catch(error, stackTrace) { - onError(error, stackTrace, false); - }finally { - windowManager.show(); - } -} - -Future> _initStorage() async { - final errors = []; - try { - await GetStorage(GameController.storageName, settingsDirectory.path).initStorage; - await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage; - await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage; - await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage; - await GetStorage(DllController.storageName, settingsDirectory.path).initStorage; - }catch(error) { - appWithNoStorage = true; - errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); - } - - try { - Get.put(GameController()); - }catch(error) { - errors.add(error); - } - - try { - Get.put(BackendController()); - }catch(error) { - errors.add(error); - } - - try { - final controller = HostingController(); - Get.put(controller); - controller.discardServer(); - }catch(error) { - errors.add(error); - } - - try { - Get.put(SettingsController()); - }catch(error) { - errors.add(error); - } - - try { - Get.put(DllController()); - }catch(error) { - errors.add(error); - } - - return errors; -} - class RebootApplication extends StatefulWidget { - final List errors; + final List errors; const RebootApplication({Key? key, required this.errors}) : super(key: key); @override @@ -248,15 +181,12 @@ class _RebootApplicationState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(widget.errors)); - } - - void _handleErrors(List errors) { - for(final error in errors) { - if(error != null) { + // Not pretty but make sure the errors are shown + Future.delayed(const Duration(seconds: 5)).then((_) { + for(final error in widget.errors) { onError(error, null, false); } - } + }); } @override @@ -272,7 +202,7 @@ class _RebootApplicationState extends State { color: SystemTheme.accentColor.accent.toAccentColor(), darkTheme: _createTheme(Brightness.dark), theme: _createTheme(Brightness.light), - home: const HomePage() + home: const RebootPager() )); FluentThemeData _createTheme(Brightness brightness) => FluentThemeData( diff --git a/gui/lib/src/button/backend_start_button.dart b/gui/lib/src/button/backend_start_button.dart new file mode 100644 index 0000000..0658dbd --- /dev/null +++ b/gui/lib/src/button/backend_start_button.dart @@ -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 createState() => _BackendButtonState(); +} + +class _BackendButtonState extends State { + final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); + final BackendController _backendController = Get.find(); + final StreamController _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; + } +} diff --git a/gui/lib/src/widget/file/file_selector.dart b/gui/lib/src/button/file_selector.dart similarity index 98% rename from gui/lib/src/widget/file/file_selector.dart rename to gui/lib/src/button/file_selector.dart index 467c32f..ea7bab5 100644 --- a/gui/lib/src/widget/file/file_selector.dart +++ b/gui/lib/src/button/file_selector.dart @@ -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?); diff --git a/gui/lib/src/widget/game/game_start_button.dart b/gui/lib/src/button/game_start_button.dart similarity index 91% rename from gui/lib/src/widget/game/game_start_button.dart rename to gui/lib/src/button/game_start_button.dart index f6172dd..199d072 100644 --- a/gui/lib/src/widget/game/game_start_button.dart +++ b/gui/lib/src/button/game_start_button.dart @@ -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 { final HostingController _hostingController = Get.find(); final BackendController _backendController = Get.find(); final DllController _dllController = Get.find(); + final ServerBrowserController _serverBrowserController = Get.find(); InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameServerInfoBar; @@ -91,8 +94,8 @@ class _LaunchButtonState extends State { 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 { 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 { } } - Future _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool forceLinkedHosting) async { + Future _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 { return result; } - Future _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async { + Future _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 { 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 _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async { + Future _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 { return gameProcess.pid; } - Future _createPausedProcess(FortniteVersion version, bool host, String executableName) async { + Future _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 { 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 { 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 { } return; } - _backendController.joinLocalhost(); + _backendController.gameServerAddress.text = kDefaultGameServerHost; + final accessible = await _checkPublicGameServer(gameServerPort); if (!accessible) { showRebootInfoBar( @@ -477,10 +497,8 @@ class _LaunchButtonState extends State { 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 { 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 { ); 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 { } } - Future _injectOrShowError(InjectableDll injectable, bool hosting) async { + Future _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 { } } - Future _getDllFileOrStop(String version, InjectableDll injectable, bool host) async { + Future _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 { 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 { ), 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(); diff --git a/gui/lib/src/widget/server/server_type_selector.dart b/gui/lib/src/button/server_type_selector.dart similarity index 68% rename from gui/lib/src/widget/server/server_type_selector.dart rename to gui/lib/src/button/server_type_selector.dart index 2b5e860..183b561 100644 --- a/gui/lib/src/widget/server/server_type_selector.dart +++ b/gui/lib/src/button/server_type_selector.dart @@ -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 { - late final BackendController _controller = Get.find(); + late final BackendController _backendController = Get.find(); @override Widget build(BuildContext context) { @@ -24,27 +24,27 @@ class _ServerTypeSelectorState extends State { 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; } } diff --git a/gui/lib/src/widget/version/version_selector.dart b/gui/lib/src/button/version_selector.dart similarity index 91% rename from gui/lib/src/widget/version/version_selector.dart rename to gui/lib/src/button/version_selector.dart index a45a1fb..ee68cdf 100644 --- a/gui/lib/src/widget/version/version_selector.dart +++ b/gui/lib/src/button/version_selector.dart @@ -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 openImportDialog(FortniteVersion? version) => showRebootDialog( + static Future openImportDialog(GameVersion? version) => showRebootDialog( builder: (context) => ImportVersionDialog( version: version, closable: true, @@ -83,7 +83,7 @@ class _VersionSelectorState extends State { ); }); - 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 { 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 { onPressed: () => _gameController.selectedVersion.value = version ); - Future _openVersionOptions(FortniteVersion version) async { + Future _openVersionOptions(GameVersion version) async { final result = await _flyoutController.showFlyout<_ContextualOption?>( builder: (context) => MenuFlyout( items: _ContextualOption.values @@ -167,7 +167,7 @@ class _VersionSelectorState extends State { _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 { return false; } - Future _openDeleteDialog(FortniteVersion version) { + Future _openDeleteDialog(GameVersion version) { return showRebootDialog( builder: (context) => ContentDialog( content: Column( diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index 0c7e506..a2614bd 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -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 type; + late final Rx type; late final TextEditingController gameServerAddress; late final FocusNode gameServerAddressFocusNode; late final Rx consoleKey; late final RxBool started; late final RxBool detached; - late final List _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 _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 toggle() { + Future 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 start({required bool interactive}) async { + Future 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() - .instance - .value - ?.kill(); - Get.find() - .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(); - 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 stop({required bool interactive}) async { + Future 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(); - 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 joinServer(String uuid, FortniteServer server) async { - if(!kDebugMode && uuid == server.id) { - _showRebootInfoBar( - translations.joinSelfServer, - duration: infoBarLongDuration, - severity: InfoBarSeverity.error - ); - return; - } - - final version = Get.find() - .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 _isServerValid(String address) async { - final result = await pingGameServer(address); - if(result) { - return true; - } - - _showRebootInfoBar( - translations.offlineServer, - duration: infoBarLongDuration, - severity: InfoBarSeverity.error - ); - return false; - } - - Future _askForPassword() async { - final confirmPasswordController = TextEditingController(); - final showPassword = RxBool(false); - final showPasswordTrailing = RxBool(false); - return await showRebootDialog( - 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().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 restart() async { - if(started.value) { - await stop(interactive: false); - await start(interactive: true); - } + _interactiveEntry?.close(); } } \ No newline at end of file diff --git a/gui/lib/src/controller/dll_controller.dart b/gui/lib/src/controller/dll_controller.dart index a5dd46d..6571aa1 100644 --- a/gui/lib/src/controller/dll_controller.dart +++ b/gui/lib/src/controller/dll_controller.dart @@ -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 timer; late final TextEditingController beforeS20Mirror; late final TextEditingController aboveS20Mirror; late final RxBool customGameServer; late final RxnInt timestamp; late final Rx status; - late final Map _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 download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async { + Future 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 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() - .instance - .value - ?.kill(); - Get.find() - .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 } \ No newline at end of file diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index 1e88c72..acaa7c9 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -16,8 +16,8 @@ class GameController extends GetxController { late final TextEditingController username; late final TextEditingController password; late final TextEditingController customLaunchArgs; - late final Rx> versions; - late final Rxn selectedVersion; + late final Rx> versions; + late final Rxn selectedVersion; late final RxBool started; late final Rxn 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)); } diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index d293294..12b6538 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -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 instance; - late final Rxn> 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 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 event) { - log("[SUPABASE] New event: ${event}"); - servers.value = event; - published.value = event.any((element) => element.id == uuid); - } - - Future 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 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; - } - } } diff --git a/gui/lib/src/controller/server_browser_controller.dart b/gui/lib/src/controller/server_browser_controller.dart new file mode 100644 index 0000000..28881b5 --- /dev/null +++ b/gui/lib/src/controller/server_browser_controller.dart @@ -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> servers; + final Map _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 addServer(ServerBrowserEntry entry) async { + try { + _semaphore.acquire(); + await _client.addEntry(entry); + } finally { + _semaphore.release(); + } + } + + Future removeServer(String uuid) async { + try { + _semaphore.acquire(); + await _client.removeEntry(uuid); + } finally { + _semaphore.release(); + } + } + + StreamSubscription addEventsListener(void Function(ServerBrowserEvent) onData) { + return _client.addListener(onData); + } + + ServerBrowserEntry? getServerById(String uuid) { + return _entries[uuid]; + } +} \ No newline at end of file diff --git a/gui/lib/src/controller/settings_controller.dart b/gui/lib/src/controller/settings_controller.dart index 884edda..4a33880 100644 --- a/gui/lib/src/controller/settings_controller.dart +++ b/gui/lib/src/controller/settings_controller.dart @@ -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 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 _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; - } - } } \ No newline at end of file diff --git a/gui/lib/src/message/backend.dart b/gui/lib/src/message/backend.dart new file mode 100644 index 0000000..7dd7440 --- /dev/null +++ b/gui/lib/src/message/backend.dart @@ -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), + ) + ); +} \ No newline at end of file diff --git a/gui/lib/src/widget/message/data.dart b/gui/lib/src/message/data.dart similarity index 100% rename from gui/lib/src/widget/message/data.dart rename to gui/lib/src/message/data.dart index 8b1ba5e..12d9efa 100644 --- a/gui/lib/src/widget/message/data.dart +++ b/gui/lib/src/message/data.dart @@ -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 showResetDialog(Function() onConfirm) => showRebootDialog( builder: (context) => InfoDialog( diff --git a/gui/lib/src/widget/message/dll.dart b/gui/lib/src/message/dll.dart similarity index 100% rename from gui/lib/src/widget/message/dll.dart rename to gui/lib/src/message/dll.dart index d65cadf..c91a688 100644 --- a/gui/lib/src/widget/message/dll.dart +++ b/gui/lib/src/message/dll.dart @@ -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 showDllDeletedDialog() => showRebootDialog( builder: (context) => InfoDialog( diff --git a/gui/lib/src/widget/version/download_version.dart b/gui/lib/src/message/download_version.dart similarity index 91% rename from gui/lib/src/widget/version/download_version.dart rename to gui/lib/src/message/download_version.dart index 993dbe3..561ae5e 100644 --- a/gui/lib/src/widget/version/download_version.dart +++ b/gui/lib/src/message/download_version.dart @@ -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 { final GlobalKey _formFieldKey = GlobalKey(); final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form); - final Rxn _build = Rxn(); + final Rxn _build = Rxn(); final RxnInt _timeLeft = RxnInt(); final Rxn _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 { _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 _onDownloadComplete(FortniteBuild build) async { + Future _onDownloadComplete(GameBuild build) async { if (!mounted) { return; } @@ -174,7 +168,7 @@ class _DownloadVersionDialogState extends State { _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 { _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 { _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 { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildSelector, + InfoLabel( label: translations.versionName, child: TextFormBox( @@ -296,8 +291,6 @@ class _DownloadVersionDialogState extends State { height: 16.0 ), - _buildSelector, - FileSelector( label: translations.gameFolderTitle, placeholder: translations.buildInstallationDirectoryPlaceholder, @@ -336,13 +329,13 @@ class _DownloadVersionDialogState extends State { Widget get _buildSelector => InfoLabel( label: translations.build, - child: FormField( + child: FormField( key: _formFieldKey, validator: (data) => _checkBuild(data), builder: (formContext) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ComboBox( + ComboBox( placeholder: Text(translations.selectBuild), isExpanded: true, items: downloadableBuilds.where((build) => build.available) @@ -377,7 +370,7 @@ class _DownloadVersionDialogState extends State { ) ); - String? _checkBuild(FortniteBuild? data) { + String? _checkBuild(GameBuild? data) { if(data == null) { return translations.selectBuild; } @@ -385,7 +378,7 @@ class _DownloadVersionDialogState extends State { return null; } - ComboBoxItem _buildBuildItem(FortniteBuild element) => ComboBoxItem( + ComboBoxItem _buildBuildItem(GameBuild element) => ComboBoxItem( value: element, child: Text(element.gameVersion) ); diff --git a/gui/lib/src/widget/message/error.dart b/gui/lib/src/message/error.dart similarity index 90% rename from gui/lib/src/widget/message/error.dart rename to gui/lib/src/message/error.dart index 12c2b01..9f286e0 100644 --- a/gui/lib/src/widget/message/error.dart +++ b/gui/lib/src/message/error.dart @@ -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; } diff --git a/gui/lib/src/widget/version/import_version.dart b/gui/lib/src/message/import_version.dart similarity index 97% rename from gui/lib/src/widget/version/import_version.dart rename to gui/lib/src/message/import_version.dart index 1ef735d..82d0e5b 100644 --- a/gui/lib/src/widget/version/import_version.dart +++ b/gui/lib/src/message/import_version.dart @@ -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 { } if(widget.version == null) { - final version = FortniteVersion( + final version = GameVersion( name: name, gameVersion: gameVersion, location: shippingExes.first.parent diff --git a/gui/lib/src/widget/message/onboard.dart b/gui/lib/src/message/onboard.dart similarity index 91% rename from gui/lib/src/widget/message/onboard.dart rename to gui/lib/src/message/onboard.dart index 93460c4..789725d 100644 --- a/gui/lib/src/widget/message/onboard.dart +++ b/gui/lib/src/message/onboard.dart @@ -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(); @@ -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(); 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( diff --git a/gui/lib/src/widget/message/profile.dart b/gui/lib/src/message/profile.dart similarity index 100% rename from gui/lib/src/widget/message/profile.dart rename to gui/lib/src/message/profile.dart index 1d54082..f883a48 100644 --- a/gui/lib/src/widget/message/profile.dart +++ b/gui/lib/src/message/profile.dart @@ -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 showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{ final showPassword = RxBool(false); diff --git a/gui/lib/src/messenger/dialog.dart b/gui/lib/src/messenger/dialog.dart index 8b58735..4fa9f07 100644 --- a/gui/lib/src/messenger/dialog.dart +++ b/gui/lib/src/messenger/dialog.dart @@ -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; diff --git a/gui/lib/src/widget/window/info_bar_area.dart b/gui/lib/src/messenger/info_bar_area.dart similarity index 100% rename from gui/lib/src/widget/window/info_bar_area.dart rename to gui/lib/src/messenger/info_bar_area.dart diff --git a/gui/lib/src/messenger/overlay.dart b/gui/lib/src/messenger/overlay.dart index 09326fb..dc80b35 100644 --- a/gui/lib/src/messenger/overlay.dart +++ b/gui/lib/src/messenger/overlay.dart @@ -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()); diff --git a/gui/lib/src/widget/page/backend_page.dart b/gui/lib/src/page/backend_page.dart similarity index 83% rename from gui/lib/src/widget/page/backend_page.dart rename to gui/lib/src/page/backend_page.dart index 7337096..2202301 100644 --- a/gui/lib/src/widget/page/backend_page.dart +++ b/gui/lib/src/page/backend_page.dart @@ -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 backendTypeOverlayTargetKey = GlobalKey(); @@ -22,7 +22,7 @@ final GlobalKey backendGameServerAddressOverlayTargetKey = G final GlobalKey backendUnrealEngineOverlayTargetKey = GlobalKey(); final GlobalKey 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 createState() => _BackendPageState(); + AbstractPageState createState() => _BackendPageState(); } -class _BackendPageState extends RebootPageState { +class _BackendPageState extends AbstractPageState { final BackendController _backendController = Get.find(); InfoBarEntry? _infoBarEntry; @@ -77,7 +77,7 @@ class _BackendPageState extends RebootPageState { ]; 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 { }); 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 { }); 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 { }); 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 { 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 { }); 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 { ); 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 { ); @override - Widget get button => const ServerButton(); + Widget get button => const BackendButton(); } diff --git a/gui/lib/src/page/browser_page.dart b/gui/lib/src/page/browser_page.dart new file mode 100644 index 0000000..14959bf --- /dev/null +++ b/gui/lib/src/page/browser_page.dart @@ -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 createState() => _BrowsePageState(); +} + +class _BrowsePageState extends AbstractPageState { + final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); + final BackendController _backendController = Get.find(); + final ServerBrowserController _serverBrowserController = Get.find(); + final TextEditingController _filterController = TextEditingController(); + final StreamController _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? 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? 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 _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 _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 _askForPassword() async { + final confirmPasswordController = TextEditingController(); + final showPassword = RxBool(false); + final showPasswordTrailing = RxBool(false); + return await showRebootDialog( + 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().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 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; + } + } +} \ No newline at end of file diff --git a/gui/lib/src/widget/page/host_page.dart b/gui/lib/src/page/host_page.dart similarity index 86% rename from gui/lib/src/widget/page/host_page.dart rename to gui/lib/src/page/host_page.dart index f08a45b..2d74167 100644 --- a/gui/lib/src/widget/page/host_page.dart +++ b/gui/lib/src/page/host_page.dart @@ -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 hostVersionOverlayTargetKey = GlobalKey(); final GlobalKey hostInfoOverlayTargetKey = GlobalKey(); @@ -27,7 +27,7 @@ final GlobalKey hostInfoPasswordOverlayTargetKey = GlobalKey final GlobalKey hostShareOverlayTargetKey = GlobalKey(); final GlobalKey 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 createState() => _HostingPageState(); + AbstractPageState createState() => _HostingPageState(); } -class _HostingPageState extends RebootPageState { - final GameController _gameController = Get.find(); +class _HostingPageState extends AbstractPageState { + final ServerBrowserController _serverBrowserController = Get.find(); final HostingController _hostingController = Get.find(); final DllController _dllController = Get.find(); @@ -159,31 +159,6 @@ class _HostingPageState extends RebootPageState { ) ), )) - ), - 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 { } 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 diff --git a/gui/lib/src/widget/page/info_page.dart b/gui/lib/src/page/info_page.dart similarity index 81% rename from gui/lib/src/widget/page/info_page.dart rename to gui/lib/src/page/info_page.dart index 50ddfe9..92201d4 100644 --- a/gui/lib/src/widget/page/info_page.dart +++ b/gui/lib/src/page/info_page.dart @@ -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 createState() => _InfoPageState(); + AbstractPageState 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 { +class _InfoPageState extends AbstractPageState { static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new"; static const String _kDiscordInviteUrl = "https://discord.gg/rebootmp"; diff --git a/gui/lib/src/page/pages.dart b/gui/lib/src/page/pages.dart index 199e726..6aa78e5 100644 --- a/gui/lib/src/page/pages.dart +++ b/gui/lib/src/page/pages.dart @@ -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 pagesController = StreamController.broadcast(); bool hitBack = false; -final List pages = [ +final List pages = [ const PlayPage(), const HostPage(), const BrowsePage(), @@ -28,7 +28,7 @@ final List pages = [ final List> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey()); -final RxInt pageIndex = RxInt(RebootPageType.play.index); +final RxInt pageIndex = RxInt(PageType.play.index); final HashMap _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 appStack = _createAppStack(); Queue _createAppStack() { @@ -71,13 +73,12 @@ Queue _createAppStack() { final Map> _pagesStack = Map.fromEntries(List.generate(pages.length, (index) => MapEntry(index, Queue()))); -Queue get pageStack => _pagesStack[pageIndex.value]!; +Queue 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); } diff --git a/gui/lib/src/widget/page/play_page.dart b/gui/lib/src/page/play_page.dart similarity index 80% rename from gui/lib/src/widget/page/play_page.dart rename to gui/lib/src/page/play_page.dart index 1425898..2b49610 100644 --- a/gui/lib/src/widget/page/play_page.dart +++ b/gui/lib/src/page/play_page.dart @@ -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 gameVersionOverlayTargetKey = GlobalKey(); -class PlayPage extends RebootPage { +class PlayPage extends AbstractPage { const PlayPage({Key? key}) : super(key: key); @override - RebootPageState createState() => _PlayPageState(); + AbstractPageState 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 { +class _PlayPageState extends AbstractPageState { final GameController _gameController = Get.find(); final DllController _dllController = Get.find(); diff --git a/gui/lib/src/widget/page/settings_page.dart b/gui/lib/src/page/settings_page.dart similarity index 82% rename from gui/lib/src/widget/page/settings_page.dart rename to gui/lib/src/page/settings_page.dart index 36651f7..e1546b7 100644 --- a/gui/lib/src/widget/page/settings_page.dart +++ b/gui/lib/src/page/settings_page.dart @@ -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 settingsConsoleDllInputKey = GlobalKey(); @@ -22,7 +21,7 @@ final GlobalKey settingsAuthDllInputKey = GlobalKey(); final GlobalKey settingsMemoryDllInputKey = GlobalKey(); final GlobalKey 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 createState() => _SettingsPageState(); + AbstractPageState createState() => _SettingsPageState(); } -class _SettingsPageState extends RebootPageState { +class _SettingsPageState extends AbstractPageState { final SettingsController _settingsController = Get.find(); final DllController _dllController = Get.find(); int? _downloadFromMirrorId; @@ -70,9 +69,9 @@ class _SettingsPageState extends RebootPageState { 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 { 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 { 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 { 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 { } }); - 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); - } } \ No newline at end of file diff --git a/gui/lib/src/page/page.dart b/gui/lib/src/pager/abstract_page.dart similarity index 86% rename from gui/lib/src/page/page.dart rename to gui/lib/src/pager/abstract_page.dart index 3ff62fb..edd020d 100644 --- a/gui/lib/src/page/page.dart +++ b/gui/lib/src/pager/abstract_page.dart @@ -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 extends State with AutomaticKeepAliveClientMixin { +abstract class AbstractPageState extends State with AutomaticKeepAliveClientMixin { final SettingsController _settingsController = Get.find(); @override diff --git a/gui/lib/src/page/page_suggestion.dart b/gui/lib/src/pager/page_suggestion.dart similarity index 76% rename from gui/lib/src/page/page_suggestion.dart rename to gui/lib/src/pager/page_suggestion.dart index b0b8104..f7c3988 100644 --- a/gui/lib/src/page/page_suggestion.dart +++ b/gui/lib/src/pager/page_suggestion.dart @@ -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 + }); } diff --git a/gui/lib/src/page/page_type.dart b/gui/lib/src/pager/page_type.dart similarity index 72% rename from gui/lib/src/page/page_type.dart rename to gui/lib/src/pager/page_type.dart index 9ae2588..f8aae68 100644 --- a/gui/lib/src/page/page_type.dart +++ b/gui/lib/src/pager/page_type.dart @@ -1,4 +1,4 @@ -enum RebootPageType { +enum PageType { play, host, browser, diff --git a/gui/lib/src/widget/page/home_page.dart b/gui/lib/src/pager/pager.dart similarity index 80% rename from gui/lib/src/widget/page/home_page.dart rename to gui/lib/src/pager/pager.dart index 54f8195..e77381c 100644 --- a/gui/lib/src/widget/page/home_page.dart +++ b/gui/lib/src/pager/pager.dart @@ -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 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 createState() => _HomePageState(); + State createState() => _RebootPagerState(); } -class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { +class _RebootPagerState extends State with WindowListener, AutomaticKeepAliveClientMixin { final BackendController _backendController = Get.find(); final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); + final ServerBrowserController _serverBrowserController = Get.find(); final SettingsController _settingsController = Get.find(); final DllController _dllController = Get.find(); final GlobalKey _searchKey = GlobalKey(); @@ -61,7 +62,6 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA _syncPageViewWithNavigator(); WidgetsBinding.instance.addPostFrameCallback((_) { _checkUpdates(); - _initAppLink(); _checkGameServer(); }); } @@ -79,30 +79,6 @@ class _HomePageState extends State 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 _checkGameServer() async { try { final address = _backendController.gameServerAddress.text; @@ -115,7 +91,7 @@ class _HomePageState extends State 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 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 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 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 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 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 with WindowListener, AutomaticKeepA stream: pagesController.stream, builder: (context, _) { final elements = []; - 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 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 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 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 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); } diff --git a/gui/lib/src/widget/file/file_setting_tile.dart b/gui/lib/src/tile/file_setting_tile.dart similarity index 96% rename from gui/lib/src/widget/file/file_setting_tile.dart rename to gui/lib/src/tile/file_setting_tile.dart index b26fbe8..61f61e0 100644 --- a/gui/lib/src/widget/file/file_setting_tile.dart +++ b/gui/lib/src/tile/file_setting_tile.dart @@ -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; diff --git a/gui/lib/src/widget/window/info_tile.dart b/gui/lib/src/tile/info_tile.dart similarity index 97% rename from gui/lib/src/widget/window/info_tile.dart rename to gui/lib/src/tile/info_tile.dart index 7c1763a..3ff6c66 100644 --- a/gui/lib/src/widget/window/info_tile.dart +++ b/gui/lib/src/tile/info_tile.dart @@ -19,7 +19,7 @@ class InfoTile extends StatelessWidget { ), child: Expander( key: expanderKey, - header: Row( + header: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( diff --git a/gui/lib/src/widget/fluent/profile_tile.dart b/gui/lib/src/tile/profile_tile.dart similarity index 89% rename from gui/lib/src/widget/fluent/profile_tile.dart rename to gui/lib/src/tile/profile_tile.dart index 3a0df72..0f91919 100644 --- a/gui/lib/src/widget/fluent/profile_tile.dart +++ b/gui/lib/src/tile/profile_tile.dart @@ -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 { 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; } diff --git a/gui/lib/src/widget/fluent/setting_tile.dart b/gui/lib/src/tile/setting_tile.dart similarity index 87% rename from gui/lib/src/widget/fluent/setting_tile.dart rename to gui/lib/src/tile/setting_tile.dart index 2d961cf..e0c00d5 100644 --- a/gui/lib/src/widget/fluent/setting_tile.dart +++ b/gui/lib/src/tile/setting_tile.dart @@ -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 { ); 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 { 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 { 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 { ); } + SkeletonAvatar get _skeletonIcon => const SkeletonAvatar(style: SkeletonAvatarStyle( + width: 30, + height: 30, + shape: BoxShape.circle + )); + void Function()? _buildOnPressed() { if(widget.onPressed != null) { return widget.onPressed; diff --git a/gui/lib/src/util/types.dart b/gui/lib/src/util/extensions.dart similarity index 64% rename from gui/lib/src/util/types.dart rename to gui/lib/src/util/extensions.dart index 706d461..f348807 100644 --- a/gui/lib/src/util/types.dart +++ b/gui/lib/src/util/extensions.dart @@ -16,4 +16,14 @@ extension StringExtension on String { return substring(index + leading.length); } +} + +extension FutureExtension on Future { + Future withMinimumDuration(Duration duration) async { + final result = await Future.wait([ + Future.delayed(duration), + this + ]); + return result.last; + } } \ No newline at end of file diff --git a/gui/lib/src/util/os.dart b/gui/lib/src/util/os.dart index dfe1057..a555d5f 100644 --- a/gui/lib/src/util/os.dart +++ b/gui/lib/src/util/os.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:ffi'; import 'dart:io'; diff --git a/gui/lib/src/util/updater.dart b/gui/lib/src/util/updater.dart new file mode 100644 index 0000000..cf93eb6 --- /dev/null +++ b/gui/lib/src/util/updater.dart @@ -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 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 _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; + } +} \ No newline at end of file diff --git a/gui/lib/src/widget/page/browser_page.dart b/gui/lib/src/widget/page/browser_page.dart deleted file mode 100644 index 1d52202..0000000 --- a/gui/lib/src/widget/page/browser_page.dart +++ /dev/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 createState() => _BrowsePageState(); -} - -class _BrowsePageState extends RebootPageState { - final GameController _gameController = Get.find(); - final HostingController _hostingController = Get.find(); - final BackendController _backendController = Get.find(); - final TextEditingController _filterController = TextEditingController(); - final StreamController _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 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 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 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; - } - } -} \ No newline at end of file diff --git a/gui/lib/src/widget/server/server_start_button.dart b/gui/lib/src/widget/server/server_start_button.dart deleted file mode 100644 index 6f59073..0000000 --- a/gui/lib/src/widget/server/server_start_button.dart +++ /dev/null @@ -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 createState() => _ServerButtonState(); -} - -class _ServerButtonState extends State { - late final BackendController _controller = Get.find(); - late final StreamController _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; - } -} diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index e24dff7..c3910d8 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -57,7 +57,6 @@ dependencies: port_forwarder: ^1.0.0 # Server browser - supabase_flutter: ^2.7.0 dart_ipify: ^1.1.1 # Storage diff --git a/server_browser_backend/bin/main.dart b/server_browser_backend/bin/main.dart index 38d27dd..9495ded 100644 --- a/server_browser_backend/bin/main.dart +++ b/server_browser_backend/bin/main.dart @@ -1,6 +1,6 @@ import 'package:server_browser_backend/server_browser_backend.dart'; void main() async { - final server = WebSocketServer(); + final server = ServerBrowserBackend(); await server.start(port: 8080); } \ No newline at end of file diff --git a/server_browser_backend/lib/server_browser_backend.dart b/server_browser_backend/lib/server_browser_backend.dart index aea390a..60ea459 100644 --- a/server_browser_backend/lib/server_browser_backend.dart +++ b/server_browser_backend/lib/server_browser_backend.dart @@ -1,4 +1,4 @@ library; -export 'src/server_entry.dart'; +export 'src/server_browser_entry.dart'; export 'src/web_socket.dart'; \ No newline at end of file diff --git a/server_browser_backend/lib/src/server_entry.dart b/server_browser_backend/lib/src/server_browser_entry.dart similarity index 88% rename from server_browser_backend/lib/src/server_entry.dart rename to server_browser_backend/lib/src/server_browser_entry.dart index b3cba4c..7e610bc 100644 --- a/server_browser_backend/lib/src/server_entry.dart +++ b/server_browser_backend/lib/src/server_browser_entry.dart @@ -1,4 +1,4 @@ -class ServerEntry { +class ServerBrowserEntry { final String id; final String name; final String description; @@ -9,7 +9,7 @@ class ServerEntry { final String author; final bool discoverable; - ServerEntry({ + ServerBrowserEntry({ required this.id, required this.name, required this.description, @@ -35,8 +35,8 @@ class ServerEntry { }; } - static ServerEntry fromJson(Map json) { - return ServerEntry( + static ServerBrowserEntry fromJson(Map json) { + return ServerBrowserEntry( id: json['id'], name: json['name'], description: json['description'], diff --git a/server_browser_backend/lib/src/web_socket.dart b/server_browser_backend/lib/src/web_socket.dart index 6ed129c..241aca4 100644 --- a/server_browser_backend/lib/src/web_socket.dart +++ b/server_browser_backend/lib/src/web_socket.dart @@ -1,13 +1,14 @@ import 'dart:convert'; import 'dart:io'; -import 'package:server_browser_backend/src/server_entry.dart'; +import 'package:server_browser_backend/src/server_browser_entry.dart'; -class WebSocketServer { +class ServerBrowserBackend { static const String addEvent = 'add'; static const String removeEvent = 'remove'; + static const String pingEvent = 'ping'; - final Map _entries = {}; + final Map _entries = {}; final Set _clients = {}; late HttpServer _server; @@ -54,8 +55,11 @@ class WebSocketServer { type = data['type']; final payload = data['data']; switch (type) { + case pingEvent: + client.add(json.encode({"type": pingEvent})); + break; case addEvent: - final entry = ServerEntry.fromJson(payload); + final entry = ServerBrowserEntry.fromJson(payload); _entries[entry.id] = entry; _broadcastEvent(addEvent, entry.toJson()); break; diff --git a/server_browser_backend/package.bat b/server_browser_backend/package.bat new file mode 100644 index 0000000..a2c9c9d --- /dev/null +++ b/server_browser_backend/package.bat @@ -0,0 +1 @@ +dart compile exe bin/main.dart -o dist/server.exe \ No newline at end of file diff --git a/server_browser_backend/package.sh b/server_browser_backend/package.sh new file mode 100644 index 0000000..0c3d713 --- /dev/null +++ b/server_browser_backend/package.sh @@ -0,0 +1,2 @@ +# Run me from WSL +dart compile exe bin/main.dart -o dist/server \ No newline at end of file diff --git a/server_browser_backend/test/test.dart b/server_browser_backend/test/test.dart index c8dd961..7deb96e 100644 --- a/server_browser_backend/test/test.dart +++ b/server_browser_backend/test/test.dart @@ -5,11 +5,11 @@ import 'package:test/test.dart'; void main() { group('WebSocket Server Tests', () { - late WebSocketServer server; + late ServerBrowserBackend server; final int testPort = 8081; setUp(() async { - server = WebSocketServer(); + server = ServerBrowserBackend(); await server.start(port: testPort); });