Final version

This commit is contained in:
Alessandro Autiero
2023-09-21 16:48:31 +02:00
parent 4bba21c038
commit 73c1cc8526
90 changed files with 3204 additions and 2608 deletions

View File

@@ -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';

View File

@@ -1,2 +1,2 @@
const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co';
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M';
const String supabaseUrl = 'https://pocjparoguvaeeyjapjb.supabase.co';
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBvY2pwYXJvZ3V2YWVleWphcGpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTUzMTM4NTUsImV4cCI6MjAxMDg4OTg1NX0.BffJtbQvX1NVUy-9Nj4GVzUJXPK_1GyezDE0V5MRiao';

View File

@@ -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);
}

View File

@@ -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<List<FortniteBuild>> fetchBuilds(ignored) async {
var response = await http.get(_manifestSourceUrl);
var response = await _dio.get<String>(
_manifestSourceUrl,
options: Options(
responseType: ResponseType.plain
)
);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
}
var results = <FortniteBuild>[];
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<void> 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<void> _download(ArchiveDownloadOptions options, File tempFile, Completer<dynamic> 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<Response> _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<void> _extract(Completer<dynamic> 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<void> _extract(Completer<dynamic> 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<dynamic> _setupLifecycle(ArchiveDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
@@ -134,19 +188,3 @@ Completer<dynamic> _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);
}

View File

@@ -13,10 +13,9 @@ final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
List<String> 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<String> 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<int> downloadRebootDll(String url, int? lastUpdateMs, {int hours = 24, bool force = false}) async {
Directory? outputDir;

View File

@@ -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

16
gui/README.md Normal file
View File

@@ -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.

BIN
gui/assets/build/7zip.exe Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 774 B

View File

Before

Width:  |  Height:  |  Size: 1011 B

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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
}
}

3
gui/l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: reboot_en.arb
output-localization-file: reboot_localizations.dart

268
gui/lib/l10n/reboot_en.arb Normal file
View File

@@ -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"
}

251
gui/lib/l10n/reboot_pl.arb Normal file
View File

@@ -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"
}

View File

@@ -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<Object?> errors) => errors.where((element) => element != null).forEach((element) => onError(element, null, false));
void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
}
Future<void> _checkGameServer() async {
try {
@@ -70,7 +74,7 @@ Future<void> _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<Object?> _initWindow() async {
try {
await windowManager.ensureInitialized();
await Window.initialize();
var settingsController = Get.find<SettingsController>();
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<SettingsController>();
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<RebootApplication> {
final SettingsController _settingsController = Get.find<SettingsController>();
@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,

View File

@@ -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<bool> freePort() => freeAuthenticatorPort();
@override
RebootPageType get pageType => RebootPageType.authenticator;
@override
Future<int> startEmbeddedInternal() => startEmbeddedAuthenticator(detached.value);

View File

@@ -14,7 +14,6 @@ class GameController extends GetxController {
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started;
late final RxBool autoStartGameServer;
late final Rxn<GameInstance> 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;
}

View File

@@ -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<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => _isValidServer(element)).toSet();
bool _isValidServer(Map<String, dynamic> element) =>
element["id"] != uuid && element["ip"] != null;
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet();
Future<void> 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;

View File

@@ -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<bool> freePort() => freeMatchmakerPort();
@override
RebootPageType get pageType => RebootPageType.matchmaker;
@override
Future<int> startEmbeddedInternal() => startEmbeddedMatchmaker(detached.value);

View File

@@ -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<bool> get isPortTaken async => !(await isPortFree);
RebootPageType get pageType;
Future<bool> freePort();
@protected
@@ -196,15 +200,6 @@ abstract class ServerController extends GetxController {
}
}
Stream<ServerResult> restart() async* {
await resetWinNat();
if(started()) {
yield* stop();
}
yield* start();
}
Stream<ServerResult> toggle() async* {
if(started()) {
yield* stop();

View File

@@ -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> 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(){

View File

@@ -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),
)
);
}

View File

@@ -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();
},
);

View File

@@ -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<DialogButton> {
Widget get _secondaryButton {
return Button(
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? "Close"),
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
);
}

View File

@@ -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<int, OverlayEntry?> _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

View File

@@ -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<void> 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();
},
)
],
)
);

View File

@@ -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())
)
));
}

View File

@@ -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<void> showMissingDllError(String name) async {
showAppDialog(
builder: (context) => InfoDialog(
text: "$name dll is not a valid dll, fix it in the settings tab"
)
);
}
Future<void> 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<void> 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<void> 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<void> 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."
)
);
}

View File

