mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
<feat: New project structure>
<feat: New release>
This commit is contained in:
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Form to suggest a feature
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 774 B |
BIN
assets/images/browse.png
Normal file
BIN
assets/images/browse.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1011 B |
|
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
@@ -1,3 +0,0 @@
|
|||||||
cd %UserProfile%\.reboot_launcher\backend-lawin
|
|
||||||
lawinserver-win.exe
|
|
||||||
pause
|
|
||||||
2
assets/lawin/run.bat
Normal file
2
assets/lawin/run.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
lawinserver-win.exe
|
||||||
|
pause
|
||||||
Binary file not shown.
105
lib/cli.dart
105
lib/cli.dart
@@ -1,105 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
|
||||||
import 'package:reboot_launcher/src/cli/compatibility.dart';
|
|
||||||
import 'package:reboot_launcher/src/cli/config.dart';
|
|
||||||
import 'package:reboot_launcher/src/cli/game.dart';
|
|
||||||
import 'package:reboot_launcher/src/cli/reboot.dart';
|
|
||||||
import 'package:reboot_launcher/src/cli/server.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart' as server;
|
|
||||||
|
|
||||||
late String? username;
|
|
||||||
late bool host;
|
|
||||||
late bool verbose;
|
|
||||||
late String dll;
|
|
||||||
late FortniteVersion version;
|
|
||||||
late bool autoRestart;
|
|
||||||
|
|
||||||
void main(List<String> args) async {
|
|
||||||
stdout.writeln("Reboot Launcher");
|
|
||||||
stdout.writeln("Wrote by Auties00");
|
|
||||||
stdout.writeln("Version 5.3");
|
|
||||||
|
|
||||||
kill();
|
|
||||||
|
|
||||||
var gameJson = await getControllerJson("game");
|
|
||||||
var serverJson = await getControllerJson("server");
|
|
||||||
var settingsJson = await getControllerJson("settings");
|
|
||||||
var versions = getVersions(gameJson);
|
|
||||||
var parser = ArgParser()
|
|
||||||
..addCommand("list")
|
|
||||||
..addCommand("launch")
|
|
||||||
..addOption("version")
|
|
||||||
..addOption("username")
|
|
||||||
..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson))
|
|
||||||
..addOption("server-host")
|
|
||||||
..addOption("server-port")
|
|
||||||
..addOption("matchmaking-address")
|
|
||||||
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? rebootDllFile)
|
|
||||||
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
|
|
||||||
..addFlag("log", defaultsTo: false)
|
|
||||||
..addFlag("host", defaultsTo: false)
|
|
||||||
..addFlag("auto-restart", defaultsTo: false, negatable: true);
|
|
||||||
var result = parser.parse(args);
|
|
||||||
if (result.command?.name == "list") {
|
|
||||||
stdout.writeln("Versions list: ");
|
|
||||||
versions.map((entry) => "${entry.location.path}(${entry.name})")
|
|
||||||
.forEach((element) => stdout.writeln(element));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dll = result["dll"];
|
|
||||||
host = result["host"];
|
|
||||||
username = result["username"] ?? gameJson["username"];
|
|
||||||
verbose = result["log"];
|
|
||||||
|
|
||||||
version = _createVersion(gameJson["version"], result["version"], versions);
|
|
||||||
await downloadRequiredDLLs();
|
|
||||||
if(result["update"]) {
|
|
||||||
stdout.writeln("Updating reboot dll...");
|
|
||||||
try {
|
|
||||||
await downloadRebootDll(rebootDownloadUrl, 0);
|
|
||||||
}catch(error){
|
|
||||||
stderr.writeln("Cannot update reboot dll: $error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln("Launching game...");
|
|
||||||
var executable = await version.executable;
|
|
||||||
if(executable == null){
|
|
||||||
throw Exception("Missing game executable at: ${version.location.path}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverType = getServerType(result);
|
|
||||||
var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"];
|
|
||||||
var serverPort = result["server-port"] ?? serverJson["${serverType.id}_port"];
|
|
||||||
var started = await startServer(serverHost, serverPort, serverType);
|
|
||||||
if(!started){
|
|
||||||
stderr.writeln("Cannot start server!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
server.writeMatchmakingIp(result["matchmaking-address"]);
|
|
||||||
autoRestart = result["auto-restart"];
|
|
||||||
await startGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
FortniteVersion _createVersion(String? versionName, String? versionPath, List<FortniteVersion> versions) {
|
|
||||||
if (versionPath != null) {
|
|
||||||
return FortniteVersion(name: "dummy", location: Directory(versionPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(versionName != null){
|
|
||||||
try {
|
|
||||||
return versions.firstWhere((element) => versionName == element.name);
|
|
||||||
}catch(_){
|
|
||||||
throw Exception("Cannot find version $versionName");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception(
|
|
||||||
"Specify a version using --version or open the launcher GUI and select it manually");
|
|
||||||
}
|
|
||||||
154
lib/main.dart
154
lib/main.dart
@@ -1,25 +1,30 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_launcher/src/util/error.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/interactive/error.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/page/home_page.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/supabase.dart';
|
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/interactive/server.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/watch.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:url_protocol/url_protocol.dart';
|
||||||
|
|
||||||
const double kDefaultWindowWidth = 1024;
|
const double kDefaultWindowWidth = 1536;
|
||||||
const double kDefaultWindowHeight = 1024;
|
const double kDefaultWindowHeight = 1024;
|
||||||
final GlobalKey appKey = GlobalKey();
|
const String kCustomUrlSchema = "reboot";
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
runZonedGuarded(() async {
|
runZonedGuarded(() async {
|
||||||
@@ -30,37 +35,12 @@ void main() async {
|
|||||||
);
|
);
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await SystemTheme.accentColor.load();
|
await SystemTheme.accentColor.load();
|
||||||
await GetStorage.init("reboot_game");
|
var storageError = await _initStorage();
|
||||||
await GetStorage.init("reboot_server");
|
var urlError = await _initUrlHandler();
|
||||||
await GetStorage.init("reboot_update");
|
var windowError = await _initWindow();
|
||||||
await GetStorage.init("reboot_settings");
|
var observerError = _initObservers();
|
||||||
await GetStorage.init("reboot_hosting");
|
|
||||||
var gameController = GameController();
|
|
||||||
Get.put(gameController);
|
|
||||||
Get.put(ServerController());
|
|
||||||
Get.put(BuildController());
|
|
||||||
Get.put(SettingsController());
|
|
||||||
Get.put(HostingController());
|
|
||||||
await windowManager.ensureInitialized();
|
|
||||||
var controller = Get.find<SettingsController>();
|
|
||||||
var size = Size(controller.width, controller.height);
|
|
||||||
await windowManager.setSize(size);
|
|
||||||
if(controller.offsetX != null && controller.offsetY != null){
|
|
||||||
await windowManager.setPosition(Offset(controller.offsetX!, controller.offsetY!));
|
|
||||||
}else {
|
|
||||||
await windowManager.setAlignment(Alignment.center);
|
|
||||||
};
|
|
||||||
await Window.initialize();
|
|
||||||
await Window.setEffect(
|
|
||||||
effect: WindowEffect.acrylic,
|
|
||||||
color: Colors.transparent,
|
|
||||||
dark: SystemTheme.isDarkMode
|
|
||||||
);
|
|
||||||
var supabase = Supabase.instance.client;
|
|
||||||
await supabase.from('hosts')
|
|
||||||
.delete()
|
|
||||||
.match({'id': gameController.uuid});
|
|
||||||
runApp(const RebootApplication());
|
runApp(const RebootApplication());
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors([urlError, storageError, windowError, observerError]));
|
||||||
},
|
},
|
||||||
(error, stack) => onError(error, stack, false),
|
(error, stack) => onError(error, stack, false),
|
||||||
zoneSpecification: ZoneSpecification(
|
zoneSpecification: ZoneSpecification(
|
||||||
@@ -68,6 +48,96 @@ void main() async {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleErrors(List<Object?> errors) => errors.where((element) => element != null).forEach((element) => onError(element, null, false));
|
||||||
|
|
||||||
|
Future<Object?> _initUrlHandler() async {
|
||||||
|
try {
|
||||||
|
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
|
||||||
|
var appLinks = AppLinks();
|
||||||
|
var initialUrl = await appLinks.getInitialAppLink();
|
||||||
|
if(initialUrl != null) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var gameController = Get.find<GameController>();
|
||||||
|
var matchmakerController = Get.find<MatchmakerController>();
|
||||||
|
appLinks.uriLinkStream.listen((uri) {
|
||||||
|
var uuid = _parseCustomUrl(uri);
|
||||||
|
var server = gameController.findServerById(uuid);
|
||||||
|
if(server != null) {
|
||||||
|
matchmakerController.joinServer(server);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage(
|
||||||
|
"No server found: invalid or expired link",
|
||||||
|
duration: snackbarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}catch(error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _parseCustomUrl(Uri uri) => uri.host;
|
||||||
|
|
||||||
|
Future<Object?> _initWindow() async {
|
||||||
|
try {
|
||||||
|
await windowManager.ensureInitialized();
|
||||||
|
var settingsController = Get.find<SettingsController>();
|
||||||
|
var size = Size(settingsController.width, settingsController.height);
|
||||||
|
await windowManager.setSize(size);
|
||||||
|
if(settingsController.offsetX != null && settingsController.offsetY != null){
|
||||||
|
await windowManager.setPosition(Offset(settingsController.offsetX!, settingsController.offsetY!));
|
||||||
|
}else {
|
||||||
|
await windowManager.setAlignment(Alignment.center);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}catch(error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _initObservers() {
|
||||||
|
try {
|
||||||
|
var gameController = Get.find<GameController>();
|
||||||
|
var gameInstance = gameController.instance.value;
|
||||||
|
gameInstance?.startObserver();
|
||||||
|
var hostingController = Get.find<HostingController>();
|
||||||
|
var hostingInstance = hostingController.instance.value;
|
||||||
|
hostingInstance?.startObserver();
|
||||||
|
return null;
|
||||||
|
}catch(error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object?> _initStorage() async {
|
||||||
|
try {
|
||||||
|
await GetStorage("reboot_game", settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage("reboot_authenticator", settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage("reboot_matchmaker", settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage("reboot_update", settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage("reboot_settings", settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage("reboot_hosting", settingsDirectory.path).initStorage;
|
||||||
|
Get.put(GameController());
|
||||||
|
Get.put(AuthenticatorController());
|
||||||
|
Get.put(MatchmakerController());
|
||||||
|
Get.put(BuildController());
|
||||||
|
Get.put(SettingsController());
|
||||||
|
Get.put(HostingController());
|
||||||
|
var updateController = UpdateController();
|
||||||
|
Get.put(updateController);
|
||||||
|
updateController.update();
|
||||||
|
return null;
|
||||||
|
}catch(error) {
|
||||||
|
print(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RebootApplication extends StatefulWidget {
|
class RebootApplication extends StatefulWidget {
|
||||||
const RebootApplication({Key? key}) : super(key: key);
|
const RebootApplication({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:win32/win32.dart';
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getControllerJson(String name) async {
|
|
||||||
var folder = await _getWindowsPath(FOLDERID_Documents);
|
|
||||||
if(folder == null){
|
|
||||||
throw Exception("Missing documents folder");
|
|
||||||
}
|
|
||||||
|
|
||||||
var file = File("$folder\\$name.gs");
|
|
||||||
if(!file.existsSync()){
|
|
||||||
return HashMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonDecode(file.readAsStringSync());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> _getWindowsPath(String folderID) {
|
|
||||||
final Pointer<Pointer<Utf16>> pathPtrPtr = calloc<Pointer<Utf16>>();
|
|
||||||
final Pointer<GUID> knownFolderID = calloc<GUID>()..ref.setGUID(folderID);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final int hr = SHGetKnownFolderPath(
|
|
||||||
knownFolderID,
|
|
||||||
KF_FLAG_DEFAULT,
|
|
||||||
NULL,
|
|
||||||
pathPtrPtr,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
if (hr == E_INVALIDARG || hr == E_FAIL) {
|
|
||||||
throw WindowsException(hr);
|
|
||||||
}
|
|
||||||
return Future<String?>.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String path = pathPtrPtr.value.toDartString();
|
|
||||||
return Future<String>.value(path);
|
|
||||||
} finally {
|
|
||||||
calloc.free(pathPtrPtr);
|
|
||||||
calloc.free(knownFolderID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
|
|
||||||
Iterable<String> getServerTypes() => ServerType.values.map((entry) => entry.id);
|
|
||||||
|
|
||||||
String getDefaultServerType(Map<String, dynamic> json) {
|
|
||||||
var type = ServerType.values.elementAt(json["type"] ?? 0);
|
|
||||||
return type.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerType getServerType(ArgResults result) {
|
|
||||||
var type = ServerType.of(result["server-type"]);
|
|
||||||
if(type == null){
|
|
||||||
throw Exception("Unknown server type: $result. Use --server-type only with ${getServerTypes().join(", ")}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FortniteVersion> getVersions(Map<String, dynamic> gameJson) {
|
|
||||||
Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]");
|
|
||||||
return iterable.map((entry) => FortniteVersion.fromJson(entry))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:process_run/shell.dart';
|
|
||||||
import 'package:reboot_launcher/cli.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/injector.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/process.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
|
|
||||||
final List<String> _errorStrings = [
|
|
||||||
"port 3551 failed: Connection refused",
|
|
||||||
"Unable to login to Fortnite servers",
|
|
||||||
"HTTP 400 response from ",
|
|
||||||
"Network failure when attempting to check platform restrictions",
|
|
||||||
"UOnlineAccountCommon::ForceLogout"
|
|
||||||
];
|
|
||||||
|
|
||||||
Process? _gameProcess;
|
|
||||||
Process? _launcherProcess;
|
|
||||||
Process? _eacProcess;
|
|
||||||
|
|
||||||
Future<void> startGame() async {
|
|
||||||
await _startLauncherProcess(version);
|
|
||||||
await _startEacProcess(version);
|
|
||||||
|
|
||||||
var executable = await version.executable;
|
|
||||||
if (executable == null) {
|
|
||||||
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username == null) {
|
|
||||||
username = "Reboot${host ? 'Host' : 'Player'}";
|
|
||||||
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, ""))
|
|
||||||
..exitCode.then((_) => _onClose())
|
|
||||||
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.launcher == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
|
|
||||||
suspend(_launcherProcess!.pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.eacExecutable == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
|
||||||
suspend(_eacProcess!.pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
|
||||||
if(verbose) {
|
|
||||||
stdout.writeln(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(line.contains("Platform has ")){
|
|
||||||
_injectOrShowError("cobalt.dll");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) {
|
|
||||||
_onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_errorStrings.any((element) => line.contains(element))){
|
|
||||||
stderr.writeln("The backend doesn't work! Token expired");
|
|
||||||
_onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(line.contains("Region ")){
|
|
||||||
if(hosting) {
|
|
||||||
_injectOrShowError(dll, false);
|
|
||||||
}else {
|
|
||||||
_injectOrShowError("console.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
_injectOrShowError("memoryleak.dll");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _kill() {
|
|
||||||
_gameProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
_launcherProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
_eacProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
|
||||||
if (_gameProcess == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
stdout.writeln("Injecting $binary...");
|
|
||||||
var dll = locate ? File("${assetsDirectory.path}\\dlls\\$binary") : File(binary);
|
|
||||||
if(!dll.existsSync()){
|
|
||||||
throw Exception("Cannot inject $dll: missing file");
|
|
||||||
}
|
|
||||||
|
|
||||||
await injectDll(_gameProcess!.pid, dll.path);
|
|
||||||
} catch (exception) {
|
|
||||||
throw Exception("Cannot inject binary: $binary");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onClose() {
|
|
||||||
_kill();
|
|
||||||
sleep(const Duration(seconds: 3));
|
|
||||||
stdout.writeln("The game was closed");
|
|
||||||
if(autoRestart){
|
|
||||||
stdout.writeln("Restarting automatically game");
|
|
||||||
startGame();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
|
|
||||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
|
|
||||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
|
|
||||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memoryleak.dll";
|
|
||||||
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
|
|
||||||
|
|
||||||
Future<void> downloadRequiredDLLs() async {
|
|
||||||
stdout.writeln("Downloading necessary components...");
|
|
||||||
var consoleDll = File("${assetsDirectory.path}\\dlls\\console.dll");
|
|
||||||
if(!consoleDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_consoleDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download console.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll");
|
|
||||||
if(!craniumDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_baseDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download cobalt.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
|
|
||||||
if(!memoryFixDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_memoryFixDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download memoryleak.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!serverDirectory.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_embeddedConfigDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download embedded server config");
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
|
||||||
await tempZip.writeAsBytes(response.bodyBytes);
|
|
||||||
await extractFileToDisk(tempZip.path, serverDirectory.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:process_run/shell.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart' as server;
|
|
||||||
|
|
||||||
Future<bool> startServer(String? host, String? port, ServerType type) async {
|
|
||||||
stdout.writeln("Starting backend server...");
|
|
||||||
switch(type){
|
|
||||||
case ServerType.local:
|
|
||||||
var result = await server.ping(host ?? "127.0.0.1", port ?? "3551");
|
|
||||||
if(result == null){
|
|
||||||
throw Exception("Local backend server is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln("Detected local backend server");
|
|
||||||
return true;
|
|
||||||
case ServerType.embedded:
|
|
||||||
stdout.writeln("Starting an embedded server...");
|
|
||||||
await server.startServer(false);
|
|
||||||
var result = await server.ping(host ?? "127.0.0.1", port ?? "3551");
|
|
||||||
if(result == null){
|
|
||||||
throw Exception("Cannot start embedded server");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
case ServerType.remote:
|
|
||||||
if(host == null){
|
|
||||||
throw Exception("Missing host for remote server");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(port == null){
|
|
||||||
throw Exception("Missing host for remote server");
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln("Starting a reverse proxy to $host:$port");
|
|
||||||
return await _changeReverseProxyState(host, port) != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
|
|
||||||
host = host.trim();
|
|
||||||
if(host.isEmpty){
|
|
||||||
throw Exception("Missing host name");
|
|
||||||
}
|
|
||||||
|
|
||||||
port = port.trim();
|
|
||||||
if(port.isEmpty){
|
|
||||||
throw Exception("Missing port");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(int.tryParse(port) == null){
|
|
||||||
throw Exception("Invalid port, use only numbers");
|
|
||||||
}
|
|
||||||
|
|
||||||
try{
|
|
||||||
var uri = await server.ping(host, port);
|
|
||||||
if(uri == null){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await server.startRemoteServer(uri);
|
|
||||||
}catch(error){
|
|
||||||
throw Exception("Cannot start reverse proxy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void kill() async {
|
|
||||||
var shell = Shell(
|
|
||||||
commandVerbose: false,
|
|
||||||
commentVerbose: false,
|
|
||||||
verbose: false
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await shell.run("taskkill /f /im FortniteLauncher.exe");
|
|
||||||
await shell.run("taskkill /f /im FortniteClient-Win64-Shipping_EAC.exe");
|
|
||||||
}catch(_){
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
lib/src/controller/authenticator_controller.dart
Normal file
24
lib/src/controller/authenticator_controller.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
|
||||||
|
class AuthenticatorController extends ServerController {
|
||||||
|
AuthenticatorController() : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get controllerName => "authenticator";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storageName => "reboot_authenticator";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get defaultHost => kDefaultAuthenticatorHost;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get defaultPort => kDefaultAuthenticatorPort;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> get isPortFree => isAuthenticatorPortFree();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> freePort() => freeAuthenticatorPort();
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
class BuildController extends GetxController {
|
class BuildController extends GetxController {
|
||||||
List<FortniteBuild>? _builds;
|
List<FortniteBuild>? _builds;
|
||||||
Rxn<FortniteBuild> selectedBuildRx;
|
Rxn<FortniteBuild> selectedBuild;
|
||||||
|
|
||||||
BuildController() : selectedBuildRx = Rxn();
|
BuildController() : selectedBuild = Rxn();
|
||||||
|
|
||||||
List<FortniteBuild>? get builds => _builds;
|
List<FortniteBuild>? get builds => _builds;
|
||||||
|
|
||||||
@@ -14,6 +14,11 @@ class BuildController extends GetxController {
|
|||||||
if(builds == null || builds.isEmpty){
|
if(builds == null || builds.isEmpty){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedBuildRx.value = builds[0];
|
selectedBuild.value = builds[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset(){
|
||||||
|
_builds = null;
|
||||||
|
selectedBuild.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,25 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
|
||||||
const String kDefaultPlayerName = "Player";
|
|
||||||
|
|
||||||
class GameController extends GetxController {
|
class GameController extends GetxController {
|
||||||
late final String uuid;
|
late final String uuid;
|
||||||
late final GetStorage _storage;
|
late final GetStorage _storage;
|
||||||
late final TextEditingController username;
|
late final TextEditingController username;
|
||||||
late final TextEditingController password;
|
late final TextEditingController password;
|
||||||
late final RxBool showPassword;
|
|
||||||
late final TextEditingController customLaunchArgs;
|
late final TextEditingController customLaunchArgs;
|
||||||
late final Rx<List<FortniteVersion>> versions;
|
late final Rx<List<FortniteVersion>> versions;
|
||||||
late final Rxn<FortniteVersion> _selectedVersion;
|
late final Rxn<FortniteVersion> _selectedVersion;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool autoStartGameServer;
|
late final RxBool autoStartGameServer;
|
||||||
GameInstance? instance;
|
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||||
|
late final Rxn<GameInstance> instance;
|
||||||
|
|
||||||
GameController() {
|
GameController() {
|
||||||
_storage = GetStorage("reboot_game");
|
_storage = GetStorage("reboot_game");
|
||||||
@@ -33,8 +30,7 @@ class GameController extends GetxController {
|
|||||||
versions = Rx(decodedVersions);
|
versions = Rx(decodedVersions);
|
||||||
versions.listen((data) => _saveVersions());
|
versions.listen((data) => _saveVersions());
|
||||||
var decodedSelectedVersionName = _storage.read("version");
|
var decodedSelectedVersionName = _storage.read("version");
|
||||||
var decodedSelectedVersion = decodedVersions.firstWhereOrNull(
|
var decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName);
|
||||||
(element) => element.name == decodedSelectedVersionName);
|
|
||||||
uuid = _storage.read("uuid") ?? const Uuid().v4();
|
uuid = _storage.read("uuid") ?? const Uuid().v4();
|
||||||
_storage.write("uuid", uuid);
|
_storage.write("uuid", uuid);
|
||||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||||
@@ -42,12 +38,35 @@ class GameController extends GetxController {
|
|||||||
username.addListener(() => _storage.write("username", username.text));
|
username.addListener(() => _storage.write("username", username.text));
|
||||||
password = TextEditingController(text: _storage.read("password") ?? "");
|
password = TextEditingController(text: _storage.read("password") ?? "");
|
||||||
password.addListener(() => _storage.write("password", password.text));
|
password.addListener(() => _storage.write("password", password.text));
|
||||||
showPassword = RxBool(false);
|
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? "");
|
||||||
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? ""));
|
|
||||||
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
|
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
|
||||||
started = RxBool(false);
|
started = RxBool(false);
|
||||||
autoStartGameServer = RxBool(_storage.read("auto_game_server") ?? true);
|
autoStartGameServer = RxBool(_storage.read("auto_game_server") ?? true);
|
||||||
autoStartGameServer.listen((value) => _storage.write("auto_game_server", value));
|
autoStartGameServer.listen((value) => _storage.write("auto_game_server", value));
|
||||||
|
var supabase = Supabase.instance.client;
|
||||||
|
servers = Rxn();
|
||||||
|
supabase.from('hosts')
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.map((event) => event.where((element) => element["ip"] != null).toSet())
|
||||||
|
.listen((event) {
|
||||||
|
if(servers.value == null) {
|
||||||
|
servers.value = event;
|
||||||
|
}else {
|
||||||
|
servers.value?.addAll(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var serializedInstance = _storage.read("instance");
|
||||||
|
instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null);
|
||||||
|
instance.listen((value) => _storage.write("instance", jsonEncode(value?.toJson())));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
username.text = kDefaultPlayerName;
|
||||||
|
password.text = "";
|
||||||
|
customLaunchArgs.text = "";
|
||||||
|
versions.value = [];
|
||||||
|
autoStartGameServer.value = true;
|
||||||
|
instance.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteVersion? getVersionByName(String name) {
|
FortniteVersion? getVersionByName(String name) {
|
||||||
@@ -94,4 +113,13 @@ class GameController extends GetxController {
|
|||||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||||
versions.update((val) => function(version));
|
versions.update((val) => function(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? findServerById(String uuid) {
|
||||||
|
try {
|
||||||
|
print(uuid);
|
||||||
|
return servers.value?.firstWhere((element) => element["id"] == uuid);
|
||||||
|
} on StateError catch(_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
46
lib/src/controller/hosting_controller.dart
Normal file
46
lib/src/controller/hosting_controller.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
|
const String kDefaultServerName = "Reboot Game Server";
|
||||||
|
const String kDefaultDescription = "Just another server";
|
||||||
|
|
||||||
|
class HostingController extends GetxController {
|
||||||
|
late final GetStorage _storage;
|
||||||
|
late final TextEditingController name;
|
||||||
|
late final TextEditingController description;
|
||||||
|
late final TextEditingController password;
|
||||||
|
late final RxBool showPassword;
|
||||||
|
late final RxBool discoverable;
|
||||||
|
late final RxBool started;
|
||||||
|
late final Rxn<GameInstance> instance;
|
||||||
|
|
||||||
|
HostingController() {
|
||||||
|
_storage = GetStorage("reboot_hosting");
|
||||||
|
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
|
||||||
|
name.addListener(() => _storage.write("name", name.text));
|
||||||
|
description = TextEditingController(text: _storage.read("description") ?? kDefaultDescription);
|
||||||
|
description.addListener(() => _storage.write("description", description.text));
|
||||||
|
password = TextEditingController(text: _storage.read("password") ?? "");
|
||||||
|
password.addListener(() => _storage.write("password", password.text));
|
||||||
|
discoverable = RxBool(_storage.read("discoverable") ?? true);
|
||||||
|
discoverable.listen((value) => _storage.write("discoverable", value));
|
||||||
|
started = RxBool(false);
|
||||||
|
showPassword = RxBool(false);
|
||||||
|
var serializedInstance = _storage.read("instance");
|
||||||
|
instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null);
|
||||||
|
instance.listen((value) => _storage.write("instance", jsonEncode(value?.toJson())));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
name.text = kDefaultServerName;
|
||||||
|
description.text = kDefaultDescription;
|
||||||
|
showPassword.value = false;
|
||||||
|
discoverable.value = false;
|
||||||
|
started.value = false;
|
||||||
|
instance.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/src/controller/matchmaker_controller.dart
Normal file
30
lib/src/controller/matchmaker_controller.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
|
||||||
|
class MatchmakerController extends ServerController {
|
||||||
|
late final TextEditingController gameServerAddress;
|
||||||
|
|
||||||
|
MatchmakerController() : super() {
|
||||||
|
gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? kDefaultMatchmakerHost);
|
||||||
|
gameServerAddress.addListener(() => storage.write("game_server_address", gameServerAddress.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get controllerName => "matchmaker";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storageName => "reboot_matchmaker";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get defaultHost => kDefaultMatchmakerHost;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get defaultPort => kDefaultMatchmakerPort;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> get isPortFree => isMatchmakerPortFree();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> freePort() => freeMatchmakerPort();
|
||||||
|
}
|
||||||
189
lib/src/controller/server_controller.dart
Normal file
189
lib/src/controller/server_controller.dart
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:sync/semaphore.dart';
|
||||||
|
|
||||||
|
abstract class ServerController extends GetxController {
|
||||||
|
late final GetStorage storage;
|
||||||
|
late final TextEditingController host;
|
||||||
|
late final TextEditingController port;
|
||||||
|
late final Rx<ServerType> type;
|
||||||
|
late final Semaphore semaphore;
|
||||||
|
late RxBool started;
|
||||||
|
late RxBool detached;
|
||||||
|
Process? embeddedServer;
|
||||||
|
HttpServer? localServer;
|
||||||
|
HttpServer? remoteServer;
|
||||||
|
|
||||||
|
ServerController() {
|
||||||
|
storage = GetStorage(storageName);
|
||||||
|
started = RxBool(false);
|
||||||
|
type = Rx(ServerType.values.elementAt(storage.read("type") ?? 0));
|
||||||
|
type.listen((value) {
|
||||||
|
host.text = _readHost();
|
||||||
|
port.text = _readPort();
|
||||||
|
storage.write("type", value.index);
|
||||||
|
if (!started.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop();
|
||||||
|
});
|
||||||
|
host = TextEditingController(text: _readHost());
|
||||||
|
host.addListener(() =>
|
||||||
|
storage.write("${type.value.name}_host", host.text));
|
||||||
|
port = TextEditingController(text: _readPort());
|
||||||
|
port.addListener(() =>
|
||||||
|
storage.write("${type.value.name}_port", port.text));
|
||||||
|
detached = RxBool(storage.read("detached") ?? false);
|
||||||
|
detached.listen((value) => storage.write("detached", value));
|
||||||
|
semaphore = Semaphore();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get controllerName;
|
||||||
|
|
||||||
|
String get storageName;
|
||||||
|
|
||||||
|
String get defaultHost;
|
||||||
|
|
||||||
|
String get defaultPort;
|
||||||
|
|
||||||
|
Future<bool> get isPortFree;
|
||||||
|
|
||||||
|
Future<bool> get isPortTaken async => !(await isPortFree);
|
||||||
|
|
||||||
|
Future<bool> freePort();
|
||||||
|
|
||||||
|
void reset() async {
|
||||||
|
type.value = ServerType.values.elementAt(0);
|
||||||
|
for (var type in ServerType.values) {
|
||||||
|
storage.write("${type.name}_host", null);
|
||||||
|
storage.write("${type.name}_port", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
host.text = type.value != ServerType.remote ? defaultHost : "";
|
||||||
|
port.text = defaultPort;
|
||||||
|
detached.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _readHost() {
|
||||||
|
String? value = storage.read("${type.value.name}_host");
|
||||||
|
return value != null && value.isNotEmpty ? value
|
||||||
|
: type.value != ServerType.remote ? defaultHost : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String _readPort() =>
|
||||||
|
storage.read("${type.value.name}_port") ?? defaultPort;
|
||||||
|
|
||||||
|
Stream<ServerResult> start() async* {
|
||||||
|
try {
|
||||||
|
var host = this.host.text.trim();
|
||||||
|
if (host.isEmpty) {
|
||||||
|
yield ServerResult(ServerResultType.missingHostError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = this.port.text.trim();
|
||||||
|
if (port.isEmpty) {
|
||||||
|
yield ServerResult(ServerResultType.missingPortError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var portNumber = int.tryParse(port);
|
||||||
|
if (portNumber == null) {
|
||||||
|
yield ServerResult(ServerResultType.illegalPortError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type() != ServerType.local && await isPortTaken) {
|
||||||
|
yield ServerResult(ServerResultType.freeingPort);
|
||||||
|
var result = await freePort();
|
||||||
|
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
||||||
|
if(!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch(type()){
|
||||||
|
case ServerType.embedded:
|
||||||
|
embeddedServer = await startEmbeddedAuthenticator(detached());
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
yield ServerResult(ServerResultType.pingingRemote);
|
||||||
|
var uriResult = await ping(host, port);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.pingError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteServer = await startRemoteAuthenticatorProxy(uriResult);
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
if(port != defaultPort) {
|
||||||
|
localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$defaultPort"));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.pingingLocal);
|
||||||
|
var uriResult = await pingSelf(defaultPort);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.pingError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.startSuccess);
|
||||||
|
started.value = true;
|
||||||
|
}catch(error, stackTrace) {
|
||||||
|
yield ServerResult(
|
||||||
|
ServerResultType.startError,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stop() async {
|
||||||
|
started.value = false;
|
||||||
|
try{
|
||||||
|
switch(type()){
|
||||||
|
case ServerType.embedded:
|
||||||
|
freePort();
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
await remoteServer?.close(force: true);
|
||||||
|
remoteServer = null;
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
await localServer?.close(force: true);
|
||||||
|
localServer = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}catch(_){
|
||||||
|
started.value = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ServerResult> restart() async* {
|
||||||
|
await resetWinNat();
|
||||||
|
if(started()) {
|
||||||
|
await stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ServerResult> toggle() async* {
|
||||||
|
if(started()) {
|
||||||
|
await stop();
|
||||||
|
}else {
|
||||||
|
yield* start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,19 @@
|
|||||||
import 'dart:ui';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
|
||||||
|
|
||||||
class SettingsController extends GetxController {
|
class SettingsController extends GetxController {
|
||||||
static const String _kDefaultIp = "127.0.0.1";
|
static const String _kDefaultIp = "127.0.0.1";
|
||||||
static const bool _kDefaultAutoUpdate = true;
|
|
||||||
|
|
||||||
late final GetStorage _storage;
|
late final GetStorage _storage;
|
||||||
late final String originalDll;
|
late final String originalDll;
|
||||||
late final TextEditingController updateUrl;
|
|
||||||
late final TextEditingController rebootDll;
|
late final TextEditingController rebootDll;
|
||||||
late final TextEditingController consoleDll;
|
late final TextEditingController consoleDll;
|
||||||
late final TextEditingController authDll;
|
late final TextEditingController authDll;
|
||||||
late final TextEditingController matchmakingIp;
|
|
||||||
late final RxBool autoUpdate;
|
|
||||||
late final RxBool firstRun;
|
late final RxBool firstRun;
|
||||||
late final RxInt index;
|
|
||||||
late double width;
|
late double width;
|
||||||
late double height;
|
late double height;
|
||||||
late double? offsetX;
|
late double? offsetX;
|
||||||
@@ -31,27 +22,16 @@ class SettingsController extends GetxController {
|
|||||||
|
|
||||||
SettingsController() {
|
SettingsController() {
|
||||||
_storage = GetStorage("reboot_settings");
|
_storage = GetStorage("reboot_settings");
|
||||||
updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
|
|
||||||
updateUrl.addListener(() => _storage.write("update_url", updateUrl.text));
|
|
||||||
rebootDll = _createController("reboot", "reboot.dll");
|
rebootDll = _createController("reboot", "reboot.dll");
|
||||||
consoleDll = _createController("console", "console.dll");
|
consoleDll = _createController("console", "console.dll");
|
||||||
authDll = _createController("cobalt", "cobalt.dll");
|
authDll = _createController("cobalt", "cobalt.dll");
|
||||||
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? _kDefaultIp);
|
|
||||||
matchmakingIp.addListener(() async {
|
|
||||||
var text = matchmakingIp.text;
|
|
||||||
_storage.write("ip", text);
|
|
||||||
writeMatchmakingIp(text);
|
|
||||||
});
|
|
||||||
width = _storage.read("width") ?? kDefaultWindowWidth;
|
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||||
height = _storage.read("height") ?? kDefaultWindowHeight;
|
height = _storage.read("height") ?? kDefaultWindowHeight;
|
||||||
offsetX = _storage.read("offset_x");
|
offsetX = _storage.read("offset_x");
|
||||||
offsetY = _storage.read("offset_y");
|
offsetY = _storage.read("offset_y");
|
||||||
autoUpdate = RxBool(_storage.read("auto_update") ?? _kDefaultAutoUpdate);
|
|
||||||
autoUpdate.listen((value) => _storage.write("auto_update", value));
|
|
||||||
scrollingDistance = 0.0;
|
scrollingDistance = 0.0;
|
||||||
firstRun = RxBool(_storage.read("first_run") ?? true);
|
firstRun = RxBool(_storage.read("first_run") ?? true);
|
||||||
firstRun.listen((value) => _storage.write("first_run", value));
|
firstRun.listen((value) => _storage.write("first_run", value));
|
||||||
index = RxInt(firstRun() ? 3 : 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingController _createController(String key, String name) {
|
TextEditingController _createController(String key, String name) {
|
||||||
@@ -60,9 +40,10 @@ class SettingsController extends GetxController {
|
|||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveWindowSize() {
|
void saveWindowSize() async {
|
||||||
_storage.write("width", window.physicalSize.width);
|
var size = await windowManager.getSize();
|
||||||
_storage.write("height", window.physicalSize.height);
|
_storage.write("width", size.width);
|
||||||
|
_storage.write("height", size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveWindowOffset(Offset position) {
|
void saveWindowOffset(Offset position) {
|
||||||
@@ -71,13 +52,11 @@ class SettingsController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reset(){
|
void reset(){
|
||||||
updateUrl.text = rebootDownloadUrl;
|
|
||||||
rebootDll.text = _controllerDefaultPath("reboot.dll");
|
rebootDll.text = _controllerDefaultPath("reboot.dll");
|
||||||
consoleDll.text = _controllerDefaultPath("console.dll");
|
consoleDll.text = _controllerDefaultPath("console.dll");
|
||||||
authDll.text = _controllerDefaultPath("cobalt.dll");
|
authDll.text = _controllerDefaultPath("cobalt.dll");
|
||||||
matchmakingIp.text = _kDefaultIp;
|
firstRun.value = true;
|
||||||
writeMatchmakingIp(_kDefaultIp);
|
writeMatchmakingIp(_kDefaultIp);
|
||||||
autoUpdate.value = _kDefaultAutoUpdate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name";
|
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name";
|
||||||
47
lib/src/controller/update_controller.dart
Normal file
47
lib/src/controller/update_controller.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:reboot_common/common.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;
|
||||||
|
|
||||||
|
UpdateController() {
|
||||||
|
_storage = GetStorage("reboot_update");
|
||||||
|
timestamp = RxnInt(_storage.read("ts"));
|
||||||
|
timestamp.listen((value) => _storage.write("ts", value));
|
||||||
|
var timerIndex = _storage.read("timer");
|
||||||
|
timer = Rx(timerIndex == null ? UpdateTimer.never : UpdateTimer.values.elementAt(timerIndex));
|
||||||
|
timer.listen((value) => _storage.write("timer", value.index));
|
||||||
|
url = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
|
||||||
|
url.addListener(() => _storage.write("update_url", url.text));
|
||||||
|
status = Rx(UpdateStatus.waiting);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update() async {
|
||||||
|
if(timer.value == UpdateTimer.never) {
|
||||||
|
status.value = UpdateStatus.success;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
timestamp.value = await downloadRebootDll(url.text, timestamp.value);
|
||||||
|
status.value = UpdateStatus.success;
|
||||||
|
}catch(_) {
|
||||||
|
status.value = UpdateStatus.error;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
timestamp.value = null;
|
||||||
|
timer.value = UpdateTimer.never;
|
||||||
|
url.text = rebootDownloadUrl;
|
||||||
|
status.value = UpdateStatus.waiting;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||||
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
|
|
||||||
import 'dialog_button.dart';
|
import 'dialog_button.dart';
|
||||||
|
|
||||||
|
Future<T?> showDialog<T extends Object?>({required WidgetBuilder builder}) => fluent.showDialog(
|
||||||
|
context: pageKey.currentContext!,
|
||||||
|
useRootNavigator: false,
|
||||||
|
builder: builder
|
||||||
|
);
|
||||||
|
|
||||||
abstract class AbstractDialog extends StatelessWidget {
|
abstract class AbstractDialog extends StatelessWidget {
|
||||||
const AbstractDialog({Key? key}) : super(key: key);
|
const AbstractDialog({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -19,19 +27,13 @@ class GenericDialog extends AbstractDialog {
|
|||||||
const GenericDialog({super.key, required this.header, required this.buttons, this.padding});
|
const GenericDialog({super.key, required this.header, required this.buttons, this.padding});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => ContentDialog(
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
ContentDialog(
|
|
||||||
style: ContentDialogThemeData(
|
style: ContentDialogThemeData(
|
||||||
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
||||||
),
|
),
|
||||||
content: header,
|
content: header,
|
||||||
actions: buttons
|
actions: buttons
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormDialog extends AbstractDialog {
|
class FormDialog extends AbstractDialog {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
|
||||||
class DialogButton extends StatefulWidget {
|
class DialogButton extends StatefulWidget {
|
||||||
final String? text;
|
final String? text;
|
||||||
@@ -22,39 +22,30 @@ class DialogButton extends StatefulWidget {
|
|||||||
|
|
||||||
class _DialogButtonState extends State<DialogButton> {
|
class _DialogButtonState extends State<DialogButton> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||||
return widget.type == ButtonType.only ? _createOnlyButton() : _createButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
SizedBox _createOnlyButton() {
|
SizedBox get _onlyButton => SizedBox(
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: _createButton()
|
child: _button
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _createButton() {
|
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||||
return widget.type == ButtonType.primary ? _createPrimaryActionButton()
|
|
||||||
: _createSecondaryActionButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _createPrimaryActionButton() {
|
Widget get _primaryButton {
|
||||||
return FilledButton(
|
return Button(
|
||||||
onPressed: widget.onTap!,
|
onPressed: widget.onTap!,
|
||||||
child: Text(widget.text!),
|
child: Text(widget.text!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _createSecondaryActionButton() {
|
Widget get _secondaryButton {
|
||||||
return Button(
|
return Button(
|
||||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||||
child: Text(widget.text ?? "Close"),
|
child: Text(widget.text ?? "Close"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDefaultSecondaryActionTap() {
|
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||||
Navigator.of(context).pop(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ButtonType {
|
enum ButtonType {
|
||||||
38
lib/src/dialog/message.dart
Normal file
38
lib/src/dialog/message.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
|
import 'package:sync/semaphore.dart';
|
||||||
|
|
||||||
|
Semaphore _semaphore = Semaphore();
|
||||||
|
OverlayEntry? _lastOverlay;
|
||||||
|
|
||||||
|
void showMessage(String text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration}) {
|
||||||
|
try {
|
||||||
|
_semaphore.acquire();
|
||||||
|
if(_lastOverlay?.mounted == true) {
|
||||||
|
_lastOverlay?.remove();
|
||||||
|
}
|
||||||
|
var pageIndexValue = pageIndex.value;
|
||||||
|
_lastOverlay = showSnackbar(
|
||||||
|
pageKey.currentContext!,
|
||||||
|
InfoBar(
|
||||||
|
title: Text(text),
|
||||||
|
isLong: true,
|
||||||
|
isIconVisible: true,
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: loading ? const ProgressBar() : const SizedBox()
|
||||||
|
),
|
||||||
|
severity: severity
|
||||||
|
),
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: 330.0,
|
||||||
|
right: 16.0,
|
||||||
|
bottom: pageIndexValue == 0 || pageIndexValue == 1 || pageIndexValue == 3 || pageIndexValue == 4 ? 72 : 16
|
||||||
|
),
|
||||||
|
duration: duration
|
||||||
|
);
|
||||||
|
}finally {
|
||||||
|
_semaphore.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
|
||||||
|
|
||||||
String? lastError;
|
String? lastError;
|
||||||
@@ -11,7 +11,7 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(appKey.currentContext == null || appKey.currentState?.mounted == false){
|
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,13 +20,12 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastError = exception.toString();
|
lastError = exception.toString();
|
||||||
var route = ModalRoute.of(appKey.currentContext!);
|
var route = ModalRoute.of(pageKey.currentContext!);
|
||||||
if(route != null && !route.isCurrent){
|
if(route != null && !route.isCurrent){
|
||||||
Navigator.of(appKey.currentContext!).pop(false);
|
Navigator.of(pageKey.currentContext!).pop(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
exception: exception,
|
exception: exception,
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/main.dart';
|
import '../dialog/dialog.dart';
|
||||||
import 'dialog.dart';
|
|
||||||
|
|
||||||
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
|
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
|
||||||
"If you are unsure which version works best, use build 7.40. "
|
"If you are unsure which version works best, use build 7.40. "
|
||||||
"If you are a passionate programmer you can add support by opening a PR on Github. ";
|
"If you are a passionate programmer you can add support by opening a PR on Github. ";
|
||||||
|
|
||||||
const String _corruptedBuildError = "An unknown error happened while launching Fortnite. "
|
const String _corruptedBuildError = "An unknown occurred while launching Fortnite. "
|
||||||
"Some critical could be missing in your installation. "
|
"Some critical files could be missing in your installation. "
|
||||||
"Download the build again from the launcher, not locally, or from a different source. "
|
"Download the build again from the launcher, not locally, or from a different source. "
|
||||||
"Alternatively, something could have gone wrong in the launcher. ";
|
"Alternatively, something could have gone wrong in the launcher. ";
|
||||||
|
|
||||||
Future<void> showBrokenError() async {
|
Future<void> showBrokenError() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => const InfoDialog(
|
builder: (context) => const InfoDialog(
|
||||||
text: "The backend server is not working correctly"
|
text: "The backend server is not working correctly"
|
||||||
)
|
)
|
||||||
@@ -24,7 +21,6 @@ Future<void> showBrokenError() async {
|
|||||||
|
|
||||||
Future<void> showMissingDllError(String name) async {
|
Future<void> showMissingDllError(String name) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
text: "$name dll is not a valid dll, fix it in the settings tab"
|
text: "$name dll is not a valid dll, fix it in the settings tab"
|
||||||
)
|
)
|
||||||
@@ -33,7 +29,6 @@ Future<void> showMissingDllError(String name) async {
|
|||||||
|
|
||||||
Future<void> showTokenErrorFixable() async {
|
Future<void> showTokenErrorFixable() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => const InfoDialog(
|
builder: (context) => const InfoDialog(
|
||||||
text: "A token error occurred. "
|
text: "A token error occurred. "
|
||||||
"The backend server has been automatically restarted to fix the issue. "
|
"The backend server has been automatically restarted to fix the issue. "
|
||||||
@@ -44,7 +39,6 @@ Future<void> showTokenErrorFixable() async {
|
|||||||
|
|
||||||
Future<void> showTokenErrorCouldNotFix() async {
|
Future<void> showTokenErrorCouldNotFix() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => const InfoDialog(
|
builder: (context) => const InfoDialog(
|
||||||
text: "A token error occurred. "
|
text: "A token error occurred. "
|
||||||
"The game couldn't be recovered, open an issue on Discord."
|
"The game couldn't be recovered, open an issue on Discord."
|
||||||
@@ -54,7 +48,6 @@ Future<void> showTokenErrorCouldNotFix() async {
|
|||||||
|
|
||||||
Future<void> showTokenErrorUnfixable() async {
|
Future<void> showTokenErrorUnfixable() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => const InfoDialog(
|
builder: (context) => const InfoDialog(
|
||||||
text: "A token error occurred. "
|
text: "A token error occurred. "
|
||||||
"This issue cannot be resolved automatically as the server isn't embedded."
|
"This issue cannot be resolved automatically as the server isn't embedded."
|
||||||
@@ -67,7 +60,6 @@ Future<void> showTokenErrorUnfixable() async {
|
|||||||
Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async {
|
Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async {
|
||||||
if(error == null) {
|
if(error == null) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
text: server ? _unsupportedServerError : _corruptedBuildError
|
text: server ? _unsupportedServerError : _corruptedBuildError
|
||||||
)
|
)
|
||||||
@@ -76,7 +68,6 @@ Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? st
|
|||||||
}
|
}
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => ErrorDialog(
|
builder: (context) => ErrorDialog(
|
||||||
exception: error,
|
exception: error,
|
||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
@@ -87,7 +78,6 @@ Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? st
|
|||||||
|
|
||||||
Future<void> showMissingBuildError(FortniteVersion version) async {
|
Future<void> showMissingBuildError(FortniteVersion version) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
text: "${version.location.path} no longer contains a Fortnite executable. "
|
text: "${version.location.path} no longer contains a Fortnite executable. "
|
||||||
"This probably means that you deleted it or move it somewhere else."
|
"This probably means that you deleted it or move it somewhere else."
|
||||||
83
lib/src/interactive/profile.dart
Normal file
83
lib/src/interactive/profile.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
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/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.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 showDialog<bool?>(
|
||||||
|
builder: (context) => Obx(() => FormDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InfoLabel(
|
||||||
|
label: "Username/Email",
|
||||||
|
child: TextFormBox(
|
||||||
|
placeholder: "Type your username or email",
|
||||||
|
controller: _gameController.username,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
enableSuggestions: true,
|
||||||
|
autofocus: true,
|
||||||
|
autocorrect: false,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
InfoLabel(
|
||||||
|
label: "Password",
|
||||||
|
child: TextFormBox(
|
||||||
|
placeholder: "Type your password, if you have one",
|
||||||
|
controller: _gameController.password,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
obscureText: !showPassword.value,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
||||||
|
suffix: Button(
|
||||||
|
onPressed: () => showPassword.value = !showPassword.value,
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: ButtonState.all(const CircleBorder()),
|
||||||
|
backgroundColor: ButtonState.all(Colors.transparent)
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: showPasswordTrailing.value ? null : Colors.transparent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
text: "Cancel",
|
||||||
|
type: ButtonType.secondary
|
||||||
|
),
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text: "Save",
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
))
|
||||||
|
) ?? false;
|
||||||
|
if(result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameController.username.text = oldUsername;
|
||||||
|
_gameController.password.text = oldPassword;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
222
lib/src/interactive/server.dart
Normal file
222
lib/src/interactive/server.dart
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:flutter/material.dart' show Icons;
|
||||||
|
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||||
|
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||||
|
|
||||||
|
extension ServerControllerDialog on ServerController {
|
||||||
|
Future<bool> restartInteractive() async {
|
||||||
|
var stream = restart();
|
||||||
|
return await _handleStream(stream, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> toggleInteractive([bool showSuccessMessage = true]) async {
|
||||||
|
var stream = toggle();
|
||||||
|
return await _handleStream(stream, showSuccessMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<bool> _handleStream(Stream<ServerResult> stream, bool showSuccessMessage) async {
|
||||||
|
var completer = Completer<bool>();
|
||||||
|
stream.listen((event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case ServerResultType.missingHostError:
|
||||||
|
showMessage(
|
||||||
|
"Cannot launch game: missing hostname in $controllerName configuration",
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.missingPortError:
|
||||||
|
showMessage(
|
||||||
|
"Cannot launch game: missing port in $controllerName configuration",
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.illegalPortError:
|
||||||
|
showMessage(
|
||||||
|
"Cannot launch game: invalid port in $controllerName configuration",
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.freeingPort:
|
||||||
|
case ServerResultType.freePortSuccess:
|
||||||
|
case ServerResultType.freePortError:
|
||||||
|
showMessage(
|
||||||
|
"Message",
|
||||||
|
loading: event.type == ServerResultType.freeingPort,
|
||||||
|
severity: event.type == ServerResultType.freeingPort ? InfoBarSeverity.info : event.type == ServerResultType.freePortSuccess ? InfoBarSeverity.success : InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.pingingRemote:
|
||||||
|
showMessage(
|
||||||
|
"Pinging remote server...",
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: const Duration(seconds: 10)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.pingingLocal:
|
||||||
|
showMessage(
|
||||||
|
"Pinging ${type().name} server...",
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: const Duration(seconds: 10)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.pingError:
|
||||||
|
showMessage(
|
||||||
|
"Cannot ping ${type().name} server",
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ServerResultType.startSuccess:
|
||||||
|
if(showSuccessMessage) {
|
||||||
|
showMessage(
|
||||||
|
"The $controllerName was started successfully",
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
}
|
||||||
|
completer.complete(true);
|
||||||
|
break;
|
||||||
|
case ServerResultType.startError:
|
||||||
|
showMessage(
|
||||||
|
"An error occurred while starting the $controllerName: ${event.error ?? "unknown error"}",
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.type.isError) {
|
||||||
|
completer.complete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await completer.future;
|
||||||
|
if(result && type() == ServerType.embedded) {
|
||||||
|
watchProcess(embeddedServer!.pid).then((value) {
|
||||||
|
if(started()) {
|
||||||
|
pageIndex.value = 3;
|
||||||
|
started.value = false;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => showMessage(
|
||||||
|
"The $controllerName was terminated unexpectedly: if this wasn't intentional, file a bug report",
|
||||||
|
severity: InfoBarSeverity.warning,
|
||||||
|
duration: snackbarLongDuration
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MatchmakerControllerExtension on MatchmakerController {
|
||||||
|
Future<void> joinServer(Map<String, dynamic> entry) async {
|
||||||
|
var hashedPassword = entry["password"];
|
||||||
|
var hasPassword = hashedPassword != null;
|
||||||
|
var embedded = type.value == ServerType.embedded;
|
||||||
|
var author = entry["author"];
|
||||||
|
var encryptedIp = entry["ip"];
|
||||||
|
if(!hasPassword) {
|
||||||
|
_onSuccess(embedded, encryptedIp, author);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmPassword = await _askForPassword();
|
||||||
|
if(confirmPassword == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||||
|
showMessage(
|
||||||
|
"Wrong password: please try again",
|
||||||
|
duration: snackbarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
||||||
|
_onSuccess(embedded, decryptedIp, author);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<String?> _askForPassword() async {
|
||||||
|
var confirmPasswordController = TextEditingController();
|
||||||
|
var showPassword = RxBool(false);
|
||||||
|
var showPasswordTrailing = RxBool(false);
|
||||||
|
return await showDialog<String?>(
|
||||||
|
builder: (context) => FormDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InfoLabel(
|
||||||
|
label: "Password",
|
||||||
|
child: Obx(() => TextFormBox(
|
||||||
|
placeholder: "Type the server's password",
|
||||||
|
controller: confirmPasswordController,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
obscureText: !showPassword.value,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofocus: true,
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
||||||
|
suffix: Button(
|
||||||
|
onPressed: () => showPasswordTrailing.value = !showPasswordTrailing.value,
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: ButtonState.all(const CircleBorder()),
|
||||||
|
backgroundColor: ButtonState.all(Colors.transparent)
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: showPassword.value ? null : Colors.transparent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
text: "Cancel",
|
||||||
|
type: ButtonType.secondary
|
||||||
|
),
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text: "Confirm",
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSuccess(bool embedded, String decryptedIp, String author) {
|
||||||
|
if(embedded) {
|
||||||
|
gameServerAddress.text = decryptedIp;
|
||||||
|
pageIndex.value = 0;
|
||||||
|
}else {
|
||||||
|
FlutterClipboard.controlC(decryptedIp);
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => showMessage(
|
||||||
|
embedded ? "You joined $author's server successfully!" : "Copied IP to the clipboard",
|
||||||
|
duration: snackbarLongDuration,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import 'package:version/version.dart';
|
|
||||||
|
|
||||||
class FortniteBuild {
|
|
||||||
final Version version;
|
|
||||||
final String link;
|
|
||||||
|
|
||||||
FortniteBuild({required this.version, required this.link});
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
|
||||||
|
|
||||||
class FortniteVersion {
|
|
||||||
String name;
|
|
||||||
Directory location;
|
|
||||||
|
|
||||||
FortniteVersion.fromJson(json)
|
|
||||||
: name = json["name"],
|
|
||||||
location = Directory(json["location"]);
|
|
||||||
|
|
||||||
FortniteVersion({required this.name, required this.location});
|
|
||||||
|
|
||||||
static File? findExecutable(Directory directory, String name) {
|
|
||||||
try{
|
|
||||||
var result = directory.listSync(recursive: true)
|
|
||||||
.firstWhere((element) => path.basename(element.path) == name);
|
|
||||||
return File(result.path);
|
|
||||||
}catch(_){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<File?> get executable async {
|
|
||||||
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Reboot.exe");
|
|
||||||
if(result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
|
||||||
if(original == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Future.wait([
|
|
||||||
compute(patchMatchmaking, original),
|
|
||||||
compute(patchHeadless, original)
|
|
||||||
]);
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
File? get launcher {
|
|
||||||
return findExecutable(location, "FortniteLauncher.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
File? get eacExecutable {
|
|
||||||
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'name': name,
|
|
||||||
'location': location.path
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'FortniteVersion{name: $name, location: $location';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
class GameInstance {
|
|
||||||
final Process gameProcess;
|
|
||||||
final Process? launcherProcess;
|
|
||||||
final Process? eacProcess;
|
|
||||||
final int? watchDogProcessPid;
|
|
||||||
bool tokenError;
|
|
||||||
bool hasChildServer;
|
|
||||||
|
|
||||||
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.watchDogProcessPid, this.hasChildServer)
|
|
||||||
: tokenError = false;
|
|
||||||
|
|
||||||
void kill() {
|
|
||||||
gameProcess.kill(ProcessSignal.sigabrt);
|
|
||||||
launcherProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
eacProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
if(watchDogProcessPid != null){
|
|
||||||
Process.killPid(watchDogProcessPid!, ProcessSignal.sigabrt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
enum ServerType {
|
|
||||||
embedded,
|
|
||||||
remote,
|
|
||||||
local;
|
|
||||||
|
|
||||||
static ServerType? of(String id){
|
|
||||||
try {
|
|
||||||
return ServerType.values
|
|
||||||
.firstWhere((element) => element.id == id);
|
|
||||||
}catch(_){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get id {
|
|
||||||
return this == ServerType.embedded ? "embedded"
|
|
||||||
: this == ServerType.remote ? "remote"
|
|
||||||
: "local";
|
|
||||||
}
|
|
||||||
|
|
||||||
String get name {
|
|
||||||
return this == ServerType.embedded ? "Embedded (Lawin)"
|
|
||||||
: this == ServerType.remote ? "Remote"
|
|
||||||
: "Local";
|
|
||||||
}
|
|
||||||
|
|
||||||
String get message {
|
|
||||||
return this == ServerType.embedded ? "A server will be automatically started in the background"
|
|
||||||
: this == ServerType.remote ? "A reverse proxy to the remote server will be created"
|
|
||||||
: "Assumes that you are running yourself the server locally";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
enum UpdateStatus {
|
|
||||||
waiting,
|
|
||||||
started,
|
|
||||||
success,
|
|
||||||
error;
|
|
||||||
|
|
||||||
bool isDone() => this == UpdateStatus.success || this == UpdateStatus.error;
|
|
||||||
}
|
|
||||||
131
lib/src/page/authenticator_page.dart
Normal file
131
lib/src/page/authenticator_page.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/server/start_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/server/type_selector.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
|
||||||
|
class AuthenticatorPage extends StatefulWidget {
|
||||||
|
const AuthenticatorPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AuthenticatorPage> createState() => _AuthenticatorPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthenticatorPageState extends State<AuthenticatorPage> with AutomaticKeepAliveClientMixin {
|
||||||
|
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Obx(() => Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Authenticator configuration",
|
||||||
|
subtitle: "This section contains the authenticator's configuration",
|
||||||
|
content: const ServerTypeSelector(
|
||||||
|
authenticator: true
|
||||||
|
),
|
||||||
|
expandedContent: [
|
||||||
|
if(_authenticatorController.type.value == ServerType.remote)
|
||||||
|
SettingTile(
|
||||||
|
title: "Host",
|
||||||
|
subtitle: "The hostname of the authenticator",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Host",
|
||||||
|
controller: _authenticatorController.host,
|
||||||
|
readOnly: !_isRemote
|
||||||
|
)
|
||||||
|
),
|
||||||
|
if(_authenticatorController.type.value != ServerType.embedded)
|
||||||
|
SettingTile(
|
||||||
|
title: "Port",
|
||||||
|
subtitle: "The port of the authenticator",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Port",
|
||||||
|
controller: _authenticatorController.port,
|
||||||
|
readOnly: !_isRemote
|
||||||
|
)
|
||||||
|
),
|
||||||
|
if(_authenticatorController.type.value == ServerType.embedded)
|
||||||
|
SettingTile(
|
||||||
|
title: "Detached",
|
||||||
|
subtitle: "Whether the embedded authenticator should be started as a separate process, useful for debugging",
|
||||||
|
contentWidth: null,
|
||||||
|
isChild: true,
|
||||||
|
content: Obx(() => ToggleSwitch(
|
||||||
|
checked: _authenticatorController.detached(),
|
||||||
|
onChanged: (value) => _authenticatorController.detached.value = value
|
||||||
|
))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Installation directory",
|
||||||
|
subtitle: "Opens the folder where the embedded authenticator is located",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => launchUrl(authenticatorDirectory.uri),
|
||||||
|
child: const Text("Show Files")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Reset authenticator",
|
||||||
|
subtitle: "Resets the authenticator's settings to their default values",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => showDialog(
|
||||||
|
builder: (context) => InfoDialog(
|
||||||
|
text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
text: "Close",
|
||||||
|
),
|
||||||
|
DialogButton(
|
||||||
|
type: ButtonType.primary,
|
||||||
|
text: "Reset",
|
||||||
|
onTap: () {
|
||||||
|
_authenticatorController.reset();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
child: const Text("Reset"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
const ServerButton(
|
||||||
|
authenticator: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isRemote => _authenticatorController.type.value == ServerType.remote;
|
||||||
|
}
|
||||||
265
lib/src/page/browse_page.dart
Normal file
265
lib/src/page/browse_page.dart
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/interactive/server.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
import 'package:skeletons/skeletons.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
|
||||||
|
class BrowsePage extends StatefulWidget {
|
||||||
|
const BrowsePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BrowsePage> createState() => _BrowsePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMixin {
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
||||||
|
final TextEditingController _filterController = TextEditingController();
|
||||||
|
final StreamController<String> _filterControllerStream = StreamController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return FutureBuilder(
|
||||||
|
future: Future.delayed(const Duration(seconds: 1)), // Fake delay to show loading
|
||||||
|
builder: (context, futureSnapshot) => Obx(() {
|
||||||
|
var ready = futureSnapshot.connectionState == ConnectionState.done;
|
||||||
|
var data = _gameController.servers.value;
|
||||||
|
if(ready && data?.isEmpty == true) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"No servers are available right now",
|
||||||
|
style: FluentTheme.of(context).typography.titleLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Host a server yourself or come back later",
|
||||||
|
style: FluentTheme.of(context).typography.body
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSearchBar(ready),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<String?>(
|
||||||
|
stream: _filterControllerStream.stream,
|
||||||
|
builder: (context, filterSnapshot) {
|
||||||
|
var items = _getItems(data, filterSnapshot.data, ready);
|
||||||
|
var itemsCount = items != null ? items.length * 2 : null;
|
||||||
|
if(itemsCount == 0) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"No results found",
|
||||||
|
style: FluentTheme.of(context).typography.titleLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"No server matches your query",
|
||||||
|
style: FluentTheme.of(context).typography.body
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: itemsCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if(index % 2 != 0) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 8.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = _getItem(index ~/ 2, items);
|
||||||
|
if(!ready || entry == null) {
|
||||||
|
return const SettingTile(
|
||||||
|
content: SkeletonAvatar(
|
||||||
|
style: SkeletonAvatarStyle(
|
||||||
|
height: 32,
|
||||||
|
width: 64
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasPassword = entry["password"] != null;
|
||||||
|
return SettingTile(
|
||||||
|
title: "${_formatName(entry)} • ${entry["author"]}",
|
||||||
|
subtitle: "${_formatDescription(entry)} • ${_formatVersion(entry)}",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => _matchmakerController.joinServer(entry),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if(hasPassword)
|
||||||
|
const Icon(FluentIcons.lock),
|
||||||
|
if(hasPassword)
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Text(_matchmakerController.type.value == ServerType.embedded ? "Join Server" : "Copy IP"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Map<String, dynamic>>? _getItems(Set<Map<String, dynamic>>? data, String? filter, bool ready) {
|
||||||
|
if (!ready) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.where((entry) => _isValidItem(entry, filter)).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidItem(Map<String, dynamic> entry, String? filter) =>
|
||||||
|
(entry["discoverable"] ?? false) && (filter == null || _filterServer(entry, filter));
|
||||||
|
|
||||||
|
bool _filterServer(Map<String, dynamic> element, String filter) {
|
||||||
|
String? id = element["id"];
|
||||||
|
if(id?.toLowerCase().contains(filter) == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = Uri.tryParse(filter);
|
||||||
|
if(uri != null && id?.toLowerCase().contains(uri.host.toLowerCase()) == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? name = element["name"];
|
||||||
|
if(name?.toLowerCase().contains(filter) == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? author = element["author"];
|
||||||
|
if(author?.toLowerCase().contains(filter) == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? description = element["description"];
|
||||||
|
if(description?.toLowerCase().contains(filter) == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchBar(bool ready) {
|
||||||
|
if(ready) {
|
||||||
|
return TextBox(
|
||||||
|
placeholder: 'Find a server',
|
||||||
|
controller: _filterController,
|
||||||
|
onChanged: (value) => _filterControllerStream.add(value),
|
||||||
|
suffix: _searchBarIcon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SkeletonLine(
|
||||||
|
style: SkeletonLineStyle(
|
||||||
|
height: 32
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _searchBarIcon => Button(
|
||||||
|
onPressed: _filterController.text.isEmpty ? null : () {
|
||||||
|
_filterController.clear();
|
||||||
|
_filterControllerStream.add("");
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: _filterController.text.isNotEmpty ? null : ButtonState.all(Colors.transparent),
|
||||||
|
border: _filterController.text.isNotEmpty ? null : ButtonState.all(const BorderSide(color: Colors.transparent))
|
||||||
|
),
|
||||||
|
child: _searchBarIconData
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _searchBarIconData {
|
||||||
|
var color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||||
|
if (_filterController.text.isNotEmpty) {
|
||||||
|
return Icon(
|
||||||
|
FluentIcons.clear,
|
||||||
|
size: 8.0,
|
||||||
|
color: color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transform.flip(
|
||||||
|
flipX: true,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.search,
|
||||||
|
size: 12.0,
|
||||||
|
color: color
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? _getItem(int index, Set? data) {
|
||||||
|
if(data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= data.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.elementAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatName(Map<String, dynamic> entry) {
|
||||||
|
String result = entry['name'];
|
||||||
|
return result.isEmpty ? kDefaultServerName : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDescription(Map<String, dynamic> entry) {
|
||||||
|
String result = entry['description'];
|
||||||
|
return result.isEmpty ? kDefaultDescription : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatVersion(Map<String, dynamic> entry) {
|
||||||
|
var version = entry['version'];
|
||||||
|
var versionSplit = version.indexOf("-");
|
||||||
|
var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
|
||||||
|
String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion;
|
||||||
|
if(result.toLowerCase().startsWith("fortnite ")) {
|
||||||
|
result = result.substring(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Fortnite $result";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
253
lib/src/page/home_page.dart
Normal file
253
lib/src/page/home_page.dart
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/browse_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/authenticator_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/matchmaker_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/play_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/settings_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/home/pane.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/home/profile.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/os/border.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/os/title_bar.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'hosting_page.dart';
|
||||||
|
import 'info_page.dart';
|
||||||
|
|
||||||
|
const int pagesLength = 7;
|
||||||
|
final RxInt pageIndex = RxInt(0);
|
||||||
|
final Queue<int> _pagesStack = Queue();
|
||||||
|
final List<GlobalKey> _pageKeys = List.generate(pagesLength, (index) => GlobalKey());
|
||||||
|
GlobalKey get pageKey => _pageKeys[pageIndex.value];
|
||||||
|
|
||||||
|
class HomePage extends StatefulWidget {
|
||||||
|
const HomePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
|
static const double _kDefaultPadding = 12.0;
|
||||||
|
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
final GlobalKey _searchKey = GlobalKey();
|
||||||
|
final FocusNode _searchFocusNode = FocusNode();
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final RxBool _focused = RxBool(true);
|
||||||
|
final RxBool _fullScreen = RxBool(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
windowManager.show();
|
||||||
|
windowManager.addListener(this);
|
||||||
|
_searchController.addListener(_onSearch);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSearch() {
|
||||||
|
// TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
windowManager.removeListener(this);
|
||||||
|
_searchFocusNode.dispose();
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowEnterFullScreen() {
|
||||||
|
_fullScreen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowLeaveFullScreen() {
|
||||||
|
_fullScreen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowFocus() {
|
||||||
|
_focused.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowBlur() {
|
||||||
|
_focused.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowResized() {
|
||||||
|
_settingsController.saveWindowSize();
|
||||||
|
super.onWindowResized();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMoved() {
|
||||||
|
windowManager.getPosition()
|
||||||
|
.then((value) => _settingsController.saveWindowOffset(value));
|
||||||
|
super.onWindowMoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Obx(() => NavigationPaneTheme(
|
||||||
|
data: NavigationPaneThemeData(
|
||||||
|
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
child: NavigationView(
|
||||||
|
paneBodyBuilder: (pane, body) => Padding(
|
||||||
|
padding: const EdgeInsets.all(_kDefaultPadding),
|
||||||
|
child: SizedBox(
|
||||||
|
key: pageKey,
|
||||||
|
child: body
|
||||||
|
)
|
||||||
|
),
|
||||||
|
appBar: NavigationAppBar(
|
||||||
|
height: 32,
|
||||||
|
title: _draggableArea,
|
||||||
|
actions: WindowTitleBar(focused: _focused()),
|
||||||
|
leading: _backButton,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
),
|
||||||
|
pane: NavigationPane(
|
||||||
|
selected: pageIndex.value,
|
||||||
|
onChanged: (index) {
|
||||||
|
_pagesStack.add(pageIndex.value);
|
||||||
|
pageIndex.value = index;
|
||||||
|
},
|
||||||
|
menuButton: const SizedBox(),
|
||||||
|
displayMode: PaneDisplayMode.open,
|
||||||
|
items: _items,
|
||||||
|
header: const ProfileWidget(),
|
||||||
|
autoSuggestBox: _autoSuggestBox,
|
||||||
|
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
|
||||||
|
),
|
||||||
|
contentShape: const RoundedRectangleBorder(),
|
||||||
|
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||||
|
transitionBuilder: (child, animation) => child
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
if (isWin11)
|
||||||
|
Obx(() => !_fullScreen.value && _focused.value ? const WindowBorder() : const SizedBox())
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _backButton => Obx(() {
|
||||||
|
pageIndex.value;
|
||||||
|
return Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
|
||||||
|
backgroundColor: ButtonState.all(Colors.transparent),
|
||||||
|
border: ButtonState.all(const BorderSide(color: Colors.transparent))
|
||||||
|
),
|
||||||
|
onPressed: _pagesStack.isEmpty ? null : () => pageIndex.value = _pagesStack.removeLast(),
|
||||||
|
child: const Icon(FluentIcons.back, size: 12.0),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
GestureDetector get _draggableArea => GestureDetector(
|
||||||
|
onDoubleTap: () async => await windowManager.isMaximized() ? await windowManager.restore() : await windowManager.maximize(),
|
||||||
|
onHorizontalDragStart: (event) => windowManager.startDragging(),
|
||||||
|
onVerticalDragStart: (event) => windowManager.startDragging()
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _autoSuggestBox => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: TextBox(
|
||||||
|
key: _searchKey,
|
||||||
|
controller: _searchController,
|
||||||
|
placeholder: 'Find a setting',
|
||||||
|
focusNode: _searchFocusNode,
|
||||||
|
autofocus: true,
|
||||||
|
suffix: Button(
|
||||||
|
onPressed: null,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: ButtonState.all(Colors.transparent),
|
||||||
|
border: ButtonState.all(const BorderSide(color: Colors.transparent))
|
||||||
|
),
|
||||||
|
child: Transform.flip(
|
||||||
|
flipX: true,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.search,
|
||||||
|
size: 12.0,
|
||||||
|
color: FluentTheme.of(context).resources.textFillColorPrimary
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
List<NavigationPaneItem> get _items => [
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Play"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/play.png")
|
||||||
|
),
|
||||||
|
body: const PlayPage()
|
||||||
|
),
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Host"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/host.png")
|
||||||
|
),
|
||||||
|
body: const HostingPage()
|
||||||
|
),
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Server Browser"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/browse.png")
|
||||||
|
),
|
||||||
|
body: const BrowsePage()
|
||||||
|
),
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Authenticator"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/auth.png")
|
||||||
|
),
|
||||||
|
body: const AuthenticatorPage()
|
||||||
|
),
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Matchmaker"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/matchmaker.png")
|
||||||
|
),
|
||||||
|
body: const MatchmakerPage()
|
||||||
|
),
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Info"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/info.png")
|
||||||
|
),
|
||||||
|
body: const InfoPage()
|
||||||
|
),
|
||||||
|
RebootPaneItem(
|
||||||
|
title: const Text("Settings"),
|
||||||
|
icon: SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: Image.asset("assets/images/settings.png")
|
||||||
|
),
|
||||||
|
body: const SettingsPage()
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
String get searchValue => _searchController.text;
|
||||||
|
}
|
||||||
215
lib/src/page/hosting_page.dart
Normal file
215
lib/src/page/hosting_page.dart
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_launcher/main.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
import 'package:flutter/material.dart' show Icons;
|
||||||
|
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/game/start_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||||
|
|
||||||
|
class HostingPage extends StatefulWidget {
|
||||||
|
const HostingPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HostingPage> createState() => _HostingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
final UpdateController _updateController = Get.find<UpdateController>();
|
||||||
|
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Obx(() => Column(
|
||||||
|
children: _updateController.status.value != UpdateStatus.error ? [] : [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: _updateError
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
SettingTile(
|
||||||
|
title: "Game Server",
|
||||||
|
subtitle: "Provide basic information about your server",
|
||||||
|
expandedContent: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Name",
|
||||||
|
subtitle: "The name of your game server",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Name",
|
||||||
|
controller: _hostingController.name
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Description",
|
||||||
|
subtitle: "The description of your game server",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Description",
|
||||||
|
controller: _hostingController.description
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Password",
|
||||||
|
subtitle: "The password of your game server for the server browser",
|
||||||
|
isChild: true,
|
||||||
|
content: Obx(() => TextFormBox(
|
||||||
|
placeholder: "Password",
|
||||||
|
controller: _hostingController.password,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
obscureText: !_hostingController.showPassword.value,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (text) => _showPasswordTrailing.value = text.isNotEmpty,
|
||||||
|
suffix: Button(
|
||||||
|
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: ButtonState.all(const CircleBorder()),
|
||||||
|
backgroundColor: ButtonState.all(Colors.transparent)
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_hostingController.showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Discoverable",
|
||||||
|
subtitle: "Make your server available to other players on the server browser",
|
||||||
|
isChild: true,
|
||||||
|
contentWidth: null,
|
||||||
|
content: Obx(() => ToggleSwitch(
|
||||||
|
checked: _hostingController.discoverable(),
|
||||||
|
onChanged: (value) => _hostingController.discoverable.value = value
|
||||||
|
))
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
const SettingTile(
|
||||||
|
title: "Version",
|
||||||
|
subtitle: "Select the version of Fortnite you want to host",
|
||||||
|
content: VersionSelector(),
|
||||||
|
expandedContent: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Add a version from this PC's local storage",
|
||||||
|
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
||||||
|
content: Button(
|
||||||
|
onPressed: VersionSelector.openAddDialog,
|
||||||
|
child: Text("Add build"),
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Download any version from the cloud",
|
||||||
|
subtitle: "Download any Fortnite build easily from the cloud",
|
||||||
|
content: Button(
|
||||||
|
onPressed: VersionSelector.openDownloadDialog,
|
||||||
|
child: Text("Download"),
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Share",
|
||||||
|
subtitle: "Make it easy for other people to join your server with the options in this section",
|
||||||
|
expandedContent: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Link",
|
||||||
|
subtitle: "Copies a link for your server to the clipboard (requires the Reboot Launcher)",
|
||||||
|
isChild: true,
|
||||||
|
content: Button(
|
||||||
|
onPressed: () async {
|
||||||
|
FlutterClipboard.controlC("$kCustomUrlSchema://${_gameController.uuid}");
|
||||||
|
showMessage(
|
||||||
|
"Copied your link to the clipboard",
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text("Copy Link"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Public IP",
|
||||||
|
subtitle: "Copies your current public IP to the clipboard (doesn't require the Reboot Launcher)",
|
||||||
|
isChild: true,
|
||||||
|
content: Button(
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
showMessage(
|
||||||
|
"Obtaining your public IP...",
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
var ip = await Ipify.ipv4();
|
||||||
|
FlutterClipboard.controlC(ip);
|
||||||
|
showMessage(
|
||||||
|
"Copied your IP to the clipboard",
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
}catch(error) {
|
||||||
|
showMessage(
|
||||||
|
"An error occurred while obtaining your public IP: $error",
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: snackbarLongDuration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Copy IP"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
const LaunchButton(
|
||||||
|
host: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _updateError => MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _updateController.update,
|
||||||
|
child: const InfoBar(
|
||||||
|
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
|
||||||
|
severity: InfoBarSeverity.info
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
133
lib/src/page/info_page.dart
Normal file
133
lib/src/page/info_page.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
|
|
||||||
|
class InfoPage extends StatefulWidget {
|
||||||
|
const InfoPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InfoPage> createState() => _InfoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
late final ScrollController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance);
|
||||||
|
_controller.addListener(() {
|
||||||
|
_settingsController.scrollingDistance = _controller.offset;
|
||||||
|
});
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
SettingTile(
|
||||||
|
title: 'What is Project Reboot?',
|
||||||
|
subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. '
|
||||||
|
'The project was started on Discord by Milxnor. '
|
||||||
|
'The project is no longer being actively maintained.',
|
||||||
|
titleStyle: FluentTheme
|
||||||
|
.of(context)
|
||||||
|
.typography
|
||||||
|
.title,
|
||||||
|
contentWidth: null,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: 'What is a game server?',
|
||||||
|
subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. '
|
||||||
|
'You can join someone else\'s game server, or host one on your PC by going to the "Host" tab. ',
|
||||||
|
titleStyle: FluentTheme
|
||||||
|
.of(context)
|
||||||
|
.typography
|
||||||
|
.title,
|
||||||
|
contentWidth: null,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: 'What is a client?',
|
||||||
|
subtitle: 'A client is the actual Fortnite game. '
|
||||||
|
'You can download any version of Fortnite from the launcher in the "Play" tab. '
|
||||||
|
'You can also import versions from your local PC, but remember that these may be corrupted. '
|
||||||
|
'If a local version doesn\'t work, try installing it from the launcher before reporting a bug.',
|
||||||
|
titleStyle: FluentTheme
|
||||||
|
.of(context)
|
||||||
|
.typography
|
||||||
|
.title,
|
||||||
|
contentWidth: null,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: 'What is an authenticator?',
|
||||||
|
subtitle: 'An authenticator is a program that handles authentication, parties and voice chats. '
|
||||||
|
'By default, a LawinV1 server will be started for you to play. '
|
||||||
|
'You can use also use an authenticator running locally(on your PC) or remotely(on another PC). '
|
||||||
|
'Changing the authenticator settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! '
|
||||||
|
'If you need to restore these settings, go to the "Settings" tab and click on "Restore Defaults". ',
|
||||||
|
titleStyle: FluentTheme
|
||||||
|
.of(context)
|
||||||
|
.typography
|
||||||
|
.title,
|
||||||
|
contentWidth: null,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: 'Do I need to update DLLs?',
|
||||||
|
subtitle: 'No, all the files that the launcher uses are automatically updated. '
|
||||||
|
'You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. '
|
||||||
|
'Unless you are an advanced user, changing these options is not recommended',
|
||||||
|
titleStyle: FluentTheme
|
||||||
|
.of(context)
|
||||||
|
.typography
|
||||||
|
.title,
|
||||||
|
contentWidth: null,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: 'Where can I report bugs or ask for new features?',
|
||||||
|
subtitle: 'Go to the "Settings" tab and click on report bug. '
|
||||||
|
'Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement',
|
||||||
|
titleStyle: FluentTheme
|
||||||
|
.of(context)
|
||||||
|
.typography
|
||||||
|
.title,
|
||||||
|
contentWidth: null,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
lib/src/page/matchmaker_page.dart
Normal file
139
lib/src/page/matchmaker_page.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/server/type_selector.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/server/start_button.dart';
|
||||||
|
|
||||||
|
class MatchmakerPage extends StatefulWidget {
|
||||||
|
const MatchmakerPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MatchmakerPage> createState() => _MatchmakerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MatchmakerPageState extends State<MatchmakerPage> with AutomaticKeepAliveClientMixin {
|
||||||
|
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Obx(() => SettingTile(
|
||||||
|
title: "Matchmaker configuration",
|
||||||
|
subtitle: "This section contains the matchmaker's configuration",
|
||||||
|
content: const ServerTypeSelector(
|
||||||
|
authenticator: false
|
||||||
|
),
|
||||||
|
expandedContent: [
|
||||||
|
if(_matchmakerController.type.value == ServerType.remote)
|
||||||
|
SettingTile(
|
||||||
|
title: "Host",
|
||||||
|
subtitle: "The hostname of the matchmaker",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Host",
|
||||||
|
controller: _matchmakerController.host,
|
||||||
|
readOnly: _matchmakerController.type.value != ServerType.remote
|
||||||
|
)
|
||||||
|
),
|
||||||
|
if(_matchmakerController.type.value != ServerType.embedded)
|
||||||
|
SettingTile(
|
||||||
|
title: "Port",
|
||||||
|
subtitle: "The port of the matchmaker",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Port",
|
||||||
|
controller: _matchmakerController.port,
|
||||||
|
readOnly: _matchmakerController.type.value != ServerType.remote
|
||||||
|
)
|
||||||
|
),
|
||||||
|
if(_matchmakerController.type.value == ServerType.embedded)
|
||||||
|
SettingTile(
|
||||||
|
title: "Game server address",
|
||||||
|
subtitle: "The address of the game server used by the matchmaker",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Address",
|
||||||
|
controller: _matchmakerController.gameServerAddress
|
||||||
|
)
|
||||||
|
),
|
||||||
|
if(_matchmakerController.type.value == ServerType.embedded)
|
||||||
|
SettingTile(
|
||||||
|
title: "Detached",
|
||||||
|
subtitle: "Whether the embedded matchmaker should be started as a separate process, useful for debugging",
|
||||||
|
contentWidth: null,
|
||||||
|
isChild: true,
|
||||||
|
content: Obx(() => ToggleSwitch(
|
||||||
|
checked: _matchmakerController.detached.value,
|
||||||
|
onChanged: (value) => _matchmakerController.detached.value = value
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Installation directory",
|
||||||
|
subtitle: "Opens the folder where the embedded matchmaker is located",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => launchUrl(authenticatorDirectory.uri),
|
||||||
|
child: const Text("Show Files")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Reset matchmaker",
|
||||||
|
subtitle: "Resets the authenticator's settings to their default values",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => showDialog(
|
||||||
|
builder: (context) => InfoDialog(
|
||||||
|
text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
text: "Close",
|
||||||
|
),
|
||||||
|
DialogButton(
|
||||||
|
type: ButtonType.primary,
|
||||||
|
text: "Reset",
|
||||||
|
onTap: () {
|
||||||
|
_matchmakerController.reset();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
child: const Text("Reset"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
const ServerButton(
|
||||||
|
authenticator: false
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/src/page/play_page.dart
Normal file
109
lib/src/page/play_page.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/home_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/game/start_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PlayPage extends StatefulWidget {
|
||||||
|
const PlayPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlayPage> createState() => _PlayPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayPageState extends State<PlayPage> {
|
||||||
|
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
late final RxBool _selfServer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_selfServer = RxBool(_isLocalPlay);
|
||||||
|
_matchmakerController.gameServerAddress.addListener(() => _selfServer.value = _isLocalPlay);
|
||||||
|
_hostingController.started.listen((_) => _selfServer.value = _isLocalPlay);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isLocalPlay => isLocalHost(_matchmakerController.gameServerAddress.text)
|
||||||
|
&& !_hostingController.started.value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
const SettingTile(
|
||||||
|
title: "Version",
|
||||||
|
subtitle: "Select the version of Fortnite you want to host",
|
||||||
|
content: VersionSelector(),
|
||||||
|
expandedContent: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Add a version from this PC's local storage",
|
||||||
|
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
||||||
|
content: Button(
|
||||||
|
onPressed: VersionSelector.openAddDialog,
|
||||||
|
child: Text("Add build"),
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Download any version from the cloud",
|
||||||
|
subtitle: "Download any Fortnite build easily from the cloud",
|
||||||
|
content: Button(
|
||||||
|
onPressed: VersionSelector.openDownloadDialog,
|
||||||
|
child: Text("Download"),
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Game Server",
|
||||||
|
subtitle: "Helpful shortcuts to find the server where you want to play",
|
||||||
|
expandedContent: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Host a server",
|
||||||
|
subtitle: "Do you want to play with your friends? Host a server for them!",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => pageIndex.value = 1,
|
||||||
|
child: const Text("Host")
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Join a server",
|
||||||
|
subtitle: "Find a server where you can play on the launcher's server browser",
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => pageIndex.value = 2,
|
||||||
|
child: const Text("Browse")
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
const LaunchButton(
|
||||||
|
startLabel: 'Launch Fortnite',
|
||||||
|
stopLabel: 'Close Fortnite',
|
||||||
|
host: false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
lib/src/page/settings_page.dart
Normal file
182
lib/src/page/settings_page.dart
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
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/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/util/checks.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
|
||||||
|
|
||||||
|
class SettingsPage extends StatefulWidget {
|
||||||
|
const SettingsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin {
|
||||||
|
final BuildController _buildController = Get.find<BuildController>();
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
final UpdateController _updateController = Get.find<UpdateController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Client settings",
|
||||||
|
subtitle: "This section contains the dlls used to make the Fortnite client work",
|
||||||
|
expandedContent: [
|
||||||
|
_createFileSetting(
|
||||||
|
title: "Unreal engine console",
|
||||||
|
description: "This file is injected to unlock the Unreal Engine Console",
|
||||||
|
controller: _settingsController.consoleDll
|
||||||
|
),
|
||||||
|
_createFileSetting(
|
||||||
|
title: "Authentication patcher",
|
||||||
|
description: "This file is injected to redirect all HTTP requests to the launcher's authenticator",
|
||||||
|
controller: _settingsController.authDll
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Custom launch arguments",
|
||||||
|
subtitle: "Additional arguments to use when launching the game",
|
||||||
|
isChild: true,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "Arguments...",
|
||||||
|
controller: _gameController.customLaunchArgs,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Server settings",
|
||||||
|
subtitle: "This section contains settings related to the game server implementation",
|
||||||
|
expandedContent: [
|
||||||
|
_createFileSetting(
|
||||||
|
title: "Game server",
|
||||||
|
description: "This file is injected to create a game server & host matches",
|
||||||
|
controller: _settingsController.rebootDll
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Update mirror",
|
||||||
|
subtitle: "The URL used to update the game server dll",
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: "URL",
|
||||||
|
controller: _updateController.url,
|
||||||
|
validator: checkUpdateUrl
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Update timer",
|
||||||
|
subtitle: "Determines when the game server dll should be updated",
|
||||||
|
content: Obx(() => DropDownButton(
|
||||||
|
leading: Text(_updateController.timer.value.text),
|
||||||
|
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||||
|
text: Text(entry.text),
|
||||||
|
onPressed: () => _updateController.timer.value = entry
|
||||||
|
)).toList()
|
||||||
|
)),
|
||||||
|
isChild: true
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Launcher utilities",
|
||||||
|
subtitle: "This section contains handy settings for the launcher",
|
||||||
|
expandedContent: [
|
||||||
|
SettingTile(
|
||||||
|
title: "Installation directory",
|
||||||
|
subtitle: "Opens the installation directory",
|
||||||
|
isChild: true,
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => launchUrl(installationDirectory.uri),
|
||||||
|
child: const Text("Show Files"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Create a bug report",
|
||||||
|
subtitle: "Help me fix bugs by reporting them",
|
||||||
|
isChild: true,
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")),
|
||||||
|
child: const Text("Report a bug"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SettingTile(
|
||||||
|
title: "Reset settings",
|
||||||
|
subtitle: "Resets the launcher's settings to their default values",
|
||||||
|
isChild: true,
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => showDialog(
|
||||||
|
builder: (context) => InfoDialog(
|
||||||
|
text: "Do you want to reset all the launcher's settings to their default values? This action is irreversible",
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
text: "Close",
|
||||||
|
),
|
||||||
|
DialogButton(
|
||||||
|
type: ButtonType.primary,
|
||||||
|
text: "Reset",
|
||||||
|
onTap: () {
|
||||||
|
_buildController.reset();
|
||||||
|
_gameController.reset();
|
||||||
|
_hostingController.reset();
|
||||||
|
_authenticatorController.reset();
|
||||||
|
_settingsController.reset();
|
||||||
|
_updateController.reset();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
child: const Text("Reset"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
|
||||||
|
title: title,
|
||||||
|
subtitle: description,
|
||||||
|
content: FileSelector(
|
||||||
|
placeholder: "Path",
|
||||||
|
windowTitle: "Select a file",
|
||||||
|
controller: controller,
|
||||||
|
validator: checkDll,
|
||||||
|
extension: "dll",
|
||||||
|
folder: false
|
||||||
|
),
|
||||||
|
isChild: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _UpdateTimerExtension on UpdateTimer {
|
||||||
|
String get text => this == UpdateTimer.never ? "Never" : "Every $name";
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:get_storage/get_storage.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/update_controller.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/update_status.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
|
||||||
|
|
||||||
|
|
||||||
const String kDefaultServerName = "Reboot Game Server";
|
|
||||||
|
|
||||||
class HostingController extends GetxController {
|
|
||||||
late final GetStorage _storage;
|
|
||||||
late final TextEditingController name;
|
|
||||||
late final TextEditingController description;
|
|
||||||
late final RxBool discoverable;
|
|
||||||
late final RxBool started;
|
|
||||||
late final Rx<UpdateStatus> updateStatus;
|
|
||||||
GameInstance? instance;
|
|
||||||
|
|
||||||
HostingController() {
|
|
||||||
_storage = GetStorage("reboot_hosting");
|
|
||||||
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
|
|
||||||
name.addListener(() => _storage.write("name", name.text));
|
|
||||||
description = TextEditingController(text: _storage.read("description") ?? "");
|
|
||||||
description.addListener(() => _storage.write("description", description.text));
|
|
||||||
discoverable = RxBool(_storage.read("discoverable") ?? false);
|
|
||||||
discoverable.listen((value) => _storage.write("discoverable", value));
|
|
||||||
updateStatus = Rx(UpdateStatus.waiting);
|
|
||||||
started = RxBool(false);
|
|
||||||
startUpdater();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> startUpdater() async {
|
|
||||||
var settings = Get.find<SettingsController>();
|
|
||||||
if(!settings.autoUpdate()){
|
|
||||||
updateStatus.value = UpdateStatus.success;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus.value = UpdateStatus.started;
|
|
||||||
try {
|
|
||||||
updateTime = await downloadRebootDll(settings.updateUrl.text, updateTime);
|
|
||||||
updateStatus.value = UpdateStatus.success;
|
|
||||||
}catch(_) {
|
|
||||||
updateStatus.value = UpdateStatus.error;
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:get_storage/get_storage.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
|
|
||||||
class ServerController extends GetxController {
|
|
||||||
static const String _kDefaultServerHost = "127.0.0.1";
|
|
||||||
static const String _kDefaultServerPort = "3551";
|
|
||||||
|
|
||||||
late final GetStorage _storage;
|
|
||||||
late final TextEditingController host;
|
|
||||||
late final TextEditingController port;
|
|
||||||
late final Rx<ServerType> type;
|
|
||||||
late RxBool started;
|
|
||||||
late RxBool detached;
|
|
||||||
HttpServer? remoteServer;
|
|
||||||
|
|
||||||
ServerController() {
|
|
||||||
_storage = GetStorage("reboot_server");
|
|
||||||
started = RxBool(false);
|
|
||||||
type = Rx(ServerType.values.elementAt(_storage.read("type") ?? 0));
|
|
||||||
type.listen((value) {
|
|
||||||
host.text = _readHost();
|
|
||||||
port.text = _readPort();
|
|
||||||
_storage.write("type", value.index);
|
|
||||||
if(!started.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
|
||||||
});
|
|
||||||
host = TextEditingController(text: _readHost());
|
|
||||||
host.addListener(() => _storage.write("${type.value.id}_host", host.text));
|
|
||||||
port = TextEditingController(text: _readPort());
|
|
||||||
port.addListener(() => _storage.write("${type.value.id}_port", port.text));
|
|
||||||
detached = RxBool(_storage.read("detached") ?? false);
|
|
||||||
detached.listen((value) => _storage.write("detached", value));
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() async {
|
|
||||||
await stop();
|
|
||||||
type.value = ServerType.values.elementAt(0);
|
|
||||||
for(var type in ServerType.values){
|
|
||||||
_storage.write("${type.id}_host", null);
|
|
||||||
_storage.write("${type.id}_port", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
host.text = type.value != ServerType.remote ? _kDefaultServerHost : "";
|
|
||||||
port.text = _kDefaultServerPort;
|
|
||||||
detached.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _readHost() {
|
|
||||||
String? value = _storage.read("${type.value.id}_host");
|
|
||||||
return value != null && value.isNotEmpty ? value
|
|
||||||
: type.value != ServerType.remote ? _kDefaultServerHost : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String _readPort() {
|
|
||||||
return _storage.read("${type.value.id}_port") ?? _kDefaultServerPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> stop() async {
|
|
||||||
started.value = false;
|
|
||||||
try{
|
|
||||||
switch(type()){
|
|
||||||
case ServerType.embedded:
|
|
||||||
stopServer();
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
await remoteServer?.close(force: true);
|
|
||||||
remoteServer = null;
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}catch(_){
|
|
||||||
started.value = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import 'package:get_storage/get_storage.dart';
|
|
||||||
|
|
||||||
final GetStorage _storage = GetStorage("reboot_update");
|
|
||||||
|
|
||||||
int? get updateTime => _storage.read("last_update_v2");
|
|
||||||
set updateTime(int? updateTime) => _storage.write("last_update_v2", updateTime);
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/build.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/checks.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/build_selector.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_name_input.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart';
|
|
||||||
import 'dialog.dart';
|
|
||||||
import 'dialog_button.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 Rxn<String> _timeLeft = Rxn();
|
|
||||||
final Rxn<double> _downloadProgress = Rxn();
|
|
||||||
|
|
||||||
late DiskSpace _diskSpace;
|
|
||||||
late Future _fetchFuture;
|
|
||||||
late Future _diskFuture;
|
|
||||||
|
|
||||||
CancelableOperation? _manifestDownloadProcess;
|
|
||||||
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() {
|
|
||||||
if (_status.value != DownloadStatus.extracting && _status.value != DownloadStatus.extracting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_manifestDownloadProcess == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Process.run('${assetsDirectory.path}\\builds\\stop.bat', []);
|
|
||||||
_manifestDownloadProcess?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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: "Fetching builds and disks...",
|
|
||||||
onStop: () => Navigator.of(context).pop()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return FormDialog(
|
|
||||||
content: _createFormBody(),
|
|
||||||
buttons: _createFormButtons()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
case DownloadStatus.downloading:
|
|
||||||
return GenericDialog(
|
|
||||||
header: _createDownloadBody(),
|
|
||||||
buttons: _createCloseButton()
|
|
||||||
);
|
|
||||||
case DownloadStatus.extracting:
|
|
||||||
return GenericDialog(
|
|
||||||
header: _createExtractingBody(),
|
|
||||||
buttons: _createCloseButton()
|
|
||||||
);
|
|
||||||
case DownloadStatus.error:
|
|
||||||
return ErrorDialog(
|
|
||||||
exception: _error ?? Exception("unknown error"),
|
|
||||||
stackTrace: _stackTrace,
|
|
||||||
errorMessageBuilder: (exception) => "Cannot download version: $exception"
|
|
||||||
);
|
|
||||||
case DownloadStatus.done:
|
|
||||||
return const InfoDialog(
|
|
||||||
text: "The download was completed successfully!",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
List<DialogButton> _createFormButtons() {
|
|
||||||
return [
|
|
||||||
DialogButton(type: ButtonType.secondary),
|
|
||||||
DialogButton(
|
|
||||||
text: "Download",
|
|
||||||
type: ButtonType.primary,
|
|
||||||
onTap: () => _startDownload(context),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startDownload(BuildContext context) async {
|
|
||||||
try {
|
|
||||||
var build = _buildController.selectedBuildRx.value;
|
|
||||||
if(build == null){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_status.value = DownloadStatus.downloading;
|
|
||||||
var future = downloadArchiveBuild(
|
|
||||||
build.link,
|
|
||||||
Directory(_pathController.text),
|
|
||||||
(progress, eta) => _onDownloadProgress(progress, eta, false),
|
|
||||||
(progress, eta) => _onDownloadProgress(progress, eta, true),
|
|
||||||
);
|
|
||||||
future.then((value) => _onDownloadComplete());
|
|
||||||
future.onError((error, stackTrace) => _onDownloadError(error, stackTrace));
|
|
||||||
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
|
|
||||||
} catch (exception, stackTrace) {
|
|
||||||
_onDownloadError(exception, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDownloadComplete() async {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_status.value = DownloadStatus.done;
|
|
||||||
_gameController.addVersion(FortniteVersion(
|
|
||||||
name: _nameController.text,
|
|
||||||
location: Directory(_pathController.text)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_status.value = DownloadStatus.error;
|
|
||||||
_error = error;
|
|
||||||
_stackTrace = stackTrace;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onDownloadProgress(double? progress, String? timeLeft, bool extracting) {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
|
||||||
_timeLeft.value = timeLeft;
|
|
||||||
_downloadProgress.value = progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _createDownloadBody() => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
"Downloading...",
|
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"${(_downloadProgress.value ?? 0).round()}%",
|
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
|
||||||
),
|
|
||||||
|
|
||||||
if(_timeLeft.value != null)
|
|
||||||
Text(
|
|
||||||
"Time left: ${_timeLeft.value}",
|
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble())
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _createExtractingBody() => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
"Extracting...",
|
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ProgressBar()
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _createFormBody() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
BuildSelector(
|
|
||||||
onSelected: _updateFormDefaults
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 16.0
|
|
||||||
),
|
|
||||||
|
|
||||||
VersionNameInput(
|
|
||||||
controller: _nameController
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 16.0
|
|
||||||
),
|
|
||||||
|
|
||||||
FileSelector(
|
|
||||||
label: "Path",
|
|
||||||
placeholder: "Type the download destination",
|
|
||||||
windowTitle: "Select download destination",
|
|
||||||
controller: _pathController,
|
|
||||||
validator: checkDownloadDestination,
|
|
||||||
folder: true
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 16.0
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DialogButton> _createCloseButton() {
|
|
||||||
return [
|
|
||||||
DialogButton(
|
|
||||||
text: "Stop",
|
|
||||||
type: ButtonType.only
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateFormDefaults() async {
|
|
||||||
if(_diskSpace.disks.isEmpty){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _fetchFuture;
|
|
||||||
var bestDisk = _diskSpace.disks
|
|
||||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
|
||||||
var build = _buildController.selectedBuildRx.value;
|
|
||||||
if(build== null){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
|
|
||||||
"${build.version}";
|
|
||||||
_nameController.text = build.version.toString();
|
|
||||||
_formKey.currentState?.validate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
|
||||||
import 'package:sync/semaphore.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/main.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
|
||||||
import 'dialog.dart';
|
|
||||||
import 'dialog_button.dart';
|
|
||||||
|
|
||||||
extension ServerControllerDialog on ServerController {
|
|
||||||
static Semaphore semaphore = Semaphore();
|
|
||||||
|
|
||||||
Future<bool> restart(bool closeLocalPromptAutomatically) async {
|
|
||||||
await resetWinNat();
|
|
||||||
return (!started() || await stop()) && await toggle(closeLocalPromptAutomatically);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> toggle(bool closeLocalPromptAutomatically) async {
|
|
||||||
try{
|
|
||||||
semaphore.acquire();
|
|
||||||
if (type() == ServerType.local) {
|
|
||||||
return _pingSelfInteractive(closeLocalPromptAutomatically);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _toggle();
|
|
||||||
if(!result){
|
|
||||||
started.value = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ping = await _pingSelfInteractive(true);
|
|
||||||
if(!ping){
|
|
||||||
started.value = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}finally{
|
|
||||||
semaphore.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _toggle([ServerResultType? lastResultType]) async {
|
|
||||||
if (started.value) {
|
|
||||||
var result = await stop();
|
|
||||||
if (!result) {
|
|
||||||
started.value = true;
|
|
||||||
_showCannotStopError();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
started.value = true;
|
|
||||||
var conditions = await checkServerPreconditions(host.text, port.text, type.value);
|
|
||||||
var result = conditions.type == ServerResultType.canStart ? await _startServer() : conditions;
|
|
||||||
if(result.type == ServerResultType.alreadyStarted) {
|
|
||||||
started.value = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var handled = await _handleResultType(result, lastResultType);
|
|
||||||
if (!handled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ServerResult> _startServer() async {
|
|
||||||
try{
|
|
||||||
switch(type()){
|
|
||||||
case ServerType.embedded:
|
|
||||||
startServer(detached());
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
var uriResult = await _pingRemoteInteractive();
|
|
||||||
if(uriResult == null){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.cannotPingServer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteServer = await startRemoteServer(uriResult);
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}catch(error, stackTrace){
|
|
||||||
return ServerResult(
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace,
|
|
||||||
type: ServerResultType.unknownError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.canStart
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _handleResultType(ServerResult result, ServerResultType? lastResultType) async {
|
|
||||||
var newResultType = result.type;
|
|
||||||
switch (newResultType) {
|
|
||||||
case ServerResultType.missingHostError:
|
|
||||||
_showMissingHostError();
|
|
||||||
return false;
|
|
||||||
case ServerResultType.missingPortError:
|
|
||||||
_showMissingPortError();
|
|
||||||
return false;
|
|
||||||
case ServerResultType.illegalPortError:
|
|
||||||
_showIllegalPortError();
|
|
||||||
return false;
|
|
||||||
case ServerResultType.cannotPingServer:
|
|
||||||
return false;
|
|
||||||
case ServerResultType.backendPortTakenError:
|
|
||||||
if (lastResultType == ServerResultType.backendPortTakenError) {
|
|
||||||
_showPortTakenError(3551);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await freeLawinPort();
|
|
||||||
await stop();
|
|
||||||
return _toggle(newResultType);
|
|
||||||
case ServerResultType.matchmakerPortTakenError:
|
|
||||||
if (lastResultType == ServerResultType.matchmakerPortTakenError) {
|
|
||||||
_showPortTakenError(8080);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await freeMatchmakerPort();
|
|
||||||
await stop();
|
|
||||||
return _toggle(newResultType);
|
|
||||||
case ServerResultType.unknownError:
|
|
||||||
if(lastResultType == ServerResultType.unknownError) {
|
|
||||||
_showUnknownError(result);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await resetWinNat();
|
|
||||||
await stop();
|
|
||||||
return _toggle(newResultType);
|
|
||||||
case ServerResultType.alreadyStarted:
|
|
||||||
case ServerResultType.canStart:
|
|
||||||
return true;
|
|
||||||
case ServerResultType.stopped:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
|
|
||||||
try {
|
|
||||||
Future<bool> ping() async {
|
|
||||||
for(var i = 0; i < 3; i++){
|
|
||||||
var result = await pingSelf(port.text);
|
|
||||||
if(result != null){
|
|
||||||
return true;
|
|
||||||
}else {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var future = _waitFutureOrTime(ping());
|
|
||||||
var result = await showDialog<bool>(
|
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) =>
|
|
||||||
FutureBuilderDialog(
|
|
||||||
future: future,
|
|
||||||
loadingMessage: "Pinging ${type().id} server...",
|
|
||||||
successfulBody: FutureBuilderDialog.ofMessage(
|
|
||||||
"The ${type().id} server works correctly"),
|
|
||||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
|
||||||
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
|
||||||
errorMessageBuilder: (
|
|
||||||
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
|
||||||
closeAutomatically: closeAutomatically
|
|
||||||
)
|
|
||||||
) ?? false;
|
|
||||||
return result && await future;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> _pingRemoteInteractive() async {
|
|
||||||
try {
|
|
||||||
var future = ping(host.text, port.text);
|
|
||||||
await showDialog<bool>(
|
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) =>
|
|
||||||
FutureBuilderDialog(
|
|
||||||
future: future,
|
|
||||||
closeAutomatically: true,
|
|
||||||
loadingMessage: "Pinging remote server...",
|
|
||||||
successfulBody: FutureBuilderDialog.ofMessage(
|
|
||||||
"The server at ${host.text}:${port
|
|
||||||
.text} works correctly"),
|
|
||||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
|
||||||
"The server at ${host.text}:${port
|
|
||||||
.text} doesn't work. Check the hostname and/or the port and try again."),
|
|
||||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return await future;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showPortTakenError(int port) async {
|
|
||||||
showDialog(
|
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => InfoDialog(
|
|
||||||
text: "Port $port is already in use and the associating process cannot be killed. Kill it manually and try again.",
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showCannotStopError() {
|
|
||||||
if(!started.value){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDialog(
|
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) =>
|
|
||||||
const InfoDialog(
|
|
||||||
text: "Cannot stop backend server"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showUnexpectedServerError() => showDialog(
|
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) => InfoDialog(
|
|
||||||
text: "The backend server died unexpectedly",
|
|
||||||
buttons: [
|
|
||||||
DialogButton(
|
|
||||||
text: "Close",
|
|
||||||
type: ButtonType.secondary,
|
|
||||||
onTap: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
|
|
||||||
DialogButton(
|
|
||||||
text: "Open log",
|
|
||||||
type: ButtonType.primary,
|
|
||||||
onTap: () {
|
|
||||||
if(serverLogFile.existsSync()){
|
|
||||||
showMessage("No log is available");
|
|
||||||
}else {
|
|
||||||
launchUrl(serverLogFile.uri);
|
|
||||||
}
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
void _showIllegalPortError() => showMessage("Invalid port for backend server");
|
|
||||||
|
|
||||||
void _showMissingPortError() => showMessage("Missing port for backend server");
|
|
||||||
|
|
||||||
void _showMissingHostError() => showMessage("Missing the host name for backend server");
|
|
||||||
|
|
||||||
Future<Object?> _showUnknownError(ServerResult result) => showDialog(
|
|
||||||
context: appKey.currentContext!,
|
|
||||||
builder: (context) =>
|
|
||||||
ErrorDialog(
|
|
||||||
exception: result.error ?? Exception("Unknown error"),
|
|
||||||
stackTrace: result.stackTrace,
|
|
||||||
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) => Future.wait<bool>([resultFuture, Future.delayed(const Duration(seconds: 1)).then((value) => true)]).then((value) => value.reduce((f, s) => f && s));
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/main.dart';
|
|
||||||
|
|
||||||
void showMessage(String text){
|
|
||||||
showSnackbar(
|
|
||||||
appKey.currentContext!,
|
|
||||||
Snackbar(
|
|
||||||
content: Text(text, textAlign: TextAlign.center),
|
|
||||||
extended: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart';
|
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class BrowsePage extends StatefulWidget {
|
|
||||||
const BrowsePage(
|
|
||||||
{Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BrowsePage> createState() => _BrowsePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMixin {
|
|
||||||
Future? _query;
|
|
||||||
Stream<List<Map<String, dynamic>>>? _stream;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
if(_query != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_query = _stream != null ? Future.value(_stream) : _initStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initStream() async {
|
|
||||||
var supabase = Supabase.instance.client;
|
|
||||||
_stream = supabase.from('hosts')
|
|
||||||
.stream(primaryKey: ['id'])
|
|
||||||
.asBroadcastStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _query,
|
|
||||||
builder: (context, value) => StreamBuilder<List<Map<String, dynamic>>>(
|
|
||||||
stream: _stream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if(snapshot.hasError){
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
"Cannot fetch servers: ${snapshot.error}",
|
|
||||||
textAlign: TextAlign.center
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = snapshot.data;
|
|
||||||
if(data == null){
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Server Browser',
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: FluentTheme.of(context).typography.title
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 4.0
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'Looking for a match? This is the right place!',
|
|
||||||
textAlign: TextAlign.start
|
|
||||||
),
|
|
||||||
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: data.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
var version = data[index]['version'];
|
|
||||||
var versionSplit = version.indexOf("-");
|
|
||||||
version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
|
|
||||||
version = version.endsWith(".0") ? version.substring(0, version.length - 2) : version;
|
|
||||||
return SettingTile(
|
|
||||||
title: "${data[index]['name']} • Fortnite $version",
|
|
||||||
subtitle: data[index]['description'],
|
|
||||||
content: Button(
|
|
||||||
onPressed: () {},
|
|
||||||
child: const Text('Join'),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/main.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/page/launcher_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/page/server_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/page/settings_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/shared/profile_widget.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/os/window_border.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/os/window_title_bar.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
import 'hosting_page.dart';
|
|
||||||
import 'info_page.dart';
|
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
|
||||||
const HomePage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<HomePage> createState() => _HomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
|
||||||
static const double _kDefaultPadding = 12.0;
|
|
||||||
static const int _kPagesLength = 6;
|
|
||||||
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
final GlobalKey _searchKey = GlobalKey();
|
|
||||||
final FocusNode _searchFocusNode = FocusNode();
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
|
||||||
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
|
||||||
final RxBool _focused = RxBool(true);
|
|
||||||
final List<GlobalKey<NavigatorState>> _navigators = List.generate(_kPagesLength, (index) => GlobalKey());
|
|
||||||
final List<RxInt> _navigationStatus = List.generate(_kPagesLength, (index) => RxInt(0));
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
windowManager.show();
|
|
||||||
windowManager.addListener(this);
|
|
||||||
_searchController.addListener(_onSearch);
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSearch() {
|
|
||||||
if (searchValue.isEmpty) {
|
|
||||||
_searchItems.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_searchItems.value = _allItems
|
|
||||||
.whereType<PaneItem>()
|
|
||||||
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
|
|
||||||
.toList()
|
|
||||||
.cast<NavigationPaneItem>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
windowManager.removeListener(this);
|
|
||||||
_searchFocusNode.dispose();
|
|
||||||
_searchController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowFocus() {
|
|
||||||
_focused.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowBlur() {
|
|
||||||
_focused.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowResized() {
|
|
||||||
_settingsController.saveWindowSize();
|
|
||||||
super.onWindowResized();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onWindowMoved() {
|
|
||||||
windowManager.getPosition()
|
|
||||||
.then((value) => _settingsController.saveWindowOffset(value));
|
|
||||||
super.onWindowMoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Stack(children: [
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, specs) => Obx(() => NavigationPaneTheme(
|
|
||||||
data: NavigationPaneThemeData(
|
|
||||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.9),
|
|
||||||
),
|
|
||||||
child: NavigationView(
|
|
||||||
paneBodyBuilder: (pane, body) => Padding(
|
|
||||||
padding: const EdgeInsets.all(_kDefaultPadding),
|
|
||||||
child: body
|
|
||||||
),
|
|
||||||
appBar: NavigationAppBar(
|
|
||||||
height: 32,
|
|
||||||
title: _draggableArea,
|
|
||||||
actions: WindowTitleBar(focused: _focused()),
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
leading: _backButton
|
|
||||||
),
|
|
||||||
pane: NavigationPane(
|
|
||||||
key: appKey,
|
|
||||||
selected: _selectedIndex,
|
|
||||||
onChanged: _onIndexChanged,
|
|
||||||
menuButton: const SizedBox(),
|
|
||||||
displayMode: PaneDisplayMode.open,
|
|
||||||
items: _items,
|
|
||||||
header: ProfileWidget(),
|
|
||||||
footerItems: _footerItems,
|
|
||||||
autoSuggestBox: _autoSuggestBox,
|
|
||||||
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
|
|
||||||
),
|
|
||||||
contentShape: const RoundedRectangleBorder(),
|
|
||||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
|
||||||
transitionBuilder: (child, animation) => child),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
if (isWin11)
|
|
||||||
Obx(() => _focused.value ? const WindowBorder() : const SizedBox())
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
GestureDetector get _draggableArea => GestureDetector(
|
|
||||||
onDoubleTap: () async => await windowManager.isMaximized() ? await windowManager.restore() : await windowManager.maximize(),
|
|
||||||
onHorizontalDragStart: (event) => windowManager.startDragging(),
|
|
||||||
onVerticalDragStart: (event) => windowManager.startDragging()
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _backButton => Obx(() {
|
|
||||||
for (var entry in _navigationStatus) {
|
|
||||||
entry.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var onBack = _onBack();
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Button(
|
|
||||||
onPressed: onBack,
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: ButtonState.all(Colors.transparent),
|
|
||||||
border: ButtonState.all(BorderSide(color: Colors.transparent))
|
|
||||||
),
|
|
||||||
child: const Icon(FluentIcons.back, size: 13.0)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Function()? _onBack() {
|
|
||||||
var navigator = _navigators[_settingsController.index.value].currentState;
|
|
||||||
if (navigator == null || !navigator.mounted || !navigator.canPop()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var status = _navigationStatus[_settingsController.index.value];
|
|
||||||
if (status.value <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () async {
|
|
||||||
Navigator.pop(navigator.context);
|
|
||||||
status.value -= 1;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onIndexChanged(int index) {
|
|
||||||
_navigationStatus[_settingsController.index()].value = 0;
|
|
||||||
_settingsController.index.value = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget get _autoSuggestBox => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: TextBox(
|
|
||||||
key: _searchKey,
|
|
||||||
controller: _searchController,
|
|
||||||
placeholder: 'Find a setting',
|
|
||||||
focusNode: _searchFocusNode,
|
|
||||||
autofocus: true
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
int? get _selectedIndex {
|
|
||||||
var searchItems = _searchItems();
|
|
||||||
if (searchItems == null) {
|
|
||||||
return _settingsController.index();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_settingsController.index() >= _allItems.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOnScreen =
|
|
||||||
searchItems.indexOf(_allItems[_settingsController.index()]);
|
|
||||||
if (indexOnScreen.isNegative) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexOnScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<NavigationPaneItem> get _allItems => [..._items, ..._footerItems];
|
|
||||||
|
|
||||||
List<NavigationPaneItem> get _footerItems => searchValue.isNotEmpty ? [] : [
|
|
||||||
PaneItem(
|
|
||||||
title: const Text("Downloads"),
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: Image.asset("assets/images/download.png")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
body: const SettingsPage()
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
title: const Text("Settings"),
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: Image.asset("assets/images/settings.png")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
body: const SettingsPage()
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
List<NavigationPaneItem> get _items => _searchItems() ?? [
|
|
||||||
PaneItem(
|
|
||||||
title: const Text("Play"),
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: Image.asset("assets/images/play.png")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
body: LauncherPage(_navigators[0], _navigationStatus[0])
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
title: const Text("Host"),
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: Image.asset("assets/images/host.png")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
body: HostingPage(_navigators[1], _navigationStatus[1])
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
title: const Text("Authenticator"),
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: Image.asset("assets/images/cloud.png")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
body: ServerPage(_navigators[2], _navigationStatus[2])
|
|
||||||
),
|
|
||||||
PaneItem(
|
|
||||||
title: const Text("Tutorial"),
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: Image.asset("assets/images/info.png")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
body: InfoPage(_navigators[3], _navigationStatus[3])
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
String get searchValue => _searchController.text;
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/model/update_status.dart';
|
|
||||||
import 'browse_page.dart';
|
|
||||||
|
|
||||||
class HostingPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<HostingPage> createState() => _HostingPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
|
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Obx(() => !_settingsController.autoUpdate() || _hostingController.updateStatus().isDone() ? _body : _updateScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget get _updateScreen => const Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
ProgressRing(),
|
|
||||||
SizedBox(height: 8.0),
|
|
||||||
Text("Updating Reboot DLL...")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _body => Navigator(
|
|
||||||
key: widget.navigatorKey,
|
|
||||||
initialRoute: "home",
|
|
||||||
onGenerateRoute: (settings) {
|
|
||||||
var screen = _createScreen(settings.name);
|
|
||||||
return FluentPageRoute(
|
|
||||||
builder: (context) => screen,
|
|
||||||
settings: settings
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _createScreen(String? name) {
|
|
||||||
switch(name){
|
|
||||||
case "home":
|
|
||||||
return _HostPage(widget.navigatorKey, widget.nestedNavigation);
|
|
||||||
case "browse":
|
|
||||||
return const BrowsePage();
|
|
||||||
default:
|
|
||||||
throw Exception("Unknown page: $name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HostPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const _HostPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_HostPage> createState() => _HostPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HostPageState extends State<_HostPage> with AutomaticKeepAliveClientMixin {
|
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Obx(() => SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: _hostingController.updateStatus.value == UpdateStatus.error ? _updateError : _rebootGuiInfo,
|
|
||||||
)),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Game Server",
|
|
||||||
subtitle: "Provide basic information about your server",
|
|
||||||
expandedContentSpacing: 0,
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Name",
|
|
||||||
subtitle: "The name of your game server",
|
|
||||||
isChild: true,
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "Name",
|
|
||||||
controller: _hostingController.name
|
|
||||||
)
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Description",
|
|
||||||
subtitle: "The description of your game server",
|
|
||||||
isChild: true,
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "Description",
|
|
||||||
controller: _hostingController.description
|
|
||||||
)
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Discoverable",
|
|
||||||
subtitle: "Make your server available to other players on the server browser",
|
|
||||||
isChild: true,
|
|
||||||
contentWidth: null,
|
|
||||||
content: Obx(() => ToggleSwitch(
|
|
||||||
checked: _hostingController.discoverable(),
|
|
||||||
onChanged: (value) => _hostingController.discoverable.value = value
|
|
||||||
))
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Version",
|
|
||||||
subtitle: "Select the version of Fortnite you want to host",
|
|
||||||
content: const VersionSelector(),
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Add a version from this PC's local storage",
|
|
||||||
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => VersionSelector.openAddDialog(context),
|
|
||||||
child: const Text("Add build"),
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Download any version from the cloud",
|
|
||||||
subtitle: "A curated list of supported versions by Project Reboot",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => VersionSelector.openDownloadDialog(context),
|
|
||||||
child: const Text("Download"),
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Browse available servers",
|
|
||||||
subtitle: "See a list of other game servers that are being hosted",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () {
|
|
||||||
widget.navigatorKey.currentState?.pushNamed('browse');
|
|
||||||
widget.nestedNavigation.value += 1;
|
|
||||||
},
|
|
||||||
child: const Text("Browse")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
const LaunchButton(
|
|
||||||
host: true
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoBar get _rebootGuiInfo => const InfoBar(
|
|
||||||
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
|
|
||||||
severity: InfoBarSeverity.info
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _updateError => MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: _hostingController.startUpdater,
|
|
||||||
child: const InfoBar(
|
|
||||||
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
|
|
||||||
severity: InfoBarSeverity.info
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/checks.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
|
||||||
|
|
||||||
class InfoPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<InfoPage> createState() => _InfoPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
late final ScrollController _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance);
|
|
||||||
_controller.addListener(() {
|
|
||||||
_settingsController.scrollingDistance = _controller.offset;
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Navigator(
|
|
||||||
key: widget.navigatorKey,
|
|
||||||
initialRoute: "introduction",
|
|
||||||
onGenerateRoute: (settings) {
|
|
||||||
var screen = _createScreen(settings.name);
|
|
||||||
return FluentPageRoute(
|
|
||||||
builder: (context) => screen,
|
|
||||||
settings: settings
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _createScreen(String? name) {
|
|
||||||
switch(name){
|
|
||||||
case "introduction":
|
|
||||||
return _IntroductionPage(widget.navigatorKey, widget.nestedNavigation);
|
|
||||||
case "play":
|
|
||||||
return _PlayPage(widget.navigatorKey, widget.nestedNavigation);
|
|
||||||
default:
|
|
||||||
throw Exception("Unknown page: $name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _IntroductionPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const _IntroductionPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_IntroductionPage> createState() => _IntroductionPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _IntroductionPageState extends State<_IntroductionPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
SettingTile(
|
|
||||||
title: 'What is Project Reboot?',
|
|
||||||
subtitle: 'Project Reboot allows anyone to easily host a game server for most of Fortnite\'s seasons. The project was started on Discord by Milxnor and it\'s still being actively developed. Also, it\'s open source on Github!',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
contentWidth: null,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: 'What is a game server?',
|
|
||||||
subtitle: 'When you join a Fortnite Game, your client connects to a game server that allows you to play with others. You can join someone else\'s game server, or host one on your PC. You can host your own game server by going to the "Host" tab. Just like in Minecraft, a client needs to connect to a server hosted on a certain IP or domain. In short, remember that you need both a client and a server to play!',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
contentWidth: null,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: 'What is a client?',
|
|
||||||
subtitle: 'A client is the actual Fortnite game. You can download any version of Fortnite from the launcher in the "Play" tab. You can also import versions from your local PC, but remember that these may be corrupted. If a local version doesn\'t work, try installing it from the launcher before reporting a bug.',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
contentWidth: null,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: 'What is a backend?',
|
|
||||||
subtitle: 'A backend is the program that handles authentication, parties and voice chats. By default, a LawinV1 server will be started for you to play. You can use also use a backend running locally, that is on your PC, or remotely, that is on another PC. Changing the backend settings can break the client and game server: unless you are an advanced user, do not edit, for any reason, these settings! If you need to restore these settings, click on "Restore Defaults" in the "Backend" tab.',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
contentWidth: null,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: 'Do I need to update DLLs?',
|
|
||||||
subtitle: 'No, all the files that the launcher uses are automatically updated. Though, you need to update the launcher yourself if you haven\'t downloaded it from the Microsoft Store. You can use your own DLLs by going to the "Settings" tab, but make sure that they don\'t create a console that reads IO or the launcher will stop working correctly. Unless you are an advanced user, changing these options is not recommended',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
contentWidth: null,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: 'Where can I report bugs or ask for new features?',
|
|
||||||
subtitle: 'Go to the "Settings" tab and click on report bug. Please make sure to be as specific as possible when filing a report as it\'s crucial to make it as easy to fix/implement',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
contentWidth: null,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48,
|
|
||||||
child: Button(
|
|
||||||
child: const Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text("How do I play?")
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
widget.navigatorKey.currentState?.pushNamed("play");
|
|
||||||
widget.nestedNavigation.value += 1;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PlayPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const _PlayPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_PlayPage> createState() => _PlayPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PlayPageState extends State<_PlayPage> {
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
final RxBool _localGameServer = RxBool(true);
|
|
||||||
final TextEditingController _remoteGameServerController = TextEditingController();
|
|
||||||
final StreamController _remoteGameServerStream = StreamController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
var ip = _settingsController.matchmakingIp.text;
|
|
||||||
_remoteGameServerController.text = isLocalHost(ip) ? "" : ip;
|
|
||||||
_remoteGameServerController.addListener(() => _remoteGameServerStream.add(null));
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
SettingTile(
|
|
||||||
title: '1. Select a username',
|
|
||||||
subtitle: 'Choose a name for other players to see while you are in-game',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
expandedContentHeaderHeight: 80,
|
|
||||||
contentWidth: 0,
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Username",
|
|
||||||
subtitle: "The username that other players will see when you are in game",
|
|
||||||
isChild: true,
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "Username",
|
|
||||||
controller: _gameController.username,
|
|
||||||
autovalidateMode: AutovalidateMode.always
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: '2. Download Fortnite',
|
|
||||||
subtitle: 'Download or import the version of Fortnite you want to play. Make sure that it\'s the same as the game server\'s you want to join!',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
expandedContentHeaderHeight: 80,
|
|
||||||
contentWidth: 0,
|
|
||||||
expandedContent: [
|
|
||||||
const SettingTile(
|
|
||||||
title: "Version",
|
|
||||||
subtitle: "Select the version of Fortnite you want to play",
|
|
||||||
content: VersionSelector(),
|
|
||||||
isChild: true,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Add a version from this PC's local storage",
|
|
||||||
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
|
||||||
isChild: true,
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => VersionSelector.openAddDialog(context),
|
|
||||||
child: const Text("Add build"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Download any version from the cloud",
|
|
||||||
subtitle: "A curated list of supported versions by Project Reboot",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => VersionSelector.openDownloadDialog(context),
|
|
||||||
child: const Text("Download"),
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
StreamBuilder(
|
|
||||||
stream: _remoteGameServerStream.stream,
|
|
||||||
builder: (context, snapshot) => SettingTile(
|
|
||||||
title: '3. Choose a game server',
|
|
||||||
subtitle: 'Select the game server you want to use to play Fortnite.',
|
|
||||||
titleStyle: FluentTheme.of(context).typography.title,
|
|
||||||
expandedContentHeaderHeight: 80,
|
|
||||||
contentWidth: 0,
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Local Game Server",
|
|
||||||
subtitle: "Select this option if you want to host the game server on your PC",
|
|
||||||
contentWidth: null,
|
|
||||||
isChild: true,
|
|
||||||
content: Obx(() => Checkbox(
|
|
||||||
checked: _remoteGameServerController.text.isEmpty && _localGameServer(),
|
|
||||||
onChanged: (value) {
|
|
||||||
_localGameServer.value = value ?? false;
|
|
||||||
_remoteGameServerController.text = _settingsController.matchmakingIp.text = "";
|
|
||||||
}
|
|
||||||
))
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Remote Game Server",
|
|
||||||
subtitle: "Select this option if you want to join a match hosted on someone else's pc",
|
|
||||||
isChild: true,
|
|
||||||
content: TextFormBox(
|
|
||||||
controller: _remoteGameServerController,
|
|
||||||
onChanged: (value) =>_localGameServer.value = false,
|
|
||||||
placeholder: "Type the game server's ip",
|
|
||||||
validator: checkMatchmaking
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
LaunchButton(
|
|
||||||
startLabel: 'Start playing',
|
|
||||||
stopLabel: 'Close game',
|
|
||||||
host: false,
|
|
||||||
check: () {
|
|
||||||
if(_gameController.selectedVersion == null){
|
|
||||||
showMessage("Select a Fortnite version before continuing");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!_localGameServer() && _remoteGameServerController.text.isEmpty){
|
|
||||||
showMessage("Select a game server before continuing");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_localGameServer()){
|
|
||||||
_settingsController.matchmakingIp.text = "127.0.0.1";
|
|
||||||
_gameController.autoStartGameServer.value = true;
|
|
||||||
}else {
|
|
||||||
_settingsController.matchmakingIp.text = _remoteGameServerController.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
_settingsController.firstRun.value = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/page/browse_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/page/play_page.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class LauncherPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LauncherPage> createState() => _LauncherPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LauncherPageState extends State<LauncherPage> with AutomaticKeepAliveClientMixin {
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Navigator(
|
|
||||||
key: widget.navigatorKey,
|
|
||||||
initialRoute: "home",
|
|
||||||
onGenerateRoute: (settings) {
|
|
||||||
var screen = _createScreen(settings.name);
|
|
||||||
return FluentPageRoute(
|
|
||||||
builder: (context) => screen,
|
|
||||||
settings: settings
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _createScreen(String? name) {
|
|
||||||
switch(name){
|
|
||||||
case "home":
|
|
||||||
return PlayPage(widget.navigatorKey, widget.nestedNavigation);
|
|
||||||
case "browse":
|
|
||||||
return const BrowsePage();
|
|
||||||
default:
|
|
||||||
throw Exception("Unknown page: $name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/checks.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
|
||||||
|
|
||||||
class PlayPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
const PlayPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PlayPage> createState() => _PlayPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PlayPageState extends State<PlayPage> {
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
final StreamController _matchmakingStream = StreamController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_gameController.password.addListener(() => _matchmakingStream.add(null));
|
|
||||||
_settingsController.matchmakingIp.addListener(() =>
|
|
||||||
_matchmakingStream.add(null));
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Version",
|
|
||||||
subtitle: "Select the version of Fortnite you want to play",
|
|
||||||
content: const VersionSelector(),
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Add a version from this PC's local storage",
|
|
||||||
subtitle: "Versions coming from your local disk are not guaranteed to work",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () =>
|
|
||||||
VersionSelector.openAddDialog(context),
|
|
||||||
child: const Text("Add build"),
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Download any version from the cloud",
|
|
||||||
subtitle: "A curated list of supported versions by Project Reboot",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () =>
|
|
||||||
VersionSelector.openDownloadDialog(context),
|
|
||||||
child: const Text("Download"),
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
StreamBuilder(
|
|
||||||
stream: _matchmakingStream.stream,
|
|
||||||
builder: (context, value) =>
|
|
||||||
SettingTile(
|
|
||||||
title: "Matchmaking host",
|
|
||||||
subtitle: "Enter the IP address of the game server hosting the match",
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "IP:PORT",
|
|
||||||
controller: _settingsController.matchmakingIp,
|
|
||||||
validator: checkMatchmaking,
|
|
||||||
autovalidateMode: AutovalidateMode.always
|
|
||||||
),
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Automatically start game server",
|
|
||||||
subtitle: "This option is available when the matchmaker is set to localhost",
|
|
||||||
contentWidth: null,
|
|
||||||
content: !isLocalHost(
|
|
||||||
_settingsController.matchmakingIp.text) ||
|
|
||||||
_gameController.password.text.isNotEmpty
|
|
||||||
? _disabledAutoGameServerSwitch
|
|
||||||
: _autoGameServerSwitch,
|
|
||||||
isChild: true
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Browse available servers",
|
|
||||||
subtitle: "Discover new game servers that fit your play-style",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () {
|
|
||||||
widget.navigatorKey.currentState
|
|
||||||
?.pushNamed('browse');
|
|
||||||
widget.nestedNavigation.value += 1;
|
|
||||||
},
|
|
||||||
child: const Text("Browse")
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
const LaunchButton(
|
|
||||||
host: false
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _disabledAutoGameServerSwitch => Container(
|
|
||||||
foregroundDecoration: const BoxDecoration(
|
|
||||||
color: Colors.grey,
|
|
||||||
backgroundBlendMode: BlendMode.saturation,
|
|
||||||
),
|
|
||||||
child: _autoGameServerSwitch,
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _autoGameServerSwitch => Obx(() => ToggleSwitch(
|
|
||||||
checked: _gameController.autoStartGameServer() &&
|
|
||||||
isLocalHost(_settingsController.matchmakingIp.text) &&
|
|
||||||
_gameController.password.text.isEmpty,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (!isLocalHost(_settingsController.matchmakingIp.text)) {
|
|
||||||
showMessage(
|
|
||||||
"This option isn't available when the matchmaker isn't set to 127.0.0.1");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gameController.password.text.isNotEmpty) {
|
|
||||||
showMessage(
|
|
||||||
"This option isn't available when the password isn't empty(LawinV2)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameController.autoStartGameServer.value = value;
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart';
|
|
||||||
|
|
||||||
class ServerPage extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final RxInt nestedNavigation;
|
|
||||||
|
|
||||||
const ServerPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ServerPage> createState() => _ServerPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMixin {
|
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Obx(() => Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: InfoBar(
|
|
||||||
title: Text("The backend server handles authentication and parties, not game hosting"),
|
|
||||||
severity: InfoBarSeverity.info
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Host",
|
|
||||||
subtitle: "Enter the host of the backend server",
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "Host",
|
|
||||||
controller: _serverController.host,
|
|
||||||
enabled: _isRemote
|
|
||||||
)
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Port",
|
|
||||||
subtitle: "Enter the port of the backend server",
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "Port",
|
|
||||||
controller: _serverController.port,
|
|
||||||
enabled: _isRemote
|
|
||||||
)
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Type",
|
|
||||||
subtitle: "Select the type of backend to use",
|
|
||||||
content: ServerTypeSelector()
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Detached",
|
|
||||||
subtitle: "Choose whether the backend should be started as a separate process, useful for debugging",
|
|
||||||
contentWidth: null,
|
|
||||||
content: Obx(() => ToggleSwitch(
|
|
||||||
checked: _serverController.detached(),
|
|
||||||
onChanged: (value) => _serverController.detached.value = value
|
|
||||||
))
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Server files",
|
|
||||||
subtitle: "The location where the backend is stored",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => launchUrl(serverDirectory.uri),
|
|
||||||
child: const Text("Open")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Reset Backend",
|
|
||||||
subtitle: "Resets the launcher's backend to its default settings",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => InfoDialog(
|
|
||||||
text: "Do you want to reset the backend? This action is irreversible",
|
|
||||||
buttons: [
|
|
||||||
DialogButton(
|
|
||||||
type: ButtonType.secondary,
|
|
||||||
text: "Close",
|
|
||||||
),
|
|
||||||
DialogButton(
|
|
||||||
type: ButtonType.primary,
|
|
||||||
text: "Reset",
|
|
||||||
onTap: () {
|
|
||||||
_serverController.reset();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
child: const Text("Reset"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
const ServerButton()
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _isRemote => _serverController.type.value == ServerType.remote;
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/checks.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/setting_tile.dart';
|
|
||||||
|
|
||||||
class SettingsPage extends StatefulWidget {
|
|
||||||
const SettingsPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SettingsPage> createState() => _SettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin {
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
|
||||||
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
SettingTile(
|
|
||||||
title: "File settings",
|
|
||||||
subtitle: "This section contains all the settings related to files used by Fortnite",
|
|
||||||
expandedContent: [
|
|
||||||
_createFileSetting(
|
|
||||||
title: "Game server",
|
|
||||||
description: "This file is injected to create a game server to host matches",
|
|
||||||
controller: _settingsController.rebootDll
|
|
||||||
),
|
|
||||||
_createFileSetting(
|
|
||||||
title: "Unreal engine console",
|
|
||||||
description: "This file is injected to unlock the Unreal Engine Console in-game",
|
|
||||||
controller: _settingsController.consoleDll
|
|
||||||
),
|
|
||||||
_createFileSetting(
|
|
||||||
title: "Authentication patcher",
|
|
||||||
description: "This file is injected to redirect all HTTP requests to the local backend",
|
|
||||||
controller: _settingsController.authDll
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Automatic updates",
|
|
||||||
subtitle: "Choose whether the launcher and its files should be automatically updated",
|
|
||||||
contentWidth: null,
|
|
||||||
content: Obx(() => ToggleSwitch(
|
|
||||||
checked: _settingsController.autoUpdate.value,
|
|
||||||
onChanged: (value) => _settingsController.autoUpdate.value = value
|
|
||||||
)),
|
|
||||||
expandedContentSpacing: 0,
|
|
||||||
expandedContent: [
|
|
||||||
SettingTile(
|
|
||||||
title: "Update Mirror",
|
|
||||||
subtitle: "The URL used to pull the latest update once a day",
|
|
||||||
content: Obx(() => TextFormBox(
|
|
||||||
placeholder: "URL",
|
|
||||||
controller: _settingsController.updateUrl,
|
|
||||||
enabled: _settingsController.autoUpdate.value,
|
|
||||||
validator: checkUpdateUrl
|
|
||||||
)),
|
|
||||||
isChild: true
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Custom launch arguments",
|
|
||||||
subtitle: "Enter additional arguments to use when launching the game",
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: "Arguments...",
|
|
||||||
controller: _gameController.customLaunchArgs,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Create a bug report",
|
|
||||||
subtitle: "Help me fix bugs by reporting them",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")),
|
|
||||||
child: const Text("Report a bug"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Reset settings",
|
|
||||||
subtitle: "Resets the launcher's settings to their default values",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => InfoDialog(
|
|
||||||
text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
|
|
||||||
buttons: [
|
|
||||||
DialogButton(
|
|
||||||
type: ButtonType.secondary,
|
|
||||||
text: "Close",
|
|
||||||
),
|
|
||||||
DialogButton(
|
|
||||||
type: ButtonType.primary,
|
|
||||||
text: "Reset",
|
|
||||||
onTap: () {
|
|
||||||
_settingsController.reset();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
child: const Text("Reset"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
title: "Version status",
|
|
||||||
subtitle: "Current version: 8.1",
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => launchUrl(installationDirectory.uri),
|
|
||||||
child: const Text("Show Files"),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
|
|
||||||
title: title,
|
|
||||||
subtitle: description,
|
|
||||||
content: FileSelector(
|
|
||||||
placeholder: "Path",
|
|
||||||
windowTitle: "Select a file",
|
|
||||||
controller: controller,
|
|
||||||
validator: checkDll,
|
|
||||||
extension: "dll",
|
|
||||||
folder: false
|
|
||||||
),
|
|
||||||
isChild: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
|
|
||||||
class SettingTile extends StatefulWidget {
|
|
||||||
static const double kDefaultContentWidth = 200.0;
|
|
||||||
static const double kDefaultSpacing = 8.0;
|
|
||||||
static const double kDefaultHeaderHeight = 72;
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final TextStyle? titleStyle;
|
|
||||||
final String subtitle;
|
|
||||||
final TextStyle? subtitleStyle;
|
|
||||||
final Widget? content;
|
|
||||||
final double? contentWidth;
|
|
||||||
final List<Widget>? expandedContent;
|
|
||||||
final double expandedContentHeaderHeight;
|
|
||||||
final double expandedContentSpacing;
|
|
||||||
final bool isChild;
|
|
||||||
|
|
||||||
const SettingTile(
|
|
||||||
{Key? key,
|
|
||||||
required this.title,
|
|
||||||
this.titleStyle,
|
|
||||||
required this.subtitle,
|
|
||||||
this.subtitleStyle,
|
|
||||||
this.content,
|
|
||||||
this.contentWidth = kDefaultContentWidth,
|
|
||||||
this.expandedContentHeaderHeight = kDefaultHeaderHeight,
|
|
||||||
this.expandedContentSpacing = kDefaultSpacing,
|
|
||||||
this.expandedContent,
|
|
||||||
this.isChild = false})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SettingTile> createState() => _SettingTileState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingTileState extends State<SettingTile> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if(widget.expandedContent == null){
|
|
||||||
return _contentCard;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Expander(
|
|
||||||
initiallyExpanded: true,
|
|
||||||
headerShape: (open) => const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
|
|
||||||
),
|
|
||||||
header: SizedBox(
|
|
||||||
height: widget.expandedContentHeaderHeight,
|
|
||||||
child: _header
|
|
||||||
),
|
|
||||||
trailing: _trailing,
|
|
||||||
content: _content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget get _content {
|
|
||||||
var contents = widget.expandedContent!;
|
|
||||||
var items = List.generate(contents.length * 2, (index) => index % 2 == 0 ? contents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing));
|
|
||||||
return Column(
|
|
||||||
children: items
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget get _trailing => SizedBox(
|
|
||||||
width: widget.contentWidth,
|
|
||||||
child: widget.content
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _header => ListTile(
|
|
||||||
title: Text(
|
|
||||||
widget.title,
|
|
||||||
style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
widget.subtitle,
|
|
||||||
style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _contentCard {
|
|
||||||
if (widget.isChild) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: _contentCardBody
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)),
|
|
||||||
child: _contentCardBody
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget get _contentCardBody => ListTile(
|
|
||||||
title: Text(
|
|
||||||
widget.title,
|
|
||||||
style: widget.titleStyle ?? FluentTheme.of(context).typography.subtitle,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
widget.subtitle,
|
|
||||||
style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body
|
|
||||||
),
|
|
||||||
trailing: _trailing
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
|
|
||||||
|
|
||||||
class ServerButton extends StatefulWidget {
|
|
||||||
const ServerButton({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ServerButton> createState() => _ServerButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerButtonState extends State<ServerButton> {
|
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Align(
|
|
||||||
alignment: AlignmentDirectional.bottomCenter,
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Obx(() => SizedBox(
|
|
||||||
height: 48,
|
|
||||||
child: Button(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(_buttonText),
|
|
||||||
),
|
|
||||||
onPressed: () => _serverController.toggle(false)
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
String get _buttonText {
|
|
||||||
if(_serverController.type.value == ServerType.local){
|
|
||||||
return "Check backend";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_serverController.started.value){
|
|
||||||
return "Stop backend";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Start backend";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
|
||||||
|
|
||||||
class ServerTypeSelector extends StatelessWidget {
|
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
|
||||||
|
|
||||||
ServerTypeSelector({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DropDownButton(
|
|
||||||
leading: Text(_serverController.type.value.name),
|
|
||||||
items: ServerType.values
|
|
||||||
.map((type) => _createItem(type))
|
|
||||||
.toList()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuFlyoutItem _createItem(ServerType type) {
|
|
||||||
return MenuFlyoutItem(
|
|
||||||
text: Tooltip(
|
|
||||||
message: type.message,
|
|
||||||
child: Text(type.name)
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
await _serverController.stop();
|
|
||||||
_serverController.type(type);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
|
|
||||||
class SquaredPaneItem extends PaneItem {
|
|
||||||
SquaredPaneItem({
|
|
||||||
super.key,
|
|
||||||
required super.title,
|
|
||||||
required super.icon,
|
|
||||||
required super.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(
|
|
||||||
BuildContext context,
|
|
||||||
bool selected,
|
|
||||||
VoidCallback? onPressed, {
|
|
||||||
PaneDisplayMode? displayMode,
|
|
||||||
bool showTextOnTop = true,
|
|
||||||
int? itemIndex,
|
|
||||||
bool? autofocus,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
SizedBox.square(
|
|
||||||
dimension: 48,
|
|
||||||
child: icon
|
|
||||||
),
|
|
||||||
title!
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import '../../controller/game_controller.dart';
|
|
||||||
|
|
||||||
class ProfileWidget extends StatelessWidget {
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
|
||||||
|
|
||||||
ProfileWidget({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12.0,
|
|
||||||
vertical: 12.0
|
|
||||||
),
|
|
||||||
child: GestureDetector(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 64,
|
|
||||||
height: 64,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
shape: BoxShape.circle
|
|
||||||
),
|
|
||||||
child: Image.asset("assets/images/user.png")
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 12.0,
|
|
||||||
),
|
|
||||||
const Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Auties00",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"alautiero@gmail.com",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w100
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
|
|
||||||
class SmartCheckBox extends StatefulWidget {
|
|
||||||
final CheckboxController controller;
|
|
||||||
final Widget? content;
|
|
||||||
const SmartCheckBox({Key? key, required this.controller, this.content}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SmartCheckBox> createState() => _SmartCheckBoxState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SmartCheckBoxState extends State<SmartCheckBox> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Checkbox(
|
|
||||||
checked: widget.controller.value,
|
|
||||||
onChanged: (checked) => setState(() => widget.controller.value = checked ?? false),
|
|
||||||
content: widget.content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CheckboxController {
|
|
||||||
bool value;
|
|
||||||
|
|
||||||
CheckboxController({this.value = false});
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
|
|
||||||
class SmartInput extends StatelessWidget {
|
|
||||||
final String? label;
|
|
||||||
final String placeholder;
|
|
||||||
final TextEditingController controller;
|
|
||||||
final TextInputType type;
|
|
||||||
final bool enabled;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
final bool readOnly;
|
|
||||||
final AutovalidateMode validatorMode;
|
|
||||||
final String? Function(String?)? validator;
|
|
||||||
|
|
||||||
const SmartInput(
|
|
||||||
{Key? key,
|
|
||||||
required this.placeholder,
|
|
||||||
required this.controller,
|
|
||||||
this.label,
|
|
||||||
this.onTap,
|
|
||||||
this.enabled = true,
|
|
||||||
this.readOnly = false,
|
|
||||||
this.type = TextInputType.text,
|
|
||||||
this.validatorMode = AutovalidateMode.disabled,
|
|
||||||
this.validator})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if(label != null){
|
|
||||||
return InfoLabel(
|
|
||||||
label: label!,
|
|
||||||
child: _body
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _body;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFormBox get _body => TextFormBox(
|
|
||||||
enabled: enabled,
|
|
||||||
controller: controller,
|
|
||||||
keyboardType: type,
|
|
||||||
placeholder: placeholder,
|
|
||||||
onTap: onTap,
|
|
||||||
readOnly: readOnly,
|
|
||||||
autovalidateMode: validatorMode,
|
|
||||||
validator: validator
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:html/parser.dart' show parse;
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/time.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
|
||||||
|
|
||||||
import 'os.dart';
|
|
||||||
|
|
||||||
final Uri _manifestSourceUrl = Uri.parse(
|
|
||||||
"https://github.com/simplyblk/Fortnitebuilds/blob/main/README.md");
|
|
||||||
|
|
||||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
|
||||||
var response = await http.get(_manifestSourceUrl);
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var document = parse(response.body);
|
|
||||||
var elements = document.getElementsByTagName("table")
|
|
||||||
.map((element) => element.querySelector("tbody"))
|
|
||||||
.expand((element) => element?.getElementsByTagName("tr") ?? [])
|
|
||||||
.toList();
|
|
||||||
var results = <FortniteBuild>[];
|
|
||||||
for (var tableEntry in elements) {
|
|
||||||
var children = tableEntry.querySelectorAll("td");
|
|
||||||
var version = parser.tryParse(children[0].text);
|
|
||||||
if (version == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var link = children[3].firstChild?.attributes?["href"];
|
|
||||||
if (link == null || link == "N/A") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.add(FortniteBuild(version: version, link: link));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function(double?, String?) onDecompress) async {
|
|
||||||
var outputDir = Directory("${destination.path}\\.build");
|
|
||||||
outputDir.createSync(recursive: true);
|
|
||||||
try {
|
|
||||||
destination.createSync(recursive: true);
|
|
||||||
var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1);
|
|
||||||
var extension = path.extension(fileName);
|
|
||||||
var tempFile = File("${outputDir.path}\\$fileName");
|
|
||||||
if(tempFile.existsSync()) {
|
|
||||||
tempFile.deleteSync(recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = http.Client();
|
|
||||||
var request = http.Request("GET", Uri.parse(archiveUrl));
|
|
||||||
request.headers['Connection'] = 'Keep-Alive';
|
|
||||||
var response = await client.send(request);
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
var length = response.contentLength!;
|
|
||||||
var received = 0;
|
|
||||||
var sink = tempFile.openWrite();
|
|
||||||
await response.stream.map((s) {
|
|
||||||
received += s.length;
|
|
||||||
var now = DateTime.now();
|
|
||||||
var eta = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch;
|
|
||||||
onProgress((received / length) * 100, toETA(eta));
|
|
||||||
return s;
|
|
||||||
}).pipe(sink);
|
|
||||||
|
|
||||||
var receiverPort = ReceivePort();
|
|
||||||
var file = _CompressedFile(extension, tempFile.path, destination.path, receiverPort.sendPort);
|
|
||||||
Isolate.spawn<_CompressedFile>(_decompress, file);
|
|
||||||
var completer = Completer();
|
|
||||||
receiverPort.forEach((element) {
|
|
||||||
onDecompress(element.progress, element.eta);
|
|
||||||
if(element.progress != null && element.progress >= 100){
|
|
||||||
completer.complete(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await completer.future;
|
|
||||||
delete(outputDir);
|
|
||||||
} catch(message) {
|
|
||||||
throw Exception("Cannot download build: $message");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Progress report somehow
|
|
||||||
Future<void> _decompress(_CompressedFile file) async {
|
|
||||||
try{
|
|
||||||
file.sendPort.send(_FileUpdate(null, null));
|
|
||||||
switch (file.extension.toLowerCase()) {
|
|
||||||
case '.zip':
|
|
||||||
var process = await Process.start(
|
|
||||||
'tar',
|
|
||||||
['-xf', file.tempFile, '-C', file.destination],
|
|
||||||
mode: ProcessStartMode.inheritStdio
|
|
||||||
);
|
|
||||||
await process.exitCode;
|
|
||||||
break;
|
|
||||||
case '.rar':
|
|
||||||
var process = await Process.start(
|
|
||||||
'${assetsDirectory.path}\\builds\\winrar.exe',
|
|
||||||
['x', file.tempFile, '*.*', file.destination],
|
|
||||||
mode: ProcessStartMode.inheritStdio
|
|
||||||
);
|
|
||||||
await process.exitCode;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
file.sendPort.send(_FileUpdate(100, null));
|
|
||||||
}catch(exception){
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CompressedFile {
|
|
||||||
final String extension;
|
|
||||||
final String tempFile;
|
|
||||||
final String destination;
|
|
||||||
final SendPort sendPort;
|
|
||||||
|
|
||||||
_CompressedFile(this.extension, this.tempFile, this.destination, this.sendPort);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FileUpdate {
|
|
||||||
final double? progress;
|
|
||||||
final String? eta;
|
|
||||||
|
|
||||||
_FileUpdate(this.progress, this.eta);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
String? checkVersion(String? text, List<FortniteVersion> versions) {
|
String? checkVersion(String? text, List<FortniteVersion> versions) {
|
||||||
if (text == null || text.isEmpty) {
|
if (text == null || text.isEmpty) {
|
||||||
@@ -32,7 +32,7 @@ String? checkGameFolder(text) {
|
|||||||
return "Directory doesn't exist";
|
return "Directory doesn't exist";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||||
return "Invalid game path";
|
return "Invalid game path";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
52
lib/src/util/cryptography.dart
Normal file
52
lib/src/util/cryptography.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:bcrypt/bcrypt.dart';
|
||||||
|
import 'package:pointycastle/export.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
const int _ivLength = 16;
|
||||||
|
const int _keyLength = 32;
|
||||||
|
|
||||||
|
String hashPassword(String plaintext) => BCrypt.hashpw(plaintext, BCrypt.gensalt());
|
||||||
|
|
||||||
|
bool checkPassword(String password, String hashedText) => BCrypt.checkpw(password, hashedText);
|
||||||
|
|
||||||
|
String aes256Encrypt(String plainText, String password) {
|
||||||
|
final random = Random.secure();
|
||||||
|
final iv = Uint8List.fromList(List.generate(_ivLength, (index) => random.nextInt(256)));
|
||||||
|
final keyDerivationData = Uint8List.fromList(utf8.encode(password));
|
||||||
|
final derive = PBKDF2KeyDerivator(HMac(SHA256Digest(), _ivLength * 8));
|
||||||
|
var params = Pbkdf2Parameters(iv, _ivLength * 8, _keyLength);
|
||||||
|
derive.init(params);
|
||||||
|
final key = derive.process(keyDerivationData);
|
||||||
|
final cipherParams = PaddedBlockCipherParameters(
|
||||||
|
KeyParameter(key),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
final aes = AESEngine();
|
||||||
|
final paddingCipher = PaddedBlockCipherImpl(PKCS7Padding(), aes);
|
||||||
|
paddingCipher.init(true, cipherParams);
|
||||||
|
final plainBytes = Uint8List.fromList(utf8.encode(plainText));
|
||||||
|
final encryptedBytes = paddingCipher.process(plainBytes);
|
||||||
|
return base64.encode([...iv, ...encryptedBytes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String aes256Decrypt(String encryptedText, String password) {
|
||||||
|
final encryptedBytes = base64.decode(encryptedText);
|
||||||
|
final salt = encryptedBytes.sublist(0, _ivLength);
|
||||||
|
final payload = encryptedBytes.sublist(_ivLength);
|
||||||
|
final keyDerivationData = Uint8List.fromList(utf8.encode(password));
|
||||||
|
final derive = PBKDF2KeyDerivator(HMac(SHA256Digest(), _ivLength * 8));
|
||||||
|
var params = Pbkdf2Parameters(salt, _ivLength * 8, _keyLength);
|
||||||
|
derive.init(params);
|
||||||
|
final key = derive.process(keyDerivationData);
|
||||||
|
final cipherParams = PaddedBlockCipherParameters(
|
||||||
|
KeyParameter(key),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
final aes = AESEngine();
|
||||||
|
final paddingCipher = PaddedBlockCipherImpl(PKCS7Padding(), aes);
|
||||||
|
paddingCipher.init(false, cipherParams);
|
||||||
|
final decryptedBytes = paddingCipher.process(payload);
|
||||||
|
return utf8.decode(decryptedBytes);
|
||||||
|
}
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// ignore_for_file: non_constant_identifier_names
|
|
||||||
|
|
||||||
import 'dart:ffi';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:win32/win32.dart';
|
|
||||||
|
|
||||||
final _kernel32 = DynamicLibrary.open('kernel32.dll');
|
|
||||||
final _CreateRemoteThread = _kernel32.lookupFunction<
|
|
||||||
IntPtr Function(
|
|
||||||
IntPtr hProcess,
|
|
||||||
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
|
|
||||||
IntPtr dwStackSize,
|
|
||||||
Pointer loadLibraryAddress,
|
|
||||||
Pointer lpParameter,
|
|
||||||
Uint32 dwCreationFlags,
|
|
||||||
Pointer<Uint32> lpThreadId),
|
|
||||||
int Function(
|
|
||||||
int hProcess,
|
|
||||||
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
|
|
||||||
int dwStackSize,
|
|
||||||
Pointer loadLibraryAddress,
|
|
||||||
Pointer lpParameter,
|
|
||||||
int dwCreationFlags,
|
|
||||||
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
|
|
||||||
int CreateRemoteThread(
|
|
||||||
int hProcess,
|
|
||||||
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
|
|
||||||
int dwStackSize,
|
|
||||||
Pointer loadLibraryAddress,
|
|
||||||
Pointer lpParameter,
|
|
||||||
int dwCreationFlags,
|
|
||||||
Pointer<Uint32> lpThreadId) =>
|
|
||||||
_CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize,
|
|
||||||
loadLibraryAddress, lpParameter, dwCreationFlags, lpThreadId);
|
|
||||||
|
|
||||||
Future<void> injectDll(int pid, String dll) async {
|
|
||||||
var process = OpenProcess(
|
|
||||||
0x43A,
|
|
||||||
0,
|
|
||||||
pid
|
|
||||||
);
|
|
||||||
|
|
||||||
var processAddress = GetProcAddress(
|
|
||||||
GetModuleHandle("KERNEL32".toNativeUtf16()),
|
|
||||||
"LoadLibraryA".toNativeUtf8()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (processAddress == nullptr) {
|
|
||||||
throw Exception("Cannot get process address for pid $pid");
|
|
||||||
}
|
|
||||||
|
|
||||||
var dllAddress = VirtualAllocEx(
|
|
||||||
process,
|
|
||||||
nullptr,
|
|
||||||
dll.length + 1,
|
|
||||||
0x3000,
|
|
||||||
0x4
|
|
||||||
);
|
|
||||||
|
|
||||||
var writeMemoryResult = WriteProcessMemory(
|
|
||||||
process,
|
|
||||||
dllAddress,
|
|
||||||
dll.toNativeUtf8(),
|
|
||||||
dll.length,
|
|
||||||
nullptr
|
|
||||||
);
|
|
||||||
|
|
||||||
if (writeMemoryResult != 1) {
|
|
||||||
throw Exception("Memory write failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var createThreadResult = CreateRemoteThread(
|
|
||||||
process,
|
|
||||||
nullptr,
|
|
||||||
0,
|
|
||||||
processAddress,
|
|
||||||
dllAddress,
|
|
||||||
0,
|
|
||||||
nullptr
|
|
||||||
);
|
|
||||||
|
|
||||||
if (createThreadResult == -1) {
|
|
||||||
throw Exception("Thread creation failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var closeResult = CloseHandle(process);
|
|
||||||
if(closeResult != 1){
|
|
||||||
throw Exception("Cannot close handle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||||
import 'package:win32/win32.dart';
|
|
||||||
|
|
||||||
|
|
||||||
const int appBarSize = 2;
|
|
||||||
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
|
||||||
|
|
||||||
bool isLocalHost(String host) => host.trim() == "127.0.0.1" || host.trim().toLowerCase() == "localhost" || host.trim() == "0.0.0.0";
|
|
||||||
|
|
||||||
bool get isWin11 {
|
bool get isWin11 {
|
||||||
var result = _regex.firstMatch(Platform.operatingSystemVersion)?.group(1);
|
var result = _winBuildRegex.firstMatch(Platform.operatingSystemVersion)?.group(1);
|
||||||
if(result == null){
|
if(result == null){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -19,70 +11,3 @@ bool get isWin11 {
|
|||||||
var intBuild = int.tryParse(result);
|
var intBuild = int.tryParse(result);
|
||||||
return intBuild != null && intBuild > 22000;
|
return intBuild != null && intBuild > 22000;
|
||||||
}
|
}
|
||||||
|
|
||||||
int startBackgroundProcess(String executable, List<String> args) {
|
|
||||||
var executablePath = TEXT('$executable ${args.map((entry) => '"$entry"').join(" ")}');
|
|
||||||
var startupInfo = calloc<STARTUPINFO>();
|
|
||||||
var processInfo = calloc<PROCESS_INFORMATION>();
|
|
||||||
var success = CreateProcess(
|
|
||||||
nullptr,
|
|
||||||
executablePath,
|
|
||||||
nullptr,
|
|
||||||
nullptr,
|
|
||||||
FALSE,
|
|
||||||
CREATE_NO_WINDOW,
|
|
||||||
nullptr,
|
|
||||||
nullptr,
|
|
||||||
startupInfo,
|
|
||||||
processInfo
|
|
||||||
);
|
|
||||||
if (success == 0) {
|
|
||||||
var error = GetLastError();
|
|
||||||
throw Exception("Cannot start process: $error");
|
|
||||||
}
|
|
||||||
|
|
||||||
var pid = processInfo.ref.dwProcessId;
|
|
||||||
free(startupInfo);
|
|
||||||
free(processInfo);
|
|
||||||
return pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> runElevated(String executable, String args) async {
|
|
||||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
|
||||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
|
||||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
|
||||||
shellInput.ref.nShow = SW_HIDE;
|
|
||||||
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
|
||||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
|
||||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
|
||||||
var shellResult = ShellExecuteEx(shellInput);
|
|
||||||
return shellResult == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory get installationDirectory =>
|
|
||||||
File(Platform.resolvedExecutable).parent;
|
|
||||||
|
|
||||||
Directory get logsDirectory =>
|
|
||||||
Directory("${installationDirectory.path}\\logs");
|
|
||||||
|
|
||||||
Directory get assetsDirectory =>
|
|
||||||
Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
|
||||||
|
|
||||||
Directory get tempDirectory =>
|
|
||||||
Directory(Platform.environment["Temp"]!);
|
|
||||||
|
|
||||||
Future<bool> delete(FileSystemEntity file) async {
|
|
||||||
try {
|
|
||||||
await file.delete(recursive: true);
|
|
||||||
return true;
|
|
||||||
}catch(_){
|
|
||||||
return Future.delayed(const Duration(seconds: 5)).then((value) async {
|
|
||||||
try {
|
|
||||||
await file.delete(recursive: true);
|
|
||||||
return true;
|
|
||||||
}catch(_){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
final Uint8List _originalHeadless = Uint8List.fromList([
|
|
||||||
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
|
|
||||||
]);
|
|
||||||
|
|
||||||
final Uint8List _patchedHeadless = Uint8List.fromList([
|
|
||||||
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
|
|
||||||
]);
|
|
||||||
|
|
||||||
final Uint8List _originalMatchmaking = Uint8List.fromList([
|
|
||||||
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
|
|
||||||
]);
|
|
||||||
|
|
||||||
final Uint8List _patchedMatchmaking = Uint8List.fromList([
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
||||||
]);
|
|
||||||
|
|
||||||
Future<bool> patchHeadless(File file) async =>
|
|
||||||
_patch(file, _originalHeadless, _patchedHeadless);
|
|
||||||
|
|
||||||
Future<bool> patchMatchmaking(File file) async =>
|
|
||||||
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
|
|
||||||
|
|
||||||
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
|
||||||
try {
|
|
||||||
if(original.length != patched.length){
|
|
||||||
throw Exception("Cannot mutate length of binary file");
|
|
||||||
}
|
|
||||||
|
|
||||||
var read = await file.readAsBytes();
|
|
||||||
var length = await file.length();
|
|
||||||
var offset = 0;
|
|
||||||
var counter = 0;
|
|
||||||
while(offset < length){
|
|
||||||
if(read[offset] == original[counter]){
|
|
||||||
counter++;
|
|
||||||
}else {
|
|
||||||
counter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset++;
|
|
||||||
if(counter == original.length){
|
|
||||||
for(var index = 0; index < patched.length; index++){
|
|
||||||
read[offset - counter + index] = patched[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
await file.writeAsBytes(read, mode: FileMode.write);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}catch(_){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import 'dart:ffi';
|
|
||||||
|
|
||||||
import 'package:win32/win32.dart';
|
|
||||||
|
|
||||||
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
|
||||||
|
|
||||||
// ignore: non_constant_identifier_names
|
|
||||||
int NtResumeProcess(int hWnd) {
|
|
||||||
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
|
||||||
int Function(int hWnd)>('NtResumeProcess');
|
|
||||||
return function(hWnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: non_constant_identifier_names
|
|
||||||
int NtSuspendProcess(int hWnd) {
|
|
||||||
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
|
||||||
int Function(int hWnd)>('NtSuspendProcess');
|
|
||||||
return function(hWnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool suspend(int pid) {
|
|
||||||
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
|
||||||
final result = NtSuspendProcess(processHandle);
|
|
||||||
CloseHandle(processHandle);
|
|
||||||
return (result == 0) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool resume(int pid) {
|
|
||||||
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
|
||||||
final result = NtResumeProcess(processHandle);
|
|
||||||
CloseHandle(processHandle);
|
|
||||||
return (result == 0) ? true : false;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
|
|
||||||
const String rebootDownloadUrl =
|
|
||||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/main/Release.zip";
|
|
||||||
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
|
|
||||||
|
|
||||||
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
|
|
||||||
Directory? outputDir;
|
|
||||||
var now = DateTime.now();
|
|
||||||
try {
|
|
||||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
|
||||||
var exists = await rebootDllFile.exists();
|
|
||||||
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) {
|
|
||||||
return lastUpdateMs!;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await http.get(Uri.parse(rebootDownloadUrl));
|
|
||||||
outputDir = await installationDirectory.createTemp("reboot_out");
|
|
||||||
var tempZip = File("${outputDir.path}\\reboot.zip");
|
|
||||||
await tempZip.writeAsBytes(response.bodyBytes);
|
|
||||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
|
||||||
var rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
|
|
||||||
if (!exists || sha1.convert(await rebootDllFile.readAsBytes()) != sha1.convert(await rebootDll.readAsBytes())) {
|
|
||||||
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
return now.millisecondsSinceEpoch;
|
|
||||||
}catch(message) {
|
|
||||||
if(url == rebootDownloadUrl){
|
|
||||||
var asset = File('${assetsDirectory.path}\\dlls\\reboot.dll');
|
|
||||||
await rebootDllFile.writeAsBytes(asset.readAsBytesSync());
|
|
||||||
return now.millisecondsSinceEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Exception("Cannot download reboot.zip, invalid zip: $message");
|
|
||||||
}finally{
|
|
||||||
if(outputDir != null) {
|
|
||||||
delete(outputDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
|
||||||
return lastUpdateMs != null
|
|
||||||
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:ini/ini.dart';
|
|
||||||
import 'package:process_run/shell.dart';
|
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:shelf/shelf_io.dart';
|
|
||||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
|
||||||
|
|
||||||
final serverLogFile = File("${logsDirectory.path}\\server.log");
|
|
||||||
final serverDirectory = Directory("${assetsDirectory.path}\\lawin");
|
|
||||||
final serverExeFile = File("${serverDirectory.path}\\lawinserver-win.exe");
|
|
||||||
|
|
||||||
Future<void> writeMatchmakingIp(String text) async {
|
|
||||||
var file = File("${assetsDirectory.path}\\lawin\\Config\\config.ini");
|
|
||||||
if(!file.existsSync()){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var splitIndex = text.indexOf(":");
|
|
||||||
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
|
|
||||||
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : "7777";
|
|
||||||
var config = Config.fromString(file.readAsStringSync());
|
|
||||||
config.set("GameServer", "ip", ip);
|
|
||||||
config.set("GameServer", "port", port);
|
|
||||||
file.writeAsStringSync(config.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> startServer(bool detached) async {
|
|
||||||
var process = await Process.start(
|
|
||||||
serverExeFile.path,
|
|
||||||
[],
|
|
||||||
workingDirectory: serverDirectory.path,
|
|
||||||
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal,
|
|
||||||
runInShell: detached
|
|
||||||
);
|
|
||||||
if(!detached) {
|
|
||||||
serverLogFile.createSync(recursive: true);
|
|
||||||
process.outLines.forEach((element) =>
|
|
||||||
serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
|
||||||
process.errLines.forEach((element) =>
|
|
||||||
serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stopServer() async {
|
|
||||||
await freeLawinPort();
|
|
||||||
await freeMatchmakerPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isLawinPortFree() async {
|
|
||||||
return http.get(Uri.parse("http://127.0.0.1:3551/unknown"))
|
|
||||||
.timeout(const Duration(milliseconds: 500))
|
|
||||||
.then((value) => false)
|
|
||||||
.onError((error, stackTrace) => true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> isMatchmakerPortFree() async {
|
|
||||||
return HttpServer.bind("127.0.0.1", 8080)
|
|
||||||
.then((socket) => socket.close())
|
|
||||||
.then((_) => true)
|
|
||||||
.onError((error, _) => false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> freeLawinPort() async {
|
|
||||||
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_lawin.bat");
|
|
||||||
await Process.run(releaseBat.path, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> freeMatchmakerPort() async {
|
|
||||||
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_matchmaker.bat");
|
|
||||||
await Process.run(releaseBat.path, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> resetWinNat() async {
|
|
||||||
var binary = File("${serverDirectory.path}\\winnat.bat");
|
|
||||||
await runElevated(binary.path, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> createRebootArgs(String username, String password, bool host, String additionalArgs) {
|
|
||||||
if(password.isEmpty) {
|
|
||||||
username = username.isEmpty ? kDefaultPlayerName : username;
|
|
||||||
username = host ? "$username${Random().nextInt(1000)}" : username;
|
|
||||||
username = '$username@projectreboot.dev';
|
|
||||||
}
|
|
||||||
password = password.isNotEmpty ? password : "Rebooted";
|
|
||||||
var args = [
|
|
||||||
"-epicapp=Fortnite",
|
|
||||||
"-epicenv=Prod",
|
|
||||||
"-epiclocale=en-us",
|
|
||||||
"-epicportal",
|
|
||||||
"-skippatchcheck",
|
|
||||||
"-nobe",
|
|
||||||
"-fromfl=eac",
|
|
||||||
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
|
||||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
|
||||||
"-AUTH_LOGIN=$username",
|
|
||||||
"-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}",
|
|
||||||
"-AUTH_TYPE=epic"
|
|
||||||
];
|
|
||||||
|
|
||||||
if(host){
|
|
||||||
args.addAll([
|
|
||||||
"-nullrhi",
|
|
||||||
"-nosplash",
|
|
||||||
"-nosound",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(additionalArgs.isNotEmpty){
|
|
||||||
args.addAll(additionalArgs.split(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> pingSelf(String port) async => ping("127.0.0.1", port);
|
|
||||||
|
|
||||||
Future<Uri?> ping(String host, String port, [bool https=false]) async {
|
|
||||||
var hostName = _getHostName(host);
|
|
||||||
var declaredScheme = _getScheme(host);
|
|
||||||
try{
|
|
||||||
var uri = Uri(
|
|
||||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
|
||||||
host: hostName,
|
|
||||||
port: int.parse(port),
|
|
||||||
path: "unknown"
|
|
||||||
);
|
|
||||||
var client = HttpClient()
|
|
||||||
..connectionTimeout = const Duration(seconds: 5);
|
|
||||||
var request = await client.getUrl(uri);
|
|
||||||
var response = await request.close();
|
|
||||||
var body = utf8.decode(await response.single);
|
|
||||||
return body.contains("epicgames") || body.contains("lawinserver") ? uri : null;
|
|
||||||
}catch(_){
|
|
||||||
return https || declaredScheme != null ? null : await ping(host, port, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
|
|
||||||
|
|
||||||
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
|
||||||
|
|
||||||
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type) async {
|
|
||||||
host = host.trim();
|
|
||||||
if(host.isEmpty){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.missingHostError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
port = port.trim();
|
|
||||||
if(port.isEmpty){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.missingPortError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var portNumber = int.tryParse(port);
|
|
||||||
if(portNumber == null){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.illegalPortError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isLocalHost(host) && portNumber == 3551 && type == ServerType.remote){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.illegalPortError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(type != ServerType.local && !(await isLawinPortFree())){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.backendPortTakenError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(type == ServerType.embedded && !(await isMatchmakerPortFree())){
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.backendPortTakenError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerResult(
|
|
||||||
type: ServerResultType.canStart
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<HttpServer> startRemoteServer(Uri uri) async => await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
|
||||||
|
|
||||||
class ServerResult {
|
|
||||||
final int? pid;
|
|
||||||
final Object? error;
|
|
||||||
final StackTrace? stackTrace;
|
|
||||||
final ServerResultType type;
|
|
||||||
|
|
||||||
ServerResult({this.pid, this.error, this.stackTrace, required this.type});
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ServerResultType {
|
|
||||||
missingHostError,
|
|
||||||
missingPortError,
|
|
||||||
illegalPortError,
|
|
||||||
cannotPingServer,
|
|
||||||
backendPortTakenError,
|
|
||||||
matchmakerPortTakenError,
|
|
||||||
canStart,
|
|
||||||
alreadyStarted,
|
|
||||||
unknownError,
|
|
||||||
stopped
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
String toETA(double milliseconds){
|
|
||||||
var roundedMilliseconds = milliseconds.toInt();
|
|
||||||
var duration = Duration(milliseconds: roundedMilliseconds);
|
|
||||||
return "${duration.inHours.toString().padLeft(2, "0")}:"
|
|
||||||
"${duration.inMinutes.remainder(60).toString().padLeft(2, "0")}:"
|
|
||||||
"${duration.inSeconds.remainder(60).toString().padLeft(2, "0")}";
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DateTimeIso on DateTime {
|
|
||||||
String toIsoString() {
|
|
||||||
String y = (year >= -9999 && year <= 9999) ? _fourDigits(year) : _sixDigits(year);
|
|
||||||
String m = _twoDigits(month);
|
|
||||||
String d = _twoDigits(day);
|
|
||||||
String h = _twoDigits(hour);
|
|
||||||
String min = _twoDigits(minute);
|
|
||||||
String sec = _twoDigits(second);
|
|
||||||
String ms = _threeDigits(millisecond);
|
|
||||||
return "$y-$m-${d}T$h:$min:$sec.${ms}Z";
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _fourDigits(int n) {
|
|
||||||
int absN = n.abs();
|
|
||||||
String sign = n < 0 ? "-" : "";
|
|
||||||
if (absN >= 1000) return "$n";
|
|
||||||
if (absN >= 100) return "${sign}0$absN";
|
|
||||||
if (absN >= 10) return "${sign}00$absN";
|
|
||||||
return "${sign}000$absN";
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _sixDigits(int n) {
|
|
||||||
assert(n < -9999 || n > 9999);
|
|
||||||
int absN = n.abs();
|
|
||||||
String sign = n < 0 ? "-" : "+";
|
|
||||||
if (absN >= 100000) return "$sign$absN";
|
|
||||||
return "${sign}0$absN";
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _threeDigits(int n) {
|
|
||||||
if (n >= 100) return "$n";
|
|
||||||
if (n >= 10) return "0$n";
|
|
||||||
return "00$n";
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _twoDigits(int n) {
|
|
||||||
if (n >= 10) return "$n";
|
|
||||||
return "0$n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import 'package:version/version.dart';
|
|
||||||
|
|
||||||
Version? tryParse(String version) {
|
|
||||||
try {
|
|
||||||
return Version.parse(version);
|
|
||||||
} on FormatException catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
lib/src/util/watch.dart
Normal file
48
lib/src/util/watch.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
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/hosting_controller.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
final SupabaseClient _supabase = Supabase.instance.client;
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
|
||||||
|
extension GameInstanceWatcher on GameInstance {
|
||||||
|
Future<void> startObserver() async {
|
||||||
|
if(watchPid != null) {
|
||||||
|
Process.killPid(watchPid!, ProcessSignal.sigabrt);
|
||||||
|
}
|
||||||
|
|
||||||
|
watchProcess(gamePid).then((value) async {
|
||||||
|
if(hosting) {
|
||||||
|
_onHostingStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGameStopped();
|
||||||
|
});
|
||||||
|
|
||||||
|
watchPid = startBackgroundProcess(
|
||||||
|
'${assetsDirectory.path}\\misc\\watch.exe',
|
||||||
|
[_gameController.uuid, gamePid.toString(), launcherPid?.toString() ?? "-1", eacPid?.toString() ?? "-1", hosting.toString()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onGameStopped() {
|
||||||
|
_gameController.started.value = false;
|
||||||
|
_gameController.instance.value?.kill();
|
||||||
|
if(linkedHosting) {
|
||||||
|
_onHostingStopped();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onHostingStopped() async {
|
||||||
|
_hostingController.started.value = false;
|
||||||
|
_hostingController.instance.value?.kill();
|
||||||
|
await _supabase.from('hosts')
|
||||||
|
.delete()
|
||||||
|
.match({'id': _gameController.uuid});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
import 'package:reboot_launcher/src/util/selector.dart';
|
import 'package:reboot_launcher/src/util/picker.dart';
|
||||||
|
|
||||||
class FileSelector extends StatefulWidget {
|
class FileSelector extends StatefulWidget {
|
||||||
final String placeholder;
|
final String placeholder;
|
||||||
133
lib/src/widget/common/setting_tile.dart
Normal file
133
lib/src/widget/common/setting_tile.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:auto_animated_list/auto_animated_list.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:skeletons/skeletons.dart';
|
||||||
|
|
||||||
|
class SettingTile extends StatefulWidget {
|
||||||
|
static const double kDefaultContentWidth = 200.0;
|
||||||
|
static const double kDefaultSpacing = 8.0;
|
||||||
|
static const double kDefaultHeaderHeight = 72;
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
final TextStyle? titleStyle;
|
||||||
|
final String? subtitle;
|
||||||
|
final TextStyle? subtitleStyle;
|
||||||
|
final Widget? content;
|
||||||
|
final double? contentWidth;
|
||||||
|
final List<Widget>? expandedContent;
|
||||||
|
final double expandedContentHeaderHeight;
|
||||||
|
final double expandedContentSpacing;
|
||||||
|
final bool isChild;
|
||||||
|
|
||||||
|
const SettingTile(
|
||||||
|
{Key? key,
|
||||||
|
this.title,
|
||||||
|
this.titleStyle,
|
||||||
|
this.subtitle,
|
||||||
|
this.subtitleStyle,
|
||||||
|
this.content,
|
||||||
|
this.contentWidth = kDefaultContentWidth,
|
||||||
|
this.expandedContentHeaderHeight = kDefaultHeaderHeight,
|
||||||
|
this.expandedContentSpacing = kDefaultSpacing,
|
||||||
|
this.expandedContent,
|
||||||
|
this.isChild = false})
|
||||||
|
: assert(
|
||||||
|
(title == null && subtitle == null) ||
|
||||||
|
(title != null && subtitle != null),
|
||||||
|
"Title and subtitle can only be null together"),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingTile> createState() => _SettingTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingTileState extends State<SettingTile> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.expandedContent == null || widget.expandedContent?.isEmpty == true) {
|
||||||
|
return _contentCard;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Expander(
|
||||||
|
initiallyExpanded: true,
|
||||||
|
headerShape: (open) => const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
|
||||||
|
),
|
||||||
|
header: SizedBox(
|
||||||
|
height: widget.expandedContentHeaderHeight,
|
||||||
|
child: _buildTile(false)
|
||||||
|
),
|
||||||
|
trailing: _trailing,
|
||||||
|
content: _expandedContent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _expandedContent {
|
||||||
|
var expandedContents = widget.expandedContent!;
|
||||||
|
var separatedContents = List.generate(expandedContents.length * 2, (index) => index % 2 == 0 ? expandedContents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing));
|
||||||
|
return AutoAnimatedList<Widget>(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
shrinkWrap: true,
|
||||||
|
items: separatedContents,
|
||||||
|
itemBuilder: (context, child, index, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _trailing =>
|
||||||
|
SizedBox(width: widget.contentWidth, child: widget.content);
|
||||||
|
|
||||||
|
Widget get _contentCard {
|
||||||
|
if (widget.isChild) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: _buildTile(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)),
|
||||||
|
child: _buildTile(true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTile(bool trailing) {
|
||||||
|
return ListTile(
|
||||||
|
title: widget.title == null ? _skeletonTitle : _title,
|
||||||
|
subtitle: widget.title == null ? _skeletonSubtitle : _subtitle,
|
||||||
|
trailing: trailing ? _trailing : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _title => Text(
|
||||||
|
widget.title!,
|
||||||
|
style:
|
||||||
|
widget.titleStyle ?? FluentTheme.of(context).typography.subtitle,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _skeletonTitle => const SkeletonLine(
|
||||||
|
style: SkeletonLineStyle(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
right: 24.0
|
||||||
|
),
|
||||||
|
height: 18
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _subtitle => Text(
|
||||||
|
widget.subtitle!,
|
||||||
|
style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _skeletonSubtitle => const SkeletonLine(
|
||||||
|
style: SkeletonLineStyle(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 8.0,
|
||||||
|
right: 24.0
|
||||||
|
),
|
||||||
|
height: 13
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,62 +1,42 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:process_run/shell.dart';
|
import 'package:process_run/shell.dart';
|
||||||
import 'package:reboot_launcher/src/../main.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/interactive/game.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
import 'package:reboot_launcher/src/interactive/server.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/game_dialogs.dart';
|
import 'package:reboot_launcher/src/util/watch.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/injector.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/process.dart';
|
|
||||||
|
|
||||||
class LaunchButton extends StatefulWidget {
|
class LaunchButton extends StatefulWidget {
|
||||||
final bool host;
|
final bool host;
|
||||||
final String? startLabel;
|
final String? startLabel;
|
||||||
final String? stopLabel;
|
final String? stopLabel;
|
||||||
final bool Function()? check;
|
final bool Function()? onTap;
|
||||||
|
|
||||||
const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel, this.check}) : super(key: key);
|
const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel, this.onTap}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LaunchButton> createState() => _LaunchButtonState();
|
State<LaunchButton> createState() => _LaunchButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LaunchButtonState extends State<LaunchButton> {
|
class _LaunchButtonState extends State<LaunchButton> {
|
||||||
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
|
||||||
final List<String> _corruptedBuildErrors = [
|
|
||||||
"when 0 bytes remain",
|
|
||||||
"Pak chunk signature verification failed!"
|
|
||||||
];
|
|
||||||
final List<String> _errorStrings = [
|
|
||||||
"port 3551 failed: Connection refused",
|
|
||||||
"Unable to login to Fortnite servers",
|
|
||||||
"HTTP 400 response from ",
|
|
||||||
"Network failure when attempting to check platform restrictions",
|
|
||||||
"UOnlineAccountCommon::ForceLogout"
|
|
||||||
];
|
|
||||||
|
|
||||||
final GlobalKey _headlessServerKey = GlobalKey();
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final File _logFile = File("${assetsDirectory.path}\\logs\\game.log");
|
final File _logFile = File("${logsDirectory.path}\\game.log");
|
||||||
|
Completer<bool> _completer = Completer();
|
||||||
bool _fail = false;
|
bool _fail = false;
|
||||||
Future? _executor;
|
Future? _executor;
|
||||||
|
|
||||||
@@ -87,7 +67,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
String get _stopMessage => widget.stopLabel ?? (widget.host ? "Stop hosting" : "Close fortnite");
|
String get _stopMessage => widget.stopLabel ?? (widget.host ? "Stop hosting" : "Close fortnite");
|
||||||
|
|
||||||
Future<void> _start() async {
|
Future<void> _start() async {
|
||||||
if(widget.check != null && !widget.check!()){
|
if(widget.onTap != null && !widget.onTap!()){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,23 +76,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(_gameController.selectedVersion == null){
|
||||||
|
showMessage("Select a Fortnite version before continuing");
|
||||||
|
_onStop(widget.host);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_setStarted(widget.host, true);
|
_setStarted(widget.host, true);
|
||||||
if (_gameController.username.text.isEmpty) {
|
|
||||||
if(_serverController.type() != ServerType.local){
|
|
||||||
showMessage("Missing username");
|
|
||||||
_onStop(widget.host);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage("No username: expecting self sign in");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gameController.selectedVersion == null) {
|
|
||||||
showMessage("No version is selected");
|
|
||||||
_onStop(widget.host);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var element in Injectable.values) {
|
for (var element in Injectable.values) {
|
||||||
if(await _getDllPath(element, widget.host) == null) {
|
if(await _getDllPath(element, widget.host) == null) {
|
||||||
return;
|
return;
|
||||||
@@ -128,7 +98,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = _serverController.started() || await _serverController.toggle(true);
|
var result = _authenticatorController.started() || await _authenticatorController.toggleInteractive(false);
|
||||||
if(!result){
|
if(!result){
|
||||||
_onStop(widget.host);
|
_onStop(widget.host);
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +106,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
var automaticallyStartedServer = await _startMatchMakingServer();
|
var automaticallyStartedServer = await _startMatchMakingServer();
|
||||||
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
|
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
|
||||||
|
|
||||||
if(widget.host){
|
if(widget.host){
|
||||||
await _showServerLaunchingWarning();
|
await _showServerLaunchingWarning();
|
||||||
}
|
}
|
||||||
@@ -147,7 +116,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startGameProcesses(FortniteVersion version, bool host, bool hasChildServer) async {
|
Future<void> _startGameProcesses(FortniteVersion version, bool host, bool linkedHosting) async {
|
||||||
_setStarted(host, true);
|
_setStarted(host, true);
|
||||||
var launcherProcess = await _createLauncherProcess(version);
|
var launcherProcess = await _createLauncherProcess(version);
|
||||||
var eacProcess = await _createEacProcess(version);
|
var eacProcess = await _createEacProcess(version);
|
||||||
@@ -159,29 +128,23 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var gameProcess = await _createGameProcess(executable.path, host);
|
var gameProcess = await _createGameProcess(executable.path, host);
|
||||||
var watchDogProcess = _createWatchdogProcess(gameProcess, launcherProcess, eacProcess);
|
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, host, linkedHosting);
|
||||||
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, watchDogProcess, hasChildServer);
|
instance.startObserver();
|
||||||
if(host){
|
if(host){
|
||||||
_hostingController.instance = instance;
|
_hostingController.instance.value = instance;
|
||||||
}else{
|
}else{
|
||||||
_gameController.instance = instance;
|
_gameController.instance.value = instance;
|
||||||
}
|
}
|
||||||
_injectOrShowError(Injectable.sslBypass, host);
|
_injectOrShowError(Injectable.sslBypass, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
int _createWatchdogProcess(Process? gameProcess, Process? launcherProcess, Process? eacProcess) => startBackgroundProcess(
|
|
||||||
'${assetsDirectory.path}\\browse\\watch.exe',
|
|
||||||
[_gameController.uuid, _getProcessPid(gameProcess), _getProcessPid(launcherProcess), _getProcessPid(eacProcess)]
|
|
||||||
);
|
|
||||||
|
|
||||||
String _getProcessPid(Process? process) => process?.pid.toString() ?? "-1";
|
|
||||||
|
|
||||||
Future<bool> _startMatchMakingServer() async {
|
Future<bool> _startMatchMakingServer() async {
|
||||||
if(widget.host){
|
if(widget.host){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchmakingIp = _settingsController.matchmakingIp.text;
|
// var matchmakingIp = _settingsController.matchmakingIp.text;
|
||||||
|
var matchmakingIp = "127.0.0.1";
|
||||||
if(!isLocalHost(matchmakingIp)) {
|
if(!isLocalHost(matchmakingIp)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -199,14 +162,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Process> _createGameProcess(String gamePath, bool host) async {
|
Future<int> _createGameProcess(String gamePath, bool host) async {
|
||||||
var gameArgs = createRebootArgs(_safeUsername, _gameController.password.text, host, _gameController.customLaunchArgs.text);
|
var gameArgs = createRebootArgs(_safeUsername, _gameController.password.text, host, _gameController.customLaunchArgs.text);
|
||||||
var gameProcess = await Process.start(gamePath, gameArgs);
|
var gameProcess = await Process.start(gamePath, gameArgs);
|
||||||
gameProcess
|
gameProcess
|
||||||
..exitCode.then((_) => _onEnd())
|
..exitCode.then((_) => _onEnd())
|
||||||
..outLines.forEach((line) => _onGameOutput(line, host))
|
..outLines.forEach((line) => _onGameOutput(line, host))
|
||||||
..errLines.forEach((line) => _onGameOutput(line, host));
|
..errLines.forEach((line) => _onGameOutput(line, host));
|
||||||
return gameProcess;
|
return gameProcess.pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _safeUsername {
|
String get _safeUsername {
|
||||||
@@ -227,26 +190,28 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
|
Future<int?> _createLauncherProcess(FortniteVersion version) async {
|
||||||
var launcherFile = version.launcher;
|
var launcherFile = version.launcher;
|
||||||
if (launcherFile == null) {
|
if (launcherFile == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var launcherProcess = await Process.start(launcherFile.path, []);
|
var launcherProcess = await Process.start(launcherFile.path, []);
|
||||||
suspend(launcherProcess.pid);
|
var pid = launcherProcess.pid;
|
||||||
return launcherProcess;
|
suspend(pid);
|
||||||
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Process?> _createEacProcess(FortniteVersion version) async {
|
Future<int?> _createEacProcess(FortniteVersion version) async {
|
||||||
var eacFile = version.eacExecutable;
|
var eacFile = version.eacExecutable;
|
||||||
if (eacFile == null) {
|
if (eacFile == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var eacProcess = await Process.start(eacFile.path, []);
|
var eacProcess = await Process.start(eacFile.path, []);
|
||||||
suspend(eacProcess.pid);
|
var pid = eacProcess.pid;
|
||||||
return eacProcess;
|
suspend(pid);
|
||||||
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEnd() {
|
void _onEnd() {
|
||||||
@@ -259,29 +224,22 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _closeLaunchingWidget(bool success) {
|
void _closeLaunchingWidget(bool success) {
|
||||||
var context = _headlessServerKey.currentContext;
|
showMessage(
|
||||||
if(context == null || !context.mounted){
|
success ? "The headless server was started successfully" : "An error occurred while starting the headless server",
|
||||||
return;
|
severity: success ? InfoBarSeverity.success : InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
if(!_completer.isCompleted) {
|
||||||
|
_completer.complete(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
var route = ModalRoute.of(appKey.currentContext!);
|
|
||||||
if(route == null || route.isCurrent){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigator.of(context).pop(success);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showServerLaunchingWarning() async {
|
Future<void> _showServerLaunchingWarning() async {
|
||||||
var result = await showDialog<bool>(
|
showMessage(
|
||||||
context: appKey.currentContext!,
|
"Launching headless server...",
|
||||||
builder: (context) => ProgressDialog(
|
loading: true,
|
||||||
key: _headlessServerKey,
|
duration: null
|
||||||
text: "Launching headless server...",
|
);
|
||||||
onStop: () => Navigator.of(context).pop(false)
|
var result = await _completer.future;
|
||||||
)
|
|
||||||
) ?? false;
|
|
||||||
|
|
||||||
if(!result){
|
if(!result){
|
||||||
_onStop(true);
|
_onStop(true);
|
||||||
return;
|
return;
|
||||||
@@ -291,24 +249,36 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var password = _hostingController.password.text;
|
||||||
|
var hasPassword = password.isNotEmpty;
|
||||||
|
var ip = await Ipify.ipv4();
|
||||||
|
if(hasPassword) {
|
||||||
|
ip = aes256Encrypt(ip, password);
|
||||||
|
}
|
||||||
|
|
||||||
var supabase = Supabase.instance.client;
|
var supabase = Supabase.instance.client;
|
||||||
await supabase.from('hosts').insert({
|
await supabase.from('hosts').insert({
|
||||||
'id': _gameController.uuid,
|
'id': _gameController.uuid,
|
||||||
'name': _hostingController.name.text,
|
'name': _hostingController.name.text,
|
||||||
'description': _hostingController.description.text,
|
'description': _hostingController.description.text,
|
||||||
'version': _gameController.selectedVersion?.name ?? 'unknown'
|
'author': _gameController.username.text,
|
||||||
|
'ip': ip,
|
||||||
|
'version': _gameController.selectedVersion?.name,
|
||||||
|
'password': hasPassword ? hashPassword(password) : null,
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
'discoverable': _hostingController.discoverable.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onGameOutput(String line, bool host) {
|
void _onGameOutput(String line, bool host) {
|
||||||
_logFile.createSync(recursive: true);
|
_logFile.createSync(recursive: true);
|
||||||
_logFile.writeAsString("$line\n", mode: FileMode.append);
|
_logFile.writeAsString("$line\n", mode: FileMode.append);
|
||||||
if (line.contains(_shutdownLine)) {
|
if (line.contains(shutdownLine)) {
|
||||||
_onStop(host);
|
_onStop(host);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_corruptedBuildErrors.any((element) => line.contains(element))){
|
if(corruptedBuildErrors.any((element) => line.contains(element))){
|
||||||
if(_fail){
|
if(_fail){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,7 +289,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_errorStrings.any((element) => line.contains(element))){
|
if(cannotConnectErrors.any((element) => line.contains(element))){
|
||||||
if(_fail){
|
if(_fail){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -339,27 +309,20 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_injectOrShowError(Injectable.memoryFix, host);
|
_injectOrShowError(Injectable.memoryFix, host);
|
||||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
var instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
instance?.tokenError = false;
|
instance?.tokenError = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showTokenError(bool host) async {
|
Future<void> _showTokenError(bool host) async {
|
||||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
var instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
if(_serverController.type() != ServerType.embedded) {
|
if(_authenticatorController.type() != ServerType.embedded) {
|
||||||
showTokenErrorUnfixable();
|
showTokenErrorUnfixable();
|
||||||
instance?.tokenError = true;
|
instance?.tokenError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenError = instance?.tokenError;
|
await _authenticatorController.restartInteractive();
|
||||||
instance?.tokenError = true;
|
|
||||||
await _serverController.restart(true);
|
|
||||||
if (tokenError == true) {
|
|
||||||
showTokenErrorCouldNotFix();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showTokenErrorFixable();
|
showTokenErrorFixable();
|
||||||
_onStop(host);
|
_onStop(host);
|
||||||
_start();
|
_start();
|
||||||
@@ -370,17 +333,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
await _executor;
|
await _executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
var instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
if(instance != null){
|
if(instance != null){
|
||||||
if(instance.hasChildServer){
|
if(instance.linkedHosting){
|
||||||
_onStop(true);
|
_onStop(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.kill();
|
instance.kill();
|
||||||
if(host){
|
if(host){
|
||||||
_hostingController.instance = null;
|
_hostingController.instance.value = null;
|
||||||
}else {
|
}else {
|
||||||
_gameController.instance = null;
|
_gameController.instance.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,22 +355,24 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
.delete()
|
.delete()
|
||||||
.match({'id': _gameController.uuid});
|
.match({'id': _gameController.uuid});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_completer = Completer();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
|
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
|
||||||
var instance = hosting ? _hostingController.instance : _gameController.instance;
|
var instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var gameProcess = instance.gameProcess;
|
var gameProcess = instance.gamePid;
|
||||||
var dllPath = await _getDllPath(injectable, hosting);
|
var dllPath = await _getDllPath(injectable, hosting);
|
||||||
if(dllPath == null) {
|
if(dllPath == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await injectDll(gameProcess.pid, dllPath.path);
|
await injectDll(gameProcess, dllPath.path);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showMessage("Cannot inject $injectable.dll: $exception");
|
showMessage("Cannot inject $injectable.dll: $exception");
|
||||||
_onStop(hosting);
|
_onStop(hosting);
|
||||||
338
lib/src/widget/home/pane.dart
Normal file
338
lib/src/widget/home/pane.dart
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
|
||||||
|
class RebootPaneItem extends PaneItem {
|
||||||
|
RebootPaneItem({required super.title, required super.icon, required super.body});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
bool selected,
|
||||||
|
VoidCallback? onPressed, {
|
||||||
|
PaneDisplayMode? displayMode,
|
||||||
|
bool showTextOnTop = true,
|
||||||
|
int? itemIndex,
|
||||||
|
bool? autofocus,
|
||||||
|
}) {
|
||||||
|
final maybeBody = _InheritedNavigationView.maybeOf(context);
|
||||||
|
final mode = displayMode ??
|
||||||
|
maybeBody?.displayMode ??
|
||||||
|
maybeBody?.pane?.displayMode ??
|
||||||
|
PaneDisplayMode.minimal;
|
||||||
|
assert(mode != PaneDisplayMode.auto);
|
||||||
|
assert(debugCheckHasFluentTheme(context));
|
||||||
|
|
||||||
|
final isTransitioning = maybeBody?.isTransitioning ?? false;
|
||||||
|
|
||||||
|
final theme = NavigationPaneTheme.of(context);
|
||||||
|
final titleText = title?.getProperty<String>() ?? '';
|
||||||
|
|
||||||
|
final baseStyle = title?.getProperty<TextStyle>() ?? const TextStyle();
|
||||||
|
|
||||||
|
final isTop = mode == PaneDisplayMode.top;
|
||||||
|
final isMinimal = mode == PaneDisplayMode.minimal;
|
||||||
|
final isCompact = mode == PaneDisplayMode.compact;
|
||||||
|
|
||||||
|
final onItemTapped =
|
||||||
|
(onPressed == null && onTap == null) || !enabled || isTransitioning
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
onPressed?.call();
|
||||||
|
onTap?.call();
|
||||||
|
};
|
||||||
|
|
||||||
|
final button = HoverButton(
|
||||||
|
autofocus: autofocus ?? this.autofocus,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onPressed: onItemTapped,
|
||||||
|
cursor: mouseCursor,
|
||||||
|
focusEnabled: isMinimal ? (maybeBody?.minimalPaneOpen ?? false) : true,
|
||||||
|
forceEnabled: enabled,
|
||||||
|
builder: (context, states) {
|
||||||
|
var textStyle = () {
|
||||||
|
var style = !isTop
|
||||||
|
? (selected
|
||||||
|
? theme.selectedTextStyle?.resolve(states)
|
||||||
|
: theme.unselectedTextStyle?.resolve(states))
|
||||||
|
: (selected
|
||||||
|
? theme.selectedTopTextStyle?.resolve(states)
|
||||||
|
: theme.unselectedTopTextStyle?.resolve(states));
|
||||||
|
if (style == null) return baseStyle;
|
||||||
|
return style.merge(baseStyle);
|
||||||
|
}();
|
||||||
|
|
||||||
|
final textResult = titleText.isNotEmpty
|
||||||
|
? Padding(
|
||||||
|
padding: theme.labelPadding ?? EdgeInsets.zero,
|
||||||
|
child: RichText(
|
||||||
|
text: title!.getProperty<InlineSpan>(textStyle)!,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
softWrap: false,
|
||||||
|
textAlign: title?.getProperty<TextAlign>() ?? TextAlign.start,
|
||||||
|
textHeightBehavior: title?.getProperty<TextHeightBehavior>(),
|
||||||
|
textWidthBasis: title?.getProperty<TextWidthBasis>() ??
|
||||||
|
TextWidthBasis.parent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
Widget result() {
|
||||||
|
final iconThemeData = IconThemeData(
|
||||||
|
color: textStyle.color ??
|
||||||
|
(selected
|
||||||
|
? theme.selectedIconColor?.resolve(states)
|
||||||
|
: theme.unselectedIconColor?.resolve(states)),
|
||||||
|
size: textStyle.fontSize ?? 16.0,
|
||||||
|
);
|
||||||
|
switch (mode) {
|
||||||
|
case PaneDisplayMode.compact:
|
||||||
|
return Container(
|
||||||
|
key: itemKey,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: kPaneItemMinHeight,
|
||||||
|
),
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
child: Padding(
|
||||||
|
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: iconThemeData,
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: () {
|
||||||
|
if (infoBadge != null) {
|
||||||
|
return Stack(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
PositionedDirectional(
|
||||||
|
end: -8,
|
||||||
|
top: -8,
|
||||||
|
child: infoBadge!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case PaneDisplayMode.minimal:
|
||||||
|
case PaneDisplayMode.open:
|
||||||
|
final shouldShowTrailing = !isTransitioning;
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
key: itemKey,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: kPaneItemMinHeight,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Padding(
|
||||||
|
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: iconThemeData,
|
||||||
|
child: Center(child: icon),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: textResult),
|
||||||
|
if (shouldShowTrailing) ...[
|
||||||
|
if (infoBadge != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 8.0),
|
||||||
|
child: infoBadge!,
|
||||||
|
),
|
||||||
|
if (trailing != null)
|
||||||
|
IconTheme.merge(
|
||||||
|
data: const IconThemeData(size: 16.0),
|
||||||
|
child: trailing!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
case PaneDisplayMode.top:
|
||||||
|
Widget result = Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Padding(
|
||||||
|
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: iconThemeData,
|
||||||
|
child: Center(child: icon),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showTextOnTop) textResult,
|
||||||
|
if (trailing != null)
|
||||||
|
IconTheme.merge(
|
||||||
|
data: const IconThemeData(size: 16.0),
|
||||||
|
child: trailing!,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
if (infoBadge != null) {
|
||||||
|
return Stack(key: itemKey, clipBehavior: Clip.none, children: [
|
||||||
|
result,
|
||||||
|
if (infoBadge != null)
|
||||||
|
PositionedDirectional(
|
||||||
|
end: -3,
|
||||||
|
top: 3,
|
||||||
|
child: infoBadge!,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return KeyedSubtree(key: itemKey, child: result);
|
||||||
|
default:
|
||||||
|
throw '$mode is not a supported type';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
label: titleText.isEmpty ? null : titleText,
|
||||||
|
selected: selected,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: () {
|
||||||
|
final tileColor = this.tileColor ??
|
||||||
|
theme.tileColor ??
|
||||||
|
kDefaultPaneItemColor(context, isTop);
|
||||||
|
final newStates = states.toSet()..remove(ButtonStates.disabled);
|
||||||
|
if (selected && selectedTileColor != null) {
|
||||||
|
return selectedTileColor!.resolve(newStates);
|
||||||
|
}
|
||||||
|
return tileColor.resolve(
|
||||||
|
selected
|
||||||
|
? {
|
||||||
|
states.isHovering
|
||||||
|
? ButtonStates.pressing
|
||||||
|
: ButtonStates.hovering,
|
||||||
|
}
|
||||||
|
: newStates,
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: FocusBorder(
|
||||||
|
focused: states.isFocused,
|
||||||
|
renderOutside: false,
|
||||||
|
child: () {
|
||||||
|
final showTooltip = ((isTop && !showTextOnTop) || isCompact) &&
|
||||||
|
titleText.isNotEmpty &&
|
||||||
|
!states.isDisabled;
|
||||||
|
|
||||||
|
if (showTooltip) {
|
||||||
|
return Tooltip(
|
||||||
|
richMessage: title?.getProperty<InlineSpan>(),
|
||||||
|
style: TooltipThemeData(textStyle: baseStyle),
|
||||||
|
child: result(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result();
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final index = () {
|
||||||
|
if (itemIndex != null) return itemIndex;
|
||||||
|
if (maybeBody?.pane?.indicator != null) {
|
||||||
|
return maybeBody!.pane!.effectiveIndexOf(this);
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
key: key,
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(horizontal: 12.0, vertical: 2.0),
|
||||||
|
child: () {
|
||||||
|
if (maybeBody?.pane?.indicator != null &&
|
||||||
|
index != null &&
|
||||||
|
!index.isNegative) {
|
||||||
|
final key = PaneItemKeys.of(index, context);
|
||||||
|
|
||||||
|
return Stack(children: [
|
||||||
|
button,
|
||||||
|
Positioned.fill(
|
||||||
|
child: _InheritedNavigationView.merge(
|
||||||
|
currentItemIndex: index,
|
||||||
|
currentItemSelected: selected,
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: key,
|
||||||
|
child: maybeBody!.pane!.indicator!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InheritedNavigationView extends InheritedWidget {
|
||||||
|
const _InheritedNavigationView({
|
||||||
|
super.key,
|
||||||
|
required super.child,
|
||||||
|
required this.displayMode,
|
||||||
|
this.minimalPaneOpen = false,
|
||||||
|
this.pane,
|
||||||
|
this.previousItemIndex = 0,
|
||||||
|
this.currentItemIndex = -1,
|
||||||
|
this.isTransitioning = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PaneDisplayMode displayMode;
|
||||||
|
|
||||||
|
final bool minimalPaneOpen;
|
||||||
|
|
||||||
|
final NavigationPane? pane;
|
||||||
|
|
||||||
|
final int previousItemIndex;
|
||||||
|
|
||||||
|
final int currentItemIndex;
|
||||||
|
|
||||||
|
final bool isTransitioning;
|
||||||
|
|
||||||
|
static _InheritedNavigationView? maybeOf(BuildContext context) {
|
||||||
|
return context
|
||||||
|
.dependOnInheritedWidgetOfExactType<_InheritedNavigationView>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget merge({
|
||||||
|
Key? key,
|
||||||
|
required Widget child,
|
||||||
|
int? currentItemIndex,
|
||||||
|
NavigationPane? pane,
|
||||||
|
PaneDisplayMode? displayMode,
|
||||||
|
bool? minimalPaneOpen,
|
||||||
|
int? previousItemIndex,
|
||||||
|
bool? currentItemSelected,
|
||||||
|
bool? isTransitioning,
|
||||||
|
}) {
|
||||||
|
return Builder(builder: (context) {
|
||||||
|
final current = _InheritedNavigationView.maybeOf(context);
|
||||||
|
return _InheritedNavigationView(
|
||||||
|
key: key,
|
||||||
|
displayMode:
|
||||||
|
displayMode ?? current?.displayMode ?? PaneDisplayMode.open,
|
||||||
|
minimalPaneOpen: minimalPaneOpen ?? current?.minimalPaneOpen ?? false,
|
||||||
|
currentItemIndex: currentItemIndex ?? current?.currentItemIndex ?? -1,
|
||||||
|
pane: pane ?? current?.pane,
|
||||||
|
previousItemIndex: previousItemIndex ?? current?.previousItemIndex ?? 0,
|
||||||
|
isTransitioning: isTransitioning ?? current?.isTransitioning ?? false,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(covariant _InheritedNavigationView oldWidget) {
|
||||||
|
return oldWidget.displayMode != displayMode ||
|
||||||
|
oldWidget.minimalPaneOpen != minimalPaneOpen ||
|
||||||
|
oldWidget.pane != pane ||
|
||||||
|
oldWidget.previousItemIndex != previousItemIndex ||
|
||||||
|
oldWidget.currentItemIndex != currentItemIndex ||
|
||||||
|
oldWidget.isTransitioning != isTransitioning;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
lib/src/widget/home/profile.dart
Normal file
101
lib/src/widget/home/profile.dart
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
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/interactive/profile.dart';
|
||||||
|
|
||||||
|
class ProfileWidget extends StatefulWidget {
|
||||||
|
const ProfileWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileWidget> createState() => _ProfileWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileWidgetState extends State<ProfileWidget> {
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12.0,
|
||||||
|
horizontal: 12.0
|
||||||
|
),
|
||||||
|
child: Button(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: ButtonState.all(EdgeInsets.zero),
|
||||||
|
backgroundColor: ButtonState.all(Colors.transparent),
|
||||||
|
border: ButtonState.all(const BorderSide(color: Colors.transparent))
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if(await showProfileForm(context)) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle
|
||||||
|
),
|
||||||
|
child: Image.asset("assets/images/user.png")
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12.0,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_username,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600
|
||||||
|
),
|
||||||
|
maxLines: 1
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_email,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w100
|
||||||
|
),
|
||||||
|
maxLines: 1
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
String get _username {
|
||||||
|
var username = _gameController.username.text;
|
||||||
|
if(username.isEmpty) {
|
||||||
|
return kDefaultPlayerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var atIndex = username.indexOf("@");
|
||||||
|
if(atIndex == -1) {
|
||||||
|
return username.substring(0, 1).toUpperCase() + username.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = username.substring(0, atIndex);
|
||||||
|
return result.substring(0, 1).toUpperCase() + result.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _email {
|
||||||
|
var username = _gameController.username.text;
|
||||||
|
if(username.isEmpty) {
|
||||||
|
return "$kDefaultPlayerName@projectreboot.dev";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(username.contains("@")) {
|
||||||
|
return username.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$username@projectreboot.dev".toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
class WindowBorder extends StatelessWidget {
|
class WindowBorder extends StatelessWidget {
|
||||||
@@ -17,7 +17,7 @@ class WindowBorder extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: SystemTheme.accentColor.accent,
|
color: SystemTheme.accentColor.accent,
|
||||||
width: appBarSize.toDouble()
|
width: appBarWidth.toDouble()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'icons.dart';
|
import 'icons.dart';
|
||||||
import 'mouse_state_builder.dart';
|
import 'mouse.dart';
|
||||||
|
|
||||||
typedef WindowButtonIconBuilder = Widget Function(
|
typedef WindowButtonIconBuilder = Widget Function(
|
||||||
WindowButtonContext buttonContext);
|
WindowButtonContext buttonContext);
|
||||||
@@ -2,9 +2,6 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// Switched to CustomPaint icons by https://github.com/esDotDev
|
|
||||||
|
|
||||||
/// Close
|
|
||||||
class CloseIcon extends StatelessWidget {
|
class CloseIcon extends StatelessWidget {
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
@@ -14,7 +11,6 @@ class CloseIcon extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) => Align(
|
Widget build(BuildContext context) => Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
// Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
|
|
||||||
Transform.rotate(
|
Transform.rotate(
|
||||||
angle: pi * .25,
|
angle: pi * .25,
|
||||||
child:
|
child:
|
||||||
@@ -27,7 +23,6 @@ class CloseIcon extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximize
|
|
||||||
class MaximizeIcon extends StatelessWidget {
|
class MaximizeIcon extends StatelessWidget {
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
@@ -47,7 +42,6 @@ class _MaximizePainter extends _IconPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore
|
|
||||||
class RestoreIcon extends StatelessWidget {
|
class RestoreIcon extends StatelessWidget {
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
@@ -76,7 +70,6 @@ class _RestorePainter extends _IconPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimize
|
|
||||||
class MinimizeIcon extends StatelessWidget {
|
class MinimizeIcon extends StatelessWidget {
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
@@ -97,7 +90,6 @@ class _MinimizePainter extends _IconPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helpers
|
|
||||||
abstract class _IconPainter extends CustomPainter {
|
abstract class _IconPainter extends CustomPainter {
|
||||||
_IconPainter(this.color);
|
_IconPainter(this.color);
|
||||||
|
|
||||||
@@ -69,6 +69,8 @@ class _MouseStateBuilderState extends State<MouseStateBuilder> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onTapUp: (_) {},
|
onTapUp: (_) {},
|
||||||
child: widget.builder(context, _mouseState)));
|
child: widget.builder(context, _mouseState)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:reboot_launcher/src/ui/widget/os/window_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/os/buttons.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
class WindowTitleBar extends StatelessWidget {
|
class WindowTitleBar extends StatelessWidget {
|
||||||
49
lib/src/widget/server/start_button.dart
Normal file
49
lib/src/widget/server/start_button.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/interactive/server.dart';
|
||||||
|
|
||||||
|
class ServerButton extends StatefulWidget {
|
||||||
|
final bool authenticator;
|
||||||
|
const ServerButton({Key? key, required this.authenticator}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServerButton> createState() => _ServerButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServerButtonState extends State<ServerButton> {
|
||||||
|
late final ServerController _controller = widget.authenticator ? Get.find<AuthenticatorController>() : Get.find<MatchmakerController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Align(
|
||||||
|
alignment: AlignmentDirectional.bottomCenter,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Obx(() => SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Button(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(_buttonText),
|
||||||
|
),
|
||||||
|
onPressed: () => _controller.toggleInteractive()
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
String get _buttonText {
|
||||||
|
if(_controller.type.value == ServerType.local){
|
||||||
|
return "Check ${_controller.controllerName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_controller.started.value){
|
||||||
|
return "Stop ${_controller.controllerName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Start ${_controller.controllerName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/src/widget/server/type_selector.dart
Normal file
57
lib/src/widget/server/type_selector.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/src/model/server_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
|
||||||
|
class ServerTypeSelector extends StatefulWidget {
|
||||||
|
final bool authenticator;
|
||||||
|
|
||||||
|
const ServerTypeSelector({Key? key, required this.authenticator})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServerTypeSelector> createState() => _ServerTypeSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||||
|
late final ServerController _controller = widget.authenticator ? Get.find<AuthenticatorController>() : Get.find<MatchmakerController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() => DropDownButton(
|
||||||
|
leading: Text(_controller.type.value.label),
|
||||||
|
items: ServerType.values
|
||||||
|
.map((type) => _createItem(type))
|
||||||
|
.toList()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuFlyoutItem _createItem(ServerType type) {
|
||||||
|
return MenuFlyoutItem(
|
||||||
|
text: Tooltip(
|
||||||
|
message: type.message,
|
||||||
|
child: Text(type.label)
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
await _controller.stop();
|
||||||
|
_controller.type.value = type;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ServerTypeExtension on ServerType {
|
||||||
|
String get label {
|
||||||
|
return this == ServerType.embedded ? "Embedded"
|
||||||
|
: this == ServerType.remote ? "Remote"
|
||||||
|
: "Local";
|
||||||
|
}
|
||||||
|
|
||||||
|
String get message {
|
||||||
|
return this == ServerType.embedded ? "A server will be automatically started in the background"
|
||||||
|
: this == ServerType.remote ? "A reverse proxy to the remote server will be created"
|
||||||
|
: "Assumes that you are running yourself the server locally";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_name_input.dart';
|
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/util/checks.dart';
|
import 'package:reboot_launcher/src/util/checks.dart';
|
||||||
import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart';
|
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
|
||||||
import 'dialog.dart';
|
import 'package:reboot_launcher/src/widget/version/version_name_input.dart';
|
||||||
import 'dialog_button.dart';
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class AddLocalVersion extends StatelessWidget {
|
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 GameController _gameController = Get.find<GameController>();
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final TextEditingController _gamePathController = TextEditingController();
|
final TextEditingController _gamePathController = TextEditingController();
|
||||||
|
|
||||||
AddLocalVersion({Key? key})
|
@override
|
||||||
: super(key: key);
|
void initState() {
|
||||||
|
_gamePathController.addListener(() async {
|
||||||
|
var file = Directory(_gamePathController.text);
|
||||||
|
if(await file.exists()) {
|
||||||
|
if(_nameController.text.isEmpty) {
|
||||||
|
_nameController.text = path.basename(_gamePathController.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -47,7 +66,7 @@ class AddLocalVersion extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
FileSelector(
|
FileSelector(
|
||||||
label: "Path",
|
label: "Game folder",
|
||||||
placeholder: "Type the game folder",
|
placeholder: "Type the game folder",
|
||||||
windowTitle: "Select game folder",
|
windowTitle: "Select game folder",
|
||||||
controller: _gamePathController,
|
controller: _gamePathController,
|
||||||
@@ -70,10 +89,10 @@ class AddLocalVersion extends StatelessWidget {
|
|||||||
type: ButtonType.primary,
|
type: ButtonType.primary,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
_gameController.addVersion(FortniteVersion(
|
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||||
name: _nameController.text,
|
name: _nameController.text,
|
||||||
location: Directory(_gamePathController.text)
|
location: Directory(_gamePathController.text)
|
||||||
));
|
)));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
342
lib/src/widget/version/add_server_version.dart
Normal file
342
lib/src/widget/version/add_server_version.dart
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/version/version_build_selector.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/version/version_name_input.dart';
|
||||||
|
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||||
|
|
||||||
|
import 'package:reboot_launcher/src/util/checks.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
|
||||||
|
import '../../dialog/dialog.dart';
|
||||||
|
import '../../dialog/dialog_button.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> _downloadProgress = Rxn();
|
||||||
|
|
||||||
|
late DiskSpace _diskSpace;
|
||||||
|
late Future _fetchFuture;
|
||||||
|
late Future _diskFuture;
|
||||||
|
|
||||||
|
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}\\misc\\stop.bat', []);
|
||||||
|
_downloadPort?.send("kill");
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: "Fetching builds and disks...",
|
||||||
|
onStop: () => Navigator.of(context).pop()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormDialog(
|
||||||
|
content: _formBody,
|
||||||
|
buttons: _formButtons
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
case DownloadStatus.downloading:
|
||||||
|
return GenericDialog(
|
||||||
|
header: _downloadBody,
|
||||||
|
buttons: _stopButton
|
||||||
|
);
|
||||||
|
case DownloadStatus.extracting:
|
||||||
|
return GenericDialog(
|
||||||
|
header: _extractingBody,
|
||||||
|
buttons: _stopButton
|
||||||
|
);
|
||||||
|
case DownloadStatus.error:
|
||||||
|
return ErrorDialog(
|
||||||
|
exception: _error ?? Exception("unknown error"),
|
||||||
|
stackTrace: _stackTrace,
|
||||||
|
errorMessageBuilder: (exception) => "Cannot download version: $exception"
|
||||||
|
);
|
||||||
|
case DownloadStatus.done:
|
||||||
|
return const InfoDialog(
|
||||||
|
text: "The download was completed successfully!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
List<DialogButton> get _formButtons => [
|
||||||
|
DialogButton(type: ButtonType.secondary),
|
||||||
|
DialogButton(
|
||||||
|
text: "Download",
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onTap: () => _startDownload(context),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
void _startDownload(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
var build = _buildController.selectedBuild.value;
|
||||||
|
if(build == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_status.value = DownloadStatus.downloading;
|
||||||
|
var communicationPort = ReceivePort();
|
||||||
|
communicationPort.listen((message) {
|
||||||
|
if(message is ArchiveDownloadProgress) {
|
||||||
|
_onDownloadProgress(message.progress, message.minutesLeft, message.extracting);
|
||||||
|
}else if(message is SendPort) {
|
||||||
|
_downloadPort = message;
|
||||||
|
}else {
|
||||||
|
_onDownloadError("Unexpected message: $message", null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var options = ArchiveDownloadOptions(
|
||||||
|
build.link,
|
||||||
|
Directory(_pathController.text),
|
||||||
|
communicationPort.sendPort
|
||||||
|
);
|
||||||
|
var errorPort = ReceivePort();
|
||||||
|
errorPort.listen((message) => _onDownloadError(message, null));
|
||||||
|
var exitPort = ReceivePort();
|
||||||
|
exitPort.listen((message) {
|
||||||
|
if(_status.value != DownloadStatus.error) {
|
||||||
|
_onDownloadComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Isolate.spawn(
|
||||||
|
downloadArchiveBuild,
|
||||||
|
options,
|
||||||
|
onError: errorPort.sendPort,
|
||||||
|
onExit: exitPort.sendPort,
|
||||||
|
errorsAreFatal: true
|
||||||
|
);
|
||||||
|
} catch (exception, stackTrace) {
|
||||||
|
_onDownloadError(exception, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDownloadComplete() async {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_status.value = DownloadStatus.done;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||||
|
name: _nameController.text,
|
||||||
|
location: Directory(_pathController.text)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_status.value = DownloadStatus.error;
|
||||||
|
_error = error;
|
||||||
|
_stackTrace = stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDownloadProgress(double progress, int timeLeft, bool extracting) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||||
|
_timeLeft.value = timeLeft;
|
||||||
|
_downloadProgress.value = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _downloadBody {
|
||||||
|
var timeLeft = _timeLeft.value;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
"Downloading...",
|
||||||
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${(_downloadProgress.value ?? 0).round()}%",
|
||||||
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
|
),
|
||||||
|
|
||||||
|
if(timeLeft != null)
|
||||||
|
Text(
|
||||||
|
"Time left: ${timeLeft == 0 ? "less than a minute" : "about $timeLeft minute${timeLeft > 1 ? 's' : ''}"}",
|
||||||
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble())
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _extractingBody => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
"Extracting...",
|
||||||
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ProgressBar()
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _formBody => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
BuildSelector(
|
||||||
|
onSelected: _updateFormDefaults
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
|
),
|
||||||
|
|
||||||
|
VersionNameInput(
|
||||||
|
controller: _nameController
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
|
),
|
||||||
|
|
||||||
|
FileSelector(
|
||||||
|
label: "Installation directory",
|
||||||
|
placeholder: "Type the installation directory",
|
||||||
|
windowTitle: "Select installation directory",
|
||||||
|
controller: _pathController,
|
||||||
|
validator: checkDownloadDestination,
|
||||||
|
folder: true
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
List<DialogButton> get _stopButton => [
|
||||||
|
DialogButton(
|
||||||
|
text: "Stop",
|
||||||
|
type: ButtonType.only
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> _updateFormDefaults() async {
|
||||||
|
if(_diskSpace.disks.isEmpty){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _fetchFuture;
|
||||||
|
var bestDisk = _diskSpace.disks
|
||||||
|
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||||
|
var build = _buildController.selectedBuild.value;
|
||||||
|
if(build== null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||||
|
_nameController.text = build.version.toString();
|
||||||
|
_formKey.currentState?.validate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||||
|
|
||||||
class BuildSelector extends StatefulWidget {
|
class BuildSelector extends StatefulWidget {
|
||||||
final Function() onSelected;
|
final Function() onSelected;
|
||||||
@@ -23,13 +23,13 @@ class _BuildSelectorState extends State<BuildSelector> {
|
|||||||
placeholder: const Text('Select a fortnite build'),
|
placeholder: const Text('Select a fortnite build'),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: _createItems(),
|
items: _createItems(),
|
||||||
value: _buildController.selectedBuildRx.value,
|
value: _buildController.selectedBuild.value,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if(value == null){
|
if(value == null){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildController.selectedBuildRx.value = value;
|
_buildController.selectedBuild.value = value;
|
||||||
widget.onSelected();
|
widget.onSelected();
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
|
||||||
class VersionNameInput extends StatelessWidget {
|
class VersionNameInput extends StatelessWidget {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart';
|
import 'package:reboot_launcher/src/widget/version/add_local_version.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
|
import 'package:reboot_launcher/src/widget/version/add_server_version.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart';
|
import 'package:reboot_launcher/src/dialog/message.dart';
|
||||||
import 'package:reboot_launcher/src/util/checks.dart';
|
import 'package:reboot_launcher/src/util/checks.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart';
|
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
|
||||||
|
|
||||||
class VersionSelector extends StatefulWidget {
|
class VersionSelector extends StatefulWidget {
|
||||||
const VersionSelector({Key? key}) : super(key: key);
|
const VersionSelector({Key? key}) : super(key: key);
|
||||||
|
|
||||||
static void openDownloadDialog(BuildContext context) async {
|
static Future<void> openDownloadDialog() => showDialog<bool>(
|
||||||
await showDialog<bool>(
|
builder: (context) => const AddServerVersion(),
|
||||||
context: context,
|
|
||||||
builder: (dialogContext) => const AddServerVersion()
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
static void openAddDialog(BuildContext context) async {
|
static Future<void> openAddDialog() => showDialog<bool>(
|
||||||
await showDialog<bool>(
|
builder: (context) => const AddLocalVersion(),
|
||||||
context: context,
|
);
|
||||||
builder: (context) => AddLocalVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<VersionSelector> createState() => _VersionSelectorState();
|
State<VersionSelector> createState() => _VersionSelectorState();
|
||||||
@@ -39,7 +33,7 @@ class VersionSelector extends StatefulWidget {
|
|||||||
|
|
||||||
class _VersionSelectorState extends State<VersionSelector> {
|
class _VersionSelectorState extends State<VersionSelector> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final CheckboxController _deleteFilesController = CheckboxController();
|
final RxBool _deleteFilesController = RxBool(false);
|
||||||
final FlyoutController _flyoutController = FlyoutController();
|
final FlyoutController _flyoutController = FlyoutController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -154,19 +148,12 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _onExplorerError() {
|
bool _onExplorerError() {
|
||||||
showSnackbar(
|
showMessage("This version doesn't exist on the local machine");
|
||||||
context,
|
|
||||||
const Snackbar(
|
|
||||||
content: Text("This version doesn't exist on the local machine", textAlign: TextAlign.center),
|
|
||||||
extended: true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
|
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
|
||||||
return showDialog<bool>(
|
return showDialog<bool>(
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
builder: (context) => ContentDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -179,10 +166,11 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
|
|
||||||
const SizedBox(height: 12.0),
|
const SizedBox(height: 12.0),
|
||||||
|
|
||||||
SmartCheckBox(
|
Obx(() => Checkbox(
|
||||||
controller: _deleteFilesController,
|
checked: _deleteFilesController.value,
|
||||||
|
onChanged: (bool? value) => _deleteFilesController.value = value ?? false,
|
||||||
content: const Text("Delete version files from disk")
|
content: const Text("Delete version files from disk")
|
||||||
)
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -203,7 +191,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
var nameController = TextEditingController(text: version.name);
|
var nameController = TextEditingController(text: version.name);
|
||||||
var pathController = TextEditingController(text: version.location.path);
|
var pathController = TextEditingController(text: version.location.path);
|
||||||
return showDialog<String?>(
|
return showDialog<String?>(
|
||||||
context: context,
|
|
||||||
builder: (context) => FormDialog(
|
builder: (context) => FormDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co';
|
|
||||||
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M';
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:reboot_launcher/supabase.dart';
|
|
||||||
import 'package:supabase/supabase.dart';
|
|
||||||
|
|
||||||
void main(List<String> args) async {
|
|
||||||
if(args.length != 4){
|
|
||||||
stderr.writeln("Wrong args length: $args");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var instance = _GameInstance(args[0], int.parse(args[1]), int.parse(args[2]), int.parse(args[3]));
|
|
||||||
var supabase = SupabaseClient(supabaseUrl, supabaseAnonKey);
|
|
||||||
while(true){
|
|
||||||
sleep(const Duration(seconds: 2));
|
|
||||||
stdout.writeln("Looking up tasks...");
|
|
||||||
var result = Process.runSync('tasklist', []);
|
|
||||||
var output = result.stdout.toString();
|
|
||||||
if(output.contains(" ${instance.gameProcess} ")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln("Killing $instance");
|
|
||||||
Process.killPid(instance.gameProcess, ProcessSignal.sigabrt);
|
|
||||||
if(instance.launcherProcess != -1){
|
|
||||||
Process.killPid(instance.launcherProcess, ProcessSignal.sigabrt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(instance.eacProcess != -1){
|
|
||||||
Process.killPid(instance.eacProcess, ProcessSignal.sigabrt);
|
|
||||||
}
|
|
||||||
|
|
||||||
await supabase.from('hosts')
|
|
||||||
.delete()
|
|
||||||
.match({'id': instance.uuid});
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GameInstance {
|
|
||||||
final String uuid;
|
|
||||||
final int gameProcess;
|
|
||||||
final int launcherProcess;
|
|
||||||
final int eacProcess;
|
|
||||||
|
|
||||||
_GameInstance(this.uuid, this.gameProcess, this.launcherProcess, this.eacProcess);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return '{uuid: $uuid, gameProcess: $gameProcess, launcherProcess: $launcherProcess, eacProcess: $eacProcess}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
pubspec.yaml
43
pubspec.yaml
@@ -1,47 +1,52 @@
|
|||||||
name: reboot_launcher
|
name: reboot_launcher
|
||||||
description: Launcher for project reboot
|
description: Graphical User Interface for Project Reboot
|
||||||
version: "8.1.0"
|
version: "1.0.0"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.6 <=3.3.3"
|
sdk: ">=2.19.0 <=3.3.3"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
reboot_common:
|
||||||
|
path: ./../common
|
||||||
fluent_ui: ^4.7.3
|
fluent_ui: ^4.7.3
|
||||||
bitsdojo_window_windows: ^0.1.5
|
bitsdojo_window_windows: ^0.1.5
|
||||||
system_theme: ^2.0.0
|
system_theme: ^2.0.0
|
||||||
http: ^0.13.5
|
|
||||||
html: ^0.15.0
|
|
||||||
shared_preferences: ^2.0.15
|
|
||||||
file_picker: ^5.2.0+1
|
file_picker: ^5.2.0+1
|
||||||
context_menus: ^1.0.1
|
|
||||||
process_run: ^0.12.3+2
|
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
archive: ^3.3.1
|
archive: ^3.3.1
|
||||||
version: ^3.0.2
|
|
||||||
crypto: ^3.0.2
|
crypto: ^3.0.2
|
||||||
async: ^2.8.2
|
async: ^2.8.2
|
||||||
get: ^4.6.5
|
get: ^4.6.5
|
||||||
get_storage: ^2.0.3
|
get_storage: ^2.0.3
|
||||||
window_manager: ^0.2.7
|
window_manager: ^0.2.7
|
||||||
shelf_proxy: ^1.0.2
|
|
||||||
args: ^2.3.1
|
|
||||||
win32: 3.0.0
|
win32: 3.0.0
|
||||||
clipboard: ^0.1.3
|
clipboard: ^0.1.3
|
||||||
sync: ^0.3.0
|
|
||||||
ini: ^2.1.0
|
|
||||||
universal_disk_space: ^0.2.3
|
universal_disk_space: ^0.2.3
|
||||||
jaguar: ^3.1.3
|
|
||||||
hex: ^0.2.0
|
|
||||||
uuid: ^3.0.6
|
uuid: ^3.0.6
|
||||||
supabase_flutter: ^1.10.0
|
supabase_flutter: ^1.10.0
|
||||||
supabase: ^1.9.1
|
skeletons: ^0.0.3
|
||||||
fluentui_system_icons: ^1.1.202
|
bcrypt: ^1.1.3
|
||||||
|
upnp2: ^3.0.10
|
||||||
|
dart_ipify: ^1.1.1
|
||||||
|
path: ^1.8.3
|
||||||
|
pointycastle: ^3.7.3
|
||||||
|
sync: ^0.3.0
|
||||||
|
process_run: ^0.13.1
|
||||||
|
auto_animated_list: ^1.0.4
|
||||||
flutter_acrylic: ^1.1.3
|
flutter_acrylic: ^1.1.3
|
||||||
|
app_links: ^3.4.3
|
||||||
|
url_protocol: ^1.0.0
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
xml: ^6.3.0
|
||||||
|
http: ^0.13.5
|
||||||
|
win32: ^3.0.0
|
||||||
|
ffi: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -54,8 +59,7 @@ dev_dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- assets/builds/
|
- assets/misc/
|
||||||
- assets/browse/
|
|
||||||
- assets/dlls/
|
- assets/dlls/
|
||||||
- assets/icons/
|
- assets/icons/
|
||||||
- assets/images/
|
- assets/images/
|
||||||
@@ -74,5 +78,4 @@ msix_config:
|
|||||||
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
||||||
logo_path: ./assets/icons/reboot.ico
|
logo_path: ./assets/icons/reboot.ico
|
||||||
architecture: x64
|
architecture: x64
|
||||||
store: true
|
|
||||||
capabilities: "internetClient"
|
capabilities: "internetClient"
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Reboot Launcher
|
|
||||||
|
|
||||||
Developed by Auties00
|
|
||||||
|
|
||||||
The main page
|
|
||||||
|
|
||||||
### Username input
|
|
||||||
|
|
||||||
Pretty easy to use, just write the name that you want people to see in game. It will automatically change based on the state of the host switch that you can see in the same page. This is done because, as the launcher supports friends, you probably want different usernames. Anyways they will be memorized and change as I just wrote based on the host switch.
|
|
||||||
|
|
||||||
### Version Selector
|
|
||||||
|
|
||||||
Just use it to select a Fortnite version to launch, if you have none use the first button to the right if you already have the one you need installed on your pc(Add a local build) or the second to the right if you need to install it from the cloud(Download a build).
|
|
||||||
|
|
||||||
### Add a local build
|
|
||||||
|
|
||||||
It's the first button to the right of the version selector, you can use it add a Fortnite build that you already have on the launcher
|
|
||||||
|
|
||||||
### Download a build
|
|
||||||
|
|
||||||
It's the second button to the right of the version selector in the home, you can use it to download practically any Fortnite build that exists.
|
|
||||||
Builds marked as "Fortnite Manifest" are very fast to download and can be downloaded freely.
|
|
||||||
Builds marked as "Google Drive" are slower to download as the file has to be unarchived. Also if you download too many builds in a very short amount of time Google may rate limit you(error code 429).
|
|
||||||
Some google builds may be unavailable temporarily(status code 404).
|
|
||||||
In conclusion if you have to download a build, prefer the ones marked as "Fortnite Manifest"
|
|
||||||
|
|
||||||
### Launch / Close
|
|
||||||
|
|
||||||
Use this button to launch the game. Then click it again when it says close to close the game. Remember this last part because some goofy ass actually asked me why the game was crashing, plot twist: they were closing the game.
|
|
||||||
|
|
||||||
### Host Switch
|
|
||||||
|
|
||||||
Whether the reboot.dll should be injected for you to be able to host games. If not on, you will be instead able to play games
|
|
||||||
|
|
||||||
## Server page
|
|
||||||
|
|
||||||
Pretty much don't touch this except you really know what you are doing
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Host
|
|
||||||
|
|
||||||
The host of the remote server to use for the backend server. Only enabled if you are not using the embedded server.
|
|
||||||
|
|
||||||
### Port
|
|
||||||
|
|
||||||
The port of the remote server to use for the backend server. Only enabled if you are not using the embedded server.
|
|
||||||
|
|
||||||
### Embedded
|
|
||||||
|
|
||||||
A switch to determine whether an embedded backend server should be used or if you want to use a remote one
|
|
||||||
|
|
||||||
### Check address / Start or Stop server
|
|
||||||
|
|
||||||
If the embedded switch is off, this button checks that the remote backend actually works. If it's on, instead, it will start or stop the lawin server. If you click on the launch button in the launcher page, the server will automatically start if you are using the embedded server and if it's not already running. If the 3551 port is already in use on your pc, the launcher will tell you and provide an option to stop the associated process automatically.
|
|
||||||
|
|
||||||
# Info
|
|
||||||
|
|
||||||
Just a nice page to see the version of the launcher and the join discord button
|
|
||||||
|
|
||||||
# FAQ
|
|
||||||
|
|
||||||
1. Does the Reboot DLL auto update?
|
|
||||||
Yes, once every 24 hours
|
|
||||||
2. Can I launch multiple game instances?
|
|
||||||
Obviously, just open the launcher again. You can have as many windows as you like.
|
|
||||||
3. Where can I download the launcher?
|
|
||||||
Discord or soon the Microsoft store
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user