mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
Reboot v3
This commit is contained in:
@@ -73,7 +73,6 @@ void main(List<String> args) async {
|
||||
}
|
||||
|
||||
await patchHeadless(version.executable!);
|
||||
await patchMatchmaking(version.executable!);
|
||||
|
||||
var serverType = getServerType(result);
|
||||
var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"];
|
||||
|
||||
109
lib/main.dart
109
lib/main.dart
@@ -14,50 +14,60 @@ import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/home_page.dart';
|
||||
import 'package:reboot_launcher/supabase.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
|
||||
const double kDefaultWindowWidth = 885;
|
||||
const double kDefaultWindowHeight = 885;
|
||||
const double kDefaultWindowWidth = 1024;
|
||||
const double kDefaultWindowHeight = 1024;
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
void main() async {
|
||||
await installationDirectory.create(recursive: true);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemTheme.accentColor.load();
|
||||
await GetStorage.init("reboot_game");
|
||||
await GetStorage.init("reboot_server");
|
||||
await GetStorage.init("reboot_update");
|
||||
await GetStorage.init("reboot_settings");
|
||||
await GetStorage.init("reboot_hosting");
|
||||
Get.put(GameController());
|
||||
Get.put(ServerController());
|
||||
Get.put(BuildController());
|
||||
Get.put(SettingsController());
|
||||
Get.put(HostingController());
|
||||
doWhenWindowReady(() {
|
||||
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
|
||||
var controller = Get.find<SettingsController>();
|
||||
var size = Size(controller.width, controller.height);
|
||||
var window = appWindow as WinDesktopWindow;
|
||||
window.setWindowCutOnMaximize(appBarSize * 2);
|
||||
appWindow.size = size;
|
||||
if(controller.offsetX != null && controller.offsetY != null){
|
||||
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
|
||||
}else {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
runZonedGuarded(() async {
|
||||
await installationDirectory.create(recursive: true);
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
anonKey: supabaseAnonKey
|
||||
);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemTheme.accentColor.load();
|
||||
await GetStorage.init("reboot_game");
|
||||
await GetStorage.init("reboot_server");
|
||||
await GetStorage.init("reboot_update");
|
||||
await GetStorage.init("reboot_settings");
|
||||
await GetStorage.init("reboot_hosting");
|
||||
var gameController = GameController();
|
||||
Get.put(gameController);
|
||||
Get.put(ServerController());
|
||||
Get.put(BuildController());
|
||||
Get.put(SettingsController());
|
||||
Get.put(HostingController());
|
||||
doWhenWindowReady(() {
|
||||
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
|
||||
var controller = Get.find<SettingsController>();
|
||||
var size = Size(controller.width, controller.height);
|
||||
var window = appWindow as WinDesktopWindow;
|
||||
window.setWindowCutOnMaximize(appBarSize * 2);
|
||||
appWindow.size = size;
|
||||
if(controller.offsetX != null && controller.offsetY != null){
|
||||
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
|
||||
}else {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
appWindow.title = "Reboot Launcher";
|
||||
appWindow.show();
|
||||
});
|
||||
|
||||
runZonedGuarded(
|
||||
() async => runApp(const RebootApplication()),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
)
|
||||
);
|
||||
appWindow.title = "Reboot Launcher";
|
||||
appWindow.show();
|
||||
});
|
||||
var supabase = Supabase.instance.client;
|
||||
await supabase.from('hosts')
|
||||
.delete()
|
||||
.match({'id': gameController.uuid});
|
||||
runApp(const RebootApplication());
|
||||
},
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
));
|
||||
}
|
||||
|
||||
class RebootApplication extends StatefulWidget {
|
||||
@@ -70,21 +80,18 @@ class RebootApplication extends StatefulWidget {
|
||||
class _RebootApplicationState extends State<RebootApplication> {
|
||||
@override
|
||||
Widget build(BuildContext context) => FluentApp(
|
||||
title: "Reboot Launcher",
|
||||
themeMode: ThemeMode.system,
|
||||
debugShowCheckedModeBanner: false,
|
||||
color: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
darkTheme: _createTheme(Brightness.dark),
|
||||
theme: _createTheme(Brightness.light),
|
||||
home: HomePage(key: appKey),
|
||||
title: "Reboot Launcher",
|
||||
themeMode: ThemeMode.system,
|
||||
debugShowCheckedModeBanner: false,
|
||||
color: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
darkTheme: _createTheme(Brightness.dark),
|
||||
theme: _createTheme(Brightness.light),
|
||||
home: const HomePage()
|
||||
);
|
||||
|
||||
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
|
||||
brightness: brightness,
|
||||
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
visualDensity: VisualDensity.standard,
|
||||
focusTheme: FocusThemeData(
|
||||
glowFactor: is10footScreen() ? 2.0 : 0.0,
|
||||
),
|
||||
brightness: brightness,
|
||||
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
|
||||
visualDensity: VisualDensity.standard
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
|
||||
import '../model/server_type.dart';
|
||||
import '../util/server.dart' as server;
|
||||
@@ -62,7 +60,7 @@ Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
return await server.startRemoteServer(uri);
|
||||
}catch(error){
|
||||
throw Exception("Cannot start reverse proxy");
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@ 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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,18 @@ import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
|
||||
class BuildController extends GetxController {
|
||||
List<FortniteBuild>? builds;
|
||||
FortniteBuild? _selectedBuild;
|
||||
final List<Function()> _listeners;
|
||||
late RxBool cancelledDownload;
|
||||
List<FortniteBuild>? _builds;
|
||||
Rxn<FortniteBuild> selectedBuildRx;
|
||||
|
||||
BuildController() : _listeners = [] {
|
||||
cancelledDownload = RxBool(false);
|
||||
}
|
||||
BuildController() : selectedBuildRx = Rxn();
|
||||
|
||||
FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0);
|
||||
List<FortniteBuild>? get builds => _builds;
|
||||
|
||||
set selectedBuild(FortniteBuild build) {
|
||||
_selectedBuild = build;
|
||||
for (var listener in _listeners) {
|
||||
listener();
|
||||
set builds(List<FortniteBuild>? builds) {
|
||||
_builds = builds;
|
||||
if(builds == null || builds.isEmpty){
|
||||
return;
|
||||
}
|
||||
selectedBuildRx.value = builds[0];
|
||||
}
|
||||
|
||||
void addOnBuildChangedListener(Function() listener) => _listeners.add(listener);
|
||||
|
||||
void removeOnBuildChangedListener() => _listeners.clear();
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../model/update_status.dart';
|
||||
|
||||
const String kDefaultPlayerName = "Player";
|
||||
|
||||
class GameController extends GetxController {
|
||||
late final String uuid;
|
||||
late final GetStorage _storage;
|
||||
late final TextEditingController username;
|
||||
late final TextEditingController password;
|
||||
@@ -21,7 +23,6 @@ class GameController extends GetxController {
|
||||
late final Rx<List<FortniteVersion>> versions;
|
||||
late final Rxn<FortniteVersion> _selectedVersion;
|
||||
late final RxBool started;
|
||||
late final Rx<UpdateStatus> updateStatus;
|
||||
GameInstance? instance;
|
||||
|
||||
GameController() {
|
||||
@@ -35,6 +36,8 @@ class GameController extends GetxController {
|
||||
var decodedSelectedVersionName = _storage.read("version");
|
||||
var decodedSelectedVersion = decodedVersions.firstWhereOrNull(
|
||||
(element) => element.name == decodedSelectedVersionName);
|
||||
uuid = _storage.read("uuid") ?? const Uuid().v4();
|
||||
_storage.write("uuid", uuid);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
|
||||
username.addListener(() => _storage.write("username", username.text));
|
||||
@@ -44,7 +47,6 @@ class GameController extends GetxController {
|
||||
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? ""));
|
||||
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
|
||||
started = RxBool(false);
|
||||
updateStatus = Rx(UpdateStatus.waiting);
|
||||
}
|
||||
|
||||
FortniteVersion? getVersionByName(String name) {
|
||||
@@ -67,6 +69,9 @@ class GameController extends GetxController {
|
||||
|
||||
void removeVersion(FortniteVersion version) {
|
||||
versions.update((val) => val?.remove(version));
|
||||
if (selectedVersion?.name == version.name || hasNoVersions) {
|
||||
selectedVersion = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveVersions() async {
|
||||
@@ -81,7 +86,7 @@ class GameController extends GetxController {
|
||||
FortniteVersion? get selectedVersion => _selectedVersion();
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
_selectedVersion(version);
|
||||
_selectedVersion.value = version;
|
||||
_storage.write("version", version?.name);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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 '../../model/game_instance.dart';
|
||||
import '../../model/update_status.dart';
|
||||
import '../../util/reboot.dart';
|
||||
|
||||
|
||||
const String kDefaultServerName = "Reboot Game Server";
|
||||
@@ -10,19 +14,39 @@ const String kDefaultServerName = "Reboot Game Server";
|
||||
class HostingController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final TextEditingController name;
|
||||
late final TextEditingController category;
|
||||
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));
|
||||
category = TextEditingController(text: _storage.read("category") ?? "");
|
||||
category.addListener(() => _storage.write("category", category.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.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/ui/widget/home/version_name_input.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
@@ -38,12 +39,12 @@ class AddLocalVersion extends StatelessWidget {
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
TextFormBox(
|
||||
controller: _nameController,
|
||||
header: "Name",
|
||||
placeholder: "Type the version's name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
@@ -53,6 +54,10 @@ class AddLocalVersion extends StatelessWidget {
|
||||
controller: _gamePathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
@@ -32,16 +32,16 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
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;
|
||||
|
||||
DownloadStatus _status = DownloadStatus.form;
|
||||
String _timeLeft = "00:00:00";
|
||||
double _downloadProgress = 0;
|
||||
CancelableOperation? _manifestDownloadProcess;
|
||||
CancelableOperation? _driveDownloadOperation;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@@ -54,7 +54,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -62,57 +61,74 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_buildController.removeOnBuildChangedListener();
|
||||
_onDisposed();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onDisposed() {
|
||||
if (_status != DownloadStatus.downloading) {
|
||||
void _cancelDownload() {
|
||||
if (_status.value != DownloadStatus.extracting && _status.value != DownloadStatus.extracting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_manifestDownloadProcess != null) {
|
||||
_manifestDownloadProcess?.cancel();
|
||||
_buildController.cancelledDownload(true);
|
||||
if (_manifestDownloadProcess == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_driveDownloadOperation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_driveDownloadOperation!.cancel();
|
||||
_buildController.cancelledDownload(true);
|
||||
Process.run('${assetsDirectory.path}\\builds\\stop.bat', []);
|
||||
_manifestDownloadProcess?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch(_status){
|
||||
case DownloadStatus.form:
|
||||
return _createFormDialog();
|
||||
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!",
|
||||
);
|
||||
}
|
||||
}
|
||||
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 [
|
||||
@@ -127,61 +143,56 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
setState(() => _status = DownloadStatus.downloading);
|
||||
var build = _buildController.selectedBuildRx.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = DownloadStatus.downloading;
|
||||
var future = downloadArchiveBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
build.link,
|
||||
Directory(_pathController.text),
|
||||
_onDownloadProgress,
|
||||
_onUnrar
|
||||
(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);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUnrar() {
|
||||
setState(() => _status = DownloadStatus.extracting);
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.done;
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text)
|
||||
));
|
||||
});
|
||||
_status.value = DownloadStatus.done;
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text)
|
||||
));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
print("Error");
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.error;
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
});
|
||||
_status.value = DownloadStatus.error;
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onDownloadProgress(double progress, String timeLeft) {
|
||||
void _onDownloadProgress(double? progress, String? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.downloading;
|
||||
_timeLeft = timeLeft;
|
||||
_downloadProgress = progress;
|
||||
});
|
||||
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||
_timeLeft.value = timeLeft;
|
||||
_downloadProgress.value = progress;
|
||||
}
|
||||
|
||||
Widget _createDownloadBody() => Column(
|
||||
@@ -204,14 +215,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
"${(_downloadProgress.value ?? 0).round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
if(_timeLeft.value != null)
|
||||
Text(
|
||||
"Time left: ${_timeLeft.value}",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
@@ -221,7 +233,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())
|
||||
child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
@@ -257,39 +269,27 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
],
|
||||
);
|
||||
|
||||
Widget _createFormDialog() {
|
||||
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()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createFormBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const BuildSelector(),
|
||||
const SizedBox(height: 20.0),
|
||||
VersionNameInput(controller: _nameController),
|
||||
BuildSelector(
|
||||
onSelected: _updateFormDefaults
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
VersionNameInput(
|
||||
controller: _nameController
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: "Path",
|
||||
placeholder: "Type the download destination",
|
||||
@@ -298,6 +298,10 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -319,9 +323,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
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 "
|
||||
"${_buildController.selectedBuild.version.toString()}";
|
||||
_nameController.text = _buildController.selectedBuild.version.toString();
|
||||
"${build.version}";
|
||||
_nameController.text = build.version.toString();
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class FormDialog extends AbstractDialog {
|
||||
text: entry.text,
|
||||
type: entry.type,
|
||||
onTap: () {
|
||||
if(!Form.of(context)!.validate()) {
|
||||
if(!Form.of(context).validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,13 @@ import '../../../main.dart';
|
||||
import 'dialog.dart';
|
||||
|
||||
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
|
||||
"This means that you cannot currently host this version of the game. "
|
||||
"For a list of supported versions, check #info in the Discord server. "
|
||||
"If you are unsure which version works best, use build 7.40. "
|
||||
"If you are a passionate programmer you can add support by opening a PR on Github. ";
|
||||
|
||||
const String _corruptedBuildError = "The build you are currently using is corrupted. "
|
||||
"This means that some critical files are missing for the game to launch. "
|
||||
"Download the build again from the launcher or, if it's not available there, from another source. "
|
||||
"Occasionally some files might get corrupted if there isn't enough space on your drive.";
|
||||
const String _corruptedBuildError = "An unknown error happened while launching Fortnite. "
|
||||
"Some critical could be missing in your installation. "
|
||||
"Download the build again from the launcher, not locally, or from a different source. "
|
||||
"Alternatively, something could have gone wrong in the launcher. ";
|
||||
|
||||
Future<void> showBrokenError() async {
|
||||
showDialog(
|
||||
@@ -82,7 +80,7 @@ Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? st
|
||||
builder: (context) => ErrorDialog(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => _corruptedBuildError
|
||||
errorMessageBuilder: (exception) => "${_corruptedBuildError}Error message: $exception"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,11 +125,6 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _showPortTakenDialog(3551);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await freeLawinPort();
|
||||
await stop();
|
||||
return _toggle(newResultType);
|
||||
@@ -139,11 +134,6 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _showPortTakenDialog(8080);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await freeMatchmakerPort();
|
||||
await stop();
|
||||
return _toggle(newResultType);
|
||||
@@ -203,14 +193,13 @@ extension ServerControllerDialog on ServerController {
|
||||
|
||||
Future<Uri?> _pingRemoteInteractive() async {
|
||||
try {
|
||||
var mainFuture = ping(host.text, port.text).then((value) => value != null);
|
||||
var future = _waitFutureOrTime(mainFuture);
|
||||
var result = await showDialog<bool>(
|
||||
var future = ping(host.text, port.text);
|
||||
await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: future,
|
||||
closeAutomatically: false,
|
||||
closeAutomatically: true,
|
||||
loadingMessage: "Pinging remote server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
@@ -220,8 +209,8 @@ extension ServerControllerDialog on ServerController {
|
||||
.text} doesn't work. Check the hostname and/or the port and try again."),
|
||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
||||
)
|
||||
) ?? false;
|
||||
return result ? await future : null;
|
||||
);
|
||||
return await future;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
@@ -236,27 +225,6 @@ extension ServerControllerDialog on ServerController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _showPortTakenDialog(int port) async {
|
||||
return await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
InfoDialog(
|
||||
text: "Port $port is already in use, do you want to kill the associated process?",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
DialogButton(
|
||||
text: "Kill",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
)
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
void _showCannotStopError() {
|
||||
if(!started.value){
|
||||
return;
|
||||
@@ -298,7 +266,7 @@ extension ServerControllerDialog on ServerController {
|
||||
)
|
||||
);
|
||||
|
||||
void _showIllegalPortError() => showMessage("Illegal port for backend server, use only numbers");
|
||||
void _showIllegalPortError() => showMessage("Invalid port for backend server");
|
||||
|
||||
void _showMissingPortError() => showMessage("Missing port for backend server");
|
||||
|
||||
|
||||
113
lib/src/ui/page/browse_page.dart
Normal file
113
lib/src/ui/page/browse_page.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
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/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/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'),
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
|
||||
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/main.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/launcher_page.dart';
|
||||
import 'package:reboot_launcher/src/ui/page/server_page.dart';
|
||||
@@ -21,8 +22,9 @@ class HomePage extends StatefulWidget {
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener {
|
||||
static const double _defaultPadding = 12.0;
|
||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
static const double _kDefaultPadding = 12.0;
|
||||
static const int _kPagesLength = 5;
|
||||
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@@ -32,8 +34,11 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final RxInt _index = RxInt(0);
|
||||
final RxBool _nestedNavigation = RxBool(false);
|
||||
final GlobalKey<NavigatorState> _settingsNavigatorKey = GlobalKey();
|
||||
final List<GlobalKey<NavigatorState>> _navigators = List.generate(_kPagesLength, (index) => GlobalKey());
|
||||
final List<RxBool> _navigationStatus = List.generate(_kPagesLength, (index) => RxBool(false));
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -85,38 +90,45 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => Stack(
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Stack(
|
||||
children: [
|
||||
NavigationView(
|
||||
paneBodyBuilder: (body) => Padding(
|
||||
padding: const EdgeInsets.all(_defaultPadding),
|
||||
child: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: _selectedIndex,
|
||||
onChanged: _onIndexChanged,
|
||||
displayMode: PaneDisplayMode.auto,
|
||||
items: _items,
|
||||
footerItems: _footerItems,
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
|
||||
),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
LayoutBuilder(
|
||||
builder: (context, specs) => Obx(() => NavigationView(
|
||||
paneBodyBuilder: (pane, body) => Padding(
|
||||
padding: const EdgeInsets.all(_kDefaultPadding),
|
||||
child: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton
|
||||
),
|
||||
pane: NavigationPane(
|
||||
key: appKey,
|
||||
selected: _selectedIndex,
|
||||
onChanged: _onIndexChanged,
|
||||
displayMode: specs.biggest.width <= 1536 ? PaneDisplayMode.compact : PaneDisplayMode.open,
|
||||
items: _items,
|
||||
footerItems: _footerItems,
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
|
||||
),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
))
|
||||
),
|
||||
if(_focused() && isWin11)
|
||||
const WindowBorder()
|
||||
Obx(() => isWin11 && _focused.value ? const WindowBorder() : const SizedBox())
|
||||
]
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _backButton => Obx(() {
|
||||
// ignore: unused_local_variable
|
||||
var ignored = _nestedNavigation.value;
|
||||
for(var entry in _navigationStatus){
|
||||
entry.value;
|
||||
}
|
||||
|
||||
return PaneItem(
|
||||
icon: const Icon(FluentIcons.back, size: 14.0),
|
||||
body: const SizedBox.shrink(),
|
||||
@@ -128,15 +140,20 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
);
|
||||
});
|
||||
|
||||
void Function()? _onBack() {
|
||||
var navigator = _settingsNavigatorKey.currentState;
|
||||
Function()? _onBack() {
|
||||
var navigator = _navigators[_index.value].currentState;
|
||||
if(navigator == null || !navigator.mounted || !navigator.canPop()){
|
||||
return null;
|
||||
}
|
||||
|
||||
var status = _navigationStatus[_index.value];
|
||||
if(!status.value){
|
||||
return null;
|
||||
}
|
||||
|
||||
return () async {
|
||||
Navigator.pop(navigator.context);
|
||||
_nestedNavigation.value = false;
|
||||
status.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,25 +204,25 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
PaneItem(
|
||||
title: const Text("Play"),
|
||||
icon: const Icon(FluentIcons.game),
|
||||
body: const LauncherPage()
|
||||
body: LauncherPage(_navigators[0], _navigationStatus[0])
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Host"),
|
||||
icon: const Icon(FluentIcons.server_processes),
|
||||
body: const HostingPage()
|
||||
body: HostingPage(_navigators[1], _navigationStatus[1])
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Backend"),
|
||||
icon: const Icon(FluentIcons.user_window),
|
||||
body: ServerPage()
|
||||
body: ServerPage(_navigators[2], _navigationStatus[2])
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: InfoPage(_settingsNavigatorKey, _nestedNavigation)
|
||||
body: InfoPage(_navigators[3], _navigationStatus[3])
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,35 +1,70 @@
|
||||
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/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
|
||||
|
||||
import '../../model/update_status.dart';
|
||||
import '../../util/reboot.dart';
|
||||
import '../controller/update_controller.dart';
|
||||
import 'browse_page.dart';
|
||||
|
||||
|
||||
class HostingPage extends StatefulWidget {
|
||||
const HostingPage(
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final RxBool nestedNavigation;
|
||||
const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<HostingPage> createState() => _HostingPageState();
|
||||
}
|
||||
|
||||
class _HostingPageState extends State<HostingPage> {
|
||||
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => !_settingsController.autoUpdate() || _hostingController.updateStatus().isDone() ? _body : _updateScreen);
|
||||
}
|
||||
|
||||
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 _homeScreen;
|
||||
case "browse":
|
||||
return const BrowsePage();
|
||||
default:
|
||||
throw Exception("Unknown page: $name");
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _homeScreen => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const SizedBox(
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
child: _hostingController.updateStatus.value == UpdateStatus.error ? _updateError :_rebootGuiInfo,
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
@@ -48,12 +83,12 @@ class _HostingPageState extends State<HostingPage> {
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
title: "Category",
|
||||
subtitle: "The category of your game server",
|
||||
title: "Description",
|
||||
subtitle: "The description of your game server",
|
||||
isChild: true,
|
||||
content: TextFormBox(
|
||||
placeholder: "Category",
|
||||
controller: _hostingController.category
|
||||
placeholder: "Description",
|
||||
controller: _hostingController.description
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -96,10 +131,55 @@ class _HostingPageState extends State<HostingPage> {
|
||||
)
|
||||
]
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.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 = true;
|
||||
},
|
||||
child: const Text("Browse")
|
||||
)
|
||||
),
|
||||
const Expanded(child: SizedBox()),
|
||||
const LaunchButton(
|
||||
host: true
|
||||
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 _updateScreen => const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
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
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class InfoPage extends StatefulWidget {
|
||||
State<InfoPage> createState() => _InfoPageState();
|
||||
}
|
||||
|
||||
class _InfoPageState extends State<InfoPage> {
|
||||
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
|
||||
final List<String> _elseTitles = [
|
||||
"Open the home page",
|
||||
"Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port",
|
||||
@@ -42,6 +42,9 @@ class _InfoPageState extends State<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final ScrollController _controller;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance);
|
||||
@@ -71,8 +74,6 @@ class _InfoPageState extends State<InfoPage> {
|
||||
);
|
||||
|
||||
Widget _createScreen(String? name) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => widget.nestedNavigation.value = name != "home");
|
||||
switch(name){
|
||||
case "home":
|
||||
return _homeScreen;
|
||||
@@ -91,7 +92,10 @@ class _InfoPageState extends State<InfoPage> {
|
||||
_createCardWidget(
|
||||
text: "Play on someone else's server",
|
||||
description: "If one of your friends is hosting a game server, click here",
|
||||
onClick: () => widget.navigatorKey.currentState?.pushNamed("else")
|
||||
onClick: () {
|
||||
widget.navigatorKey.currentState?.pushNamed("else");
|
||||
widget.nestedNavigation.value = true;
|
||||
}
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
@@ -101,7 +105,10 @@ class _InfoPageState extends State<InfoPage> {
|
||||
_createCardWidget(
|
||||
text: "Host your own server",
|
||||
description: "If you want to create your own server to invite your friends or to play around by yourself, click here",
|
||||
onClick: () => widget.navigatorKey.currentState?.pushNamed("own")
|
||||
onClick: () {
|
||||
widget.navigatorKey.currentState?.pushNamed("own");
|
||||
widget.nestedNavigation.value = true;
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,90 +1,76 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/build_controller.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/page/browse_page.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../model/update_status.dart';
|
||||
import '../../util/checks.dart';
|
||||
import '../../util/reboot.dart';
|
||||
import '../controller/update_controller.dart';
|
||||
|
||||
class LauncherPage extends StatefulWidget {
|
||||
const LauncherPage(
|
||||
{Key? key})
|
||||
: super(key: key);
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final RxBool nestedNavigation;
|
||||
const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LauncherPage> createState() => _LauncherPageState();
|
||||
}
|
||||
|
||||
class _LauncherPageState extends State<LauncherPage> {
|
||||
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 _GamePage(widget.navigatorKey, widget.nestedNavigation);
|
||||
case "browse":
|
||||
return const BrowsePage();
|
||||
default:
|
||||
throw Exception("Unknown page: $name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _GamePage extends StatefulWidget {
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final RxBool nestedNavigation;
|
||||
const _GamePage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_GamePage> createState() => _GamePageState();
|
||||
}
|
||||
|
||||
class _GamePageState extends State<_GamePage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if(_gameController.updateStatus() == UpdateStatus.waiting) {
|
||||
_startUpdater();
|
||||
_setupBuildWarning();
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setupBuildWarning() {
|
||||
void onCancelWarning() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
showSnackbar(context, const Snackbar(content: Text("Download cancelled")));
|
||||
_buildController.cancelledDownload(false);
|
||||
});
|
||||
_buildController.cancelledDownload.listen((value) => value ? onCancelWarning() : {});
|
||||
}
|
||||
|
||||
Future<void> _startUpdater() async {
|
||||
if(!_settingsController.autoUpdate()){
|
||||
_gameController.updateStatus.value = UpdateStatus.success;
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.updateStatus.value = UpdateStatus.started;
|
||||
try {
|
||||
updateTime = await downloadRebootDll(_settingsController.updateUrl.text, updateTime);
|
||||
_gameController.updateStatus.value = UpdateStatus.success;
|
||||
}catch(_) {
|
||||
_gameController.updateStatus.value = UpdateStatus.error;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => !_settingsController.autoUpdate() || _gameController.updateStatus().isDone() ? _homePage : _updateScreen);
|
||||
|
||||
Widget get _homePage => Column(
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _gameController.updateStatus() == UpdateStatus.error ? _updateError : const SizedBox(),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SizedBox(height: _gameController.updateStatus() == UpdateStatus.error ? 16.0 : 0.0),
|
||||
),
|
||||
SettingTile(
|
||||
title: "Credentials",
|
||||
subtitle: "Your in-game login credentials",
|
||||
@@ -144,7 +130,10 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
title: "Browse available servers",
|
||||
subtitle: "Discover new game servers that fit your play-style",
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(Uri.parse("https://google.com/search?q=One+Day+This+Will+Be+Ready")),
|
||||
onPressed: () {
|
||||
widget.navigatorKey.currentState?.pushNamed('browse');
|
||||
widget.nestedNavigation.value = true;
|
||||
},
|
||||
child: const Text("Browse")
|
||||
),
|
||||
isChild: true
|
||||
@@ -185,32 +174,4 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _updateScreen => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _updateError => MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => _startUpdater(),
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
|
||||
severity: InfoBarSeverity.info
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3,20 +3,31 @@ import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../widget/shared/setting_tile.dart';
|
||||
|
||||
class ServerPage extends StatelessWidget {
|
||||
class ServerPage extends StatefulWidget {
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final RxBool 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>();
|
||||
|
||||
ServerPage({Key? key}) : super(key: key);
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
|
||||
@@ -13,24 +14,36 @@ import '../../util/selector.dart';
|
||||
import '../dialog/dialog.dart';
|
||||
import '../widget/shared/setting_tile.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
class SettingsPage extends StatefulWidget {
|
||||
|
||||
SettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingTile(
|
||||
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 Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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
|
||||
title: "Game server",
|
||||
description: "This file is injected to create a game server to host matches",
|
||||
controller: _settingsController.rebootDll
|
||||
),
|
||||
_createFileSetting(
|
||||
title: "Unreal engine console",
|
||||
@@ -43,115 +56,111 @@ class SettingsPage extends StatelessWidget {
|
||||
controller: _settingsController.authDll
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.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(),
|
||||
onChanged: (value) => _settingsController.autoUpdate.value = value
|
||||
))
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.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: 16.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/new/choose")),
|
||||
child: const Text("Report a bug"),
|
||||
)
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.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 settings 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: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Version status",
|
||||
subtitle: "Current version: 7.0",
|
||||
content: Button(
|
||||
onPressed: () => launchUrl(installationDirectory.uri),
|
||||
child: const Text("Show Files"),
|
||||
)
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => ListTile(
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
trailing: SizedBox(
|
||||
width: 256,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
placeholder: "Path",
|
||||
controller: controller,
|
||||
validator: checkDll,
|
||||
autovalidateMode: AutovalidateMode.always
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: () async {
|
||||
var selected = await compute(openFilePicker, "dll");
|
||||
controller.text = selected ?? controller.text;
|
||||
},
|
||||
child: const Icon(FluentIcons.open_folder_horizontal),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.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: 16.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: 16.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: 16.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: 16.0,
|
||||
),
|
||||
SettingTile(
|
||||
title: "Version status",
|
||||
subtitle: "Current version: 8.0",
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
|
||||
class BuildSelector extends StatefulWidget {
|
||||
final Function() onSelected;
|
||||
|
||||
const BuildSelector({Key? key}) : super(key: key);
|
||||
const BuildSelector({Key? key, required this.onSelected}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BuildSelector> createState() => _BuildSelectorState();
|
||||
@@ -18,14 +19,20 @@ class _BuildSelectorState extends State<BuildSelector> {
|
||||
Widget build(BuildContext context) {
|
||||
return InfoLabel(
|
||||
label: "Build",
|
||||
child: ComboBox<FortniteBuild>(
|
||||
child: Obx(() => ComboBox<FortniteBuild>(
|
||||
placeholder: const Text('Select a fortnite build'),
|
||||
isExpanded: true,
|
||||
items: _createItems(),
|
||||
value: _buildController.selectedBuild,
|
||||
onChanged: (value) =>
|
||||
value == null ? {} : setState(() => _buildController.selectedBuild = value)
|
||||
)
|
||||
value: _buildController.selectedBuildRx.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_buildController.selectedBuildRx.value = value;
|
||||
widget.onSelected();
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:reboot_launcher/src/../main.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/model/game_instance.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../../../util/process.dart';
|
||||
|
||||
@@ -37,6 +38,7 @@ class LaunchButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LaunchButtonState extends State<LaunchButton> {
|
||||
static const String _kLoadingRoute = '/loading';
|
||||
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
||||
final List<String> _corruptedBuildErrors = [
|
||||
"when 0 bytes remain",
|
||||
@@ -50,6 +52,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
|
||||
final GlobalKey _headlessServerKey = GlobalKey();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
@@ -116,10 +119,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
try {
|
||||
_fail = false;
|
||||
var version = _gameController.selectedVersion!;
|
||||
var gamePath = version.executable?.path;
|
||||
if(gamePath == null){
|
||||
if(version.executable?.path == null){
|
||||
showMissingBuildError(version);
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
@@ -131,7 +132,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
await compute(patchMatchmaking, version.executable!);
|
||||
await compute(patchHeadless, version.executable!);
|
||||
|
||||
var automaticallyStartedServer = await _startMatchMakingServer();
|
||||
@@ -141,9 +141,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await _showServerLaunchingWarning();
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
_closeDialogIfOpen(false);
|
||||
showCorruptedBuildError(widget.host, exception, stacktrace);
|
||||
_closeLaunchingWidget(false);
|
||||
_onStop(widget.host);
|
||||
showCorruptedBuildError(widget.host, exception, stacktrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
var launcherProcess = await _createLauncherProcess(version);
|
||||
var eacProcess = await _createEacProcess(version);
|
||||
var gameProcess = await _createGameProcess(version.executable!.path, host);
|
||||
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
|
||||
var watchDogProcess = _createWatchdogProcess(gameProcess, launcherProcess, eacProcess);
|
||||
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, watchDogProcess, hasChildServer);
|
||||
if(host){
|
||||
_hostingController.instance = instance;
|
||||
}else{
|
||||
@@ -161,6 +162,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_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 {
|
||||
if(widget.host){
|
||||
return false;
|
||||
@@ -226,33 +234,50 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
_closeDialogIfOpen(false);
|
||||
_closeLaunchingWidget(false);
|
||||
_onStop(widget.host);
|
||||
}
|
||||
|
||||
void _closeDialogIfOpen(bool success) {
|
||||
void _closeLaunchingWidget(bool success) {
|
||||
var context = _headlessServerKey.currentContext;
|
||||
if(context == null || !context.mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var route = ModalRoute.of(appKey.currentContext!);
|
||||
if(route == null || route.isCurrent){
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(appKey.currentContext!).pop(success);
|
||||
Navigator.of(context).pop(success);
|
||||
}
|
||||
|
||||
Future<void> _showServerLaunchingWarning() async {
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => ProgressDialog(
|
||||
key: _headlessServerKey,
|
||||
text: "Launching headless server...",
|
||||
onStop: () =>_onEnd()
|
||||
onStop: () => Navigator.of(context).pop(false)
|
||||
)
|
||||
) ?? false;
|
||||
|
||||
if(result){
|
||||
if(!result){
|
||||
_onStop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
_onStop(widget.host);
|
||||
if(!_hostingController.discoverable.value){
|
||||
return;
|
||||
}
|
||||
|
||||
var supabase = Supabase.instance.client;
|
||||
await supabase.from('hosts').insert({
|
||||
'id': _gameController.uuid,
|
||||
'name': _hostingController.name.text,
|
||||
'description': _hostingController.description.text,
|
||||
'version': _gameController.selectedVersion?.name ?? 'unknown'
|
||||
});
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool host) {
|
||||
@@ -280,7 +305,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_closeLaunchingWidget(false);
|
||||
_showTokenError(host);
|
||||
return;
|
||||
}
|
||||
@@ -290,7 +315,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_injectOrShowError(Injectable.console, host);
|
||||
}else {
|
||||
_injectOrShowError(Injectable.reboot, host)
|
||||
.then((value) => _closeDialogIfOpen(true));
|
||||
.then((value) => _closeLaunchingWidget(true));
|
||||
}
|
||||
|
||||
_injectOrShowError(Injectable.memoryFix, host);
|
||||
@@ -340,6 +365,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
_setStarted(host, false);
|
||||
|
||||
if(host){
|
||||
var supabase = Supabase.instance.client;
|
||||
await supabase.from('hosts')
|
||||
.delete()
|
||||
.match({'id': _gameController.uuid});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
|
||||
@@ -387,12 +419,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onDllFail(File dllPath, bool hosting) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_closeLaunchingWidget(false);
|
||||
showMissingDllError(path.basename(dllPath.path));
|
||||
_onStop(hosting);
|
||||
});
|
||||
|
||||
@@ -9,15 +9,16 @@ class VersionNameInput extends StatelessWidget {
|
||||
VersionNameInput({Key? key, required this.controller}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormBox(
|
||||
header: "Name",
|
||||
placeholder: "Type the version's name",
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
validator: _validate,
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => InfoLabel(
|
||||
label: "Name",
|
||||
child: TextFormBox(
|
||||
controller: controller,
|
||||
placeholder: "Type the version's name",
|
||||
autofocus: true,
|
||||
validator: _validate,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction
|
||||
),
|
||||
);
|
||||
|
||||
String? _validate(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
|
||||
@@ -135,12 +136,8 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
_gameController.removeVersion(version);
|
||||
if (_gameController.selectedVersion?.name == version.name || _gameController.hasNoVersions) {
|
||||
_gameController.selectedVersion = null;
|
||||
}
|
||||
|
||||
if (_deleteFilesController.value && await version.location.exists()) {
|
||||
version.location.delete(recursive: true);
|
||||
delete(version.location);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -213,12 +210,14 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormBox(
|
||||
controller: nameController,
|
||||
header: "Name",
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkChangeVersion(text)
|
||||
InfoLabel(
|
||||
label: "Name",
|
||||
child: TextFormBox(
|
||||
controller: nameController,
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkChangeVersion(text)
|
||||
)
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
@@ -228,6 +227,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
FileSelector(
|
||||
placeholder: "Type the new game folder",
|
||||
windowTitle: "Select game folder",
|
||||
label: "Path",
|
||||
controller: pathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
|
||||
@@ -48,28 +48,16 @@ class _FileSelectorState extends State<FileSelector> {
|
||||
) : _buildBody;
|
||||
}
|
||||
|
||||
Widget get _buildBody => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
controller: widget.controller,
|
||||
placeholder: widget.placeholder,
|
||||
validator: widget.validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
||||
)
|
||||
Widget get _buildBody => TextFormBox(
|
||||
controller: widget.controller,
|
||||
placeholder: widget.placeholder,
|
||||
validator: widget.validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction,
|
||||
suffix: !widget.allowNavigator ? null : Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
),
|
||||
if (widget.allowNavigator)
|
||||
const SizedBox(width: 16.0),
|
||||
if (widget.allowNavigator)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
)
|
||||
)
|
||||
],
|
||||
suffixMode: OverlayVisibilityMode.editing
|
||||
);
|
||||
|
||||
void _onPressed() {
|
||||
|
||||
@@ -26,16 +26,24 @@ class SmartInput extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormBox(
|
||||
if(label != null){
|
||||
return InfoLabel(
|
||||
label: label!,
|
||||
child: _body
|
||||
);
|
||||
}
|
||||
|
||||
return _body;
|
||||
}
|
||||
|
||||
TextFormBox get _body => TextFormBox(
|
||||
enabled: enabled,
|
||||
controller: controller,
|
||||
header: label,
|
||||
keyboardType: type,
|
||||
placeholder: placeholder,
|
||||
onTap: onTap,
|
||||
readOnly: readOnly,
|
||||
autovalidateMode: validatorMode,
|
||||
validator: validator
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
@@ -7,7 +9,6 @@ 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 'package:path/path.dart' as path;
|
||||
import 'package:unrar_file/unrar_file.dart';
|
||||
|
||||
import 'os.dart';
|
||||
|
||||
@@ -44,21 +45,28 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function() onRar) async {
|
||||
var outputDir = await destination.createTemp("build");
|
||||
|
||||
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");
|
||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
var tempFile = File("${outputDir.path}\\$fileName");
|
||||
if(tempFile.existsSync()) {
|
||||
tempFile.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
var client = http.Client();
|
||||
var response = await client.send(
|
||||
http.Request("GET", Uri.parse(archiveUrl)));
|
||||
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();
|
||||
@@ -69,17 +77,66 @@ Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Func
|
||||
onProgress((received / length) * 100, toETA(eta));
|
||||
return s;
|
||||
}).pipe(sink);
|
||||
onRar();
|
||||
if(extension.toLowerCase() == ".zip"){
|
||||
await extractFileToDisk(tempFile.path, destination.path);
|
||||
}else if(extension.toLowerCase() == ".rar") {
|
||||
await UnrarFile.extract_rar(tempFile.path, destination.path);
|
||||
} else {
|
||||
throw Exception("Unknown file extension: $extension");
|
||||
}
|
||||
|
||||
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");
|
||||
}finally {
|
||||
outputDir.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -68,5 +68,13 @@ String? checkMatchmaking(String? text) {
|
||||
return "Empty hostname";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return "Empty URL";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import '../../../main.dart';
|
||||
import '../ui/dialog/dialog.dart';
|
||||
|
||||
|
||||
String? lastError;
|
||||
|
||||
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
if(exception == null){
|
||||
return;
|
||||
@@ -12,6 +15,16 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(lastError == exception.toString()){
|
||||
return;
|
||||
}
|
||||
|
||||
lastError = exception.toString();
|
||||
var route = ModalRoute.of(appKey.currentContext!);
|
||||
if(route != null && !route.isCurrent){
|
||||
Navigator.of(appKey.currentContext!).pop(false);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
|
||||
@@ -4,11 +4,12 @@ import 'package:win32/win32.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
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 {
|
||||
var result = _regex.firstMatch(Platform.operatingSystemVersion)?.group(1);
|
||||
if(result == null){
|
||||
@@ -19,6 +20,33 @@ bool get isWin11 {
|
||||
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();
|
||||
|
||||
@@ -12,8 +12,8 @@ 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 now = DateTime.now();
|
||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
var exists = await rebootDllFile.exists();
|
||||
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) {
|
||||
@@ -32,6 +32,12 @@ Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
|
||||
|
||||
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) {
|
||||
|
||||
@@ -36,7 +36,8 @@ Future<void> startServer(bool detached) async {
|
||||
serverExeFile.path,
|
||||
[],
|
||||
workingDirectory: serverDirectory.path,
|
||||
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
|
||||
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal,
|
||||
runInShell: detached
|
||||
);
|
||||
if(!detached) {
|
||||
serverLogFile.createSync(recursive: true);
|
||||
@@ -156,7 +157,14 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
);
|
||||
}
|
||||
|
||||
if(int.tryParse(port) == null){
|
||||
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
|
||||
);
|
||||
@@ -179,9 +187,7 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
);
|
||||
}
|
||||
|
||||
Future<HttpServer> startRemoteServer(Uri uri) async {
|
||||
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
}
|
||||
Future<HttpServer> startRemoteServer(Uri uri) async => await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
|
||||
class ServerResult {
|
||||
final int? pid;
|
||||
|
||||
2
lib/supabase.dart
Normal file
2
lib/supabase.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co';
|
||||
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M';
|
||||
51
lib/watch.dart
Normal file
51
lib/watch.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
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}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user