@@ -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<GameController>();
@@ -20,9 +21,9 @@ Future<bool> 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<bool> 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<bool> 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)
)
]
))

View File

@@ -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<bool> restartInteractive() async {
var stream = restart();
return await _handleStream(stream, false);
}
Future<bool> toggleInteractive([bool showSuccessMessage = true]) async {
Future<bool> toggleInteractive(RebootPageType caller, [bool showSuccessMessage = true]) async {
var stream = toggle();
return await _handleStream(stream, showSuccessMessage);
return await _handleStream(caller, stream, showSuccessMessage);
}
Future<bool> _handleStream(Stream<ServerResult> stream, bool showSuccessMessage) async {
Future<bool> _handleStream(RebootPageType caller, Stream<ServerResult> stream, bool showSuccessMessage) async {
var completer = Completer<bool>();
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
));

View File

@@ -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<PageSetting> get settings;
bool get hasButton;
@override
RebootPageState createState();
}
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
@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<Widget> get settings;
Widget? get button;
}

View File

@@ -0,0 +1,26 @@
class PageSetting {
final String name;
final String description;
final String? content;
final List<PageSetting>? 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";
}

View File

@@ -0,0 +1,9 @@
enum RebootPageType {
play,
host,
browser,
authenticator,
matchmaker,
info,
settings
}

View File

@@ -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<AuthenticatorPage> createState() => _AuthenticatorPageState();
}
class _AuthenticatorPageState extends State<AuthenticatorPage> with AutomaticKeepAliveClientMixin {
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
@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;
}

View File

@@ -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<BrowsePage> createState() => _BrowsePageState();
}
class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMixin {
final HostingController _hostingController = Get.find<HostingController>();
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
final TextEditingController _filterController = TextEditingController();
final StreamController<String> _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<String?>(
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<Map<String, dynamic>>? _getItems(Set<Map<String, dynamic>>? 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<String, dynamic> entry, String? filter) =>
filter == null || _filterServer(entry, filter);
bool _filterServer(Map<String, dynamic> 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<String, dynamic>? _getItem(int index, Set? data) {
if(data == null) {
return null;
}
if (index >= data.length) {
return null;
}
return data.elementAt(index);
}
String _formatName(Map<String, dynamic> entry) {
String result = entry['name'];
return result.isEmpty ? kDefaultServerName : result;
}
String _formatDescription(Map<String, dynamic> entry) {
String result = entry['description'];
return result.isEmpty ? kDefaultDescription : result;
}
String _formatVersion(Map<String, dynamic> 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;
}

View File

@@ -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<HostingPage> createState() => _HostingPageState();
}
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
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<void> _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();
}
}
}

View File

@@ -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<PageSetting> 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<AuthenticatorPage> createState() => _AuthenticatorPageState();
}
class _AuthenticatorPageState extends RebootPageState<AuthenticatorPage> {
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
@override
List<Widget> 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
),
],
))
)
],
));
}

View File

@@ -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<int> _pagesStack = Queue();
final List<GlobalKey> _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<HomePage> with WindowListener, AutomaticKeepA
final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController();
final RxBool _focused = RxBool(true);
final Queue<int> _pagesStack = Queue();
bool _hitBack = false;
@override
bool get wantKeepAlive => true;
@@ -46,21 +40,26 @@ class _HomePageState extends State<HomePage> 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<HomePage> 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<HomePage> 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<HomePage> 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<HomePage> 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<PageSetting>(
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<NavigationPaneItem> get _items => [
RebootPaneItem(
title: const Text("Play"),
icon: SizedBox.square(
dimension: 24,
child: Image.asset("assets/images/play.png")
List<AutoSuggestBoxItem<PageSetting>> get _suggestedItems => pages.mapMany((page) {
var icon = SizedBox.square(
dimension: 24,
child: Image.asset(page.iconAsset)
);
var outerResults = <AutoSuggestBoxItem<PageSetting>>[];
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 = <AutoSuggestBoxItem<PageSetting>>[];
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<NavigationPaneItem> 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
);
}

View File

@@ -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<InfoPage> 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<PageSetting> get settings => [];
}
class _InfoPageState extends RebootPageState<InfoPage> {
@override
Widget? get button => null;
@override
List<SettingTile> 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,
)
];
}

View File

@@ -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<MatchmakerPage> 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<PageSetting> 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<MatchmakerPage> {
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
@override
Widget? get button => const ServerButton(
authenticator: false
);
@override
List<Widget> 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),
)
);
}

View File

@@ -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<PlayPage> 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<PageSetting> 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<PlayPage> {
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
final HostingController _hostingController = Get.find<HostingController>();
late final RxBool _selfServer;
@override
void initState() {
_selfServer = RxBool(_isLocalPlay);
_matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay);
_hostingController.started.listen((_) => _selfServer.value = _isLocalPlay);
super.initState();
}
bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text)
&& !_hostingController.started.value;
@override
Widget? get button => LaunchButton(
startLabel: translations.launchFortnite,
stopLabel: translations.closeFortnite,
host: false
);
@override
List<SettingTile> 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
)
]
);
}

View File

@@ -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<BrowsePage> createState() => _BrowsePageState();
@override
List<PageSetting> get settings => [];
}
class _BrowsePageState extends RebootPageState<BrowsePage> {
final HostingController _hostingController = Get.find<HostingController>();
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
final TextEditingController _filterController = TextEditingController();
final StreamController<String> _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<Map<String, dynamic>> data) => Column(
children: [
_searchBar,
const SizedBox(
height: 16,
),
Expanded(
child: StreamBuilder<String?>(
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<Map<String, dynamic>> 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<String, dynamic> entry, String? filter) =>
filter == null || filter.isEmpty || _filterServer(entry, filter);
bool _filterServer(Map<String, dynamic> 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<String, dynamic> entry) {
String result = entry['name'];
return result.isEmpty ? translations.defaultServerName : result;
}
String _formatDescription(Map<String, dynamic> entry) {
String result = entry['description'];
return result.isEmpty ? translations.defaultServerDescription : result;
}
String _formatVersion(Map<String, dynamic> 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<Widget> get settings => [];
}

View File

@@ -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<HostPage> createState() => _HostingPageState();
@override
List<PageSetting> 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<HostPage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
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<SettingTile> 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<void> _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
);
}

View File

@@ -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<SettingsPage> createState() => _SettingsPageState();
@override
List<PageSetting> 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<SettingsPage> {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
@override
Widget? get button => null;
@override
List<Widget> 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;
}
}
}

View File

@@ -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<InfoPage> createState() => _InfoPageState();
}
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
final SettingsController _settingsController = Get.find<SettingsController>();
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,
)
],
),
)
],
);
}
}

View File

@@ -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<MatchmakerPage> createState() => _MatchmakerPageState();
}
class _MatchmakerPageState extends State<MatchmakerPage> with AutomaticKeepAliveClientMixin {
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
@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
)
],
);
}
}

View File

@@ -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<RebootPage> pages = [
const PlayPage(),
const HostPage(),
const BrowsePage(),
const AuthenticatorPage(),
const MatchmakerPage(),
const InfoPage(),
const SettingsPage()
];
final RxInt pageIndex = RxInt(0);
final HashMap<int, GlobalKey> _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<int> get pagesWithButtonIndexes => pages.where((page) => page.hasButton)
.map((page) => page.index)
.toList();

View File

@@ -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<PlayPage> createState() => _PlayPageState();
}
class _PlayPageState extends State<PlayPage> {
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
final HostingController _hostingController = Get.find<HostingController>();
late final RxBool _selfServer;
@override
void initState() {
_selfServer = RxBool(_isLocalPlay);
_matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay);
_hostingController.started.listen((_) => _selfServer.value = _isLocalPlay);
super.initState();
}
bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text)
&& !_hostingController.started.value;
@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
)
]
);
}
}

View File

@@ -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<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
@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";
}

View File

@@ -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<FortniteVersion> 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<FortniteVersion> 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;

View File

@@ -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<bool> _pingGameServer(String hostname, int port) async {
var socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
var dataToSend = utf8.encode(DateTime.now().toIso8601String());
socket.send(dataToSend, InternetAddress(hostname), port);
await for (var event in socket) {
switch(event) {
case RawSocketEvent.read:
return true;
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
case RawSocketEvent.write:
break;
}
}
return false;
}
Future<bool> get _timeoutFuture => Future.delayed(_timeout).then((value) => false);
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
var start = DateTime.now();
@@ -10,23 +32,20 @@ Future<bool> 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);
}
}

View File

@@ -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;
}

View File

@@ -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<FileSelector> {
void _onPressed() {
if(_selecting){
showInfoBar("Folder selector is already opened");
return;
}

View File

@@ -39,21 +39,28 @@ class SettingTile extends StatefulWidget {
class _SettingTileState extends State<SettingTile> {
@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
);
}()
);
}

View File

