mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
Release 9.2.0
This commit is contained in:
@@ -14,7 +14,6 @@ class BackendController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final TextEditingController gameServerAddress;
|
||||
late final FocusNode gameServerAddressFocusNode;
|
||||
late final RxnString gameServerOwner;
|
||||
late final RxBool started;
|
||||
late final RxBool detached;
|
||||
StreamSubscription? worker;
|
||||
@@ -22,7 +21,7 @@ class BackendController extends GetxController {
|
||||
HttpServer? remoteServer;
|
||||
|
||||
BackendController() {
|
||||
storage = appWithNoStorage ? null : GetStorage("backend");
|
||||
storage = appWithNoStorage ? null : GetStorage("backend_storage");
|
||||
started = RxBool(false);
|
||||
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
|
||||
type.listen((value) {
|
||||
@@ -64,8 +63,10 @@ class BackendController extends GetxController {
|
||||
}
|
||||
});
|
||||
gameServerAddressFocusNode = FocusNode();
|
||||
gameServerOwner = RxnString(storage?.read("game_server_owner"));
|
||||
gameServerOwner.listen((value) => storage?.write("game_server_owner", value));
|
||||
}
|
||||
|
||||
void joinLocalhost() {
|
||||
gameServerAddress.text = kDefaultGameServerHost;
|
||||
}
|
||||
|
||||
void reset() async {
|
||||
@@ -147,12 +148,11 @@ class BackendController extends GetxController {
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
final process = await startEmbeddedBackend(detached.value);
|
||||
final processPid = process.pid;
|
||||
watchProcess(processPid).then((value) {
|
||||
if(started()) {
|
||||
started.value = false;
|
||||
}
|
||||
});
|
||||
watchProcess(process.pid)
|
||||
.asStream()
|
||||
.asBroadcastStream()
|
||||
.where((_) => !started())
|
||||
.map((_) => ServerResult(ServerResultType.processError));
|
||||
break;
|
||||
case ServerType.remote:
|
||||
yield ServerResult(ServerResultType.pingingRemote);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
class BuildController extends GetxController {
|
||||
List<FortniteBuild>? _builds;
|
||||
Rxn<FortniteBuild> _selectedBuild;
|
||||
|
||||
BuildController() : _selectedBuild = Rxn();
|
||||
|
||||
List<FortniteBuild>? get builds => _builds;
|
||||
|
||||
FortniteBuild? get selectedBuild => _selectedBuild.value;
|
||||
|
||||
set selectedBuild(FortniteBuild? value) {
|
||||
_selectedBuild.value = value;
|
||||
}
|
||||
|
||||
set builds(List<FortniteBuild>? builds) {
|
||||
_builds = builds;
|
||||
_selectedBuild.value = builds?.firstOrNull;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class GameController extends GetxController {
|
||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||
|
||||
GameController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("game");
|
||||
_storage = appWithNoStorage ? null : GetStorage("game_storage");
|
||||
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
||||
final decodedVersions = decodedVersionsJson
|
||||
.map((entry) => FortniteVersion.fromJson(entry))
|
||||
@@ -33,8 +33,7 @@ class GameController extends GetxController {
|
||||
versions = Rx(decodedVersions);
|
||||
versions.listen((data) => _saveVersions());
|
||||
final decodedSelectedVersionName = _storage?.read("version");
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((
|
||||
element) => element.name == decodedSelectedVersionName);
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
username = TextEditingController(
|
||||
text: _storage?.read("username") ?? kDefaultPlayerName);
|
||||
@@ -88,7 +87,7 @@ class GameController extends GetxController {
|
||||
}
|
||||
|
||||
FortniteVersion? getVersionByName(String name) {
|
||||
return versions.value.firstWhereOrNull((element) => element.name == name);
|
||||
return versions.value.firstWhereOrNull((element) => element.content.toString() == name);
|
||||
}
|
||||
|
||||
void addVersion(FortniteVersion version) {
|
||||
@@ -99,15 +98,9 @@ class GameController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
FortniteVersion removeVersionByName(String versionName) {
|
||||
var version = versions.value.firstWhere((element) => element.name == versionName);
|
||||
removeVersion(version);
|
||||
return version;
|
||||
}
|
||||
|
||||
void removeVersion(FortniteVersion version) {
|
||||
versions.update((val) => val?.remove(version));
|
||||
if (selectedVersion?.name == version.name || hasNoVersions) {
|
||||
if (selectedVersion == version || hasNoVersions) {
|
||||
selectedVersion = null;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +118,7 @@ class GameController extends GetxController {
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
_selectedVersion.value = version;
|
||||
_storage?.write("version", version?.name);
|
||||
_storage?.write("version", version?.content.toString());
|
||||
}
|
||||
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class HostingController extends GetxController {
|
||||
late final GetStorage? _storage;
|
||||
late final String uuid;
|
||||
late final TextEditingController name;
|
||||
late final FocusNode nameFocusNode;
|
||||
late final TextEditingController description;
|
||||
late final FocusNode descriptionFocusNode;
|
||||
late final TextEditingController password;
|
||||
late final FocusNode passwordFocusNode;
|
||||
late final RxBool showPassword;
|
||||
late final RxBool discoverable;
|
||||
late final Rx<GameServerType> type;
|
||||
@@ -19,10 +27,11 @@ class HostingController extends GetxController {
|
||||
late final RxBool started;
|
||||
late final RxBool published;
|
||||
late final Rxn<GameInstance> instance;
|
||||
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||
late final Rxn<Set<FortniteServer>> servers;
|
||||
late final Semaphore _semaphore;
|
||||
|
||||
HostingController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("hosting");
|
||||
_storage = appWithNoStorage ? null : GetStorage("hosting_storage");
|
||||
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
||||
_storage?.write("uuid", uuid);
|
||||
name = TextEditingController(text: _storage?.read("name"));
|
||||
@@ -31,6 +40,9 @@ class HostingController extends GetxController {
|
||||
description.addListener(() => _storage?.write("description", description.text));
|
||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||
password.addListener(() => _storage?.write("password", password.text));
|
||||
nameFocusNode = FocusNode();
|
||||
descriptionFocusNode = FocusNode();
|
||||
passwordFocusNode = FocusNode();
|
||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
||||
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
|
||||
@@ -43,16 +55,80 @@ class HostingController extends GetxController {
|
||||
instance = Rxn();
|
||||
final supabase = Supabase.instance.client;
|
||||
servers = Rxn();
|
||||
supabase.from("hosting")
|
||||
supabase.from("hosting_v2")
|
||||
.stream(primaryKey: ['id'])
|
||||
.map((event) => _parseValidServers(event))
|
||||
.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);
|
||||
published.value = event.any((element) => element.id == uuid);
|
||||
});
|
||||
_semaphore = Semaphore();
|
||||
}
|
||||
|
||||
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet();
|
||||
Future<void> publishServer(String author, String version) async {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
log("[SERVER] Publishing server...");
|
||||
if(published.value) {
|
||||
log("[SERVER] Already published");
|
||||
return;
|
||||
}
|
||||
|
||||
final passwordText = password.text;
|
||||
final hasPassword = passwordText.isNotEmpty;
|
||||
var ip = await Ipify.ipv4();
|
||||
if(hasPassword) {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting_v2");
|
||||
final payload = FortniteServer(
|
||||
id: uuid,
|
||||
name: name.text,
|
||||
description: description.text,
|
||||
author: author,
|
||||
ip: ip,
|
||||
version: version,
|
||||
password: hasPassword ? hashPassword(passwordText) : null,
|
||||
timestamp: DateTime.now(),
|
||||
discoverable: discoverable.value
|
||||
).toJson();
|
||||
log("[SERVER] Payload: ${jsonEncode(payload)}");
|
||||
if(published()) {
|
||||
await hosts.update(payload)
|
||||
.eq("id", uuid);
|
||||
}else {
|
||||
await hosts.insert(payload);
|
||||
}
|
||||
|
||||
published.value = true;
|
||||
log("[SERVER] Published");
|
||||
}catch(error) {
|
||||
log("[SERVER] Cannot publish server: $error");
|
||||
published.value = false;
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardServer() async {
|
||||
try {
|
||||
_semaphore.acquire();
|
||||
log("[SERVER] Discarding server...");
|
||||
final supabase = Supabase.instance.client;
|
||||
await supabase.from("hosting_v2")
|
||||
.delete()
|
||||
.match({'id': uuid});
|
||||
servers.value?.removeWhere((element) => element.id == uuid);
|
||||
log("[SERVER] Discarded server");
|
||||
}catch(error) {
|
||||
log("[SERVER] Cannot discard server: $error");
|
||||
}finally {
|
||||
published.value = false;
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
name.text = "";
|
||||
@@ -65,9 +141,9 @@ class HostingController extends GetxController {
|
||||
autoRestart.value = true;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? findServerById(String uuid) {
|
||||
FortniteServer? findServerById(String uuid) {
|
||||
try {
|
||||
return servers.value?.firstWhere((element) => element["id"] == uuid);
|
||||
return servers.value?.firstWhere((element) => element.id == uuid);
|
||||
} on StateError catch(_) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,71 +1,333 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final GetStorage? _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController gameServerDll;
|
||||
late final TextEditingController unrealEngineConsoleDll;
|
||||
late final TextEditingController backendDll;
|
||||
late final TextEditingController memoryLeakDll;
|
||||
late final TextEditingController gameServerPort;
|
||||
late final RxBool firstRun;
|
||||
late final RxString language;
|
||||
late final Rx<ThemeMode> themeMode;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController url;
|
||||
late final RxBool customGameServer;
|
||||
late final RxBool firstRun;
|
||||
late final Map<String, Future<bool>> _operations;
|
||||
late double width;
|
||||
late double height;
|
||||
late double? offsetX;
|
||||
late double? offsetY;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future<bool>? _updater;
|
||||
|
||||
SettingsController() {
|
||||
_storage = GetStorage("settings");
|
||||
gameServerDll = _createController("game_server", "reboot.dll");
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
|
||||
backendDll = _createController("backend", "cobalt.dll");
|
||||
memoryLeakDll = _createController("memory_leak", "memory.dll");
|
||||
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
|
||||
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
firstRun = RxBool(_storage.read("first_run_new1") ?? true);
|
||||
firstRun.listen((value) => _storage.write("first_run_new1", value));
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage.write("theme", value.index));
|
||||
language = RxString(_storage.read("language") ?? currentLocale);
|
||||
language.listen((value) => _storage.write("language", value));
|
||||
_storage = appWithNoStorage ? null : GetStorage("settings_storage");
|
||||
gameServerDll = _createController("game_server", InjectableDll.reboot);
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
||||
backendDll = _createController("backend", InjectableDll.cobalt);
|
||||
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
|
||||
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
||||
width = _storage?.read("width") ?? kDefaultWindowWidth;
|
||||
height = _storage?.read("height") ?? kDefaultWindowHeight;
|
||||
offsetX = _storage?.read("offset_x");
|
||||
offsetY = _storage?.read("offset_y");
|
||||
themeMode = Rx(ThemeMode.values.elementAt(_storage?.read("theme") ?? 0));
|
||||
themeMode.listen((value) => _storage?.write("theme", value.index));
|
||||
language = RxString(_storage?.read("language") ?? currentLocale);
|
||||
language.listen((value) => _storage?.write("language", value));
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
final timerIndex = _storage?.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage?.write("timer", value.index));
|
||||
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
|
||||
url.addListener(() => _storage?.write("update_url", url.text));
|
||||
status = Rx(UpdateStatus.waiting);
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
|
||||
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
|
||||
_operations = {};
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, String name) {
|
||||
var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name));
|
||||
controller.addListener(() => _storage.write(key, controller.text));
|
||||
TextEditingController _createController(String key, InjectableDll dll) {
|
||||
final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll));
|
||||
controller.addListener(() => _storage?.write(key, controller.text));
|
||||
return controller;
|
||||
}
|
||||
|
||||
void saveWindowSize(Size size) {
|
||||
_storage.write("width", size.width);
|
||||
_storage.write("height", size.height);
|
||||
_storage?.write("width", size.width);
|
||||
_storage?.write("height", size.height);
|
||||
}
|
||||
|
||||
void saveWindowOffset(Offset position) {
|
||||
offsetX = position.dx;
|
||||
offsetY = position.dy;
|
||||
_storage.write("offset_x", offsetX);
|
||||
_storage.write("offset_y", offsetY);
|
||||
_storage?.write("offset_x", offsetX);
|
||||
_storage?.write("offset_y", offsetY);
|
||||
}
|
||||
|
||||
void reset(){
|
||||
gameServerDll.text = _controllerDefaultPath("reboot.dll");
|
||||
unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll");
|
||||
backendDll.text = _controllerDefaultPath("cobalt.dll");
|
||||
gameServerDll.text = _getDefaultPath(InjectableDll.reboot);
|
||||
unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console);
|
||||
backendDll.text = _getDefaultPath(InjectableDll.cobalt);
|
||||
memoryLeakDll.text = _getDefaultPath(InjectableDll.memory);
|
||||
gameServerPort.text = kDefaultGameServerPort;
|
||||
firstRun.value = true;
|
||||
timestamp.value = null;
|
||||
timer.value = UpdateTimer.never;
|
||||
url.text = kRebootDownloadUrl;
|
||||
status.value = UpdateStatus.waiting;
|
||||
customGameServer.value = false;
|
||||
updateReboot();
|
||||
}
|
||||
|
||||
String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
if(appVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspec = await _getPubspecYaml();
|
||||
if(pubspec == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final latestVersion = Version.parse(pubspec["version"]);
|
||||
if(latestVersion <= appVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
late InfoBarEntry infoBar;
|
||||
infoBar = showRebootInfoBar(
|
||||
translations.updateAvailable(latestVersion.toString()),
|
||||
duration: null,
|
||||
severity: InfoBarSeverity.warning,
|
||||
action: Button(
|
||||
child: Text(translations.updateAvailableAction),
|
||||
onPressed: () {
|
||||
infoBar.close();
|
||||
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> _getPubspecYaml() async {
|
||||
try {
|
||||
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||
if(pubspecResponse.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadYaml(pubspecResponse.body);
|
||||
}catch(error) {
|
||||
log("[UPDATER] Cannot check for updates: $error");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateReboot({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater!;
|
||||
}
|
||||
|
||||
final result = _updateReboot(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<bool> _updateReboot(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
return true;
|
||||
}
|
||||
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
hours: timer.value.hours,
|
||||
force: force
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!silent) {
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
if(!silent) {
|
||||
infoBarEntry = showRebootInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}catch(message) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showRebootInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(
|
||||
force: true,
|
||||
silent: silent
|
||||
),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
(File, bool) getInjectableData(InjectableDll dll) {
|
||||
final defaultPath = canonicalize(_getDefaultPath(dll));
|
||||
switch(dll){
|
||||
case InjectableDll.reboot:
|
||||
if(customGameServer.value) {
|
||||
final file = File(gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return (file, true);
|
||||
}
|
||||
}
|
||||
|
||||
return (rebootDllFile, false);
|
||||
case InjectableDll.console:
|
||||
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
||||
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
||||
case InjectableDll.cobalt:
|
||||
final backendFile = File(backendDll.text);
|
||||
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
||||
case InjectableDll.memory:
|
||||
final memoryLeakFile = File(memoryLeakDll.text);
|
||||
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
|
||||
}
|
||||
}
|
||||
|
||||
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
|
||||
|
||||
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
|
||||
log("[DLL] Asking for $filePath(silent: $silent)");
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
log("[DLL] Download task already exists");
|
||||
return old;
|
||||
}
|
||||
|
||||
log("[DLL] Creating new download task...");
|
||||
final newRun = _downloadCriticalDllInteractive(filePath, silent);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
|
||||
final fileName = basename(filePath).toLowerCase();
|
||||
log("[DLL] File name: $fileName");
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName == "reboot.dll") {
|
||||
log("[DLL] Downloading reboot.dll...");
|
||||
return await updateReboot(
|
||||
silent: silent
|
||||
);
|
||||
}
|
||||
|
||||
if(File(filePath).existsSync()) {
|
||||
log("[DLL] File already exists");
|
||||
return true;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
entry = showRebootInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
entry = await showRebootInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}catch(message) {
|
||||
log("[DLL] Error: $message");
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showRebootInfoBar(
|
||||
translations.downloadDllError(fileName, error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await downloadCriticalDllInteractive(filePath);
|
||||
completer.complete(null);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
return false;
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
int get hours {
|
||||
switch(this) {
|
||||
case UpdateTimer.never:
|
||||
return -1;
|
||||
case UpdateTimer.hour:
|
||||
return 1;
|
||||
case UpdateTimer.day:
|
||||
return 24;
|
||||
case UpdateTimer.week:
|
||||
return 24 * 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class UpdateController {
|
||||
late final GetStorage? _storage;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
late final TextEditingController url;
|
||||
late final RxBool customGameServer;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
Future? _updater;
|
||||
|
||||
UpdateController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage("update");
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
var timerIndex = _storage?.read("timer");
|
||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||
timer.listen((value) => _storage?.write("timer", value.index));
|
||||
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
|
||||
url.addListener(() => _storage?.write("update_url", url.text));
|
||||
status = Rx(UpdateStatus.waiting);
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
}
|
||||
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
if(appVersion == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||
if(pubspecResponse.statusCode != 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pubspec = loadYaml(pubspecResponse.body);
|
||||
final latestVersion = Version.parse(pubspec["version"]);
|
||||
if(latestVersion <= appVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
late InfoBarEntry infoBar;
|
||||
infoBar = showInfoBar(
|
||||
translations.updateAvailable(latestVersion.toString()),
|
||||
duration: null,
|
||||
severity: InfoBarSeverity.warning,
|
||||
action: Button(
|
||||
child: Text(translations.updateAvailableAction),
|
||||
onPressed: () {
|
||||
infoBar.close();
|
||||
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateReboot({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater;
|
||||
}
|
||||
|
||||
final result = _updateReboot(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<void> _updateReboot(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
final needsUpdate = await hasRebootDllUpdate(
|
||||
timestamp.value,
|
||||
hours: timer.value.hours,
|
||||
force: force
|
||||
);
|
||||
if(!needsUpdate) {
|
||||
status.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error =
|
||||
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(
|
||||
force: true,
|
||||
silent: silent
|
||||
),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
}
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
timestamp.value = null;
|
||||
timer.value = UpdateTimer.never;
|
||||
url.text = kRebootDownloadUrl;
|
||||
status.value = UpdateStatus.waiting;
|
||||
customGameServer.value = false;
|
||||
updateReboot();
|
||||
}
|
||||
}
|
||||
|
||||
extension _UpdateTimerExtension on UpdateTimer {
|
||||
int get hours {
|
||||
switch(this) {
|
||||
case UpdateTimer.never:
|
||||
return -1;
|
||||
case UpdateTimer.hour:
|
||||
return 1;
|
||||
case UpdateTimer.day:
|
||||
return 24;
|
||||
case UpdateTimer.week:
|
||||
return 24 * 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton {
|
||||
return Button(
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _secondaryButton {
|
||||
return Button(
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import 'dialog_button.dart';
|
||||
|
||||
bool inDialog = false;
|
||||
|
||||
Future<T?> showAppDialog<T extends Object?>({required WidgetBuilder builder}) async {
|
||||
Future<T?> showRebootDialog<T extends Object?>({required WidgetBuilder builder, bool dismissWithEsc = true}) async {
|
||||
inDialog = true;
|
||||
pagesController.add(null);
|
||||
try {
|
||||
return await fluent.showDialog(
|
||||
context: appKey.currentContext!,
|
||||
context: appNavigatorKey.currentContext!,
|
||||
useRootNavigator: false,
|
||||
dismissWithEsc: dismissWithEsc,
|
||||
builder: builder
|
||||
);
|
||||
}finally {
|
||||
@@ -58,7 +57,7 @@ class FormDialog extends AbstractDialog {
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
final parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
return GenericDialog(
|
||||
header: content,
|
||||
buttons: parsed
|
||||
@@ -117,8 +116,9 @@ class InfoDialog extends AbstractDialog {
|
||||
class ProgressDialog extends AbstractDialog {
|
||||
final String text;
|
||||
final Function()? onStop;
|
||||
final bool showButton;
|
||||
|
||||
const ProgressDialog({required this.text, this.onStop, Key? key}) : super(key: key);
|
||||
const ProgressDialog({required this.text, this.onStop, this.showButton = true, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -132,11 +132,12 @@ class ProgressDialog extends AbstractDialog {
|
||||
),
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
if(showButton)
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -239,7 +240,7 @@ class ErrorDialog extends AbstractDialog {
|
||||
type: type,
|
||||
onTap: () async {
|
||||
FlutterClipboard.controlC("$error\n$stackTrace");
|
||||
showInfoBar(translations.copyErrorDialogSuccess);
|
||||
showRebootInfoBar(translations.copyErrorDialogSuccess);
|
||||
onClick();
|
||||
},
|
||||
);
|
||||
@@ -262,4 +263,62 @@ class ErrorDialog extends AbstractDialog {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
final Color? color;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
this.color,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton => Button(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
||||
),
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
|
||||
Widget get _secondaryButton => Button(
|
||||
style: widget.color != null ? ButtonStyle(
|
||||
backgroundColor: ButtonState.all(widget.color!)
|
||||
) : null,
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4);
|
||||
const infoBarShortDuration = Duration(seconds: 2);
|
||||
const _height = 64.0;
|
||||
|
||||
InfoBarEntry showInfoBar(dynamic text, {
|
||||
InfoBarEntry showRebootInfoBar(dynamic text, {
|
||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
@@ -21,33 +21,40 @@ InfoBarEntry showInfoBar(dynamic text, {
|
||||
return overlayEntry;
|
||||
}
|
||||
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => SizedBox(
|
||||
width: double.infinity,
|
||||
height: _height,
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: _height
|
||||
),
|
||||
child: Mica(
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
),
|
||||
elevation: 1,
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 2.0,
|
||||
right: 6.0
|
||||
),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
170
gui/lib/src/messenger/abstract/overlay.dart
Normal file
170
gui/lib/src/messenger/abstract/overlay.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
|
||||
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
||||
|
||||
class OverlayTarget extends StatefulWidget {
|
||||
final Widget child;
|
||||
const OverlayTarget({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<OverlayTarget> createState() => OverlayTargetState();
|
||||
|
||||
OverlayTargetState of(BuildContext context) => context.findAncestorStateOfType<OverlayTargetState>()!;
|
||||
}
|
||||
|
||||
class OverlayTargetState extends State<OverlayTarget> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
|
||||
void showOverlay({
|
||||
required String text,
|
||||
required WidgetBuilder actionBuilder,
|
||||
Offset offset = Offset.zero,
|
||||
bool ignoreTargetPointers = true,
|
||||
AttachMode attachMode = AttachMode.start
|
||||
}) {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final color = FluentTheme.of(context).acrylicBackgroundColor;
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: _AbsorbPointer(
|
||||
exclusion: ignoreTargetPointers ? null : renderBox
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
left: position.dx - (attachMode != AttachMode.start ? renderBox.size.width : 0) + offset.dx,
|
||||
top: position.dy + (renderBox.size.height / 2) + offset.dy,
|
||||
child: CustomPaint(
|
||||
painter: _CallOutShape(color, attachMode != AttachMode.start),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(text),
|
||||
const SizedBox(height: 12.0),
|
||||
actionBuilder(context, () => entry.remove())
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
appOverlayKey.currentState?.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachMode {
|
||||
start,
|
||||
middle,
|
||||
end;
|
||||
}
|
||||
|
||||
// Harder than one would think
|
||||
class _CallOutShape extends CustomPainter {
|
||||
final Color color;
|
||||
final bool end;
|
||||
_CallOutShape(this.color, this.end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final fillPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final borderPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.25
|
||||
..color = Colors.white;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(10, 0);
|
||||
if(!end) {
|
||||
path.lineTo(12.5, 0);
|
||||
path.lineTo(20, -12.5);
|
||||
path.lineTo(27.5, 0);
|
||||
}else {
|
||||
path.lineTo(size.width - 27.5, 0);
|
||||
path.lineTo(size.width - 20, -12.5);
|
||||
path.lineTo(size.width - 12.5, 0);
|
||||
}
|
||||
|
||||
path.lineTo(size.width - 10, 0);
|
||||
path.arcToPoint(Offset(size.width, 10), radius: Radius.circular(10));
|
||||
path.lineTo(size.width, size.height - 10);
|
||||
path.arcToPoint(Offset(size.width - 10, size.height), radius: Radius.circular(10));
|
||||
path.lineTo(10, size.height);
|
||||
path.arcToPoint(Offset(0, size.height - 10), radius: Radius.circular(10));
|
||||
path.lineTo(0, 10);
|
||||
path.arcToPoint(Offset(10, 0), radius: Radius.circular(10));
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, borderPaint);
|
||||
canvas.drawShadow(path, color, 1, true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _AbsorbPointer extends SingleChildRenderObjectWidget {
|
||||
final RenderBox? exclusion;
|
||||
const _AbsorbPointer({
|
||||
required this.exclusion
|
||||
});
|
||||
|
||||
@override
|
||||
_RenderAbsorbPointer createRenderObject(BuildContext context) => _RenderAbsorbPointer(
|
||||
exclusion: exclusion
|
||||
);
|
||||
}
|
||||
|
||||
class _RenderAbsorbPointer extends RenderProxyBox {
|
||||
final RenderBox? exclusion;
|
||||
_RenderAbsorbPointer({
|
||||
required this.exclusion,
|
||||
RenderBox? child
|
||||
}) : super(child);
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, { required Offset position }) {
|
||||
final exclusion = this.exclusion;
|
||||
if(exclusion == null) {
|
||||
return size.contains(position);
|
||||
}
|
||||
|
||||
// 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)
|
||||
position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight);
|
||||
final exclusionPosition = exclusion.localToGlobal(Offset.zero);
|
||||
final exclusionSize = Rect.fromLTRB(
|
||||
exclusionPosition.dx,
|
||||
exclusionPosition.dy,
|
||||
exclusionPosition.dx + exclusion.size.width,
|
||||
exclusionPosition.dy + exclusion.size.height
|
||||
);
|
||||
return !exclusionSize.contains(position);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) => super.visitChildrenForSemantics(visitor);
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.isBlockingUserActions = true;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showResetDialog(Function() onConfirm) => showAppDialog(
|
||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.resetDefaultsDialogTitle,
|
||||
buttons: [
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showAppDialog(
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.dllDeletedTitle,
|
||||
buttons: [
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import '../../util/log.dart';
|
||||
|
||||
|
||||
String? lastError;
|
||||
|
||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
@@ -21,12 +18,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
}
|
||||
|
||||
lastError = exception.toString();
|
||||
var route = ModalRoute.of(pageKey.currentContext!);
|
||||
final route = ModalRoute.of(pageKey.currentContext!);
|
||||
if(route != null && !route.isCurrent){
|
||||
Navigator.of(pageKey.currentContext!).pop(false);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showAppDialog(
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: exception,
|
||||
346
gui/lib/src/messenger/implementation/onboard.dart
Normal file
346
gui/lib/src/messenger/implementation/onboard.dart
Normal file
@@ -0,0 +1,346 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
void startOnboarding() {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
settingsController.firstRun.value = false;
|
||||
profileOverlayKey.currentState!.showOverlay(
|
||||
text: translations.startOnboardingText,
|
||||
offset: Offset(27.5, 17.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.startOnboardingActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
await showProfileForm(context);
|
||||
_promptPlayPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayPage() {
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptPlayPageActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
_promptPlayVersion();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayVersion() {
|
||||
final gameController = Get.find<GameController>();
|
||||
final hasBuilds = gameController.versions.value.isNotEmpty;
|
||||
gameVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayVersionText,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: hasBuilds ? translations.promptPlayVersionActionLabelHasBuilds : translations.promptPlayVersionActionLabelNoBuilds,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
if(!hasBuilds) {
|
||||
await VersionSelector.openDownloadDialog(closable: false);
|
||||
}
|
||||
_promptServerBrowserPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptServerBrowserPage() {
|
||||
pageIndex.value = RebootPageType.browser.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptServerBrowserPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptServerBrowserPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostPage() {
|
||||
pageIndex.value = RebootPageType.host.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInfo();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptHostInfo() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostInfoOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInfoText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => Row(
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelSkip,
|
||||
themed: false,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = false;
|
||||
_promptHostVersion();
|
||||
}
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelConfigure,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = true;
|
||||
hostInfoTileKey.currentState!.openNestedPage();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformation() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.nameFocusNode.requestFocus();
|
||||
hostInfoNameOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationDescription();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationDescription() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.descriptionFocusNode.requestFocus();
|
||||
hostInfoDescriptionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationDescriptionText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(70, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationDescriptionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationPassword();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationPassword() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.passwordFocusNode.requestFocus();
|
||||
hostInfoPasswordOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationPasswordText,
|
||||
ignoreTargetPointers: false,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationPasswordActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
Navigator.of(hostInfoTileKey.currentContext!).pop();
|
||||
pageStack.removeLast();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostVersion() {
|
||||
hostVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostVersionText,
|
||||
attachMode: AttachMode.end,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostVersionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostShare();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostShare() {
|
||||
final backendController = Get.find<BackendController>();
|
||||
hostShareOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostShareText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostShareActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
backendController.type.value = ServerType.embedded;
|
||||
_promptBackendPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptBackendPage() {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendTypePage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendTypePage() {
|
||||
backendTypeOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendTypePageText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendTypePageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendGameServerAddress();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendGameServerAddress() {
|
||||
backendGameServerAddressOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendGameServerAddressText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendGameServerAddressActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendUnrealEngineKey();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendUnrealEngineKey() {
|
||||
backendUnrealEngineOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendUnrealEngineKeyText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-465, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendUnrealEngineKeyActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendDetached();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendDetached() {
|
||||
backendDetachedOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendDetachedText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-410, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendDetachedActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptInfoTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptInfoTab() {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptInfoTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptInfoTabActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptSettingsTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptSettingsTab() {
|
||||
pageIndex.value = RebootPageType.settings.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptSettingsTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptSettingsTabActionLabel,
|
||||
onTap: onClose
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
bool themed = true,
|
||||
required void Function() onTap,
|
||||
}) => Button(
|
||||
style: themed ? ButtonStyle(
|
||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
||||
) : null,
|
||||
child: Text(label),
|
||||
onPressed: onTap
|
||||
);
|
||||
@@ -2,18 +2,17 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
Future<bool> showProfileForm(BuildContext context) async{
|
||||
var showPassword = RxBool(false);
|
||||
var oldUsername = _gameController.username.text;
|
||||
var showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
var oldPassword = _gameController.password.text;
|
||||
var result = await showAppDialog<bool?>(
|
||||
final showPassword = RxBool(false);
|
||||
final oldUsername = _gameController.username.text;
|
||||
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
final oldPassword = _gameController.password.text;
|
||||
final result = await showRebootDialog<bool?>(
|
||||
builder: (context) => Obx(() => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1,24 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:dart_ipify/dart_ipify.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/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/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:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final Semaphore _publishingSemaphore = Semaphore();
|
||||
|
||||
extension ServerControllerDialog on BackendController {
|
||||
Future<bool> toggleInteractive() async {
|
||||
@@ -39,108 +35,108 @@ extension ServerControllerDialog on BackendController {
|
||||
}
|
||||
|
||||
InfoBarEntry _handeEvent(ServerResult event) {
|
||||
switch (event.type) {
|
||||
log("[BACKEND] Handling event: $event");
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.startingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.startSuccess:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.startError:
|
||||
print(event.stackTrace);
|
||||
return showInfoBar(
|
||||
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 showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.stopSuccess:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.stoppedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.stopError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.stopServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.missingHostError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.missingHostNameError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.missingPortError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.missingPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.illegalPortError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.illegalPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.freeingPort:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.freeingPort,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.freePortSuccess:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.freedPort,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
case ServerResultType.freePortError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.pingingRemote:
|
||||
return showInfoBar(
|
||||
translations.pingingRemoteServer,
|
||||
return showRebootInfoBar(
|
||||
translations.pingingServer(ServerType.remote.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingingLocal:
|
||||
return showInfoBar(
|
||||
translations.pingingLocalServer(type.value.name),
|
||||
return showRebootInfoBar(
|
||||
translations.pingingServer(type.value.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingError:
|
||||
return showInfoBar(
|
||||
return showRebootInfoBar(
|
||||
translations.pingError(type.value.name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.processError:
|
||||
return showRebootInfoBar(
|
||||
translations.backendProcessError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void joinLocalHost() {
|
||||
gameServerAddress.text = kDefaultGameServerHost;
|
||||
gameServerOwner.value = null;
|
||||
}
|
||||
|
||||
Future<void> joinServer(String uuid, Map<String, dynamic> entry) async {
|
||||
final id = entry["id"];
|
||||
if(uuid == id) {
|
||||
showInfoBar(
|
||||
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
|
||||
if(!kDebugMode && uuid == server.id) {
|
||||
showRebootInfoBar(
|
||||
translations.joinSelfServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -148,18 +144,29 @@ extension ServerControllerDialog on BackendController {
|
||||
return;
|
||||
}
|
||||
|
||||
final hashedPassword = entry["password"];
|
||||
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 = entry["author"];
|
||||
final encryptedIp = entry["ip"];
|
||||
final author = server.author;
|
||||
final encryptedIp = server.ip;
|
||||
if(!hasPassword) {
|
||||
final valid = await _isServerValid(encryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(embedded, encryptedIp, author);
|
||||
_onSuccess(gameController, embedded, encryptedIp, author, version);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,7 +176,7 @@ extension ServerControllerDialog on BackendController {
|
||||
}
|
||||
|
||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.wrongServerPassword,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -183,7 +190,7 @@ extension ServerControllerDialog on BackendController {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(embedded, decryptedIp, author);
|
||||
_onSuccess(gameController, embedded, decryptedIp, author, version);
|
||||
}
|
||||
|
||||
Future<bool> _isServerValid(String address) async {
|
||||
@@ -192,7 +199,7 @@ extension ServerControllerDialog on BackendController {
|
||||
return true;
|
||||
}
|
||||
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.offlineServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -204,7 +211,7 @@ extension ServerControllerDialog on BackendController {
|
||||
final confirmPasswordController = TextEditingController();
|
||||
final showPassword = RxBool(false);
|
||||
final showPasswordTrailing = RxBool(false);
|
||||
return await showAppDialog<String?>(
|
||||
return await showRebootDialog<String?>(
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -253,80 +260,18 @@ extension ServerControllerDialog on BackendController {
|
||||
);
|
||||
}
|
||||
|
||||
void _onSuccess(bool embedded, String decryptedIp, String author) {
|
||||
void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
||||
if(embedded) {
|
||||
gameServerAddress.text = decryptedIp;
|
||||
gameServerOwner.value = author;
|
||||
pageIndex.value = 0;
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
}else {
|
||||
FlutterClipboard.controlC(decryptedIp);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
controller.selectedVersion = version;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.success
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
extension HostingControllerExtension on HostingController {
|
||||
Future<void> publishServer(String author, String version) async {
|
||||
try {
|
||||
_publishingSemaphore.acquire();
|
||||
if(published.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final passwordText = password.text;
|
||||
final hasPassword = passwordText.isNotEmpty;
|
||||
var ip = await Ipify.ipv4();
|
||||
if(hasPassword) {
|
||||
ip = aes256Encrypt(ip, passwordText);
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
final hosts = supabase.from("hosting");
|
||||
final payload = {
|
||||
'name': name.text,
|
||||
'description': description.text,
|
||||
'author': author,
|
||||
'ip': ip,
|
||||
'version': version,
|
||||
'password': hasPassword ? hashPassword(passwordText) : null,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'discoverable': discoverable.value
|
||||
};
|
||||
if(published()) {
|
||||
await hosts.update(payload).eq("id", uuid);
|
||||
}else {
|
||||
payload["id"] = uuid;
|
||||
await hosts.insert(payload);
|
||||
}
|
||||
|
||||
published.value = true;
|
||||
}catch(error) {
|
||||
published.value = false;
|
||||
}finally {
|
||||
_publishingSemaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> discardServer() async {
|
||||
try {
|
||||
_publishingSemaphore.acquire();
|
||||
if(!published.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
await supabase.from("hosting")
|
||||
.delete()
|
||||
.match({'id': uuid});
|
||||
published.value = false;
|
||||
}catch(_) {
|
||||
published.value = true;
|
||||
}finally {
|
||||
_publishingSemaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
463
gui/lib/src/messenger/implementation/version.dart
Normal file
463
gui/lib/src/messenger/implementation/version.dart
Normal file
@@ -0,0 +1,463 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddVersionDialog extends StatefulWidget {
|
||||
final bool closable;
|
||||
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddVersionDialog> createState() => _AddVersionDialogState();
|
||||
}
|
||||
|
||||
class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
|
||||
|
||||
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
|
||||
final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive);
|
||||
final Rxn<FortniteBuild> _build = Rxn();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future<List<FortniteBuild>> _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
Isolate? _isolate;
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = compute(fetchBuilds, null);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
_isolate?.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case _DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
|
||||
builder: (context, snapshot) {
|
||||
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.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case _DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
||||
);
|
||||
case _DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
if(widget.closable)
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
|
||||
type: widget.closable ? ButtonType.primary : ButtonType.only,
|
||||
color: FluentTheme.of(context).accentColor,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final topResult = _formKey.currentState?.validate();
|
||||
if(topResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fieldResult = _formFieldKey.currentState?.validate();
|
||||
if(fieldResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final source = _source.value;
|
||||
if(source == _BuildSource.local) {
|
||||
Navigator.of(context).pop();
|
||||
_addFortniteVersion(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
_isolate = await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete(FortniteBuild build) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_addFortniteVersion(build);
|
||||
}
|
||||
|
||||
void _addFortniteVersion(FortniteBuild build) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
content: build.version,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSourceSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
_buildBuildSelector(builds),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
String? _checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
final directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
|
||||
label: translations.build,
|
||||
child: FormField<FortniteBuild?>(
|
||||
key: _formFieldKey,
|
||||
validator: (data) => _checkBuild(data),
|
||||
builder: (formContext) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(),
|
||||
value: _build.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_build.value = value;
|
||||
formContext.didChange(value);
|
||||
formContext.validate();
|
||||
_updateFormDefaults();
|
||||
}
|
||||
),
|
||||
if(formContext.hasError)
|
||||
const SizedBox(height: 4.0),
|
||||
if(formContext.hasError)
|
||||
Text(
|
||||
formContext.errorText ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: formContext.hasError ? 8.0 : 16.0
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
String? _checkBuild(FortniteBuild? data) {
|
||||
if(data == null) {
|
||||
return translations.selectBuild;
|
||||
}
|
||||
|
||||
final versions = _gameController.versions.value;
|
||||
if (versions.any((element) => data.version == element.content)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
Widget _buildSourceSelector() => InfoLabel(
|
||||
label: translations.source,
|
||||
child: ComboBox<_BuildSource>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(),
|
||||
value: _source.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_source.value = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>(
|
||||
value: element,
|
||||
child: Text(element.translatedName)
|
||||
);
|
||||
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: translations.stopLoadingDialogAction,
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_source.value != _BuildSource.local && _build.value?.available != true) {
|
||||
_build.value = null;
|
||||
}
|
||||
|
||||
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) {
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
}
|
||||
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum _DownloadStatus {
|
||||
form,
|
||||
downloading,
|
||||
extracting,
|
||||
error,
|
||||
done
|
||||
}
|
||||
|
||||
enum _BuildSource {
|
||||
local,
|
||||
githubArchive;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _BuildSource.local:
|
||||
return translations.localBuild;
|
||||
case _BuildSource.githubArchive:
|
||||
return translations.githubArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ 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/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.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/keyboard.dart';
|
||||
@@ -16,7 +18,10 @@ import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/implementation/data.dart';
|
||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
||||
|
||||
class BackendPage extends RebootPage {
|
||||
const BackendPage({Key? key}) : super(key: key);
|
||||
@@ -42,7 +47,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
InfoBarEntry? _infoBarEntry;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
|
||||
@@ -60,7 +65,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_type,
|
||||
@@ -84,10 +89,13 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.matchmakerConfigurationAddressName),
|
||||
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
content: OverlayTarget(
|
||||
key: backendGameServerAddressOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -152,15 +160,18 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
OverlayTarget(
|
||||
key: backendDetachedOverlayTargetKey,
|
||||
child: ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Widget get _unrealEngineConsoleKey => Obx(() {
|
||||
if(_backendController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -173,14 +184,18 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
title: Text(translations.settingsClientConsoleKeyName),
|
||||
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
||||
contentWidth: null,
|
||||
content: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
content: OverlayTarget(
|
||||
key: backendUnrealEngineOverlayTargetKey,
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showRebootInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -221,7 +236,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.backendTypeName),
|
||||
subtitle: Text(translations.backendTypeDescription),
|
||||
content: const ServerTypeSelector()
|
||||
content: ServerTypeSelector(
|
||||
overlayKey: backendTypeOverlayTargetKey
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
359
gui/lib/src/page/implementation/browser_page.dart
Normal file
359
gui/lib/src/page/implementation/browser_page.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.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';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
final Rx<_Filter> _filter = Rx(_Filter.all);
|
||||
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
final data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable)
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<FortniteServer> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildFilter(context),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
_buildSort(context),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildSort(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.arrow_sort_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Sort by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 230,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_sort.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Sort.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _sort.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
|
||||
Row _buildFilter(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.filter_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Filter by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 125,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_filter.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Filter.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _filter.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopulatedListBody(Set<FortniteServer> items) => Obx(() {
|
||||
final filter = _filter.value;
|
||||
final sorted = items.where((element) {
|
||||
switch(filter) {
|
||||
case _Filter.all:
|
||||
return true;
|
||||
case _Filter.accessible:
|
||||
return element.password == null;
|
||||
case _Filter.playable:
|
||||
return _gameController.getVersionByName(element.version) != null;
|
||||
}
|
||||
}).toList();
|
||||
final sort = _sort.value;
|
||||
sorted.sort((first, second) {
|
||||
switch(sort) {
|
||||
case _Sort.timeAscending:
|
||||
return first.timestamp.compareTo(second.timestamp);
|
||||
case _Sort.timeDescending:
|
||||
return second.timestamp.compareTo(first.timestamp);
|
||||
case _Sort.nameAscending:
|
||||
return first.name.compareTo(second.name);
|
||||
case _Sort.nameDescending:
|
||||
return second.name.compareTo(first.name);
|
||||
}
|
||||
});
|
||||
if(sorted.isEmpty) {
|
||||
return _noServersByQuery;
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = sorted.elementAt(index);
|
||||
final hasPassword = entry.password != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry.author}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
|
||||
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(FortniteServer entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(FortniteServer element, String filter) {
|
||||
filter = filter.toLowerCase();
|
||||
|
||||
final uri = Uri.tryParse(filter);
|
||||
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return element.id.toLowerCase().contains(filter.toLowerCase())
|
||||
|| element.name.toLowerCase().contains(filter)
|
||||
|| element.author.toLowerCase().contains(filter)
|
||||
|| element.description.toLowerCase().contains(filter);
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
final color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(FortniteServer server) {
|
||||
final result = server.name;
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(FortniteServer server) {
|
||||
final result = server.description;
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}";
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
|
||||
enum _Filter {
|
||||
all,
|
||||
accessible,
|
||||
playable;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Filter.all:
|
||||
return translations.all;
|
||||
case _Filter.accessible:
|
||||
return translations.accessible;
|
||||
case _Filter.playable:
|
||||
return translations.playable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _Sort {
|
||||
timeAscending,
|
||||
timeDescending,
|
||||
nameAscending,
|
||||
nameDescending;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Sort.timeAscending:
|
||||
return translations.timeAscending;
|
||||
case _Sort.timeDescending:
|
||||
return translations.timeDescending;
|
||||
case _Sort.nameAscending:
|
||||
return translations.nameAscending;
|
||||
case _Sort.nameDescending:
|
||||
return translations.nameDescending;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,14 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_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/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.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/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -29,9 +28,12 @@ import 'package:reboot_launcher/src/widget/profile_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/title_bar.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'info_page.dart';
|
||||
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
static const double kDefaultPadding = 12.0;
|
||||
static const double kTitleBarHeight = 32;
|
||||
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -39,16 +41,14 @@ class HomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
static const double _kDefaultPadding = 12.0;
|
||||
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -56,7 +56,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
windowManager.setPreventClose(true);
|
||||
windowManager.addListener(this);
|
||||
_syncPageViewWithNavigator();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkUpdates();
|
||||
_initAppLink();
|
||||
@@ -64,6 +66,18 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
});
|
||||
}
|
||||
|
||||
void _syncPageViewWithNavigator() {
|
||||
var lastPage = pageIndex.value;
|
||||
pageIndex.listen((index) {
|
||||
if(index == lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
}
|
||||
|
||||
void _initAppLink() async {
|
||||
final appLinks = AppLinks();
|
||||
final initialUrl = await appLinks.getInitialLink();
|
||||
@@ -78,9 +92,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
final uuid = uri.host;
|
||||
final server = _hostingController.findServerById(uuid);
|
||||
if(server != null) {
|
||||
_backendController.joinServer(_hostingController.uuid, server);
|
||||
_backendController.joinServerInteractive(_hostingController.uuid, server);
|
||||
}else {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.noServerFound,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -100,10 +114,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
return;
|
||||
}
|
||||
|
||||
var oldOwner = _backendController.gameServerOwner.value;
|
||||
_backendController.joinLocalHost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||
_backendController.joinLocalhost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
translations.serverNoLongerAvailableUnnamed,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
));
|
||||
@@ -114,27 +127,34 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
}
|
||||
|
||||
void _checkUpdates() {
|
||||
_updateController.notifyLauncherUpdate();
|
||||
_settingsController.notifyLauncherUpdate();
|
||||
|
||||
if(!dllsDirectory.existsSync()) {
|
||||
dllsDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
for(final injectable in InjectableDll.values) {
|
||||
downloadCriticalDllInteractive(
|
||||
injectable.path,
|
||||
silent: true
|
||||
);
|
||||
final (file, custom) = _settingsController.getInjectableData(injectable);
|
||||
if(!custom) {
|
||||
_settingsController.downloadCriticalDllInteractive(
|
||||
file.path,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
downloadCriticalDllInteractive(filePath);
|
||||
_settingsController.downloadCriticalDllInteractive(filePath);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
exit(0); // Force closing
|
||||
void onWindowClose() async {
|
||||
try {
|
||||
await _hostingController.discardServer();
|
||||
}finally {
|
||||
exit(0); // Force closing
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -153,7 +173,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
_focused.value = false;
|
||||
_focused.value = !_focused.value;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -218,137 +238,364 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
if(eventName != "move") {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
_settingsController.language.value;
|
||||
loadTranslations(context);
|
||||
// InfoPage.initInfoTiles();
|
||||
return Obx(() {
|
||||
return NavigationPaneTheme(
|
||||
data: NavigationPaneThemeData(
|
||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
),
|
||||
child: NavigationView(
|
||||
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||
padding: _kDefaultPadding,
|
||||
controller: pagesController,
|
||||
body: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
height: 32,
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: pageIndex.value,
|
||||
onChanged: (index) {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeOut,
|
||||
indicatorSize: 3.25
|
||||
return Container(
|
||||
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: HomePage.kTitleBarHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
_backButton,
|
||||
Expanded(child: _draggableArea),
|
||||
WindowTitleBar(focused: _focused())
|
||||
],
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appNavigatorKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Overlay(
|
||||
key: appOverlayKey,
|
||||
initialEntries: [
|
||||
OverlayEntry(
|
||||
maintainState: true,
|
||||
builder: (context) => Row(
|
||||
children: [
|
||||
_buildLateralView(),
|
||||
_buildBody()
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
contentShape: const RoundedRectangleBorder(),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 _buildBodyContent() => PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
pagesController.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: pages[index]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
);
|
||||
|
||||
Widget _buildBodyHeader() {
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(_buildBodyHeaderRootPage(inactiveColor));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
|
||||
elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan(
|
||||
text: pages[pageIndex.value].name,
|
||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < pageStack.length; i++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
|
||||
pagesController.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
);
|
||||
|
||||
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
);
|
||||
|
||||
TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan(
|
||||
text: nestedPageName,
|
||||
recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var j = 0; j < nestedPageIndex - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
pagesController.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
);
|
||||
|
||||
Widget _buildLateralView() => SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ProfileWidget(
|
||||
overlayKey: profileOverlayKey
|
||||
),
|
||||
_autoSuggestBox,
|
||||
const SizedBox(height: 12.0),
|
||||
_buildNavigationTrail()
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildNavigationTrail() => Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
primary: true,
|
||||
itemCount: pages.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) => _buildNavigationItem(pages[index]),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
Widget _buildNavigationItem(RebootPage page) {
|
||||
final index = page.type.index;
|
||||
return OverlayTarget(
|
||||
key: getOverlayTargetKeyByPage(index),
|
||||
child: HoverButton(
|
||||
onPressed: () {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
builder: (context, states) => Obx(() => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
pageIndex.value == index ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
Text(page.name)
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _backButton => StreamBuilder(
|
||||
stream: pagesController.stream,
|
||||
builder: (context, _) => Button(
|
||||
style: ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||
if(inDialog) {
|
||||
Navigator.of(appKey.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();
|
||||
}
|
||||
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),
|
||||
)
|
||||
}
|
||||
pagesController.add(null);
|
||||
},
|
||||
child: const Icon(FluentIcons.back, size: 12.0),
|
||||
)
|
||||
);
|
||||
|
||||
GestureDetector get _draggableArea => GestureDetector(
|
||||
onDoubleTap: appWindow.maximizeOrRestore,
|
||||
onHorizontalDragStart: (_) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (_) => appWindow.startDragging()
|
||||
onHorizontalDragStart: (_) => windowManager.startDragging(),
|
||||
onVerticalDragStart: (_) => windowManager.startDragging()
|
||||
);
|
||||
|
||||
Widget get _autoSuggestBox => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
Widget get _autoSuggestBox => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
enabled: !firstRun,
|
||||
placeholder: translations.find,
|
||||
focusNode: _searchFocusNode,
|
||||
selectionHeightStyle: BoxHeightStyle.max,
|
||||
itemBuilder: (context, item) => ListTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = item.value.pageIndex;
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
},
|
||||
leading: item.child,
|
||||
title: Text(
|
||||
item.value.name,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1
|
||||
)
|
||||
),
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
items: _suggestedItems,
|
||||
autofocus: true,
|
||||
trailingIcon: IgnorePointer(
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: const Icon(FluentIcons.search)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
|
||||
final pageIcon = SizedBox.square(
|
||||
@@ -367,282 +614,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
));
|
||||
return results;
|
||||
}).toList();
|
||||
|
||||
List<NavigationPaneItem> get _items => pages.map((page) => _createItem(page)).toList();
|
||||
|
||||
NavigationPaneItem _createItem(RebootPage page) => PaneItem(
|
||||
title: Text(page.name),
|
||||
icon: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
body: page
|
||||
);
|
||||
}
|
||||
|
||||
class _PaneBody extends StatefulWidget {
|
||||
const _PaneBody({
|
||||
required this.padding,
|
||||
required this.controller,
|
||||
required this.body
|
||||
});
|
||||
|
||||
final double padding;
|
||||
final StreamController<void> controller;
|
||||
final Widget? body;
|
||||
|
||||
@override
|
||||
State<_PaneBody> createState() => _PaneBodyState();
|
||||
}
|
||||
|
||||
class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var lastPage = pageIndex.value;
|
||||
pageIndex.listen((index) {
|
||||
if(index == lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.padding,
|
||||
right: widget.padding * 2,
|
||||
top: widget.padding,
|
||||
bottom: widget.padding * 2
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: widget.controller.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(TextSpan(
|
||||
text: pages[pageIndex.value].name,
|
||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < pageStack.length; i++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
|
||||
widget.controller.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
));
|
||||
elements.add(TextSpan(
|
||||
text: innerPage,
|
||||
recognizer: i == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var j = 0; j < i - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
widget.controller.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: i == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
widget.controller.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: widget.body ?? const SizedBox.shrink()
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomPane extends NavigationPaneWidget {
|
||||
final SettingsController settingsController;
|
||||
_CustomPane(this.settingsController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, NavigationPaneWidgetData data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
data.appBar,
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
data.pane.header ?? const SizedBox.shrink(),
|
||||
data.pane.autoSuggestBox ?? const SizedBox.shrink(),
|
||||
const SizedBox(height: 12.0),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: data.scrollController,
|
||||
child: ListView.separated(
|
||||
controller: data.scrollController,
|
||||
itemCount: data.pane.items.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = data.pane.items[index] as PaneItem;
|
||||
return Obx(() {
|
||||
final firstRun = settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
onPressed: firstRun ? null : () => data.pane.onChanged?.call(index),
|
||||
builder: (context, states) => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
data.pane.indicator ?? const SizedBox.shrink(),
|
||||
item.icon,
|
||||
const SizedBox(width: 12.0),
|
||||
item.title ?? const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: data.content
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _NestedPageObserver extends NavigatorObserver {
|
||||
|
||||
@@ -10,11 +10,10 @@ import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.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/messenger/abstract/overlay.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';
|
||||
@@ -23,7 +22,13 @@ import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
|
||||
|
||||
class HostPage extends RebootPage {
|
||||
const HostPage({Key? key}) : super(key: key);
|
||||
@@ -47,7 +52,6 @@ class HostPage extends RebootPage {
|
||||
class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||
@@ -67,39 +71,32 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
|
||||
@override
|
||||
Widget get button => LaunchButton(
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
);
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_information,
|
||||
versionSelectSettingTile,
|
||||
buildVersionSelector(
|
||||
key: hostVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
_internalFiles,
|
||||
_share,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _information => SettingTile(
|
||||
key: hostInfoTileKey,
|
||||
icon: Icon(
|
||||
FluentIcons.info_24_regular
|
||||
),
|
||||
title: Text(translations.hostGameServerName),
|
||||
subtitle: Text(translations.hostGameServerDescription),
|
||||
overlayKey: hostInfoOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -107,10 +104,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerNameName),
|
||||
subtitle: Text(translations.hostGameServerNameDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
onChanged: (_) => _updateServer()
|
||||
content: OverlayTarget(
|
||||
key: hostInfoNameOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
focusNode: _hostingController.nameFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -119,10 +120,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerDescriptionName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
onChanged: (_) => _updateServer()
|
||||
content: OverlayTarget(
|
||||
key: hostInfoDescriptionOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
focusNode: _hostingController.descriptionFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -131,28 +136,32 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerPasswordName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: Obx(() => TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
content: Obx(() => OverlayTarget(
|
||||
key: hostInfoPasswordOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
focusNode: _hostingController.passwordFocusNode,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
),
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
@@ -183,105 +192,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(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _updateController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateController.customGameServer.value = entry.key;
|
||||
_updateController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _updateController.url,
|
||||
validator: checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
_updateController.infoBarEntry?.close();
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _options => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
@@ -347,12 +257,121 @@ 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(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _settingsController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsController.customGameServer.value = entry.key;
|
||||
_settingsController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_settingsController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _settingsController.url,
|
||||
validator: _checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_settingsController.timer.value = entry;
|
||||
_settingsController.infoBarEntry?.close();
|
||||
_settingsController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
String? _checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyURL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
SettingTile get _share => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.link_24_regular
|
||||
),
|
||||
title: Text(translations.hostShareName),
|
||||
subtitle: Text(translations.hostShareDescription),
|
||||
overlayKey: hostShareOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -382,7 +401,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final ip = await Ipify.ipv4();
|
||||
entry.close();
|
||||
FlutterClipboard.controlC(ip);
|
||||
_showCopiedIp();
|
||||
_showCopiedIp();
|
||||
}catch(error) {
|
||||
entry?.close();
|
||||
_showCannotCopyIp(error);
|
||||
@@ -394,6 +413,18 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
Future<void> _updateServer() async {
|
||||
if(!_hostingController.published()) {
|
||||
return;
|
||||
@@ -409,29 +440,29 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showCopiedLink() => showInfoBar(
|
||||
void _showCopiedLink() => showRebootInfoBar(
|
||||
translations.hostShareLinkMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
InfoBarEntry _showCopyingIp() => showInfoBar(
|
||||
InfoBarEntry _showCopyingIp() => showRebootInfoBar(
|
||||
translations.hostShareIpMessageLoading,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
void _showCopiedIp() => showInfoBar(
|
||||
void _showCopiedIp() => showRebootInfoBar(
|
||||
translations.hostShareIpMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
void _showCannotCopyIp(Object error) => showInfoBar(
|
||||
void _showCannotCopyIp(Object error) => showRebootInfoBar(
|
||||
translations.hostShareIpMessageError(error.toString()),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
|
||||
void _showCannotUpdateGameServer(Object error) => showInfoBar(
|
||||
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar(
|
||||
translations.cannotUpdateGameServer(error.toString()),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -1,80 +1,14 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
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/dialog/abstract/info_bar.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.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/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class InfoPage extends RebootPage {
|
||||
static late List<InfoTile> _infoTiles;
|
||||
static late List<_QuizEntry> _quizEntries;
|
||||
|
||||
static Object? initInfoTiles() {
|
||||
try {
|
||||
final faqDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\faq");
|
||||
final infoTiles = SplayTreeMap<int, InfoTile>();
|
||||
for(final entry in faqDirectory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
infoTiles[index] = InfoTile(
|
||||
title: Text(questionName),
|
||||
content: Text(entry.readAsStringSync())
|
||||
);
|
||||
}
|
||||
}
|
||||
_infoTiles = infoTiles.values.toList(growable: false);
|
||||
|
||||
final questionsDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\questions");
|
||||
final questions = SplayTreeMap<int, _QuizEntry>();
|
||||
for(final entry in questionsDirectory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
questions[index] = _QuizEntry(
|
||||
question: questionName,
|
||||
options: entry.readAsStringSync().split("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
_quizEntries = questions.values.toList(growable: false);
|
||||
|
||||
return null;
|
||||
}catch(error) {
|
||||
_infoTiles = [];
|
||||
_quizEntries = [];
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -87,207 +21,59 @@ class InfoPage extends RebootPage {
|
||||
String get iconAsset => "assets/images/info.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => Get.find<SettingsController>().firstRun.value && pageName != null;
|
||||
bool hasButton(String? routeName) => false;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.info;
|
||||
}
|
||||
|
||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final Rxn<Widget> _quizPage;
|
||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
||||
static const String _kDiscordInviteUrl = "https://discord.gg/reboot";
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
_discord,
|
||||
_tutorial,
|
||||
_reportBug
|
||||
];
|
||||
|
||||
SettingTile get _reportBug => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.bug_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsBugReportName),
|
||||
subtitle: Text(translations.settingsUtilsBugReportSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kReportBugUrl),
|
||||
child: Text(translations.settingsUtilsBugReportContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _tutorial => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.chat_help_24_regular
|
||||
),
|
||||
title: Text(translations.infoVideoName),
|
||||
subtitle: Text(translations.infoVideoDescription),
|
||||
content: Button(
|
||||
onPressed: () => startOnboarding(),
|
||||
child: Text(translations.infoVideoContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _discord => SettingTile(
|
||||
icon: Icon(
|
||||
Icons.discord_outlined
|
||||
),
|
||||
title: Text(translations.infoDiscordName),
|
||||
subtitle: Text(translations.infoDiscordDescription),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kDiscordInviteUrl),
|
||||
child: Text(translations.infoDiscordContent)
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_quizPage = Rxn(_settingsController.firstRun.value ? _QuizRoute(
|
||||
entries: InfoPage._quizEntries,
|
||||
onSuccess: () => _quizPage.value = null
|
||||
) : null);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> get settings => InfoPage._infoTiles;
|
||||
|
||||
@override
|
||||
Widget? get button {
|
||||
if(_quizPage.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final page = _quizPage.value;
|
||||
if(page == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: () => Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: translations.quiz
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => page
|
||||
)),
|
||||
child: Text(
|
||||
translations.startQuiz
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizRoute extends StatefulWidget {
|
||||
final List<_QuizEntry> entries;
|
||||
final void Function() onSuccess;
|
||||
const _QuizRoute({
|
||||
required this.entries,
|
||||
required this.onSuccess
|
||||
});
|
||||
|
||||
@override
|
||||
State<_QuizRoute> createState() => _QuizRouteState();
|
||||
}
|
||||
|
||||
class _QuizRouteState extends State<_QuizRoute> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final List<RxInt> _selectedIndexes = List.generate(widget.entries.length, (_) => RxInt(-1));
|
||||
int _triesLeft = 3;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: widget.entries.indexed.expand((entry) {
|
||||
final selectedIndex = _selectedIndexes[entry.$1];
|
||||
return [
|
||||
Text(
|
||||
"${entry.$1 + 1}. ${entry.$2.question}",
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
...entry.$2.options.indexed.map<Widget>((value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Obx(() => RadioButton(
|
||||
checked: value.$1 == selectedIndex.value,
|
||||
content: Text(value.$2, textAlign: TextAlign.center),
|
||||
onChanged: (_) => selectedIndex.value = value.$1
|
||||
)),
|
||||
)),
|
||||
const SizedBox(height: 12.0)
|
||||
];
|
||||
}).toList()
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Obx(() {
|
||||
var clickable = true;
|
||||
for(final index in _selectedIndexes) {
|
||||
if(index.value == -1) {
|
||||
clickable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Button(
|
||||
onPressed: clickable ? () async {
|
||||
if(_triesLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var right = 0;
|
||||
final total = widget.entries.length;
|
||||
for(var i = 0; i < total; i++) {
|
||||
final selectedIndex = _selectedIndexes[i].value;
|
||||
final correctIndex = widget.entries[i].correctIndex;
|
||||
if(selectedIndex == correctIndex) {
|
||||
right++;
|
||||
}
|
||||
}
|
||||
|
||||
if(right == total) {
|
||||
widget.onSuccess();
|
||||
showInfoBar(
|
||||
translations.quizSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
_settingsController.firstRun.value = false;
|
||||
Navigator.of(context).pop();
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
return;
|
||||
}
|
||||
|
||||
switch(--_triesLeft) {
|
||||
case 0:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizZeroTriesLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
exit(0);
|
||||
case 1:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizOneTryLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizTwoTriesLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
}
|
||||
} : null,
|
||||
child: Text(translations.checkQuiz),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizEntry {
|
||||
final String question;
|
||||
final List<String> options;
|
||||
late final int correctIndex;
|
||||
|
||||
_QuizEntry({required this.question, required this.options}) {
|
||||
final correct = options.first;
|
||||
options.shuffle();
|
||||
correctIndex = options.indexOf(correct);
|
||||
}
|
||||
Widget? get button => null;
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
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/controller/backend_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/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.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/page/pages.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_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||
|
||||
class PlayPage extends RebootPage {
|
||||
const PlayPage({Key? key}) : super(key: key);
|
||||
@@ -36,8 +37,49 @@ class PlayPage extends RebootPage {
|
||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFirstLaunchInfo(),
|
||||
Expanded(
|
||||
child: super.build(context),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstLaunchInfo() => Obx(() {
|
||||
if(!_settingsController.firstRun.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.welcomeTitle),
|
||||
severity: InfoBarSeverity.warning,
|
||||
isLong: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(translations.welcomeDescription)
|
||||
),
|
||||
action: Button(
|
||||
child: Text(translations.welcomeAction),
|
||||
onPressed: () => startOnboarding(),
|
||||
),
|
||||
onClose: () => _settingsController.firstRun.value = false
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget? get button => LaunchButton(
|
||||
startLabel: translations.launchFortnite,
|
||||
@@ -47,25 +89,13 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
versionSelectSettingTile,
|
||||
buildVersionSelector(
|
||||
key: gameVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
_internalFiles,
|
||||
_multiplayer
|
||||
];
|
||||
|
||||
SettingTile get _multiplayer => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.people_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerName),
|
||||
subtitle: Text(translations.playGameServerDescription),
|
||||
children: [
|
||||
_hostSettingTile,
|
||||
_browseServerTile,
|
||||
_matchmakerTile,
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
@@ -95,8 +125,8 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerOptionsName),
|
||||
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||
title: Text(translations.settingsClientOptionsName),
|
||||
subtitle: Text(translations.settingsClientOptionsDescription),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -111,34 +141,4 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _matchmakerTile => SettingTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus());
|
||||
},
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerCustomName),
|
||||
subtitle: Text(translations.playGameServerCustomDescription),
|
||||
);
|
||||
|
||||
SettingTile get _browseServerTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.browser.index,
|
||||
icon: Icon(
|
||||
FluentIcons.search_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerBrowserName),
|
||||
subtitle: Text(translations.playGameServerBrowserDescription)
|
||||
);
|
||||
|
||||
SettingTile get _hostSettingTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.host.index,
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerHostName),
|
||||
subtitle: Text(translations.playGameServerHostDescription),
|
||||
);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
var data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry["id"] != _hostingController.uuid) && entry["discoverable"])
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<Map<String, dynamic>> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: items.isEmpty ? _noServersByQuery : _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildPopulatedListBody(Set<Map<String, dynamic>> items) => ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var entry = items.elementAt(index ~/ 2);
|
||||
var hasPassword = entry["password"] != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry["author"]}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(Map<String, dynamic> entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(Map<String, dynamic> element, String filter) {
|
||||
String? id = element["id"];
|
||||
if(id?.toLowerCase().contains(filter.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var uri = Uri.tryParse(filter);
|
||||
if(uri != null
|
||||
&& uri.host.isNotEmpty
|
||||
&& id?.toLowerCase().contains(uri.host.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? name = element["name"];
|
||||
if(name?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? author = element["author"];
|
||||
if(author?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? description = element["description"];
|
||||
if(description?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
var color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(Map<String, dynamic> entry) {
|
||||
String result = entry['name'];
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(Map<String, dynamic> entry) {
|
||||
String result = entry['description'];
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(Map<String, dynamic> entry) {
|
||||
var version = entry['version'];
|
||||
var versionSplit = version.indexOf("-");
|
||||
var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
|
||||
String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion;
|
||||
if(result.toLowerCase().startsWith("fortnite ")) {
|
||||
result = result.substring(0, 10);
|
||||
}
|
||||
|
||||
return "Fortnite $result";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
@@ -5,8 +5,8 @@ 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/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.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';
|
||||
@@ -40,52 +40,67 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
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()
|
||||
))
|
||||
),
|
||||
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(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
),
|
||||
_language,
|
||||
_theme,
|
||||
_resetDefaults,
|
||||
_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 _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _installationDirectory => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
@@ -97,15 +112,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
||||
)
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
extension _ThemeModeExtension on ThemeMode {
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'dart:collection';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.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/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
|
||||
|
||||
@@ -26,15 +26,15 @@ final List<RebootPage> pages = [
|
||||
const SettingsPage()
|
||||
];
|
||||
|
||||
final RxInt pageIndex = _initialPageIndex;
|
||||
RxInt get _initialPageIndex {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
return RxInt(settingsController.firstRun.value ? RebootPageType.info.index : RebootPageType.play.index);
|
||||
}
|
||||
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
|
||||
|
||||
final RxInt pageIndex = RxInt(RebootPageType.play.index);
|
||||
|
||||
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
||||
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey();
|
||||
|
||||
final GlobalKey<OverlayState> appOverlayKey = GlobalKey();
|
||||
|
||||
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
|
||||
|
||||
@@ -79,4 +79,8 @@ void addSubPageToStack(String pageName) {
|
||||
appStack.add(identifier);
|
||||
_pagesStack[index]!.add(identifier);
|
||||
pagesController.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
GlobalKey<OverlayTargetState> getOverlayTargetKeyByPage(int pageIndex) => _flyoutPageControllers[pageIndex];
|
||||
|
||||
GlobalKey<OverlayTargetState> get pageOverlayTargetKey => _flyoutPageControllers[pageIndex.value];
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
String? checkVersion(String? text, List<FortniteVersion> versions) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
if (versions.any((element) => element.name == text)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkChangeVersion(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyVersionName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
var directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkDll(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDllPath;
|
||||
}
|
||||
|
||||
if (!File(text).existsSync()) {
|
||||
return translations.dllDoesNotExist;
|
||||
}
|
||||
|
||||
if (!text.endsWith(".dll")) {
|
||||
return translations.invalidDllExtension;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkMatchmaking(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyHostname;
|
||||
}
|
||||
|
||||
var ipParts = text.split(":");
|
||||
if(ipParts.length > 2){
|
||||
return translations.hostnameFormat;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyURL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final Map<String, Future<void>> _operations = {};
|
||||
|
||||
Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
return old;
|
||||
}
|
||||
|
||||
final newRun = _downloadCriticalDllInteractive(filePath, silent);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
|
||||
final fileName = path.basename(filePath).toLowerCase();
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName == "reboot.dll") {
|
||||
await _updateController.updateReboot(
|
||||
silent: silent
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(File(filePath).existsSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
|
||||
if(!silent) {
|
||||
entry = showInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry?.close();
|
||||
if(!silent) {
|
||||
entry = await showInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showInfoBar(
|
||||
translations.downloadDllError(fileName, error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
onDismissed: () => completer.complete(null),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
await downloadCriticalDllInteractive(filePath);
|
||||
completer.complete(null);
|
||||
},
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
await completer.future;
|
||||
}
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
extension InjectableDllExtension on InjectableDll {
|
||||
String get path {
|
||||
final SettingsController settingsController = Get.find<SettingsController>();
|
||||
switch(this){
|
||||
case InjectableDll.reboot:
|
||||
if(_updateController.customGameServer.value) {
|
||||
final file = File(settingsController.gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return rebootDllFile.path;
|
||||
case InjectableDll.console:
|
||||
return settingsController.unrealEngineConsoleDll.text;
|
||||
case InjectableDll.cobalt:
|
||||
return settingsController.backendDll.text;
|
||||
case InjectableDll.memory:
|
||||
return settingsController.memoryLeakDll.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final File _loggingFile = _createLoggingFile();
|
||||
final Semaphore _semaphore = Semaphore(1);
|
||||
|
||||
File _createLoggingFile() {
|
||||
final file = File("${logsDirectory.path}\\launcher.log");
|
||||
file.parent.createSync(recursive: true);
|
||||
if(file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
file.createSync();
|
||||
return file;
|
||||
}
|
||||
|
||||
void log(String message) async {
|
||||
try {
|
||||
await _semaphore.acquire();
|
||||
print(message);
|
||||
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
}catch(error) {
|
||||
print(error);
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
@@ -3,45 +3,52 @@ import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
const Duration _timeout = Duration(seconds: 2);
|
||||
|
||||
Future<bool> _pingGameServer(String hostname, int port) async {
|
||||
RawDatagramSocket? socket;
|
||||
try {
|
||||
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
final dataToSend = utf8.encode(DateTime.now().toIso8601String());
|
||||
socket.send(dataToSend, InternetAddress(hostname), port);
|
||||
await for (var event in socket) {
|
||||
switch(event) {
|
||||
case RawSocketEvent.read:
|
||||
case RawSocketEvent.write:
|
||||
return true;
|
||||
case RawSocketEvent.readClosed:
|
||||
case RawSocketEvent.closed:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}finally {
|
||||
socket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> get _timeoutFuture => Future.delayed(_timeout).then((value) => false);
|
||||
const Duration _timeout = Duration(seconds: 5);
|
||||
|
||||
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
|
||||
var start = DateTime.now();
|
||||
var firstTime = true;
|
||||
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
|
||||
var split = address.split(":");
|
||||
var hostname = split[0];
|
||||
if(isLocalHost(hostname)) {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
var port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||
var result = await Future.any([_timeoutFuture, _pingGameServer(hostname, port)]);
|
||||
return false;
|
||||
}catch(error) {
|
||||
log("[MATCHMAKER] Error: $error");
|
||||
return false;
|
||||
}finally {
|
||||
socket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final start = DateTime.now();
|
||||
var firstTime = true;
|
||||
final split = address.split(":");
|
||||
var hostname = split[0];
|
||||
if(isLocalHost(hostname)) {
|
||||
hostname = "127.0.0.1";
|
||||
}
|
||||
|
||||
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
|
||||
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
|
||||
final result = await ping(hostname, port)
|
||||
.timeout(_timeout, onTimeout: () => false);
|
||||
if(result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_name_input.dart';
|
||||
|
||||
class AddLocalVersion extends StatefulWidget {
|
||||
const AddLocalVersion({Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<AddLocalVersion> createState() => _AddLocalVersionState();
|
||||
}
|
||||
|
||||
class _AddLocalVersionState extends State<AddLocalVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _gamePathController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_gamePathController.addListener(() async {
|
||||
var file = Directory(_gamePathController.text);
|
||||
if(await file.exists()) {
|
||||
if(_nameController.text.isEmpty) {
|
||||
var text = path.basename(_gamePathController.text);
|
||||
_nameController.text = text;
|
||||
_nameController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.localBuildsWarning),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: translations.gameFolderPlaceholder,
|
||||
windowTitle: translations.gameFolderPlaceWindowTitle,
|
||||
controller: _gamePathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.saveLocalVersion,
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_gamePathController.text)
|
||||
)));
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_name_input.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
const AddServerVersion({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddServerVersion> createState() => _AddServerVersionState();
|
||||
}
|
||||
|
||||
class _AddServerVersionState extends State<AddServerVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final Rx<DownloadStatus> _status = Rx(DownloadStatus.form);
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
Isolate? _isolate;
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
_isolate?.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return FormDialog(
|
||||
content: _formBody,
|
||||
buttons: _formButtons
|
||||
);
|
||||
}
|
||||
);
|
||||
case DownloadStatus.downloading:
|
||||
case DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
||||
);
|
||||
case DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: translations.download,
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final build = _buildController.selectedBuild;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(message.progress, message.minutesLeft, message.extracting);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
_isolate = await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(double progress, int? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_status.value == DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _formBody => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: translations.buildInstallationDirectory,
|
||||
placeholder: translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildSelector() => InfoLabel(
|
||||
label: translations.build,
|
||||
child: Obx(() => ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _builds,
|
||||
value: _buildController.selectedBuild,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_buildController.selectedBuild = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
List<ComboBoxItem<FortniteBuild>> get _builds => _buildController.builds!
|
||||
.map((element) => _buildItem(element))
|
||||
.toList();
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: "Stop",
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_diskSpace.disks.isEmpty){
|
||||
return;
|
||||
}
|
||||
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _buildController.selectedBuild;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
final buildName = build.version.toString();
|
||||
_nameController.text = buildName;
|
||||
_nameController.selection = TextSelection.collapsed(offset: buildName.length);
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.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';
|
||||
@@ -15,8 +16,26 @@ SettingTile createFileSetting({required String title, required String descriptio
|
||||
placeholder: translations.selectPathPlaceholder,
|
||||
windowTitle: translations.selectPathWindowTitle,
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
validator: _checkDll,
|
||||
extension: "dll",
|
||||
folder: false
|
||||
folder: false,
|
||||
validatorMode: AutovalidateMode.always
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:path/path.dart';
|
||||
@@ -13,17 +12,16 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.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/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
final bool host;
|
||||
@@ -43,6 +41,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
InfoBarEntry? _gameClientInfoBar;
|
||||
InfoBarEntry? _gameServerInfoBar;
|
||||
CancelableOperation? _operation;
|
||||
@@ -95,6 +94,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
||||
for (final injectable in InjectableDll.values) {
|
||||
if(await _getDllFileOrStop(injectable, host) == null) {
|
||||
_onStop(
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -124,12 +127,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget();
|
||||
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
|
||||
final started = host ? _hostingController.started() : _gameController.started();
|
||||
if(!started) {
|
||||
result?.kill();
|
||||
return;
|
||||
}
|
||||
|
||||
if(linkedHostingInstance != null || host){
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
|
||||
}else {
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
@@ -148,7 +155,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_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");
|
||||
return null;
|
||||
}
|
||||
@@ -158,7 +165,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return null;
|
||||
}
|
||||
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer();
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer(host);
|
||||
if(!response) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||
return null;
|
||||
@@ -172,19 +179,24 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<bool> _askForAutomaticGameServer() async {
|
||||
final result = await showAppDialog<bool>(
|
||||
Future<bool> _askForAutomaticGameServer(bool host) async {
|
||||
if (host ? !_hostingController.started() : !_gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(reason: _StopReason.normal);
|
||||
return false;
|
||||
}
|
||||
|
||||
final result = await showRebootDialog<bool>(
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. "
|
||||
"If you don't want to join another player's server, you should start a game server. This is necessary to be able to play: for more information check the Info tab in the launcher.",
|
||||
text: translations.automaticGameServerDialogContent,
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Ignore"
|
||||
text: translations.automaticGameServerDialogIgnore
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
text: "Start server",
|
||||
text: translations.automaticGameServerDialogStart,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
@@ -214,7 +226,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
|
||||
final instance = GameInstance(
|
||||
versionName: version.name,
|
||||
versionName: version.content.toString(),
|
||||
gamePid: gameProcess,
|
||||
launcherPid: launcherProcess,
|
||||
eacPid: eacProcess,
|
||||
@@ -247,7 +259,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
executable: executable,
|
||||
args: gameArgs,
|
||||
useTempBatch: false,
|
||||
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
);
|
||||
void onGameOutput(String line, bool error) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
|
||||
@@ -267,12 +282,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
||||
gameProcess.exitCode.then((_) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running");
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance.launched}): stop signal");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
|
||||
_onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: host
|
||||
@@ -289,7 +299,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final process = await startProcess(
|
||||
executable: file,
|
||||
useTempBatch: false,
|
||||
name: "${version.name}-${basenameWithoutExtension(file.path)}"
|
||||
name: "${version.content}-${basenameWithoutExtension(file.path)}",
|
||||
environment: {
|
||||
"OPENSSL_ia32cap": "~0x20000000"
|
||||
}
|
||||
);
|
||||
final pid = process.pid;
|
||||
suspend(pid);
|
||||
@@ -304,7 +317,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
try {
|
||||
final windowManager = VirtualDesktopManager.getInstance();
|
||||
_virtualDesktop = windowManager.createDesktop();
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
|
||||
var success = false;
|
||||
try {
|
||||
success = await windowManager.moveWindowToDesktop(
|
||||
@@ -391,7 +404,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onGameClientInjected() {
|
||||
_gameClientInfoBar?.close();
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameClientStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -399,10 +412,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
Future<void> _onGameServerInjected() async {
|
||||
_gameServerInfoBar?.close();
|
||||
final theme = FluentTheme.of(appKey.currentContext!);
|
||||
if(_gameServerInfoBar != null) {
|
||||
_gameServerInfoBar?.close();
|
||||
}else {
|
||||
_gameClientInfoBar?.close();
|
||||
}
|
||||
|
||||
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
|
||||
try {
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.waitingForGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
@@ -414,17 +432,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
);
|
||||
_gameServerInfoBar?.close();
|
||||
if (!localPingResult) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameServerStartWarning,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
return;
|
||||
}
|
||||
_backendController.joinLocalHost();
|
||||
_backendController.joinLocalhost();
|
||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||
if (!accessible) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameServerStartLocalWarning,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
@@ -436,7 +454,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_gameController.username.text,
|
||||
_hostingController.instance.value!.versionName,
|
||||
);
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.gameServerStarted,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -448,7 +466,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||
try {
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.checkingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
@@ -464,12 +482,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
"$publicIp:$gameServerPort",
|
||||
timeout: const Duration(days: 365)
|
||||
);
|
||||
_gameServerInfoBar = showInfoBar(
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.checkGameServerFixMessage(gameServerPort),
|
||||
action: Button(
|
||||
onPressed: () async {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
},
|
||||
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
|
||||
child: Text(translations.checkGameServerFixAction),
|
||||
),
|
||||
severity: InfoBarSeverity.warning,
|
||||
@@ -486,7 +502,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(host == null) {
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
await _backendController.worker?.cancel();
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
@@ -542,14 +557,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
case _StopReason.normal:
|
||||
break;
|
||||
case _StopReason.missingVersionError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.missingVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.missingExecutableError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.missingExecutableError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -557,7 +572,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
break;
|
||||
case _StopReason.exitCode:
|
||||
if(instance != null && !instance.launched) {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -565,28 +580,39 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
break;
|
||||
case _StopReason.corruptedVersionError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.corruptedDllError:
|
||||
showInfoBar(
|
||||
translations.corruptedDllError(error!),
|
||||
showRebootInfoBar(
|
||||
translations.corruptedDllError(error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.missingCustomDllError:
|
||||
showRebootInfoBar(
|
||||
translations.missingCustomDllError(error!),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
break;
|
||||
case _StopReason.tokenError:
|
||||
showInfoBar(
|
||||
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
|
||||
showRebootInfoBar(
|
||||
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
action: Button(
|
||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||
child: Text(translations.openLog),
|
||||
)
|
||||
);
|
||||
break;
|
||||
case _StopReason.unknownError:
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.unknownFortniteError(error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
@@ -610,7 +636,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
if(dllPath == null) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedDllError,
|
||||
reason: _StopReason.missingCustomDllError,
|
||||
error: injectable.name,
|
||||
host: hosting
|
||||
);
|
||||
return;
|
||||
@@ -631,33 +658,62 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host) async {
|
||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||
final path = injectable.path;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
|
||||
final file = File(path);
|
||||
final (file, customDll) = _settingsController.getInjectableData(injectable);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
||||
if(await file.exists()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path exists");
|
||||
return file;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
|
||||
if(customDll || isRetry) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
|
||||
await downloadCriticalDllInteractive(path);
|
||||
await _settingsController.downloadCriticalDllInteractive(file.path);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
|
||||
return _getDllFileOrStop(injectable, host);
|
||||
return _getDllFileOrStop(injectable, host, true);
|
||||
}
|
||||
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showInfoBar(
|
||||
translations.launchingHeadlessServer,
|
||||
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
|
||||
translations.launchingGameServer,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
InfoBarEntry _showLaunchingGameClientWidget() => _gameClientInfoBar = showInfoBar(
|
||||
translations.launchingGameClient,
|
||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
|
||||
return _gameClientInfoBar = showRebootInfoBar(
|
||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
||||
loading: true,
|
||||
duration: null
|
||||
duration: null,
|
||||
action: Obx(() {
|
||||
if(_hostingController.started.value || linkedHosting) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 2.0
|
||||
),
|
||||
child: Button(
|
||||
onPressed: () async {
|
||||
_backendController.joinLocalhost();
|
||||
if(!_hostingController.started.value) {
|
||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
|
||||
_gameClientInfoBar?.close();
|
||||
_showLaunchingGameClientWidget(version, hostType, true);
|
||||
}
|
||||
},
|
||||
child: Text(translations.startGameServer),
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _StopReason {
|
||||
@@ -665,6 +721,7 @@ enum _StopReason {
|
||||
missingVersionError,
|
||||
missingExecutableError,
|
||||
corruptedVersionError,
|
||||
missingCustomDllError,
|
||||
corruptedDllError,
|
||||
backendError,
|
||||
matchmakerError,
|
||||
|
||||
@@ -2,11 +2,12 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/profile.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
||||
|
||||
class ProfileWidget extends StatefulWidget {
|
||||
const ProfileWidget({Key? key}) : super(key: key);
|
||||
final GlobalKey<OverlayTargetState> overlayKey;
|
||||
const ProfileWidget({required this.overlayKey});
|
||||
|
||||
@override
|
||||
State<ProfileWidget> createState() => _ProfileWidgetState();
|
||||
@@ -14,14 +15,13 @@ class ProfileWidget extends StatefulWidget {
|
||||
|
||||
class _ProfileWidgetState extends State<ProfileWidget> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
Widget build(BuildContext context) => OverlayTarget(
|
||||
key: widget.overlayKey,
|
||||
child: HoverButton(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
onPressed: firstRun ? null : () async {
|
||||
onPressed: () async {
|
||||
if(await showProfileForm(context)) {
|
||||
setState(() {});
|
||||
}
|
||||
@@ -78,8 +78,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
|
||||
String get _username {
|
||||
var username = _gameController.username.text;
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerButton extends StatefulWidget {
|
||||
|
||||
@@ -2,12 +2,13 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerTypeSelector extends StatefulWidget {
|
||||
const ServerTypeSelector({Key? key})
|
||||
: super(key: key);
|
||||
final Key overlayKey;
|
||||
const ServerTypeSelector({required this.overlayKey});
|
||||
|
||||
@override
|
||||
State<ServerTypeSelector> createState() => _ServerTypeSelectorState();
|
||||
@@ -18,12 +19,16 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
leading: Text(_controller.type.value.label),
|
||||
items: ServerType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()
|
||||
return Obx(() => OverlayTarget(
|
||||
key: widget.overlayKey,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_controller.type.value.label),
|
||||
items: ServerType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:skeletons/skeletons.dart';
|
||||
|
||||
class SettingTile extends StatelessWidget {
|
||||
class SettingTile extends StatefulWidget {
|
||||
static const double kDefaultHeight = 80.0;
|
||||
static const double kDefaultContentWidth = 200.0;
|
||||
static const double kDefaultHeaderHeight = 72;
|
||||
|
||||
final void Function()? onPressed;
|
||||
final Icon icon;
|
||||
@@ -13,28 +14,38 @@ class SettingTile extends StatelessWidget {
|
||||
final Text? subtitle;
|
||||
final Widget? content;
|
||||
final double? contentWidth;
|
||||
final Key? overlayKey;
|
||||
final List<Widget>? children;
|
||||
|
||||
const SettingTile({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.content,
|
||||
this.contentWidth = kDefaultContentWidth,
|
||||
this.overlayKey,
|
||||
this.children
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0
|
||||
),
|
||||
child: HoverButton(
|
||||
onPressed: _buildOnPressed(context),
|
||||
builder: (context, states) => Container(
|
||||
height: 80,
|
||||
State<SettingTile> createState() => SettingTileState();
|
||||
}
|
||||
|
||||
class SettingTileState extends State<SettingTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0
|
||||
),
|
||||
child: HoverButton(
|
||||
onPressed: _buildOnPressed(),
|
||||
builder: (context, states) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: SettingTile.kDefaultHeight
|
||||
),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
@@ -42,76 +53,92 @@ class SettingTile extends StatelessWidget {
|
||||
states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0))
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Card(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(4.0)
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 6.0
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 16.0),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title == null ? _skeletonTitle : title!,
|
||||
subtitle == null ? _skeletonSubtitle : subtitle!,
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
_trailing
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
child: _buildBody()
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Card _buildBody() {
|
||||
return Card(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(6.0)
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 12.0
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if(widget.overlayKey != null)
|
||||
OverlayTarget(
|
||||
key: widget.overlayKey,
|
||||
child: widget.icon,
|
||||
)
|
||||
else
|
||||
widget.icon,
|
||||
const SizedBox(width: 16.0),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
widget.title == null ? _skeletonTitle : widget.title!,
|
||||
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
_trailing
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void Function()? _buildOnPressed(BuildContext context) {
|
||||
if(onPressed != null) {
|
||||
return onPressed;
|
||||
void Function()? _buildOnPressed() {
|
||||
if(widget.onPressed != null) {
|
||||
return widget.onPressed;
|
||||
}
|
||||
|
||||
final children = this.children;
|
||||
final children = this.widget.children;
|
||||
if (children == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return () async {
|
||||
await Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: title?.data
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index]
|
||||
)
|
||||
));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value);
|
||||
};
|
||||
return () => openNestedPage();
|
||||
}
|
||||
|
||||
Future<void> openNestedPage() async {
|
||||
final children = this.widget.children;
|
||||
if (children == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: widget.title?.data
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index]
|
||||
)
|
||||
));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value);
|
||||
}
|
||||
|
||||
Widget get _trailing {
|
||||
final hasContent = content != null;
|
||||
final hasChildren = children?.isNotEmpty == true;
|
||||
final hasListener = onPressed != null;
|
||||
final hasContent = widget.content != null;
|
||||
final hasChildren = widget.children?.isNotEmpty == true;
|
||||
final hasListener = widget.onPressed != null;
|
||||
if(hasContent && hasChildren) {
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: contentWidth,
|
||||
child: content
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
Icon(
|
||||
@@ -123,8 +150,8 @@ class SettingTile extends StatelessWidget {
|
||||
|
||||
if (hasContent) {
|
||||
return SizedBox(
|
||||
width: contentWidth,
|
||||
child: content
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class VersionNameInput extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController controller;
|
||||
|
||||
VersionNameInput({Key? key, required this.controller}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InfoLabel(
|
||||
label: translations.versionName,
|
||||
child: TextFormBox(
|
||||
controller: controller,
|
||||
placeholder: translations.versionNameLabel,
|
||||
autofocus: true,
|
||||
validator: (version) => checkVersion(version, _gameController.versions.value),
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/version.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
const VersionSelector({Key? key}) : super(key: key);
|
||||
|
||||
static Future<void> openDownloadDialog() => showAppDialog<bool>(
|
||||
builder: (context) => const AddServerVersion(),
|
||||
);
|
||||
|
||||
static Future<void> openAddDialog() => showAppDialog<bool>(
|
||||
builder: (context) => const AddLocalVersion(),
|
||||
static Future<void> openDownloadDialog({bool closable = true}) => showRebootDialog<bool>(
|
||||
builder: (context) => AddVersionDialog(
|
||||
closable: closable,
|
||||
),
|
||||
dismissWithEsc: closable
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -48,7 +42,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_gameController.selectedVersion?.name ?? translations.selectVersion,
|
||||
_gameController.selectedVersion?.content.toString() ?? translations.selectVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -58,30 +52,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
);
|
||||
});
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
final items = _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
items.add(MenuFlyoutItem(
|
||||
text: Text(translations.addLocalBuildContent),
|
||||
onPressed: VersionSelector.openAddDialog
|
||||
));
|
||||
items.add(MenuFlyoutItem(
|
||||
text: Text(translations.downloadBuildContent),
|
||||
onPressed: VersionSelector.openDownloadDialog
|
||||
));
|
||||
return items;
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: _createOptionsMenu(
|
||||
version: version,
|
||||
close: true,
|
||||
child: Text(version.name),
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version
|
||||
);
|
||||
|
||||
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||
@@ -104,13 +74,64 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
child: child
|
||||
);
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
final items = _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
items.add(MenuFlyoutItem(
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
FluentIcons.add_24_regular,
|
||||
size: 12
|
||||
),
|
||||
),
|
||||
text: Text(translations.addVersion),
|
||||
onPressed: VersionSelector.openDownloadDialog
|
||||
));
|
||||
return items;
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _openVersionOptions(version);
|
||||
},
|
||||
child: Text(version.content.toString())
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () => _openVersionOptions(version),
|
||||
icon: Icon(
|
||||
FluentIcons.more_vertical_24_regular
|
||||
)
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version
|
||||
);
|
||||
|
||||
Future<void> _openVersionOptions(FortniteVersion version) async {
|
||||
final result = await _flyoutController.showFlyout<_ContextualOption?>(
|
||||
builder: (context) => MenuFlyout(
|
||||
items: _ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.toList()
|
||||
),
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.transparent
|
||||
);
|
||||
_handleResult(result, version, true);
|
||||
}
|
||||
|
||||
void _handleResult(_ContextualOption? result, FortniteVersion version, bool close) async {
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case _ContextualOption.openExplorer:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -118,23 +139,8 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
launchUrl(version.location.uri)
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
break;
|
||||
case _ContextualOption.modify:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
await _openRenameDialog(context, version);
|
||||
break;
|
||||
case _ContextualOption.delete:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _openDeleteDialog(context, version) ?? false;
|
||||
final result = await _openDeleteDialog(context, version) ?? false;
|
||||
if(!mounted || !result){
|
||||
return;
|
||||
}
|
||||
@@ -149,25 +155,25 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createOption(BuildContext context, _ContextualOption entry) {
|
||||
return MenuFlyoutItem(
|
||||
text: Text(entry.name),
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => Navigator.of(context).pop(entry)
|
||||
);
|
||||
}
|
||||
|
||||
bool _onExplorerError() {
|
||||
showInfoBar(translations.missingVersion);
|
||||
showRebootInfoBar(translations.missingVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
|
||||
return showAppDialog<bool>(
|
||||
return showRebootDialog<bool>(
|
||||
builder: (context) => ContentDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -189,87 +195,32 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(translations.deleteVersionCancel),
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(false),
|
||||
text: translations.deleteVersionCancel
|
||||
),
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(translations.deleteVersionConfirm),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
text: translations.deleteVersionConfirm
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
|
||||
var nameController = TextEditingController(text: version.name);
|
||||
var pathController = TextEditingController(text: version.location.path);
|
||||
return showAppDialog<String?>(
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoLabel(
|
||||
label: translations.versionName,
|
||||
child: TextFormBox(
|
||||
controller: nameController,
|
||||
placeholder: translations.newVersionNameLabel,
|
||||
autofocus: true,
|
||||
validator: (text) => checkChangeVersion(text)
|
||||
)
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
placeholder: translations.newVersionNameLabel,
|
||||
windowTitle: translations.gameFolderPlaceWindowTitle,
|
||||
label: translations.gameFolderLabel,
|
||||
controller: pathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.newVersionNameConfirm,
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.updateVersion(version, (version) {
|
||||
version.name = nameController.text;
|
||||
version.location = Directory(pathController.text);
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _ContextualOption {
|
||||
openExplorer,
|
||||
modify,
|
||||
delete
|
||||
}
|
||||
delete;
|
||||
|
||||
extension _ContextualOptionExtension on _ContextualOption {
|
||||
String get name {
|
||||
return this == _ContextualOption.openExplorer ? translations.openInExplorer
|
||||
: this == _ContextualOption.modify ? translations.modify
|
||||
: translations.delete;
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _ContextualOption.openExplorer:
|
||||
return translations.openInExplorer;
|
||||
case _ContextualOption.delete:
|
||||
return translations.delete;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
SettingTile get versionSelectSettingTile => SettingTile(
|
||||
SettingTile buildVersionSelector({
|
||||
required GlobalKey<OverlayTargetState> key
|
||||
}) => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.play_24_regular
|
||||
),
|
||||
@@ -15,6 +18,9 @@ SettingTile get versionSelectSettingTile => SettingTile(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: SettingTile.kDefaultContentWidth,
|
||||
),
|
||||
child: const VersionSelector()
|
||||
child: OverlayTarget(
|
||||
key: key,
|
||||
child: const VersionSelector(),
|
||||
)
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user