mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4327541ac6 | ||
|
|
64dc971da4 | ||
|
|
d36da909ed | ||
|
|
90448eeaa1 | ||
|
|
b319479def | ||
|
|
d5e41ed646 | ||
|
|
9e20ec86e6 | ||
|
|
004fc41292 | ||
|
|
ee466df630 | ||
|
|
fdb1d694d9 | ||
|
|
0cfa4af236 | ||
|
|
d42946c44b | ||
|
|
0a59a32c1b | ||
|
|
2046cb14f6 | ||
|
|
e3f7a1d2cc | ||
|
|
cd6752ed3f | ||
|
|
e1df46efd9 | ||
|
|
dccd05e57f | ||
|
|
eb7745cc4d | ||
|
|
7d5e17642a | ||
|
|
6f91ad0404 | ||
|
|
0c38528e77 | ||
|
|
dfebe74518 | ||
|
|
bfe15e43d9 |
@@ -1,7 +1,7 @@
|
|||||||

|

|
||||||
|
|
||||||
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
|
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
|
||||||
Join our discord at https://discord.gg/reboot
|
Join our [Discord](https://discord.gg/rebootmp)
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
|||||||
778
backend/responses/keychain.json
vendored
778
backend/responses/keychain.json
vendored
File diff suppressed because it is too large
Load Diff
29
backend/structure/mcp.js
vendored
29
backend/structure/mcp.js
vendored
@@ -7932,6 +7932,35 @@ express.post("/fortnite/api/game/v2/profile/*/client/SetHeroCosmeticVariants", a
|
|||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// any dedicated_server request
|
||||||
|
express.post("/fortnite/api/game/v2/profile/*/dedicated_server/*", async (req, res) => {
|
||||||
|
const profile = require(`./../profiles/${req.query.profileId || "athena"}.json`);
|
||||||
|
|
||||||
|
// do not change any of these or you will end up breaking it
|
||||||
|
var ApplyProfileChanges = [];
|
||||||
|
var BaseRevision = profile.rvn || 0;
|
||||||
|
var QueryRevision = req.query.rvn || -1;
|
||||||
|
|
||||||
|
// this doesn't work properly on version v12.20 and above but whatever
|
||||||
|
if (QueryRevision != BaseRevision) {
|
||||||
|
ApplyProfileChanges = [{
|
||||||
|
"changeType": "fullProfileUpdate",
|
||||||
|
"profile": profile
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
"profileRevision": profile.rvn || 0,
|
||||||
|
"profileId": req.query.profileId || "athena",
|
||||||
|
"profileChangesBaseRevision": BaseRevision,
|
||||||
|
"profileChanges": ApplyProfileChanges,
|
||||||
|
"profileCommandRevision": profile.commandRevision || 0,
|
||||||
|
"serverTime": new Date().toISOString(),
|
||||||
|
"responseVersion": 1
|
||||||
|
})
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
// any mcp request that doesn't have something assigned to it
|
// any mcp request that doesn't have something assigned to it
|
||||||
express.post("/fortnite/api/game/v2/profile/*/client/*", async (req, res) => {
|
express.post("/fortnite/api/game/v2/profile/*/client/*", async (req, res) => {
|
||||||
const profile = require(`./../profiles/${req.query.profileId || "athena"}.json`);
|
const profile = require(`./../profiles/${req.query.profileId || "athena"}.json`);
|
||||||
|
|||||||
156
cli/lib/cli.dart
156
cli/lib/cli.dart
@@ -1,87 +1,89 @@
|
|||||||
import 'dart:io';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
class Parser {
|
||||||
import 'package:reboot_cli/src/game.dart';
|
final List<Command> commands;
|
||||||
import 'package:reboot_cli/src/reboot.dart';
|
|
||||||
import 'package:reboot_cli/src/server.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
|
|
||||||
late String? username;
|
Parser({required this.commands});
|
||||||
late bool host;
|
|
||||||
late bool verbose;
|
|
||||||
late String dll;
|
|
||||||
late FortniteVersion version;
|
|
||||||
late bool autoRestart;
|
|
||||||
|
|
||||||
void main(List<String> args) async {
|
CommandCall? parse(List<String> args) {
|
||||||
stdout.writeln("Reboot Launcher");
|
var position = 0;
|
||||||
stdout.writeln("Wrote by Auties00");
|
var allowedCommands = _toMap(commands);
|
||||||
stdout.writeln("Version 1.0");
|
var allowedParameters = <String>{};
|
||||||
|
Command? command;
|
||||||
kill();
|
CommandCall? head;
|
||||||
|
CommandCall? tail;
|
||||||
var parser = ArgParser()
|
String? parameterName;
|
||||||
..addOption("path", mandatory: true)
|
while(position < args.length) {
|
||||||
..addOption("username")
|
final current = args[position].toLowerCase();
|
||||||
..addOption("server-type", allowed: ServerType.values.map((entry) => entry.name), defaultsTo: ServerType.embedded.name)
|
if(parameterName != null) {
|
||||||
..addOption("server-host")
|
tail?.parameters[parameterName] = current;
|
||||||
..addOption("server-port")
|
parameterName = null;
|
||||||
..addOption("matchmaking-address")
|
}else if(allowedParameters.contains(current.toLowerCase())) {
|
||||||
..addOption("dll", defaultsTo: rebootDllFile.path)
|
parameterName = current.substring(2);
|
||||||
..addFlag("update", defaultsTo: true, negatable: true)
|
if(args.elementAtOrNull(position + 1) == '"') {
|
||||||
..addFlag("log", defaultsTo: false)
|
position++;
|
||||||
..addFlag("host", defaultsTo: false)
|
}
|
||||||
..addFlag("auto-restart", defaultsTo: false, negatable: true);
|
}else {
|
||||||
var result = parser.parse(args);
|
final newCommand = allowedCommands[current];
|
||||||
|
if(newCommand != null) {
|
||||||
dll = result["dll"];
|
final newCall = CommandCall(name: newCommand.name);
|
||||||
host = result["host"];
|
if(head == null) {
|
||||||
username = result["username"] ?? kDefaultPlayerName;
|
head = newCall;
|
||||||
verbose = result["log"];
|
tail = newCall;
|
||||||
version = FortniteVersion(name: "Dummy", location: Directory(result["path"]));
|
}
|
||||||
|
if(tail != null) {
|
||||||
await downloadRequiredDLLs();
|
tail.subCall = newCall;
|
||||||
if(result["update"]) {
|
}
|
||||||
stdout.writeln("Updating reboot dll...");
|
tail = newCall;
|
||||||
try {
|
command = newCommand;
|
||||||
await downloadRebootDll(kRebootDownloadUrl);
|
allowedCommands = _toMap(newCommand.subCommands);
|
||||||
}catch(error){
|
allowedParameters = _toParameters(command);
|
||||||
stderr.writeln("Cannot update reboot dll: $error");
|
}
|
||||||
|
}
|
||||||
|
position++;
|
||||||
}
|
}
|
||||||
|
return head;
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.writeln("Launching game...");
|
Set<String> _toParameters(Command? parent) => parent?.parameters
|
||||||
var executable = version.shippingExecutable;
|
.map((e) => '--${e.toLowerCase()}')
|
||||||
if(executable == null){
|
.toSet() ?? {};
|
||||||
throw Exception("Missing game executable at: ${version.location.path}");
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverHost = result["server-host"]?.trim();
|
Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
|
||||||
if(serverHost?.isEmpty == true){
|
children,
|
||||||
throw Exception("Missing host name");
|
key: (command) => command.name.toLowerCase(),
|
||||||
}
|
value: (command) => command
|
||||||
|
|
||||||
final serverPort = result["server-port"]?.trim();
|
|
||||||
if(serverPort?.isEmpty == true){
|
|
||||||
throw Exception("Missing port");
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverPortNumber = serverPort == null ? null : int.tryParse(serverPort);
|
|
||||||
if(serverPort != null && serverPortNumber == null){
|
|
||||||
throw Exception("Invalid port, use only numbers");
|
|
||||||
}
|
|
||||||
|
|
||||||
var started = await startServerCli(
|
|
||||||
serverHost,
|
|
||||||
serverPortNumber,
|
|
||||||
ServerType.values.firstWhere((element) => element.name == result["server-type"])
|
|
||||||
);
|
);
|
||||||
if(!started){
|
}
|
||||||
stderr.writeln("Cannot start server!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeMatchmakingIp(result["matchmaking-address"]);
|
class Command {
|
||||||
autoRestart = result["auto-restart"];
|
final String name;
|
||||||
await startGame();
|
final List<String> parameters;
|
||||||
|
final List<Command> subCommands;
|
||||||
|
|
||||||
|
const Command({required this.name, required this.parameters, required this.subCommands});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Command{name: $name, parameters: $parameters, subCommands: $subCommands}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parameter {
|
||||||
|
final String name;
|
||||||
|
final bool Function(String) validator;
|
||||||
|
|
||||||
|
const Parameter({required this.name, required this.validator});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Parameter{name: $name, validator: $validator}';
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandCall {
|
||||||
|
final String name;
|
||||||
|
final Map<String, String> parameters;
|
||||||
|
CommandCall? subCall;
|
||||||
|
|
||||||
|
CommandCall({required this.name}) : parameters = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CommandCall{name: $name, parameters: $parameters, subCall: $subCall}';
|
||||||
}
|
}
|
||||||
102
cli/lib/main.dart
Normal file
102
cli/lib/main.dart
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import 'package:interact/interact.dart';
|
||||||
|
import 'package:reboot_cli/cli.dart';
|
||||||
|
import 'package:tint/tint.dart';
|
||||||
|
|
||||||
|
const Command _buildImport = Command(name: 'import', parameters: ['version', 'path'], subCommands: []);
|
||||||
|
const Command _buildDownload = Command(name: 'download', parameters: ['version', 'path'], subCommands: []);
|
||||||
|
const Command _build = Command(name: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]);
|
||||||
|
const Command _play = Command(name: 'play', parameters: [], subCommands: []);
|
||||||
|
const Command _host = Command(name: 'host', parameters: [], subCommands: []);
|
||||||
|
const Command _backend = Command(name: 'backend', parameters: [], subCommands: []);
|
||||||
|
|
||||||
|
void main(List<String> args) {
|
||||||
|
_welcome();
|
||||||
|
|
||||||
|
final parser = Parser(commands: [_build, _play, _host, _backend]);
|
||||||
|
final command = parser.parse(args);
|
||||||
|
print(command);
|
||||||
|
_handleRootCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleRootCommand(CommandCall? call) {
|
||||||
|
switch(call == null ? null : call.name) {
|
||||||
|
case 'build':
|
||||||
|
_handleBuildCommand(call?.subCall);
|
||||||
|
break;
|
||||||
|
case 'play':
|
||||||
|
_handleBuildCommand(call?.subCall);
|
||||||
|
break;
|
||||||
|
case 'host':
|
||||||
|
_handleBuildCommand(call?.subCall);
|
||||||
|
break;
|
||||||
|
case 'backend':
|
||||||
|
_handleBuildCommand(call?.subCall);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_askRootCommand();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _askRootCommand() {
|
||||||
|
final commands = [_build.name, _play.name, _host.name, _backend.name];
|
||||||
|
final commandSelector = Select.withTheme(
|
||||||
|
prompt: ' Select a command:',
|
||||||
|
options: commands,
|
||||||
|
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '')
|
||||||
|
);
|
||||||
|
_handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBuildCommand(CommandCall? call) {
|
||||||
|
switch(call == null ? null : call.name) {
|
||||||
|
case 'import':
|
||||||
|
_handleBuildImportCommand(call!);
|
||||||
|
break;
|
||||||
|
case 'download':
|
||||||
|
_handleBuildDownloadCommand(call!);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_askBuildCommand();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBuildImportCommand(CommandCall call) {
|
||||||
|
final version = call.parameters['path'];
|
||||||
|
final path = call.parameters['path'];
|
||||||
|
print(version);
|
||||||
|
print(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBuildDownloadCommand(CommandCall call) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _askBuildCommand() {
|
||||||
|
final commands = [_buildImport.name, _buildDownload.name];
|
||||||
|
final commandSelector = Select.withTheme(
|
||||||
|
prompt: ' Select a build command:',
|
||||||
|
options: commands,
|
||||||
|
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '')
|
||||||
|
);
|
||||||
|
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePlayCommand(CommandCall? call) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleHostCommand(CommandCall? call) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBackendCommand(CommandCall? call) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _welcome() => print("""
|
||||||
|
🎮 Reboot Launcher
|
||||||
|
🔥 Launch, manage, and play Fortnite using Project Reboot!
|
||||||
|
🚀 Developed by Auties00 - Version 10.0.7
|
||||||
|
""".green());
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:process_run/process_run.dart';
|
|
||||||
import 'package:reboot_cli/cli.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
|
|
||||||
Process? _gameProcess;
|
|
||||||
Process? _launcherProcess;
|
|
||||||
Process? _eacProcess;
|
|
||||||
|
|
||||||
Future<void> startGame() async {
|
|
||||||
await _startLauncherProcess(version);
|
|
||||||
await _startEacProcess(version);
|
|
||||||
|
|
||||||
var executable = await version.shippingExecutable;
|
|
||||||
if (executable == null) {
|
|
||||||
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username == null) {
|
|
||||||
username = "Reboot${host ? 'Host' : 'Player'}";
|
|
||||||
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
|
|
||||||
..exitCode.then((_) => _onClose())
|
|
||||||
..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
|
|
||||||
_injectOrShowError("cobalt.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.launcherExecutable == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []);
|
|
||||||
suspend(_launcherProcess!.pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.eacExecutable == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
|
||||||
suspend(_eacProcess!.pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
|
||||||
if(verbose) {
|
|
||||||
stdout.writeln(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGameOutput(
|
|
||||||
line: line,
|
|
||||||
host: hosting,
|
|
||||||
onDisplayAttached: () {}, // TODO: Support virtual desktops
|
|
||||||
onLoggedIn: onLoggedIn,
|
|
||||||
onMatchEnd: onMatchEnd,
|
|
||||||
onShutdown: onShutdown,
|
|
||||||
onTokenError: onTokenError,
|
|
||||||
onBuildCorrupted: onBuildCorrupted
|
|
||||||
);
|
|
||||||
|
|
||||||
if (line.contains(kShutdownLine)) {
|
|
||||||
_onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(kCannotConnectErrors.any((element) => line.contains(element))){
|
|
||||||
stderr.writeln("The backend doesn't work! Token expired");
|
|
||||||
_onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(line.contains("Region ")){
|
|
||||||
if(hosting) {
|
|
||||||
_injectOrShowError(dll, false);
|
|
||||||
}else {
|
|
||||||
_injectOrShowError("console.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
_injectOrShowError("memory.dll");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _kill() {
|
|
||||||
_gameProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
_launcherProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
_eacProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
|
||||||
if (_gameProcess == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
stdout.writeln("Injecting $binary...");
|
|
||||||
var dll = locate ? File("${dllsDirectory.path}\\$binary") : File(binary);
|
|
||||||
if(!dll.existsSync()){
|
|
||||||
throw Exception("Cannot inject $dll: missing file");
|
|
||||||
}
|
|
||||||
|
|
||||||
await injectDll(_gameProcess!.pid, dll);
|
|
||||||
} catch (exception) {
|
|
||||||
throw Exception("Cannot inject binary: $binary");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onClose() {
|
|
||||||
_kill();
|
|
||||||
sleep(const Duration(seconds: 3));
|
|
||||||
stdout.writeln("The game was closed");
|
|
||||||
if(autoRestart){
|
|
||||||
stdout.writeln("Restarting automatically game");
|
|
||||||
startGame();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
|
|
||||||
// TODO: Use github
|
|
||||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
|
|
||||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
|
|
||||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memory.dll";
|
|
||||||
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
|
|
||||||
|
|
||||||
Future<void> downloadRequiredDLLs() async {
|
|
||||||
stdout.writeln("Downloading necessary components...");
|
|
||||||
var consoleDll = File("${dllsDirectory.path}\\console.dll");
|
|
||||||
if(!consoleDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_consoleDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download console.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
|
|
||||||
if(!craniumDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_baseDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download cobalt.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var memoryFixDll = File("${dllsDirectory.path}\\memory.dll");
|
|
||||||
if(!memoryFixDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_memoryFixDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download memory.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!backendDirectory.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_embeddedConfigDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download embedded server config");
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
|
||||||
await tempZip.writeAsBytes(response.bodyBytes);
|
|
||||||
await extractFileToDisk(tempZip.path, backendDirectory.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_common/src/util/backend.dart' as server;
|
|
||||||
|
|
||||||
Future<bool> startServerCli(String? host, int? port, ServerType type) async {
|
|
||||||
stdout.writeln("Starting backend server...");
|
|
||||||
switch(type){
|
|
||||||
case ServerType.local:
|
|
||||||
final result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
|
|
||||||
if(result == null){
|
|
||||||
throw Exception("Local backend server is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln("Detected local backend server");
|
|
||||||
return true;
|
|
||||||
case ServerType.embedded:
|
|
||||||
stdout.writeln("Starting an embedded server...");
|
|
||||||
await server.startEmbeddedBackend(false);
|
|
||||||
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
|
|
||||||
if(result == null){
|
|
||||||
throw Exception("Cannot start embedded server");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
case ServerType.remote:
|
|
||||||
if(host == null){
|
|
||||||
throw Exception("Missing host for remote server");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(port == null){
|
|
||||||
throw Exception("Missing host for remote server");
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln("Starting a reverse proxy to $host:$port");
|
|
||||||
return await _changeReverseProxyState(host, port) != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HttpServer?> _changeReverseProxyState(String host, int port) async {
|
|
||||||
try{
|
|
||||||
var uri = await pingBackend(host, port);
|
|
||||||
if(uri == null){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await server.startRemoteBackendProxy(uri);
|
|
||||||
}catch(error){
|
|
||||||
throw Exception("Cannot start reverse proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void kill() async {
|
|
||||||
try {
|
|
||||||
await Process.run("taskkill", ["/f", "/im", "FortniteLauncher.exe"]);
|
|
||||||
await Process.run("taskkill", ["/f", "/im", "FortniteClient-Win64-Shipping_EAC.exe"]);
|
|
||||||
}catch(_){
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
name: reboot_cli
|
name: reboot_cli
|
||||||
description: Command Line Interface for Project Reboot
|
description: Command Line Interface for Project Reboot
|
||||||
version: "1.0.0"
|
version: "10.0.7"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.19.0 <=3.3.4"
|
sdk: ">=2.19.0 <=3.5.3"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
reboot_common:
|
reboot_common:
|
||||||
path: ./../common
|
path: ./../common
|
||||||
args: ^2.3.1
|
tint: ^2.0.1
|
||||||
process_run: ^0.13.1
|
interact: ^2.2.0
|
||||||
|
args: ^2.6.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
xml: ^6.3.0
|
xml: ^6.3.0
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const String kDefaultPlayerName = "Player";
|
const String kDefaultPlayerName = "Player";
|
||||||
|
const String kDefaultHostName = "Host";
|
||||||
const String kDefaultGameServerHost = "127.0.0.1";
|
const String kDefaultGameServerHost = "127.0.0.1";
|
||||||
const String kDefaultGameServerPort = "7777";
|
const String kDefaultGameServerPort = "7777";
|
||||||
const String kInitializedLine = "Game Engine Initialized";
|
const String kInitializedLine = "Game Engine Initialized";
|
||||||
@@ -21,4 +22,5 @@ const List<String> kCannotConnectErrors = [
|
|||||||
"UOnlineAccountCommon::ForceLogout"
|
"UOnlineAccountCommon::ForceLogout"
|
||||||
];
|
];
|
||||||
const String kGameFinishedLine = "PlayersLeft: 1";
|
const String kGameFinishedLine = "PlayersLeft: 1";
|
||||||
const String kDisplayInitializedLine = "Display";
|
const String kDisplayLine = "Display";
|
||||||
|
const String kDisplayInitializedLine = "Initialized";
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
enum InjectableDll {
|
enum InjectableDll {
|
||||||
console,
|
console,
|
||||||
cobalt,
|
auth,
|
||||||
reboot,
|
gameServer,
|
||||||
memory
|
memoryLeak
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InjectableDllVersionAware on InjectableDll {
|
||||||
|
bool get isVersionDependent => this == InjectableDll.gameServer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ class FortniteBuild {
|
|||||||
|
|
||||||
class FortniteBuildDownloadProgress {
|
class FortniteBuildDownloadProgress {
|
||||||
final double progress;
|
final double progress;
|
||||||
final int? minutesLeft;
|
final int? timeLeft;
|
||||||
final bool extracting;
|
final bool extracting;
|
||||||
|
final int speed;
|
||||||
|
|
||||||
FortniteBuildDownloadProgress({
|
FortniteBuildDownloadProgress({
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.extracting,
|
required this.extracting,
|
||||||
this.minutesLeft,
|
required this.timeLeft,
|
||||||
|
required this.speed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
|
||||||
class GameInstance {
|
class GameInstance {
|
||||||
final String versionName;
|
final Version version;
|
||||||
final int gamePid;
|
final int gamePid;
|
||||||
final int? launcherPid;
|
final int? launcherPid;
|
||||||
final int? eacPid;
|
final int? eacPid;
|
||||||
@@ -17,7 +18,7 @@ class GameInstance {
|
|||||||
GameInstance? child;
|
GameInstance? child;
|
||||||
|
|
||||||
GameInstance({
|
GameInstance({
|
||||||
required this.versionName,
|
required this.version,
|
||||||
required this.gamePid,
|
required this.gamePid,
|
||||||
required this.launcherPid,
|
required this.launcherPid,
|
||||||
required this.eacPid,
|
required this.eacPid,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
class ServerResult {
|
class ServerResult {
|
||||||
final ServerResultType type;
|
final ServerResultType type;
|
||||||
|
final ServerImplementation? implementation;
|
||||||
final Object? error;
|
final Object? error;
|
||||||
final StackTrace? stackTrace;
|
final StackTrace? stackTrace;
|
||||||
|
|
||||||
ServerResult(this.type, {this.error, this.stackTrace});
|
ServerResult(this.type, {this.implementation, this.error, this.stackTrace});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -11,22 +14,32 @@ class ServerResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ServerImplementation {
|
||||||
|
final Process? process;
|
||||||
|
final HttpServer? server;
|
||||||
|
|
||||||
|
ServerImplementation({this.process, this.server});
|
||||||
|
}
|
||||||
|
|
||||||
enum ServerResultType {
|
enum ServerResultType {
|
||||||
starting,
|
starting,
|
||||||
|
startMissingHostError,
|
||||||
|
startMissingPortError,
|
||||||
|
startIllegalPortError,
|
||||||
|
startFreeingPort,
|
||||||
|
startFreePortSuccess,
|
||||||
|
startFreePortError,
|
||||||
|
startPingingRemote,
|
||||||
|
startPingingLocal,
|
||||||
|
startPingError,
|
||||||
|
startedImplementation,
|
||||||
startSuccess,
|
startSuccess,
|
||||||
startError,
|
startError,
|
||||||
stopping,
|
stopping,
|
||||||
stopSuccess,
|
stopSuccess,
|
||||||
stopError,
|
stopError;
|
||||||
missingHostError,
|
|
||||||
missingPortError,
|
bool get isStart => name.contains("start");
|
||||||
illegalPortError,
|
|
||||||
freeingPort,
|
|
||||||
freePortSuccess,
|
|
||||||
freePortError,
|
|
||||||
pingingRemote,
|
|
||||||
pingingLocal,
|
|
||||||
pingError;
|
|
||||||
|
|
||||||
bool get isError => name.contains("Error");
|
bool get isError => name.contains("Error");
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,122 @@ final Semaphore _semaphore = Semaphore();
|
|||||||
String? _lastIp;
|
String? _lastIp;
|
||||||
String? _lastPort;
|
String? _lastPort;
|
||||||
|
|
||||||
|
Stream<ServerResult> startBackend({required ServerType type, required String host, required String port, required bool detached, required void Function(String) onError}) async* {
|
||||||
|
Process? process;
|
||||||
|
HttpServer? server;
|
||||||
|
try {
|
||||||
|
host = host.trim();
|
||||||
|
port = port.trim();
|
||||||
|
if(type != ServerType.local || port != kDefaultBackendPort.toString()) {
|
||||||
|
yield ServerResult(ServerResultType.starting);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.isEmpty) {
|
||||||
|
yield ServerResult(ServerResultType.startMissingHostError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port.isEmpty) {
|
||||||
|
yield ServerResult(ServerResultType.startMissingPortError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final portNumber = int.tryParse(port);
|
||||||
|
if (portNumber == null) {
|
||||||
|
yield ServerResult(ServerResultType.startIllegalPortError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type != ServerType.local || port != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
||||||
|
yield ServerResult(ServerResultType.startFreeingPort);
|
||||||
|
final result = await freeBackendPort();
|
||||||
|
if(!result) {
|
||||||
|
yield ServerResult(ServerResultType.startFreePortError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.startFreePortSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(type){
|
||||||
|
case ServerType.embedded:
|
||||||
|
process = await startEmbeddedBackend(detached, onError: onError);
|
||||||
|
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(process: process));
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
yield ServerResult(ServerResultType.startPingingRemote);
|
||||||
|
final uriResult = await pingBackend(host, portNumber);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.startPingError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = await startRemoteBackendProxy(uriResult);
|
||||||
|
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
if(portNumber != kDefaultBackendPort) {
|
||||||
|
yield ServerResult(ServerResultType.startPingingLocal);
|
||||||
|
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.startPingError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$port"));
|
||||||
|
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.startPingingLocal);
|
||||||
|
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.startPingError);
|
||||||
|
process?.kill(ProcessSignal.sigterm);
|
||||||
|
server?.close(force: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.startSuccess);
|
||||||
|
}catch(error, stackTrace) {
|
||||||
|
yield ServerResult(
|
||||||
|
ServerResultType.startError,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace
|
||||||
|
);
|
||||||
|
process?.kill(ProcessSignal.sigterm);
|
||||||
|
server?.close(force: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ServerResult> stopBackend({required ServerType type, required ServerImplementation? implementation}) async* {
|
||||||
|
yield ServerResult(ServerResultType.stopping);
|
||||||
|
try{
|
||||||
|
switch(type){
|
||||||
|
case ServerType.embedded:
|
||||||
|
final process = implementation?.process;
|
||||||
|
if(process != null) {
|
||||||
|
Process.killPid(process.pid, ProcessSignal.sigterm);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
await implementation?.server?.close(force: true);
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
await implementation?.server?.close(force: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yield ServerResult(ServerResultType.stopSuccess);
|
||||||
|
}catch(error, stackTrace){
|
||||||
|
yield ServerResult(
|
||||||
|
ServerResultType.stopError,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
|
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
|
||||||
final process = await startProcess(
|
final process = await startProcess(
|
||||||
executable: backendStartExecutable,
|
executable: backendStartExecutable,
|
||||||
@@ -25,7 +141,9 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
|
|||||||
log("[BACKEND] Error: $error");
|
log("[BACKEND] Error: $error");
|
||||||
onError?.call(error);
|
onError?.call(error);
|
||||||
});
|
});
|
||||||
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
|
if(!detached) {
|
||||||
|
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
|
||||||
|
}
|
||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +179,7 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
|||||||
await request.close().timeout(const Duration(seconds: 10));
|
await request.close().timeout(const Duration(seconds: 10));
|
||||||
log("[BACKEND] Ping successful");
|
log("[BACKEND] Ping successful");
|
||||||
return uri;
|
return uri;
|
||||||
}catch(error){
|
}catch(error) {
|
||||||
log("[BACKEND] Cannot ping backend: $error");
|
log("[BACKEND] Cannot ping backend: $error");
|
||||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,165 +3,311 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:dio/io.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_common/src/extension/types.dart';
|
import 'package:reboot_common/src/extension/types.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
const String kStopBuildDownloadSignal = "kill";
|
const String kStopBuildDownloadSignal = "kill";
|
||||||
|
|
||||||
final Dio _dio = _buildDioInstance();
|
final int _ariaPort = 6800;
|
||||||
Dio _buildDioInstance() {
|
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
|
||||||
final dio = Dio();
|
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
|
||||||
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter;
|
final String _ariaSecret = "RebootLauncher";
|
||||||
httpClientAdapter.createHttpClient = () {
|
|
||||||
final client = HttpClient();
|
|
||||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
|
||||||
return client;
|
|
||||||
};
|
|
||||||
return dio;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
|
|
||||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||||
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
|
final List<FortniteBuild> downloadableBuilds = [
|
||||||
const String _unavailableError = "The build downloader is not available right now";
|
FortniteBuild(version: Version.parse("1.7.2"), link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
|
||||||
const String _genericError = "The build downloader is not working correctly";
|
FortniteBuild(version: Version.parse("1.8"), link: "https://public.simplyblk.xyz/1.8.rar", available: true),
|
||||||
const int _maxErrors = 100;
|
FortniteBuild(version: Version.parse("1.8.1"), link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("1.8.2"), link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
|
||||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
FortniteBuild(version: Version.parse("1.9"), link: "https://public.simplyblk.xyz/1.9.rar", available: true),
|
||||||
final response = await _dio.get<String>(
|
FortniteBuild(version: Version.parse("1.9.1"), link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
|
||||||
_archiveSourceUrl,
|
FortniteBuild(version: Version.parse("1.10"), link: "https://public.simplyblk.xyz/1.10.rar", available: true),
|
||||||
options: Options(
|
FortniteBuild(version: Version.parse("1.11"), link: "https://public.simplyblk.xyz/1.11.zip", available: true),
|
||||||
responseType: ResponseType.plain
|
FortniteBuild(version: Version.parse("2.1.0"), link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
|
||||||
)
|
FortniteBuild(version: Version.parse("2.2.0"), link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
|
||||||
);
|
FortniteBuild(version: Version.parse("2.3"), link: "https://public.simplyblk.xyz/2.3.rar", available: true),
|
||||||
if (response.statusCode != 200) {
|
FortniteBuild(version: Version.parse("2.4.0"), link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
|
||||||
return [];
|
FortniteBuild(version: Version.parse("2.4.2"), link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("2.5.0"), link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("3.0"), link: "https://public.simplyblk.xyz/3.0.zip", available: true),
|
||||||
var results = <FortniteBuild>[];
|
FortniteBuild(version: Version.parse("3.1"), link: "https://public.simplyblk.xyz/3.1.rar", available: true),
|
||||||
for (final line in response.data?.split("\n") ?? []) {
|
FortniteBuild(version: Version.parse("3.1.1"), link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
|
||||||
if (!line.startsWith("|")) {
|
FortniteBuild(version: Version.parse("3.2"), link: "https://public.simplyblk.xyz/3.2.zip", available: true),
|
||||||
continue;
|
FortniteBuild(version: Version.parse("3.3"), link: "https://public.simplyblk.xyz/3.3.rar", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("3.5"), link: "https://public.simplyblk.xyz/3.5.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("3.6"), link: "https://public.simplyblk.xyz/3.6.zip", available: true),
|
||||||
var parts = line.substring(1, line.length - 1).split("|");
|
FortniteBuild(version: Version.parse("4.0"), link: "https://public.simplyblk.xyz/4.0.zip", available: true),
|
||||||
if (parts.isEmpty) {
|
FortniteBuild(version: Version.parse("4.1"), link: "https://public.simplyblk.xyz/4.1.zip", available: true),
|
||||||
continue;
|
FortniteBuild(version: Version.parse("4.2"), link: "https://public.simplyblk.xyz/4.2.zip", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("4.4"), link: "https://public.simplyblk.xyz/4.4.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("4.5"), link: "https://public.simplyblk.xyz/4.5.rar", available: true),
|
||||||
var versionName = parts.first.trim();
|
FortniteBuild(version: Version.parse("5.00"), link: "https://public.simplyblk.xyz/5.00.rar", available: true),
|
||||||
final separator = versionName.indexOf("-");
|
FortniteBuild(version: Version.parse("5.0.1"), link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
|
||||||
if(separator != -1) {
|
FortniteBuild(version: Version.parse("5.10"), link: "https://public.simplyblk.xyz/5.10.rar", available: true),
|
||||||
versionName = versionName.substring(0, separator);
|
FortniteBuild(version: Version.parse("5.21"), link: "https://public.simplyblk.xyz/5.21.rar", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("5.30"), link: "https://public.simplyblk.xyz/5.30.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("5.40"), link: "https://public.simplyblk.xyz/5.40.rar", available: true),
|
||||||
final link = parts.last.trim();
|
FortniteBuild(version: Version.parse("6.00"), link: "https://public.simplyblk.xyz/6.00.rar", available: true),
|
||||||
try {
|
FortniteBuild(version: Version.parse("6.01"), link: "https://public.simplyblk.xyz/6.01.rar", available: true),
|
||||||
results.add(FortniteBuild(
|
FortniteBuild(version: Version.parse("6.1.1"), link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
|
||||||
version: Version.parse(versionName),
|
FortniteBuild(version: Version.parse("6.02"), link: "https://public.simplyblk.xyz/6.02.rar", available: true),
|
||||||
link: link,
|
FortniteBuild(version: Version.parse("6.2.1"), link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
|
||||||
available: link.endsWith(".zip") || link.endsWith(".rar")
|
FortniteBuild(version: Version.parse("6.10"), link: "https://public.simplyblk.xyz/6.10.rar", available: true),
|
||||||
));
|
FortniteBuild(version: Version.parse("6.10.1"), link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
|
||||||
} on FormatException {
|
FortniteBuild(version: Version.parse("6.10.2"), link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
|
||||||
// Ignore
|
FortniteBuild(version: Version.parse("6.21"), link: "https://public.simplyblk.xyz/6.21.rar", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("6.22"), link: "https://public.simplyblk.xyz/6.22.rar", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("6.30"), link: "https://public.simplyblk.xyz/6.30.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("6.31"), link: "https://public.simplyblk.xyz/6.31.rar", available: true),
|
||||||
return results;
|
FortniteBuild(version: Version.parse("7.00"), link: "https://public.simplyblk.xyz/7.00.rar", available: true),
|
||||||
}
|
FortniteBuild(version: Version.parse("7.10"), link: "https://public.simplyblk.xyz/7.10.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("7.20"), link: "https://public.simplyblk.xyz/7.20.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("7.30"), link: "https://public.simplyblk.xyz/7.30.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("7.40"), link: "https://public.simplyblk.xyz/7.40.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("8.00"), link: "https://public.simplyblk.xyz/8.00.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("8.20"), link: "https://public.simplyblk.xyz/8.20.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("8.30"), link: "https://public.simplyblk.xyz/8.30.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("8.40"), link: "https://public.simplyblk.xyz/8.40.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("8.50"), link: "https://public.simplyblk.xyz/8.50.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("8.51"), link: "https://public.simplyblk.xyz/8.51.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.00"), link: "https://public.simplyblk.xyz/9.00.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.01"), link: "https://public.simplyblk.xyz/9.01.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.10"), link: "https://public.simplyblk.xyz/9.10.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.21"), link: "https://public.simplyblk.xyz/9.21.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.30"), link: "https://public.simplyblk.xyz/9.30.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.40"), link: "https://public.simplyblk.xyz/9.40.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("9.41"), link: "https://public.simplyblk.xyz/9.41.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("10.00"), link: "https://public.simplyblk.xyz/10.00.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("10.10"), link: "https://public.simplyblk.xyz/10.10.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("10.20"), link: "https://public.simplyblk.xyz/10.20.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("10.31"), link: "https://public.simplyblk.xyz/10.31.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("10.40"), link: "https://public.simplyblk.xyz/10.40.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("11.00"), link: "https://public.simplyblk.xyz/11.00.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("11.31"), link: "https://public.simplyblk.xyz/11.31.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("12.00"), link: "https://public.simplyblk.xyz/12.00.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("12.21"), link: "https://public.simplyblk.xyz/12.21.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("12.50"), link: "https://public.simplyblk.xyz/12.50.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("12.61"), link: "https://public.simplyblk.xyz/12.61.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("13.00"), link: "https://public.simplyblk.xyz/13.00.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("13.40"), link: "https://public.simplyblk.xyz/13.40.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("14.00"), link: "https://public.simplyblk.xyz/14.00.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("14.40"), link: "https://public.simplyblk.xyz/14.40.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("14.60"), link: "https://public.simplyblk.xyz/14.60.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("15.30"), link: "https://public.simplyblk.xyz/15.30.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("16.40"), link: "https://public.simplyblk.xyz/16.40.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("17.30"), link: "https://public.simplyblk.xyz/17.30.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("17.50"), link: "https://public.simplyblk.xyz/17.50.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("18.40"), link: "https://public.simplyblk.xyz/18.40.zip", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("19.10"), link: "https://public.simplyblk.xyz/19.10.rar", available: true),
|
||||||
|
FortniteBuild(version: Version.parse("20.40"), link: "https://public.simplyblk.xyz/20.40.zip", available: true),
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
||||||
|
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||||
|
final outputFile = File("${options.destination.path}\\.build\\$fileName");
|
||||||
try {
|
try {
|
||||||
final stopped = _setupLifecycle(options);
|
final stopped = _setupLifecycle(options);
|
||||||
final outputDir = Directory("${options.destination.path}\\.build");
|
await outputFile.parent.create(recursive: true);
|
||||||
await outputDir.create(recursive: true);
|
|
||||||
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
|
||||||
final extension = path.extension(fileName);
|
|
||||||
final tempFile = File("${outputDir.path}\\$fileName");
|
|
||||||
if(await tempFile.exists()) {
|
|
||||||
await tempFile.delete(recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
final downloadItemCompleter = Completer<File>();
|
||||||
final response = _downloadArchive(options, stopped, tempFile, startTime);
|
|
||||||
await Future.any([stopped.future, response]);
|
|
||||||
if(!stopped.isCompleted) {
|
|
||||||
await _extractArchive(stopped, extension, tempFile, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(outputDir);
|
await _startAriaServer();
|
||||||
}catch(error) {
|
final downloadId = await _startAriaDownload(options, outputFile);
|
||||||
_onError(error, options);
|
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
|
||||||
}
|
try {
|
||||||
}
|
final statusRequestId = Uuid().toString().replaceAll("-", "");
|
||||||
|
final statusRequest = {
|
||||||
|
"jsonrcp": "2.0",
|
||||||
|
"id": statusRequestId,
|
||||||
|
"method": "aria2.tellStatus",
|
||||||
|
"params": [
|
||||||
|
"token:${_ariaSecret}",
|
||||||
|
downloadId
|
||||||
|
]
|
||||||
|
};
|
||||||
|
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
|
||||||
|
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
|
||||||
|
if(statusResponseJson == null) {
|
||||||
|
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, Completer stopped, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
|
final result = statusResponseJson["result"];
|
||||||
var received = byteStart ?? 0;
|
final files = result["files"] as List?;
|
||||||
try {
|
if(files == null || files.isEmpty) {
|
||||||
await _dio.download(
|
downloadItemCompleter.completeError("Download aborted");
|
||||||
options.build.link,
|
timer.cancel();
|
||||||
tempFile.path,
|
return;
|
||||||
onReceiveProgress: (data, length) {
|
}
|
||||||
if(stopped.isCompleted) {
|
|
||||||
throw StateError("Download interrupted");
|
final error = result["errorCode"];
|
||||||
|
if(error != null) {
|
||||||
|
final errorCode = int.tryParse(error);
|
||||||
|
if(errorCode == 0) {
|
||||||
|
final path = File(files[0]["path"]);
|
||||||
|
downloadItemCompleter.complete(path);
|
||||||
|
}else if(errorCode == 3) {
|
||||||
|
downloadItemCompleter.completeError("This build is not available yet");
|
||||||
|
}else {
|
||||||
|
final errorMessage = result["errorMessage"];
|
||||||
|
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
|
||||||
}
|
}
|
||||||
|
|
||||||
received = data;
|
timer.cancel();
|
||||||
final percentage = (received / length) * 100;
|
return;
|
||||||
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options);
|
}
|
||||||
},
|
|
||||||
deleteOnError: false,
|
|
||||||
options: Options(
|
|
||||||
validateStatus: (statusCode) {
|
|
||||||
if(statusCode == 200) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(statusCode == 403 || statusCode == 503) {
|
final speed = int.parse(result["downloadSpeed"] ?? "0");
|
||||||
throw _deniedConnectionError;
|
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
|
||||||
}
|
final totalLength = int.parse(files[0]["length"] ?? "0");
|
||||||
|
|
||||||
if(statusCode == 404) {
|
final percentage = completedLength * 100 / totalLength;
|
||||||
throw _unavailableError;
|
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
|
||||||
}
|
_onProgress(
|
||||||
|
options.port,
|
||||||
|
percentage,
|
||||||
|
speed,
|
||||||
|
minutesLeft,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}catch(error) {
|
||||||
|
throw "Invalid download status (${error})";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
throw _genericError;
|
await Future.any([stopped.future, downloadItemCompleter.future]);
|
||||||
},
|
if(!stopped.isCompleted) {
|
||||||
headers: byteStart == null || byteStart <= 0 ? {
|
final extension = path.extension(fileName);
|
||||||
"Cookie": "_c_t_c=1"
|
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
|
||||||
} : {
|
}else {
|
||||||
"Cookie": "_c_t_c=1",
|
await _stopAriaDownload(downloadId);
|
||||||
"Range": "bytes=${byteStart}-"
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
if(stopped.isCompleted) {
|
_onError(error, options);
|
||||||
return;
|
}finally {
|
||||||
}
|
delete(outputFile);
|
||||||
|
|
||||||
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
|
|
||||||
_onError(error, options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _downloadArchive(options, stopped, tempFile, startTime, received, errorsCount + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _startAriaServer() async {
|
||||||
|
await stopDownloadServer();
|
||||||
|
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
|
||||||
|
if(!aria2c.existsSync()) {
|
||||||
|
throw "Missing aria2c.exe";
|
||||||
|
}
|
||||||
|
|
||||||
|
final process = await startProcess(
|
||||||
|
executable: aria2c,
|
||||||
|
args: [
|
||||||
|
"--max-connection-per-server=${Platform.numberOfProcessors}",
|
||||||
|
"--split=${Platform.numberOfProcessors}",
|
||||||
|
"--enable-rpc",
|
||||||
|
"--rpc-listen-all=true",
|
||||||
|
"--rpc-allow-origin-all",
|
||||||
|
"--rpc-secret=$_ariaSecret",
|
||||||
|
"--rpc-listen-port=$_ariaPort",
|
||||||
|
"--file-allocation=none",
|
||||||
|
"--check-certificate=false"
|
||||||
|
],
|
||||||
|
window: false
|
||||||
|
);
|
||||||
|
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
|
||||||
|
process.stdError.listen((error) => log("[ARIA] Error: $error"));
|
||||||
|
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
|
||||||
|
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
|
||||||
|
if(await _isAriaRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
throw "cannot start download server (timeout exceeded)";
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _isAriaRunning() async {
|
||||||
|
try {
|
||||||
|
final statusRequestId = Uuid().toString().replaceAll("-", "");
|
||||||
|
final statusRequest = {
|
||||||
|
"jsonrcp": "2.0",
|
||||||
|
"id": statusRequestId,
|
||||||
|
"method": "aria2.getVersion",
|
||||||
|
"params": [
|
||||||
|
"token:${_ariaSecret}"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
|
||||||
|
return response.statusCode == 200;
|
||||||
|
}catch(_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
|
||||||
|
http.Response? addDownloadResponse;
|
||||||
|
try {
|
||||||
|
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
|
||||||
|
final addDownloadRequest = {
|
||||||
|
"jsonrcp": "2.0",
|
||||||
|
"id": addDownloadRequestId,
|
||||||
|
"method": "aria2.addUri",
|
||||||
|
"params": [
|
||||||
|
"token:${_ariaSecret}",
|
||||||
|
[options.build.link],
|
||||||
|
{
|
||||||
|
"dir": outputFile.parent.path,
|
||||||
|
"out": path.basename(outputFile.path)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
|
||||||
|
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
|
||||||
|
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
|
||||||
|
if(downloadId == null) {
|
||||||
|
throw "Start failed (${addDownloadResponse.body})";
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadId;
|
||||||
|
}catch(error) {
|
||||||
|
throw "Start failed (${addDownloadResponse?.body ?? error})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopAriaDownload(String downloadId) async {
|
||||||
|
try {
|
||||||
|
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
|
||||||
|
final addDownloadRequest = {
|
||||||
|
"jsonrcp": "2.0",
|
||||||
|
"id": addDownloadRequestId,
|
||||||
|
"method": "aria2.forceRemove",
|
||||||
|
"params": [
|
||||||
|
"token:${_ariaSecret}",
|
||||||
|
downloadId
|
||||||
|
]
|
||||||
|
};
|
||||||
|
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
|
||||||
|
stopDownloadServer();
|
||||||
|
}catch(error) {
|
||||||
|
throw "Stop failed (${error})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopDownloadServer() async {
|
||||||
|
await killProcessByPort(_ariaPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
|
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
|
||||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
Process? process;
|
Process? process;
|
||||||
switch (extension.toLowerCase()) {
|
switch (extension.toLowerCase()) {
|
||||||
case ".zip":
|
case ".zip":
|
||||||
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
|
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
|
||||||
if(!sevenZip.existsSync()) {
|
if(!sevenZip.existsSync()) {
|
||||||
throw "Corrupted installation: missing 7zip.exe";
|
throw "Missing 7zip.exe";
|
||||||
}
|
}
|
||||||
|
|
||||||
process = await startProcess(
|
process = await startProcess(
|
||||||
@@ -176,10 +322,15 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
);
|
);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
process.stdOutput.listen((data) {
|
process.stdOutput.listen((data) {
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
if(data.toLowerCase().contains("everything is ok")) {
|
if(data.toLowerCase().contains("everything is ok")) {
|
||||||
completed = true;
|
completed = true;
|
||||||
_onProgress(startTime, now, 100, true, options);
|
_onProgress(
|
||||||
|
options.port,
|
||||||
|
100,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
process?.kill(ProcessSignal.sigabrt);
|
process?.kill(ProcessSignal.sigabrt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -190,7 +341,13 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
}
|
}
|
||||||
|
|
||||||
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
|
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
|
||||||
_onProgress(startTime, now, percentage, true, options);
|
_onProgress(
|
||||||
|
options.port,
|
||||||
|
percentage,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
process.stdError.listen((data) {
|
process.stdError.listen((data) {
|
||||||
if(!data.isBlank) {
|
if(!data.isBlank) {
|
||||||
@@ -206,7 +363,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
case ".rar":
|
case ".rar":
|
||||||
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
||||||
if(!winrar.existsSync()) {
|
if(!winrar.existsSync()) {
|
||||||
throw "Corrupted installation: missing winrar.exe";
|
throw "Missing winrar.exe";
|
||||||
}
|
}
|
||||||
|
|
||||||
process = await startProcess(
|
process = await startProcess(
|
||||||
@@ -221,11 +378,16 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
);
|
);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
process.stdOutput.listen((data) {
|
process.stdOutput.listen((data) {
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
||||||
if(data == "All OK") {
|
if(data == "All OK") {
|
||||||
completed = true;
|
completed = true;
|
||||||
_onProgress(startTime, now, 100, true, options);
|
_onProgress(
|
||||||
|
options.port,
|
||||||
|
100,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
process?.kill(ProcessSignal.sigabrt);
|
process?.kill(ProcessSignal.sigabrt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -236,7 +398,13 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
}
|
}
|
||||||
|
|
||||||
final percentage = int.parse(element).toDouble();
|
final percentage = int.parse(element).toDouble();
|
||||||
_onProgress(startTime, now, percentage, true, options);
|
_onProgress(
|
||||||
|
options.port,
|
||||||
|
percentage,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
process.stdError.listen((data) {
|
process.stdError.listen((data) {
|
||||||
if(!data.isBlank) {
|
if(!data.isBlank) {
|
||||||
@@ -257,21 +425,22 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
process.kill(ProcessSignal.sigabrt);
|
process.kill(ProcessSignal.sigabrt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
|
||||||
if(percentage == 0) {
|
if(percentage == 0) {
|
||||||
options.port.send(FortniteBuildDownloadProgress(
|
port.send(FortniteBuildDownloadProgress(
|
||||||
progress: percentage,
|
progress: percentage,
|
||||||
extracting: extracting
|
extracting: extracting,
|
||||||
|
timeLeft: null,
|
||||||
|
speed: speed
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now;
|
port.send(FortniteBuildDownloadProgress(
|
||||||
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
|
|
||||||
options.port.send(FortniteBuildDownloadProgress(
|
|
||||||
progress: percentage,
|
progress: percentage,
|
||||||
extracting: extracting,
|
extracting: extracting,
|
||||||
minutesLeft: minutesLeft
|
timeLeft: minutesLeft,
|
||||||
|
speed: speed
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,4 +460,5 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
|||||||
});
|
});
|
||||||
options.port.send(lifecyclePort.sendPort);
|
options.port.send(lifecyclePort.sendPort);
|
||||||
return stopped;
|
return stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,42 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
bool _watcher = false;
|
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
|
||||||
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
|
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
|
||||||
const String kRebootDownloadUrl =
|
|
||||||
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
|
const String kRebootBelowS20DownloadUrl =
|
||||||
|
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
|
||||||
|
const String kRebootAboveS20DownloadUrl =
|
||||||
|
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
|
||||||
|
|
||||||
|
const String _kRebootBelowS20FallbackDownloadUrl =
|
||||||
|
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip";
|
||||||
|
const String _kRebootAboveS20FallbackDownloadUrl =
|
||||||
|
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.zip";
|
||||||
|
|
||||||
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
||||||
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||||
final exists = await rebootDllFile.exists();
|
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> downloadCriticalDll(String name, String outputPath) async {
|
Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
|
||||||
|
String? name;
|
||||||
|
switch(dll) {
|
||||||
|
case InjectableDll.console:
|
||||||
|
name = "console.dll";
|
||||||
|
case InjectableDll.auth:
|
||||||
|
name = "cobalt.dll";
|
||||||
|
case InjectableDll.memoryLeak:
|
||||||
|
name = "memory.dll";
|
||||||
|
case InjectableDll.gameServer:
|
||||||
|
name = null;
|
||||||
|
}
|
||||||
|
if(name == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
|
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
|
||||||
if(response.statusCode != 200) {
|
if(response.statusCode != 200) {
|
||||||
throw Exception("Cannot download $name: status code ${response.statusCode}");
|
throw Exception("Cannot download $name: status code ${response.statusCode}");
|
||||||
@@ -28,13 +51,15 @@ Future<void> downloadCriticalDll(String name, String outputPath) async {
|
|||||||
await output.writeAsBytes(response.bodyBytes, flush: true);
|
await output.writeAsBytes(response.bodyBytes, flush: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> downloadRebootDll(String url) async {
|
Future<void> downloadRebootDll(File file, String url, bool aboveS20) async {
|
||||||
Directory? outputDir;
|
Directory? outputDir;
|
||||||
final now = DateTime.now();
|
|
||||||
try {
|
try {
|
||||||
final response = await http.get(Uri.parse(url));
|
var response = await http.get(Uri.parse(url));
|
||||||
if(response.statusCode != 200) {
|
if(response.statusCode != 200) {
|
||||||
throw Exception("Cannot download reboot.zip: status code ${response.statusCode}");
|
response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl));
|
||||||
|
if(response.statusCode != 200) {
|
||||||
|
throw Exception("status code ${response.statusCode}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outputDir = await installationDirectory.createTemp("reboot_out");
|
outputDir = await installationDirectory.createTemp("reboot_out");
|
||||||
@@ -42,8 +67,7 @@ Future<int> downloadRebootDll(String url) async {
|
|||||||
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
|
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
|
||||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||||
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
|
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
|
||||||
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
|
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
|
||||||
return now.millisecondsSinceEpoch;
|
|
||||||
} finally{
|
} finally{
|
||||||
if(outputDir != null) {
|
if(outputDir != null) {
|
||||||
delete(outputDir);
|
delete(outputDir);
|
||||||
@@ -55,17 +79,4 @@ Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
|||||||
return lastUpdateMs != null
|
return lastUpdateMs != null
|
||||||
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
|
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<String> watchDlls() async* {
|
|
||||||
if(_watcher) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_watcher = true;
|
|
||||||
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
|
|
||||||
if (event.path.endsWith(".dll")) {
|
|
||||||
yield event.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,10 +8,7 @@ import 'dart:isolate';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_common/src/util/log.dart';
|
|
||||||
import 'package:sync/semaphore.dart';
|
|
||||||
import 'package:win32/win32.dart';
|
import 'package:win32/win32.dart';
|
||||||
|
|
||||||
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
||||||
@@ -98,8 +95,8 @@ Future<bool> startElevatedProcess({required String executable, required String a
|
|||||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||||
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
|
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
|
||||||
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
|
||||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||||
return ShellExecuteEx(shellInput) == 1;
|
return ShellExecuteEx(shellInput) == 1;
|
||||||
@@ -154,46 +151,21 @@ final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
|||||||
int Function(int hWnd)>('NtSuspendProcess');
|
int Function(int hWnd)>('NtSuspendProcess');
|
||||||
|
|
||||||
bool suspend(int pid) {
|
bool suspend(int pid) {
|
||||||
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||||
final result = _NtSuspendProcess(processHandle);
|
|
||||||
CloseHandle(processHandle);
|
|
||||||
return result == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool resume(int pid) {
|
|
||||||
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
|
||||||
final result = _NtResumeProcess(processHandle);
|
|
||||||
CloseHandle(processHandle);
|
|
||||||
return result == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _watchProcess(int pid) {
|
|
||||||
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
|
||||||
try {
|
try {
|
||||||
WaitForSingleObject(processHandle, INFINITE);
|
return _NtSuspendProcess(processHandle) == 0;
|
||||||
}finally {
|
} finally {
|
||||||
CloseHandle(processHandle);
|
CloseHandle(processHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> watchProcess(int pid) async {
|
bool resume(int pid) {
|
||||||
var completer = Completer<bool>();
|
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||||
var exitPort = ReceivePort();
|
try {
|
||||||
exitPort.listen((_) {
|
return _NtResumeProcess(processHandle) == 0;
|
||||||
if(!completer.isCompleted) {
|
} finally {
|
||||||
completer.complete(true);
|
CloseHandle(processHandle);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
var errorPort = ReceivePort();
|
|
||||||
errorPort.listen((_) => completer.complete(false));
|
|
||||||
await Isolate.spawn(
|
|
||||||
_watchProcess,
|
|
||||||
pid,
|
|
||||||
onExit: exitPort.sendPort,
|
|
||||||
onError: errorPort.sendPort,
|
|
||||||
errorsAreFatal: true
|
|
||||||
);
|
|
||||||
return await completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
|
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
|
||||||
@@ -278,17 +250,13 @@ void handleGameOutput({
|
|||||||
}else if(line.contains(kGameFinishedLine) && host) {
|
}else if(line.contains(kGameFinishedLine) && host) {
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
|
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
|
||||||
onMatchEnd();
|
onMatchEnd();
|
||||||
}else if(line.contains(kDisplayInitializedLine) && host) {
|
}else if(line.contains(kDisplayLine) && line.contains(kDisplayInitializedLine) && host) {
|
||||||
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
|
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
|
||||||
onDisplayAttached();
|
onDisplayAttached();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _parseUsername(String username, bool host) {
|
String _parseUsername(String username, bool host) {
|
||||||
if(host) {
|
|
||||||
return "Player${Random().nextInt(1000)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username.isEmpty) {
|
if (username.isEmpty) {
|
||||||
return kDefaultPlayerName;
|
return kDefaultPlayerName;
|
||||||
}
|
}
|
||||||
@@ -310,16 +278,8 @@ final class _ExtendedProcess implements Process {
|
|||||||
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
|
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
|
||||||
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
|
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> get exitCode {
|
Future<int> get exitCode => _delegate.exitCode;
|
||||||
try {
|
|
||||||
return _delegate.exitCode;
|
|
||||||
}catch(_) {
|
|
||||||
return watchProcess(_delegate.pid)
|
|
||||||
.then((_) => -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
||||||
|
|||||||
@@ -7,19 +7,18 @@ environment:
|
|||||||
sdk: ">=3.0.0 <=4.0.0"
|
sdk: ">=3.0.0 <=4.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
dio: ^5.3.2
|
win32: ^5.5.4
|
||||||
win32: 3.0.0
|
ffi: ^2.1.3
|
||||||
ffi: ^2.1.0
|
path: ^1.9.0
|
||||||
path: ^1.8.3
|
http: ^1.2.2
|
||||||
http: ^1.1.0
|
crypto: ^3.0.5
|
||||||
crypto: ^3.0.2
|
archive: ^3.6.1
|
||||||
archive: ^3.3.7
|
|
||||||
ini: ^2.1.0
|
ini: ^2.1.0
|
||||||
shelf_proxy: ^1.0.2
|
shelf_proxy: ^1.0.2
|
||||||
sync: ^0.3.0
|
sync: ^0.3.0
|
||||||
uuid: ^3.0.6
|
uuid: ^4.5.1
|
||||||
shelf_web_socket: ^2.0.0
|
shelf_web_socket: ^2.0.0
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^5.0.0
|
||||||
@@ -1,16 +1,39 @@
|
|||||||
# reboot_launcher
|
|
||||||
|
|
||||||
Launcher for project reboot
|
# Reboot Launcher
|
||||||
|
|
||||||
|
Welcome to the **Reboot Launcher**!
|
||||||
|
This is a GUI application developed as part of the **Reboot Project**.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
### Running the Project
|
||||||
|
To launch the project in development mode, simply run:
|
||||||
|
```
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
### Building the Project
|
||||||
|
To create a production-ready build, use:
|
||||||
|
```
|
||||||
|
flutter build
|
||||||
|
```
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
### Packaging the Project
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
To package the application for distribution, run:
|
||||||
|
```
|
||||||
|
package.bat
|
||||||
|
```
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
## Requirements
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
- [Flutter SDK](https://flutter.dev/docs/get-started/install)
|
||||||
samples, guidance on mobile development, and a full API reference.
|
- Supported operating systems: Windows
|
||||||
|
|
||||||
|
## Other platforms
|
||||||
|
|
||||||
|
Native support for these platforms is not currently planned, but Linux support is a priority for the 10.0 release cycle
|
||||||
|
|
||||||
|
- [Linux Tutorial using Proton](https://www.reddit.com/r/linux_gaming/comments/1fwa4l8/guide_running_a_fortnite_private_server_to_play/)
|
||||||
|
- No tutorials are available for MacOS(got lost when the Reboot discord was banned), but it's possible to run Reboot using a compatibility layer
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Contributions are welcome! Feel free to open an issue or submit a pull request.
|
||||||
BIN
gui/assets/build/aria2c.exe
Normal file
BIN
gui/assets/build/aria2c.exe
Normal file
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
taskkill /f /im winrar.exe
|
|
||||||
taskkill /f /im tar.exe
|
|
||||||
BIN
gui/dependencies/dlls/RebootFallback.zip
Normal file
BIN
gui/dependencies/dlls/RebootFallback.zip
Normal file
Binary file not shown.
BIN
gui/dependencies/dlls/RebootS20Fallback.zip
Normal file
BIN
gui/dependencies/dlls/RebootS20Fallback.zip
Normal file
Binary file not shown.
Binary file not shown.
BIN
gui/dependencies/dlls/starfall.dll
Normal file
BIN
gui/dependencies/dlls/starfall.dll
Normal file
Binary file not shown.
@@ -76,7 +76,7 @@
|
|||||||
"playGameServerCustomContent": "Enter IP",
|
"playGameServerCustomContent": "Enter IP",
|
||||||
"settingsName": "Settings",
|
"settingsName": "Settings",
|
||||||
"settingsClientName": "Internal files",
|
"settingsClientName": "Internal files",
|
||||||
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite",
|
"settingsClientDescription": "Configure the internal files used by the launcher",
|
||||||
"settingsClientOptionsName": "Options",
|
"settingsClientOptionsName": "Options",
|
||||||
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
|
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
|
||||||
"settingsClientConsoleName": "Unreal engine patcher",
|
"settingsClientConsoleName": "Unreal engine patcher",
|
||||||
@@ -94,18 +94,19 @@
|
|||||||
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
||||||
"settingsServerOptionsName": "Options",
|
"settingsServerOptionsName": "Options",
|
||||||
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
||||||
"settingsServerTypeName": "Type",
|
"settingsServerTypeName": "Game server type",
|
||||||
"settingsServerTypeDescription": "The type of game server to inject",
|
"settingsServerTypeDescription": "The type of game server to inject",
|
||||||
"settingsServerTypeEmbeddedName": "Embedded",
|
"settingsServerTypeEmbeddedName": "Embedded",
|
||||||
"settingsServerTypeCustomName": "Custom",
|
"settingsServerTypeCustomName": "Custom",
|
||||||
"settingsServerFileName": "Implementation",
|
"settingsOldServerFileName": "Game server",
|
||||||
"settingsServerFileDescription": "The file injected to create the game server",
|
"settingsServerFileDescription": "The file injected to create the game server",
|
||||||
"settingsServerPortName": "Port",
|
"settingsServerPortName": "Port",
|
||||||
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
|
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
|
||||||
"settingsServerMirrorName": "Update mirror",
|
"settingsServerOldMirrorName": "Update mirror (Before season 20)",
|
||||||
|
"settingsServerNewMirrorName": "Update mirror (Season 20 and above)",
|
||||||
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
||||||
"settingsServerMirrorPlaceholder": "mirror",
|
"settingsServerMirrorPlaceholder": "mirror",
|
||||||
"settingsServerTimerName": "Update timer",
|
"settingsServerTimerName": "Game server updater",
|
||||||
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
|
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
|
||||||
"settingsUtilsName": "Launcher",
|
"settingsUtilsName": "Launcher",
|
||||||
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
||||||
@@ -127,7 +128,7 @@
|
|||||||
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
||||||
"addLocalBuildName": "Add a version from this PC's local storage",
|
"addLocalBuildName": "Add a version from this PC's local storage",
|
||||||
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
|
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
|
||||||
"addVersion": "Add version",
|
"addVersion": "New version",
|
||||||
"downloadBuildName": "Download any version from the cloud",
|
"downloadBuildName": "Download any version from the cloud",
|
||||||
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
||||||
"downloadBuildContent": "Download build",
|
"downloadBuildContent": "Download build",
|
||||||
@@ -215,6 +216,7 @@
|
|||||||
"downloadedVersion": "The download was completed successfully!",
|
"downloadedVersion": "The download was completed successfully!",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloading": "Downloading...",
|
"downloading": "Downloading...",
|
||||||
|
"startingDownload": "Starting download...",
|
||||||
"extracting": "Extracting...",
|
"extracting": "Extracting...",
|
||||||
"buildProgress": "{progress}%",
|
"buildProgress": "{progress}%",
|
||||||
"buildInstallationDirectory": "Installation directory",
|
"buildInstallationDirectory": "Installation directory",
|
||||||
@@ -234,7 +236,7 @@
|
|||||||
"startGame": "Start fortnite",
|
"startGame": "Start fortnite",
|
||||||
"stopGame": "Close fortnite",
|
"stopGame": "Close fortnite",
|
||||||
"waitingForGameServer": "Waiting for the game server to boot up...",
|
"waitingForGameServer": "Waiting for the game server to boot up...",
|
||||||
"gameServerStartWarning": "The game server was started successfully, but Reboot didn't load",
|
"gameServerStartWarning": "Unsupported version: the game server crashed while setting up the server",
|
||||||
"gameServerStartLocalWarning": "The game server was started successfully, but other players can't join",
|
"gameServerStartLocalWarning": "The game server was started successfully, but other players can't join",
|
||||||
"gameServerStarted": "The game server was started successfully",
|
"gameServerStarted": "The game server was started successfully",
|
||||||
"gameClientStarted": "The game client was started successfully",
|
"gameClientStarted": "The game client was started successfully",
|
||||||
@@ -324,6 +326,8 @@
|
|||||||
"backendErrorMessage": "The backend reported an unexpected error",
|
"backendErrorMessage": "The backend reported an unexpected error",
|
||||||
"welcomeTitle": "Welcome to Reboot Launcher",
|
"welcomeTitle": "Welcome to Reboot Launcher",
|
||||||
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
|
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
|
||||||
|
"hostAccountText": "The host tab shows different credentials compared to the play tab.\nIf you are advanced user, you can set a different email and password\nhere if the backend you are using needs authentication.",
|
||||||
|
"hostAccountAction": "I understand",
|
||||||
"welcomeAction": "Take the tour",
|
"welcomeAction": "Take the tour",
|
||||||
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
|
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
|
||||||
"startOnboardingActionLabel": "Let's do it",
|
"startOnboardingActionLabel": "Let's do it",
|
||||||
@@ -368,5 +372,7 @@
|
|||||||
"automaticGameServerDialogStart": "Start server",
|
"automaticGameServerDialogStart": "Start server",
|
||||||
"gameResetDefaultsName": "Reset",
|
"gameResetDefaultsName": "Reset",
|
||||||
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
|
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
|
||||||
"gameResetDefaultsContent": "Reset"
|
"gameResetDefaultsContent": "Reset",
|
||||||
|
"selectFile": "Select a file",
|
||||||
|
"reset": "Reset"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||||
@@ -16,12 +15,12 @@ import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/error.dart';
|
import 'package:reboot_launcher/src/widget/message/error.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
import 'package:reboot_launcher/src/widget/page/home_page.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/url_protocol.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
import 'package:url_protocol/url_protocol.dart';
|
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@@ -83,9 +82,7 @@ Future<void> _startApp() async {
|
|||||||
errors.add(uncaughtError);
|
errors.add(uncaughtError);
|
||||||
} finally{
|
} finally{
|
||||||
log("[APP] Started applications with errors: $errors");
|
log("[APP] Started applications with errors: $errors");
|
||||||
runApp(RebootApplication(
|
runApp(RebootApplication(errors: errors));
|
||||||
errors: errors,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,45 +143,47 @@ Future<Object?> _initVersion() async {
|
|||||||
|
|
||||||
Future<Object?> _initUrlHandler() async {
|
Future<Object?> _initUrlHandler() async {
|
||||||
try {
|
try {
|
||||||
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
|
registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
|
||||||
return null;
|
return null;
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initWindow() => doWhenWindowReady(() async {
|
Future<void> _initWindow() async {
|
||||||
try {
|
try {
|
||||||
await SystemTheme.accentColor.load();
|
await SystemTheme.accentColor.load();
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
await Window.initialize();
|
await Window.initialize();
|
||||||
var settingsController = Get.find<SettingsController>();
|
var settingsController = Get.find<SettingsController>();
|
||||||
var size = Size(settingsController.width, settingsController.height);
|
var size = Size(settingsController.width, settingsController.height);
|
||||||
appWindow.size = size;
|
await windowManager.setSize(size);
|
||||||
var offsetX = settingsController.offsetX;
|
var offsetX = settingsController.offsetX;
|
||||||
var offsetY = settingsController.offsetY;
|
var offsetY = settingsController.offsetY;
|
||||||
if(offsetX != null && offsetY != null){
|
if(offsetX != null && offsetY != null) {
|
||||||
appWindow.position = Offset(
|
final position = Offset(
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY
|
offsetY
|
||||||
);
|
);
|
||||||
|
await windowManager.setPosition(position);
|
||||||
}else {
|
}else {
|
||||||
appWindow.alignment = Alignment.center;
|
await windowManager.setAlignment(Alignment.center);
|
||||||
}
|
}
|
||||||
|
await windowManager.setPreventClose(true);
|
||||||
|
await windowManager.setResizable(true);
|
||||||
if(isWin11) {
|
if(isWin11) {
|
||||||
await Window.setEffect(
|
await Window.setEffect(
|
||||||
effect: WindowEffect.acrylic,
|
effect: WindowEffect.acrylic,
|
||||||
color: Colors.transparent,
|
color: Colors.green,
|
||||||
dark: isDarkMode
|
dark: isDarkMode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}catch(error, stackTrace) {
|
}catch(error, stackTrace) {
|
||||||
onError(error, stackTrace, false);
|
onError(error, stackTrace, false);
|
||||||
}finally {
|
}finally {
|
||||||
appWindow.show();
|
windowManager.show();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
Future<List<Object>> _initStorage() async {
|
Future<List<Object>> _initStorage() async {
|
||||||
final errors = <Object>[];
|
final errors = <Object>[];
|
||||||
@@ -231,7 +230,6 @@ Future<List<Object>> _initStorage() async {
|
|||||||
errors.add(error);
|
errors.add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +251,11 @@ class _RebootApplicationState extends State<RebootApplication> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleErrors(List<Object?> errors) {
|
void _handleErrors(List<Object?> errors) {
|
||||||
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
|
for(final error in errors) {
|
||||||
|
if(error != null) {
|
||||||
|
onError(error, null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class BackendController extends GetxController {
|
class BackendController extends GetxController {
|
||||||
static const String storageName = "backend_storage";
|
static const String storageName = "v2_backend_storage";
|
||||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
@@ -22,10 +34,9 @@ class BackendController extends GetxController {
|
|||||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool detached;
|
late final RxBool detached;
|
||||||
StreamSubscription? worker;
|
late final List<InfoBarEntry> _infoBars;
|
||||||
int? embeddedProcessPid;
|
StreamSubscription? _worker;
|
||||||
HttpServer? localServer;
|
ServerImplementation? _implementation;
|
||||||
HttpServer? remoteServer;
|
|
||||||
|
|
||||||
BackendController() {
|
BackendController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
@@ -35,11 +46,6 @@ class BackendController extends GetxController {
|
|||||||
host.text = _readHost();
|
host.text = _readHost();
|
||||||
port.text = _readPort();
|
port.text = _readPort();
|
||||||
_storage?.write("type", value.index);
|
_storage?.write("type", value.index);
|
||||||
if (!started.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
|
||||||
});
|
});
|
||||||
host = TextEditingController(text: _readHost());
|
host = TextEditingController(text: _readHost());
|
||||||
host.addListener(() =>
|
host.addListener(() =>
|
||||||
@@ -70,31 +76,30 @@ class BackendController extends GetxController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
gameServerAddressFocusNode = FocusNode();
|
gameServerAddressFocusNode = FocusNode();
|
||||||
consoleKey = Rx(_readConsoleKey());
|
consoleKey = Rx(() {
|
||||||
|
final consoleKeyValue = _storage?.read("console_key");
|
||||||
|
if(consoleKeyValue == null) {
|
||||||
|
return _kDefaultConsoleKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
|
||||||
|
if(consoleKeyNumber == null) {
|
||||||
|
return _kDefaultConsoleKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
|
||||||
|
if(!consoleKey.isUnrealEngineKey) {
|
||||||
|
return _kDefaultConsoleKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consoleKey;
|
||||||
|
}());
|
||||||
_writeConsoleKey(consoleKey.value);
|
_writeConsoleKey(consoleKey.value);
|
||||||
consoleKey.listen((newValue) {
|
consoleKey.listen((newValue) {
|
||||||
_storage?.write("console_key", newValue.usbHidUsage);
|
_storage?.write("console_key", newValue.usbHidUsage);
|
||||||
_writeConsoleKey(newValue);
|
_writeConsoleKey(newValue);
|
||||||
});
|
});
|
||||||
}
|
_infoBars = [];
|
||||||
|
|
||||||
PhysicalKeyboardKey _readConsoleKey() {
|
|
||||||
final consoleKeyValue = _storage?.read("console_key");
|
|
||||||
if(consoleKeyValue == null) {
|
|
||||||
return _kDefaultConsoleKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
|
|
||||||
if(consoleKeyNumber == null) {
|
|
||||||
return _kDefaultConsoleKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
|
|
||||||
if(!consoleKey.isUnrealEngineKey) {
|
|
||||||
return _kDefaultConsoleKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return consoleKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
||||||
@@ -103,6 +108,21 @@ class BackendController extends GetxController {
|
|||||||
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
|
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _readHost() {
|
||||||
|
String? value = _storage?.read("${type.value.name}_host");
|
||||||
|
if (value != null && value.isNotEmpty) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.value != ServerType.remote) {
|
||||||
|
return kDefaultBackendHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||||
|
|
||||||
void joinLocalhost() {
|
void joinLocalhost() {
|
||||||
gameServerAddress.text = kDefaultGameServerHost;
|
gameServerAddress.text = kDefaultGameServerHost;
|
||||||
}
|
}
|
||||||
@@ -121,179 +141,407 @@ class BackendController extends GetxController {
|
|||||||
detached.value = false;
|
detached.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _readHost() {
|
Future<bool> toggle() {
|
||||||
String? value = _storage?.read("${type.value.name}_host");
|
if(started.value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
return stop(interactive: true);
|
||||||
return value;
|
}else {
|
||||||
|
return start(interactive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.value != ServerType.remote) {
|
|
||||||
return kDefaultBackendHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _readPort() =>
|
Future<bool> start({required bool interactive}) async {
|
||||||
_storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
if(started.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* {
|
_cancel();
|
||||||
try {
|
final stream = startBackend(
|
||||||
if(started.value) {
|
type: type.value,
|
||||||
return;
|
host: host.text,
|
||||||
}
|
port: port.text,
|
||||||
|
detached: detached.value,
|
||||||
final serverType = type.value;
|
onError: (errorMessage) {
|
||||||
final hostData = this.host.text.trim();
|
stop(interactive: false);
|
||||||
final portData = this.port.text.trim();
|
_showRebootInfoBar(
|
||||||
started.value = true;
|
translations.backendErrorMessage,
|
||||||
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
|
severity: InfoBarSeverity.error,
|
||||||
yield ServerResult(ServerResultType.starting);
|
duration: infoBarLongDuration,
|
||||||
}
|
action: Button(
|
||||||
|
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||||
if (hostData.isEmpty) {
|
child: Text(translations.openLog),
|
||||||
yield ServerResult(ServerResultType.missingHostError);
|
)
|
||||||
started.value = false;
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (portData.isEmpty) {
|
|
||||||
yield ServerResult(ServerResultType.missingPortError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final portNumber = int.tryParse(portData);
|
|
||||||
if (portNumber == null) {
|
|
||||||
yield ServerResult(ServerResultType.illegalPortError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
|
||||||
yield ServerResult(ServerResultType.freeingPort);
|
|
||||||
final result = await freeBackendPort();
|
|
||||||
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
|
||||||
if(!result) {
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
final completer = Completer<bool>();
|
||||||
|
InfoBarEntry? entry;
|
||||||
|
_worker = stream.listen((event) {
|
||||||
|
entry?.close();
|
||||||
|
entry = _handeEvent(event, interactive);
|
||||||
|
if(event.type.isError) {
|
||||||
|
completer.complete(false);
|
||||||
|
}else if(event.type.isSuccess) {
|
||||||
|
completer.complete(true);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return await completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
switch(serverType){
|
Future<bool> stop({required bool interactive}) async {
|
||||||
case ServerType.embedded:
|
if(!started.value) {
|
||||||
final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
|
return true;
|
||||||
if(started.value) {
|
}
|
||||||
started.value = false;
|
|
||||||
onError(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
watchProcess(process.pid).then((_) {
|
|
||||||
if(started.value) {
|
|
||||||
started.value = false;
|
|
||||||
onExit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
embeddedProcessPid = process.pid;
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
yield ServerResult(ServerResultType.pingingRemote);
|
|
||||||
final uriResult = await pingBackend(hostData, portNumber);
|
|
||||||
if(uriResult == null) {
|
|
||||||
yield ServerResult(ServerResultType.pingError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteServer = await startRemoteBackendProxy(uriResult);
|
_cancel();
|
||||||
break;
|
final stream = stopBackend(
|
||||||
case ServerType.local:
|
type: type.value,
|
||||||
if(portNumber != kDefaultBackendPort) {
|
implementation: _implementation
|
||||||
yield ServerResult(ServerResultType.pingingLocal);
|
);
|
||||||
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
|
final completer = Completer<bool>();
|
||||||
if(uriResult == null) {
|
InfoBarEntry? entry;
|
||||||
yield ServerResult(ServerResultType.pingError);
|
_worker = stream.listen((event) {
|
||||||
started.value = false;
|
entry?.close();
|
||||||
return;
|
entry = _handeEvent(event, interactive);
|
||||||
}
|
if(event.type.isError) {
|
||||||
|
completer.complete(false);
|
||||||
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
|
}else if(event.type.isSuccess) {
|
||||||
}else {
|
completer.complete(true);
|
||||||
// If the local server is running on port 3551 there is no reverse proxy running
|
|
||||||
// We only need to check if everything is working
|
|
||||||
started.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return await completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.pingingLocal);
|
void _cancel() {
|
||||||
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
|
_worker?.cancel(); // Do not await or it will hang
|
||||||
if(uriResult == null) {
|
_infoBars.forEach((infoBar) => infoBar.close());
|
||||||
yield ServerResult(ServerResultType.pingError);
|
_infoBars.clear();
|
||||||
remoteServer?.close(force: true);
|
}
|
||||||
localServer?.close(force: true);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.startSuccess);
|
InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
|
||||||
}catch(error, stackTrace) {
|
log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})");
|
||||||
yield ServerResult(
|
started.value = event.type.isStart && !event.type.isError;
|
||||||
ServerResultType.startError,
|
switch (event.type) {
|
||||||
error: error,
|
case ServerResultType.starting:
|
||||||
stackTrace: stackTrace
|
if(interactive) {
|
||||||
);
|
return _showRebootInfoBar(
|
||||||
remoteServer?.close(force: true);
|
translations.startingServer,
|
||||||
localServer?.close(force: true);
|
severity: InfoBarSeverity.info,
|
||||||
started.value = false;
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startSuccess:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.stopping:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.stoppingServer,
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.stopSuccess:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.stoppedServer,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.stopError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.stopServerError(event.error ?? translations.unknownError),
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startMissingHostError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.missingHostNameError,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startMissingPortError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.missingPortError,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startIllegalPortError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.illegalPortError,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startFreeingPort:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.freeingPort,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startFreePortSuccess:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.freedPort,
|
||||||
|
severity: InfoBarSeverity.success,
|
||||||
|
duration: infoBarShortDuration
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startFreePortError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.freePortError(event.error ?? translations.unknownError),
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startPingingRemote:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.pingingServer(ServerType.remote.name),
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startPingingLocal:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.pingingServer(type.value.name),
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startPingError:
|
||||||
|
if(interactive) {
|
||||||
|
return _showRebootInfoBar(
|
||||||
|
translations.pingError(type.value.name),
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
case ServerResultType.startedImplementation:
|
||||||
|
_implementation = event.implementation;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<ServerResult> stop() async* {
|
Future<void> joinServer(String uuid, FortniteServer server) async {
|
||||||
if(!started.value) {
|
if(!kDebugMode && uuid == server.id) {
|
||||||
|
_showRebootInfoBar(
|
||||||
|
translations.joinSelfServer,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.stopping);
|
final version = Get.find<GameController>()
|
||||||
started.value = false;
|
.getVersionByName(server.version.toString());
|
||||||
try{
|
if(version == null) {
|
||||||
switch(type()){
|
_showRebootInfoBar(
|
||||||
case ServerType.embedded:
|
translations.cannotJoinServerVersion(server.version.toString()),
|
||||||
final embeddedProcessPid = this.embeddedProcessPid;
|
duration: infoBarLongDuration,
|
||||||
if(embeddedProcessPid != null) {
|
severity: InfoBarSeverity.error
|
||||||
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
|
|
||||||
this.embeddedProcessPid = null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
await remoteServer?.close(force: true);
|
|
||||||
remoteServer = null;
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
await localServer?.close(force: true);
|
|
||||||
localServer = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
yield ServerResult(ServerResultType.stopSuccess);
|
|
||||||
}catch(error, stackTrace){
|
|
||||||
yield ServerResult(
|
|
||||||
ServerResultType.stopError,
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace
|
|
||||||
);
|
);
|
||||||
started.value = true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hashedPassword = server.password;
|
||||||
|
final hasPassword = hashedPassword != null;
|
||||||
|
final embedded = type.value == ServerType.embedded;
|
||||||
|
final author = server.author;
|
||||||
|
final encryptedIp = server.ip;
|
||||||
|
if(!hasPassword) {
|
||||||
|
final valid = await _isServerValid(encryptedIp);
|
||||||
|
if(!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onServerJoined(embedded, encryptedIp, author, version);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final confirmPassword = await _askForPassword();
|
||||||
|
if(confirmPassword == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||||
|
_showRebootInfoBar(
|
||||||
|
translations.wrongServerPassword,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
||||||
|
final valid = await _isServerValid(decryptedIp);
|
||||||
|
if(!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onServerJoined(embedded, decryptedIp, author, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<ServerResult> toggle({required void Function() onExit, required void Function(String) onError}) async* {
|
Future<bool> _isServerValid(String address) async {
|
||||||
if(started()) {
|
final result = await pingGameServer(address);
|
||||||
yield* stop();
|
if(result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showRebootInfoBar(
|
||||||
|
translations.offlineServer,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _askForPassword() async {
|
||||||
|
final confirmPasswordController = TextEditingController();
|
||||||
|
final showPassword = RxBool(false);
|
||||||
|
final showPasswordTrailing = RxBool(false);
|
||||||
|
return await showRebootDialog<String?>(
|
||||||
|
builder: (context) => FormDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InfoLabel(
|
||||||
|
label: translations.serverPassword,
|
||||||
|
child: Obx(() => TextFormBox(
|
||||||
|
placeholder: translations.serverPasswordPlaceholder,
|
||||||
|
controller: confirmPasswordController,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
obscureText: !showPassword.value,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofocus: true,
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
||||||
|
suffix: !showPasswordTrailing.value ? null : Button(
|
||||||
|
onPressed: () => showPassword.value = !showPassword.value,
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||||
|
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
text: translations.serverPasswordCancel,
|
||||||
|
type: ButtonType.secondary
|
||||||
|
),
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text: translations.serverPasswordConfirm,
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onServerJoined(bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
||||||
|
if(embedded) {
|
||||||
|
gameServerAddress.text = decryptedIp;
|
||||||
|
pageIndex.value = RebootPageType.play.index;
|
||||||
}else {
|
}else {
|
||||||
yield* start(
|
FlutterClipboard.controlC(decryptedIp);
|
||||||
onExit: onExit,
|
}
|
||||||
onError: onError
|
Get.find<GameController>()
|
||||||
);
|
.selectedVersion = version;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
|
||||||
|
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBarEntry _showRebootInfoBar(dynamic text, {
|
||||||
|
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||||
|
bool loading = false,
|
||||||
|
Duration? duration = infoBarShortDuration,
|
||||||
|
void Function()? onDismissed,
|
||||||
|
Widget? action
|
||||||
|
}) {
|
||||||
|
final result = showRebootInfoBar(
|
||||||
|
text,
|
||||||
|
severity: severity,
|
||||||
|
loading: loading,
|
||||||
|
duration: duration,
|
||||||
|
onDismissed: onDismissed,
|
||||||
|
action: action
|
||||||
|
);
|
||||||
|
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
|
||||||
|
_infoBars.add(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> restart() async {
|
||||||
|
if(started.value) {
|
||||||
|
await stop(interactive: false);
|
||||||
|
await start(interactive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,47 +4,45 @@ import 'dart:io';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class DllController extends GetxController {
|
class DllController extends GetxController {
|
||||||
static const String storageName = "dll_storage";
|
static const String storageName = "v2_dll_storage";
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final String originalDll;
|
late final TextEditingController customGameServerDll;
|
||||||
late final TextEditingController gameServerDll;
|
|
||||||
late final TextEditingController unrealEngineConsoleDll;
|
late final TextEditingController unrealEngineConsoleDll;
|
||||||
late final TextEditingController backendDll;
|
late final TextEditingController backendDll;
|
||||||
late final TextEditingController memoryLeakDll;
|
late final TextEditingController memoryLeakDll;
|
||||||
late final TextEditingController gameServerPort;
|
late final TextEditingController gameServerPort;
|
||||||
late final Rx<UpdateTimer> timer;
|
late final Rx<UpdateTimer> timer;
|
||||||
late final TextEditingController url;
|
late final TextEditingController beforeS20Mirror;
|
||||||
|
late final TextEditingController aboveS20Mirror;
|
||||||
late final RxBool customGameServer;
|
late final RxBool customGameServer;
|
||||||
late final RxnInt timestamp;
|
late final RxnInt timestamp;
|
||||||
late final Rx<UpdateStatus> status;
|
late final Rx<UpdateStatus> status;
|
||||||
InfoBarEntry? infoBarEntry;
|
|
||||||
Future<bool>? _updater;
|
|
||||||
|
|
||||||
DllController() {
|
DllController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
gameServerDll = _createController("game_server", InjectableDll.reboot);
|
customGameServerDll = _createController("game_server", InjectableDll.gameServer);
|
||||||
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
||||||
backendDll = _createController("backend", InjectableDll.cobalt);
|
backendDll = _createController("backend", InjectableDll.auth);
|
||||||
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
|
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
|
||||||
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
||||||
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
||||||
final timerIndex = _storage?.read("timer");
|
final timerIndex = _storage?.read("timer");
|
||||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||||
timer.listen((value) => _storage?.write("timer", value.index));
|
timer.listen((value) => _storage?.write("timer", value.index));
|
||||||
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
|
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl);
|
||||||
url.addListener(() => _storage?.write("update_url", url.text));
|
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
|
||||||
|
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
|
||||||
|
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
|
||||||
status = Rx(UpdateStatus.waiting);
|
status = Rx(UpdateStatus.waiting);
|
||||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||||
@@ -59,16 +57,16 @@ class DllController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetGame() {
|
void resetGame() {
|
||||||
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
|
customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
|
||||||
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
|
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
|
||||||
backendDll.text = getDefaultDllPath(InjectableDll.cobalt);
|
backendDll.text = getDefaultDllPath(InjectableDll.auth);
|
||||||
memoryLeakDll.text = getDefaultDllPath(InjectableDll.memory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetServer() {
|
void resetServer() {
|
||||||
gameServerPort.text = kDefaultGameServerPort;
|
gameServerPort.text = kDefaultGameServerPort;
|
||||||
timer.value = UpdateTimer.hour;
|
timer.value = UpdateTimer.hour;
|
||||||
url.text = kRebootDownloadUrl;
|
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||||
|
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
|
||||||
status.value = UpdateStatus.waiting;
|
status.value = UpdateStatus.waiting;
|
||||||
customGameServer.value = false;
|
customGameServer.value = false;
|
||||||
timestamp.value = null;
|
timestamp.value = null;
|
||||||
@@ -76,16 +74,7 @@ class DllController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
|
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
|
||||||
if(_updater != null) {
|
InfoBarEntry? infoBarEntry;
|
||||||
return await _updater!;
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = _updateGameServerDll(force, silent);
|
|
||||||
_updater = result;
|
|
||||||
return await result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _updateGameServerDll(bool force, bool silent) async {
|
|
||||||
try {
|
try {
|
||||||
if(customGameServer.value) {
|
if(customGameServer.value) {
|
||||||
status.value = UpdateStatus.success;
|
status.value = UpdateStatus.success;
|
||||||
@@ -109,7 +98,15 @@ class DllController extends GetxController {
|
|||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
timestamp.value = await downloadRebootDll(url.text);
|
await Future.wait(
|
||||||
|
[
|
||||||
|
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false),
|
||||||
|
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true),
|
||||||
|
Future.delayed(const Duration(seconds: 1))
|
||||||
|
],
|
||||||
|
eagerError: false
|
||||||
|
);
|
||||||
|
timestamp.value = DateTime.now().millisecondsSinceEpoch;
|
||||||
status.value = UpdateStatus.success;
|
status.value = UpdateStatus.success;
|
||||||
infoBarEntry?.close();
|
infoBarEntry?.close();
|
||||||
if(!silent) {
|
if(!silent) {
|
||||||
@@ -126,61 +123,78 @@ class DllController extends GetxController {
|
|||||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||||
error = error.toLowerCase();
|
error = error.toLowerCase();
|
||||||
status.value = UpdateStatus.error;
|
status.value = UpdateStatus.error;
|
||||||
showRebootInfoBar(
|
infoBarEntry = showRebootInfoBar(
|
||||||
translations.downloadDllError("reboot.dll", error.toString()),
|
translations.downloadDllError(error.toString(), "reboot.dll"),
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
action: Button(
|
action: Button(
|
||||||
onPressed: () => updateGameServerDll(
|
onPressed: () async {
|
||||||
force: true,
|
infoBarEntry?.close();
|
||||||
silent: silent
|
updateGameServerDll(
|
||||||
),
|
force: true,
|
||||||
|
silent: silent
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Text(translations.downloadDllRetry),
|
child: Text(translations.downloadDllRetry),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}finally {
|
|
||||||
_updater = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(File, bool) getInjectableData(InjectableDll dll) {
|
(File, bool) getInjectableData(Version version, InjectableDll dll) {
|
||||||
final defaultPath = canonicalize(getDefaultDllPath(dll));
|
final defaultPath = canonicalize(getDefaultDllPath(dll));
|
||||||
switch(dll){
|
switch(dll){
|
||||||
case InjectableDll.reboot:
|
case InjectableDll.gameServer:
|
||||||
if(customGameServer.value) {
|
if(customGameServer.value) {
|
||||||
final file = File(gameServerDll.text);
|
return (File(customGameServerDll.text), true);
|
||||||
if(file.existsSync()) {
|
|
||||||
return (file, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (rebootDllFile, false);
|
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
||||||
case InjectableDll.console:
|
case InjectableDll.console:
|
||||||
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
||||||
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
||||||
case InjectableDll.cobalt:
|
case InjectableDll.auth:
|
||||||
final backendFile = File(backendDll.text);
|
final backendFile = File(backendDll.text);
|
||||||
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
||||||
case InjectableDll.memory:
|
case InjectableDll.memoryLeak:
|
||||||
final memoryLeakFile = File(memoryLeakDll.text);
|
final memoryFile = File(memoryLeakDll.text);
|
||||||
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
|
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
|
TextEditingController getDllEditingController(InjectableDll dll) {
|
||||||
|
switch(dll) {
|
||||||
|
case InjectableDll.console:
|
||||||
|
return unrealEngineConsoleDll;
|
||||||
|
case InjectableDll.auth:
|
||||||
|
return backendDll;
|
||||||
|
case InjectableDll.gameServer:
|
||||||
|
return customGameServerDll;
|
||||||
|
case InjectableDll.memoryLeak:
|
||||||
|
return memoryLeakDll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false, bool force = false}) async {
|
String getDefaultDllPath(InjectableDll dll) {
|
||||||
log("[DLL] Asking for $filePath(silent: $silent)");
|
switch(dll) {
|
||||||
final fileName = basename(filePath).toLowerCase();
|
case InjectableDll.console:
|
||||||
log("[DLL] File name: $fileName");
|
return "${dllsDirectory.path}\\console.dll";
|
||||||
|
case InjectableDll.auth:
|
||||||
|
return "${dllsDirectory.path}\\cobalt.dll";
|
||||||
|
case InjectableDll.gameServer:
|
||||||
|
return "${dllsDirectory.path}\\reboot.dll";
|
||||||
|
case InjectableDll.memoryLeak:
|
||||||
|
return "${dllsDirectory.path}\\memory.dll";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
|
||||||
|
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
|
||||||
InfoBarEntry? entry;
|
InfoBarEntry? entry;
|
||||||
try {
|
try {
|
||||||
if (fileName == "reboot.dll") {
|
if (dll == InjectableDll.gameServer) {
|
||||||
log("[DLL] Downloading reboot.dll...");
|
return await updateGameServerDll(silent: silent);
|
||||||
return await updateGameServerDll(
|
|
||||||
silent: silent
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!force && File(filePath).existsSync()) {
|
if(!force && File(filePath).existsSync()) {
|
||||||
@@ -196,7 +210,7 @@ class DllController extends GetxController {
|
|||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await downloadCriticalDll(fileName, filePath);
|
await downloadDependency(dll, filePath);
|
||||||
entry?.close();
|
entry?.close();
|
||||||
if(!silent) {
|
if(!silent) {
|
||||||
entry = await showRebootInfoBar(
|
entry = await showRebootInfoBar(
|
||||||
@@ -215,13 +229,13 @@ class DllController extends GetxController {
|
|||||||
error = error.toLowerCase();
|
error = error.toLowerCase();
|
||||||
final completer = Completer();
|
final completer = Completer();
|
||||||
await showRebootInfoBar(
|
await showRebootInfoBar(
|
||||||
translations.downloadDllError(fileName, error.toString()),
|
translations.downloadDllError(error.toString(), dll.name),
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
onDismissed: () => completer.complete(null),
|
onDismissed: () => completer.complete(null),
|
||||||
action: Button(
|
action: Button(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await downloadCriticalDllInteractive(filePath);
|
await download(dll, filePath, silent: silent, force: force);
|
||||||
completer.complete(null);
|
completer.complete(null);
|
||||||
},
|
},
|
||||||
child: Text(translations.downloadDllRetry),
|
child: Text(translations.downloadDllRetry),
|
||||||
@@ -231,6 +245,32 @@ class DllController extends GetxController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void guardFiles() {
|
||||||
|
for(final injectable in InjectableDll.values) {
|
||||||
|
final controller = getDllEditingController(injectable);
|
||||||
|
final defaultPath = getDefaultDllPath(injectable);
|
||||||
|
if (path.equals(controller.text, defaultPath)) {
|
||||||
|
download(injectable, controller.text);
|
||||||
|
}
|
||||||
|
controller.addListener(() async {
|
||||||
|
try {
|
||||||
|
if (!path.equals(controller.text, defaultPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final filePath = controller.text;
|
||||||
|
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
|
||||||
|
if (path.equals(event.path, filePath)) {
|
||||||
|
await download(injectable, filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(_) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _UpdateTimerExtension on UpdateTimer {
|
extension _UpdateTimerExtension on UpdateTimer {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:reboot_common/common.dart';
|
|||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
|
|
||||||
class GameController extends GetxController {
|
class GameController extends GetxController {
|
||||||
static const String storageName = "game_storage";
|
static const String storageName = "v2_game_storage";
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final TextEditingController username;
|
late final TextEditingController username;
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import 'package:sync/semaphore.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class HostingController extends GetxController {
|
class HostingController extends GetxController {
|
||||||
static const String storageName = "hosting_storage";
|
static const String storageName = "v2_hosting_storage";
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final String uuid;
|
late final String uuid;
|
||||||
|
late final TextEditingController accountUsername;
|
||||||
|
late final TextEditingController accountPassword;
|
||||||
late final TextEditingController name;
|
late final TextEditingController name;
|
||||||
late final FocusNode nameFocusNode;
|
late final FocusNode nameFocusNode;
|
||||||
late final TextEditingController description;
|
late final TextEditingController description;
|
||||||
@@ -37,6 +39,10 @@ class HostingController extends GetxController {
|
|||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
||||||
_storage?.write("uuid", uuid);
|
_storage?.write("uuid", uuid);
|
||||||
|
accountUsername = TextEditingController(text: _storage?.read("account_username") ?? kDefaultHostName);
|
||||||
|
accountUsername.addListener(() => _storage?.write("account_username", accountUsername.text));
|
||||||
|
accountPassword = TextEditingController(text: _storage?.read("account_password") ?? "");
|
||||||
|
accountPassword.addListener(() => _storage?.write("account_password", password.text));
|
||||||
name = TextEditingController(text: _storage?.read("name"));
|
name = TextEditingController(text: _storage?.read("name"));
|
||||||
name.addListener(() => _storage?.write("name", name.text));
|
name.addListener(() => _storage?.write("name", name.text));
|
||||||
description = TextEditingController(text: _storage?.read("description"));
|
description = TextEditingController(text: _storage?.read("description"));
|
||||||
@@ -56,20 +62,36 @@ class HostingController extends GetxController {
|
|||||||
published = RxBool(false);
|
published = RxBool(false);
|
||||||
showPassword = RxBool(false);
|
showPassword = RxBool(false);
|
||||||
instance = Rxn();
|
instance = Rxn();
|
||||||
final supabase = Supabase.instance.client;
|
|
||||||
servers = Rxn();
|
servers = Rxn();
|
||||||
supabase.from("hosting_v2")
|
_listenServers();
|
||||||
.stream(primaryKey: ['id'])
|
|
||||||
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
|
|
||||||
.listen((event) {
|
|
||||||
servers.value = event;
|
|
||||||
published.value = event.any((element) => element.id == uuid);
|
|
||||||
});
|
|
||||||
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
||||||
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
|
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
|
||||||
_semaphore = Semaphore();
|
_semaphore = Semaphore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _listenServers([int attempt = 0]) {
|
||||||
|
log("[SUPABASE] Listening...");
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
supabase.from("hosting_v2")
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
|
||||||
|
.listen(
|
||||||
|
_onNewServer,
|
||||||
|
onError: (error) async {
|
||||||
|
log("[SUPABASE] Error: ${error}");
|
||||||
|
await Future.delayed(Duration(seconds: attempt * 5));
|
||||||
|
_listenServers(attempt + 1);
|
||||||
|
},
|
||||||
|
cancelOnError: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onNewServer(Set<FortniteServer> event) {
|
||||||
|
log("[SUPABASE] New event: ${event}");
|
||||||
|
servers.value = event;
|
||||||
|
published.value = event.any((element) => element.id == uuid);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> publishServer(String author, String version) async {
|
Future<void> publishServer(String author, String version) async {
|
||||||
try {
|
try {
|
||||||
_semaphore.acquire();
|
_semaphore.acquire();
|
||||||
@@ -136,6 +158,8 @@ class HostingController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
|
accountUsername.text = kDefaultHostName;
|
||||||
|
accountPassword.text = "";
|
||||||
name.text = "";
|
name.text = "";
|
||||||
description.text = "";
|
description.text = "";
|
||||||
showPassword.value = false;
|
showPassword.value = false;
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
class SettingsController extends GetxController {
|
class SettingsController extends GetxController {
|
||||||
static const String storageName = "settings_storage";
|
static const String storageName = "v2_settings_storage";
|
||||||
|
|
||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final RxString language;
|
late final RxString language;
|
||||||
late final Rx<ThemeMode> themeMode;
|
late final Rx<ThemeMode> themeMode;
|
||||||
late final RxBool firstRun;
|
late final RxBool firstRun;
|
||||||
late final RxBool debug;
|
|
||||||
late double width;
|
late double width;
|
||||||
late double height;
|
late double height;
|
||||||
late double? offsetX;
|
late double? offsetX;
|
||||||
@@ -39,7 +36,6 @@ class SettingsController extends GetxController {
|
|||||||
language.listen((value) => _storage?.write("language", value));
|
language.listen((value) => _storage?.write("language", value));
|
||||||
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
|
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
|
||||||
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
|
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
|
||||||
debug = RxBool(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveWindowSize(Size size) {
|
void saveWindowSize(Size size) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ class _DialogButtonState extends State<DialogButton> {
|
|||||||
|
|
||||||
Widget get _primaryButton => Button(
|
Widget get _primaryButton => Button(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
|
||||||
),
|
),
|
||||||
onPressed: widget.onTap!,
|
onPressed: widget.onTap!,
|
||||||
child: Text(widget.text!),
|
child: Text(widget.text!),
|
||||||
@@ -308,7 +308,7 @@ class _DialogButtonState extends State<DialogButton> {
|
|||||||
|
|
||||||
Widget get _secondaryButton => Button(
|
Widget get _secondaryButton => Button(
|
||||||
style: widget.color != null ? ButtonStyle(
|
style: widget.color != null ? ButtonStyle(
|
||||||
backgroundColor: ButtonState.all(widget.color!)
|
backgroundColor: WidgetStateProperty.all(widget.color!)
|
||||||
) : null,
|
) : null,
|
||||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:clipboard/clipboard.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.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:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
final List<InfoBarEntry> _infoBars = [];
|
|
||||||
|
|
||||||
extension ServerControllerDialog on BackendController {
|
|
||||||
void cancelInteractive() {
|
|
||||||
worker?.cancel(); // Do not await or it will hang
|
|
||||||
_infoBars.forEach((infoBar) => infoBar.close());
|
|
||||||
_infoBars.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> toggleInteractive() async {
|
|
||||||
cancelInteractive();
|
|
||||||
final stream = toggle(
|
|
||||||
onExit: () {
|
|
||||||
cancelInteractive();
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.backendProcessError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (errorMessage) {
|
|
||||||
cancelInteractive();
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.backendErrorMessage,
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
action: Button(
|
|
||||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
|
||||||
child: Text(translations.openLog),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
final completer = Completer<bool>();
|
|
||||||
InfoBarEntry? entry;
|
|
||||||
worker = stream.listen((event) {
|
|
||||||
entry?.close();
|
|
||||||
entry = _handeEvent(event);
|
|
||||||
if(event.type.isError) {
|
|
||||||
completer.complete(false);
|
|
||||||
}else if(event.type.isSuccess) {
|
|
||||||
completer.complete(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return await completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoBarEntry _handeEvent(ServerResult event) {
|
|
||||||
log("[BACKEND] Handling event: $event");
|
|
||||||
switch (event.type) {
|
|
||||||
case ServerResultType.starting:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.startingServer,
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
case ServerResultType.startSuccess:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
|
||||||
severity: InfoBarSeverity.success
|
|
||||||
);
|
|
||||||
case ServerResultType.startError:
|
|
||||||
print(event.stackTrace);
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration
|
|
||||||
);
|
|
||||||
case ServerResultType.stopping:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.stoppingServer,
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
case ServerResultType.stopSuccess:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.stoppedServer,
|
|
||||||
severity: InfoBarSeverity.success
|
|
||||||
);
|
|
||||||
case ServerResultType.stopError:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.stopServerError(event.error ?? translations.unknownError),
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration
|
|
||||||
);
|
|
||||||
case ServerResultType.missingHostError:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.missingHostNameError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
case ServerResultType.missingPortError:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.missingPortError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
case ServerResultType.illegalPortError:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.illegalPortError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
case ServerResultType.freeingPort:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.freeingPort,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
case ServerResultType.freePortSuccess:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.freedPort,
|
|
||||||
severity: InfoBarSeverity.success,
|
|
||||||
duration: infoBarShortDuration
|
|
||||||
);
|
|
||||||
case ServerResultType.freePortError:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.freePortError(event.error ?? translations.unknownError),
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration
|
|
||||||
);
|
|
||||||
case ServerResultType.pingingRemote:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.pingingServer(ServerType.remote.name),
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
case ServerResultType.pingingLocal:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.pingingServer(type.value.name),
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
case ServerResultType.pingError:
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.pingError(type.value.name),
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
|
|
||||||
if(!kDebugMode && uuid == server.id) {
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.joinSelfServer,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final gameController = Get.find<GameController>();
|
|
||||||
final version = gameController.getVersionByName(server.version.toString());
|
|
||||||
if(version == null) {
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.cannotJoinServerVersion(server.version.toString()),
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final hashedPassword = server.password;
|
|
||||||
final hasPassword = hashedPassword != null;
|
|
||||||
final embedded = type.value == ServerType.embedded;
|
|
||||||
final author = server.author;
|
|
||||||
final encryptedIp = server.ip;
|
|
||||||
if(!hasPassword) {
|
|
||||||
final valid = await _isServerValid(encryptedIp);
|
|
||||||
if(!valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSuccess(gameController, embedded, encryptedIp, author, version);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final confirmPassword = await _askForPassword();
|
|
||||||
if(confirmPassword == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.wrongServerPassword,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
|
||||||
final valid = await _isServerValid(decryptedIp);
|
|
||||||
if(!valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSuccess(gameController, embedded, decryptedIp, author, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _isServerValid(String address) async {
|
|
||||||
final result = await pingGameServer(address);
|
|
||||||
if(result) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.offlineServer,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> _askForPassword() async {
|
|
||||||
final confirmPasswordController = TextEditingController();
|
|
||||||
final showPassword = RxBool(false);
|
|
||||||
final showPasswordTrailing = RxBool(false);
|
|
||||||
return await showRebootDialog<String?>(
|
|
||||||
builder: (context) => FormDialog(
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
InfoLabel(
|
|
||||||
label: translations.serverPassword,
|
|
||||||
child: Obx(() => TextFormBox(
|
|
||||||
placeholder: translations.serverPasswordPlaceholder,
|
|
||||||
controller: confirmPasswordController,
|
|
||||||
autovalidateMode: AutovalidateMode.always,
|
|
||||||
obscureText: !showPassword.value,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofocus: true,
|
|
||||||
autocorrect: false,
|
|
||||||
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
|
||||||
suffix: !showPasswordTrailing.value ? null : Button(
|
|
||||||
onPressed: () => showPassword.value = !showPassword.value,
|
|
||||||
style: ButtonStyle(
|
|
||||||
shape: ButtonState.all(const CircleBorder()),
|
|
||||||
backgroundColor: ButtonState.all(Colors.transparent)
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
|
|
||||||
),
|
|
||||||
)
|
|
||||||
))
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8.0)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
buttons: [
|
|
||||||
DialogButton(
|
|
||||||
text: translations.serverPasswordCancel,
|
|
||||||
type: ButtonType.secondary
|
|
||||||
),
|
|
||||||
|
|
||||||
DialogButton(
|
|
||||||
text: translations.serverPasswordConfirm,
|
|
||||||
type: ButtonType.primary,
|
|
||||||
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
|
||||||
if(embedded) {
|
|
||||||
gameServerAddress.text = decryptedIp;
|
|
||||||
pageIndex.value = RebootPageType.play.index;
|
|
||||||
}else {
|
|
||||||
FlutterClipboard.controlC(decryptedIp);
|
|
||||||
}
|
|
||||||
controller.selectedVersion = version;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
|
|
||||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.success
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoBarEntry _showRebootInfoBar(dynamic text, {
|
|
||||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
|
||||||
bool loading = false,
|
|
||||||
Duration? duration = infoBarShortDuration,
|
|
||||||
void Function()? onDismissed,
|
|
||||||
Widget? action
|
|
||||||
}) {
|
|
||||||
final result = showRebootInfoBar(
|
|
||||||
text,
|
|
||||||
severity: severity,
|
|
||||||
loading: loading,
|
|
||||||
duration: duration,
|
|
||||||
onDismissed: onDismissed,
|
|
||||||
action: action
|
|
||||||
);
|
|
||||||
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
|
|
||||||
_infoBars.add(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
import 'package:reboot_launcher/src/widget/page/home_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
|
||||||
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
||||||
@@ -148,7 +148,7 @@ class _RenderAbsorbPointer extends RenderProxyBox {
|
|||||||
|
|
||||||
// 32 is the height of the title bar (need this offset as the overlay area doesn't include it)
|
// 32 is the height of the title bar (need this offset as the overlay area doesn't include it)
|
||||||
// Not an optimal solution but it works (calculating it is kind of complicated)
|
// Not an optimal solution but it works (calculating it is kind of complicated)
|
||||||
position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight);
|
position = Offset(position.dx, position.dy);
|
||||||
final exclusionPosition = exclusion.localToGlobal(Offset.zero);
|
final exclusionPosition = exclusion.localToGlobal(Offset.zero);
|
||||||
final exclusionSize = Rect.fromLTRB(
|
final exclusionSize = Rect.fromLTRB(
|
||||||
exclusionPosition.dx,
|
exclusionPosition.dx,
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.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/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/setting_tile.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 hasButton(String? pageName) => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RebootPageState<SettingsPage> createState() => _SettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? get button => null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> get settings => [
|
|
||||||
_language,
|
|
||||||
_theme,
|
|
||||||
_debugMode,
|
|
||||||
_installationDirectory,
|
|
||||||
];
|
|
||||||
|
|
||||||
SettingTile get _language => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.local_language_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsUtilsLanguageName),
|
|
||||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
|
||||||
content: Obx(() => DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
|
||||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
|
||||||
text: Text(_getLocaleName(locale.languageCode)),
|
|
||||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
|
||||||
)).toList()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingTile get _theme => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.dark_theme_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsUtilsThemeName),
|
|
||||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
|
||||||
content: Obx(() => DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(_settingsController.themeMode.value.title),
|
|
||||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
|
||||||
text: Text(themeMode.title),
|
|
||||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
|
||||||
)).toList()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
SettingTile get _installationDirectory => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.folder_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsUtilsInstallationDirectoryName),
|
|
||||||
subtitle: Text(translations.settingsUtilsInstallationDirectorySubtitle),
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => launchUrl(installationDirectory.uri),
|
|
||||||
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
SettingTile get _debugMode => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.developer_board_24_regular
|
|
||||||
),
|
|
||||||
title: Text("Debug mode"),
|
|
||||||
subtitle: Text("Whether the launcher should disable automatic features for troubleshooting"),
|
|
||||||
contentWidth: null,
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
_settingsController.debug.value ? translations.on : translations.off
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0
|
|
||||||
),
|
|
||||||
Obx(() => ToggleSwitch(
|
|
||||||
checked: _settingsController.debug.value,
|
|
||||||
onChanged: (value) => _settingsController.debug.value = value
|
|
||||||
))
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,9 +1,8 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
import 'package:reboot_launcher/src/widget/message/onboard.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
abstract class RebootPage extends StatefulWidget {
|
abstract class RebootPage extends StatefulWidget {
|
||||||
@@ -35,7 +34,6 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildFirstLaunchInfo(),
|
_buildFirstLaunchInfo(),
|
||||||
_buildDebugInfo(),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _listView
|
child: _listView
|
||||||
)
|
)
|
||||||
@@ -47,7 +45,6 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildFirstLaunchInfo(),
|
_buildFirstLaunchInfo(),
|
||||||
_buildDebugInfo(),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -99,35 +96,6 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildDebugInfo() => Obx(() {
|
|
||||||
if(!_settingsController.debug.value) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
bottom: 8.0
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: InfoBar(
|
|
||||||
title: Text("Debug mode is enabled"),
|
|
||||||
severity: InfoBarSeverity.warning,
|
|
||||||
isLong: true,
|
|
||||||
content: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text( "• Automatic dll injection is disabled\n"
|
|
||||||
"• The game server cannot start automatically\n"
|
|
||||||
"• The game server runs in a normal window")
|
|
||||||
),
|
|
||||||
onClose: () {
|
|
||||||
_settingsController.debug.value = false;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ListView get _listView => ListView.builder(
|
ListView get _listView => ListView.builder(
|
||||||
itemCount: settings.length,
|
itemCount: settings.length,
|
||||||
itemBuilder: (context, index) => settings[index],
|
itemBuilder: (context, index) => settings[index],
|
||||||
@@ -3,16 +3,16 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/browser_page.dart';
|
import 'package:reboot_launcher/src/widget/page/browser_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
import 'package:reboot_launcher/src/widget/page/host_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
import 'package:reboot_launcher/src/widget/page/info_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
import 'package:reboot_launcher/src/widget/page/play_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/settings_page.dart';
|
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
|
||||||
|
|
||||||
final StreamController<void> pagesController = StreamController.broadcast();
|
final StreamController<void> pagesController = StreamController.broadcast();
|
||||||
bool hitBack = false;
|
bool hitBack = false;
|
||||||
|
|||||||
@@ -1,43 +1,34 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
const Duration _timeout = Duration(seconds: 5);
|
const Duration _timeout = Duration(seconds: 5);
|
||||||
|
|
||||||
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
|
Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
|
||||||
Future<bool> ping(String hostname, int port) async {
|
final completer = Completer<bool>();
|
||||||
log("[MATCHMAKER] Pinging $hostname:$port");
|
final start = DateTime.now();
|
||||||
RawDatagramSocket? socket;
|
_pingGameServerOrTimeout(completer, start, timeout, address);
|
||||||
try {
|
return completer;
|
||||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
}
|
||||||
await for (final event in socket) {
|
|
||||||
log("[MATCHMAKER] Event: $event");
|
|
||||||
switch(event) {
|
|
||||||
case RawSocketEvent.read:
|
|
||||||
log("[MATCHMAKER] Success");
|
|
||||||
return true;
|
|
||||||
case RawSocketEvent.write:
|
|
||||||
log("[MATCHMAKER] Sending data");
|
|
||||||
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
|
|
||||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
|
||||||
case RawSocketEvent.readClosed:
|
|
||||||
case RawSocketEvent.closed:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
Future<void> _pingGameServerOrTimeout(Completer<bool> completer, DateTime start, Duration timeout, String address) async {
|
||||||
}catch(error) {
|
while (!completer.isCompleted && max(DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch, 0) < timeout.inMilliseconds) {
|
||||||
log("[MATCHMAKER] Error: $error");
|
final result = await pingGameServer(address);
|
||||||
return false;
|
if(result) {
|
||||||
}finally {
|
completer.complete(true);
|
||||||
socket?.close();
|
}else {
|
||||||
|
await Future.delayed(_timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!completer.isCompleted) {
|
||||||
|
completer.complete(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final start = DateTime.now();
|
Future<bool> pingGameServer(String address) async {
|
||||||
var firstTime = true;
|
|
||||||
final split = address.split(":");
|
final split = address.split(":");
|
||||||
var hostname = split[0];
|
var hostname = split[0];
|
||||||
if(isLocalHost(hostname)) {
|
if(isLocalHost(hostname)) {
|
||||||
@@ -45,19 +36,37 @@ Future<bool> pingGameServer(String address, {Duration? timeout}) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||||
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
|
return await _ping(hostname, port)
|
||||||
final result = await ping(hostname, port)
|
.timeout(_timeout, onTimeout: () => false);
|
||||||
.timeout(_timeout, onTimeout: () => false);
|
}
|
||||||
if(result) {
|
|
||||||
return true;
|
|
||||||
|
Future<bool> _ping(String hostname, int port) async {
|
||||||
|
log("[MATCHMAKER] Pinging $hostname:$port");
|
||||||
|
RawDatagramSocket? socket;
|
||||||
|
try {
|
||||||
|
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||||
|
await for (final event in socket) {
|
||||||
|
log("[MATCHMAKER] Event: $event");
|
||||||
|
switch(event) {
|
||||||
|
case RawSocketEvent.read:
|
||||||
|
log("[MATCHMAKER] Success");
|
||||||
|
return true;
|
||||||
|
case RawSocketEvent.write:
|
||||||
|
log("[MATCHMAKER] Sending data");
|
||||||
|
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
|
||||||
|
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||||
|
case RawSocketEvent.readClosed:
|
||||||
|
case RawSocketEvent.closed:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(firstTime) {
|
return false;
|
||||||
firstTime = false;
|
}catch(error) {
|
||||||
}else {
|
log("[MATCHMAKER] Error: $error");
|
||||||
await Future.delayed(_timeout);
|
return false;
|
||||||
}
|
}finally {
|
||||||
|
socket?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:win32/win32.dart';
|
import 'package:win32/win32.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||||
|
|
||||||
@@ -24,10 +25,13 @@ bool get isWin11 {
|
|||||||
return intBuild != null && intBuild > 22000;
|
return intBuild != null && intBuild > 22000;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> openFolderPicker(String title) async =>
|
Future<String?> openFolderPicker(String title) async {
|
||||||
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
|
FilePicker.platform = FilePickerWindows();
|
||||||
|
return await FilePicker.platform.getDirectoryPath(dialogTitle: title);
|
||||||
|
}
|
||||||
|
|
||||||
Future<String?> openFilePicker(String extension) async {
|
Future<String?> openFilePicker(String extension) async {
|
||||||
|
FilePicker.platform = FilePickerWindows();
|
||||||
var result = await FilePicker.platform.pickFiles(
|
var result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
@@ -93,7 +97,7 @@ class IVirtualDesktop extends IUnknown {
|
|||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertFromHString(result.value);
|
return _convertFromHString(result.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +284,7 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, COMObject, int)>()(
|
.asFunction<int Function(Pointer, COMObject, int)>()(
|
||||||
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
|
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
}
|
}
|
||||||
@@ -369,7 +373,7 @@ List<int> _getHWnds(int pid, String? excludedWindowName) {
|
|||||||
result.ref.excluded = excludedWindowName.toNativeUtf16();
|
result.ref.excluded = excludedWindowName.toNativeUtf16();
|
||||||
}
|
}
|
||||||
|
|
||||||
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
|
EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
|
||||||
final length = result.ref.HWndLength;
|
final length = result.ref.HWndLength;
|
||||||
final HWndsPointer = result.ref.HWnd;
|
final HWndsPointer = result.ref.HWnd;
|
||||||
if(HWndsPointer == nullptr) {
|
if(HWndsPointer == nullptr) {
|
||||||
@@ -397,7 +401,7 @@ class VirtualDesktopManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final hr = CoInitializeEx(
|
final hr = CoInitializeEx(
|
||||||
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
throw WindowsException(hr);
|
throw WindowsException(hr);
|
||||||
}
|
}
|
||||||
@@ -468,3 +472,77 @@ class VirtualDesktopManager {
|
|||||||
void setDesktopName(IVirtualDesktop desktop, String newName) =>
|
void setDesktopName(IVirtualDesktop desktop, String newName) =>
|
||||||
windowManager.setDesktopName(desktop, newName);
|
windowManager.setDesktopName(desktop, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _convertFromHString(int hstring) =>
|
||||||
|
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
|
||||||
|
|
||||||
|
int _convertToHString(String string) {
|
||||||
|
final hString = calloc<HSTRING>();
|
||||||
|
final stringPtr = string.toNativeUtf16();
|
||||||
|
try {
|
||||||
|
final hr = WindowsCreateString(stringPtr, string.length, hString);
|
||||||
|
if (FAILED(hr)) throw WindowsException(hr);
|
||||||
|
return hString.value;
|
||||||
|
} finally {
|
||||||
|
free(stringPtr);
|
||||||
|
free(hString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WindowManagerExtension on WindowManager {
|
||||||
|
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowsDisk {
|
||||||
|
static final String _nullTerminator = String.fromCharCode(0);
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
final int freeBytesAvailable;
|
||||||
|
final int totalNumberOfBytes;
|
||||||
|
|
||||||
|
const WindowsDisk._internal(this.path, this.freeBytesAvailable, this.totalNumberOfBytes);
|
||||||
|
|
||||||
|
static List<WindowsDisk> available() {
|
||||||
|
final buffer = malloc.allocate<Utf16>(MAX_PATH);
|
||||||
|
try {
|
||||||
|
final length = GetLogicalDriveStrings(MAX_PATH, buffer);
|
||||||
|
if (length == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toDartString(length: length)
|
||||||
|
.split(_nullTerminator)
|
||||||
|
.where((drive) => drive.length > 1)
|
||||||
|
.map((driveName) {
|
||||||
|
final freeBytesAvailable = calloc<Uint64>();
|
||||||
|
final totalNumberOfBytes = calloc<Uint64>();
|
||||||
|
final totalNumberOfFreeBytes = calloc<Uint64>();
|
||||||
|
try {
|
||||||
|
GetDiskFreeSpaceEx(
|
||||||
|
driveName.toNativeUtf16(),
|
||||||
|
freeBytesAvailable,
|
||||||
|
totalNumberOfBytes,
|
||||||
|
totalNumberOfFreeBytes
|
||||||
|
);
|
||||||
|
return WindowsDisk._internal(
|
||||||
|
driveName,
|
||||||
|
freeBytesAvailable.value,
|
||||||
|
totalNumberOfBytes.value
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
calloc.free(freeBytesAvailable);
|
||||||
|
calloc.free(totalNumberOfBytes);
|
||||||
|
calloc.free(totalNumberOfFreeBytes);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
|
} finally {
|
||||||
|
calloc.free(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'WindowsDisk{path: $path, freeBytesAvailable: $freeBytesAvailable, totalNumberOfBytes: $totalNumberOfBytes}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,4 +5,15 @@ extension IterableExtension<E> on Iterable<E> {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StringExtension on String {
|
||||||
|
String? after(String leading) {
|
||||||
|
final index = indexOf(leading);
|
||||||
|
if(index == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return substring(index + leading.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
63
gui/lib/src/util/url_protocol.dart
Normal file
63
gui/lib/src/util/url_protocol.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:win32/win32.dart';
|
||||||
|
|
||||||
|
final _hive = HKEY_CURRENT_USER;
|
||||||
|
|
||||||
|
void registerUrlProtocol(String scheme, {String? executable, List<String>? arguments}) {
|
||||||
|
final prefix = _regPrefix(scheme);
|
||||||
|
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
|
||||||
|
final args = _getArguments(arguments).map((a) => _sanitize(a));
|
||||||
|
final cmd =
|
||||||
|
'${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
|
||||||
|
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
|
||||||
|
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
|
||||||
|
_regCreateStringKey(_hive, prefix + '\\shell\\open\\command', '', cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterUrlProtocol(String scheme) {
|
||||||
|
final txtKey = TEXT(_regPrefix(scheme));
|
||||||
|
try {
|
||||||
|
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
|
||||||
|
} finally {
|
||||||
|
free(txtKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
|
||||||
|
|
||||||
|
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
|
||||||
|
final txtKey = TEXT(key);
|
||||||
|
final txtValue = TEXT(valueName);
|
||||||
|
final txtData = TEXT(data);
|
||||||
|
try {
|
||||||
|
return RegSetKeyValue(
|
||||||
|
hKey,
|
||||||
|
txtKey,
|
||||||
|
txtValue,
|
||||||
|
REG_VALUE_TYPE.REG_SZ,
|
||||||
|
txtData,
|
||||||
|
txtData.length * 2 + 2,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
free(txtKey);
|
||||||
|
free(txtValue);
|
||||||
|
free(txtData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _sanitize(String value) {
|
||||||
|
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
|
||||||
|
return '"$value"';
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _getArguments(List<String>? arguments) {
|
||||||
|
if (arguments == null) return ['%s'];
|
||||||
|
|
||||||
|
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
|
||||||
|
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
@@ -19,10 +19,10 @@ class FileSelector extends StatefulWidget {
|
|||||||
required this.controller,
|
required this.controller,
|
||||||
required this.validator,
|
required this.validator,
|
||||||
required this.folder,
|
required this.folder,
|
||||||
|
required this.allowNavigator,
|
||||||
this.label,
|
this.label,
|
||||||
this.extension,
|
this.extension,
|
||||||
this.validatorMode,
|
this.validatorMode,
|
||||||
this.allowNavigator = true,
|
|
||||||
Key? key})
|
Key? key})
|
||||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
121
gui/lib/src/widget/file/file_setting_tile.dart
Normal file
121
gui/lib/src/widget/file/file_setting_tile.dart
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
|
|
||||||
|
const double _kButtonDimensions = 30;
|
||||||
|
const double _kButtonSpacing = 8;
|
||||||
|
|
||||||
|
// FIXME: If the user clicks on the reset button, the text field checker won't be called
|
||||||
|
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
|
||||||
|
final obx = RxString(controller.text);
|
||||||
|
controller.addListener(() => obx.value = controller.text);
|
||||||
|
final selecting = RxBool(false);
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.document_24_regular
|
||||||
|
),
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: Text(description),
|
||||||
|
contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FileSelector(
|
||||||
|
placeholder: translations.selectPathPlaceholder,
|
||||||
|
windowTitle: translations.selectPathWindowTitle,
|
||||||
|
controller: controller,
|
||||||
|
validator: _checkDll,
|
||||||
|
extension: "dll",
|
||||||
|
folder: false,
|
||||||
|
validatorMode: AutovalidateMode.always,
|
||||||
|
allowNavigator: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: _kButtonSpacing),
|
||||||
|
Obx(() => Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
||||||
|
),
|
||||||
|
child: Tooltip(
|
||||||
|
message: translations.selectFile,
|
||||||
|
child: Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||||
|
),
|
||||||
|
onPressed: () => _onPressed(selecting, controller),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: _kButtonDimensions,
|
||||||
|
child: Icon(
|
||||||
|
fluentIcons.FluentIcons.open_folder_horizontal
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
const SizedBox(width: _kButtonSpacing),
|
||||||
|
Obx(() => Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
||||||
|
),
|
||||||
|
child: Tooltip(
|
||||||
|
message: translations.reset,
|
||||||
|
child: Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||||
|
),
|
||||||
|
onPressed: onReset,
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: _kButtonDimensions,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.arrow_reset_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPressed(RxBool selecting, TextEditingController controller) {
|
||||||
|
if(selecting.value){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selecting.value = true;
|
||||||
|
compute(openFilePicker, "dll")
|
||||||
|
.then((value) => _updateText(controller, value))
|
||||||
|
.then((_) => selecting.value = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateText(TextEditingController controller, String? value) {
|
||||||
|
final text = value ?? controller.text;
|
||||||
|
controller.text = text;
|
||||||
|
controller.selection = TextSelection.collapsed(offset: text.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _checkDll(String? text) {
|
||||||
|
if (text == null || text.isEmpty) {
|
||||||
|
return translations.invalidDllPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = File(text);
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
return translations.dllDoesNotExist;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text.endsWith(".dll")) {
|
||||||
|
return translations.invalidDllExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
|
||||||
|
|
||||||
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
|
|
||||||
final obx = RxString(controller.text);
|
|
||||||
controller.addListener(() => obx.value = controller.text);
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.document_24_regular
|
|
||||||
),
|
|
||||||
title: Text(title),
|
|
||||||
subtitle: Text(description),
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: FileSelector(
|
|
||||||
placeholder: translations.selectPathPlaceholder,
|
|
||||||
windowTitle: translations.selectPathWindowTitle,
|
|
||||||
controller: controller,
|
|
||||||
validator: _checkDll,
|
|
||||||
extension: "dll",
|
|
||||||
folder: false,
|
|
||||||
validatorMode: AutovalidateMode.always
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8.0),
|
|
||||||
Obx(() => Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
|
|
||||||
),
|
|
||||||
child: Button(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: ButtonState.all(EdgeInsets.zero)
|
|
||||||
),
|
|
||||||
onPressed: onReset,
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 30,
|
|
||||||
child: Icon(
|
|
||||||
FluentIcons.arrow_reset_24_regular
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _checkDll(String? text) {
|
|
||||||
if (text == null || text.isEmpty) {
|
|
||||||
return translations.invalidDllPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
final file = File(text);
|
|
||||||
if (!file.existsSync()) {
|
|
||||||
return translations.dllDoesNotExist;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!text.endsWith(".dll")) {
|
|
||||||
return translations.invalidDllExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,11 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/message/profile.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
|
||||||
class ProfileWidget extends StatefulWidget {
|
class ProfileWidget extends StatefulWidget {
|
||||||
final GlobalKey<OverlayTargetState> overlayKey;
|
final GlobalKey<OverlayTargetState> overlayKey;
|
||||||
@@ -15,6 +18,7 @@ class ProfileWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _ProfileWidgetState extends State<ProfileWidget> {
|
class _ProfileWidgetState extends State<ProfileWidget> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => OverlayTarget(
|
Widget build(BuildContext context) => OverlayTarget(
|
||||||
@@ -22,7 +26,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
child: HoverButton(
|
child: HoverButton(
|
||||||
margin: const EdgeInsets.all(8.0),
|
margin: const EdgeInsets.all(8.0),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if(await showProfileForm(context)) {
|
if(await showProfileForm(context, _username, _password)) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -57,7 +61,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_username,
|
_usernameLabel,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600
|
fontWeight: FontWeight.w600
|
||||||
@@ -65,7 +69,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
maxLines: 1
|
maxLines: 1
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_email,
|
_emailLabel,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w100
|
fontWeight: FontWeight.w100
|
||||||
@@ -81,8 +85,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
String get _username {
|
String get _usernameLabel {
|
||||||
var username = _gameController.username.text;
|
final username = _username.text;
|
||||||
if(username.isEmpty) {
|
if(username.isEmpty) {
|
||||||
return kDefaultPlayerName;
|
return kDefaultPlayerName;
|
||||||
}
|
}
|
||||||
@@ -96,8 +100,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
return result.substring(0, 1).toUpperCase() + result.substring(1);
|
return result.substring(0, 1).toUpperCase() + result.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _email {
|
String get _emailLabel {
|
||||||
var username = _gameController.username.text;
|
final username = _username.text;
|
||||||
if(username.isEmpty) {
|
if(username.isEmpty) {
|
||||||
return "$kDefaultPlayerName@projectreboot.dev";
|
return "$kDefaultPlayerName@projectreboot.dev";
|
||||||
}
|
}
|
||||||
@@ -108,4 +112,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
|
|
||||||
return "$username@projectreboot.dev".toLowerCase();
|
return "$username@projectreboot.dev".toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username;
|
||||||
|
TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:skeletons/skeletons.dart';
|
import 'package:skeletons/skeletons.dart';
|
||||||
|
|
||||||
@@ -80,15 +80,19 @@ class SettingTileState extends State<SettingTile> {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
widget.icon,
|
widget.icon,
|
||||||
|
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Expanded(
|
||||||
children: [
|
child: Column(
|
||||||
widget.title == null ? _skeletonTitle : widget.title!,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
|
children: [
|
||||||
],
|
widget.title == null ? _skeletonTitle : widget.title!,
|
||||||
|
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
_trailing
|
_trailing
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -12,16 +12,15 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
class LaunchButton extends StatefulWidget {
|
class LaunchButton extends StatefulWidget {
|
||||||
final bool host;
|
final bool host;
|
||||||
@@ -41,12 +40,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
InfoBarEntry? _gameClientInfoBar;
|
InfoBarEntry? _gameClientInfoBar;
|
||||||
InfoBarEntry? _gameServerInfoBar;
|
InfoBarEntry? _gameServerInfoBar;
|
||||||
CancelableOperation? _operation;
|
CancelableOperation? _operation;
|
||||||
CancelableOperation? _pingOperation;
|
Completer? _pingOperation;
|
||||||
IVirtualDesktop? _virtualDesktop;
|
IVirtualDesktop? _virtualDesktop;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -95,7 +93,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
||||||
for (final injectable in InjectableDll.values) {
|
for (final injectable in InjectableDll.values) {
|
||||||
if(await _getDllFileOrStop(injectable, host) == null) {
|
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +110,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
||||||
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
|
final backendResult = _backendController.started() || await _backendController.toggle();
|
||||||
if(!backendResult){
|
if(!backendResult){
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||||
_onStop(
|
_onStop(
|
||||||
@@ -121,7 +119,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
||||||
final serverType = _settingsController.debug.value ? GameServerType.window : _hostingController.type.value;
|
final serverType = _hostingController.type.value;
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
||||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||||
@@ -159,11 +157,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_settingsController.debug.value) {
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] The user is on debug mode, not asking for auto server");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||||
return null;
|
return null;
|
||||||
@@ -235,7 +228,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||||
final instance = GameInstance(
|
final instance = GameInstance(
|
||||||
versionName: version.content.toString(),
|
version: version.content,
|
||||||
gamePid: gameProcess,
|
gamePid: gameProcess,
|
||||||
launcherPid: launcherProcess,
|
launcherPid: launcherProcess,
|
||||||
eacPid: eacProcess,
|
eacPid: eacProcess,
|
||||||
@@ -248,7 +241,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}else{
|
}else{
|
||||||
_gameController.instance.value = instance;
|
_gameController.instance.value = instance;
|
||||||
}
|
}
|
||||||
await _injectOrShowError(InjectableDll.cobalt, host);
|
await _injectOrShowError(InjectableDll.auth, host);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -256,8 +249,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
|
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||||
final gameArgs = createRebootArgs(
|
final gameArgs = createRebootArgs(
|
||||||
_gameController.username.text,
|
host ? _hostingController.accountUsername.text : _gameController.username.text,
|
||||||
_gameController.password.text,
|
host ? _hostingController.accountPassword.text : _gameController.password.text,
|
||||||
host,
|
host,
|
||||||
hostType,
|
hostType,
|
||||||
false,
|
false,
|
||||||
@@ -280,13 +273,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
line: line,
|
line: line,
|
||||||
host: host,
|
host: host,
|
||||||
onShutdown: () => _onStop(reason: _StopReason.normal),
|
onShutdown: () => _onStop(reason: _StopReason.normal),
|
||||||
onTokenError: () {
|
onTokenError: () => _onStop(reason: _StopReason.tokenError),
|
||||||
if(_settingsController.debug.value) {
|
|
||||||
log("[PROCESS] Ignoring token error because debug mode is on");
|
|
||||||
}else {
|
|
||||||
_onStop(reason: _StopReason.tokenError);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onBuildCorrupted: () {
|
onBuildCorrupted: () {
|
||||||
if(instance == null) {
|
if(instance == null) {
|
||||||
return;
|
return;
|
||||||
@@ -410,7 +397,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
if(instance != null && !instance.launched) {
|
if(instance != null && !instance.launched) {
|
||||||
instance.launched = true;
|
instance.launched = true;
|
||||||
instance.tokenError = false;
|
instance.tokenError = false;
|
||||||
await _injectOrShowError(InjectableDll.memory, host);
|
await _injectOrShowError(InjectableDll.memoryLeak, host);
|
||||||
if(!host){
|
if(!host){
|
||||||
await _injectOrShowError(InjectableDll.console, host);
|
await _injectOrShowError(InjectableDll.console, host);
|
||||||
_onGameClientInjected();
|
_onGameClientInjected();
|
||||||
@@ -419,7 +406,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
if(gameServerPort != null) {
|
if(gameServerPort != null) {
|
||||||
await killProcessByPort(gameServerPort);
|
await killProcessByPort(gameServerPort);
|
||||||
}
|
}
|
||||||
await _injectOrShowError(InjectableDll.reboot, host);
|
await _injectOrShowError(InjectableDll.gameServer, host);
|
||||||
_onGameServerInjected();
|
_onGameServerInjected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,11 +436,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
final gameServerPort = _dllController.gameServerPort.text;
|
final gameServerPort = _dllController.gameServerPort.text;
|
||||||
this._pingOperation = await CancelableOperation.fromFuture(pingGameServer(
|
final pingOperation = pingGameServerOrTimeout(
|
||||||
"127.0.0.1:$gameServerPort",
|
"127.0.0.1:$gameServerPort",
|
||||||
timeout: const Duration(minutes: 2)
|
const Duration(minutes: 2)
|
||||||
));
|
);
|
||||||
final localPingResult = (await _pingOperation?.value) ?? false;
|
this._pingOperation = pingOperation;
|
||||||
|
final localPingResult = await pingOperation.future;
|
||||||
_gameServerInfoBar?.close();
|
_gameServerInfoBar?.close();
|
||||||
if (!localPingResult) {
|
if (!localPingResult) {
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
@@ -475,8 +463,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _hostingController.publishServer(
|
await _hostingController.publishServer(
|
||||||
_gameController.username.text,
|
_hostingController.accountUsername.text,
|
||||||
_hostingController.instance.value!.versionName,
|
_hostingController.instance.value!.version.toString(),
|
||||||
);
|
);
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.gameServerStarted,
|
translations.gameServerStarted,
|
||||||
@@ -496,18 +484,18 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
final publicIp = await Ipify.ipv4();
|
final publicIp = await Ipify.ipv4();
|
||||||
this._pingOperation = CancelableOperation.fromFuture(pingGameServer("$publicIp:$gameServerPort"));
|
final available = await pingGameServer("$publicIp:$gameServerPort");
|
||||||
final externalResult = (await _pingOperation?.value) ?? false;
|
if(available) {
|
||||||
if (externalResult) {
|
_gameServerInfoBar?.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_gameServerInfoBar?.close();
|
final pingOperation = pingGameServerOrTimeout(
|
||||||
this._pingOperation = CancelableOperation.fromFuture(pingGameServer(
|
|
||||||
"$publicIp:$gameServerPort",
|
"$publicIp:$gameServerPort",
|
||||||
timeout: const Duration(days: 365)
|
const Duration(days: 1)
|
||||||
));
|
);
|
||||||
final future = await _pingOperation?.value ?? false;
|
this._pingOperation = pingOperation;
|
||||||
|
_gameServerInfoBar?.close();
|
||||||
_gameServerInfoBar = showRebootInfoBar(
|
_gameServerInfoBar = showRebootInfoBar(
|
||||||
translations.checkGameServerFixMessage(gameServerPort),
|
translations.checkGameServerFixMessage(gameServerPort),
|
||||||
action: Button(
|
action: Button(
|
||||||
@@ -518,26 +506,31 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
duration: null,
|
duration: null,
|
||||||
loading: true
|
loading: true
|
||||||
);
|
);
|
||||||
return await future;
|
final result = await pingOperation.future;
|
||||||
}finally {
|
|
||||||
_gameServerInfoBar?.close();
|
_gameServerInfoBar?.close();
|
||||||
|
return result;
|
||||||
|
}catch(_) {
|
||||||
|
_gameServerInfoBar?.close();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||||
if(host == null) {
|
if(host == null) {
|
||||||
await _pingOperation?.cancel();
|
try {
|
||||||
_pingOperation = null;
|
_pingOperation?.complete(false);
|
||||||
|
}catch(_) {
|
||||||
|
// Ignore: might be running, don't bother checking
|
||||||
|
} finally {
|
||||||
|
_pingOperation = null;
|
||||||
|
}
|
||||||
await _operation?.cancel();
|
await _operation?.cancel();
|
||||||
_operation = null;
|
_operation = null;
|
||||||
_backendController.cancelInteractive();
|
_backendController.stop(interactive: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
host = host ?? widget.host;
|
host = host ?? widget.host;
|
||||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
if(instance == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(host){
|
if(host){
|
||||||
_hostingController.instance.value = null;
|
_hostingController.instance.value = null;
|
||||||
@@ -561,11 +554,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(reason == _StopReason.normal) {
|
if(reason == _StopReason.normal) {
|
||||||
instance.launched = true;
|
instance?.launched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.kill();
|
instance?.kill();
|
||||||
final child = instance.child;
|
final child = instance?.child;
|
||||||
if(child != null) {
|
if(child != null) {
|
||||||
await _onStop(
|
await _onStop(
|
||||||
reason: reason,
|
reason: reason,
|
||||||
@@ -602,7 +595,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.exitCode:
|
case _StopReason.exitCode:
|
||||||
if(!instance.launched) {
|
if(instance != null && !instance.launched) {
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.corruptedVersionError,
|
translations.corruptedVersionError,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
@@ -612,9 +605,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
break;
|
break;
|
||||||
case _StopReason.corruptedVersionError:
|
case _StopReason.corruptedVersionError:
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.corruptedVersionError,
|
translations.corruptedVersionError,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
action: Button(
|
action: Button(
|
||||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||||
child: Text(translations.openLog),
|
child: Text(translations.openLog),
|
||||||
@@ -636,15 +629,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.tokenError:
|
case _StopReason.tokenError:
|
||||||
_backendController.stop();
|
_backendController.stop(interactive: false);
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.tokenError(instance.injectedDlls.map((element) => element.name).join(", ")),
|
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
action: Button(
|
action: Button(
|
||||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||||
child: Text(translations.openLog),
|
child: Text(translations.openLog),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.crash:
|
case _StopReason.crash:
|
||||||
@@ -674,7 +667,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
try {
|
try {
|
||||||
final gameProcess = instance.gamePid;
|
final gameProcess = instance.gamePid;
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
|
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
|
||||||
final dllPath = await _getDllFileOrStop(injectable, hosting);
|
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
|
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
|
||||||
if(dllPath == null) {
|
if(dllPath == null) {
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
||||||
@@ -687,10 +680,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
|
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
|
||||||
if(_settingsController.debug.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await injectDll(gameProcess, dllPath);
|
await injectDll(gameProcess, dllPath);
|
||||||
instance.injectedDlls.add(injectable);
|
instance.injectedDlls.add(injectable);
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
|
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
|
||||||
@@ -705,9 +694,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
|
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||||
final (file, customDll) = _dllController.getInjectableData(injectable);
|
final (file, customDll) = _dllController.getInjectableData(version, injectable);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
||||||
if(await file.exists()) {
|
if(await file.exists()) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||||
@@ -725,9 +714,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
||||||
await _dllController.downloadCriticalDllInteractive(file.path, force: true);
|
await _dllController.download(injectable, file.path, force: true);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||||
return _getDllFileOrStop(injectable, host, true);
|
return _getDllFileOrStop(version, injectable, host, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
||||||
@@ -738,32 +727,32 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
||||||
return _gameClientInfoBar = showRebootInfoBar(
|
return _gameClientInfoBar = showRebootInfoBar(
|
||||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null,
|
duration: null,
|
||||||
action: Obx(() {
|
action: Obx(() {
|
||||||
if(_settingsController.debug.value || _hostingController.started.value || linkedHosting) {
|
if(_hostingController.started.value || linkedHosting) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: 2.0
|
bottom: 2.0
|
||||||
),
|
),
|
||||||
child: Button(
|
child: Button(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_backendController.joinLocalhost();
|
_backendController.joinLocalhost();
|
||||||
if(!_hostingController.started.value) {
|
if(!_hostingController.started.value) {
|
||||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
|
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
|
||||||
_gameClientInfoBar?.close();
|
_gameClientInfoBar?.close();
|
||||||
_showLaunchingGameClientWidget(version, hostType, true);
|
_showLaunchingGameClientWidget(version, hostType, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(translations.startGameServer),
|
child: Text(translations.startGameServer),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
@@ -18,9 +18,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastError = exception.toString();
|
lastError = exception.toString();
|
||||||
final route = ModalRoute.of(pageKey.currentContext!);
|
if(inDialog){
|
||||||
if(route != null && !route.isCurrent){
|
final context = pageKey.currentContext;
|
||||||
Navigator.of(pageKey.currentContext!).pop(false);
|
if(context != null) {
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
inDialog = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
||||||
@@ -5,18 +5,19 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
import 'package:reboot_launcher/src/widget/message/profile.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
import 'package:reboot_launcher/src/widget/page/home_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
import 'package:reboot_launcher/src/widget/page/host_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
import 'package:reboot_launcher/src/widget/page/play_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||||
|
|
||||||
void startOnboarding() {
|
void startOnboarding() {
|
||||||
|
final gameController = Get.find<GameController>();
|
||||||
final settingsController = Get.find<SettingsController>();
|
final settingsController = Get.find<SettingsController>();
|
||||||
settingsController.firstRun.value = false;
|
settingsController.firstRun.value = false;
|
||||||
profileOverlayKey.currentState!.showOverlay(
|
profileOverlayKey.currentState!.showOverlay(
|
||||||
@@ -27,7 +28,7 @@ void startOnboarding() {
|
|||||||
label: translations.startOnboardingActionLabel,
|
label: translations.startOnboardingActionLabel,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
onClose();
|
onClose();
|
||||||
await showProfileForm(context);
|
await showProfileForm(context, gameController.username, gameController.password);
|
||||||
_promptPlayPage();
|
_promptPlayPage();
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -62,7 +63,7 @@ void _promptPlayVersion() {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
onClose();
|
onClose();
|
||||||
if(!hasBuilds) {
|
if(!hasBuilds) {
|
||||||
await VersionSelector.openDownloadDialog(closable: false);
|
await VersionSelector.openDownloadDialog();
|
||||||
}
|
}
|
||||||
_promptServerBrowserPage();
|
_promptServerBrowserPage();
|
||||||
}
|
}
|
||||||
@@ -78,6 +79,22 @@ void _promptServerBrowserPage() {
|
|||||||
context: context,
|
context: context,
|
||||||
label: translations.promptServerBrowserPageActionLabel,
|
label: translations.promptServerBrowserPageActionLabel,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
onClose();
|
||||||
|
_promptHostAccount();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _promptHostAccount() {
|
||||||
|
pageIndex.value = RebootPageType.host.index;
|
||||||
|
profileOverlayKey.currentState!.showOverlay(
|
||||||
|
text: translations.hostAccountText,
|
||||||
|
offset: Offset(27.5, 17.5),
|
||||||
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
|
context: context,
|
||||||
|
label: translations.hostAccountAction,
|
||||||
|
onTap: () async {
|
||||||
onClose();
|
onClose();
|
||||||
_promptHostPage();
|
_promptHostPage();
|
||||||
}
|
}
|
||||||
@@ -86,7 +103,6 @@ void _promptServerBrowserPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _promptHostPage() {
|
void _promptHostPage() {
|
||||||
pageIndex.value = RebootPageType.host.index;
|
|
||||||
pageOverlayTargetKey.currentState!.showOverlay(
|
pageOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptHostPageText,
|
text: translations.promptHostPageText,
|
||||||
actionBuilder: (context, onClose) => _buildActionButton(
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
@@ -339,7 +355,7 @@ Widget _buildActionButton({
|
|||||||
required void Function() onTap,
|
required void Function() onTap,
|
||||||
}) => Button(
|
}) => Button(
|
||||||
style: themed ? ButtonStyle(
|
style: themed ? ButtonStyle(
|
||||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
|
||||||
) : null,
|
) : null,
|
||||||
child: Text(label),
|
child: Text(label),
|
||||||
onPressed: onTap
|
onPressed: onTap
|
||||||
@@ -2,17 +2,14 @@ import 'package:email_validator/email_validator.dart';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
|
||||||
|
|
||||||
Future<bool> showProfileForm(BuildContext context) async{
|
|
||||||
final showPassword = RxBool(false);
|
final showPassword = RxBool(false);
|
||||||
final oldUsername = _gameController.username.text;
|
final oldUsername = username.text;
|
||||||
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||||
final oldPassword = _gameController.password.text;
|
final oldPassword = password.text;
|
||||||
final result = await showRebootDialog<bool?>(
|
final result = await showRebootDialog<bool?>(
|
||||||
builder: (context) => Obx(() => FormDialog(
|
builder: (context) => Obx(() => FormDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
@@ -25,17 +22,17 @@ Future<bool> showProfileForm(BuildContext context) async{
|
|||||||
child: TextFormBox(
|
child: TextFormBox(
|
||||||
placeholder: translations.usernameOrEmailPlaceholder,
|
placeholder: translations.usernameOrEmailPlaceholder,
|
||||||
validator: (text) {
|
validator: (text) {
|
||||||
if(_gameController.password.text.isEmpty) {
|
if(password.text.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(EmailValidator.validate(_gameController.username.text)) {
|
if(EmailValidator.validate(username.text)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return translations.invalidEmail;
|
return translations.invalidEmail;
|
||||||
},
|
},
|
||||||
controller: _gameController.username,
|
controller: username,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
enableSuggestions: true,
|
enableSuggestions: true,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -47,7 +44,7 @@ Future<bool> showProfileForm(BuildContext context) async{
|
|||||||
label: translations.password,
|
label: translations.password,
|
||||||
child: TextFormBox(
|
child: TextFormBox(
|
||||||
placeholder: translations.passwordPlaceholder,
|
placeholder: translations.passwordPlaceholder,
|
||||||
controller: _gameController.password,
|
controller: password,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
obscureText: !showPassword.value,
|
obscureText: !showPassword.value,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
@@ -56,8 +53,8 @@ Future<bool> showProfileForm(BuildContext context) async{
|
|||||||
suffix: Button(
|
suffix: Button(
|
||||||
onPressed: () => showPassword.value = !showPassword.value,
|
onPressed: () => showPassword.value = !showPassword.value,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
shape: ButtonState.all(const CircleBorder()),
|
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||||
backgroundColor: ButtonState.all(Colors.transparent)
|
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||||
@@ -87,7 +84,7 @@ Future<bool> showProfileForm(BuildContext context) async{
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_gameController.username.text = oldUsername;
|
username.text = oldUsername;
|
||||||
_gameController.password.text = oldPassword;
|
password.text = oldPassword;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,11 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
import 'package:reboot_launcher/src/util/types.dart';
|
||||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
||||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||||
|
|
||||||
class AddVersionDialog extends StatefulWidget {
|
class AddVersionDialog extends StatefulWidget {
|
||||||
@@ -32,22 +33,14 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
final Rxn<FortniteBuild> _build = Rxn();
|
final Rxn<FortniteBuild> _build = Rxn();
|
||||||
final RxnInt _timeLeft = RxnInt();
|
final RxnInt _timeLeft = RxnInt();
|
||||||
final Rxn<double> _progress = Rxn();
|
final Rxn<double> _progress = Rxn();
|
||||||
|
final RxInt _speed = RxInt(0);
|
||||||
|
|
||||||
late DiskSpace _diskSpace;
|
|
||||||
late Future<List<FortniteBuild>> _fetchFuture;
|
|
||||||
late Future _diskFuture;
|
|
||||||
|
|
||||||
Isolate? _isolate;
|
|
||||||
SendPort? _downloadPort;
|
SendPort? _downloadPort;
|
||||||
Object? _error;
|
Object? _error;
|
||||||
StackTrace? _stackTrace;
|
StackTrace? _stackTrace;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_fetchFuture = compute(fetchBuilds, null);
|
|
||||||
_diskSpace = DiskSpace();
|
|
||||||
_diskFuture = _diskSpace.scan()
|
|
||||||
.then((_) => _updateFormDefaults());
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,9 +52,9 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _cancelDownload() {
|
void _cancelDownload() {
|
||||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
|
||||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||||
_isolate?.kill(priority: Isolate.immediate);
|
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||||
|
stopDownloadServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -70,28 +63,10 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
switch(_status.value){
|
switch(_status.value){
|
||||||
case _DownloadStatus.form:
|
case _DownloadStatus.form:
|
||||||
return FutureBuilder(
|
return Obx(() => FormDialog(
|
||||||
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
|
content: _buildFormBody(downloadableBuilds),
|
||||||
builder: (context, snapshot) {
|
buttons: _formButtons
|
||||||
if (snapshot.hasError) {
|
));
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = snapshot.data;
|
|
||||||
if (data == null) {
|
|
||||||
return ProgressDialog(
|
|
||||||
text: translations.fetchingBuilds,
|
|
||||||
showButton: widget.closable,
|
|
||||||
onStop: () => Navigator.of(context).pop()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Obx(() => FormDialog(
|
|
||||||
content: _buildFormBody(data),
|
|
||||||
buttons: _formButtons
|
|
||||||
));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
case _DownloadStatus.downloading:
|
case _DownloadStatus.downloading:
|
||||||
case _DownloadStatus.extracting:
|
case _DownloadStatus.extracting:
|
||||||
return GenericDialog(
|
return GenericDialog(
|
||||||
@@ -102,7 +77,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
return ErrorDialog(
|
return ErrorDialog(
|
||||||
exception: _error ?? Exception(translations.unknownError),
|
exception: _error ?? Exception(translations.unknownError),
|
||||||
stackTrace: _stackTrace,
|
stackTrace: _stackTrace,
|
||||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
errorMessageBuilder: (exception) {
|
||||||
|
var error = exception.toString();
|
||||||
|
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
|
||||||
|
error = error.toLowerCase();
|
||||||
|
return translations.downloadVersionError(error);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
case _DownloadStatus.done:
|
case _DownloadStatus.done:
|
||||||
return InfoDialog(
|
return InfoDialog(
|
||||||
@@ -151,7 +131,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
final communicationPort = ReceivePort();
|
final communicationPort = ReceivePort();
|
||||||
communicationPort.listen((message) {
|
communicationPort.listen((message) {
|
||||||
if(message is FortniteBuildDownloadProgress) {
|
if(message is FortniteBuildDownloadProgress) {
|
||||||
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
|
_onProgress(build, message);
|
||||||
}else if(message is SendPort) {
|
}else if(message is SendPort) {
|
||||||
_downloadPort = message;
|
_downloadPort = message;
|
||||||
}else {
|
}else {
|
||||||
@@ -165,7 +145,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
);
|
);
|
||||||
final errorPort = ReceivePort();
|
final errorPort = ReceivePort();
|
||||||
errorPort.listen((message) => _onDownloadError(message, null));
|
errorPort.listen((message) => _onDownloadError(message, null));
|
||||||
_isolate = await Isolate.spawn(
|
await Isolate.spawn(
|
||||||
downloadArchiveBuild,
|
downloadArchiveBuild,
|
||||||
options,
|
options,
|
||||||
onError: errorPort.sendPort,
|
onError: errorPort.sendPort,
|
||||||
@@ -205,23 +185,24 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
_stackTrace = stackTrace;
|
_stackTrace = stackTrace;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
|
void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(progress >= 100 && extracting) {
|
if(message.progress >= 100 && message.extracting) {
|
||||||
_onDownloadComplete(build);
|
_onDownloadComplete(build);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
|
_status.value = message.extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
|
||||||
if(progress >= 0) {
|
if(message.progress >= 0) {
|
||||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
WindowsTaskbar.setProgress(message.progress.round(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
_timeLeft.value = timeLeft;
|
_timeLeft.value = message.timeLeft;
|
||||||
_progress.value = progress;
|
_progress.value = message.progress;
|
||||||
|
_speed.value = message.speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget get _progressBody {
|
Widget get _progressBody {
|
||||||
@@ -232,31 +213,33 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
_statusText,
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(
|
if(_progress.value != null)
|
||||||
height: 8.0,
|
const SizedBox(
|
||||||
),
|
height: 8.0,
|
||||||
|
),
|
||||||
|
|
||||||
Row(
|
if(_progress.value != null)
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Text(
|
children: [
|
||||||
translations.buildProgress((_progress.value ?? 0).round()),
|
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
|
||||||
),
|
|
||||||
|
|
||||||
if(timeLeft != null)
|
|
||||||
Text(
|
Text(
|
||||||
translations.timeLeft(timeLeft),
|
translations.buildProgress((_progress.value ?? 0).round()),
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
)
|
),
|
||||||
],
|
|
||||||
),
|
if(timeLeft != null && timeLeft != -1)
|
||||||
|
Text(
|
||||||
|
translations.timeLeft(timeLeft),
|
||||||
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
@@ -264,7 +247,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
child: ProgressBar(value: _progress.value?.toDouble())
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -274,7 +257,20 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
|
String get _statusText {
|
||||||
|
if (_status.value != _DownloadStatus.downloading) {
|
||||||
|
return translations.extracting;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_progress.value == null) {
|
||||||
|
return translations.startingDownload;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations.downloading;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormBody(List<FortniteBuild> builds) {
|
||||||
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -292,7 +288,8 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
||||||
controller: _pathController,
|
controller: _pathController,
|
||||||
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
||||||
folder: true
|
folder: true,
|
||||||
|
allowNavigator: true
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -300,6 +297,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String? _checkGameFolder(text) {
|
String? _checkGameFolder(text) {
|
||||||
if (text == null || text.isEmpty) {
|
if (text == null || text.isEmpty) {
|
||||||
@@ -421,16 +419,15 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
|||||||
_build.value = null;
|
_build.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) {
|
final disks = WindowsDisk.available();
|
||||||
await _fetchFuture;
|
if(_source.value != _BuildSource.local && disks.isNotEmpty) {
|
||||||
final bestDisk = _diskSpace.disks
|
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
|
||||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
|
||||||
final build = _build.value;
|
final build = _build.value;
|
||||||
if(build == null){
|
if(build == null){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
|
||||||
_pathController.text = pathText;
|
_pathController.text = pathText;
|
||||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||||
}
|
}
|
||||||
@@ -5,17 +5,16 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/widget/message/data.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/server_start_button.dart';
|
import 'package:reboot_launcher/src/widget/server/server_start_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
||||||
@@ -153,9 +152,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Obx(() => Text(
|
||||||
_backendController.detached.value ? translations.on : translations.off
|
_backendController.detached.value ? translations.on : translations.off
|
||||||
),
|
)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16.0
|
width: 16.0
|
||||||
),
|
),
|
||||||
@@ -163,7 +162,12 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
key: backendDetachedOverlayTargetKey,
|
key: backendDetachedOverlayTargetKey,
|
||||||
child: ToggleSwitch(
|
child: ToggleSwitch(
|
||||||
checked: _backendController.detached(),
|
checked: _backendController.detached(),
|
||||||
onChanged: (value) => _backendController.detached.value = value
|
onChanged: (value) async {
|
||||||
|
_backendController.detached.value = value;
|
||||||
|
if(_backendController.started.value) {
|
||||||
|
await _backendController.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
@@ -9,12 +8,11 @@ import 'package:reboot_common/common.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
import 'package:reboot_launcher/src/page/page_type.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/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
|
|
||||||
class BrowsePage extends RebootPage {
|
class BrowsePage extends RebootPage {
|
||||||
const BrowsePage({Key? key}) : super(key: key);
|
const BrowsePage({Key? key}) : super(key: key);
|
||||||
@@ -211,10 +209,18 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||||
),
|
),
|
||||||
title: Text("${_formatName(entry)} • ${entry.author}"),
|
title: Text(
|
||||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
"${_formatName(entry)} • ${entry.author}",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"${_formatDescription(entry)} • ${_formatVersion(entry)}",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis
|
||||||
|
),
|
||||||
content: Button(
|
content: Button(
|
||||||
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
|
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
||||||
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -276,8 +282,8 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
|
|||||||
_filterControllerStream.add("");
|
_filterControllerStream.add("");
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
backgroundColor: ButtonState.all(Colors.transparent),
|
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
shape: ButtonState.all(Border())
|
shape: WidgetStateProperty.all(Border())
|
||||||
),
|
),
|
||||||
child: _searchBarIconData
|
child: _searchBarIconData
|
||||||
);
|
);
|
||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart' show MaterialPage;
|
import 'package:flutter/material.dart' show MaterialPage;
|
||||||
@@ -11,29 +10,28 @@ import 'package:get/get.dart';
|
|||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
|
import 'package:reboot_launcher/src/widget/message/dll.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
import 'package:reboot_launcher/src/page/page_suggestion.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
|
||||||
import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
import 'package:reboot_launcher/src/widget/fluent/profile_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
static const double kDefaultPadding = 12.0;
|
static const double kDefaultPadding = 12.0;
|
||||||
static const double kTitleBarHeight = 32;
|
|
||||||
|
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -43,6 +41,7 @@ class HomePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
@@ -58,7 +57,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
windowManager.setPreventClose(true);
|
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
_syncPageViewWithNavigator();
|
_syncPageViewWithNavigator();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -77,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
|
|
||||||
lastPage = index;
|
lastPage = index;
|
||||||
_pageController.jumpToPage(index);
|
_pageController.jumpToPage(index);
|
||||||
|
pagesController.add(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +93,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
final uuid = uri.host;
|
final uuid = uri.host;
|
||||||
final server = _hostingController.findServerById(uuid);
|
final server = _hostingController.findServerById(uuid);
|
||||||
if(server != null) {
|
if(server != null) {
|
||||||
_backendController.joinServerInteractive(_hostingController.uuid, server);
|
_backendController.joinServer(_hostingController.uuid, server);
|
||||||
}else {
|
}else {
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.noServerFound,
|
translations.noServerFound,
|
||||||
@@ -111,7 +110,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await pingGameServer(address);
|
final result = await pingGameServer(address);
|
||||||
if(result) {
|
if(result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,28 +134,50 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
dllsDirectory.createSync(recursive: true);
|
dllsDirectory.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(final injectable in InjectableDll.values) {
|
_dllController.guardFiles();
|
||||||
final (file, custom) = _dllController.getInjectableData(injectable);
|
|
||||||
if(!custom) {
|
|
||||||
_dllController.downloadCriticalDllInteractive(
|
|
||||||
file.path,
|
|
||||||
silent: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
|
||||||
_dllController.downloadCriticalDllInteractive(filePath);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowClose() async {
|
void onWindowClose() async {
|
||||||
try {
|
try {
|
||||||
await _hostingController.discardServer();
|
await windowManager.hide();
|
||||||
}finally {
|
}catch(error) {
|
||||||
exit(0); // Force closing
|
log("[WINDOW] Cannot hide window: $error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _hostingController.discardServer();
|
||||||
|
}catch(error) {
|
||||||
|
log("[HOSTING] Cannot discard server on exit: $error");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(_backendController.started.value) {
|
||||||
|
await _backendController.toggle();
|
||||||
|
}
|
||||||
|
}catch(error) {
|
||||||
|
log("[BACKEND] Cannot stop backend on exit: $error");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_gameController.instance.value?.kill();
|
||||||
|
}catch(error) {
|
||||||
|
log("[GAME] Cannot stop game on exit: $error");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_hostingController.instance.value?.kill();
|
||||||
|
}catch(error) {
|
||||||
|
log("[HOST] Cannot stop host on exit: $error");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stopDownloadServer();
|
||||||
|
}catch(error) {
|
||||||
|
log("[ARIA] Cannot stop aria server on exit: $error");
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -220,14 +241,18 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowResized() {
|
void onWindowResized() {
|
||||||
_settingsController.saveWindowSize(appWindow.size);
|
|
||||||
_focused.value = true;
|
_focused.value = true;
|
||||||
|
windowManager.getSize().then((size) {
|
||||||
|
_settingsController.saveWindowSize(size);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowMoved() {
|
void onWindowMoved() {
|
||||||
_settingsController.saveWindowOffset(appWindow.position);
|
|
||||||
_focused.value = true;
|
_focused.value = true;
|
||||||
|
windowManager.getPosition().then((position) {
|
||||||
|
_settingsController.saveWindowOffset(position);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -240,107 +265,79 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
_focused.value = true;
|
_focused.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowEvent(String eventName) {
|
|
||||||
if(eventName != "move") {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
_settingsController.language.value;
|
_settingsController.language.value;
|
||||||
loadTranslations(context);
|
loadTranslations(context);
|
||||||
return Obx(() {
|
return Container(
|
||||||
return Container(
|
|
||||||
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||||
child: Column(
|
child: Navigator(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
key: appNavigatorKey,
|
||||||
children: [
|
onPopPage: (page, data) => false,
|
||||||
SizedBox(
|
pages: [
|
||||||
height: HomePage.kTitleBarHeight,
|
MaterialPage(
|
||||||
child: Row(
|
child: Overlay(
|
||||||
children: [
|
key: appOverlayKey,
|
||||||
_backButton,
|
initialEntries: [
|
||||||
Expanded(child: _draggableArea),
|
OverlayEntry(
|
||||||
WindowTitleBar(focused: _focused())
|
maintainState: true,
|
||||||
],
|
builder: (context) => Row(
|
||||||
)
|
children: [
|
||||||
),
|
_buildLateralView(),
|
||||||
Expanded(
|
_buildBody()
|
||||||
child: Navigator(
|
],
|
||||||
key: appNavigatorKey,
|
)
|
||||||
onPopPage: (page, data) => false,
|
|
||||||
pages: [
|
|
||||||
MaterialPage(
|
|
||||||
child: Overlay(
|
|
||||||
key: appOverlayKey,
|
|
||||||
initialEntries: [
|
|
||||||
OverlayEntry(
|
|
||||||
maintainState: true,
|
|
||||||
builder: (context) => Row(
|
|
||||||
children: [
|
|
||||||
_buildLateralView(),
|
|
||||||
_buildBody()
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBody() {
|
|
||||||
return Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: HomePage.kDefaultPadding,
|
|
||||||
right: HomePage.kDefaultPadding * 2,
|
|
||||||
top: HomePage.kDefaultPadding,
|
|
||||||
bottom: HomePage.kDefaultPadding * 2
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: 1000
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildBodyHeader(),
|
|
||||||
const SizedBox(height: 24.0),
|
|
||||||
Expanded(
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.loose,
|
|
||||||
children: [
|
|
||||||
_buildBodyContent(),
|
|
||||||
InfoBarArea(
|
|
||||||
key: infoBarAreaKey
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() => Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: HomePage.kDefaultPadding,
|
||||||
|
right: HomePage.kDefaultPadding * 2,
|
||||||
|
top: HomePage.kDefaultPadding,
|
||||||
|
bottom: HomePage.kDefaultPadding * 2
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 1000
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildBodyHeader(),
|
||||||
|
const SizedBox(height: 24.0),
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.loose,
|
||||||
|
children: [
|
||||||
|
_buildBodyContent(),
|
||||||
|
InfoBarArea(
|
||||||
|
key: infoBarAreaKey
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildBodyContent() => PageView.builder(
|
Widget _buildBodyContent() => PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemBuilder: (context, index) => Navigator(
|
itemBuilder: (context, index) => Navigator(
|
||||||
@@ -451,9 +448,12 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ProfileWidget(
|
Obx(() {
|
||||||
overlayKey: profileOverlayKey
|
pageIndex.value;
|
||||||
),
|
return ProfileWidget(
|
||||||
|
overlayKey: profileOverlayKey
|
||||||
|
);
|
||||||
|
}),
|
||||||
_autoSuggestBox,
|
_autoSuggestBox,
|
||||||
const SizedBox(height: 12.0),
|
const SizedBox(height: 12.0),
|
||||||
_buildNavigationTrail()
|
_buildNavigationTrail()
|
||||||
@@ -500,7 +500,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ButtonThemeData.uncheckedInputColor(
|
color: ButtonThemeData.uncheckedInputColor(
|
||||||
FluentTheme.of(context),
|
FluentTheme.of(context),
|
||||||
pageIndex.value == index ? {ButtonStates.hovering} : states,
|
pageIndex.value == index ? {WidgetState.hovered} : states,
|
||||||
transparentWhenNone: true,
|
transparentWhenNone: true,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||||
@@ -525,42 +525,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget get _backButton => StreamBuilder(
|
|
||||||
stream: pagesController.stream,
|
|
||||||
builder: (context, _) => Button(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: ButtonState.all(const EdgeInsets.symmetric(
|
|
||||||
vertical: 12.0,
|
|
||||||
horizontal: 16.0
|
|
||||||
)),
|
|
||||||
backgroundColor: ButtonState.all(Colors.transparent),
|
|
||||||
shape: ButtonState.all(Border())
|
|
||||||
),
|
|
||||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
|
||||||
if(inDialog) {
|
|
||||||
Navigator.of(appNavigatorKey.currentContext!).pop();
|
|
||||||
}else {
|
|
||||||
final lastPage = appStack.removeLast();
|
|
||||||
pageStack.remove(lastPage);
|
|
||||||
if (lastPage is int) {
|
|
||||||
hitBack = true;
|
|
||||||
pageIndex.value = lastPage;
|
|
||||||
} else {
|
|
||||||
Navigator.of(pageKey.currentContext!).pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pagesController.add(null);
|
|
||||||
},
|
|
||||||
child: const Icon(FluentIcons.back, size: 12.0),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
GestureDetector get _draggableArea => GestureDetector(
|
|
||||||
onDoubleTap: appWindow.maximizeOrRestore,
|
|
||||||
onHorizontalDragStart: (_) => windowManager.startDragging(),
|
|
||||||
onVerticalDragStart: (_) => windowManager.startDragging()
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _autoSuggestBox => Padding(
|
Widget get _autoSuggestBox => Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0,
|
horizontal: 16.0,
|
||||||
@@ -10,18 +10,16 @@ import 'package:reboot_launcher/main.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/widget/message/data.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
import 'package:reboot_launcher/src/page/page_type.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/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||||
@@ -53,7 +51,6 @@ class HostPage extends RebootPage {
|
|||||||
class _HostingPageState extends RebootPageState<HostPage> {
|
class _HostingPageState extends RebootPageState<HostPage> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
|
|
||||||
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||||
@@ -82,10 +79,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
List<Widget> get settings => [
|
List<Widget> get settings => [
|
||||||
_information,
|
_information,
|
||||||
buildVersionSelector(
|
buildVersionSelector(
|
||||||
key: hostVersionOverlayTargetKey
|
key: hostVersionOverlayTargetKey
|
||||||
),
|
),
|
||||||
_options,
|
_options,
|
||||||
_internalFiles,
|
|
||||||
_share,
|
_share,
|
||||||
_resetDefaults
|
_resetDefaults
|
||||||
];
|
];
|
||||||
@@ -155,8 +151,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
suffix: Button(
|
suffix: Button(
|
||||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
shape: ButtonState.all(const CircleBorder()),
|
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||||
backgroundColor: ButtonState.all(Colors.transparent)
|
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||||
@@ -175,9 +171,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Obx(() => Row(
|
content: Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Obx(() => Text(
|
||||||
_hostingController.discoverable.value ? translations.on : translations.off
|
_hostingController.discoverable.value ? translations.on : translations.off
|
||||||
),
|
)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16.0
|
width: 16.0
|
||||||
),
|
),
|
||||||
@@ -221,12 +217,11 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
content: Obx(() => DropDownButton(
|
content: Obx(() => DropDownButton(
|
||||||
onOpen: () => inDialog = true,
|
onOpen: () => inDialog = true,
|
||||||
onClose: () => inDialog = false,
|
onClose: () => inDialog = false,
|
||||||
leading: Text(_settingsController.debug.value ? GameServerType.window.translatedName : _hostingController.type.value.translatedName),
|
leading: Text(_hostingController.type.value.translatedName),
|
||||||
items: GameServerType.values.map((entry) => MenuFlyoutItem(
|
items: GameServerType.values.map((entry) => MenuFlyoutItem(
|
||||||
text: Text(entry.translatedName),
|
text: Text(entry.translatedName),
|
||||||
onPressed: () => _hostingController.type.value = entry
|
onPressed: () => _hostingController.type.value = entry
|
||||||
)).toList(),
|
)).toList()
|
||||||
disabled: _settingsController.debug.value
|
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
@@ -238,9 +233,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Obx(() => Text(
|
||||||
_hostingController.autoRestart.value ? translations.on : translations.off
|
_hostingController.autoRestart.value ? translations.on : translations.off
|
||||||
),
|
)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16.0
|
width: 16.0
|
||||||
),
|
),
|
||||||
@@ -271,159 +266,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
SettingTile get _internalFiles => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.archive_settings_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerName),
|
|
||||||
subtitle: Text(translations.settingsServerSubtitle),
|
|
||||||
children: [
|
|
||||||
SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.timer_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerTypeName),
|
|
||||||
subtitle: Text(translations.settingsServerTypeDescription),
|
|
||||||
content: Obx(() => DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
|
||||||
items: {
|
|
||||||
false: translations.settingsServerTypeEmbeddedName,
|
|
||||||
true: translations.settingsServerTypeCustomName
|
|
||||||
}.entries.map((entry) => MenuFlyoutItem(
|
|
||||||
text: Text(entry.value),
|
|
||||||
onPressed: () {
|
|
||||||
final oldValue = _dllController.customGameServer.value;
|
|
||||||
if(oldValue == entry.key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_dllController.customGameServer.value = entry.key;
|
|
||||||
_dllController.infoBarEntry?.close();
|
|
||||||
if(!entry.key) {
|
|
||||||
_dllController.updateGameServerDll(
|
|
||||||
force: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)).toList()
|
|
||||||
))
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
if(!_dllController.customGameServer.value) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return createFileSetting(
|
|
||||||
title: translations.settingsServerFileName,
|
|
||||||
description: translations.settingsServerFileDescription,
|
|
||||||
controller: _dllController.gameServerDll,
|
|
||||||
onReset: () {
|
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
|
|
||||||
_dllController.gameServerDll.text = path;
|
|
||||||
_dllController.downloadCriticalDllInteractive(path);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Obx(() {
|
|
||||||
if(_dllController.customGameServer.value) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.globe_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerMirrorName),
|
|
||||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormBox(
|
|
||||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
|
||||||
controller: _dllController.url,
|
|
||||||
validator: _checkUpdateUrl
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8.0),
|
|
||||||
Button(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: ButtonState.all(EdgeInsets.zero)
|
|
||||||
),
|
|
||||||
onPressed: () => _dllController.url.text = kRebootDownloadUrl,
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 30,
|
|
||||||
child: Icon(
|
|
||||||
FluentIcons.arrow_reset_24_regular
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Obx(() {
|
|
||||||
if(_dllController.customGameServer.value) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.timer_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerTimerName),
|
|
||||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Obx(() => DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(_dllController.timer.value.text),
|
|
||||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
|
||||||
text: Text(entry.text),
|
|
||||||
onPressed: () {
|
|
||||||
_dllController.timer.value = entry;
|
|
||||||
_dllController.infoBarEntry?.close();
|
|
||||||
_dllController.updateGameServerDll(
|
|
||||||
force: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)).toList()
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8.0),
|
|
||||||
Button(
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: ButtonState.all(EdgeInsets.zero)
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
_dllController.updateGameServerDll(force: true);
|
|
||||||
},
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 30,
|
|
||||||
child: Icon(
|
|
||||||
FluentIcons.arrow_download_24_regular
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
String? _checkUpdateUrl(String? text) {
|
|
||||||
if (text == null || text.isEmpty) {
|
|
||||||
return translations.emptyURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingTile get _share => SettingTile(
|
SettingTile get _share => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.link_24_regular
|
FluentIcons.link_24_regular
|
||||||
@@ -494,8 +336,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_hostingController.publishServer(
|
_hostingController.publishServer(
|
||||||
_gameController.username.text,
|
_hostingController.accountUsername.text,
|
||||||
_hostingController.instance.value!.versionName
|
_hostingController.instance.value!.version.toString()
|
||||||
);
|
);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
_showCannotUpdateGameServer(error);
|
_showCannotUpdateGameServer(error);
|
||||||
@@ -529,14 +371,4 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
extension _UpdateTimerExtension on UpdateTimer {
|
|
||||||
String get text {
|
|
||||||
if (this == UpdateTimer.never) {
|
|
||||||
return translations.updateGameServerDllNever;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations.updateGameServerDllEvery(name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
import 'package:reboot_launcher/src/widget/message/onboard.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class InfoPage extends RebootPage {
|
class InfoPage extends RebootPage {
|
||||||
@@ -29,7 +29,7 @@ class InfoPage extends RebootPage {
|
|||||||
|
|
||||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
||||||
static const String _kDiscordInviteUrl = "https://discord.gg/reboot";
|
static const String _kDiscordInviteUrl = "https://discord.gg/rebootmp";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<SettingTile> get settings => [
|
List<SettingTile> get settings => [
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/widget/message/data.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||||
|
|
||||||
@@ -38,7 +34,6 @@ class PlayPage extends RebootPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
|
|
||||||
@@ -55,50 +50,9 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
|||||||
key: gameVersionOverlayTargetKey
|
key: gameVersionOverlayTargetKey
|
||||||
),
|
),
|
||||||
_options,
|
_options,
|
||||||
_internalFiles,
|
|
||||||
_resetDefaults
|
_resetDefaults
|
||||||
];
|
];
|
||||||
|
|
||||||
SettingTile get _internalFiles => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.archive_settings_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsClientName),
|
|
||||||
subtitle: Text(translations.settingsClientDescription),
|
|
||||||
children: [
|
|
||||||
createFileSetting(
|
|
||||||
title: translations.settingsClientConsoleName,
|
|
||||||
description: translations.settingsClientConsoleDescription,
|
|
||||||
controller: _dllController.unrealEngineConsoleDll,
|
|
||||||
onReset: () {
|
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.console);
|
|
||||||
_dllController.unrealEngineConsoleDll.text = path;
|
|
||||||
_dllController.downloadCriticalDllInteractive(path, force: true);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
createFileSetting(
|
|
||||||
title: translations.settingsClientAuthName,
|
|
||||||
description: translations.settingsClientAuthDescription,
|
|
||||||
controller: _dllController.backendDll,
|
|
||||||
onReset: () {
|
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.cobalt);
|
|
||||||
_dllController.backendDll.text = path;
|
|
||||||
_dllController.downloadCriticalDllInteractive(path, force: true);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
createFileSetting(
|
|
||||||
title: translations.settingsClientMemoryName,
|
|
||||||
description: translations.settingsClientMemoryDescription,
|
|
||||||
controller: _dllController.memoryLeakDll,
|
|
||||||
onReset: () {
|
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.memory);
|
|
||||||
_dllController.memoryLeakDll.text = path;
|
|
||||||
_dllController.downloadCriticalDllInteractive(path, force: true);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
SettingTile get _options => SettingTile(
|
SettingTile get _options => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.options_24_regular
|
FluentIcons.options_24_regular
|
||||||
375
gui/lib/src/widget/page/settings_page.dart
Normal file
375
gui/lib/src/widget/page/settings_page.dart
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/reboot_localizations.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/dll_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/page_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
|
import 'package: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 hasButton(String? pageName) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RebootPageState<SettingsPage> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
final DllController _dllController = Get.find<DllController>();
|
||||||
|
int? _downloadFromMirrorId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? get button => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get settings => [
|
||||||
|
_language,
|
||||||
|
_theme,
|
||||||
|
_internalFiles,
|
||||||
|
_installationDirectory,
|
||||||
|
];
|
||||||
|
|
||||||
|
SettingTile get _internalFiles => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.archive_settings_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsClientName),
|
||||||
|
subtitle: Text(translations.settingsClientDescription),
|
||||||
|
children: [
|
||||||
|
createFileSetting(
|
||||||
|
title: translations.settingsClientConsoleName,
|
||||||
|
description: translations.settingsClientConsoleDescription,
|
||||||
|
controller: _dllController.unrealEngineConsoleDll,
|
||||||
|
onReset: () {
|
||||||
|
final path = _dllController.getDefaultDllPath(InjectableDll.console);
|
||||||
|
_dllController.unrealEngineConsoleDll.text = path;
|
||||||
|
_dllController.download(InjectableDll.console, path, force: true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
createFileSetting(
|
||||||
|
title: translations.settingsClientAuthName,
|
||||||
|
description: translations.settingsClientAuthDescription,
|
||||||
|
controller: _dllController.backendDll,
|
||||||
|
onReset: () {
|
||||||
|
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
|
||||||
|
_dllController.backendDll.text = path;
|
||||||
|
_dllController.download(InjectableDll.auth, path, force: true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
createFileSetting(
|
||||||
|
title: translations.settingsClientMemoryName,
|
||||||
|
description: translations.settingsClientMemoryDescription,
|
||||||
|
controller: _dllController.memoryLeakDll,
|
||||||
|
onReset: () {
|
||||||
|
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
|
||||||
|
_dllController.memoryLeakDll.text = path;
|
||||||
|
_dllController.download(InjectableDll.memoryLeak, path, force: true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_internalFilesServerType,
|
||||||
|
_internalFilesUpdateTimer,
|
||||||
|
_internalFilesServerSource,
|
||||||
|
_internalFilesNewServerSource,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _internalFilesServerType => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.games_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerTypeName),
|
||||||
|
subtitle: Text(translations.settingsServerTypeDescription),
|
||||||
|
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||||
|
content: Obx(() => DropDownButton(
|
||||||
|
onOpen: () => inDialog = true,
|
||||||
|
onClose: () => inDialog = false,
|
||||||
|
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||||
|
items: {
|
||||||
|
false: translations.settingsServerTypeEmbeddedName,
|
||||||
|
true: translations.settingsServerTypeCustomName
|
||||||
|
}.entries.map((entry) => MenuFlyoutItem(
|
||||||
|
text: Text(entry.value),
|
||||||
|
onPressed: () {
|
||||||
|
final oldValue = _dllController.customGameServer.value;
|
||||||
|
if(oldValue == entry.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dllController.customGameServer.value = entry.key;
|
||||||
|
if(!entry.key) {
|
||||||
|
_dllController.updateGameServerDll(
|
||||||
|
force: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).toList()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _internalFilesServerSource => Obx(() {
|
||||||
|
if(!_dllController.customGameServer.value) {
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.globe_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerOldMirrorName),
|
||||||
|
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||||
|
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormBox(
|
||||||
|
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||||
|
controller: _dllController.beforeS20Mirror,
|
||||||
|
onChanged: _scheduleMirrorDownload
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||||
|
),
|
||||||
|
onPressed: () => _dllController.updateGameServerDll(force: true),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 30,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.arrow_download_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||||
|
_dllController.updateGameServerDll(force: true);
|
||||||
|
},
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 30,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.arrow_reset_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return createFileSetting(
|
||||||
|
title: translations.settingsOldServerFileName,
|
||||||
|
description: translations.settingsServerFileDescription,
|
||||||
|
controller: _dllController.customGameServerDll,
|
||||||
|
onReset: () {
|
||||||
|
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
|
||||||
|
_dllController.customGameServerDll.text = path;
|
||||||
|
_dllController.download(InjectableDll.gameServer, path);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void _scheduleMirrorDownload(String value) async {
|
||||||
|
if(_downloadFromMirrorId != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Uri.tryParse(value) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final id = Random.secure().nextInt(1000000);
|
||||||
|
_downloadFromMirrorId = id;
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
if(_downloadFromMirrorId == id) {
|
||||||
|
await _dllController.updateGameServerDll(force: true);
|
||||||
|
}
|
||||||
|
_downloadFromMirrorId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _internalFilesNewServerSource => Obx(() {
|
||||||
|
if(!_dllController.customGameServer.value) {
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.globe_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerNewMirrorName),
|
||||||
|
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||||
|
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormBox(
|
||||||
|
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||||
|
controller: _dllController.aboveS20Mirror,
|
||||||
|
onChanged: _scheduleMirrorDownload
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||||
|
),
|
||||||
|
onPressed: () => _dllController.updateGameServerDll(force: true),
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 30,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.arrow_download_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(EdgeInsets.zero)
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||||
|
_dllController.updateGameServerDll(force: true);
|
||||||
|
},
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 30,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.arrow_reset_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget get _internalFilesUpdateTimer => Obx(() {
|
||||||
|
if(_dllController.customGameServer.value) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.timer_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerTimerName),
|
||||||
|
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||||
|
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
||||||
|
content: Obx(() => DropDownButton(
|
||||||
|
onOpen: () => inDialog = true,
|
||||||
|
onClose: () => inDialog = false,
|
||||||
|
leading: Text(_dllController.timer.value.text),
|
||||||
|
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||||
|
text: Text(entry.text),
|
||||||
|
onPressed: () {
|
||||||
|
_dllController.timer.value = entry;
|
||||||
|
_dllController.updateGameServerDll(
|
||||||
|
force: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)).toList()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingTile get _language => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.local_language_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsUtilsLanguageName),
|
||||||
|
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||||
|
content: Obx(() => DropDownButton(
|
||||||
|
onOpen: () => inDialog = true,
|
||||||
|
onClose: () => inDialog = false,
|
||||||
|
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||||
|
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||||
|
text: Text(_getLocaleName(locale.languageCode)),
|
||||||
|
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||||
|
)).toList()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingTile get _theme => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.dark_theme_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsUtilsThemeName),
|
||||||
|
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||||
|
content: Obx(() => DropDownButton(
|
||||||
|
onOpen: () => inDialog = true,
|
||||||
|
onClose: () => inDialog = false,
|
||||||
|
leading: Text(_settingsController.themeMode.value.title),
|
||||||
|
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||||
|
text: Text(themeMode.title),
|
||||||
|
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||||
|
)).toList()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
SettingTile get _installationDirectory => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.folder_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsUtilsInstallationDirectoryName),
|
||||||
|
subtitle: Text(translations.settingsUtilsInstallationDirectorySubtitle),
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => launchUrl(installationDirectory.uri),
|
||||||
|
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _UpdateTimerExtension on UpdateTimer {
|
||||||
|
String get text {
|
||||||
|
if (this == UpdateTimer.never) {
|
||||||
|
return translations.updateGameServerDllNever;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations.updateGameServerDllEvery(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
class ServerButton extends StatefulWidget {
|
class ServerButton extends StatefulWidget {
|
||||||
@@ -46,7 +45,7 @@ class _ServerButtonState extends State<ServerButton> {
|
|||||||
builder: (context, snapshot) => Obx(() => Text(_buttonText))
|
builder: (context, snapshot) => Obx(() => Text(_buttonText))
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => _controller.toggleInteractive()
|
onPressed: () => _controller.toggle()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -2,8 +2,8 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
class ServerTypeSelector extends StatefulWidget {
|
class ServerTypeSelector extends StatefulWidget {
|
||||||
@@ -32,18 +32,16 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuFlyoutItem _createItem(ServerType type) {
|
MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem(
|
||||||
return MenuFlyoutItem(
|
text: Text(type.label),
|
||||||
text: Text(type.label),
|
onPressed: () async {
|
||||||
onPressed: () async {
|
await _controller.stop(interactive: false);
|
||||||
_controller.stop();
|
_controller.type.value = type;
|
||||||
_controller.type.value = type;
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ServerTypeExtension on ServerType {
|
extension _ServerTypeExtension on ServerType {
|
||||||
String get label {
|
String get label {
|
||||||
return this == ServerType.embedded ? translations.embedded
|
return this == ServerType.embedded ? translations.embedded
|
||||||
: this == ServerType.remote ? translations.remote
|
: this == ServerType.remote ? translations.remote
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
|
|
||||||
import 'package:system_theme/system_theme.dart';
|
|
||||||
|
|
||||||
class WindowTitleBar extends StatelessWidget {
|
|
||||||
final bool focused;
|
|
||||||
|
|
||||||
const WindowTitleBar({Key? key, required this.focused}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var lightMode = FluentTheme.of(context).brightness.isLight;
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
MinimizeWindowButton(
|
|
||||||
colors: WindowButtonColors(
|
|
||||||
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
|
|
||||||
iconMouseDown: lightMode ? Colors.black : Colors.white,
|
|
||||||
iconMouseOver: lightMode ? Colors.black : Colors.white,
|
|
||||||
normal: Colors.transparent,
|
|
||||||
mouseOver: _color,
|
|
||||||
mouseDown: _color.withOpacity(0.7)),
|
|
||||||
),
|
|
||||||
MaximizeWindowButton(
|
|
||||||
colors: WindowButtonColors(
|
|
||||||
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
|
|
||||||
iconMouseDown: lightMode ? Colors.black : Colors.white,
|
|
||||||
iconMouseOver: lightMode ? Colors.black : Colors.white,
|
|
||||||
normal: Colors.transparent,
|
|
||||||
mouseOver: _color,
|
|
||||||
mouseDown: _color.withOpacity(0.7)),
|
|
||||||
),
|
|
||||||
CloseWindowButton(
|
|
||||||
colors: WindowButtonColors(
|
|
||||||
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
|
|
||||||
iconMouseDown: lightMode ? Colors.black : Colors.white,
|
|
||||||
iconMouseOver: lightMode ? Colors.black : Colors.white,
|
|
||||||
normal: Colors.transparent,
|
|
||||||
mouseOver: Colors.red,
|
|
||||||
mouseDown: Colors.red.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get _color =>
|
|
||||||
SystemTheme.accentColor.accent;
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import 'package:bitsdojo_window/bitsdojo_window.dart' show appWindow;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'title_bar_icons.dart';
|
|
||||||
import 'title_bar_mouse.dart';
|
|
||||||
|
|
||||||
typedef WindowButtonIconBuilder = Widget Function(
|
|
||||||
WindowButtonContext buttonContext);
|
|
||||||
typedef WindowButtonBuilder = Widget Function(
|
|
||||||
WindowButtonContext buttonContext, Widget icon);
|
|
||||||
|
|
||||||
class WindowButtonContext {
|
|
||||||
BuildContext context;
|
|
||||||
MouseState mouseState;
|
|
||||||
Color? backgroundColor;
|
|
||||||
Color iconColor;
|
|
||||||
|
|
||||||
WindowButtonContext(
|
|
||||||
{required this.context,
|
|
||||||
required this.mouseState,
|
|
||||||
this.backgroundColor,
|
|
||||||
required this.iconColor});
|
|
||||||
}
|
|
||||||
|
|
||||||
class WindowButtonColors {
|
|
||||||
late Color normal;
|
|
||||||
late Color mouseOver;
|
|
||||||
late Color mouseDown;
|
|
||||||
late Color iconNormal;
|
|
||||||
late Color iconMouseOver;
|
|
||||||
late Color iconMouseDown;
|
|
||||||
|
|
||||||
WindowButtonColors(
|
|
||||||
{Color? normal,
|
|
||||||
Color? mouseOver,
|
|
||||||
Color? mouseDown,
|
|
||||||
Color? iconNormal,
|
|
||||||
Color? iconMouseOver,
|
|
||||||
Color? iconMouseDown}) {
|
|
||||||
this.normal = normal ?? _defaultButtonColors.normal;
|
|
||||||
this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
|
|
||||||
this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
|
|
||||||
this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
|
|
||||||
this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
|
|
||||||
this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final _defaultButtonColors = WindowButtonColors(
|
|
||||||
normal: Colors.transparent,
|
|
||||||
iconNormal: const Color(0xFF805306),
|
|
||||||
mouseOver: const Color(0xFF404040),
|
|
||||||
mouseDown: const Color(0xFF202020),
|
|
||||||
iconMouseOver: const Color(0xFFFFFFFF),
|
|
||||||
iconMouseDown: const Color(0xFFF0F0F0));
|
|
||||||
|
|
||||||
class WindowButton extends StatelessWidget {
|
|
||||||
final WindowButtonBuilder? builder;
|
|
||||||
final WindowButtonIconBuilder? iconBuilder;
|
|
||||||
late final WindowButtonColors colors;
|
|
||||||
final bool animate;
|
|
||||||
final EdgeInsets? padding;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
|
|
||||||
WindowButton(
|
|
||||||
{Key? key,
|
|
||||||
WindowButtonColors? colors,
|
|
||||||
this.builder,
|
|
||||||
@required this.iconBuilder,
|
|
||||||
this.padding,
|
|
||||||
this.onPressed,
|
|
||||||
this.animate = false})
|
|
||||||
: super(key: key) {
|
|
||||||
this.colors = colors ?? _defaultButtonColors;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getBackgroundColor(MouseState mouseState) {
|
|
||||||
if (mouseState.isMouseDown) return colors.mouseDown;
|
|
||||||
if (mouseState.isMouseOver) return colors.mouseOver;
|
|
||||||
return colors.normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getIconColor(MouseState mouseState) {
|
|
||||||
if (mouseState.isMouseDown) return colors.iconMouseDown;
|
|
||||||
if (mouseState.isMouseOver) return colors.iconMouseOver;
|
|
||||||
return colors.iconNormal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MouseStateBuilder(
|
|
||||||
builder: (context, mouseState) {
|
|
||||||
WindowButtonContext buttonContext = WindowButtonContext(
|
|
||||||
mouseState: mouseState,
|
|
||||||
context: context,
|
|
||||||
backgroundColor: getBackgroundColor(mouseState),
|
|
||||||
iconColor: getIconColor(mouseState));
|
|
||||||
|
|
||||||
var icon =
|
|
||||||
(iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
|
|
||||||
var fadeOutColor =
|
|
||||||
getBackgroundColor(MouseState()..isMouseOver = true)
|
|
||||||
.withOpacity(0);
|
|
||||||
var padding = this.padding ?? EdgeInsets.zero;
|
|
||||||
var animationMs = mouseState.isMouseOver
|
|
||||||
? (animate ? 100 : 0)
|
|
||||||
: (animate ? 200 : 0);
|
|
||||||
Widget iconWithPadding = Padding(padding: padding, child: icon);
|
|
||||||
iconWithPadding = AnimatedContainer(
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
duration: Duration(milliseconds: animationMs),
|
|
||||||
color: buttonContext.backgroundColor ?? fadeOutColor,
|
|
||||||
child: iconWithPadding);
|
|
||||||
var button = (builder != null)
|
|
||||||
? builder!(buttonContext, icon)
|
|
||||||
: iconWithPadding;
|
|
||||||
return SizedBox.square(dimension: 45, child: button);
|
|
||||||
},
|
|
||||||
onPressed: onPressed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MinimizeWindowButton extends WindowButton {
|
|
||||||
MinimizeWindowButton(
|
|
||||||
{Key? key,
|
|
||||||
WindowButtonColors? colors,
|
|
||||||
VoidCallback? onPressed,
|
|
||||||
bool? animate})
|
|
||||||
: super(
|
|
||||||
key: key,
|
|
||||||
colors: colors,
|
|
||||||
animate: animate ?? false,
|
|
||||||
iconBuilder: (buttonContext) =>
|
|
||||||
MinimizeIcon(color: buttonContext.iconColor),
|
|
||||||
onPressed: onPressed ?? () => appWindow.minimize());
|
|
||||||
}
|
|
||||||
|
|
||||||
class MaximizeWindowButton extends WindowButton {
|
|
||||||
MaximizeWindowButton(
|
|
||||||
{Key? key,
|
|
||||||
WindowButtonColors? colors,
|
|
||||||
VoidCallback? onPressed,
|
|
||||||
bool? animate})
|
|
||||||
: super(
|
|
||||||
key: key,
|
|
||||||
colors: colors,
|
|
||||||
animate: animate ?? false,
|
|
||||||
iconBuilder: (buttonContext) =>
|
|
||||||
MaximizeIcon(color: buttonContext.iconColor),
|
|
||||||
onPressed: onPressed ??
|
|
||||||
() => appWindow.maximizeOrRestore());
|
|
||||||
}
|
|
||||||
|
|
||||||
final _defaultCloseButtonColors = WindowButtonColors(
|
|
||||||
mouseOver: const Color(0xFFD32F2F),
|
|
||||||
mouseDown: const Color(0xFFB71C1C),
|
|
||||||
iconNormal: const Color(0xFF805306),
|
|
||||||
iconMouseOver: const Color(0xFFFFFFFF));
|
|
||||||
|
|
||||||
class CloseWindowButton extends WindowButton {
|
|
||||||
CloseWindowButton(
|
|
||||||
{Key? key,
|
|
||||||
WindowButtonColors? colors,
|
|
||||||
VoidCallback? onPressed,
|
|
||||||
bool? animate})
|
|
||||||
: super(
|
|
||||||
key: key,
|
|
||||||
colors: colors ?? _defaultCloseButtonColors,
|
|
||||||
animate: animate ?? false,
|
|
||||||
iconBuilder: (buttonContext) =>
|
|
||||||
CloseIcon(color: buttonContext.iconColor),
|
|
||||||
onPressed: onPressed ?? () => appWindow.close());
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class CloseIcon extends StatelessWidget {
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const CloseIcon({Key? key, required this.color}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Stack(children: [
|
|
||||||
Transform.rotate(
|
|
||||||
angle: pi * .25,
|
|
||||||
child:
|
|
||||||
Center(child: Container(width: 14, height: 1, color: color))),
|
|
||||||
Transform.rotate(
|
|
||||||
angle: pi * -.25,
|
|
||||||
child:
|
|
||||||
Center(child: Container(width: 14, height: 1, color: color))),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MaximizeIcon extends StatelessWidget {
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const MaximizeIcon({Key? key, required this.color}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MaximizePainter extends _IconPainter {
|
|
||||||
_MaximizePainter(Color color) : super(color);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
Paint p = getPaint(color);
|
|
||||||
canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestoreIcon extends StatelessWidget {
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const RestoreIcon({
|
|
||||||
Key? key,
|
|
||||||
required this.color,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RestorePainter extends _IconPainter {
|
|
||||||
_RestorePainter(Color color) : super(color);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
Paint p = getPaint(color);
|
|
||||||
canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
|
|
||||||
canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
|
|
||||||
canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
|
|
||||||
canvas.drawLine(
|
|
||||||
Offset(size.width, 0), Offset(size.width, size.height - 2), p);
|
|
||||||
canvas.drawLine(Offset(size.width, size.height - 2),
|
|
||||||
Offset(size.width - 2, size.height - 2), p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MinimizeIcon extends StatelessWidget {
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const MinimizeIcon({Key? key, required this.color}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MinimizePainter extends _IconPainter {
|
|
||||||
_MinimizePainter(Color color) : super(color);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
Paint p = getPaint(color);
|
|
||||||
canvas.drawLine(
|
|
||||||
Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class _IconPainter extends CustomPainter {
|
|
||||||
_IconPainter(this.color);
|
|
||||||
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AlignedPaint extends StatelessWidget {
|
|
||||||
const _AlignedPaint(this.painter, {Key? key}) : super(key: key);
|
|
||||||
final CustomPainter painter;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: CustomPaint(size: const Size(10, 10), painter: painter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
|
|
||||||
..color = color
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..isAntiAlias = isAntiAlias
|
|
||||||
..strokeWidth = 1;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
typedef MouseStateBuilderCB = Widget Function(
|
|
||||||
BuildContext context, MouseState mouseState);
|
|
||||||
|
|
||||||
class MouseState {
|
|
||||||
bool isMouseOver;
|
|
||||||
bool isMouseDown;
|
|
||||||
|
|
||||||
MouseState() : isMouseOver = false, isMouseDown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MouseStateBuilder extends StatefulWidget {
|
|
||||||
final MouseStateBuilderCB builder;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
|
|
||||||
const MouseStateBuilder({Key? key, required this.builder, this.onPressed})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MouseStateBuilder> createState() => _MouseStateBuilderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MouseStateBuilderState extends State<MouseStateBuilder> {
|
|
||||||
late MouseState _mouseState;
|
|
||||||
|
|
||||||
_MouseStateBuilderState() {
|
|
||||||
_mouseState = MouseState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MouseRegion(
|
|
||||||
onEnter: (event) {
|
|
||||||
setState(() {
|
|
||||||
_mouseState.isMouseOver = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onExit: (event) {
|
|
||||||
setState(() {
|
|
||||||
_mouseState.isMouseOver = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: GestureDetector(
|
|
||||||
onTapDown: (_) {
|
|
||||||
setState(() {
|
|
||||||
_mouseState.isMouseDown = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onTapCancel: () {
|
|
||||||
setState(() {
|
|
||||||
_mouseState.isMouseDown = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_mouseState.isMouseDown = false;
|
|
||||||
_mouseState.isMouseOver = false;
|
|
||||||
});
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (widget.onPressed != null) {
|
|
||||||
widget.onPressed!();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onTapUp: (_) {},
|
|
||||||
child: widget.builder(context, _mouseState)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,20 +6,20 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/implementation/version.dart';
|
import 'package:reboot_launcher/src/widget/message/version.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class VersionSelector extends StatefulWidget {
|
class VersionSelector extends StatefulWidget {
|
||||||
const VersionSelector({Key? key}) : super(key: key);
|
const VersionSelector({Key? key}) : super(key: key);
|
||||||
|
|
||||||
static Future<void> openDownloadDialog({bool closable = true}) => showRebootDialog<bool>(
|
static Future<void> openDownloadDialog() => showRebootDialog<bool>(
|
||||||
builder: (context) => AddVersionDialog(
|
builder: (context) => AddVersionDialog(
|
||||||
closable: closable,
|
closable: true,
|
||||||
),
|
),
|
||||||
dismissWithEsc: closable
|
dismissWithEsc: true
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||||
|
|
||||||
SettingTile buildVersionSelector({
|
SettingTile buildVersionSelector({
|
||||||
required GlobalKey<OverlayTargetState> key
|
required GlobalKey<OverlayTargetState> key
|
||||||
@@ -24,19 +24,22 @@ class InfoBarAreaState extends State<InfoBarArea> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Obx(() => Padding(
|
Widget build(BuildContext context) => StreamBuilder(
|
||||||
padding: EdgeInsets.only(
|
stream: pagesController.stream,
|
||||||
bottom: hasPageButton ? 72.0 : 16.0
|
builder: (context, _) => Obx(() => Padding(
|
||||||
),
|
padding: EdgeInsets.only(
|
||||||
child: Column(
|
bottom: hasPageButton ? 72.0 : 16.0
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: _children.value.map((child) => Padding(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
padding: EdgeInsets.only(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
top: 12.0
|
children: _children.value.map((child) => Padding(
|
||||||
),
|
padding: EdgeInsets.only(
|
||||||
child: child
|
top: 12.0
|
||||||
)).toList(growable: false)
|
),
|
||||||
),
|
child: child
|
||||||
));
|
)).toList(growable: false)
|
||||||
|
),
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
name: reboot_launcher
|
name: reboot_launcher
|
||||||
description: Graphical User Interface for Project Reboot
|
description: Graphical User Interface for Project Reboot
|
||||||
version: "9.2.6"
|
version: "10.0.7"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <=4.0.0"
|
# 3.19.0 is the last version that supports Windows 7/8/8.1 officially
|
||||||
|
# I have no clue who is still using Windows 7, but some users requested support, so might as well add it
|
||||||
|
# Repository Issue: https://github.com/Auties00/Reboot-Launcher/issues/58
|
||||||
|
# Flutter issue: https://github.com/flutter/flutter/issues/140830#issuecomment-1936397549
|
||||||
|
sdk: ">=3.0.0 <=3.19.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
# The flutter SDK
|
# The flutter SDK
|
||||||
@@ -17,53 +21,48 @@ dependencies:
|
|||||||
path: ./../common
|
path: ./../common
|
||||||
|
|
||||||
# Windows UI 3
|
# Windows UI 3
|
||||||
fluent_ui: ^4.8.7
|
fluent_ui: ^4.9.1
|
||||||
flutter_acrylic:
|
flutter_acrylic: ^1.1.4
|
||||||
path: ./dependencies/flutter_acrylic
|
fluentui_system_icons: ^1.1.258
|
||||||
fluentui_system_icons: ^1.1.238
|
system_theme: ^3.1.1
|
||||||
system_theme: ^2.0.0
|
|
||||||
skeletons:
|
skeletons:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/talok/skeletons
|
url: https://github.com/talok/skeletons
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
# Window management
|
# Window management
|
||||||
bitsdojo_window: ^0.1.5
|
window_manager: ^0.4.2
|
||||||
window_manager: ^0.3.8
|
|
||||||
|
|
||||||
# Extract zip archives (for example the reboot.zip)
|
# Extract zip archives (for example the reboot.zip)
|
||||||
archive: ^3.3.1
|
archive: ^3.6.1
|
||||||
|
|
||||||
# Cryptographic functions
|
# Cryptographic functions
|
||||||
crypto: ^3.0.2
|
|
||||||
bcrypt: ^1.1.3
|
bcrypt: ^1.1.3
|
||||||
pointycastle: ^3.7.3
|
pointycastle: ^3.9.1
|
||||||
|
|
||||||
# Async helpers
|
# Async helpers
|
||||||
async: ^2.8.2
|
async: ^2.11.0
|
||||||
sync: ^0.3.0
|
sync: ^0.3.0
|
||||||
|
synchronized: ^3.3.0+3
|
||||||
|
|
||||||
# State management
|
# State management
|
||||||
get: ^4.6.5
|
get: ^4.6.6
|
||||||
|
|
||||||
# Native utilities
|
# Native utilities
|
||||||
clipboard: ^0.1.3
|
clipboard: ^0.1.3
|
||||||
app_links: ^6.0.2
|
app_links: ^6.3.2
|
||||||
url_protocol: ^1.0.0
|
|
||||||
windows_taskbar: ^1.1.2
|
windows_taskbar: ^1.1.2
|
||||||
file_picker: ^8.0.3
|
file_picker: ^8.1.2
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.3.0
|
||||||
local_notifier: ^0.1.6
|
local_notifier: ^0.1.6
|
||||||
|
|
||||||
# Server browser
|
# Server browser
|
||||||
supabase_flutter: ^2.5.2
|
supabase_flutter: ^2.7.0
|
||||||
uuid: ^3.0.6
|
|
||||||
dart_ipify: ^1.1.1
|
dart_ipify: ^1.1.1
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
get_storage: ^2.0.3
|
get_storage: ^2.1.1
|
||||||
universal_disk_space: ^0.2.3
|
path: ^1.9.0
|
||||||
path: ^1.8.3
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
intl: any
|
intl: any
|
||||||
@@ -71,23 +70,17 @@ dependencies:
|
|||||||
|
|
||||||
# Auto updater
|
# Auto updater
|
||||||
yaml: ^3.1.2
|
yaml: ^3.1.2
|
||||||
package_info_plus: ^8.0.0
|
package_info_plus: ^8.0.2
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
|
|
||||||
# Validate profile
|
# Validate profile
|
||||||
email_validator: ^3.0.0
|
email_validator: ^3.0.0
|
||||||
|
|
||||||
dependency_overrides:
|
|
||||||
xml: ^6.3.0
|
|
||||||
http: ^0.13.5
|
|
||||||
win32: ^3.0.0
|
|
||||||
ffi: ^2.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_lints: ^4.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
|
||||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||||
#include <local_notifier/local_notifier_plugin.h>
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
#include <system_theme/system_theme_plugin.h>
|
#include <system_theme/system_theme_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
@@ -19,14 +18,12 @@
|
|||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AppLinksPluginCApiRegisterWithRegistrar(
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
|
||||||
FlutterAcrylicPluginRegisterWithRegistrar(
|
FlutterAcrylicPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
||||||
LocalNotifierPluginRegisterWithRegistrar(
|
LocalNotifierPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||||
SystemThemePluginRegisterWithRegistrar(
|
SystemThemePluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
app_links
|
||||||
bitsdojo_window_windows
|
|
||||||
flutter_acrylic
|
flutter_acrylic
|
||||||
local_notifier
|
local_notifier
|
||||||
screen_retriever
|
screen_retriever_windows
|
||||||
system_theme
|
system_theme
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
window_manager
|
window_manager
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
|||||||
Source: "..\..\dependencies\redist\VC_redist.x64.exe"; DestDir: {tmp}; Flags: dontcopy
|
Source: "..\..\dependencies\redist\VC_redist.x64.exe"; DestDir: {tmp}; Flags: dontcopy
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden
|
|
||||||
Filename: "{app}\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
|
Filename: "{app}\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
|
||||||
Filename: "{tmp}\VC_redist.x64.exe"; StatusMsg: "{cm:InstallingVC2017redist}"; Parameters: "/quiet"; Check: VC2017RedistNeedsInstall; Flags: waituntilterminated
|
Filename: "{tmp}\VC_redist.x64.exe"; StatusMsg: "{cm:InstallingVC2017redist}"; Parameters: "/quiet"; Check: VC2017RedistNeedsInstall; Flags: waituntilterminated
|
||||||
|
|
||||||
@@ -46,6 +45,44 @@ Name: "{autodesktop}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; T
|
|||||||
Name: "{userstartup}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup
|
Name: "{userstartup}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
|
var
|
||||||
|
Page: TInputOptionWizardPage;
|
||||||
|
|
||||||
|
procedure InitializeWizard();
|
||||||
|
begin
|
||||||
|
Page := CreateInputOptionPage(
|
||||||
|
wpWelcome,
|
||||||
|
' Allow DLL injection',
|
||||||
|
' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server',
|
||||||
|
'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' +
|
||||||
|
'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' +
|
||||||
|
'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' +
|
||||||
|
'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.',
|
||||||
|
False,
|
||||||
|
False
|
||||||
|
);
|
||||||
|
Page.Add('&Add the launcher to the Windows Exclusions list');
|
||||||
|
Page.Values[0] := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
var
|
||||||
|
ResultCode: Integer;
|
||||||
|
InstallationDir: String;
|
||||||
|
begin
|
||||||
|
if (CurStep = ssPostInstall) and Page.Values[0] then
|
||||||
|
begin
|
||||||
|
InstallationDir := ExpandConstant('{app}');
|
||||||
|
Exec('powershell.exe', '-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath ''' + InstallationDir + '''""' , '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||||
|
Log('Powershell exit code: ' + IntToStr(ResultCode));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function CompareVersion(version1, version2: String): Integer;
|
function CompareVersion(version1, version2: String): Integer;
|
||||||
var
|
var
|
||||||
packVersion1, packVersion2: Int64;
|
packVersion1, packVersion2: Int64;
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
|
||||||
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
#include <flutter/dart_project.h>
|
#include <flutter/dart_project.h>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
#include <dwmapi.h>
|
#include <dwmapi.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "Windowsx.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||||
@@ -121,7 +125,7 @@ bool Win32Window::CreateAndShow(const std::wstring &title,
|
|||||||
HWND window = CreateWindow(
|
HWND window = CreateWindow(
|
||||||
window_class,
|
window_class,
|
||||||
title.c_str(),
|
title.c_str(),
|
||||||
WS_OVERLAPPED | WS_BORDER | WS_THICKFRAME,
|
WS_OVERLAPPEDWINDOW,
|
||||||
Scale(origin.x, scale_factor),
|
Scale(origin.x, scale_factor),
|
||||||
Scale(origin.y, scale_factor),
|
Scale(origin.y, scale_factor),
|
||||||
Scale(size.width, scale_factor),
|
Scale(size.width, scale_factor),
|
||||||
@@ -160,48 +164,44 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
|||||||
}
|
}
|
||||||
|
|
||||||
LRESULT
|
LRESULT
|
||||||
Win32Window::MessageHandler(HWND hwnd,
|
Win32Window::MessageHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept {
|
||||||
UINT const message,
|
switch (uMsg) {
|
||||||
WPARAM const wparam,
|
case WM_DESTROY: {
|
||||||
LPARAM const lparam) noexcept {
|
|
||||||
switch (message) {
|
|
||||||
case WM_DESTROY:
|
|
||||||
window_handle_ = nullptr;
|
window_handle_ = nullptr;
|
||||||
Destroy();
|
Destroy();
|
||||||
if (quit_on_close_) {
|
if (quit_on_close_) {
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
case WM_DPICHANGED: {
|
case WM_DPICHANGED: {
|
||||||
auto newRectSize = reinterpret_cast<RECT *>(lparam);
|
auto newRectSize = reinterpret_cast<RECT *>(lParam);
|
||||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||||
|
SetWindowPos(hWnd, nullptr, newRectSize->left, newRectSize->top, newWidth,newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
|
||||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_SIZE: {
|
case WM_SIZE: {
|
||||||
RECT rect = GetClientArea();
|
auto rect = GetClientArea();
|
||||||
if (child_content_ != nullptr) {
|
if (child_content_ != nullptr) {
|
||||||
// Size and position the child window.
|
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,rect.bottom - rect.top, TRUE);
|
||||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
|
||||||
rect.bottom - rect.top, TRUE);
|
|
||||||
}
|
}
|
||||||
return 0;
|
return DefWindowProc(child_content_, uMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_ACTIVATE:
|
case WM_ACTIVATE: {
|
||||||
if (child_content_ != nullptr) {
|
if (child_content_ != nullptr) {
|
||||||
SetFocus(child_content_);
|
SetFocus(child_content_);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
default:
|
||||||
}
|
return DefWindowProc(window_handle_, uMsg, wParam, lParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Win32Window::Destroy() {
|
void Win32Window::Destroy() {
|
||||||
OnDestroy();
|
OnDestroy();
|
||||||
@@ -225,8 +225,7 @@ void Win32Window::SetChildContent(HWND content) {
|
|||||||
SetParent(content, window_handle_);
|
SetParent(content, window_handle_);
|
||||||
RECT frame = GetClientArea();
|
RECT frame = GetClientArea();
|
||||||
|
|
||||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,frame.bottom - frame.top, true);
|
||||||
frame.bottom - frame.top, true);
|
|
||||||
|
|
||||||
SetFocus(child_content_);
|
SetFocus(child_content_);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user