@@ -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<LaunchButton> {
final SupabaseClient _supabase = Supabase.instance.client;
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
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<LaunchButton> {
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<LaunchButton> {
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<void> _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<LaunchButton> {
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<void> _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<bool> _startMatchMakingServer() async {
Future<bool> _startMatchMakingServer(FortniteVersion version) async {
if(widget.host){
return false;
}
@@ -155,47 +143,59 @@ class _LaunchButtonState extends State<LaunchButton> {
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<int> _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<void> _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<int?> _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<int?> _createLauncherProcess(FortniteVersion version) async {
var launcherFile = version.launcher;
if (launcherFile == null) {
@@ -220,41 +220,57 @@ class _LaunchButtonState extends State<LaunchButton> {
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<void> _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<LaunchButton> {
}
_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<LaunchButton> {
}
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<bool> _checkAccessible(FluentThemeData theme, String gameServerPort) async {
Future<bool> _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<LaunchButton> {
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<void> _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<LaunchButton> {
}
_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<void> _removeHostEntry() async {
await _supabase.from('hosts')
.delete()
.match({'id': _hostingController.uuid});
}
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
Future<void> _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<LaunchButton> {
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<File?> _getDllPath(Injectable injectable, bool hosting) async {
Future<File> 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<File?> _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,

View File

@@ -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<String>() ?? '';
final baseStyle = title?.getProperty<TextStyle>() ?? 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<InlineSpan>(textStyle)!,
maxLines: 1,
overflow: TextOverflow.fade,
softWrap: false,
textAlign: title?.getProperty<TextAlign>() ?? TextAlign.start,
textHeightBehavior: title?.getProperty<TextHeightBehavior>(),
textWidthBasis: title?.getProperty<TextWidthBasis>() ??
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<InlineSpan>(),
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;
}
}

View File

@@ -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()
)
)
),
)
);
}
}

View File

@@ -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 {

View File

@@ -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<ServerButton> {
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<ServerButton> {
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);
}
}

View File

@@ -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<ServerTypeSelector> {
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<ServerTypeSelector> {
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;
}
}

View File

@@ -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<AddLocalVersion> {
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<AddLocalVersion> {
),
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<AddLocalVersion> {
),
DialogButton(
text: "Save",
text: translations.saveLocalVersion,
type: ButtonType.primary,
onTap: () {
Navigator.of(context).pop();

View File

@@ -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<AddServerVersion> {
final Rx<DownloadStatus> _status = Rx(DownloadStatus.form);
final GlobalKey<FormState> _formKey = GlobalKey();
final RxnInt _timeLeft = RxnInt();
final Rxn<double> _downloadProgress = Rxn();
final Rxn<double> _progress = Rxn();
late DiskSpace _diskSpace;
late Future _fetchFuture;
@@ -82,7 +83,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
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<AddServerVersion> {
}
);
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<AddServerVersion> {
List<DialogButton> 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<AddServerVersion> {
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<AddServerVersion> {
);
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<AddServerVersion> {
}
_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<AddServerVersion> {
}
_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<AddServerVersion> {
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<AddServerVersion> {
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<AddServerVersion> {
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<AddServerVersion> {
);
}
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<AddServerVersion> {
),
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

View File

@@ -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<BuildSelector> {
@override
Widget build(BuildContext context) {
return InfoLabel(
label: "Build",
label: translations.build,
child: Obx(() => ComboBox<FortniteBuild>(
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<BuildSelector> {
);
}
List<ComboBoxItem<FortniteBuild>> _createItems() {
return _buildController.builds!
.map((element) => _createItem(element))
.toList();
}
List<ComboBoxItem<FortniteBuild>> get _items =>_buildController.builds!
.map((element) => _buildItem(element))
.toList();
ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) {
ComboBoxItem<FortniteBuild> _buildItem(FortniteBuild element) {
return ComboBoxItem<FortniteBuild>(
value: element,
child: Text(element.version.toString())

View File

@@ -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<GameController>();
@@ -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;
}
}

View File

@@ -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<VersionSelector> {
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<VersionSelector> {
.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<VersionSelector> {
}
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<VersionSelector> {
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<VersionSelector> {
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<VersionSelector> {
),
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<VersionSelector> {
),
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;
}
}

View File

@@ -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
)
]
);

View File

@@ -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/

View File

@@ -13,6 +13,7 @@
#include <system_theme/system_theme_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
#include <windows_taskbar/windows_taskbar_plugin.h>
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"));
}

View File

@@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
system_theme
url_launcher_windows
window_manager
windows_taskbar
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -1,5 +1,5 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME);
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
#include <cstdlib>
@@ -16,13 +16,9 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME);
#include <stdio.h>
#include <fcntl.h>
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;
}

View File

@@ -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),