diff --git a/common/lib/common.dart b/common/lib/common.dart index 8646a2c..2812459 100644 --- a/common/lib/common.dart +++ b/common/lib/common.dart @@ -5,6 +5,7 @@ export 'package:reboot_common/src/constant/os.dart'; export 'package:reboot_common/src/constant/supabase.dart'; +export 'package:reboot_common/src/model/archive.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'; diff --git a/common/lib/src/constant/supabase.dart b/common/lib/src/constant/supabase.dart index 6e6378f..02fc1cb 100644 --- a/common/lib/src/constant/supabase.dart +++ b/common/lib/src/constant/supabase.dart @@ -1,2 +1,2 @@ -const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co'; -const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M'; \ No newline at end of file +const String supabaseUrl = 'https://pocjparoguvaeeyjapjb.supabase.co'; +const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBvY2pwYXJvZ3V2YWVleWphcGpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTUzMTM4NTUsImV4cCI6MjAxMDg4OTg1NX0.BffJtbQvX1NVUy-9Nj4GVzUJXPK_1GyezDE0V5MRiao'; \ No newline at end of file diff --git a/common/lib/src/model/archive.dart b/common/lib/src/model/archive.dart new file mode 100644 index 0000000..c292fb3 --- /dev/null +++ b/common/lib/src/model/archive.dart @@ -0,0 +1,18 @@ +import 'dart:io'; +import 'dart:isolate'; + +class ArchiveDownloadProgress { + final double progress; + final int? minutesLeft; + final bool extracting; + + ArchiveDownloadProgress(this.progress, this.minutesLeft, this.extracting); +} + +class ArchiveDownloadOptions { + String archiveUrl; + Directory destination; + SendPort port; + + ArchiveDownloadOptions(this.archiveUrl, this.destination, this.port); +} diff --git a/common/lib/src/util/build.dart b/common/lib/src/util/build.dart index b03e92e..a731c78 100644 --- a/common/lib/src/util/build.dart +++ b/common/lib/src/util/build.dart @@ -1,22 +1,29 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; +import 'package:dio/dio.dart'; -final Uri _manifestSourceUrl = Uri.parse( - "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md"); +final Dio _dio = Dio(); +final String _manifestSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md"; +final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); Future> fetchBuilds(ignored) async { - var response = await http.get(_manifestSourceUrl); + var response = await _dio.get( + _manifestSourceUrl, + options: Options( + responseType: ResponseType.plain + ) + ); if (response.statusCode != 200) { throw Exception("Erroneous status code: ${response.statusCode}"); } var results = []; - for (var line in response.body.split("\n")) { + for (var line in response.data?.split("\n") ?? []) { if(!line.startsWith("|")) { continue; } @@ -44,77 +51,118 @@ Future downloadArchiveBuild(ArchiveDownloadOptions options) async { var stopped = _setupLifecycle(options); var outputDir = Directory("${options.destination.path}\\.build"); outputDir.createSync(recursive: true); - try { - options.destination.createSync(recursive: true); - var fileName = options.archiveUrl.substring(options.archiveUrl.lastIndexOf("/") + 1); - var extension = path.extension(fileName); - var tempFile = File("${outputDir.path}\\$fileName"); - if(tempFile.existsSync()) { - tempFile.deleteSync(recursive: true); - } - - await _download(options, tempFile, stopped); - await _extract(stopped, extension, tempFile, options); - delete(outputDir); - } catch(message) { - throw Exception("Cannot download build: $message"); - } -} - -Future _download(ArchiveDownloadOptions options, File tempFile, Completer stopped) async { - var client = http.Client(); - var request = http.Request("GET", Uri.parse(options.archiveUrl)); - request.headers['Connection'] = 'Keep-Alive'; - var response = await client.send(request); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); + options.destination.createSync(recursive: true); + var fileName = options.archiveUrl.substring(options.archiveUrl.lastIndexOf("/") + 1); + var extension = path.extension(fileName); + var tempFile = File("${outputDir.path}\\$fileName"); + if(tempFile.existsSync()) { + tempFile.deleteSync(recursive: true); } var startTime = DateTime.now().millisecondsSinceEpoch; - var length = response.contentLength!; - var received = 0; - var sink = tempFile.openWrite(); - var subscription = response.stream.listen((data) async { - received += data.length; - var now = DateTime.now(); - var progress = (received / length) * 100; - var msLeft = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch; - var minutesLeft = (msLeft / 1000 / 60).round(); - options.port.send(ArchiveDownloadProgress(progress, minutesLeft, false)); - sink.add(data); - }); + var response = _downloadFile(options, tempFile, startTime); + await Future.any([stopped.future, response]); + if(!stopped.isCompleted) { + var awaitedResponse = await response; + if (!awaitedResponse.statusCode.toString().startsWith("20")) { + throw Exception("Erroneous status code: ${awaitedResponse.statusCode}"); + } - await Future.any([stopped.future, subscription.asFuture()]); - if(stopped.isCompleted) { - await subscription.cancel(); - }else { - await sink.flush(); - await sink.close(); - await sink.done; + await _extract(stopped, extension, tempFile, options); } + + delete(outputDir); +} + +Future _downloadFile(ArchiveDownloadOptions options, File tempFile, int startTime, [int? byteStart = null]) { + var received = byteStart ?? 0; + return _dio.download( + options.archiveUrl, + tempFile.path, + onReceiveProgress: (data, length) { + received = data; + var now = DateTime.now(); + var progress = (received / length) * 100; + var msLeft = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch; + var minutesLeft = (msLeft / 1000 / 60).round(); + options.port.send(ArchiveDownloadProgress(progress, minutesLeft, false)); + }, + deleteOnError: false, + options: Options( + headers: byteStart == null ? null : { + "Range": "bytes=${byteStart}-" + } + ) + ).catchError((error) => _downloadFile(options, tempFile, startTime, received)); } Future _extract(Completer stopped, String extension, File tempFile, ArchiveDownloadOptions options) async { - if(stopped.isCompleted) { - return; - } - - options.port.send(ArchiveDownloadProgress(0, -1, true)); + var startTime = DateTime.now().millisecondsSinceEpoch; Process? process; switch (extension.toLowerCase()) { - case '.zip': + case ".zip": process = await Process.start( - 'tar', - ['-xf', tempFile.path, '-C', options.destination.path], - mode: ProcessStartMode.inheritStdio + "${assetsDirectory.path}\\build\\7zip.exe", + ["a", "-bsp1", '-o"${options.destination.path}"', tempFile.path] ); + process.stdout.listen((bytes) { + var now = DateTime.now().millisecondsSinceEpoch; + var data = utf8.decode(bytes); + if(data == "Everything is Ok") { + options.port.send(ArchiveDownloadProgress(100, 0, true)); + return; + } + + var element = data.trim().split(" ")[0]; + if(!element.endsWith("%")) { + return; + } + + var percentage = int.parse(element.substring(0, element.length - 1)); + if(percentage == 0) { + options.port.send(ArchiveDownloadProgress(percentage.toDouble(), null, true)); + return; + } + + _onProgress(startTime, now, percentage, options); + }); break; - case '.rar': + case ".rar": process = await Process.start( - '${assetsDirectory.path}\\build\\winrar.exe', - ['x', tempFile.path, '*.*', options.destination.path], - mode: ProcessStartMode.inheritStdio + "${assetsDirectory.path}\\build\\winrar.exe", + ["x", "-o+", tempFile.path, "*.*", options.destination.path] ); + process.stdout.listen((event) { + var now = DateTime.now().millisecondsSinceEpoch; + var data = utf8.decode(event); + data.replaceAll("\r", "") + .replaceAll("\b", "") + .trim() + .split("\n") + .forEach((entry) { + if(entry == "All OK") { + options.port.send(ArchiveDownloadProgress(100, 0, true)); + return; + } + + var element = _rarProgressRegex.firstMatch(entry)?.group(1); + if(element == null) { + return; + } + + var percentage = int.parse(element); + if(percentage == 0) { + options.port.send(ArchiveDownloadProgress(percentage.toDouble(), null, true)); + return; + } + + _onProgress(startTime, now, percentage, options); + }); + }); + process.stderr.listen((event) { + var data = utf8.decode(event); + options.port.send(data); + }); break; default: throw ArgumentError("Unexpected file extension: $extension}"); @@ -123,6 +171,12 @@ Future _extract(Completer stopped, String extension, File tempFil await Future.any([stopped.future, process.exitCode]); } +void _onProgress(int startTime, int now, int percentage, ArchiveDownloadOptions options) { + var msLeft = startTime + (now - startTime) * 100 / percentage - now; + var minutesLeft = (msLeft / 1000 / 60).round(); + options.port.send(ArchiveDownloadProgress(percentage.toDouble(), minutesLeft, true)); +} + Completer _setupLifecycle(ArchiveDownloadOptions options) { var stopped = Completer(); var lifecyclePort = ReceivePort(); @@ -133,20 +187,4 @@ Completer _setupLifecycle(ArchiveDownloadOptions options) { }); options.port.send(lifecyclePort.sendPort); return stopped; -} - -class ArchiveDownloadOptions { - String archiveUrl; - Directory destination; - SendPort port; - - ArchiveDownloadOptions(this.archiveUrl, this.destination, this.port); -} - -class ArchiveDownloadProgress { - final double progress; - final int minutesLeft; - final bool extracting; - - ArchiveDownloadProgress(this.progress, this.minutesLeft, this.extracting); } \ No newline at end of file diff --git a/common/lib/src/util/reboot.dart b/common/lib/src/util/reboot.dart index 43c9730..a49eecb 100644 --- a/common/lib/src/util/reboot.dart +++ b/common/lib/src/util/reboot.dart @@ -13,10 +13,9 @@ final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll"); List createRebootArgs(String username, String password, bool host, String additionalArgs) { if(password.isEmpty) { - username = username.isEmpty ? kDefaultPlayerName : username; - username = host ? "$username${Random().nextInt(1000)}" : username; - username = '$username@projectreboot.dev'; + username = '${_parseUsername(username, host)}@projectreboot.dev'; } + password = password.isNotEmpty ? password : "Rebooted"; var args = [ "-epicapp=Fortnite", @@ -48,6 +47,23 @@ List createRebootArgs(String username, String password, bool host, Strin return args; } +String _parseUsername(String username, bool host) { + if(host) { + return "Player${Random().nextInt(1000)}"; + } + + if (username.isEmpty) { + return kDefaultPlayerName; + } + + username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim(); + if(username.isEmpty){ + return kDefaultPlayerName; + } + + return username; +} + Future downloadRebootDll(String url, int? lastUpdateMs, {int hours = 24, bool force = false}) async { Directory? outputDir; diff --git a/common/pubspec.yaml b/common/pubspec.yaml index e7f0aa9..00a302f 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -7,6 +7,7 @@ environment: sdk: ">=2.19.0 <=3.3.3" dependencies: + dio: ^5.3.2 win32: 3.0.0 ffi: ^2.1.0 path: ^1.8.3 diff --git a/documentation/PortForwarding.md b/documentation/en/How can I make my server available to other players.md similarity index 100% rename from documentation/PortForwarding.md rename to documentation/en/How can I make my server available to other players.md diff --git a/gui/README.md b/gui/README.md new file mode 100644 index 0000000..75ccbda --- /dev/null +++ b/gui/README.md @@ -0,0 +1,16 @@ +# reboot_launcher + +Launcher for project reboot + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/gui/assets/build/7zip.exe b/gui/assets/build/7zip.exe new file mode 100644 index 0000000..18342b3 Binary files /dev/null and b/gui/assets/build/7zip.exe differ diff --git a/gui/assets/images/auth.png b/gui/assets/images/authenticator.png similarity index 100% rename from gui/assets/images/auth.png rename to gui/assets/images/authenticator.png diff --git a/gui/assets/images/browse.png b/gui/assets/images/server_browser.png similarity index 100% rename from gui/assets/images/browse.png rename to gui/assets/images/server_browser.png diff --git a/gui/assets/images/tutorial_else_1.png b/gui/assets/images/tutorial_else_1.png deleted file mode 100644 index 7b8d8e8..0000000 Binary files a/gui/assets/images/tutorial_else_1.png and /dev/null differ diff --git a/gui/assets/images/tutorial_else_2.png b/gui/assets/images/tutorial_else_2.png deleted file mode 100644 index fc158c7..0000000 Binary files a/gui/assets/images/tutorial_else_2.png and /dev/null differ diff --git a/gui/assets/images/tutorial_else_3.png b/gui/assets/images/tutorial_else_3.png deleted file mode 100644 index b025c32..0000000 Binary files a/gui/assets/images/tutorial_else_3.png and /dev/null differ diff --git a/gui/assets/images/tutorial_else_4.png b/gui/assets/images/tutorial_else_4.png deleted file mode 100644 index 3162ea3..0000000 Binary files a/gui/assets/images/tutorial_else_4.png and /dev/null differ diff --git a/gui/assets/images/tutorial_else_5.png b/gui/assets/images/tutorial_else_5.png deleted file mode 100644 index cc31569..0000000 Binary files a/gui/assets/images/tutorial_else_5.png and /dev/null differ diff --git a/gui/assets/images/tutorial_else_6.png b/gui/assets/images/tutorial_else_6.png deleted file mode 100644 index 1ff494f..0000000 Binary files a/gui/assets/images/tutorial_else_6.png and /dev/null differ diff --git a/gui/assets/images/tutorial_else_7.png b/gui/assets/images/tutorial_else_7.png deleted file mode 100644 index 0e96942..0000000 Binary files a/gui/assets/images/tutorial_else_7.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_1.png b/gui/assets/images/tutorial_own_1.png deleted file mode 100644 index 7b8d8e8..0000000 Binary files a/gui/assets/images/tutorial_own_1.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_10.png b/gui/assets/images/tutorial_own_10.png deleted file mode 100644 index 1ff494f..0000000 Binary files a/gui/assets/images/tutorial_own_10.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_11.png b/gui/assets/images/tutorial_own_11.png deleted file mode 100644 index 0e96942..0000000 Binary files a/gui/assets/images/tutorial_own_11.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_2.png b/gui/assets/images/tutorial_own_2.png deleted file mode 100644 index 13813ae..0000000 Binary files a/gui/assets/images/tutorial_own_2.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_3.png b/gui/assets/images/tutorial_own_3.png deleted file mode 100644 index b025c32..0000000 Binary files a/gui/assets/images/tutorial_own_3.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_4.png b/gui/assets/images/tutorial_own_4.png deleted file mode 100644 index 3162ea3..0000000 Binary files a/gui/assets/images/tutorial_own_4.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_5.png b/gui/assets/images/tutorial_own_5.png deleted file mode 100644 index 79461fe..0000000 Binary files a/gui/assets/images/tutorial_own_5.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_6.png b/gui/assets/images/tutorial_own_6.png deleted file mode 100644 index 1ff494f..0000000 Binary files a/gui/assets/images/tutorial_own_6.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_7.png b/gui/assets/images/tutorial_own_7.png deleted file mode 100644 index e4dbf8a..0000000 Binary files a/gui/assets/images/tutorial_own_7.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_8.png b/gui/assets/images/tutorial_own_8.png deleted file mode 100644 index 11f8b41..0000000 Binary files a/gui/assets/images/tutorial_own_8.png and /dev/null differ diff --git a/gui/assets/images/tutorial_own_9.png b/gui/assets/images/tutorial_own_9.png deleted file mode 100644 index cc31569..0000000 Binary files a/gui/assets/images/tutorial_own_9.png and /dev/null differ diff --git a/gui/assets/misc/udp.ps1 b/gui/assets/misc/udp.ps1 deleted file mode 100644 index 6e2d71c..0000000 --- a/gui/assets/misc/udp.ps1 +++ /dev/null @@ -1,40 +0,0 @@ - [cmdletbinding( - DefaultParameterSetName = '', - ConfirmImpact = 'low' - )] - Param( - [Parameter( - Mandatory = $True, - Position = 0, - ParameterSetName = '', - ValueFromPipeline = $True)] - [String]$computer, - [Parameter( - Position = 1, - Mandatory = $True, - ParameterSetName = '')] - [Int16]$port - ) - Process { - $udpobject = new-Object system.Net.Sockets.Udpclient - $udpobject.client.ReceiveTimeout = 2000 - $udpobject.Connect("$computer", $port) - $a = new-object system.text.asciiencoding - $byte = $a.GetBytes("$( Get-Date )") - [void]$udpobject.Send($byte, $byte.length) - $remoteendpoint = New-Object system.net.ipendpoint([system.net.ipaddress]::Any, 0) - Try - { - $receivebytes = $udpobject.Receive([ref]$remoteendpoint) - [string]$returndata = $a.GetString($receivebytes) - If ($returndata) - { - exit 0 - } - } - Catch - { - $udpobject.close() - exit 1 - } - } diff --git a/gui/l10n.yaml b/gui/l10n.yaml new file mode 100644 index 0000000..84b5ef2 --- /dev/null +++ b/gui/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: reboot_en.arb +output-localization-file: reboot_localizations.dart \ No newline at end of file diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb new file mode 100644 index 0000000..a02e302 --- /dev/null +++ b/gui/lib/l10n/reboot_en.arb @@ -0,0 +1,268 @@ +{ + "find": "Find a setting", + "on": "On", + "off": "Off", + "resetDefaultsContent": "Reset", + "resetDefaultsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible", + "resetDefaultsDialogSecondaryAction": "Close", + "resetDefaultsDialogPrimaryAction": "Reset", + "authenticatorName": "Authenticator", + "authenticatorConfigurationName": "Authenticator configuration", + "authenticatorConfigurationDescription": "This section contains the authenticator's configuration", + "authenticatorConfigurationHostName": "Host", + "authenticatorConfigurationHostDescription": "The hostname of the authenticator", + "authenticatorConfigurationPortName": "Port", + "authenticatorConfigurationPortDescription": "The port of the authenticator", + "authenticatorConfigurationDetachedName": "Detached", + "authenticatorConfigurationDetachedDescription": "Whether the embedded authenticator should be started as a separate process, useful for debugging", + "authenticatorInstallationDirectoryName": "Installation directory", + "authenticatorInstallationDirectoryDescription": "Opens the folder where the embedded authenticator is located", + "authenticatorInstallationDirectoryContent": "Show Files", + "authenticatorResetDefaultsName": "Reset authenticator", + "authenticatorResetDefaultsDescription": "Resets the authenticator's settings to their default values", + "authenticatorResetDefaultsContent": "Reset", + "hostGameServerName": "Game server", + "hostGameServerDescription": "Provide basic information about your game server for the Server Browser", + "hostGameServerNameName": "Name", + "hostGameServerNameDescription": "The name of your game server", + "hostGameServerDescriptionName": "Description", + "hostGameServerDescriptionDescription": "The description of your game server", + "hostGameServerPasswordName": "Password", + "hostGameServerPasswordDescription": "The password of your game server, if you need one", + "hostGameServerDiscoverableName": "Discoverable", + "hostGameServerDiscoverableDescription": "Make your server available to other players on the server browser", + "hostShareName": "Share", + "hostShareDescription": "Make it easy for other people to join your server with the options in this section", + "hostShareLinkName": "Link", + "hostShareLinkDescription": "Copies a link for your server to the clipboard (requires the Reboot Launcher)", + "hostShareLinkContent": "Copy Link", + "hostShareLinkMessageSuccess": "Copied your link to the clipboard", + "hostShareIpName": "Public IP", + "hostShareIpDescription": "Copies your current public IP to the clipboard (doesn't require the Reboot Launcher)", + "hostShareIpContent": "Copy IP", + "hostShareIpMessageLoading": "Obtaining your public IP...", + "hostShareIpMessageSuccess": "Copied your link to the clipboard", + "hostShareIpMessageError": "An error occurred while obtaining your public IP: {error}", + "hostResetName": "Reset game server", + "hostResetDescription": "Resets the game server's settings to their default values", + "hostResetContent": "Reset", + "browserName": "Server Browser", + "noServersAvailableTitle": "No servers are available right now", + "noServersAvailableSubtitle": "Host a server yourself or come back later", + "joinServer": "Join Server", + "noServersAvailableByQueryTitle": "No results found", + "noServersAvailableByQuerySubtitle": "No server matches your query", + "findServer": "Find a server", + "copyIp": "Copy IP", + "hostName": "Host", + "matchmakerName": "Matchmaker", + "matchmakerConfigurationName": "Matchmaker configuration", + "matchmakerConfigurationDescription": "This section contains the matchmaker's configuration", + "matchmakerConfigurationHostName": "Host", + "matchmakerConfigurationHostDescription": "The hostname of the matchmaker", + "matchmakerConfigurationPortName": "Port", + "matchmakerConfigurationPortDescription": "The port of the matchmaker", + "matchmakerConfigurationAddressName": "Game server address", + "matchmakerConfigurationAddressDescription": "The address of the game server used by the matchmaker", + "matchmakerConfigurationDetachedName": "Detached", + "matchmakerConfigurationDetachedDescription": "Whether the embedded matchmaker should be started as a separate process, useful for debugging", + "matchmakerInstallationDirectoryName": "Installation directory", + "matchmakerInstallationDirectoryDescription": "Opens the folder where the embedded matchmaker is located", + "matchmakerInstallationDirectoryContent": "Show Files", + "matchmakerResetDefaultsName": "Reset matchmaker", + "matchmakerResetDefaultsDescription": "Resets the matchmaker's settings to their default values", + "matchmakerResetDefaultsContent": "Reset", + "matchmakerResetDefaultsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible", + "matchmakerResetDefaultsDialogSecondaryAction": "Close", + "matchmakerResetDefaultsDialogPrimaryAction": "Reset", + "playName": "Play", + "playGameServerName": "Game Server", + "playGameServerDescription": "Helpful shortcuts to find the server where you want to play", + "playGameServerContentLocal": "Your server", + "playGameServerContentBrowser": "{owner}'s server", + "playGameServerContentCustom": "{address}", + "playGameServerHostName": "Host a server", + "playGameServerHostDescription": "Do you want to create a game server for yourself or your friends? Host one!", + "playGameServerHostContent": "Host", + "playGameServerBrowserName": "Browse servers", + "playGameServerBrowserDescription": "Find a discoverable server hosted on the Reboot Launcher in the server browser", + "playGameServerBrowserContent": "Browse", + "playGameServerCustomName": "Join a Custom server", + "playGameServerCustomDescription": "Type the address of any server, whether it was hosted on the Reboot Launcher or not", + "playGameServerCustomContent": "Enter IP", + "settingsName": "Settings", + "settingsClientName": "Client settings", + "settingsClientDescription": "This section contains the dlls used to make the Fortnite client work", + "settingsClientConsoleName": "Unreal engine console", + "settingsClientConsoleDescription": "This file is injected to unlock the Unreal Engine Console", + "settingsClientAuthName": "Authentication patcher", + "settingsClientAuthDescription": "This file is injected to redirect all HTTP requests to the launcher's authenticator", + "settingsClientMemoryName": "Memory patcher", + "settingsClientMemoryDescription": "This file is injected to prevent the Fortnite client from crashing because of a memory leak", + "settingsClientArgsName": "Custom launch arguments", + "settingsClientArgsDescription": "Additional arguments to use when launching the game", + "settingsClientArgsPlaceholder": "Arguments...", + "settingsServerName": "Game server settings", + "settingsServerSubtitle": "This section contains settings related to the game server implementation", + "settingsServerFileName": "Implementation", + "settingsServerFileDescription": "This file is injected to create a game server & host matches", + "settingsServerPortName": "Port", + "settingsServerPortDescription": "The port used by the game server dll", + "settingsServerMirrorName": "Update mirror", + "settingsServerMirrorDescription": "The URL used to update the game server dll", + "settingsServerMirrorPlaceholder": "mirror", + "settingsServerTimerName": "Update timer", + "settingsServerTimerSubtitle": "Determines when the game server dll should be updated", + "settingsUtilsName": "Launcher utilities", + "settingsUtilsSubtitle": "This section contains handy settings for the launcher", + "settingsUtilsInstallationDirectoryName": "Installation directory", + "settingsUtilsInstallationDirectorySubtitle": "Opens the installation directory", + "settingsUtilsInstallationDirectoryContent": "Show Files", + "settingsUtilsBugReportName": "Create a bug report", + "settingsUtilsBugReportSubtitle": "Help me fix bugs by reporting them", + "settingsUtilsBugReportContent": "Report a bug", + "settingsUtilsResetDefaultsName": "Reset settings", + "settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values", + "settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible", + "settingsUtilsResetDefaultsContent": "Reset", + "settingsUtilsDialogSecondaryAction": "Close", + "settingsUtilsDialogPrimaryAction": "Reset", + "addVersionName": "Version", + "addVersionDescription": "Select the version of Fortnite you want to use", + "addLocalBuildName": "Add a version from this PC's local storage", + "addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work", + "addLocalBuildContent": "Add build", + "downloadBuildName": "Download any version from the cloud", + "downloadBuildDescription": "Download any Fortnite build easily from the cloud", + "downloadBuildContent": "Download", + "cannotUpdateGameServer": "An error occurred while updating the game server: {error}", + "launchFortnite": "Launch Fortnite", + "closeFortnite": "Close Fortnite", + "updateGameServerDllNever": "Never", + "updateGameServerDllEvery": "Every {name}", + "selectPathPlaceholder": "Path", + "selectPathWindowTitle": "Select a file", + "defaultDialogSecondaryAction": "Close", + "stopLoadingDialogAction": "Stop", + "copyErrorDialogTitle": "Copy error", + "copyErrorDialogSuccess": "Copied error to clipboard", + "defaultServerName": "Reboot Game Server", + "defaultServerDescription": "Just another server", + "updatingRebootDll": "Downloading reboot dll...", + "updatedRebootDll": "The reboot dll was downloaded successfully", + "updateRebootDllError": "An error occurred while downloading the reboot dll: {error}", + "updateRebootDllErrorAction": "Retry", + "uncaughtErrorMessage": "An uncaught error was thrown: {error}", + "launchingHeadlessServer": "Launching the headless server...", + "usernameOrEmail": "Username/Email", + "usernameOrEmailPlaceholder": "Type your username or email", + "password": "Password", + "passwordPlaceholder": "Type your password, if you want to use one", + "cancelProfileChanges": "Cancel", + "saveProfileChanges": "Save", + "startingServer": "Starting the {name}...", + "startedServer": "The {name} was started successfully", + "startServerError": "An error occurred while starting the {name}: {error}", + "stoppingServer": "Stopping the {name}...", + "stoppedServer": "The {name} was stopped successfully", + "stopServerError": "An error occurred while stopping the {name}: {error}", + "missingHostNameError": "Missing hostname in the {name} configuration", + "missingPortError": "Missing port in the {name} configuration", + "illegalPortError": "Invalid port in the {name} configuration", + "freeingPort": "Freeing port {port}...", + "freedPort": "Port {port} was freed successfully", + "freePortError": "An error occurred while freeing port {port}: {error}", + "pingingRemoteServer": "Pinging the remote {name}...", + "pingingLocalServer": "Pinging the {type} {name}...", + "pingError": "Cannot ping the {type} {name}", + "joinSelfServer": "You can't join your own server", + "wrongServerPassword": "Wrong password: please try again", + "offlineServer": "This server isn't online right now: please try again later", + "serverPassword": "Password", + "serverPasswordPlaceholder": "Type the server's password", + "serverPasswordCancel": "Cancel", + "serverPasswordConfirm": "Confirm", + "joinedServer": "You joined {author}'s server successfully!", + "copiedIp": "Copied IP to the clipboard", + "selectVersion": "Select a version", + "noVersions": "Please create or download a version", + "missingVersion": "This version doesn't exist on the local machine", + "deleteVersionDialogTitle": "Are you sure you want to delete this version?", + "deleteVersionFromDiskOption": "Delete version files from disk", + "deleteVersionCancel": "Keep", + "deleteVersionConfirm": "Delete", + "versionName": "Name", + "versionNameLabel": "Type the new version name", + "newVersionNameConfirm": "Save", + "newVersionNameLabel": "Type the new version name", + "gameFolderTitle": "Game folder", + "gameFolderPlaceholder": "Type the new game folder", + "gameFolderPlaceWindowTitle": "Select game folder", + "gameFolderLabel": "Path", + "openInExplorer": "Open in explorer", + "modify": "Modify", + "delete": "Delete", + "build": "Build", + "selectBuild": "Select a fortnite build", + "fetchingBuilds": "Fetching builds and disks...", + "unknownError": "Unknown error", + "downloadVersionError": "Cannot download version: {error}", + "downloadedVersion": "The download was completed successfully!", + "download": "Download", + "downloading": "Downloading...", + "extracting": "Extracting...", + "buildProgress": "{progress}%", + "buildInstallationDirectory": "Installation directory", + "buildInstallationDirectoryPlaceholder": "Type the installation directory", + "buildInstallationDirectoryWindowTitle": "Select installation directory", + "timeLeft": "Time left: {timeLeft, plural, =0{less than a minute} =1{about {timeLeft} minute} other{about {timeLeft} minutes}}", + "localBuildsWarning": "Local builds are not guaranteed to work", + "saveLocalVersion": "Save", + "embedded": "Embedded", + "remote": "Remote", + "local": "Local", + "checkServer": "Check {name}", + "startServer": "Start {name}", + "stopServer": "Stop {name}", + "startHosting": "Start hosting", + "stopHosting": "Stop hosting", + "startGame": "Start fortnite", + "stopGame": "Close fortnite", + "waitingForGameServer": "Waiting for the game server to boot up...", + "gameServerStartWarning": "The headless server was started successfully, but the game server didn't boot", + "gameServerStartLocalWarning": "The game server was started successfully, but other players can't join", + "gameServerStarted": "The game server was started successfully", + "checkingGameServer": "Checking if other players can join the game server...", + "checkGameServerFixMessage": "Other players can't join the game server as port {port} isn't open", + "checkGameServerFixAction": "Fix", + "infoName": "Help", + "emptyVersionName": "Empty version name", + "versionAlreadyExists": "This version already exists", + "emptyGamePath": "Empty game path", + "directoryDoesNotExist": "Directory doesn't exist", + "missingShippingExe": "Invalid game path: missing FortniteClient-Win64-Shipping", + "invalidDownloadPath": "Invalid download path", + "invalidDllPath": "Invalid dll path", + "dllDoesNotExist": "The file doesn't exist", + "invalidDllExtension": "This file is not a dll", + "emptyHostname": "Empty hostname", + "hostnameFormat": "Wrong hostname format: expected ip:port", + "emptyURL": "Empty update URL", + "missingVersionError": "Download or select a version before starting Fortnite", + "missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted", + "corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version", + "missingDllError": "The dll at {path} doesn't exist", + "corruptedDllError": "Cannot inject dll: {error}", + "tokenError": "Cannot log in into Fortnite: authentication error", + "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", + "serverNoLongerAvailable": "{owner}'s server is no longer available", + "serverNoLongerAvailableUnnamed": "The previous server is no longer available", + "noServerFound": "No server found: invalid or expired link", + "settingsUtilsThemeName": "Theme", + "settingsUtilsThemeDescription": "Select the theme to use inside the launcher", + "dark": "Dark", + "light": "Light", + "system": "System", + "settingsUtilsLanguageName": "Language", + "settingsUtilsLanguageDescription": "Select the language to use inside the launcher" +} diff --git a/gui/lib/l10n/reboot_pl.arb b/gui/lib/l10n/reboot_pl.arb new file mode 100644 index 0000000..d820dca --- /dev/null +++ b/gui/lib/l10n/reboot_pl.arb @@ -0,0 +1,251 @@ +{ + "find": "Znajdź ustawienie", + "on": "Wł.", + "off": "Wył.", + "resetDefaultsContent": "Zresetuj", + "resetDefaultsDialogTitle": "Czy chcesz zresetować wszystkie ustawienia na tej karcie do wartości domyślnych? To działanie jest nieodwracalne", + "resetDefaultsDialogSecondaryAction": "Zamknij", + "resetDefaultsDialogPrimaryAction": "Zresetuj", + "authenticatorName": "Uwierzytelniacz", + "authenticatorConfigurationName": "Konfiguracja uwierzytelniacza", + "authenticatorConfigurationDescription": "Ta sekcja zawiera konfigurację uwierzytelniacza.", + "authenticatorConfigurationHostName": "Hostuj", + "authenticatorConfigurationHostDescription": "Nazwa hosta uwierzytelniacza", + "authenticatorConfigurationPortName": "Port", + "authenticatorConfigurationPortDescription": "Port uwierzytelniacza", + "authenticatorConfigurationDetachedName": "Odłączony", + "authenticatorConfigurationDetachedDescription": "Czy wbudowany uwierzytelniacz powinien być uruchamiany jako oddzielny proces, przydatny do debugowania.", + "authenticatorInstallationDirectoryName": "Folder instalacji", + "authenticatorInstallationDirectoryDescription": "Otwiera folder, w którym znajduje się wbudowany uwierzytelniacz.", + "authenticatorInstallationDirectoryContent": "Pokaż pliki", + "authenticatorResetDefaultsName": "Zresetuj uwierzytelniacz", + "authenticatorResetDefaultsDescription": "Przywraca domyślne ustawienia uwierzytelniacza.", + "authenticatorResetDefaultsContent": "Zresetuj", + "hostGameServerName": "Serwer gry", + "hostGameServerDescription": "Podaj podstawowe informacje o serwerze gry dla przeglądarki serwerów.", + "hostGameServerNameName": "Nazwa", + "hostGameServerNameDescription": "Nazwa twojego serwera gry", + "hostGameServerDescriptionName": "Opis", + "hostGameServerDescriptionDescription": "Opis twojego serwera gry", + "hostGameServerPasswordName": "Hasło", + "hostGameServerPasswordDescription": "Hasło do twojego serwera gry, jeśli jest potrzebne.", + "hostGameServerDiscoverableName": "Wykrywalny", + "hostGameServerDiscoverableDescription": "Udostępnij swój serwer innym graczom w przeglądarce serwerów.", + "hostShareName": "Udostępnij", + "hostShareDescription": "Ułatw innym osobom dołączenie do twojego serwera dzięki opcjom dostępnym w tej sekcji.", + "hostShareLinkName": "Link", + "hostShareLinkDescription": "Kopiuje link twojego serwera do schowka (wymaga Reboot Launcher).", + "hostShareLinkContent": "Kopiuj link", + "hostShareLinkMessageSuccess": "Skopiowano twój link do schowka", + "hostShareIpName": "Publiczne IP", + "hostShareIpDescription": "Kopiuje bieżące publiczne IP do schowka (nie wymaga Reboot Launcher).", + "hostShareIpContent": "Kopiuj IP", + "hostShareIpMessageLoading": "Uzyskiwanie publicznego IP...", + "hostShareIpMessageSuccess": "Skopiowano twój link do schowka.", + "hostShareIpMessageError": "Wystąpił błąd podczas uzyskiwania twojego publicznego IP: {error}.", + "hostResetName": "Zresetuj serwer gry", + "hostResetDescription": "Resetuje ustawienia serwera gry do wartości domyślnych.", + "hostResetContent": "Zresetuj", + "browserName": "Przeglądarka serwerów", + "noServersAvailableTitle": "W tej chwili żadne serwery nie są dostępne.", + "noServersAvailableSubtitle": "Hostuj serwer samodzielnie lub wróć później.", + "joinServer": "Dołącz do serwera", + "noServersAvailableByQueryTitle": "Nie znaleziono rezultatów", + "noServersAvailableByQuerySubtitle": "Żaden serwer nie pasuje do Twojego zapytania", + "findServer": "Znajdź serwer", + "copyIp": "Kopiuj IP", + "hostName": "Hostuj", + "matchmakerName": "System dobierania graczy", + "matchmakerConfigurationName": "Konfiguracja systemu dobierania graczy", + "matchmakerConfigurationDescription": "Ta sekcja zawiera konfigurację systemu dobierania graczy.", + "matchmakerConfigurationHostName": "Hostuj", + "matchmakerConfigurationHostDescription": "Nazwa hosta systemu dobierania graczy.", + "matchmakerConfigurationPortName": "Port", + "matchmakerConfigurationPortDescription": "Port systemu dobierania graczy.", + "matchmakerConfigurationAddressName": "Adres serwera gry", + "matchmakerConfigurationAddressDescription": "Adres serwera gry używanego przez system dobierania graczy", + "matchmakerConfigurationDetachedName": "Odłączony", + "matchmakerConfigurationDetachedDescription": "Czy wbudowany system dobierania graczy powinien być uruchamiany jako oddzielny proces, przydatny do debugowania.", + "matchmakerInstallationDirectoryName": "Folder instalacji", + "matchmakerInstallationDirectoryDescription": "Otwiera folder, w którym znajduje się wbudowany system dobierania graczy.", + "matchmakerInstallationDirectoryContent": "Pokaż pliki", + "matchmakerResetDefaultsName": "Zresetuj system dobierania graczy", + "matchmakerResetDefaultsDescription": "Resetuje ustawienia systemu dobierania graczy do wartości domyślnych.", + "matchmakerResetDefaultsContent": "Zresetuj", + "matchmakerResetDefaultsDialogTitle": "Czy chcesz zresetować wszystkie ustawienia na tej karcie do wartości domyślnych? To działanie jest nieodwracalne.", + "matchmakerResetDefaultsDialogSecondaryAction": "Zamknij", + "matchmakerResetDefaultsDialogPrimaryAction": "Zresetuj", + "playName": "Graj", + "playGameServerName": "Server gry", + "playGameServerDescription": "Pomocne skróty do znalezienia serwera, na którym chcesz grać.", + "playGameServerContentLocal": "Twój serwer", + "playGameServerContentBrowser": "Server {owner}", + "playGameServerContentCustom": "{address}", + "playGameServerHostName": "Hostuj serwer", + "playGameServerHostDescription": "Chcesz stworzyć serwer gry dla siebie lub swoich znajomych? Hostuj jeden!", + "playGameServerHostContent": "Hostuj", + "playGameServerBrowserName": "Przeglądaj serwery", + "playGameServerBrowserDescription": "Znajdź wykrywalny serwer hostowany na Reboot Launcher w przeglądarce serwerów.", + "playGameServerBrowserContent": "Przeglądaj", + "playGameServerCustomName": "Dołącz do serwera niestandardowego", + "playGameServerCustomDescription": "Wpisz adres dowolnego serwera, niezależnie od tego, czy był on hostowany poprzez Reboot Launcher, czy nie.", + "playGameServerCustomContent": "Wprowadź IP", + "settingsName": "Ustawienia", + "settingsClientName": "Ustawienia klienta", + "settingsClientDescription": "Ta sekcja zawiera pliki dll używane do działania klienta Fortnite.", + "settingsClientConsoleName": "Konsola Unreal Engine", + "settingsClientConsoleDescription": "Ten plik jest wstrzykiwany w celu odblokowania konsoli Unreal Engine.", + "settingsClientAuthName": "Łatka uwierzytelniacza", + "settingsClientAuthDescription": "Ten plik jest wstrzykiwany w celu przekierowania wszystkich żądań HTTP do uwierzytelniacza programu uruchamiającego.", + "settingsClientMemoryName": "Łatka pamięci", + "settingsClientMemoryDescription": "Ten plik jest wstrzykiwany, aby zapobiec awarii klienta Fortnite z powodu wycieku pamięci.", + "settingsClientArgsName": "Niestandardowe argumenty uruchamiania", + "settingsClientArgsDescription": "Dodatkowe argumenty do użycia podczas uruchamiania gry.", + "settingsClientArgsPlaceholder": "-przykład", + "settingsServerName": "Ustawienia serwera gry", + "settingsServerSubtitle": "Ta sekcja zawiera ustawienia związane z implementacją serwera gry.", + "settingsServerFileName": "Implementacja", + "settingsServerFileDescription": "Ten plik jest wstrzykiwany w celu utworzenia serwera gry i hostowania meczów.", + "settingsServerPortName": "Port", + "settingsServerPortDescription": "Port używany przez dll serwera gry.", + "settingsServerMirrorName": "Aktualizacja linku", + "settingsServerMirrorDescription": "Adres URL używany do aktualizacji dll serwera gry.", + "settingsServerMirrorPlaceholder": "link", + "settingsServerTimerName": "Aktualizacja licznika czasu", + "settingsServerTimerSubtitle": "Określa, kiedy dll serwera gry powininno zostać zaktualizowane.", + "settingsUtilsName": "Narzędzia programu do uruchamiania", + "settingsUtilsSubtitle": "Ta sekcja zawiera przydatne ustawienia programu uruchamiającego.", + "settingsUtilsInstallationDirectoryName": "Katalog instalacji", + "settingsUtilsInstallationDirectorySubtitle": "Otwiera katalog instalacji", + "settingsUtilsInstallationDirectoryContent": "Pokaż pliki", + "settingsUtilsBugReportName": "Utwórz raport o błędzie", + "settingsUtilsBugReportSubtitle": "Pomóż mi naprawić błędy, zgłaszając je.", + "settingsUtilsBugReportContent": "Zgłoś błąd", + "settingsUtilsResetDefaultsName": "Zresetuj ustawienia", + "settingsUtilsResetDefaultsSubtitle": "Resetuje ustawienia programu uruchamiającego do wartości domyślnych.", + "settingsUtilsDialogTitle": "Czy chcesz zresetować wszystkie ustawienia na tej karcie do wartości domyślnych? To działanie jest nieodwracalne.", + "settingsUtilsResetDefaultsContent": "Zresetuj", + "settingsUtilsDialogSecondaryAction": "Zamknij", + "settingsUtilsDialogPrimaryAction": "Zresetuj", + "addVersionName": "Wersja", + "addVersionDescription": "Wybierz wersję Fortnite, której chcesz użyć.", + "addLocalBuildName": "Dodaj wersję z lokalnej pamięci masowej tego komputera.", + "addLocalBuildDescription": "Wersje pochodzące z dysku lokalnego nie mają gwarancji działania.", + "addLocalBuildContent": "Dodaj kompilację", + "downloadBuildName": "Pobierz dowolną wersję z chmury", + "downloadBuildDescription": "Łatwe pobieranie dowolnej wersji Fortnite z chmury.", + "downloadBuildContent": "Pobierz", + "cannotUpdateGameServer": "Wystąpił błąd podczas aktualizacji serwera gry: {error}.", + "launchFortnite": "Uruchom Fortnite", + "closeFortnite": "Zamknij Fortnite", + "updateGameServerDllNever": "Nigdy", + "updateGameServerDllEvery": "Każda {name}", + "selectPathPlaceholder": "Ścieżka", + "selectPathWindowTitle": "Wybierz plik", + "defaultDialogSecondaryAction": "Zamknij", + "stopLoadingDialogAction": "Stop", + "copyErrorDialogTitle": "Kopiuj błąd", + "copyErrorDialogSuccess": "Skopiowano błąd do schowka.", + "defaultServerName": "Serwer gry Reboot", + "defaultServerDescription": "Po prostu kolejny serwer", + "updatingRebootDll": "Pobieranie dll reboot...", + "updatedRebootDll": "Plik dll reboot został pobrany pomyślnie", + "updateRebootDllError": "Wystąpił błąd podczas pobierania dll reboot: {error}.", + "updateRebootDllErrorAction": "Ponów", + "uncaughtErrorMessage": "Wystąpił niewyłapany błąd: {error}.", + "launchingHeadlessServer": "Uruchamianie serwera bezgłowego ...", + "usernameOrEmail": "Nazwa użytkownika/Email", + "usernameOrEmailPlaceholder": "Wpisz swoją nazwę użytkownika lub adres e-mail.", + "password": "Hasło", + "passwordPlaceholder": "Wpisz hasło, jeśli chcesz je użyć.", + "cancelProfileChanges": "Anuluj", + "saveProfileChanges": "Zapisz", + "startingServer": "Uruchamianie {name}...", + "startedServer": "{name} został uruchomiony pomyślnie.", + "startServerError": "Wystąpił błąd podczas uruchamiania {name}: {error}.", + "stoppingServer": "Zatrzymanie {name}...", + "stoppedServer": "{name} został pomyślnie zatrzymany.", + "stopServerError": "Wystąpił błąd podczas zatrzymywania {name}: {error}.", + "missingHostNameError": "Brakująca nazwa hosta w konfiguracji {name}.", + "missingPortError": "Brakujący port w konfiguracji {name}.", + "illegalPortError": "Niepoprawny port w konfiguracji {name}.", + "freeingPort": "Zwalnianie portu {port}...", + "freedPort": "Port {port} został pomyślnie zwolniony.", + "freePortError": "Wystąpił błąd podczas zwalniania portu {port}: {error}.", + "pingingRemoteServer": "Pingowanie zdalnego {name}...", + "pingingLocalServer": "Pingowanie {type} {name}...", + "pingError": "Nie można pingować {type} {name}", + "joinSelfServer": "Nie możesz dołączyć do własnego serwera.", + "wrongServerPassword": "Błędne hasło: spróbuj ponownie.", + "offlineServer": "Ten serwer nie jest teraz online: spróbuj ponownie później.", + "serverPassword": "Hasło", + "serverPasswordPlaceholder": "Wpisz hasło serwera", + "serverPasswordCancel": "Anuluj", + "serverPasswordConfirm": "Potwierdź", + "joinedServer": "Udało ci się dołączyć do serwera {author}!", + "copiedIp": "Skopiowano IP do schowka", + "selectVersion": "Wybierz wersję", + "noVersions": "Proszę utwórzyć lub pobrać wersję.", + "missingVersion": "Ta wersja nie istnieje na lokalnej maszynie.", + "deleteVersionDialogTitle": "Czy na pewno chcesz usunąć tę wersję?", + "deleteVersionFromDiskOption": "Usuń pliki wersji z dysku", + "deleteVersionCancel": "Zachowaj", + "deleteVersionConfirm": "Usuń", + "versionName": "Nazwa", + "versionNameLabel": "Wpisz nazwę nowej wersji", + "newVersionNameConfirm": "Zapisz", + "newVersionNameLabel": "Wpisz nazwę nowej wersji", + "gameFolderTitle": "Folder gry", + "gameFolderPlaceholder": "Wpisz nowy folder gry", + "gameFolderPlaceWindowTitle": "Wybierz folder gry", + "gameFolderLabel": "Ścieżka", + "openInExplorer": "Otwórz w eksploratorze", + "modify": "Modyfikuj", + "delete": "Usuń", + "build": "Kompilacja", + "selectBuild": "Wybierz kompilację fortnite", + "fetchingBuilds": "Pobieranie kompilacji i dysków...", + "unknownError": "Nieznany błąd", + "downloadVersionError": "Nie można pobrać wersji: {error}.", + "downloadedVersion": "Pobieranie zostało zakończone pomyślnie!", + "download": "Pobierz", + "downloading": "Pobieranie...", + "extracting": "Wyodrębnianie...", + "buildProgress": "{progress}%", + "buildInstallationDirectory": "Katalog instalacji", + "buildInstallationDirectoryPlaceholder": "Wpisz katalog instalacji", + "buildInstallationDirectoryWindowTitle": "Wybierz katalog instalacji", + "timeLeft": "Pozostały czas: {timeLeft, plural, =0{mniej niż minuta} =1{około {timeLeft} minuta} other{około {timeLeft} minut}}", + "localBuildsWarning": "Nie ma gwarancji, że lokalne kompilacje będą działać.", + "saveLocalVersion": "Zapisz", + "embedded": "Wbudowany", + "remote": "Zdalny", + "local": "Lokalny", + "checkServer": "Sprawdź {name}", + "startServer": "Uruchom {name}", + "stopServer": "Zatrzymaj {name}", + "startHosting": "Rozpocznij hosting", + "stopHosting": "Zatrzymaj hosting", + "startGame": "Uruchom fortnite", + "stopGame": "Zamknij fortnite", + "waitingForGameServer": "Oczekiwanie na uruchomienie serwera gry...", + "gameServerStartWarning": "Serwer bezgłowy został pomyślnie uruchomiony, ale serwer gry się nie uruchomił.", + "gameServerStartLocalWarning": "Serwer gry został pomyślnie uruchomiony, ale inni gracze nie mogą do niego dołączyć.", + "gameServerStarted": "Serwer gry został pomyślnie uruchomiony.", + "checkingGameServer": "Sprawdzanie, czy inni gracze mogą dołączyć do serwera gry...", + "checkGameServerFixMessage": "Inni gracze nie mogą dołączyć do serwera gry, ponieważ port {port} nie jest otwarty.", + "checkGameServerFixAction": "Napraw", + "infoName": "Help", + "emptyVersionName": "Pusta nazwa wersji", + "versionAlreadyExists": "Ta wersja już istnieje", + "emptyGamePath": "Pusta ścieżka gry", + "directoryDoesNotExist": "Katalog nie istnieje", + "missingShippingExe": "Nieprawidłowa ścieżka do gry: brak FortniteClient-Win64-Shipping", + "invalidDownloadPath": "Nieprawidłowa ścieżka pobierania", + "invalidDllPath": "Nieprawidłowa ścieżka dll", + "dllDoesNotExist": "Plik nie istnieje", + "invalidDllExtension": "Ten plik nie jest plikiem dll", + "emptyHostname": "Pusta nazwa hosta", + "hostnameFormat": "Nieprawidłowy format nazwy hosta: oczekiwano ip:port", + "emptyURL": "Pusty adres URL aktualizacji" +} diff --git a/gui/lib/main.dart b/gui/lib/main.dart index 2c6841e..71ca6d9 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:app_links/app_links.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_common/common.dart'; @@ -17,42 +19,44 @@ import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart'; -import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/watch.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; import 'package:url_protocol/url_protocol.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; const double kDefaultWindowWidth = 1536; const double kDefaultWindowHeight = 1024; const String kCustomUrlSchema = "reboot"; -void main() async { - runZonedGuarded(() async { - await installationDirectory.create(recursive: true); - await Supabase.initialize( - url: supabaseUrl, - anonKey: supabaseAnonKey - ); - WidgetsFlutterBinding.ensureInitialized(); - await SystemTheme.accentColor.load(); - var storageError = await _initStorage(); - var urlError = await _initUrlHandler(); - var windowError = await _initWindow(); - var observerError = _initObservers(); - _checkGameServer(); - runApp(const RebootApplication()); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors([urlError, storageError, windowError, observerError])); - }, - (error, stack) => onError(error, stack, false), - zoneSpecification: ZoneSpecification( - handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) - )); -} +void main() => runZonedGuarded(() async { + await installationDirectory.create(recursive: true); + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseAnonKey + ); + WidgetsFlutterBinding.ensureInitialized(); + await SystemTheme.accentColor.load(); + _initWindow(); + var storageError = await _initStorage(); + var urlError = await _initUrlHandler(); + var observerError = _initObservers(); + _checkGameServer(); + runApp(const RebootApplication()); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors([urlError, storageError, observerError])); +}, + (error, stack) => onError(error, stack, false), + zoneSpecification: ZoneSpecification( + handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) + )); -void _handleErrors(List errors) => errors.where((element) => element != null).forEach((element) => onError(element, null, false)); +void _handleErrors(List errors) { + errors.where((element) => element != null).forEach((element) => onError(element!, null, false)); +} Future _checkGameServer() async { try { @@ -70,7 +74,7 @@ Future _checkGameServer() async { var oldOwner = matchmakerController.gameServerOwner.value; matchmakerController.joinLocalHost(); WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar( - "$oldOwner's server is no longer available", + oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner), severity: InfoBarSeverity.warning, duration: snackbarLongDuration )); @@ -105,7 +109,7 @@ void _joinServer(Uri uri) { matchmakerController.joinServer(hostingController.uuid, server); }else { showInfoBar( - "No server found: invalid or expired link", + translations.noServerFound, duration: snackbarLongDuration, severity: InfoBarSeverity.error ); @@ -114,34 +118,30 @@ void _joinServer(Uri uri) { String _parseCustomUrl(Uri uri) => uri.host; -Future _initWindow() async { - try { - await windowManager.ensureInitialized(); - await Window.initialize(); - var settingsController = Get.find(); - var size = Size(settingsController.width, settingsController.height); - appWindow.size = size; - var offsetX = settingsController.offsetX; - var offsetY = settingsController.offsetY; - if(offsetX != null && offsetY != null){ - appWindow.position = Offset( - offsetX, - offsetY - ); - }else { - appWindow.alignment = Alignment.center; - } +void _initWindow() => doWhenWindowReady(() async { + await windowManager.ensureInitialized(); + await Window.initialize(); + var settingsController = Get.find(); + var size = Size(settingsController.width, settingsController.height); + appWindow.size = size; + var offsetX = settingsController.offsetX; + var offsetY = settingsController.offsetY; + if(offsetX != null && offsetY != null){ + appWindow.position = Offset( + offsetX, + offsetY + ); + }else { + appWindow.alignment = Alignment.center; + } - await Window.setEffect( + await Window.setEffect( effect: WindowEffect.acrylic, color: Colors.transparent, - dark: true - ); - return null; - }catch(error) { - return error; - } -} + dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark + ); + appWindow.show(); +}); Object? _initObservers() { try { @@ -190,16 +190,23 @@ class RebootApplication extends StatefulWidget { } class _RebootApplicationState extends State { + final SettingsController _settingsController = Get.find(); + @override - Widget build(BuildContext context) => FluentApp( - title: "Reboot Launcher", - themeMode: ThemeMode.system, + Widget build(BuildContext context) => Obx(() => FluentApp( + locale: Locale(_settingsController.language.value), + localizationsDelegates: const [ + ...AppLocalizations.localizationsDelegates, + LocaleNamesLocalizationsDelegate() + ], + supportedLocales: AppLocalizations.supportedLocales, + themeMode: _settingsController.themeMode.value, debugShowCheckedModeBanner: false, color: SystemTheme.accentColor.accent.toAccentColor(), darkTheme: _createTheme(Brightness.dark), theme: _createTheme(Brightness.light), home: const HomePage() - ); + )); FluentThemeData _createTheme(Brightness brightness) => FluentThemeData( brightness: brightness, diff --git a/gui/lib/src/controller/authenticator_controller.dart b/gui/lib/src/controller/authenticator_controller.dart index 6a3f860..b007f85 100644 --- a/gui/lib/src/controller/authenticator_controller.dart +++ b/gui/lib/src/controller/authenticator_controller.dart @@ -1,11 +1,13 @@ import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class AuthenticatorController extends ServerController { AuthenticatorController() : super(); @override - String get controllerName => "authenticator"; + String get controllerName => translations.authenticatorName.toLowerCase(); @override String get storageName => "authenticator"; @@ -22,6 +24,9 @@ class AuthenticatorController extends ServerController { @override Future freePort() => freeAuthenticatorPort(); + @override + RebootPageType get pageType => RebootPageType.authenticator; + @override Future startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value); diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index e1e7c92..524c8f3 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -14,7 +14,6 @@ class GameController extends GetxController { late final Rx> versions; late final Rxn _selectedVersion; late final RxBool started; - late final RxBool autoStartGameServer; late final Rxn instance; GameController() { @@ -40,9 +39,6 @@ class GameController extends GetxController { customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text)); started = RxBool(false); - autoStartGameServer = RxBool(_storage.read("auto_game_server") ?? true); - autoStartGameServer.listen((value) => - _storage.write("auto_game_server", value)); var serializedInstance = _storage.read("instance"); instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null); instance.listen((_) => saveInstance()); @@ -56,7 +52,6 @@ class GameController extends GetxController { password.text = ""; customLaunchArgs.text = ""; versions.value = []; - autoStartGameServer.value = true; instance.value = null; } diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index f13b00a..786df95 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -4,12 +4,10 @@ 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/src/util/translations.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; -const String kDefaultServerName = "Reboot Game Server"; -const String kDefaultDescription = "Just another server"; - class HostingController extends GetxController { late final GetStorage _storage; late final String uuid; @@ -27,9 +25,9 @@ class HostingController extends GetxController { _storage = GetStorage("hosting"); uuid = _storage.read("uuid") ?? const Uuid().v4(); _storage.write("uuid", uuid); - name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName); + name = TextEditingController(text: _storage.read("name")); name.addListener(() => _storage.write("name", name.text)); - description = TextEditingController(text: _storage.read("description") ?? kDefaultDescription); + description = TextEditingController(text: _storage.read("description")); description.addListener(() => _storage.write("description", description.text)); password = TextEditingController(text: _storage.read("password") ?? ""); password.addListener(() => _storage.write("password", password.text)); @@ -46,25 +44,16 @@ class HostingController extends GetxController { supabase.from('hosts') .stream(primaryKey: ['id']) .map((event) => _parseValidServers(event)) - .listen((event) { - if(servers.value == null) { - servers.value = event; - }else { - servers.value?.addAll(event); - } - }); + .listen((event) => servers.value = event); } - Set> _parseValidServers(event) => event.where((element) => _isValidServer(element)).toSet(); - - bool _isValidServer(Map element) => - element["id"] != uuid && element["ip"] != null; + Set> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet(); Future saveInstance() => _storage.write("instance", jsonEncode(instance.value?.toJson())); void reset() { - name.text = kDefaultServerName; - description.text = kDefaultDescription; + name.text = ""; + description.text = ""; showPassword.value = false; discoverable.value = false; started.value = false; diff --git a/gui/lib/src/controller/matchmaker_controller.dart b/gui/lib/src/controller/matchmaker_controller.dart index 5fb233c..48716cd 100644 --- a/gui/lib/src/controller/matchmaker_controller.dart +++ b/gui/lib/src/controller/matchmaker_controller.dart @@ -2,6 +2,8 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class MatchmakerController extends ServerController { late final TextEditingController gameServerAddress; @@ -34,7 +36,7 @@ class MatchmakerController extends ServerController { } @override - String get controllerName => "matchmaker"; + String get controllerName => translations.matchmakerName.toLowerCase(); @override String get storageName => "matchmaker"; @@ -51,6 +53,9 @@ class MatchmakerController extends ServerController { @override Future freePort() => freeMatchmakerPort(); + @override + RebootPageType get pageType => RebootPageType.matchmaker; + @override Future startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value); diff --git a/gui/lib/src/controller/server_controller.dart b/gui/lib/src/controller/server_controller.dart index 666c0ba..b70a4bc 100644 --- a/gui/lib/src/controller/server_controller.dart +++ b/gui/lib/src/controller/server_controller.dart @@ -5,6 +5,7 @@ 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/src/page/abstract/page_type.dart'; import 'package:sync/semaphore.dart'; abstract class ServerController extends GetxController { @@ -15,6 +16,7 @@ abstract class ServerController extends GetxController { late final Semaphore semaphore; late RxBool started; late RxBool detached; + StreamSubscription? worker; int? embeddedServerPid; HttpServer? localServer; HttpServer? remoteServer; @@ -58,6 +60,8 @@ abstract class ServerController extends GetxController { Future get isPortTaken async => !(await isPortFree); + RebootPageType get pageType; + Future freePort(); @protected @@ -196,15 +200,6 @@ abstract class ServerController extends GetxController { } } - Stream restart() async* { - await resetWinNat(); - if(started()) { - yield* stop(); - } - - yield* start(); - } - Stream toggle() async* { if(started()) { yield* stop(); diff --git a/gui/lib/src/controller/settings_controller.dart b/gui/lib/src/controller/settings_controller.dart index 21ef8ab..f9278a4 100644 --- a/gui/lib/src/controller/settings_controller.dart +++ b/gui/lib/src/controller/settings_controller.dart @@ -1,6 +1,9 @@ +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:intl/intl.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; @@ -10,30 +13,38 @@ class SettingsController extends GetxController { late final TextEditingController gameServerDll; late final TextEditingController unrealEngineConsoleDll; late final TextEditingController authenticatorDll; + late final TextEditingController memoryLeakDll; late final TextEditingController gameServerPort; late final RxBool firstRun; + late final RxString language; + late final Rx themeMode; late double width; late double height; late double? offsetX; late double? offsetY; - late double scrollingDistance; SettingsController() { _storage = GetStorage("settings"); gameServerDll = _createController("game_server", "reboot.dll"); unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll"); authenticatorDll = _createController("authenticator", "cobalt.dll"); + memoryLeakDll = _createController("memory_leak", "memoryleak.dll"); gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort); gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text)); width = _storage.read("width") ?? kDefaultWindowWidth; height = _storage.read("height") ?? kDefaultWindowHeight; offsetX = _storage.read("offset_x"); offsetY = _storage.read("offset_y"); - scrollingDistance = 0.0; firstRun = RxBool(_storage.read("first_run") ?? true); firstRun.listen((value) => _storage.write("first_run", value)); + themeMode = Rx(ThemeMode.values.elementAt(_storage.read("theme") ?? 0)); + themeMode.listen((value) => _storage.write("theme", value.index)); + language = RxString(_storage.read("language") ?? _defaultLocale); + language.listen((value) => _storage.write("language", value)); } + String get _defaultLocale => Intl.getCurrentLocale().split("_")[0]; + TextEditingController _createController(String key, String name) { var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name)); controller.addListener(() => _storage.write(key, controller.text)); @@ -46,8 +57,10 @@ class SettingsController extends GetxController { } void saveWindowOffset(Offset position) { - _storage.write("offset_x", position.dx); - _storage.write("offset_y", position.dy); + offsetX = position.dx; + offsetY = position.dy; + _storage.write("offset_x", offsetX); + _storage.write("offset_y", offsetY); } void reset(){ diff --git a/gui/lib/src/controller/update_controller.dart b/gui/lib/src/controller/update_controller.dart index e32ced3..265759f 100644 --- a/gui/lib/src/controller/update_controller.dart +++ b/gui/lib/src/controller/update_controller.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class UpdateController { late final GetStorage _storage; @@ -30,7 +31,7 @@ class UpdateController { } showInfoBar( - "Downloading reboot dll...", + translations.updatingRebootDll, loading: true, duration: null ); @@ -43,7 +44,7 @@ class UpdateController { ); status.value = UpdateStatus.success; showInfoBar( - "The reboot dll was downloaded successfully", + translations.updatedRebootDll, severity: InfoBarSeverity.success, duration: snackbarShortDuration ); @@ -53,12 +54,12 @@ class UpdateController { error = error.toLowerCase(); status.value = UpdateStatus.error; showInfoBar( - "An error occurred while downloading the reboot dll: $error", + translations.updateRebootDllError(error.toString()), duration: snackbarLongDuration, severity: InfoBarSeverity.error, action: Button( onPressed: () => update(true), - child: const Text("Retry"), + child: Text(translations.updateRebootDllErrorAction), ) ); } diff --git a/gui/lib/src/dialog/abstract/dialog.dart b/gui/lib/src/dialog/abstract/dialog.dart index 0f683e8..ed99ce8 100644 --- a/gui/lib/src/dialog/abstract/dialog.dart +++ b/gui/lib/src/dialog/abstract/dialog.dart @@ -2,7 +2,8 @@ import 'package:clipboard/clipboard.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'dialog_button.dart'; @@ -92,17 +93,15 @@ class InfoDialog extends AbstractDialog { width: double.infinity, child: Text(text, textAlign: TextAlign.center) ), - buttons: buttons ?? [_createDefaultButton()], + buttons: buttons ?? [_defaultCloseButton], padding: const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 15.0) ); } - DialogButton _createDefaultButton() { - return DialogButton( - text: "Close", - type: ButtonType.only - ); - } + DialogButton get _defaultCloseButton =>DialogButton( + text: translations.defaultDialogSecondaryAction, + type: ButtonType.only + ); } class ProgressDialog extends AbstractDialog { @@ -124,7 +123,7 @@ class ProgressDialog extends AbstractDialog { ), buttons: [ DialogButton( - text: "Close", + text: translations.defaultDialogSecondaryAction, type: ButtonType.only, onTap: onStop ) @@ -211,7 +210,7 @@ class FutureBuilderDialog extends AbstractDialog { return DialogButton( text: snapshot.hasData || snapshot.hasError - || (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? "Close" : "Stop", + || (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? translations.defaultDialogSecondaryAction : translations.stopLoadingDialogAction, type: ButtonType.only, onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData) ); @@ -226,11 +225,11 @@ class ErrorDialog extends AbstractDialog { const ErrorDialog({Key? key, required this.exception, required this.errorMessageBuilder, this.stackTrace}) : super(key: key); static DialogButton createCopyErrorButton({required Object error, required StackTrace? stackTrace, required Function() onClick, ButtonType type = ButtonType.primary}) => DialogButton( - text: "Copy error", + text: translations.copyErrorDialogTitle, type: type, onTap: () async { - FlutterClipboard.controlC("An error occurred: $error\nStacktrace:\n $stackTrace"); - showInfoBar("Copied error to clipboard"); + FlutterClipboard.controlC("$error\n$stackTrace"); + showInfoBar(translations.copyErrorDialogSuccess); onClick(); }, ); diff --git a/gui/lib/src/dialog/abstract/dialog_button.dart b/gui/lib/src/dialog/abstract/dialog_button.dart index 8f6ae2a..0c42227 100644 --- a/gui/lib/src/dialog/abstract/dialog_button.dart +++ b/gui/lib/src/dialog/abstract/dialog_button.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class DialogButton extends StatefulWidget { final String? text; @@ -41,7 +42,7 @@ class _DialogButtonState extends State { Widget get _secondaryButton { return Button( onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, - child: Text(widget.text ?? "Close"), + child: Text(widget.text ?? translations.defaultDialogSecondaryAction), ); } diff --git a/gui/lib/src/dialog/abstract/info_bar.dart b/gui/lib/src/dialog/abstract/info_bar.dart index e2778ea..48158d2 100644 --- a/gui/lib/src/dialog/abstract/info_bar.dart +++ b/gui/lib/src/dialog/abstract/info_bar.dart @@ -1,15 +1,16 @@ import 'dart:collection'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; import 'package:sync/semaphore.dart'; Semaphore _semaphore = Semaphore(); HashMap _overlays = HashMap(); -void restoreMessage(int lastIndex) { - removeMessage(lastIndex); - var overlay = _overlays[pageIndex.value]; +void restoreMessage(int pageIndex, int lastIndex) { + removeMessageByPage(lastIndex); + var overlay = _overlays[pageIndex]; if(overlay == null) { return; } @@ -17,48 +18,62 @@ void restoreMessage(int lastIndex) { Overlay.of(pageKey.currentContext!).insert(overlay); } -void showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) { +OverlayEntry showInfoBar(dynamic text, + {RebootPageType? pageType, + InfoBarSeverity severity = InfoBarSeverity.info, + bool loading = false, + Duration? duration = snackbarShortDuration, + Widget? action}) { try { _semaphore.acquire(); - var index = pageIndex.value; - removeMessage(index); - var overlay = showSnackbar( - pageKey.currentContext!, - SizedBox( - width: double.infinity, - child: Mica( - child: InfoBar( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if(text is Widget) - text, - if(text is String) - Text(text), - if(action != null) - action - ], + var index = pageType?.index ?? pageIndex.value; + removeMessageByPage(index); + var overlay = OverlayEntry( + builder: (context) => Padding( + padding: EdgeInsets.only( + right: 12.0, + left: 12.0, + bottom: pagesWithButtonIndexes.contains(index) ? 72.0 : 16.0 + ), + child: Align( + alignment: AlignmentDirectional.bottomCenter, + child: Container( + width: double.infinity, + constraints: const BoxConstraints( + maxWidth: 1000 + ), + child: Mica( + child: InfoBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if(text is Widget) + text, + if(text is String) + Text(text), + if(action != null) + action + ], + ), + isLong: false, + isIconVisible: true, + content: SizedBox( + width: double.infinity, + child: loading ? const Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 2.0), + child: ProgressBar(), + ) : const SizedBox() + ), + severity: severity ), - isLong: false, - isIconVisible: true, - content: SizedBox( - width: double.infinity, - child: loading ? const Padding( - padding: EdgeInsets.only(top: 8.0, bottom: 2.0), - child: ProgressBar(), - ) : const SizedBox() - ), - severity: severity + ), ), ), - ), - margin: EdgeInsets.only( - right: 12.0, - left: 12.0, - bottom: index == 0 || index == 1 || index == 3 || index == 4 ? 72.0 : 16.0 - ), - duration: duration + ) ); + if(index == pageIndex.value) { + Overlay.of(pageKey.currentContext!).insert(overlay); + } _overlays[index] = overlay; if(duration != null) { Future.delayed(duration).then((_) { @@ -73,17 +88,24 @@ void showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, }); }); } + return overlay; }finally { _semaphore.release(); } } -void removeMessage(int index) { +void removeMessageByPage(int index) { + var lastOverlay = _overlays[index]; + if(lastOverlay != null) { + removeMessageByOverlay(lastOverlay); + _overlays[index] = null; + } +} + +void removeMessageByOverlay(OverlayEntry? overlay) { try { - var lastOverlay = _overlays[index]; - if(lastOverlay != null) { - lastOverlay.remove(); - _overlays[index] = null; + if(overlay != null) { + overlay.remove(); } }catch(_) { // Do not use .isMounted diff --git a/gui/lib/src/dialog/implementation/data.dart b/gui/lib/src/dialog/implementation/data.dart new file mode 100644 index 0000000..e4767bd --- /dev/null +++ b/gui/lib/src/dialog/implementation/data.dart @@ -0,0 +1,24 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; +import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; + +Future showResetDialog(Function() onConfirm) => showAppDialog( + builder: (context) => InfoDialog( + text: translations.resetDefaultsDialogTitle, + buttons: [ + DialogButton( + type: ButtonType.secondary, + text: translations.resetDefaultsDialogSecondaryAction, + ), + DialogButton( + type: ButtonType.primary, + text: translations.resetDefaultsDialogPrimaryAction, + onTap: () { + onConfirm(); + Navigator.of(context).pop(); + }, + ) + ], + ) +); \ No newline at end of file diff --git a/gui/lib/src/dialog/implementation/error.dart b/gui/lib/src/dialog/implementation/error.dart index 65ad8ec..358afcd 100644 --- a/gui/lib/src/dialog/implementation/error.dart +++ b/gui/lib/src/dialog/implementation/error.dart @@ -1,12 +1,14 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/page/home_page.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) { - if(exception == null){ +void onError(Object exception, StackTrace? stackTrace, bool framework) { + if(!kDebugMode) { return; } @@ -29,7 +31,7 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) { ErrorDialog( exception: exception, stackTrace: stackTrace, - errorMessageBuilder: (exception) => framework ? "An error was thrown by Flutter: $exception" : "An uncaught error was thrown: $exception" + errorMessageBuilder: (exception) => translations.uncaughtErrorMessage(exception.toString()) ) )); } \ No newline at end of file diff --git a/gui/lib/src/dialog/implementation/game.dart b/gui/lib/src/dialog/implementation/game.dart deleted file mode 100644 index d0852e1..0000000 --- a/gui/lib/src/dialog/implementation/game.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:reboot_common/common.dart'; - -import '../abstract/dialog.dart'; - -const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. " - "If you are unsure which version works best, use build 7.40. " - "If you are a passionate programmer you can add support by opening a PR on Github. "; - -const String _corruptedBuildError = "An unknown occurred while launching Fortnite. " - "Some critical files could be missing in your installation. " - "Download the build again from the launcher, not locally, or from a different source. " - "Alternatively, something could have gone wrong in the launcher. "; - -Future showMissingDllError(String name) async { - showAppDialog( - builder: (context) => InfoDialog( - text: "$name dll is not a valid dll, fix it in the settings tab" - ) - ); -} - -Future showTokenErrorFixable() async { - showAppDialog( - builder: (context) => const InfoDialog( - text: "A token error occurred. " - "The backend server has been automatically restarted to fix the issue. " - "The game has been restarted automatically. " - ) - ); -} - -Future showTokenErrorUnfixable() async { - showAppDialog( - builder: (context) => const InfoDialog( - text: "A token error occurred. " - "This issue cannot be resolved automatically as the server isn't embedded." - "Please restart the server manually, then relaunch your game to check if the issue has been fixed. " - "Otherwise, open an issue on Discord." - ) - ); -} - -Future showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async { - if(error == null) { - showAppDialog( - builder: (context) => InfoDialog( - text: server ? _unsupportedServerError : _corruptedBuildError - ) - ); - return; - } - - showAppDialog( - builder: (context) => ErrorDialog( - exception: error, - stackTrace: stackTrace, - errorMessageBuilder: (exception) => "${_corruptedBuildError}Error message: $exception" - ) - ); -} - -Future showMissingBuildError(FortniteVersion version) async { - showAppDialog( - builder: (context) => InfoDialog( - text: "${version.location.path} no longer contains a Fortnite executable. " - "This probably means that you deleted it or move it somewhere else." - ) - ); -} \ No newline at end of file diff --git a/gui/lib/src/dialog/implementation/profile.dart b/gui/lib/src/dialog/implementation/profile.dart index 6d90603..ebd81b8 100644 --- a/gui/lib/src/dialog/implementation/profile.dart +++ b/gui/lib/src/dialog/implementation/profile.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; final GameController _gameController = Get.find(); @@ -20,9 +21,9 @@ Future showProfileForm(BuildContext context) async{ crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoLabel( - label: "Username/Email", + label: translations.usernameOrEmail, child: TextFormBox( - placeholder: "Type your username or email", + placeholder: translations.usernameOrEmailPlaceholder, controller: _gameController.username, autovalidateMode: AutovalidateMode.always, enableSuggestions: true, @@ -32,9 +33,9 @@ Future showProfileForm(BuildContext context) async{ ), const SizedBox(height: 16.0), InfoLabel( - label: "Password", + label: translations.password, child: TextFormBox( - placeholder: "Type your password, if you have one", + placeholder: translations.passwordPlaceholder, controller: _gameController.password, autovalidateMode: AutovalidateMode.always, obscureText: !showPassword.value, @@ -59,16 +60,14 @@ Future showProfileForm(BuildContext context) async{ ), buttons: [ DialogButton( - text: "Cancel", + text: translations.cancelProfileChanges, type: ButtonType.secondary ), DialogButton( - text: "Save", + text: translations.saveProfileChanges, type: ButtonType.primary, - onTap: () { - Navigator.of(context).pop(true); - } + onTap: () => Navigator.of(context).pop(true) ) ] )) diff --git a/gui/lib/src/dialog/implementation/server.dart b/gui/lib/src/dialog/implementation/server.dart index 983124c..75fc680 100644 --- a/gui/lib/src/dialog/implementation/server.dart +++ b/gui/lib/src/dialog/implementation/server.dart @@ -12,29 +12,27 @@ import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/cryptography.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; extension ServerControllerDialog on ServerController { - Future restartInteractive() async { - var stream = restart(); - return await _handleStream(stream, false); - } - - Future toggleInteractive([bool showSuccessMessage = true]) async { + Future toggleInteractive(RebootPageType caller, [bool showSuccessMessage = true]) async { var stream = toggle(); - return await _handleStream(stream, showSuccessMessage); + return await _handleStream(caller, stream, showSuccessMessage); } - Future _handleStream(Stream stream, bool showSuccessMessage) async { + Future _handleStream(RebootPageType caller, Stream stream, bool showSuccessMessage) async { var completer = Completer(); - stream.listen((event) { + worker = stream.listen((event) { switch (event.type) { case ServerResultType.starting: showInfoBar( - "Starting the $controllerName...", + translations.startingServer(controllerName), + pageType: caller, severity: InfoBarSeverity.info, loading: true, duration: null @@ -43,7 +41,8 @@ extension ServerControllerDialog on ServerController { case ServerResultType.startSuccess: if(showSuccessMessage) { showInfoBar( - "The $controllerName was started successfully", + translations.startedServer(controllerName), + pageType: caller, severity: InfoBarSeverity.success ); } @@ -51,14 +50,17 @@ extension ServerControllerDialog on ServerController { break; case ServerResultType.startError: showInfoBar( - "An error occurred while starting the $controllerName: ${event.error ?? "unknown error"}", + translations.startServerError( + event.error ?? translations.unknownError, controllerName), + pageType: caller, severity: InfoBarSeverity.error, duration: snackbarLongDuration ); break; case ServerResultType.stopping: showInfoBar( - "Stopping the $controllerName...", + translations.stoppingServer, + pageType: caller, severity: InfoBarSeverity.info, loading: true, duration: null @@ -67,7 +69,8 @@ extension ServerControllerDialog on ServerController { case ServerResultType.stopSuccess: if(showSuccessMessage) { showInfoBar( - "The $controllerName was stopped successfully", + translations.stoppedServer(controllerName), + pageType: caller, severity: InfoBarSeverity.success ); } @@ -75,46 +78,54 @@ extension ServerControllerDialog on ServerController { break; case ServerResultType.stopError: showInfoBar( - "An error occurred while stopping the $controllerName: ${event.error ?? "unknown error"}", + translations.stopServerError( + event.error ?? translations.unknownError, controllerName), + pageType: caller, severity: InfoBarSeverity.error, duration: snackbarLongDuration ); break; case ServerResultType.missingHostError: showInfoBar( - "Missing hostname in $controllerName configuration", + translations.missingHostNameError(controllerName), + pageType: caller, severity: InfoBarSeverity.error ); break; case ServerResultType.missingPortError: showInfoBar( - "Missing port in $controllerName configuration", + translations.missingPortError(controllerName), + pageType: caller, severity: InfoBarSeverity.error ); break; case ServerResultType.illegalPortError: showInfoBar( - "Invalid port in $controllerName configuration", + translations.illegalPortError(controllerName), + pageType: caller, severity: InfoBarSeverity.error ); break; case ServerResultType.freeingPort: showInfoBar( - "Freeing port $defaultPort...", + translations.freeingPort(defaultPort), + pageType: caller, loading: true, duration: null ); break; case ServerResultType.freePortSuccess: showInfoBar( - "Port $defaultPort was freed successfully", + translations.freedPort(defaultPort), + pageType: caller, severity: InfoBarSeverity.success, duration: snackbarShortDuration ); break; case ServerResultType.freePortError: showInfoBar( - "An error occurred while freeing port $defaultPort: ${event.error ?? "unknown error"}", + translations.freePortError(event.error ?? translations.unknownError, controllerName), + pageType: caller, severity: InfoBarSeverity.error, duration: snackbarLongDuration ); @@ -122,7 +133,8 @@ extension ServerControllerDialog on ServerController { case ServerResultType.pingingRemote: if(started.value) { showInfoBar( - "Pinging the remote $controllerName...", + translations.pingingRemoteServer(controllerName), + pageType: caller, severity: InfoBarSeverity.info, loading: true, duration: null @@ -132,7 +144,8 @@ extension ServerControllerDialog on ServerController { case ServerResultType.pingingLocal: if(started.value) { showInfoBar( - "Pinging the ${type().name} $controllerName...", + translations.pingingLocalServer(controllerName, type().name), + pageType: caller, severity: InfoBarSeverity.info, loading: true, duration: null @@ -141,7 +154,8 @@ extension ServerControllerDialog on ServerController { break; case ServerResultType.pingError: showInfoBar( - "Cannot ping ${type().name} $controllerName", + translations.pingError(controllerName, type().name), + pageType: caller, severity: InfoBarSeverity.error ); break; @@ -175,7 +189,7 @@ extension MatchmakerControllerExtension on MatchmakerController { var id = entry["id"]; if(uuid == id) { showInfoBar( - "You can't join your own server", + translations.joinSelfServer, duration: snackbarLongDuration, severity: InfoBarSeverity.error ); @@ -204,7 +218,7 @@ extension MatchmakerControllerExtension on MatchmakerController { if(!checkPassword(confirmPassword, hashedPassword)) { showInfoBar( - "Wrong password: please try again", + translations.wrongServerPassword, duration: snackbarLongDuration, severity: InfoBarSeverity.error ); @@ -227,7 +241,7 @@ extension MatchmakerControllerExtension on MatchmakerController { } showInfoBar( - "This server isn't online right now: please try again later", + translations.offlineServer, duration: snackbarLongDuration, severity: InfoBarSeverity.error ); @@ -246,9 +260,9 @@ extension MatchmakerControllerExtension on MatchmakerController { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoLabel( - label: "Password", + label: translations.serverPassword, child: Obx(() => TextFormBox( - placeholder: "Type the server's password", + placeholder: translations.serverPasswordPlaceholder, controller: confirmPasswordController, autovalidateMode: AutovalidateMode.always, obscureText: !showPassword.value, @@ -274,12 +288,12 @@ extension MatchmakerControllerExtension on MatchmakerController { ), buttons: [ DialogButton( - text: "Cancel", + text: translations.serverPasswordCancel, type: ButtonType.secondary ), DialogButton( - text: "Confirm", + text: translations.serverPasswordConfirm, type: ButtonType.primary, onTap: () => Navigator.of(context).pop(confirmPasswordController.text) ) @@ -297,7 +311,7 @@ extension MatchmakerControllerExtension on MatchmakerController { FlutterClipboard.controlC(decryptedIp); } WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar( - embedded ? "You joined $author's server successfully!" : "Copied IP to the clipboard", + embedded ? translations.joinedServer(author) : translations.copiedIp, duration: snackbarLongDuration, severity: InfoBarSeverity.success )); diff --git a/gui/lib/src/page/abstract/page.dart b/gui/lib/src/page/abstract/page.dart new file mode 100644 index 0000000..d2be668 --- /dev/null +++ b/gui/lib/src/page/abstract/page.dart @@ -0,0 +1,78 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart' as messenger; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; + +abstract class RebootPage extends StatefulWidget { + const RebootPage({super.key}); + + String get name; + + String get iconAsset; + + RebootPageType get type; + + int get index => type.index; + + List get settings; + + bool get hasButton; + + @override + RebootPageState createState(); +} + +abstract class RebootPageState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + var buttonWidget = button; + if(buttonWidget == null) { + return _listView; + } + + return Column( + children: [ + Expanded( + child: _listView, + ), + const SizedBox( + height: 8.0, + ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1000 + ), + child: buttonWidget + ) + ], + ); + } + + OverlayEntry showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) => messenger.showInfoBar( + text, + pageType: widget.type, + severity: severity, + loading: loading, + duration: duration, + action: action + ); + + ListView get _listView => ListView.builder( + itemCount: settings.length * 2, + itemBuilder: (context, index) => index.isEven ? Align( + alignment: Alignment.center, + child: settings[index ~/ 2], + ) : const SizedBox(height: 8.0), + ); + + @override + bool get wantKeepAlive => true; + + List get settings; + + Widget? get button; +} + + + diff --git a/gui/lib/src/page/abstract/page_setting.dart b/gui/lib/src/page/abstract/page_setting.dart new file mode 100644 index 0000000..dce9420 --- /dev/null +++ b/gui/lib/src/page/abstract/page_setting.dart @@ -0,0 +1,26 @@ +class PageSetting { + final String name; + final String description; + final String? content; + final List? children; + final int pageIndex; + + PageSetting( + {required this.name, + required this.description, + this.content, + this.children, + this.pageIndex = -1}); + + PageSetting withPageIndex(int pageIndex) => this.pageIndex != -1 + ? this + : PageSetting( + name: name, + description: description, + content: content, + children: children, + pageIndex: pageIndex); + + @override + String toString() => "$name: $description"; +} diff --git a/gui/lib/src/page/abstract/page_type.dart b/gui/lib/src/page/abstract/page_type.dart new file mode 100644 index 0000000..d936404 --- /dev/null +++ b/gui/lib/src/page/abstract/page_type.dart @@ -0,0 +1,9 @@ +enum RebootPageType { + play, + host, + browser, + authenticator, + matchmaker, + info, + settings +} \ No newline at end of file diff --git a/gui/lib/src/page/authenticator_page.dart b/gui/lib/src/page/authenticator_page.dart deleted file mode 100644 index 1f827ca..0000000 --- a/gui/lib/src/page/authenticator_page.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; -import 'package:reboot_launcher/src/widget/server/start_button.dart'; -import 'package:reboot_launcher/src/widget/server/type_selector.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class AuthenticatorPage extends StatefulWidget { - const AuthenticatorPage({Key? key}) : super(key: key); - - @override - State createState() => _AuthenticatorPageState(); -} - -class _AuthenticatorPageState extends State with AutomaticKeepAliveClientMixin { - final AuthenticatorController _authenticatorController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Obx(() => Column( - children: [ - Expanded( - child: ListView( - children: [ - SettingTile( - title: "Authenticator configuration", - subtitle: "This section contains the authenticator's configuration", - content: const ServerTypeSelector( - authenticator: true - ), - expandedContent: [ - if(_authenticatorController.type.value == ServerType.remote) - SettingTile( - title: "Host", - subtitle: "The hostname of the authenticator", - isChild: true, - content: TextFormBox( - placeholder: "Host", - controller: _authenticatorController.host - ) - ), - if(_authenticatorController.type.value != ServerType.embedded) - SettingTile( - title: "Port", - subtitle: "The port of the authenticator", - isChild: true, - content: TextFormBox( - placeholder: "Port", - controller: _authenticatorController.port, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ] - ) - ), - if(_authenticatorController.type.value == ServerType.embedded) - SettingTile( - title: "Detached", - subtitle: "Whether the embedded authenticator should be started as a separate process, useful for debugging", - contentWidth: null, - isChild: true, - content: Obx(() => Row( - children: [ - Text( - _authenticatorController.detached.value ? "On" : "Off" - ), - const SizedBox( - width: 16.0 - ), - ToggleSwitch( - checked: _authenticatorController.detached(), - onChanged: (value) => _authenticatorController.detached.value = value - ), - ], - )) - ), - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Installation directory", - subtitle: "Opens the folder where the embedded authenticator is located", - content: Button( - onPressed: () => launchUrl(authenticatorDirectory.uri), - child: const Text("Show Files") - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Reset authenticator", - subtitle: "Resets the authenticator's settings to their default values", - content: Button( - onPressed: () => showAppDialog( - builder: (context) => InfoDialog( - text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", - buttons: [ - DialogButton( - type: ButtonType.secondary, - text: "Close", - ), - DialogButton( - type: ButtonType.primary, - text: "Reset", - onTap: () { - _authenticatorController.reset(); - Navigator.of(context).pop(); - }, - ) - ], - ) - ), - child: const Text("Reset"), - ) - ) - ] - ), - ), - const SizedBox( - height: 8.0, - ), - const ServerButton( - authenticator: true - ) - ], - )); - } - - bool get _isRemote => _authenticatorController.type.value == ServerType.remote; -} diff --git a/gui/lib/src/page/browse_page.dart b/gui/lib/src/page/browse_page.dart deleted file mode 100644 index 92e1531..0000000 --- a/gui/lib/src/page/browse_page.dart +++ /dev/null @@ -1,265 +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/hosting_controller.dart'; -import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; -import 'package:skeletons/skeletons.dart'; - -class BrowsePage extends StatefulWidget { - const BrowsePage({Key? key}) : super(key: key); - - @override - State createState() => _BrowsePageState(); -} - -class _BrowsePageState extends State with AutomaticKeepAliveClientMixin { - final HostingController _hostingController = Get.find(); - final MatchmakerController _matchmakerController = Get.find(); - final TextEditingController _filterController = TextEditingController(); - final StreamController _filterControllerStream = StreamController(); - - @override - Widget build(BuildContext context) { - super.build(context); - return FutureBuilder( - future: Future.delayed(const Duration(seconds: 1)), // Fake delay to show loading - builder: (context, futureSnapshot) => Obx(() { - var ready = futureSnapshot.connectionState == ConnectionState.done; - var data = _hostingController.servers - .value - ?.where((entry) => entry["discoverable"] ?? false) - .toSet(); - if(ready && data?.isEmpty == true) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "No servers are available right now", - style: FluentTheme.of(context).typography.titleLarge, - ), - Text( - "Host a server yourself or come back later", - style: FluentTheme.of(context).typography.body - ), - ], - ); - } - - return Column( - children: [ - _buildSearchBar(ready), - - const SizedBox( - height: 16, - ), - - Expanded( - child: StreamBuilder( - stream: _filterControllerStream.stream, - builder: (context, filterSnapshot) { - var items = _getItems(data, filterSnapshot.data, ready); - var itemsCount = items != null ? items.length * 2 : null; - if(itemsCount == 0) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "No results found", - style: FluentTheme.of(context).typography.titleLarge, - ), - Text( - "No server matches your query", - style: FluentTheme.of(context).typography.body - ), - ], - ); - } - - return ListView.builder( - itemCount: itemsCount, - itemBuilder: (context, index) { - if(index % 2 != 0) { - return const SizedBox( - height: 8.0 - ); - } - - var entry = _getItem(index ~/ 2, items); - if(!ready || entry == null) { - return const SettingTile( - content: SkeletonAvatar( - style: SkeletonAvatarStyle( - height: 32, - width: 64 - ), - ) - ); - } - - var hasPassword = entry["password"] != null; - return SettingTile( - title: "${_formatName(entry)} • ${entry["author"]}", - subtitle: "${_formatDescription(entry)} • ${_formatVersion(entry)}", - content: Button( - onPressed: () => _matchmakerController.joinServer(_hostingController.uuid, entry), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if(hasPassword) - const Icon(FluentIcons.lock), - if(hasPassword) - const SizedBox(width: 8.0), - Text(_matchmakerController.type.value == ServerType.embedded ? "Join Server" : "Copy IP"), - ], - ), - ) - ); - } - ); - } - ), - ) - ], - ); - } - ), - ); - } - - Set>? _getItems(Set>? data, String? filter, bool ready) { - if (!ready) { - return null; - } - - if (data == null) { - return null; - } - - return data.where((entry) => _isValidItem(entry, filter)).toSet(); - } - - bool _isValidItem(Map entry, String? filter) => - filter == null || _filterServer(entry, filter); - - bool _filterServer(Map element, String filter) { - String? id = element["id"]; - if(id?.toLowerCase().contains(filter) == true) { - return true; - } - - var uri = Uri.tryParse(filter); - if(uri != null && id?.toLowerCase().contains(uri.host.toLowerCase()) == true) { - return true; - } - - String? name = element["name"]; - if(name?.toLowerCase().contains(filter) == true) { - return true; - } - - String? author = element["author"]; - if(author?.toLowerCase().contains(filter) == true) { - return true; - } - - String? description = element["description"]; - if(description?.toLowerCase().contains(filter) == true) { - return true; - } - - return false; - } - - Widget _buildSearchBar(bool ready) { - if(ready) { - return TextBox( - placeholder: 'Find a server', - controller: _filterController, - onChanged: (value) => _filterControllerStream.add(value), - suffix: _searchBarIcon, - ); - } - - return const SkeletonLine( - style: SkeletonLineStyle( - height: 32 - ) - ); - } - - Widget get _searchBarIcon => Button( - onPressed: _filterController.text.isEmpty ? null : () { - _filterController.clear(); - _filterControllerStream.add(""); - }, - style: ButtonStyle( - backgroundColor: _filterController.text.isNotEmpty ? null : ButtonState.all(Colors.transparent), - border: _filterController.text.isNotEmpty ? null : ButtonState.all(const BorderSide(color: Colors.transparent)) - ), - child: _searchBarIconData - ); - - Widget get _searchBarIconData { - var 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 - ), - ); - } - - Map? _getItem(int index, Set? data) { - if(data == null) { - return null; - } - - if (index >= data.length) { - return null; - } - - return data.elementAt(index); - } - - String _formatName(Map entry) { - String result = entry['name']; - return result.isEmpty ? kDefaultServerName : result; - } - - String _formatDescription(Map entry) { - String result = entry['description']; - return result.isEmpty ? kDefaultDescription : result; - } - - String _formatVersion(Map entry) { - var version = entry['version']; - var versionSplit = version.indexOf("-"); - var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version; - String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion; - if(result.toLowerCase().startsWith("fortnite ")) { - result = result.substring(0, 10); - } - - return "Fortnite $result"; - } - - @override - bool get wantKeepAlive => true; -} diff --git a/gui/lib/src/page/hosting_page.dart b/gui/lib/src/page/hosting_page.dart deleted file mode 100644 index 5783924..0000000 --- a/gui/lib/src/page/hosting_page.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'package:clipboard/clipboard.dart'; -import 'package:dart_ipify/dart_ipify.dart'; -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/material.dart' show Icons; -import 'package:get/get.dart'; -import 'package:reboot_launcher/main.dart'; -import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; -import 'package:reboot_launcher/src/widget/game/start_button.dart'; -import 'package:reboot_launcher/src/widget/version/version_selector.dart'; -import 'package:sync/semaphore.dart'; - -class HostingPage extends StatefulWidget { - const HostingPage({Key? key}) : super(key: key); - - @override - State createState() => _HostingPageState(); -} - -class _HostingPageState extends State with AutomaticKeepAliveClientMixin { - final GameController _gameController = Get.find(); - final HostingController _hostingController = Get.find(); - final Semaphore _semaphore = Semaphore(); - late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Column( - children: [ - Expanded( - child: ListView( - children: [ - SettingTile( - title: "Game Server", - subtitle: "Provide basic information about your server", - expandedContent: [ - SettingTile( - title: "Name", - subtitle: "The name of your game server", - isChild: true, - content: TextFormBox( - placeholder: "Name", - controller: _hostingController.name, - onChanged: (_) => _updateServer() - ) - ), - SettingTile( - title: "Description", - subtitle: "The description of your game server", - isChild: true, - content: TextFormBox( - placeholder: "Description", - controller: _hostingController.description, - onChanged: (_) => _updateServer() - ) - ), - SettingTile( - title: "Password", - subtitle: "The password of your game server for the server browser", - isChild: true, - content: Obx(() => TextFormBox( - placeholder: "Password", - controller: _hostingController.password, - autovalidateMode: AutovalidateMode.always, - obscureText: !_hostingController.showPassword.value, - enableSuggestions: false, - autocorrect: false, - onChanged: (text) { - _showPasswordTrailing.value = text.isNotEmpty; - _updateServer(); - }, - suffix: Button( - onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value, - style: ButtonStyle( - shape: ButtonState.all(const CircleBorder()), - backgroundColor: ButtonState.all(Colors.transparent) - ), - child: Icon( - _hostingController.showPassword.value ? Icons.visibility_off : Icons.visibility, - color: _showPasswordTrailing.value ? null : Colors.transparent - ), - ) - )) - ), - SettingTile( - title: "Discoverable", - subtitle: "Make your server available to other players on the server browser", - isChild: true, - contentWidth: null, - content: Obx(() => Row( - children: [ - Text( - _hostingController.discoverable.value ? "On" : "Off" - ), - const SizedBox( - width: 16.0 - ), - ToggleSwitch( - checked: _hostingController.discoverable(), - onChanged: (value) async { - _hostingController.discoverable.value = value; - await _updateServer(); - } - ), - ], - )) - ) - ], - ), - const SizedBox( - height: 8.0, - ), - const SettingTile( - title: "Version", - subtitle: "Select the version of Fortnite you want to host", - content: VersionSelector(), - expandedContent: [ - SettingTile( - title: "Add a version from this PC's local storage", - subtitle: "Versions coming from your local disk are not guaranteed to work", - content: Button( - onPressed: VersionSelector.openAddDialog, - child: Text("Add build"), - ), - isChild: true - ), - SettingTile( - title: "Download any version from the cloud", - subtitle: "Download any Fortnite build easily from the cloud", - content: Button( - onPressed: VersionSelector.openDownloadDialog, - child: Text("Download"), - ), - isChild: true - ) - ] - ), - const SizedBox( - height: 8.0 - ), - SettingTile( - title: "Share", - subtitle: "Make it easy for other people to join your server with the options in this section", - expandedContent: [ - SettingTile( - title: "Link", - subtitle: "Copies a link for your server to the clipboard (requires the Reboot Launcher)", - isChild: true, - content: Button( - onPressed: () async { - FlutterClipboard.controlC("$kCustomUrlSchema://${_hostingController.uuid}"); - showInfoBar( - "Copied your link to the clipboard", - severity: InfoBarSeverity.success - ); - }, - child: const Text("Copy Link"), - ) - ), - SettingTile( - title: "Public IP", - subtitle: "Copies your current public IP to the clipboard (doesn't require the Reboot Launcher)", - isChild: true, - content: Button( - onPressed: () async { - try { - showInfoBar( - "Obtaining your public IP...", - loading: true, - duration: null - ); - var ip = await Ipify.ipv4(); - FlutterClipboard.controlC(ip); - showInfoBar( - "Copied your IP to the clipboard", - severity: InfoBarSeverity.success - ); - }catch(error) { - showInfoBar( - "An error occurred while obtaining your public IP: $error", - severity: InfoBarSeverity.error, - duration: snackbarLongDuration - ); - } - }, - child: const Text("Copy IP"), - ) - ) - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Reset game server", - subtitle: "Resets the game server's settings to their default values", - content: Button( - onPressed: () => showAppDialog( - builder: (context) => InfoDialog( - text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", - buttons: [ - DialogButton( - type: ButtonType.secondary, - text: "Close", - ), - DialogButton( - type: ButtonType.primary, - text: "Reset", - onTap: () { - _hostingController.reset(); - Navigator.of(context).pop(); - }, - ) - ], - ) - ), - child: const Text("Reset"), - ) - ) - ], - ), - ), - const SizedBox( - height: 8.0, - ), - const LaunchButton( - host: true - ) - ], - ); - } - - Future _updateServer() async { - if(!_hostingController.published()) { - return; - } - - try { - _semaphore.acquire(); - _hostingController.publishServer( - _gameController.username.text, - _hostingController.instance.value!.versionName - ); - } catch(error) { - showInfoBar( - "An error occurred while updating the game server: $error", - severity: InfoBarSeverity.success, - duration: snackbarLongDuration - ); - } finally { - _semaphore.release(); - } - } -} \ No newline at end of file diff --git a/gui/lib/src/page/implementation/authenticator_page.dart b/gui/lib/src/page/implementation/authenticator_page.dart new file mode 100644 index 0000000..4a075b2 --- /dev/null +++ b/gui/lib/src/page/implementation/authenticator_page.dart @@ -0,0 +1,155 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:reboot_launcher/src/widget/server/start_button.dart'; +import 'package:reboot_launcher/src/widget/server/type_selector.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../dialog/implementation/data.dart'; + +class AuthenticatorPage extends RebootPage { + const AuthenticatorPage({Key? key}) : super(key: key); + + @override + String get name => translations.authenticatorName; + + @override + String get iconAsset => "assets/images/authenticator.png"; + + @override + RebootPageType get type => RebootPageType.authenticator; + + @override + bool get hasButton => true; + + @override + List get settings => [ + PageSetting( + name: translations.authenticatorConfigurationName, + description: translations.authenticatorConfigurationDescription, + children: [ + PageSetting( + name: translations.authenticatorConfigurationHostName, + description: translations.authenticatorConfigurationHostDescription + ), + PageSetting( + name: translations.authenticatorConfigurationPortName, + description: translations.authenticatorConfigurationPortDescription + ), + PageSetting( + name: translations.authenticatorConfigurationDetachedName, + description: translations.authenticatorConfigurationDetachedDescription + ) + ] + ), + PageSetting( + name: translations.authenticatorInstallationDirectoryName, + description: translations.authenticatorInstallationDirectoryDescription, + content: translations.authenticatorInstallationDirectoryContent + ), + PageSetting( + name: translations.authenticatorResetDefaultsName, + description: translations.authenticatorResetDefaultsDescription, + content: translations.authenticatorResetDefaultsContent + ) + ]; + + @override + RebootPageState createState() => _AuthenticatorPageState(); +} + +class _AuthenticatorPageState extends RebootPageState { + final AuthenticatorController _authenticatorController = Get.find(); + + @override + List get settings => [ + _configuration, + _installationDirectory, + _resetDefaults + ]; + + @override + Widget get button => const ServerButton( + authenticator: true + ); + + SettingTile get _resetDefaults => SettingTile( + title: translations.authenticatorResetDefaultsName, + subtitle: translations.authenticatorResetDefaultsDescription, + content: Button( + onPressed: () => showResetDialog(_authenticatorController.reset), + child: Text(translations.authenticatorResetDefaultsContent), + ) + ); + + SettingTile get _installationDirectory => SettingTile( + title: translations.authenticatorInstallationDirectoryName, + subtitle: translations.authenticatorInstallationDirectoryDescription, + content: Button( + onPressed: () => launchUrl(authenticatorDirectory.uri), + child: Text(translations.authenticatorInstallationDirectoryContent) + ) + ); + + Widget get _configuration => Obx(() => SettingTile( + title: translations.authenticatorConfigurationName, + subtitle: translations.authenticatorConfigurationDescription, + content: const ServerTypeSelector( + authenticator: true + ), + expandedContent: [ + if(_authenticatorController.type.value == ServerType.remote) + SettingTile( + title: translations.authenticatorConfigurationHostName, + subtitle: translations.authenticatorConfigurationHostDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.authenticatorConfigurationHostName, + controller: _authenticatorController.host + ) + ), + if(_authenticatorController.type.value != ServerType.embedded) + SettingTile( + title: translations.authenticatorConfigurationPortName, + subtitle: translations.authenticatorConfigurationPortDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.authenticatorConfigurationPortName, + controller: _authenticatorController.port, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ] + ) + ), + if(_authenticatorController.type.value == ServerType.embedded) + SettingTile( + title: translations.authenticatorConfigurationDetachedName, + subtitle: translations.authenticatorConfigurationDetachedDescription, + contentWidth: null, + isChild: true, + content: Obx(() => Row( + children: [ + Text( + _authenticatorController.detached.value ? translations.on : translations.off + ), + const SizedBox( + width: 16.0 + ), + ToggleSwitch( + checked: _authenticatorController.detached(), + onChanged: (value) => _authenticatorController.detached.value = value + ), + ], + )) + ) + ], + )); +} diff --git a/gui/lib/src/page/home_page.dart b/gui/lib/src/page/implementation/home_page.dart similarity index 52% rename from gui/lib/src/page/home_page.dart rename to gui/lib/src/page/implementation/home_page.dart index e4e1e63..fc8d9f0 100644 --- a/gui/lib/src/page/home_page.dart +++ b/gui/lib/src/page/implementation/home_page.dart @@ -1,29 +1,21 @@ import 'dart:collection'; +import 'dart:ui'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show MaterialPage; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/page/authenticator_page.dart'; -import 'package:reboot_launcher/src/page/browse_page.dart'; -import 'package:reboot_launcher/src/page/hosting_page.dart'; -import 'package:reboot_launcher/src/page/info_page.dart'; -import 'package:reboot_launcher/src/page/matchmaker_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/widget/home/pane.dart'; +import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; import 'package:reboot_launcher/src/widget/home/profile.dart'; import 'package:reboot_launcher/src/widget/os/title_bar.dart'; import 'package:window_manager/window_manager.dart'; -GlobalKey appKey = GlobalKey(); -const int pagesLength = 7; -final RxInt pageIndex = RxInt(0); -final Queue _pagesStack = Queue(); -final List _pageKeys = List.generate(pagesLength, (index) => GlobalKey()); -GlobalKey get pageKey => _pageKeys[pageIndex.value]; - class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -39,6 +31,8 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA final FocusNode _searchFocusNode = FocusNode(); final TextEditingController _searchController = TextEditingController(); final RxBool _focused = RxBool(true); + final Queue _pagesStack = Queue(); + bool _hitBack = false; @override bool get wantKeepAlive => true; @@ -46,21 +40,26 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void initState() { windowManager.addListener(this); - _searchController.addListener(_onSearch); var lastValue = pageIndex.value; pageIndex.listen((value) { - if(value != lastValue) { - _pagesStack.add(lastValue); - lastValue = value; + if(_hitBack) { + _hitBack = false; + return; } + + if(value == lastValue) { + return; + } + + _pagesStack.add(lastValue); + WidgetsBinding.instance.addPostFrameCallback((_) { + restoreMessage(value, lastValue); + lastValue = value; + }); }); super.initState(); } - void _onSearch() { - // TODO: Implement - } - @override void dispose() { _searchFocusNode.dispose(); @@ -82,18 +81,32 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void onWindowResized() { _settingsController.saveWindowSize(appWindow.size); + _focused.value = true; } @override void onWindowMoved() { _settingsController.saveWindowOffset(appWindow.position); + _focused.value = true; + } + + @override + void onWindowEnterFullScreen() { + _focused.value = true; + } + + @override + void onWindowLeaveFullScreen() { + _focused.value = true; } @override Widget build(BuildContext context) { super.build(context); - windowManager.show(); - return Obx(() => NavigationPaneTheme( + return Obx(() { + _settingsController.language.value; + loadTranslations(context); + return NavigationPaneTheme( data: NavigationPaneThemeData( backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), ), @@ -128,13 +141,18 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA items: _items, header: const ProfileWidget(), autoSuggestBox: _autoSuggestBox, - autoSuggestBoxReplacement: const Icon(FluentIcons.search), + indicator: const StickyNavigationIndicator( + duration: Duration(milliseconds: 500), + curve: Curves.easeOut, + indicatorSize: 3.25 + ) ), contentShape: const RoundedRectangleBorder(), onOpenSearch: () => _searchFocusNode.requestFocus(), transitionBuilder: (child, animation) => child ) - )); + ); + }); } Widget get _backButton => Obx(() { @@ -145,7 +163,10 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA backgroundColor: ButtonState.all(Colors.transparent), border: ButtonState.all(const BorderSide(color: Colors.transparent)) ), - onPressed: _pagesStack.isEmpty ? null : () => pageIndex.value = _pagesStack.removeLast(), + onPressed: _pagesStack.isEmpty ? null : () { + _hitBack = true; + pageIndex.value = _pagesStack.removeLast(); + }, child: const Icon(FluentIcons.back, size: 12.0), ); }); @@ -157,89 +178,89 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA ); Widget get _autoSuggestBox => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: TextBox( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: AutoSuggestBox( key: _searchKey, controller: _searchController, - placeholder: 'Find a setting', + placeholder: translations.find, focusNode: _searchFocusNode, - autofocus: true, - suffix: Button( - onPressed: null, - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.transparent), - border: ButtonState.all(const BorderSide(color: Colors.transparent)) + selectionHeightStyle: BoxHeightStyle.max, + itemBuilder: (context, item) => Wrap( + children: [ + ListTile( + onPressed: () { + pageIndex.value = item.value.pageIndex; + _searchController.clear(); + _searchFocusNode.unfocus(); + }, + leading: item.child, + title: Text( + item.value.name, + overflow: TextOverflow.clip, + maxLines: 1 + ), + subtitle: item.value.description.isNotEmpty ? Text( + item.value.description, + overflow: TextOverflow.clip, + maxLines: 1 + ) : null ), - child: Transform.flip( - flipX: true, - child: Icon( - FluentIcons.search, - size: 12.0, - color: FluentTheme.of(context).resources.textFillColorPrimary + ], + ), + items: _suggestedItems, + autofocus: true, + trailingIcon: IgnorePointer( + child: IconButton( + onPressed: () {}, + icon: Transform.flip( + flipX: true, + child: const Icon(FluentIcons.search) ), ) - ) - ), + ), + ) ); - List get _items => [ - RebootPaneItem( - title: const Text("Play"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/play.png") + List> get _suggestedItems => pages.mapMany((page) { + var icon = SizedBox.square( + dimension: 24, + child: Image.asset(page.iconAsset) + ); + var outerResults = >[]; + outerResults.add(AutoSuggestBoxItem( + value: PageSetting( + name: page.name, + description: "", + pageIndex: page.index ), - body: const PlayPage() - ), - RebootPaneItem( - title: const Text("Host"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/host.png") - ), - body: const HostingPage() - ), - RebootPaneItem( - title: const Text("Server Browser"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/browse.png") - ), - body: const BrowsePage() - ), - RebootPaneItem( - title: const Text("Authenticator"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/auth.png") - ), - body: const AuthenticatorPage() - ), - RebootPaneItem( - title: const Text("Matchmaker"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/matchmaker.png") - ), - body: const MatchmakerPage() - ), - RebootPaneItem( - title: const Text("Info"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/info.png") - ), - body: const InfoPage() - ), - RebootPaneItem( - title: const Text("Settings"), - icon: SizedBox.square( - dimension: 24, - child: Image.asset("assets/images/settings.png") - ), - body: const SettingsPage() - ), - ]; + label: page.name, + child: icon + )); + outerResults.addAll(page.settings.mapMany((setting) { + var results = >[]; + results.add(AutoSuggestBoxItem( + value: setting.withPageIndex(page.index), + label: setting.toString(), + child: icon + )); + setting.children?.forEach((childSetting) => results.add(AutoSuggestBoxItem( + value: childSetting.withPageIndex(page.index), + label: childSetting.toString(), + child: icon + ))); + return results; + }).toList()); + return outerResults; + }).toList(); - String get searchValue => _searchController.text; -} + List get _items => pages.map((page) => _createItem(page)).toList(); + + NavigationPaneItem _createItem(RebootPage page) => PaneItem( + title: Text(page.name), + icon: SizedBox.square( + dimension: 24, + child: Image.asset(page.iconAsset) + ), + body: page + ); +} \ No newline at end of file diff --git a/gui/lib/src/page/implementation/info_page.dart b/gui/lib/src/page/implementation/info_page.dart new file mode 100644 index 0000000..361c862 --- /dev/null +++ b/gui/lib/src/page/implementation/info_page.dart @@ -0,0 +1,130 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/gestures.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/util/tutorial.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; + +class InfoPage extends RebootPage { + const InfoPage({Key? key}) : super(key: key); + + @override + RebootPageState createState() => _InfoPageState(); + + @override + String get name => translations.infoName; + + @override + String get iconAsset => "assets/images/info.png"; + + @override + bool get hasButton => false; + + @override + RebootPageType get type => RebootPageType.info; + + @override + List get settings => []; +} + +class _InfoPageState extends RebootPageState { + @override + Widget? get button => null; + + @override + List get settings => [ + SettingTile( + title: 'What is Project Reboot?', + subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. ' + 'The project was started on Discord by Milxnor. ' + 'The project is no longer being actively maintained.', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + SettingTile( + title: 'What is a game server?', + subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. ' + 'You can join someone else\'s game server, or host one on your PC by going to the "Host" tab. ', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + SettingTile( + title: 'What is a client?', + subtitle: 'A client is the actual Fortnite game. ' + 'You can download any version of Fortnite from the launcher in the "Play" tab. ' + 'You can also import versions from your local PC, but remember that these may be corrupted. ' + 'If a local version doesn\'t work, try installing it from the launcher before reporting a bug.', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + SettingTile( + title: 'What is an authenticator?', + subtitle: 'An authenticator is a program that handles authentication, parties and voice chats. ' + 'By default, a LawinV1 server will be started for you to play. ' + 'You can use also use an authenticator running locally(on your PC) or remotely(on another PC). ' + 'Changing the authenticator settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! ' + 'If you need to restore these settings, go to the "Settings" tab and click on "Restore Defaults". ', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + SettingTile( + title: 'Do I need to update DLLs?', + subtitle: 'No, all the files that the launcher uses are automatically updated. ' + 'You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. ' + 'Unless you are an advanced user, changing these options is not recommended', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + SettingTile( + title: 'Where can I report bugs or ask for new features?', + subtitle: 'Go to the "Settings" tab and click on report bug. ' + 'Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement', + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ), + SettingTile( + title: 'How can I make my game server accessible for other players?', + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Follow ', + style: FluentTheme.of(context).typography.body + ), + TextSpan( + text: 'this tutorial', + mouseCursor: SystemMouseCursors.click, + style: FluentTheme.of(context).typography.body?.copyWith(color: FluentTheme.of(context).accentColor), + recognizer: TapGestureRecognizer()..onTap = openPortTutorial + ) + ] + ) + ), + titleStyle: FluentTheme + .of(context) + .typography + .title, + contentWidth: null, + ) + ]; +} \ No newline at end of file diff --git a/gui/lib/src/page/implementation/matchmaker_page.dart b/gui/lib/src/page/implementation/matchmaker_page.dart new file mode 100644 index 0000000..4a256ae --- /dev/null +++ b/gui/lib/src/page/implementation/matchmaker_page.dart @@ -0,0 +1,165 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/dialog/implementation/data.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:reboot_launcher/src/widget/server/start_button.dart'; +import 'package:reboot_launcher/src/widget/server/type_selector.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MatchmakerPage extends RebootPage { + const MatchmakerPage({Key? key}) : super(key: key); + + @override + RebootPageState createState() => _MatchmakerPageState(); + + @override + String get name => translations.matchmakerName; + + @override + String get iconAsset => "assets/images/matchmaker.png"; + + @override + bool get hasButton => true; + + @override + RebootPageType get type => RebootPageType.matchmaker; + + @override + List get settings => [ + PageSetting( + name: translations.matchmakerConfigurationName, + description: translations.matchmakerConfigurationDescription, + children: [ + PageSetting( + name: translations.matchmakerConfigurationHostName, + description: translations.matchmakerConfigurationHostDescription + ), + PageSetting( + name: translations.matchmakerConfigurationPortName, + description: translations.matchmakerConfigurationPortDescription + ), + PageSetting( + name: translations.matchmakerConfigurationDetachedName, + description: translations.matchmakerConfigurationDetachedDescription + ) + ] + ), + PageSetting( + name: translations.matchmakerInstallationDirectoryName, + description: translations.matchmakerInstallationDirectoryDescription, + content: translations.matchmakerInstallationDirectoryContent + ), + PageSetting( + name: translations.matchmakerResetDefaultsName, + description: translations.matchmakerResetDefaultsDescription, + content: translations.matchmakerResetDefaultsContent + ) + ]; +} + +class _MatchmakerPageState extends RebootPageState { + final MatchmakerController _matchmakerController = Get.find(); + + @override + Widget? get button => const ServerButton( + authenticator: false + ); + + @override + List get settings => [ + _configuration, + _installationDirectory, + _resetDefaults + ]; + + Widget get _configuration => Obx(() => SettingTile( + title: translations.matchmakerConfigurationName, + subtitle: translations.matchmakerConfigurationDescription, + content: const ServerTypeSelector( + authenticator: false + ), + expandedContent: [ + if(_matchmakerController.type.value == ServerType.remote) + SettingTile( + title: translations.matchmakerConfigurationHostName, + subtitle: translations.matchmakerConfigurationHostDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.matchmakerConfigurationHostName, + controller: _matchmakerController.host + ) + ), + if(_matchmakerController.type.value != ServerType.embedded) + SettingTile( + title: translations.matchmakerConfigurationPortName, + subtitle: translations.matchmakerConfigurationPortDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.matchmakerConfigurationPortName, + controller: _matchmakerController.port, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ] + ) + ), + if(_matchmakerController.type.value == ServerType.embedded) + SettingTile( + title: translations.matchmakerConfigurationAddressName, + subtitle: translations.matchmakerConfigurationAddressDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.matchmakerConfigurationAddressName, + controller: _matchmakerController.gameServerAddress, + focusNode: _matchmakerController.gameServerAddressFocusNode + ) + ), + if(_matchmakerController.type.value == ServerType.embedded) + SettingTile( + title: translations.matchmakerConfigurationDetachedName, + subtitle: translations.matchmakerConfigurationDetachedDescription, + contentWidth: null, + isChild: true, + content: Obx(() => Row( + children: [ + Text( + _matchmakerController.detached.value ? translations.on : translations.off + ), + const SizedBox( + width: 16.0 + ), + ToggleSwitch( + checked: _matchmakerController.detached.value, + onChanged: (value) => _matchmakerController.detached.value = value + ), + ], + )), + ) + ] + )); + + SettingTile get _installationDirectory => SettingTile( + title: translations.matchmakerInstallationDirectoryName, + subtitle: translations.matchmakerInstallationDirectoryDescription, + content: Button( + onPressed: () => launchUrl(matchmakerDirectory.uri), + child: Text(translations.matchmakerInstallationDirectoryContent) + ) + ); + + SettingTile get _resetDefaults => SettingTile( + title: translations.matchmakerResetDefaultsName, + subtitle: translations.matchmakerResetDefaultsDescription, + content: Button( + onPressed: () => showResetDialog(_matchmakerController.reset), + child: Text(translations.matchmakerResetDefaultsContent), + ) + ); +} diff --git a/gui/lib/src/page/implementation/play_page.dart b/gui/lib/src/page/implementation/play_page.dart new file mode 100644 index 0000000..2e9d623 --- /dev/null +++ b/gui/lib/src/page/implementation/play_page.dart @@ -0,0 +1,143 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:reboot_launcher/src/widget/game/start_button.dart'; +import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart'; + + +class PlayPage extends RebootPage { + const PlayPage({Key? key}) : super(key: key); + + @override + RebootPageState createState() => _PlayPageState(); + + @override + bool get hasButton => true; + + @override + String get name => translations.playName; + + @override + String get iconAsset => "assets/images/play.png"; + + @override + RebootPageType get type => RebootPageType.play; + + @override + List get settings => [ + versionSelectorRebootSetting, + PageSetting( + name: translations.playGameServerName, + description: translations.playGameServerDescription, + content: translations.playGameServerContentLocal, + children: [ + PageSetting( + name: translations.playGameServerHostName, + description: translations.playGameServerHostDescription, + content: translations.playGameServerHostName + ), + PageSetting( + name: translations.playGameServerBrowserName, + description: translations.playGameServerBrowserDescription, + content: translations.playGameServerBrowserName + ), + PageSetting( + name: translations.playGameServerCustomName, + description: translations.playGameServerCustomDescription, + content: translations.playGameServerCustomContent + ) + ] + ) + ]; +} + +class _PlayPageState extends RebootPageState { + final MatchmakerController _matchmakerController = Get.find(); + final HostingController _hostingController = Get.find(); + late final RxBool _selfServer; + + @override + void initState() { + _selfServer = RxBool(_isLocalPlay); + _matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay); + _hostingController.started.listen((_) => _selfServer.value = _isLocalPlay); + super.initState(); + } + + bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text) + && !_hostingController.started.value; + + @override + Widget? get button => LaunchButton( + startLabel: translations.launchFortnite, + stopLabel: translations.closeFortnite, + host: false + ); + + @override + List get settings => [ + versionSelectorSettingTile, + _gameServerSelector + ]; + + SettingTile get _gameServerSelector => SettingTile( + title: translations.playGameServerName, + subtitle: translations.playGameServerDescription, + content: IgnorePointer( + child: Button( + style: ButtonStyle( + backgroundColor: ButtonState.all(FluentTheme.of(context).resources.controlFillColorDefault) + ), + onPressed: () {}, + child: Obx(() { + var address = _matchmakerController.gameServerAddress.text; + var owner = _matchmakerController.gameServerOwner.value; + return Text( + isLocalHost(address) ? translations.playGameServerContentLocal : owner != null ? translations.playGameServerContentBrowser(owner) : address, + textAlign: TextAlign.start + ); + }) + ), + ), + expandedContent: [ + SettingTile( + title: translations.playGameServerHostName, + subtitle: translations.playGameServerHostDescription, + content: Button( + onPressed: () => pageIndex.value = RebootPageType.host.index, + child: Text(translations.playGameServerHostName) + ), + isChild: true + ), + SettingTile( + title: translations.playGameServerBrowserName, + subtitle: translations.playGameServerBrowserDescription, + content: Button( + onPressed: () => pageIndex.value = RebootPageType.browser.index, + child: Text(translations.playGameServerBrowserName) + ), + isChild: true + ), + SettingTile( + title: translations.playGameServerCustomName, + subtitle: translations.playGameServerCustomDescription, + content: Button( + onPressed: () { + pageIndex.value = RebootPageType.matchmaker.index; + WidgetsBinding.instance.addPostFrameCallback((_) => _matchmakerController.gameServerAddressFocusNode.requestFocus()); + }, + child: Text(translations.playGameServerCustomContent) + ), + isChild: true + ) + ] + ); +} \ No newline at end of file diff --git a/gui/lib/src/page/implementation/server_browser_page.dart b/gui/lib/src/page/implementation/server_browser_page.dart new file mode 100644 index 0000000..d75470f --- /dev/null +++ b/gui/lib/src/page/implementation/server_browser_page.dart @@ -0,0 +1,247 @@ + +import 'dart:async'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; +import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:skeletons/skeletons.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 get hasButton => false; + + @override + RebootPageState createState() => _BrowsePageState(); + + @override + List get settings => []; +} + +class _BrowsePageState extends RebootPageState { + final HostingController _hostingController = Get.find(); + final MatchmakerController _matchmakerController = Get.find(); + final TextEditingController _filterController = TextEditingController(); + final StreamController _filterControllerStream = StreamController.broadcast(); + + @override + Widget build(BuildContext context) { + super.build(context); + return Obx(() { + var 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) => Column( + children: [ + _searchBar, + + const SizedBox( + height: 16, + ), + + Expanded( + child: StreamBuilder( + stream: _filterControllerStream.stream, + builder: (context, filterSnapshot) { + var items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet(); + if(items.isEmpty) { + return _noServersByQuery; + } + + return _buildPopulatedListBody(items); + } + ), + ) + ], + ); + + Widget _buildPopulatedListBody(Set> items) => ListView.builder( + itemCount: items.length * 2, + itemBuilder: (context, index) { + if(index % 2 != 0) { + return const SizedBox( + height: 8.0 + ); + } + + var entry = items.elementAt(index ~/ 2); + var hasPassword = entry["password"] != null; + return SettingTile( + title: "${_formatName(entry)} • ${entry["author"]}", + subtitle: "${_formatDescription(entry)} • ${_formatVersion(entry)}", + content: Button( + onPressed: () => _matchmakerController.joinServer(_hostingController.uuid, entry), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if(hasPassword) + const Icon(FluentIcons.lock), + if(hasPassword) + const SizedBox(width: 8.0), + Text(_matchmakerController.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(Map entry, String? filter) => + filter == null || filter.isEmpty || _filterServer(entry, filter); + + bool _filterServer(Map element, String filter) { + String? id = element["id"]; + if(id?.toLowerCase().contains(filter.toLowerCase()) == true) { + return true; + } + + var uri = Uri.tryParse(filter); + if(uri != null + && uri.host.isNotEmpty + && id?.toLowerCase().contains(uri.host.toLowerCase()) == true) { + return true; + } + + String? name = element["name"]; + if(name?.toLowerCase().contains(filter) == true) { + return true; + } + + String? author = element["author"]; + if(author?.toLowerCase().contains(filter) == true) { + return true; + } + + String? description = element["description"]; + if(description?.toLowerCase().contains(filter) == true) { + return true; + } + + return false; + } + + Widget get _searchBar => 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: _filterController.text.isNotEmpty ? null : ButtonState.all(Colors.transparent), + border: _filterController.text.isNotEmpty ? null : ButtonState.all(const BorderSide(color: Colors.transparent)) + ), + child: _searchBarIconData + ); + + Widget get _searchBarIconData { + var 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(Map entry) { + String result = entry['name']; + return result.isEmpty ? translations.defaultServerName : result; + } + + String _formatDescription(Map entry) { + String result = entry['description']; + return result.isEmpty ? translations.defaultServerDescription : result; + } + + String _formatVersion(Map entry) { + var version = entry['version']; + var versionSplit = version.indexOf("-"); + var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version; + String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion; + if(result.toLowerCase().startsWith("fortnite ")) { + result = result.substring(0, 10); + } + + return "Fortnite $result"; + } + + @override + Widget? get button => null; + + @override + List get settings => []; +} diff --git a/gui/lib/src/page/implementation/server_host_page.dart b/gui/lib/src/page/implementation/server_host_page.dart new file mode 100644 index 0000000..0ba57d1 --- /dev/null +++ b/gui/lib/src/page/implementation/server_host_page.dart @@ -0,0 +1,289 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:dart_ipify/dart_ipify.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/material.dart' show Icons; +import 'package:get/get.dart'; +import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/dialog/implementation/data.dart'; +import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:reboot_launcher/src/widget/game/start_button.dart'; +import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart'; +import 'package:sync/semaphore.dart'; + +class HostPage extends RebootPage { + const HostPage({Key? key}) : super(key: key); + + @override + String get name => "Host"; + + @override + String get iconAsset => "assets/images/host.png"; + + @override + RebootPageType get type => RebootPageType.host; + + @override + bool get hasButton => true; + + @override + RebootPageState createState() => _HostingPageState(); + + @override + List get settings => [ + PageSetting( + name: translations.hostGameServerName, + description: translations.hostGameServerDescription, + children: [ + PageSetting( + name: translations.hostGameServerNameName, + description: translations.hostGameServerNameDescription + ), + PageSetting( + name: translations.hostGameServerDescriptionName, + description: translations.hostGameServerDescriptionDescription + ), + PageSetting( + name: translations.hostGameServerPasswordName, + description: translations.hostGameServerDescriptionDescription + ), + PageSetting( + name: translations.hostGameServerDiscoverableName, + description: translations.hostGameServerDiscoverableDescription + ) + ], + ), + versionSelectorRebootSetting, + PageSetting( + name: translations.hostShareName, + description: translations.hostShareDescription, + children: [ + PageSetting( + name: translations.hostShareLinkName, + description: translations.hostShareLinkDescription, + content: translations.hostShareLinkContent + ), + PageSetting( + name: translations.hostShareIpName, + description: translations.hostShareIpDescription, + content: translations.hostShareIpContent + ) + ], + ), + PageSetting( + name: translations.hostResetName, + description: translations.hostResetDescription, + content: translations.hostResetContent + ) + ]; +} + +class _HostingPageState extends RebootPageState { + final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); + final Semaphore _semaphore = Semaphore(); + + late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); + + @override + void initState() { + if(_hostingController.name.text.isEmpty) { + _hostingController.name.text = translations.defaultServerName; + } + + if(_hostingController.description.text.isEmpty) { + _hostingController.description.text = translations.defaultServerDescription; + } + + super.initState(); + } + + @override + Widget get button => const LaunchButton( + host: true + ); + + @override + List get settings => [ + _gameServer, + versionSelectorSettingTile, + _share, + _resetDefaults + ]; + + SettingTile get _resetDefaults => SettingTile( + title: translations.hostResetName, + subtitle: translations.hostResetDescription, + content: Button( + onPressed: () => showResetDialog(_hostingController.reset), + child: Text(translations.hostResetContent), + ) + ); + + SettingTile get _gameServer => SettingTile( + title: translations.hostGameServerName, + subtitle: translations.hostGameServerDescription, + expandedContent: [ + SettingTile( + title: translations.hostGameServerNameName, + subtitle: translations.hostGameServerNameDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.hostGameServerNameName, + controller: _hostingController.name, + onChanged: (_) => _updateServer() + ) + ), + SettingTile( + title: translations.hostGameServerDescriptionName, + subtitle: translations.hostGameServerDescriptionDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.hostGameServerDescriptionName, + controller: _hostingController.description, + onChanged: (_) => _updateServer() + ) + ), + SettingTile( + title: translations.hostGameServerPasswordName, + subtitle: translations.hostGameServerDescriptionDescription, + isChild: true, + content: Obx(() => TextFormBox( + placeholder: translations.hostGameServerPasswordName, + controller: _hostingController.password, + autovalidateMode: AutovalidateMode.always, + obscureText: !_hostingController.showPassword.value, + enableSuggestions: false, + autocorrect: false, + onChanged: (text) { + _showPasswordTrailing.value = text.isNotEmpty; + _updateServer(); + }, + suffix: Button( + onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value, + style: ButtonStyle( + shape: ButtonState.all(const CircleBorder()), + backgroundColor: ButtonState.all(Colors.transparent) + ), + child: Icon( + _hostingController.showPassword.value ? Icons.visibility_off : Icons.visibility, + color: _showPasswordTrailing.value ? null : Colors.transparent + ), + ) + )) + ), + SettingTile( + title: translations.hostGameServerDiscoverableName, + subtitle: translations.hostGameServerDiscoverableDescription, + isChild: true, + contentWidth: null, + content: Obx(() => Row( + children: [ + 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(); + } + ), + ], + )) + ) + ] + ); + + SettingTile get _share => SettingTile( + title: translations.hostShareName, + subtitle: translations.hostShareDescription, + expandedContent: [ + SettingTile( + title: translations.hostShareLinkName, + subtitle: translations.hostShareLinkDescription, + isChild: true, + content: Button( + onPressed: () async { + FlutterClipboard.controlC("$kCustomUrlSchema://${_hostingController.uuid}"); + _showCopiedLink(); + }, + child: Text(translations.hostShareLinkContent), + ) + ), + SettingTile( + title: translations.hostShareIpName, + subtitle: translations.hostShareIpDescription, + isChild: true, + content: Button( + onPressed: () async { + try { + _showCopyingIp(); + var ip = await Ipify.ipv4(); + FlutterClipboard.controlC(ip); + _showCopiedIp(); + }catch(error) { + _showCannotCopyIp(error); + } + }, + child: Text(translations.hostShareIpContent), + ) + ) + ], + ); + + Future _updateServer() async { + if(!_hostingController.published()) { + return; + } + + try { + _semaphore.acquire(); + _hostingController.publishServer( + _gameController.username.text, + _hostingController.instance.value!.versionName + ); + } catch(error) { + _showCannotUpdateGameServer(error); + } finally { + _semaphore.release(); + } + } + + void _showCopiedLink() => showInfoBar( + translations.hostShareLinkMessageSuccess, + severity: InfoBarSeverity.success + ); + + void _showCopyingIp() => showInfoBar( + translations.hostShareIpMessageLoading, + loading: true, + duration: null + ); + + void _showCopiedIp() => showInfoBar( + translations.hostShareIpMessageSuccess, + severity: InfoBarSeverity.success + ); + + void _showCannotCopyIp(Object error) => showInfoBar( + translations.hostShareIpMessageError(error.toString()), + severity: InfoBarSeverity.error, + duration: snackbarLongDuration + ); + + void _showCannotUpdateGameServer(Object error) => showInfoBar( + translations.cannotUpdateGameServer(error.toString()), + severity: InfoBarSeverity.success, + duration: snackbarLongDuration + ); +} \ No newline at end of file diff --git a/gui/lib/src/page/implementation/settings_page.dart b/gui/lib/src/page/implementation/settings_page.dart new file mode 100644 index 0000000..024a3ea --- /dev/null +++ b/gui/lib/src/page/implementation/settings_page.dart @@ -0,0 +1,317 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/controller/update_controller.dart'; +import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/dialog/implementation/data.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/file_selector.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SettingsPage extends RebootPage { + const SettingsPage({Key? key}) : super(key: key); + + @override + String get name => translations.settingsName; + + @override + String get iconAsset => "assets/images/settings.png"; + + @override + RebootPageType get type => RebootPageType.settings; + + @override + bool get hasButton => false; + + @override + RebootPageState createState() => _SettingsPageState(); + + @override + List get settings => [ + PageSetting( + name: translations.settingsClientName, + description: translations.settingsClientDescription, + children: [ + PageSetting( + name: translations.settingsClientConsoleName, + description: translations.settingsClientConsoleDescription + ), + PageSetting( + name: translations.settingsClientAuthName, + description: translations.settingsClientAuthDescription + ), + PageSetting( + name: translations.settingsClientMemoryName, + description: translations.settingsClientMemoryDescription + ), + PageSetting( + name: translations.settingsClientArgsName, + description: translations.settingsClientArgsDescription + ), + ], + ), + PageSetting( + name: translations.settingsServerName, + description: translations.settingsServerSubtitle, + children: [ + PageSetting( + name: translations.settingsServerFileName, + description: translations.settingsServerFileDescription + ), + PageSetting( + name: translations.settingsServerPortName, + description: translations.settingsServerPortDescription + ), + PageSetting( + name: translations.settingsServerMirrorName, + description: translations.settingsServerMirrorDescription + ), + PageSetting( + name: translations.settingsServerTimerName, + description: translations.settingsServerTimerSubtitle + ), + ], + ), + PageSetting( + name: translations.settingsUtilsName, + description: translations.settingsUtilsSubtitle, + children: [ + PageSetting( + name: translations.settingsUtilsThemeName, + description: translations.settingsUtilsThemeDescription, + ), + PageSetting( + name: translations.settingsUtilsLanguageName, + description: translations.settingsUtilsLanguageDescription, + ), + PageSetting( + name: translations.settingsUtilsInstallationDirectoryName, + description: translations.settingsUtilsInstallationDirectorySubtitle, + content: translations.settingsUtilsInstallationDirectoryContent + ), + PageSetting( + name: translations.settingsUtilsBugReportName, + description: translations.settingsUtilsBugReportSubtitle, + content: translations.settingsUtilsBugReportContent + ), + PageSetting( + name: translations.settingsUtilsResetDefaultsName, + description: translations.settingsUtilsResetDefaultsSubtitle, + content: translations.settingsUtilsResetDefaultsContent + ) + ], + ) + ]; +} + +class _SettingsPageState extends RebootPageState { + final GameController _gameController = Get.find(); + final SettingsController _settingsController = Get.find(); + final UpdateController _updateController = Get.find(); + + @override + Widget? get button => null; + + @override + List get settings => [ + _clientSettings, + _gameServerSettings, + _launcherUtilities + ]; + + SettingTile get _clientSettings => SettingTile( + title: translations.settingsClientName, + subtitle: translations.settingsClientDescription, + expandedContent: [ + _createFileSetting( + title: translations.settingsClientConsoleName, + description: translations.settingsClientConsoleDescription, + controller: _settingsController.unrealEngineConsoleDll + ), + _createFileSetting( + title: translations.settingsClientAuthName, + description: translations.settingsClientAuthDescription, + controller: _settingsController.authenticatorDll + ), + _createFileSetting( + title: translations.settingsClientMemoryName, + description: translations.settingsClientMemoryDescription, + controller: _settingsController.memoryLeakDll + ), + SettingTile( + title: translations.settingsClientArgsName, + subtitle: translations.settingsClientArgsDescription, + isChild: true, + content: TextFormBox( + placeholder: translations.settingsClientArgsPlaceholder, + controller: _gameController.customLaunchArgs, + ) + ), + ], + ); + + SettingTile get _gameServerSettings => SettingTile( + title: translations.settingsServerName, + subtitle: translations.settingsServerSubtitle, + expandedContent: [ + _createFileSetting( + title: translations.settingsServerFileName, + description: translations.settingsServerFileDescription, + controller: _settingsController.gameServerDll + ), + SettingTile( + title: translations.settingsServerPortName, + subtitle: translations.settingsServerPortDescription, + content: TextFormBox( + placeholder: translations.settingsServerPortName, + controller: _settingsController.gameServerPort, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ] + ), + isChild: true + ), + SettingTile( + title: translations.settingsServerMirrorName, + subtitle: translations.settingsServerMirrorDescription, + content: TextFormBox( + placeholder: translations.settingsServerMirrorPlaceholder, + controller: _updateController.url, + validator: checkUpdateUrl + ), + isChild: true + ), + SettingTile( + title: translations.settingsServerTimerName, + subtitle: translations.settingsServerTimerSubtitle, + content: Obx(() => DropDownButton( + leading: Text(_updateController.timer.value.text), + items: UpdateTimer.values.map((entry) => MenuFlyoutItem( + text: Text(entry.text), + onPressed: () { + _updateController.timer.value = entry; + removeMessageByPage(6); + _updateController.update(true); + } + )).toList() + )), + isChild: true + ), + ], + ); + + SettingTile get _launcherUtilities => SettingTile( + title: translations.settingsUtilsName, + subtitle: translations.settingsUtilsSubtitle, + expandedContent: [ + SettingTile( + title: translations.settingsUtilsLanguageName, + subtitle: translations.settingsUtilsLanguageDescription, + isChild: true, + content: Obx(() => DropDownButton( + leading: Text(_getLocaleName(_settingsController.language.value)), + items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem( + text: Text(_getLocaleName(locale.languageCode)), + onPressed: () => _settingsController.language.value = locale.languageCode + )).toList() + )) + ), + SettingTile( + title: translations.settingsUtilsThemeName, + subtitle: translations.settingsUtilsThemeDescription, + isChild: true, + content: Obx(() => DropDownButton( + leading: Text(_settingsController.themeMode.value.title), + items: ThemeMode.values.map((themeMode) => MenuFlyoutItem( + text: Text(themeMode.title), + onPressed: () => _settingsController.themeMode.value = themeMode + )).toList() + )) + ), + SettingTile( + title: translations.settingsUtilsInstallationDirectoryName, + subtitle: translations.settingsUtilsInstallationDirectorySubtitle, + isChild: true, + content: Button( + onPressed: () => launchUrl(installationDirectory.uri), + child: Text(translations.settingsUtilsInstallationDirectoryContent), + ) + ), + SettingTile( + title: translations.settingsUtilsBugReportName, + subtitle: translations.settingsUtilsBugReportSubtitle, + isChild: true, + content: Button( + onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")), + child: Text(translations.settingsUtilsBugReportContent), + ) + ), + SettingTile( + title: translations.settingsUtilsResetDefaultsName, + subtitle: translations.settingsUtilsResetDefaultsSubtitle, + isChild: true, + content: Button( + onPressed: () => showResetDialog(_settingsController.reset), + child: Text(translations.settingsUtilsResetDefaultsContent), + ) + ) + ], + ); + + String _getLocaleName(String locale) { + var result = LocaleNames.of(context)!.nameOf(locale); + if(result != null) { + return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}"; + } + + return locale; + } + + Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile( + title: title, + subtitle: description, + content: FileSelector( + placeholder: translations.selectPathPlaceholder, + windowTitle: translations.selectPathWindowTitle, + controller: controller, + validator: checkDll, + extension: "dll", + folder: false + ), + isChild: true + ); +} + +extension _UpdateTimerExtension on UpdateTimer { + String get text { + if (this == UpdateTimer.never) { + return translations.updateGameServerDllNever; + } + + return translations.updateGameServerDllEvery(name); + } +} + +extension _ThemeModeExtension on ThemeMode { + String get title { + switch(this) { + case ThemeMode.system: + return translations.system; + case ThemeMode.dark: + return translations.dark; + case ThemeMode.light: + return translations.light; + } + } +} \ No newline at end of file diff --git a/gui/lib/src/page/info_page.dart b/gui/lib/src/page/info_page.dart deleted file mode 100644 index 71e8252..0000000 --- a/gui/lib/src/page/info_page.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/gestures.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/util/tutorial.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; - -class InfoPage extends StatefulWidget { - const InfoPage({Key? key}) : super(key: key); - - @override - State createState() => _InfoPageState(); -} - -class _InfoPageState extends State with AutomaticKeepAliveClientMixin { - final SettingsController _settingsController = Get.find(); - late final ScrollController _controller; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - _controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance); - _controller.addListener(() { - _settingsController.scrollingDistance = _controller.offset; - }); - - super.initState(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Column( - children: [ - Expanded( - child: ListView( - children: [ - SettingTile( - title: 'What is Project Reboot?', - subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. ' - 'The project was started on Discord by Milxnor. ' - 'The project is no longer being actively maintained.', - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'What is a game server?', - subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. ' - 'You can join someone else\'s game server, or host one on your PC by going to the "Host" tab. ', - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'What is a client?', - subtitle: 'A client is the actual Fortnite game. ' - 'You can download any version of Fortnite from the launcher in the "Play" tab. ' - 'You can also import versions from your local PC, but remember that these may be corrupted. ' - 'If a local version doesn\'t work, try installing it from the launcher before reporting a bug.', - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'What is an authenticator?', - subtitle: 'An authenticator is a program that handles authentication, parties and voice chats. ' - 'By default, a LawinV1 server will be started for you to play. ' - 'You can use also use an authenticator running locally(on your PC) or remotely(on another PC). ' - 'Changing the authenticator settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! ' - 'If you need to restore these settings, go to the "Settings" tab and click on "Restore Defaults". ', - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'Do I need to update DLLs?', - subtitle: 'No, all the files that the launcher uses are automatically updated. ' - 'You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. ' - 'Unless you are an advanced user, changing these options is not recommended', - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'Where can I report bugs or ask for new features?', - subtitle: 'Go to the "Settings" tab and click on report bug. ' - 'Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement', - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: 'How can I make my game server accessible for other players?', - subtitle: Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'Follow ', - style: FluentTheme.of(context).typography.body - ), - TextSpan( - text: 'this tutorial', - mouseCursor: SystemMouseCursors.click, - style: FluentTheme.of(context).typography.body?.copyWith(color: FluentTheme.of(context).accentColor), - recognizer: TapGestureRecognizer()..onTap = openPortTutorial - ) - ] - ) - ), - titleStyle: FluentTheme - .of(context) - .typography - .title, - contentWidth: null, - ) - ], - ), - ) - ], - ); - } -} \ No newline at end of file diff --git a/gui/lib/src/page/matchmaker_page.dart b/gui/lib/src/page/matchmaker_page.dart deleted file mode 100644 index 7baec41..0000000 --- a/gui/lib/src/page/matchmaker_page.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; -import 'package:reboot_launcher/src/widget/server/start_button.dart'; -import 'package:reboot_launcher/src/widget/server/type_selector.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class MatchmakerPage extends StatefulWidget { - const MatchmakerPage({Key? key}) : super(key: key); - - @override - State createState() => _MatchmakerPageState(); -} - -class _MatchmakerPageState extends State with AutomaticKeepAliveClientMixin { - final MatchmakerController _matchmakerController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Column( - children: [ - Expanded( - child: ListView( - children: [ - Obx(() => SettingTile( - title: "Matchmaker configuration", - subtitle: "This section contains the matchmaker's configuration", - content: const ServerTypeSelector( - authenticator: false - ), - expandedContent: [ - if(_matchmakerController.type.value == ServerType.remote) - SettingTile( - title: "Host", - subtitle: "The hostname of the matchmaker", - isChild: true, - content: TextFormBox( - placeholder: "Host", - controller: _matchmakerController.host - ) - ), - if(_matchmakerController.type.value != ServerType.embedded) - SettingTile( - title: "Port", - subtitle: "The port of the matchmaker", - isChild: true, - content: TextFormBox( - placeholder: "Port", - controller: _matchmakerController.port, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ] - ) - ), - if(_matchmakerController.type.value == ServerType.embedded) - SettingTile( - title: "Game server address", - subtitle: "The address of the game server used by the matchmaker", - isChild: true, - content: TextFormBox( - placeholder: "Address", - controller: _matchmakerController.gameServerAddress, - focusNode: _matchmakerController.gameServerAddressFocusNode - ) - ), - if(_matchmakerController.type.value == ServerType.embedded) - SettingTile( - title: "Detached", - subtitle: "Whether the embedded matchmaker should be started as a separate process, useful for debugging", - contentWidth: null, - isChild: true, - content: Obx(() => Row( - children: [ - Text( - _matchmakerController.detached.value ? "On" : "Off" - ), - const SizedBox( - width: 16.0 - ), - ToggleSwitch( - checked: _matchmakerController.detached.value, - onChanged: (value) => _matchmakerController.detached.value = value - ), - ], - )), - ) - ] - )), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Installation directory", - subtitle: "Opens the folder where the embedded matchmaker is located", - content: Button( - onPressed: () => launchUrl(matchmakerDirectory.uri), - child: const Text("Show Files") - ) - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Reset matchmaker", - subtitle: "Resets the authenticator's settings to their default values", - content: Button( - onPressed: () => showAppDialog( - builder: (context) => InfoDialog( - text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", - buttons: [ - DialogButton( - type: ButtonType.secondary, - text: "Close", - ), - DialogButton( - type: ButtonType.primary, - text: "Reset", - onTap: () { - _matchmakerController.reset(); - Navigator.of(context).pop(); - }, - ) - ], - ) - ), - child: const Text("Reset"), - ) - ) - ] - ), - ), - const SizedBox( - height: 8.0, - ), - const ServerButton( - authenticator: false - ) - ], - ); - } -} diff --git a/gui/lib/src/page/pages.dart b/gui/lib/src/page/pages.dart new file mode 100644 index 0000000..7870bab --- /dev/null +++ b/gui/lib/src/page/pages.dart @@ -0,0 +1,43 @@ +import 'dart:collection'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/implementation/authenticator_page.dart'; +import 'package:reboot_launcher/src/page/implementation/info_page.dart'; +import 'package:reboot_launcher/src/page/implementation/matchmaker_page.dart'; +import 'package:reboot_launcher/src/page/implementation/play_page.dart'; +import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart'; +import 'package:reboot_launcher/src/page/implementation/server_host_page.dart'; +import 'package:reboot_launcher/src/page/implementation/settings_page.dart'; + +final List pages = [ + const PlayPage(), + const HostPage(), + const BrowsePage(), + const AuthenticatorPage(), + const MatchmakerPage(), + const InfoPage(), + const SettingsPage() +]; + +final RxInt pageIndex = RxInt(0); + +final HashMap _pageKeys = HashMap(); + +GlobalKey appKey = GlobalKey(); +GlobalKey get pageKey { + var index = pageIndex.value; + var key = _pageKeys[index]; + if(key != null) { + return key; + } + + var result = GlobalKey(); + _pageKeys[index] = result; + return result; +} + +List get pagesWithButtonIndexes => pages.where((page) => page.hasButton) + .map((page) => page.index) + .toList(); \ No newline at end of file diff --git a/gui/lib/src/page/play_page.dart b/gui/lib/src/page/play_page.dart deleted file mode 100644 index 02ec91d..0000000 --- a/gui/lib/src/page/play_page.dart +++ /dev/null @@ -1,137 +0,0 @@ - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; -import 'package:reboot_launcher/src/page/home_page.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; -import 'package:reboot_launcher/src/widget/game/start_button.dart'; -import 'package:reboot_launcher/src/widget/version/version_selector.dart'; - - -class PlayPage extends StatefulWidget { - const PlayPage({Key? key}) : super(key: key); - - @override - State createState() => _PlayPageState(); -} - -class _PlayPageState extends State { - final MatchmakerController _matchmakerController = Get.find(); - final HostingController _hostingController = Get.find(); - late final RxBool _selfServer; - - @override - void initState() { - _selfServer = RxBool(_isLocalPlay); - _matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay); - _hostingController.started.listen((_) => _selfServer.value = _isLocalPlay); - super.initState(); - } - - bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text) - && !_hostingController.started.value; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded( - child: ListView( - children: [ - const SettingTile( - title: "Version", - subtitle: "Select the version of Fortnite you want to host", - content: VersionSelector(), - expandedContent: [ - SettingTile( - title: "Add a version from this PC's local storage", - subtitle: "Versions coming from your local disk are not guaranteed to work", - content: Button( - onPressed: VersionSelector.openAddDialog, - child: Text("Add build"), - ), - isChild: true - ), - SettingTile( - title: "Download any version from the cloud", - subtitle: "Download any Fortnite build easily from the cloud", - content: Button( - onPressed: VersionSelector.openDownloadDialog, - child: Text("Download"), - ), - isChild: true - ) - ] - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Game Server", - subtitle: "Helpful shortcuts to find the server where you want to play", - content: IgnorePointer( - child: Button( - style: ButtonStyle( - backgroundColor: ButtonState.all(FluentTheme.of(context).resources.controlFillColorDefault) - ), - onPressed: () {}, - child: Obx(() { - var address = _matchmakerController.gameServerAddress.text; - var owner = _matchmakerController.gameServerOwner.value; - return Text( - isLocalHost(address) ? "Your server" : owner != null ? "$owner's server" : address, - textAlign: TextAlign.start - ); - }) - ), - ), - expandedContent: [ - SettingTile( - title: "Host a server", - subtitle: "Do you want to create a game server for yourself or your friends? Host one!", - content: Button( - onPressed: () => pageIndex.value = 1, - child: const Text("Host") - ), - isChild: true - ), - SettingTile( - title: "Join a Reboot server", - subtitle: "Find a discoverable server hosted on the Reboot Launcher in the server browser", - content: Button( - onPressed: () => pageIndex.value = 2, - child: const Text("Browse") - ), - isChild: true - ), - SettingTile( - title: "Join a custom server", - subtitle: "Type the address of any server, whether it was hosted on the Reboot Launcher or not", - content: Button( - onPressed: () { - pageIndex.value = 4; - WidgetsBinding.instance.addPostFrameCallback((_) => _matchmakerController.gameServerAddressFocusNode.requestFocus()); - }, - child: const Text("Join") - ), - isChild: true - ) - ] - ), - ], - ) - ), - const SizedBox( - height: 8.0, - ), - const LaunchButton( - startLabel: 'Launch Fortnite', - stopLabel: 'Close Fortnite', - host: false - ) - ] - ); - } -} \ No newline at end of file diff --git a/gui/lib/src/page/settings_page.dart b/gui/lib/src/page/settings_page.dart deleted file mode 100644 index dc0cf3f..0000000 --- a/gui/lib/src/page/settings_page.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/controller/update_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/widget/common/file_selector.dart'; -import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class SettingsPage extends StatefulWidget { - const SettingsPage({Key? key}) : super(key: key); - - @override - State createState() => _SettingsPageState(); -} - -class _SettingsPageState extends State with AutomaticKeepAliveClientMixin { - final GameController _gameController = Get.find(); - final SettingsController _settingsController = Get.find(); - final UpdateController _updateController = Get.find(); - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return ListView( - children: [ - SettingTile( - title: "Client settings", - subtitle: "This section contains the dlls used to make the Fortnite client work", - expandedContent: [ - _createFileSetting( - title: "Unreal engine console", - description: "This file is injected to unlock the Unreal Engine Console", - controller: _settingsController.unrealEngineConsoleDll - ), - _createFileSetting( - title: "Authentication patcher", - description: "This file is injected to redirect all HTTP requests to the launcher's authenticator", - controller: _settingsController.authenticatorDll - ), - SettingTile( - title: "Custom launch arguments", - subtitle: "Additional arguments to use when launching the game", - isChild: true, - content: TextFormBox( - placeholder: "Arguments...", - controller: _gameController.customLaunchArgs, - ) - ), - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Game server settings", - subtitle: "This section contains settings related to the game server implementation", - expandedContent: [ - _createFileSetting( - title: "Implementation", - description: "This file is injected to create a game server & host matches", - controller: _settingsController.gameServerDll - ), - SettingTile( - title: "Port", - subtitle: "The port used by the game server dll", - content: TextFormBox( - placeholder: "Port", - controller: _settingsController.gameServerPort, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ] - ), - isChild: true - ), - SettingTile( - title: "Update mirror", - subtitle: "The URL used to update the game server dll", - content: TextFormBox( - placeholder: "URL", - controller: _updateController.url, - validator: checkUpdateUrl - ), - isChild: true - ), - SettingTile( - title: "Update timer", - subtitle: "Determines when the game server dll should be updated", - content: Obx(() => DropDownButton( - leading: Text(_updateController.timer.value.text), - items: UpdateTimer.values.map((entry) => MenuFlyoutItem( - text: Text(entry.text), - onPressed: () { - _updateController.timer.value = entry; - removeMessage(6); - _updateController.update(true); - } - )).toList() - )), - isChild: true - ), - ], - ), - const SizedBox( - height: 8.0, - ), - SettingTile( - title: "Launcher utilities", - subtitle: "This section contains handy settings for the launcher", - expandedContent: [ - SettingTile( - title: "Installation directory", - subtitle: "Opens the installation directory", - isChild: true, - content: Button( - onPressed: () => launchUrl(installationDirectory.uri), - child: const Text("Show Files"), - ) - ), - SettingTile( - title: "Create a bug report", - subtitle: "Help me fix bugs by reporting them", - isChild: true, - content: Button( - onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")), - child: const Text("Report a bug"), - ) - ), - SettingTile( - title: "Reset settings", - subtitle: "Resets the launcher's settings to their default values", - isChild: true, - content: Button( - onPressed: () => showAppDialog( - builder: (context) => InfoDialog( - text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible", - buttons: [ - DialogButton( - type: ButtonType.secondary, - text: "Close", - ), - DialogButton( - type: ButtonType.primary, - text: "Reset", - onTap: () { - _settingsController.reset(); - Navigator.of(context).pop(); - }, - ) - ], - ) - ), - child: const Text("Reset"), - ) - ) - ], - ), - ] - ); - } - - Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile( - title: title, - subtitle: description, - content: FileSelector( - placeholder: "Path", - windowTitle: "Select a file", - controller: controller, - validator: checkDll, - extension: "dll", - folder: false - ), - isChild: true - ); -} - -extension _UpdateTimerExtension on UpdateTimer { - String get text => this == UpdateTimer.never ? "Never" : "Every $name"; -} \ No newline at end of file diff --git a/gui/lib/src/util/checks.dart b/gui/lib/src/util/checks.dart index b5afd08..84ec47f 100644 --- a/gui/lib/src/util/checks.dart +++ b/gui/lib/src/util/checks.dart @@ -1,14 +1,15 @@ import 'dart:io'; import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; String? checkVersion(String? text, List versions) { if (text == null || text.isEmpty) { - return 'Empty version name'; + return translations.emptyVersionName; } if (versions.any((element) => element.name == text)) { - return 'This version already exists'; + return translations.versionAlreadyExists; } return null; @@ -16,7 +17,7 @@ String? checkVersion(String? text, List versions) { String? checkChangeVersion(String? text) { if (text == null || text.isEmpty) { - return 'Empty version name'; + return translations.emptyVersionName; } return null; @@ -24,16 +25,16 @@ String? checkChangeVersion(String? text) { String? checkGameFolder(text) { if (text == null || text.isEmpty) { - return 'Empty game path'; + return translations.emptyGamePath; } var directory = Directory(text); if (!directory.existsSync()) { - return "Directory doesn't exist"; + return translations.directoryDoesNotExist; } if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { - return "Invalid game path"; + return translations.missingShippingExe; } return null; @@ -41,7 +42,7 @@ String? checkGameFolder(text) { String? checkDownloadDestination(text) { if (text == null || text.isEmpty) { - return 'Invalid download path'; + return translations.invalidDownloadPath; } return null; @@ -49,15 +50,15 @@ String? checkDownloadDestination(text) { String? checkDll(String? text) { if (text == null || text.isEmpty) { - return "Empty dll path"; + return translations.invalidDllPath; } if (!File(text).existsSync()) { - return "This dll doesn't exist"; + return translations.dllDoesNotExist; } if (!text.endsWith(".dll")) { - return "This file is not a dll"; + return translations.invalidDllExtension; } return null; @@ -65,12 +66,12 @@ String? checkDll(String? text) { String? checkMatchmaking(String? text) { if (text == null || text.isEmpty) { - return "Empty hostname"; + return translations.emptyHostname; } var ipParts = text.split(":"); if(ipParts.length > 2){ - return "Wrong format, expected ip:port"; + return translations.hostnameFormat; } return null; @@ -78,7 +79,7 @@ String? checkMatchmaking(String? text) { String? checkUpdateUrl(String? text) { if (text == null || text.isEmpty) { - return "Empty URL"; + return translations.emptyURL; } return null; diff --git a/gui/lib/src/util/matchmaker.dart b/gui/lib/src/util/matchmaker.dart index 0191627..ef153ad 100644 --- a/gui/lib/src/util/matchmaker.dart +++ b/gui/lib/src/util/matchmaker.dart @@ -1,8 +1,30 @@ +import 'dart:convert'; import 'dart:io'; import 'package:reboot_common/common.dart'; -final File _script = File("${assetsDirectory.path}\\misc\\udp.ps1"); +const Duration _timeout = Duration(seconds: 2); + +Future _pingGameServer(String hostname, int port) async { + var socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); + var dataToSend = utf8.encode(DateTime.now().toIso8601String()); + socket.send(dataToSend, InternetAddress(hostname), port); + await for (var event in socket) { + switch(event) { + case RawSocketEvent.read: + return true; + case RawSocketEvent.readClosed: + case RawSocketEvent.closed: + return false; + case RawSocketEvent.write: + break; + } + } + + return false; +} + +Future get _timeoutFuture => Future.delayed(_timeout).then((value) => false); Future pingGameServer(String address, {Duration? timeout}) async { var start = DateTime.now(); @@ -10,23 +32,20 @@ Future pingGameServer(String address, {Duration? timeout}) async { while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) { var split = address.split(":"); var hostname = split[0]; - var port = split.length > 1 ? split[1] : kDefaultGameServerPort; - var result = await Process.run( - "powershell", - [ - _script.path, - hostname, - port - ] - ); - if (result.exitCode == 0) { + if(isLocalHost(hostname)) { + hostname = "127.0.0.1"; + } + + var port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort); + var result = await Future.any([_timeoutFuture, _pingGameServer(hostname, port)]); + if(result) { return true; } if(firstTime) { firstTime = false; }else { - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(_timeout); } } diff --git a/gui/lib/src/util/translations.dart b/gui/lib/src/util/translations.dart new file mode 100644 index 0000000..0cbb861 --- /dev/null +++ b/gui/lib/src/util/translations.dart @@ -0,0 +1,18 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; + +AppLocalizations? _translations; +bool _init = false; + +AppLocalizations get translations { + if(!_init) { + throw StateError("Translations haven't been loaded"); + } + + return _translations!; +} + +void loadTranslations(BuildContext context) { + _translations = AppLocalizations.of(context)!; + _init = true; +} \ No newline at end of file diff --git a/gui/lib/src/widget/common/file_selector.dart b/gui/lib/src/widget/common/file_selector.dart index 1b22191..f40b5ea 100644 --- a/gui/lib/src/widget/common/file_selector.dart +++ b/gui/lib/src/widget/common/file_selector.dart @@ -1,6 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/util/picker.dart'; class FileSelector extends StatefulWidget { @@ -56,7 +55,6 @@ class _FileSelectorState extends State { void _onPressed() { if(_selecting){ - showInfoBar("Folder selector is already opened"); return; } diff --git a/gui/lib/src/widget/common/setting_tile.dart b/gui/lib/src/widget/common/setting_tile.dart index 683eb5d..a856b36 100644 --- a/gui/lib/src/widget/common/setting_tile.dart +++ b/gui/lib/src/widget/common/setting_tile.dart @@ -39,21 +39,28 @@ class SettingTile extends StatefulWidget { class _SettingTileState extends State { @override Widget build(BuildContext context) { - if (widget.expandedContent == null || widget.expandedContent?.isEmpty == true) { - return _contentCard; - } + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1000 + ), + child: () { + if (widget.expandedContent == null || widget.expandedContent?.isEmpty == true) { + return _contentCard; + } - return Expander( - initiallyExpanded: true, - headerShape: (open) => const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)), - ), - header: SizedBox( - height: widget.expandedContentHeaderHeight, - child: _buildTile(false) - ), - trailing: _trailing, - content: _expandedContent + return Expander( + initiallyExpanded: true, + headerShape: (open) => const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)), + ), + header: SizedBox( + height: widget.expandedContentHeaderHeight, + child: _buildTile(false) + ), + trailing: _trailing, + content: _expandedContent + ); + }() ); } diff --git a/gui/lib/src/widget/game/start_button.dart b/gui/lib/src/widget/game/start_button.dart index 4a11756..79143c0 100644 --- a/gui/lib/src/widget/game/start_button.dart +++ b/gui/lib/src/widget/game/start_button.dart @@ -4,9 +4,7 @@ import 'dart:io'; import 'package:async/async.dart'; import 'package:dart_ipify/dart_ipify.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/gestures.dart'; import 'package:get/get.dart'; -import 'package:path/path.dart' as path; import 'package:process_run/shell.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; @@ -14,13 +12,14 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/dialog/implementation/game.dart'; +import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart' as messenger; import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/tutorial.dart'; import 'package:reboot_launcher/src/util/watch.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; class LaunchButton extends StatefulWidget { final bool host; @@ -34,14 +33,12 @@ class LaunchButton extends StatefulWidget { } class _LaunchButtonState extends State { - final SupabaseClient _supabase = Supabase.instance.client; final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); final AuthenticatorController _authenticatorController = Get.find(); final MatchmakerController _matchmakerController = Get.find(); final SettingsController _settingsController = Get.find(); - final File _logFile = File("${logsDirectory.path}\\game.log"); - bool _fail = false; + CancelableOperation? _operation; @override Widget build(BuildContext context) => Align( @@ -51,7 +48,7 @@ class _LaunchButtonState extends State { child: Obx(() => SizedBox( height: 48, child: Button( - onPressed: _start, + onPressed: () => _operation = CancelableOperation.fromFuture(_start()), child: Align( alignment: Alignment.center, child: Text(_hasStarted ? _stopMessage : _startMessage) @@ -65,27 +62,32 @@ class _LaunchButtonState extends State { void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started; - String get _startMessage => widget.startLabel ?? (widget.host ? "Start hosting" : "Launch fortnite"); + String get _startMessage => widget.startLabel ?? (widget.host ? translations.startHosting : translations.startGame); - String get _stopMessage => widget.stopLabel ?? (widget.host ? "Stop hosting" : "Close fortnite"); + String get _stopMessage => widget.stopLabel ?? (widget.host ? translations.stopHosting : translations.stopGame); Future _start() async { if (_hasStarted) { - _onStop(widget.host, false); - removeMessage(widget.host ? 1 : 0); + _onStop( + reason: _StopReason.normal + ); + return; + } + + if(_operation != null) { return; } - _fail = false; if(_gameController.selectedVersion == null){ - showInfoBar("Select a Fortnite version before continuing"); - _onStop(widget.host, false); + _onStop( + reason: _StopReason.missingVersionError + ); return; } _setStarted(widget.host, true); - for (var element in Injectable.values) { - if(await _getDllPath(element, widget.host) == null) { + for (var injectable in _Injectable.values) { + if(await _getDllFileOrStop(injectable, widget.host) == null) { return; } } @@ -94,58 +96,44 @@ class _LaunchButtonState extends State { var version = _gameController.selectedVersion!; var executable = await version.executable; if(executable == null){ - showMissingBuildError(version); - _onStop(widget.host, false); - return; - } - - var authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(false); - if(!authenticatorResult){ - _onStop(widget.host, false); - return; - } - - var matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(false); - if(!matchmakerResult){ - _onStop(widget.host, false); - return; - } - - var automaticallyStartedServer = await _startMatchMakingServer(); - await _startGameProcesses(version, widget.host, automaticallyStartedServer); - if(widget.host){ - showInfoBar( - "Launching the headless server...", - loading: true, - duration: null + _onStop( + reason: _StopReason.missingExecutableError, + error: version.location.path ); + return; } - } catch (exception, stacktrace) { - _onStop(widget.host, false); - showCorruptedBuildError(widget.host, exception, stacktrace); + + var authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(_pageType, false); + if(!authenticatorResult){ + _onStop( + reason: _StopReason.authenticatorError + ); + return; + } + + var matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(_pageType, false); + if(!matchmakerResult){ + _onStop( + reason: _StopReason.matchmakerError + ); + return; + } + + var automaticallyStartedServer = await _startMatchMakingServer(version); + await _startGameProcesses(version, widget.host, automaticallyStartedServer); + if(automaticallyStartedServer || widget.host){ + _showLaunchingGameServerWidget(); + } + } catch (exception, stackTrace) { + _onStop( + reason: _StopReason.unknownError, + error: exception.toString(), + stackTrace: stackTrace + ); } } - Future _startGameProcesses(FortniteVersion version, bool host, bool linkedHosting) async { - _setStarted(host, true); - var launcherProcess = await _createLauncherProcess(version); - var eacProcess = await _createEacProcess(version); - var executable = await version.executable; - var gameProcess = await _createGameProcess(executable!.path, host); - var instance = GameInstance(version.name, gameProcess, launcherProcess, eacProcess, host, linkedHosting); - instance.startObserver(); - if(host){ - _removeHostEntry(); - _hostingController.instance.value = instance; - _hostingController.saveInstance(); - }else{ - _gameController.instance.value = instance; - _gameController.saveInstance(); - } - _injectOrShowError(Injectable.sslBypass, host); - } - - Future _startMatchMakingServer() async { + Future _startMatchMakingServer(FortniteVersion version) async { if(widget.host){ return false; } @@ -155,47 +143,59 @@ class _LaunchButtonState extends State { return false; } - if(!_gameController.autoStartGameServer()){ - return false; - } - if(_hostingController.started()){ return false; } - var version = _gameController.selectedVersion!; - await _startGameProcesses(version, true, false); + _startGameProcesses(version, true, false); // Do not await + _setStarted(true, true); return true; } - Future _createGameProcess(String gamePath, bool host) async { - var gameArgs = createRebootArgs(_safeUsername, _gameController.password.text, host, _gameController.customLaunchArgs.text); - var gameProcess = await Process.start(gamePath, gameArgs); + Future _startGameProcesses(FortniteVersion version, bool host, bool linkedHosting) async { + var launcherProcess = await _createLauncherProcess(version); + var eacProcess = await _createEacProcess(version); + var executable = await version.executable; + var gameProcess = await _createGameProcess(executable!.path, host); + if(gameProcess == null) { + return; + } + + var instance = GameInstance(version.name, gameProcess, launcherProcess, eacProcess, host, linkedHosting); + instance.startObserver(); + if(host){ + _hostingController.discardServer(); + _hostingController.instance.value = instance; + _hostingController.saveInstance(); + }else{ + _gameController.instance.value = instance; + _gameController.saveInstance(); + } + _injectOrShowError(_Injectable.sslBypass, host); + } + + Future _createGameProcess(String gamePath, bool host) async { + if(!_hasStarted) { + return null; + } + + var gameArgs = createRebootArgs( + _gameController.username.text, + _gameController.password.text, + host, + _gameController.customLaunchArgs.text + ); + var gameProcess = await Process.start( + gamePath, + gameArgs + ); gameProcess - ..exitCode.then((_) => _onEnd()) + ..exitCode.then((_) => _onStop(reason: _StopReason.normal)) ..outLines.forEach((line) => _onGameOutput(line, host)) ..errLines.forEach((line) => _onGameOutput(line, host)); return gameProcess.pid; } - String get _safeUsername { - if (_gameController.username.text.isEmpty) { - return kDefaultPlayerName; - } - - var username = _gameController.username.text; - if(_gameController.password.text.isNotEmpty){ - return username; - } - - username = _gameController.username.text.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim(); - if(username.isEmpty){ - return kDefaultPlayerName; - } - - return username; - } - Future _createLauncherProcess(FortniteVersion version) async { var launcherFile = version.launcher; if (launcherFile == null) { @@ -220,41 +220,57 @@ class _LaunchButtonState extends State { return pid; } - void _onEnd() { - if(_fail){ - return; - } - - _onStop(widget.host, false); - } - - void _closeLaunchingWidget(bool host, bool message) async { - if(!message) { - return; - } - - if(_fail) { - showInfoBar( - "An error occurred while starting the headless server", - severity: InfoBarSeverity.error + void _onGameOutput(String line, bool host) { + if (line.contains(shutdownLine)) { + _onStop( + reason: _StopReason.normal ); return; } - var theme = FluentTheme.of(context); + if(corruptedBuildErrors.any((element) => line.contains(element))){ + _onStop( + reason: _StopReason.corruptedVersionError + ); + return; + } + + if(cannotConnectErrors.any((element) => line.contains(element))){ + _onStop( + reason: _StopReason.tokenError + ); + return; + } + + if(line.contains("Region ")){ + if(!host){ + _injectOrShowError(_Injectable.console, host); + }else { + _injectOrShowError(_Injectable.reboot, host) + .then((value) => _onGameServerInjected()); + } + + _injectOrShowError(_Injectable.memoryFix, host); + var instance = host ? _hostingController.instance.value : _gameController.instance.value; + instance?.tokenError = false; + } + } + + Future _onGameServerInjected() async { + var theme = FluentTheme.of(appKey.currentContext!); showInfoBar( - "Waiting for the game server to boot up...", + translations.waitingForGameServer, loading: true, duration: null ); var gameServerPort = _settingsController.gameServerPort.text; var localPingResult = await pingGameServer( - "localhost:$gameServerPort", + "127.0.0.1:$gameServerPort", timeout: const Duration(minutes: 1) ); if(!localPingResult) { showInfoBar( - "The headless server was started successfully, but the game server didn't boot", + translations.gameServerStartWarning, severity: InfoBarSeverity.error, duration: snackbarLongDuration ); @@ -262,10 +278,10 @@ class _LaunchButtonState extends State { } _matchmakerController.joinLocalHost(); - var accessible = await _checkAccessible(theme, gameServerPort); + var accessible = await _checkGameServer(theme, gameServerPort); if(!accessible) { showInfoBar( - "The game server was started successfully, but other players can't join", + translations.gameServerStartLocalWarning, severity: InfoBarSeverity.warning, duration: snackbarLongDuration ); @@ -273,19 +289,19 @@ class _LaunchButtonState extends State { } await _hostingController.publishServer( - _gameController.username.text, - _hostingController.instance.value!.versionName, + _gameController.username.text, + _hostingController.instance.value!.versionName, ); showInfoBar( - "The game server was started successfully", + translations.gameServerStarted, severity: InfoBarSeverity.success, duration: snackbarLongDuration ); } - Future _checkAccessible(FluentThemeData theme, String gameServerPort) async { + Future _checkGameServer(FluentThemeData theme, String gameServerPort) async { showInfoBar( - "Checking if other players can join the game server...", + translations.checkingGameServer, loading: true, duration: null ); @@ -295,107 +311,35 @@ class _LaunchButtonState extends State { return true; } - var future = CancelableOperation.fromFuture(pingGameServer( + var future = pingGameServer( "$publicIp:$gameServerPort", timeout: const Duration(days: 365) - )); + ); showInfoBar( - Text.rich( - TextSpan( - children: [ - const TextSpan( - text: "Other players can't join the game server currently: please follow " - ), - TextSpan( - text: "this tutorial", - mouseCursor: SystemMouseCursors.click, - style: TextStyle( - color: theme.accentColor.dark - ), - recognizer: TapGestureRecognizer()..onTap = openPortTutorial - ), - const TextSpan( - text: " to fix this problem" - ), - ] - ) - ), + translations.checkGameServerFixMessage(gameServerPort), action: Button( - onPressed: () { - future.cancel(); - removeMessage(1); - }, - child: const Text("Ignore"), + onPressed: openPortTutorial, + child: Text(translations.checkGameServerFixAction), ), severity: InfoBarSeverity.warning, duration: null, loading: true ); - return await future.valueOrCancellation() ?? false; + return await future; } - void _onGameOutput(String line, bool host) { - _logFile.createSync(recursive: true); - _logFile.writeAsString("$line\n", mode: FileMode.append); - if (line.contains(shutdownLine)) { - _onStop(host, false); - return; - } - - if(corruptedBuildErrors.any((element) => line.contains(element))){ - if(_fail){ - return; - } - - _fail = true; - showCorruptedBuildError(host); - _onStop(host, false); - return; - } - - if(cannotConnectErrors.any((element) => line.contains(element))){ - if(_fail){ - return; - } - - _showTokenError(host); - return; - } - - if(line.contains("Region ")){ - if(!host){ - _injectOrShowError(Injectable.console, host); - }else { - _injectOrShowError(Injectable.reboot, host) - .then((value) => _closeLaunchingWidget(host, true)); - } - - _injectOrShowError(Injectable.memoryFix, host); - var instance = host ? _hostingController.instance.value : _gameController.instance.value; - instance?.tokenError = false; - } - } - - Future _showTokenError(bool host) async { - _fail = true; - var instance = host ? _hostingController.instance.value : _gameController.instance.value; - if(_authenticatorController.type() != ServerType.embedded) { - showTokenErrorUnfixable(); - instance?.tokenError = true; - return; - } - - await _authenticatorController.restartInteractive(); - showTokenErrorFixable(); - _onStop(host, false); - _start(); - } - - void _onStop(bool host, bool showMessage) async { + void _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async { + host = host ?? widget.host; + await _operation?.cancel(); + await _authenticatorController.worker?.cancel(); + await _matchmakerController.worker?.cancel(); var instance = host ? _hostingController.instance.value : _gameController.instance.value; if(instance != null){ if(instance.linkedHosting){ - _onStop(true, showMessage); + _onStop( + reason: _StopReason.normal, + host: true + ); } instance.kill(); @@ -407,21 +351,70 @@ class _LaunchButtonState extends State { } _setStarted(host, false); - if(host){ - await _removeHostEntry(); + _hostingController.discardServer(); } - _closeLaunchingWidget(host, showMessage); + messenger.removeMessageByPage(_pageType.index); + switch(reason) { + case _StopReason.authenticatorError: + case _StopReason.matchmakerError: + case _StopReason.normal: + break; + case _StopReason.missingVersionError: + showInfoBar( + translations.missingVersionError, + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + case _StopReason.missingExecutableError: + showInfoBar( + translations.missingExecutableError, + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + case _StopReason.corruptedVersionError: + showInfoBar( + translations.corruptedVersionError, + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + case _StopReason.missingDllError: + showInfoBar( + translations.missingDllError(error!), + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + case _StopReason.corruptedDllError: + showInfoBar( + translations.corruptedDllError(error!), + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + case _StopReason.tokenError: + showInfoBar( + translations.tokenError, + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + case _StopReason.unknownError: + showInfoBar( + translations.unknownFortniteError(error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: snackbarLongDuration, + ); + break; + } + _operation = null; } - Future _removeHostEntry() async { - await _supabase.from('hosts') - .delete() - .match({'id': _hostingController.uuid}); - } - - Future _injectOrShowError(Injectable injectable, bool hosting) async { + Future _injectOrShowError(_Injectable injectable, bool hosting) async { var instance = hosting ? _hostingController.instance.value : _gameController.instance.value; if (instance == null) { return; @@ -429,51 +422,82 @@ class _LaunchButtonState extends State { try { var gameProcess = instance.gamePid; - var dllPath = await _getDllPath(injectable, hosting); + var dllPath = await _getDllFileOrStop(injectable, hosting); if(dllPath == null) { return; } await injectDll(gameProcess, dllPath.path); - } catch (exception) { - showInfoBar("Cannot inject $injectable.dll: $exception"); - _onStop(hosting, false); + } catch (error, stackTrace) { + _onStop( + reason: _StopReason.corruptedDllError, + host: hosting, + error: error.toString(), + stackTrace: stackTrace + ); } } - Future _getDllPath(Injectable injectable, bool hosting) async { - Future getPath(Injectable injectable) async { - switch(injectable){ - case Injectable.reboot: - return File(_settingsController.gameServerDll.text); - case Injectable.console: - return File(_settingsController.unrealEngineConsoleDll.text); - case Injectable.sslBypass: - return File(_settingsController.authenticatorDll.text); - case Injectable.memoryFix: - return File("${assetsDirectory.path}\\dlls\\memoryleak.dll"); - } + String _getDllPath(_Injectable injectable) { + switch(injectable){ + case _Injectable.reboot: + return _settingsController.gameServerDll.text; + case _Injectable.console: + return _settingsController.unrealEngineConsoleDll.text; + case _Injectable.sslBypass: + return _settingsController.authenticatorDll.text; + case _Injectable.memoryFix: + return _settingsController.memoryLeakDll.text; + } + } + + Future _getDllFileOrStop(_Injectable injectable, bool host) async { + var path = _getDllPath(injectable); + var file = File(path); + if(await file.exists()) { + return file; } - var dllPath = await getPath(injectable); - if(dllPath.existsSync()) { - return dllPath; - } - - _onDllFail(dllPath, hosting); + _onStop( + reason: _StopReason.missingDllError, + host: host, + error: path + ); return null; } - void _onDllFail(File dllPath, bool hosting) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _fail = true; - showMissingDllError(path.basename(dllPath.path)); - _onStop(hosting, false); - }); - } + OverlayEntry _showLaunchingGameServerWidget() => showInfoBar( + translations.launchingHeadlessServer, + loading: true, + duration: null + ); + + OverlayEntry showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) => messenger.showInfoBar( + text, + pageType: _pageType, + severity: severity, + loading: loading, + duration: duration, + action: action + ); + + RebootPageType get _pageType => widget.host ? RebootPageType.host : RebootPageType.play; } -enum Injectable { +enum _StopReason { + normal, + missingVersionError, + missingExecutableError, + corruptedVersionError, + missingDllError, + corruptedDllError, + authenticatorError, + matchmakerError, + tokenError, + unknownError +} + +enum _Injectable { console, sslBypass, reboot, diff --git a/gui/lib/src/widget/home/pane.dart b/gui/lib/src/widget/home/pane.dart deleted file mode 100644 index 0d0d7e0..0000000 --- a/gui/lib/src/widget/home/pane.dart +++ /dev/null @@ -1,338 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class RebootPaneItem extends PaneItem { - RebootPaneItem({required super.title, required super.icon, required super.body}); - - @override - Widget build( - BuildContext context, - bool selected, - VoidCallback? onPressed, { - PaneDisplayMode? displayMode, - bool showTextOnTop = true, - int? itemIndex, - bool? autofocus, - }) { - final maybeBody = _InheritedNavigationView.maybeOf(context); - final mode = displayMode ?? - maybeBody?.displayMode ?? - maybeBody?.pane?.displayMode ?? - PaneDisplayMode.minimal; - assert(mode != PaneDisplayMode.auto); - assert(debugCheckHasFluentTheme(context)); - - final isTransitioning = maybeBody?.isTransitioning ?? false; - - final theme = NavigationPaneTheme.of(context); - final titleText = title?.getProperty() ?? ''; - - final baseStyle = title?.getProperty() ?? const TextStyle(); - - final isTop = mode == PaneDisplayMode.top; - final isMinimal = mode == PaneDisplayMode.minimal; - final isCompact = mode == PaneDisplayMode.compact; - - final onItemTapped = - (onPressed == null && onTap == null) || !enabled || isTransitioning - ? null - : () { - onPressed?.call(); - onTap?.call(); - }; - - final button = HoverButton( - autofocus: autofocus ?? this.autofocus, - focusNode: focusNode, - onPressed: onItemTapped, - cursor: mouseCursor, - focusEnabled: isMinimal ? (maybeBody?.minimalPaneOpen ?? false) : true, - forceEnabled: enabled, - builder: (context, states) { - var textStyle = () { - var style = !isTop - ? (selected - ? theme.selectedTextStyle?.resolve(states) - : theme.unselectedTextStyle?.resolve(states)) - : (selected - ? theme.selectedTopTextStyle?.resolve(states) - : theme.unselectedTopTextStyle?.resolve(states)); - if (style == null) return baseStyle; - return style.merge(baseStyle); - }(); - - final textResult = titleText.isNotEmpty - ? Padding( - padding: theme.labelPadding ?? EdgeInsets.zero, - child: RichText( - text: title!.getProperty(textStyle)!, - maxLines: 1, - overflow: TextOverflow.fade, - softWrap: false, - textAlign: title?.getProperty() ?? TextAlign.start, - textHeightBehavior: title?.getProperty(), - textWidthBasis: title?.getProperty() ?? - TextWidthBasis.parent, - ), - ) - : const SizedBox.shrink(); - Widget result() { - final iconThemeData = IconThemeData( - color: textStyle.color ?? - (selected - ? theme.selectedIconColor?.resolve(states) - : theme.unselectedIconColor?.resolve(states)), - size: textStyle.fontSize ?? 16.0, - ); - switch (mode) { - case PaneDisplayMode.compact: - return Container( - key: itemKey, - constraints: const BoxConstraints( - minHeight: kPaneItemMinHeight, - ), - alignment: AlignmentDirectional.center, - child: Padding( - padding: theme.iconPadding ?? EdgeInsets.zero, - child: IconTheme.merge( - data: iconThemeData, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: () { - if (infoBadge != null) { - return Stack( - alignment: AlignmentDirectional.center, - clipBehavior: Clip.none, - children: [ - icon, - PositionedDirectional( - end: -8, - top: -8, - child: infoBadge!, - ), - ], - ); - } - return icon; - }(), - ), - ), - ), - ); - case PaneDisplayMode.minimal: - case PaneDisplayMode.open: - final shouldShowTrailing = !isTransitioning; - - return ConstrainedBox( - key: itemKey, - constraints: const BoxConstraints( - minHeight: kPaneItemMinHeight, - ), - child: Row(children: [ - Padding( - padding: theme.iconPadding ?? EdgeInsets.zero, - child: IconTheme.merge( - data: iconThemeData, - child: Center(child: icon), - ), - ), - Expanded(child: textResult), - if (shouldShowTrailing) ...[ - if (infoBadge != null) - Padding( - padding: const EdgeInsetsDirectional.only(end: 8.0), - child: infoBadge!, - ), - if (trailing != null) - IconTheme.merge( - data: const IconThemeData(size: 16.0), - child: trailing!, - ), - ], - ]), - ); - case PaneDisplayMode.top: - Widget result = Row(mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: theme.iconPadding ?? EdgeInsets.zero, - child: IconTheme.merge( - data: iconThemeData, - child: Center(child: icon), - ), - ), - if (showTextOnTop) textResult, - if (trailing != null) - IconTheme.merge( - data: const IconThemeData(size: 16.0), - child: trailing!, - ), - ]); - if (infoBadge != null) { - return Stack(key: itemKey, clipBehavior: Clip.none, children: [ - result, - if (infoBadge != null) - PositionedDirectional( - end: -3, - top: 3, - child: infoBadge!, - ), - ]); - } - return KeyedSubtree(key: itemKey, child: result); - default: - throw '$mode is not a supported type'; - } - } - - return Semantics( - label: titleText.isEmpty ? null : titleText, - selected: selected, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 6.0), - decoration: BoxDecoration( - color: () { - final tileColor = this.tileColor ?? - theme.tileColor ?? - kDefaultPaneItemColor(context, isTop); - final newStates = states.toSet()..remove(ButtonStates.disabled); - if (selected && selectedTileColor != null) { - return selectedTileColor!.resolve(newStates); - } - return tileColor.resolve( - selected - ? { - states.isHovering - ? ButtonStates.pressing - : ButtonStates.hovering, - } - : newStates, - ); - }(), - borderRadius: BorderRadius.circular(4.0), - ), - child: FocusBorder( - focused: states.isFocused, - renderOutside: false, - child: () { - final showTooltip = ((isTop && !showTextOnTop) || isCompact) && - titleText.isNotEmpty && - !states.isDisabled; - - if (showTooltip) { - return Tooltip( - richMessage: title?.getProperty(), - style: TooltipThemeData(textStyle: baseStyle), - child: result(), - ); - } - - return result(); - }(), - ), - ), - ); - }, - ); - - final index = () { - if (itemIndex != null) return itemIndex; - if (maybeBody?.pane?.indicator != null) { - return maybeBody!.pane!.effectiveIndexOf(this); - } - }(); - - return Padding( - key: key, - padding: const EdgeInsetsDirectional.symmetric(horizontal: 12.0, vertical: 2.0), - child: () { - if (maybeBody?.pane?.indicator != null && - index != null && - !index.isNegative) { - final key = PaneItemKeys.of(index, context); - - return Stack(children: [ - button, - Positioned.fill( - child: _InheritedNavigationView.merge( - currentItemIndex: index, - currentItemSelected: selected, - child: KeyedSubtree( - key: key, - child: maybeBody!.pane!.indicator!, - ), - ), - ), - ]); - } - - return button; - }(), - ); - } -} - -class _InheritedNavigationView extends InheritedWidget { - const _InheritedNavigationView({ - super.key, - required super.child, - required this.displayMode, - this.minimalPaneOpen = false, - this.pane, - this.previousItemIndex = 0, - this.currentItemIndex = -1, - this.isTransitioning = false, - }); - - final PaneDisplayMode displayMode; - - final bool minimalPaneOpen; - - final NavigationPane? pane; - - final int previousItemIndex; - - final int currentItemIndex; - - final bool isTransitioning; - - static _InheritedNavigationView? maybeOf(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType<_InheritedNavigationView>(); - } - - static Widget merge({ - Key? key, - required Widget child, - int? currentItemIndex, - NavigationPane? pane, - PaneDisplayMode? displayMode, - bool? minimalPaneOpen, - int? previousItemIndex, - bool? currentItemSelected, - bool? isTransitioning, - }) { - return Builder(builder: (context) { - final current = _InheritedNavigationView.maybeOf(context); - return _InheritedNavigationView( - key: key, - displayMode: - displayMode ?? current?.displayMode ?? PaneDisplayMode.open, - minimalPaneOpen: minimalPaneOpen ?? current?.minimalPaneOpen ?? false, - currentItemIndex: currentItemIndex ?? current?.currentItemIndex ?? -1, - pane: pane ?? current?.pane, - previousItemIndex: previousItemIndex ?? current?.previousItemIndex ?? 0, - isTransitioning: isTransitioning ?? current?.isTransitioning ?? false, - child: child, - ); - }); - } - - @override - bool updateShouldNotify(covariant _InheritedNavigationView oldWidget) { - return oldWidget.displayMode != displayMode || - oldWidget.minimalPaneOpen != minimalPaneOpen || - oldWidget.pane != pane || - oldWidget.previousItemIndex != previousItemIndex || - oldWidget.currentItemIndex != currentItemIndex || - oldWidget.isTransitioning != isTransitioning; - } -} diff --git a/gui/lib/src/widget/os/border.dart b/gui/lib/src/widget/os/border.dart deleted file mode 100644 index 98ab9ac..0000000 --- a/gui/lib/src/widget/os/border.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:reboot_common/common.dart'; -import 'package:system_theme/system_theme.dart'; - -class WindowBorder extends StatelessWidget { - const WindowBorder({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return IgnorePointer( - child: Padding( - padding: const EdgeInsets.only( - top: 1 - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: SystemTheme.accentColor.accent, - width: appBarWidth.toDouble() - ) - ) - ), - ) - ); - } -} diff --git a/gui/lib/src/widget/os/mouse.dart b/gui/lib/src/widget/os/mouse.dart index 167785e..21f680d 100644 --- a/gui/lib/src/widget/os/mouse.dart +++ b/gui/lib/src/widget/os/mouse.dart @@ -4,15 +4,10 @@ typedef MouseStateBuilderCB = Widget Function( BuildContext context, MouseState mouseState); class MouseState { - bool isMouseOver = false; - bool isMouseDown = false; + bool isMouseOver; + bool isMouseDown; - MouseState(); - - @override - String toString() { - return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver"; - } + MouseState() : isMouseOver = false, isMouseDown = false; } class MouseStateBuilder extends StatefulWidget { diff --git a/gui/lib/src/widget/server/start_button.dart b/gui/lib/src/widget/server/start_button.dart index f882cc9..aa07164 100644 --- a/gui/lib/src/widget/server/start_button.dart +++ b/gui/lib/src/widget/server/start_button.dart @@ -5,6 +5,8 @@ import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class ServerButton extends StatefulWidget { final bool authenticator; @@ -29,7 +31,7 @@ class _ServerButtonState extends State { alignment: Alignment.center, child: Text(_buttonText), ), - onPressed: () => _controller.toggleInteractive() + onPressed: () => _controller.toggleInteractive(widget.authenticator ? RebootPageType.authenticator : RebootPageType.matchmaker) ), )), ), @@ -37,13 +39,13 @@ class _ServerButtonState extends State { String get _buttonText { if(_controller.type.value == ServerType.local){ - return "Check ${_controller.controllerName}"; + return translations.checkServer(_controller.controllerName); } if(_controller.started.value){ - return "Stop ${_controller.controllerName}"; + return translations.stopServer(_controller.controllerName); } - return "Start ${_controller.controllerName}"; + return translations.startServer(_controller.controllerName); } } diff --git a/gui/lib/src/widget/server/type_selector.dart b/gui/lib/src/widget/server/type_selector.dart index 36e82cd..32df4ac 100644 --- a/gui/lib/src/widget/server/type_selector.dart +++ b/gui/lib/src/widget/server/type_selector.dart @@ -1,9 +1,10 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; -import 'package:reboot_common/src/model/server_type.dart'; +import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/authenticator_controller.dart'; import 'package:reboot_launcher/src/controller/matchmaker_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class ServerTypeSelector extends StatefulWidget { final bool authenticator; @@ -30,10 +31,7 @@ class _ServerTypeSelectorState extends State { MenuFlyoutItem _createItem(ServerType type) { return MenuFlyoutItem( - text: Tooltip( - message: type.message, - child: Text(type.label) - ), + text: Text(type.label), onPressed: () async { _controller.stop(); _controller.type.value = type; @@ -44,14 +42,8 @@ class _ServerTypeSelectorState extends State { extension ServerTypeExtension on ServerType { String get label { - return this == ServerType.embedded ? "Embedded" - : this == ServerType.remote ? "Remote" - : "Local"; - } - - String get message { - return this == ServerType.embedded ? "A server will be automatically started in the background" - : this == ServerType.remote ? "A reverse proxy to the remote server will be created" - : "Assumes that you are running yourself the server locally"; + return this == ServerType.embedded ? translations.embedded + : this == ServerType.remote ? translations.remote + : translations.local; } } diff --git a/gui/lib/src/widget/version/add_local_version.dart b/gui/lib/src/widget/version/add_local_version.dart index 6a876f5..a3da700 100644 --- a/gui/lib/src/widget/version/add_local_version.dart +++ b/gui/lib/src/widget/version/add_local_version.dart @@ -8,6 +8,7 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/common/file_selector.dart'; import 'package:reboot_launcher/src/widget/version/version_name_input.dart'; @@ -46,10 +47,10 @@ class _AddLocalVersionState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( + SizedBox( width: double.infinity, child: InfoBar( - title: Text("Local builds are not guaranteed to work"), + title: Text(translations.localBuildsWarning), severity: InfoBarSeverity.info ), ), @@ -67,9 +68,9 @@ class _AddLocalVersionState extends State { ), FileSelector( - label: "Game folder", - placeholder: "Type the game folder", - windowTitle: "Select game folder", + label: translations.gameFolderTitle, + placeholder: translations.gameFolderPlaceholder, + windowTitle: translations.gameFolderPlaceWindowTitle, controller: _gamePathController, validator: checkGameFolder, folder: true @@ -86,7 +87,7 @@ class _AddLocalVersionState extends State { ), DialogButton( - text: "Save", + text: translations.saveLocalVersion, type: ButtonType.primary, onTap: () { Navigator.of(context).pop(); diff --git a/gui/lib/src/widget/version/add_server_version.dart b/gui/lib/src/widget/version/add_server_version.dart index ac0b1b6..16aaa0b 100644 --- a/gui/lib/src/widget/version/add_server_version.dart +++ b/gui/lib/src/widget/version/add_server_version.dart @@ -8,14 +8,15 @@ import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; +import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/common/file_selector.dart'; import 'package:reboot_launcher/src/widget/version/version_build_selector.dart'; import 'package:reboot_launcher/src/widget/version/version_name_input.dart'; import 'package:universal_disk_space/universal_disk_space.dart'; - -import '../../dialog/abstract/dialog.dart'; -import '../../dialog/abstract/dialog_button.dart'; +import 'package:windows_taskbar/windows_taskbar.dart'; class AddServerVersion extends StatefulWidget { const AddServerVersion({Key? key}) : super(key: key); @@ -32,7 +33,7 @@ class _AddServerVersionState extends State { final Rx _status = Rx(DownloadStatus.form); final GlobalKey _formKey = GlobalKey(); final RxnInt _timeLeft = RxnInt(); - final Rxn _downloadProgress = Rxn(); + final Rxn _progress = Rxn(); late DiskSpace _diskSpace; late Future _fetchFuture; @@ -82,7 +83,7 @@ class _AddServerVersionState extends State { if (!snapshot.hasData) { return ProgressDialog( - text: "Fetching builds and disks...", + text: translations.fetchingBuilds, onStop: () => Navigator.of(context).pop() ); } @@ -94,24 +95,20 @@ class _AddServerVersionState extends State { } ); case DownloadStatus.downloading: - return GenericDialog( - header: _downloadBody, - buttons: _stopButton - ); case DownloadStatus.extracting: return GenericDialog( - header: _extractingBody, + header: _progressBody, buttons: _stopButton ); case DownloadStatus.error: return ErrorDialog( - exception: _error ?? Exception("unknown error"), + exception: _error ?? Exception(translations.unknownError), stackTrace: _stackTrace, - errorMessageBuilder: (exception) => "Cannot download version: $exception" + errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString()) ); case DownloadStatus.done: - return const InfoDialog( - text: "The download was completed successfully!", + return InfoDialog( + text: translations.downloadedVersion ); } }) @@ -120,7 +117,7 @@ class _AddServerVersionState extends State { List get _formButtons => [ DialogButton(type: ButtonType.secondary), DialogButton( - text: "Download", + text: translations.download, type: ButtonType.primary, onTap: () => _startDownload(context), ) @@ -137,11 +134,11 @@ class _AddServerVersionState extends State { var communicationPort = ReceivePort(); communicationPort.listen((message) { if(message is ArchiveDownloadProgress) { - _onDownloadProgress(message.progress, message.minutesLeft, message.extracting); + _onProgress(message.progress, message.minutesLeft, message.extracting); }else if(message is SendPort) { _downloadPort = message; }else { - _onDownloadError("Unexpected message: $message", null); + _onDownloadError(message, null); } }); var options = ArchiveDownloadOptions( @@ -151,20 +148,12 @@ class _AddServerVersionState extends State { ); var errorPort = ReceivePort(); errorPort.listen((message) => _onDownloadError(message, null)); - var exitPort = ReceivePort(); - var isolate = await Isolate.spawn( + await Isolate.spawn( downloadArchiveBuild, options, onError: errorPort.sendPort, - onExit: exitPort.sendPort, errorsAreFatal: true ); - exitPort.listen((message) { - isolate.kill(priority: Isolate.immediate); - if(_status.value != DownloadStatus.error) { - _onDownloadComplete(); - } - }); } catch (exception, stackTrace) { _onDownloadError(exception, stackTrace); } @@ -176,6 +165,7 @@ class _AddServerVersionState extends State { } _status.value = DownloadStatus.done; + WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( name: _nameController.text, location: Directory(_pathController.text) @@ -188,21 +178,31 @@ class _AddServerVersionState extends State { } _status.value = DownloadStatus.error; + WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); _error = error; _stackTrace = stackTrace; } - void _onDownloadProgress(double progress, int timeLeft, bool extracting) { + void _onProgress(double progress, int? timeLeft, bool extracting) { if (!mounted) { return; } + if(progress >= 100 && extracting) { + _onDownloadComplete(); + return; + } + _status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading; + if(progress >= 0) { + WindowsTaskbar.setProgress(progress.round(), 100); + } + _timeLeft.value = timeLeft; - _downloadProgress.value = progress; + _progress.value = progress; } - Widget get _downloadBody { + Widget get _progressBody { var timeLeft = _timeLeft.value; return Column( mainAxisSize: MainAxisSize.min, @@ -210,7 +210,7 @@ class _AddServerVersionState extends State { Align( alignment: Alignment.centerLeft, child: Text( - "Downloading...", + _status.value == DownloadStatus.downloading ? translations.downloading : translations.extracting, style: FluentTheme.maybeOf(context)?.typography.body, textAlign: TextAlign.start, ), @@ -224,13 +224,13 @@ class _AddServerVersionState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${(_downloadProgress.value ?? 0).round()}%", + translations.buildProgress((_progress.value ?? 0).round()), style: FluentTheme.maybeOf(context)?.typography.body, ), if(timeLeft != null) Text( - "Time left: ${timeLeft == 0 ? "less than a minute" : "about $timeLeft minute${timeLeft > 1 ? 's' : ''}"}", + translations.timeLeft(timeLeft), style: FluentTheme.maybeOf(context)?.typography.body, ) ], @@ -242,7 +242,7 @@ class _AddServerVersionState extends State { SizedBox( width: double.infinity, - child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble()) + child: ProgressBar(value: (_progress.value ?? 0).toDouble()) ), const SizedBox( @@ -252,33 +252,6 @@ class _AddServerVersionState extends State { ); } - Widget get _extractingBody => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "Extracting...", - style: FluentTheme.maybeOf(context)?.typography.body, - textAlign: TextAlign.start, - ), - ), - - const SizedBox( - height: 8.0, - ), - - const SizedBox( - width: double.infinity, - child: ProgressBar() - ), - - const SizedBox( - height: 8.0, - ) - ], - ); - Widget get _formBody => Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -300,9 +273,9 @@ class _AddServerVersionState extends State { ), FileSelector( - label: "Installation directory", - placeholder: "Type the installation directory", - windowTitle: "Select installation directory", + label: translations.buildInstallationDirectory, + placeholder: translations.buildInstallationDirectoryPlaceholder, + windowTitle: translations.buildInstallationDirectoryWindowTitle, controller: _pathController, validator: checkDownloadDestination, folder: true diff --git a/gui/lib/src/widget/version/version_build_selector.dart b/gui/lib/src/widget/version/version_build_selector.dart index 3a9a23b..fc6696d 100644 --- a/gui/lib/src/widget/version/version_build_selector.dart +++ b/gui/lib/src/widget/version/version_build_selector.dart @@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class BuildSelector extends StatefulWidget { final Function() onSelected; @@ -18,11 +19,11 @@ class _BuildSelectorState extends State { @override Widget build(BuildContext context) { return InfoLabel( - label: "Build", + label: translations.build, child: Obx(() => ComboBox( - placeholder: const Text('Select a fortnite build'), + placeholder: Text(translations.selectBuild), isExpanded: true, - items: _createItems(), + items: _items, value: _buildController.selectedBuild.value, onChanged: (value) { if(value == null){ @@ -36,13 +37,11 @@ class _BuildSelectorState extends State { ); } - List> _createItems() { - return _buildController.builds! - .map((element) => _createItem(element)) - .toList(); - } + List> get _items =>_buildController.builds! + .map((element) => _buildItem(element)) + .toList(); - ComboBoxItem _createItem(FortniteBuild element) { + ComboBoxItem _buildItem(FortniteBuild element) { return ComboBoxItem( value: element, child: Text(element.version.toString()) diff --git a/gui/lib/src/widget/version/version_name_input.dart b/gui/lib/src/widget/version/version_name_input.dart index 521005f..4fd5738 100644 --- a/gui/lib/src/widget/version/version_name_input.dart +++ b/gui/lib/src/widget/version/version_name_input.dart @@ -1,6 +1,8 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; class VersionNameInput extends StatelessWidget { final GameController _gameController = Get.find(); @@ -10,25 +12,13 @@ class VersionNameInput extends StatelessWidget { @override Widget build(BuildContext context) => InfoLabel( - label: "Name", + label: translations.versionName, child: TextFormBox( controller: controller, - placeholder: "Type the version's name", + placeholder: translations.versionNameLabel, autofocus: true, - validator: _validate, + validator: (version) => checkVersion(version, _gameController.versions.value), autovalidateMode: AutovalidateMode.onUserInteraction ), ); - - String? _validate(String? text) { - if (text == null || text.isEmpty) { - return 'Empty version name'; - } - - if (_gameController.versions.value.any((element) => element.name == text)) { - return 'This version already exists'; - } - - return null; - } } diff --git a/gui/lib/src/widget/version/version_selector.dart b/gui/lib/src/widget/version/version_selector.dart index 5821af7..91ef0eb 100644 --- a/gui/lib/src/widget/version/version_selector.dart +++ b/gui/lib/src/widget/version/version_selector.dart @@ -10,6 +10,7 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/util/checks.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/common/file_selector.dart'; import 'package:reboot_launcher/src/widget/version/add_local_version.dart'; import 'package:reboot_launcher/src/widget/version/add_server_version.dart'; @@ -42,7 +43,7 @@ class _VersionSelectorState extends State { child: FlyoutTarget( controller: _flyoutController, child: DropDownButton( - leading: Text(_gameController.selectedVersion?.name ?? "Select a version"), + leading: Text(_gameController.selectedVersion?.name ?? translations.selectVersion), items: _createSelectorItems(context) ), ) @@ -54,7 +55,7 @@ class _VersionSelectorState extends State { .toList(); MenuFlyoutItem _createDefaultVersionItem() => MenuFlyoutItem( - text: const Text("Please create or download a version"), + text: Text(translations.noVersions), onPressed: () {} ); @@ -147,7 +148,7 @@ class _VersionSelectorState extends State { } bool _onExplorerError() { - showInfoBar("This version doesn't exist on the local machine"); + showInfoBar(translations.missingVersion); return false; } @@ -159,27 +160,28 @@ class _VersionSelectorState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( + SizedBox( width: double.infinity, - child: Text("Are you sure you want to delete this version?")), + child: Text(translations.deleteVersionDialogTitle) + ), const SizedBox(height: 12.0), Obx(() => Checkbox( checked: _deleteFilesController.value, onChanged: (bool? value) => _deleteFilesController.value = value ?? false, - content: const Text("Delete version files from disk") + content: Text(translations.deleteVersionFromDiskOption) )) ], ), actions: [ Button( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Keep'), + child: Text(translations.deleteVersionCancel), ), - FilledButton( + Button( onPressed: () => Navigator.of(context).pop(true), - child: const Text('Delete'), + child: Text(translations.deleteVersionConfirm), ) ], ) @@ -197,10 +199,10 @@ class _VersionSelectorState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoLabel( - label: "Name", + label: translations.versionName, child: TextFormBox( controller: nameController, - placeholder: "Type the new version name", + placeholder: translations.newVersionNameLabel, autofocus: true, validator: (text) => checkChangeVersion(text) ) @@ -211,9 +213,9 @@ class _VersionSelectorState extends State { ), FileSelector( - placeholder: "Type the new game folder", - windowTitle: "Select game folder", - label: "Path", + placeholder: translations.newVersionNameLabel, + windowTitle: translations.gameFolderPlaceWindowTitle, + label: translations.gameFolderLabel, controller: pathController, validator: checkGameFolder, folder: true @@ -228,7 +230,7 @@ class _VersionSelectorState extends State { ), DialogButton( - text: "Save", + text: translations.newVersionNameConfirm, type: ButtonType.primary, onTap: () { Navigator.of(context).pop(); @@ -252,8 +254,8 @@ enum _ContextualOption { extension _ContextualOptionExtension on _ContextualOption { String get name { - return this == _ContextualOption.openExplorer ? "Open in explorer" - : this == _ContextualOption.modify ? "Modify" - : "Delete"; + return this == _ContextualOption.openExplorer ? translations.openInExplorer + : this == _ContextualOption.modify ? translations.modify + : translations.delete; } } diff --git a/gui/lib/src/widget/version/version_selector_tile.dart b/gui/lib/src/widget/version/version_selector_tile.dart new file mode 100644 index 0000000..6d9b05e --- /dev/null +++ b/gui/lib/src/widget/version/version_selector_tile.dart @@ -0,0 +1,48 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:reboot_launcher/src/page/abstract/page_setting.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/common/setting_tile.dart'; +import 'package:reboot_launcher/src/widget/version/version_selector.dart'; + +SettingTile get versionSelectorSettingTile => SettingTile( + title: translations.addVersionName, + subtitle: translations.addVersionDescription, + content: const VersionSelector(), + expandedContent: [ + SettingTile( + title: translations.addLocalBuildName, + subtitle: translations.addLocalBuildDescription, + content: Button( + onPressed: VersionSelector.openAddDialog, + child: Text(translations.addLocalBuildContent) + ), + isChild: true + ), + SettingTile( + title: translations.downloadBuildName, + subtitle: translations.downloadBuildDescription, + content: Button( + onPressed: VersionSelector.openDownloadDialog, + child: Text(translations.downloadBuildContent) + ), + isChild: true + ) + ] +); + +PageSetting get versionSelectorRebootSetting => PageSetting( + name: translations.addVersionName, + description: translations.addVersionDescription, + children: [ + PageSetting( + name: translations.addLocalBuildName, + description: translations.addLocalBuildDescription, + content: translations.addLocalBuildContent + ), + PageSetting( + name: translations.downloadBuildName, + description: translations.downloadBuildDescription, + content: translations.downloadBuildContent + ) + ] +); \ No newline at end of file diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index e33d3dd..86e4487 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -42,6 +42,9 @@ dependencies: auto_animated_list: ^1.0.4 app_links: ^3.4.3 url_protocol: ^1.0.0 + intl: any + windows_taskbar: ^1.1.2 + flutter_localized_locales: ^2.0.5 dependency_overrides: xml: ^6.3.0 @@ -59,6 +62,7 @@ dev_dependencies: flutter: uses-material-design: true + generate: true assets: - assets/misc/ - assets/dlls/ diff --git a/gui/windows/flutter/generated_plugin_registrant.cc b/gui/windows/flutter/generated_plugin_registrant.cc index 0418bb6..281077a 100644 --- a/gui/windows/flutter/generated_plugin_registrant.cc +++ b/gui/windows/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( @@ -29,4 +30,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); + WindowsTaskbarPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowsTaskbarPlugin")); } diff --git a/gui/windows/flutter/generated_plugins.cmake b/gui/windows/flutter/generated_plugins.cmake index 65e1e6c..9e8d035 100644 --- a/gui/windows/flutter/generated_plugins.cmake +++ b/gui/windows/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST system_theme url_launcher_windows window_manager + windows_taskbar ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/gui/windows/runner/main.cpp b/gui/windows/runner/main.cpp index 5927a20..1dc80f9 100644 --- a/gui/windows/runner/main.cpp +++ b/gui/windows/runner/main.cpp @@ -1,5 +1,5 @@ #include -auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME); +auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); #include @@ -16,13 +16,9 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME); #include #include -bool CheckOneInstance(){ +bool IsAlreadyOpen(){ HANDLE hMutex = CreateMutexW(NULL, TRUE, L"RebootLauncherMutex"); - if (hMutex == NULL) { - return false; - } - - if (GetLastError() == ERROR_ALREADY_EXISTS) { + if (hMutex == NULL && GetLastError() == ERROR_ALREADY_EXISTS) { HWND hwndExisting = FindWindowW(NULL, L"Reboot Launcher"); if (hwndExisting != NULL) { ShowWindow(hwndExisting, SW_RESTORE); @@ -30,10 +26,10 @@ bool CheckOneInstance(){ } CloseHandle(hMutex); - return false; + return true; } - return true; + return false; } constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; @@ -75,12 +71,11 @@ bool SendAppLinkToInstance(const std::wstring& title) { int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { - _putenv_s("OPENSSL_ia32cap", "~0x20000000"); if(SendAppLinkToInstance(L"Reboot Launcher")) { return EXIT_SUCCESS; } - if(!CheckOneInstance()){ + if(!IsDebuggerPresent() && IsAlreadyOpen()){ return EXIT_SUCCESS; } diff --git a/gui/windows/runner/win32_window.cpp b/gui/windows/runner/win32_window.cpp index 23ee5ec..0dcd6a1 100644 --- a/gui/windows/runner/win32_window.cpp +++ b/gui/windows/runner/win32_window.cpp @@ -121,7 +121,7 @@ bool Win32Window::CreateAndShow(const std::wstring &title, HWND window = CreateWindow( window_class, title.c_str(), - WS_OVERLAPPED | WS_THICKFRAME & ~WS_VISIBLE, + WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor),