Final version
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
18
common/lib/src/model/archive.dart
Normal 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);
|
||||
}
|
||||
@@ -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,7 +51,6 @@ 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);
|
||||
@@ -53,68 +59,110 @@ Future<void> downloadArchiveBuild(ArchiveDownloadOptions options) async {
|
||||
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");
|
||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
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 _extract(stopped, extension, tempFile, options);
|
||||
}
|
||||
|
||||
delete(outputDir);
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
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;
|
||||
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));
|
||||
sink.add(data);
|
||||
});
|
||||
|
||||
await Future.any([stopped.future, subscription.asFuture()]);
|
||||
if(stopped.isCompleted) {
|
||||
await subscription.cancel();
|
||||
}else {
|
||||
await sink.flush();
|
||||
await sink.close();
|
||||
await sink.done;
|
||||
},
|
||||
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) {
|
||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
Process? process;
|
||||
switch (extension.toLowerCase()) {
|
||||
case ".zip":
|
||||
process = await Process.start(
|
||||
"${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;
|
||||
}
|
||||
|
||||
options.port.send(ArchiveDownloadProgress(0, -1, true));
|
||||
Process? process;
|
||||
switch (extension.toLowerCase()) {
|
||||
case '.zip':
|
||||
process = await Process.start(
|
||||
'tar',
|
||||
['-xf', tempFile.path, '-C', options.destination.path],
|
||||
mode: ProcessStartMode.inheritStdio
|
||||
);
|
||||
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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 774 B |
|
Before Width: | Height: | Size: 1011 B After Width: | Height: | Size: 1011 B |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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,20 +19,21 @@ 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 {
|
||||
void main() => runZonedGuarded(() async {
|
||||
await installationDirectory.create(recursive: true);
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
@@ -38,21 +41,22 @@ void main() async {
|
||||
);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemTheme.accentColor.load();
|
||||
_initWindow();
|
||||
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]));
|
||||
},
|
||||
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,8 +118,7 @@ void _joinServer(Uri uri) {
|
||||
|
||||
String _parseCustomUrl(Uri uri) => uri.host;
|
||||
|
||||
Future<Object?> _initWindow() async {
|
||||
try {
|
||||
void _initWindow() => doWhenWindowReady(() async {
|
||||
await windowManager.ensureInitialized();
|
||||
await Window.initialize();
|
||||
var settingsController = Get.find<SettingsController>();
|
||||
@@ -135,13 +138,10 @@ Future<Object?> _initWindow() async {
|
||||
await Window.setEffect(
|
||||
effect: WindowEffect.acrylic,
|
||||
color: Colors.transparent,
|
||||
dark: true
|
||||
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
|
||||
);
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
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();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 +18,30 @@ 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(
|
||||
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(
|
||||
@@ -52,13 +68,12 @@ void showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,18 +88,25 @@ void showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
});
|
||||
});
|
||||
}
|
||||
return overlay;
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
void removeMessage(int index) {
|
||||
try {
|
||||
void removeMessageByPage(int index) {
|
||||
var lastOverlay = _overlays[index];
|
||||
if(lastOverlay != null) {
|
||||
lastOverlay.remove();
|
||||
removeMessageByOverlay(lastOverlay);
|
||||
_overlays[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
void removeMessageByOverlay(OverlayEntry? overlay) {
|
||||
try {
|
||||
if(overlay != null) {
|
||||
overlay.remove();
|
||||
}
|
||||
}catch(_) {
|
||||
// Do not use .isMounted
|
||||
// This is intended behaviour
|
||||
|
||||
24
gui/lib/src/dialog/implementation/data.dart
Normal 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();
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
@@ -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())
|
||||
)
|
||||
));
|
||||
}
|
||||
@@ -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."
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
]
|
||||
))
|
||||
|
||||
@@ -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
|
||||
));
|
||||
|
||||
78
gui/lib/src/page/abstract/page.dart
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
26
gui/lib/src/page/abstract/page_setting.dart
Normal 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";
|
||||
}
|
||||
9
gui/lib/src/page/abstract/page_type.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
enum RebootPageType {
|
||||
play,
|
||||
host,
|
||||
browser,
|
||||
authenticator,
|
||||
matchmaker,
|
||||
info,
|
||||
settings
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
155
gui/lib/src/page/implementation/authenticator_page.dart
Normal 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
|
||||
),
|
||||
],
|
||||
))
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
@@ -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,19 +40,24 @@ 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;
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
if(_hitBack) {
|
||||
_hitBack = false;
|
||||
return;
|
||||
}
|
||||
|
||||
void _onSearch() {
|
||||
// TODO: Implement
|
||||
if(value == lastValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pagesStack.add(lastValue);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
restoreMessage(value, lastValue);
|
||||
lastValue = value;
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
@@ -158,88 +179,88 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
Widget get _autoSuggestBox => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: TextBox(
|
||||
child: AutoSuggestBox<PageSetting>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: 'Find a setting',
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
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
|
||||
),
|
||||
],
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
suffix: Button(
|
||||
onPressed: null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
border: ButtonState.all(const BorderSide(color: Colors.transparent))
|
||||
),
|
||||
child: Transform.flip(
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: FluentTheme.of(context).resources.textFillColorPrimary
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
List<NavigationPaneItem> get _items => [
|
||||
RebootPaneItem(
|
||||
title: const Text("Play"),
|
||||
icon: SizedBox.square(
|
||||
List<AutoSuggestBoxItem<PageSetting>> get _suggestedItems => pages.mapMany((page) {
|
||||
var icon = SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset("assets/images/play.png")
|
||||
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
|
||||
);
|
||||
}
|
||||
130
gui/lib/src/page/implementation/info_page.dart
Normal 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,
|
||||
)
|
||||
];
|
||||
}
|
||||
165
gui/lib/src/page/implementation/matchmaker_page.dart
Normal 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),
|
||||
)
|
||||
);
|
||||
}
|
||||
143
gui/lib/src/page/implementation/play_page.dart
Normal 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
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
247
gui/lib/src/page/implementation/server_browser_page.dart
Normal 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 => [];
|
||||
}
|
||||
289
gui/lib/src/page/implementation/server_host_page.dart
Normal 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
|
||||
);
|
||||
}
|
||||
317
gui/lib/src/page/implementation/settings_page.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
43
gui/lib/src/page/pages.dart
Normal 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();
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
gui/lib/src/util/translations.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ class SettingTile extends StatefulWidget {
|
||||
class _SettingTileState extends State<SettingTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: () {
|
||||
if (widget.expandedContent == null || widget.expandedContent?.isEmpty == true) {
|
||||
return _contentCard;
|
||||
}
|
||||
@@ -55,6 +60,8 @@ class _SettingTileState extends State<SettingTile> {
|
||||
trailing: _trailing,
|
||||
content: _expandedContent
|
||||
);
|
||||
}()
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _expandedContent {
|
||||
|
||||
@@ -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);
|
||||
_onStop(
|
||||
reason: _StopReason.missingExecutableError,
|
||||
error: version.location.path
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(false);
|
||||
var authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(_pageType, false);
|
||||
if(!authenticatorResult){
|
||||
_onStop(widget.host, false);
|
||||
_onStop(
|
||||
reason: _StopReason.authenticatorError
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(false);
|
||||
var matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(_pageType, false);
|
||||
if(!matchmakerResult){
|
||||
_onStop(widget.host, false);
|
||||
_onStop(
|
||||
reason: _StopReason.matchmakerError
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var automaticallyStartedServer = await _startMatchMakingServer();
|
||||
var automaticallyStartedServer = await _startMatchMakingServer(version);
|
||||
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
|
||||
if(widget.host){
|
||||
showInfoBar(
|
||||
"Launching the headless server...",
|
||||
loading: true,
|
||||
duration: null
|
||||
if(automaticallyStartedServer || widget.host){
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
_onStop(
|
||||
reason: _StopReason.unknownError,
|
||||
error: exception.toString(),
|
||||
stackTrace: stackTrace
|
||||
);
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
_onStop(widget.host, false);
|
||||
showCorruptedBuildError(widget.host, exception, 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
|
||||
);
|
||||
@@ -277,15 +293,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_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 {
|
||||
String _getDllPath(_Injectable injectable) {
|
||||
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");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
var dllPath = await getPath(injectable);
|
||||
if(dllPath.existsSync()) {
|
||||
return dllPath;
|
||||
Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async {
|
||||
var path = _getDllPath(injectable);
|
||||
var file = File(path);
|
||||
if(await file.exists()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
_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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||
_timeLeft.value = timeLeft;
|
||||
_downloadProgress.value = progress;
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
Widget get _downloadBody {
|
||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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))
|
||||
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())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
48
gui/lib/src/widget/version/version_selector_tile.dart
Normal 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
|
||||
)
|
||||
]
|
||||
);
|
||||
@@ -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/
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
system_theme
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
windows_taskbar
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||