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:
BIN
assets/browse/watch.exe
Normal file
BIN
assets/browse/watch.exe
Normal file
Binary file not shown.
@@ -1 +1,2 @@
|
|||||||
taskkill /f /im build.exe
|
taskkill /f /im winrar.exe
|
||||||
|
taskkill /f /im tar.exe
|
||||||
BIN
assets/builds/winrar.exe
Normal file
BIN
assets/builds/winrar.exe
Normal file
Binary file not shown.
BIN
assets/dlls/reboot.dll
Normal file
BIN
assets/dlls/reboot.dll
Normal file
Binary file not shown.
@@ -73,7 +73,6 @@ void main(List<String> args) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await patchHeadless(version.executable!);
|
await patchHeadless(version.executable!);
|
||||||
await patchMatchmaking(version.executable!);
|
|
||||||
|
|
||||||
var serverType = getServerType(result);
|
var serverType = getServerType(result);
|
||||||
var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"];
|
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/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_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/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';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
const double kDefaultWindowWidth = 885;
|
const double kDefaultWindowWidth = 1024;
|
||||||
const double kDefaultWindowHeight = 885;
|
const double kDefaultWindowHeight = 1024;
|
||||||
final GlobalKey appKey = GlobalKey();
|
final GlobalKey appKey = GlobalKey();
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await installationDirectory.create(recursive: true);
|
runZonedGuarded(() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
await installationDirectory.create(recursive: true);
|
||||||
await SystemTheme.accentColor.load();
|
await Supabase.initialize(
|
||||||
await GetStorage.init("reboot_game");
|
url: supabaseUrl,
|
||||||
await GetStorage.init("reboot_server");
|
anonKey: supabaseAnonKey
|
||||||
await GetStorage.init("reboot_update");
|
);
|
||||||
await GetStorage.init("reboot_settings");
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await GetStorage.init("reboot_hosting");
|
await SystemTheme.accentColor.load();
|
||||||
Get.put(GameController());
|
await GetStorage.init("reboot_game");
|
||||||
Get.put(ServerController());
|
await GetStorage.init("reboot_server");
|
||||||
Get.put(BuildController());
|
await GetStorage.init("reboot_update");
|
||||||
Get.put(SettingsController());
|
await GetStorage.init("reboot_settings");
|
||||||
Get.put(HostingController());
|
await GetStorage.init("reboot_hosting");
|
||||||
doWhenWindowReady(() {
|
var gameController = GameController();
|
||||||
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
|
Get.put(gameController);
|
||||||
var controller = Get.find<SettingsController>();
|
Get.put(ServerController());
|
||||||
var size = Size(controller.width, controller.height);
|
Get.put(BuildController());
|
||||||
var window = appWindow as WinDesktopWindow;
|
Get.put(SettingsController());
|
||||||
window.setWindowCutOnMaximize(appBarSize * 2);
|
Get.put(HostingController());
|
||||||
appWindow.size = size;
|
doWhenWindowReady(() {
|
||||||
if(controller.offsetX != null && controller.offsetY != null){
|
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
|
||||||
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
|
var controller = Get.find<SettingsController>();
|
||||||
}else {
|
var size = Size(controller.width, controller.height);
|
||||||
appWindow.alignment = Alignment.center;
|
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.title = "Reboot Launcher";
|
||||||
appWindow.show();
|
appWindow.show();
|
||||||
});
|
});
|
||||||
|
var supabase = Supabase.instance.client;
|
||||||
runZonedGuarded(
|
await supabase.from('hosts')
|
||||||
() async => runApp(const RebootApplication()),
|
.delete()
|
||||||
(error, stack) => onError(error, stack, false),
|
.match({'id': gameController.uuid});
|
||||||
zoneSpecification: ZoneSpecification(
|
runApp(const RebootApplication());
|
||||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
},
|
||||||
)
|
(error, stack) => onError(error, stack, false),
|
||||||
);
|
zoneSpecification: ZoneSpecification(
|
||||||
|
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
class RebootApplication extends StatefulWidget {
|
class RebootApplication extends StatefulWidget {
|
||||||
@@ -70,21 +80,18 @@ class RebootApplication extends StatefulWidget {
|
|||||||
class _RebootApplicationState extends State<RebootApplication> {
|
class _RebootApplicationState extends State<RebootApplication> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => FluentApp(
|
Widget build(BuildContext context) => FluentApp(
|
||||||
title: "Reboot Launcher",
|
title: "Reboot Launcher",
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
color: SystemTheme.accentColor.accent.toAccentColor(),
|
color: SystemTheme.accentColor.accent.toAccentColor(),
|
||||||
darkTheme: _createTheme(Brightness.dark),
|
darkTheme: _createTheme(Brightness.dark),
|
||||||
theme: _createTheme(Brightness.light),
|
theme: _createTheme(Brightness.light),
|
||||||
home: HomePage(key: appKey),
|
home: const HomePage()
|
||||||
);
|
);
|
||||||
|
|
||||||
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
|
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
|
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard
|
||||||
focusTheme: FocusThemeData(
|
|
||||||
glowFactor: is10footScreen() ? 2.0 : 0.0,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:process_run/shell.dart';
|
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 '../model/server_type.dart';
|
||||||
import '../util/server.dart' as server;
|
import '../util/server.dart' as server;
|
||||||
@@ -62,7 +60,7 @@ Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
|
return await server.startRemoteServer(uri);
|
||||||
}catch(error){
|
}catch(error){
|
||||||
throw Exception("Cannot start reverse proxy");
|
throw Exception("Cannot start reverse proxy");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ class GameInstance {
|
|||||||
final Process gameProcess;
|
final Process gameProcess;
|
||||||
final Process? launcherProcess;
|
final Process? launcherProcess;
|
||||||
final Process? eacProcess;
|
final Process? eacProcess;
|
||||||
|
final int? watchDogProcessPid;
|
||||||
bool tokenError;
|
bool tokenError;
|
||||||
bool hasChildServer;
|
bool hasChildServer;
|
||||||
|
|
||||||
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.hasChildServer)
|
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.watchDogProcessPid, this.hasChildServer)
|
||||||
: tokenError = false;
|
: tokenError = false;
|
||||||
|
|
||||||
void kill() {
|
void kill() {
|
||||||
gameProcess.kill(ProcessSignal.sigabrt);
|
gameProcess.kill(ProcessSignal.sigabrt);
|
||||||
launcherProcess?.kill(ProcessSignal.sigabrt);
|
launcherProcess?.kill(ProcessSignal.sigabrt);
|
||||||
eacProcess?.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';
|
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||||
|
|
||||||
class BuildController extends GetxController {
|
class BuildController extends GetxController {
|
||||||
List<FortniteBuild>? builds;
|
List<FortniteBuild>? _builds;
|
||||||
FortniteBuild? _selectedBuild;
|
Rxn<FortniteBuild> selectedBuildRx;
|
||||||
final List<Function()> _listeners;
|
|
||||||
late RxBool cancelledDownload;
|
|
||||||
|
|
||||||
BuildController() : _listeners = [] {
|
BuildController() : selectedBuildRx = Rxn();
|
||||||
cancelledDownload = RxBool(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0);
|
List<FortniteBuild>? get builds => _builds;
|
||||||
|
|
||||||
set selectedBuild(FortniteBuild build) {
|
set builds(List<FortniteBuild>? builds) {
|
||||||
_selectedBuild = build;
|
_builds = builds;
|
||||||
for (var listener in _listeners) {
|
if(builds == null || builds.isEmpty){
|
||||||
listener();
|
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:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../../model/update_status.dart';
|
import '../../model/update_status.dart';
|
||||||
|
|
||||||
const String kDefaultPlayerName = "Player";
|
const String kDefaultPlayerName = "Player";
|
||||||
|
|
||||||
class GameController extends GetxController {
|
class GameController extends GetxController {
|
||||||
|
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;
|
||||||
@@ -21,7 +23,6 @@ class GameController extends GetxController {
|
|||||||
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 Rx<UpdateStatus> updateStatus;
|
|
||||||
GameInstance? instance;
|
GameInstance? instance;
|
||||||
|
|
||||||
GameController() {
|
GameController() {
|
||||||
@@ -35,6 +36,8 @@ class GameController extends GetxController {
|
|||||||
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();
|
||||||
|
_storage.write("uuid", uuid);
|
||||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||||
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
|
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
|
||||||
username.addListener(() => _storage.write("username", username.text));
|
username.addListener(() => _storage.write("username", username.text));
|
||||||
@@ -44,7 +47,6 @@ class GameController extends GetxController {
|
|||||||
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);
|
||||||
updateStatus = Rx(UpdateStatus.waiting);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteVersion? getVersionByName(String name) {
|
FortniteVersion? getVersionByName(String name) {
|
||||||
@@ -67,6 +69,9 @@ class GameController extends GetxController {
|
|||||||
|
|
||||||
void removeVersion(FortniteVersion version) {
|
void removeVersion(FortniteVersion version) {
|
||||||
versions.update((val) => val?.remove(version));
|
versions.update((val) => val?.remove(version));
|
||||||
|
if (selectedVersion?.name == version.name || hasNoVersions) {
|
||||||
|
selectedVersion = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveVersions() async {
|
Future<void> _saveVersions() async {
|
||||||
@@ -81,7 +86,7 @@ class GameController extends GetxController {
|
|||||||
FortniteVersion? get selectedVersion => _selectedVersion();
|
FortniteVersion? get selectedVersion => _selectedVersion();
|
||||||
|
|
||||||
set selectedVersion(FortniteVersion? version) {
|
set selectedVersion(FortniteVersion? version) {
|
||||||
_selectedVersion(version);
|
_selectedVersion.value = version;
|
||||||
_storage.write("version", version?.name);
|
_storage.write("version", version?.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
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/ui/controller/settings_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/ui/controller/update_controller.dart';
|
||||||
|
|
||||||
import '../../model/game_instance.dart';
|
import '../../model/game_instance.dart';
|
||||||
|
import '../../model/update_status.dart';
|
||||||
|
import '../../util/reboot.dart';
|
||||||
|
|
||||||
|
|
||||||
const String kDefaultServerName = "Reboot Game Server";
|
const String kDefaultServerName = "Reboot Game Server";
|
||||||
@@ -10,19 +14,39 @@ const String kDefaultServerName = "Reboot Game Server";
|
|||||||
class HostingController extends GetxController {
|
class HostingController extends GetxController {
|
||||||
late final GetStorage _storage;
|
late final GetStorage _storage;
|
||||||
late final TextEditingController name;
|
late final TextEditingController name;
|
||||||
late final TextEditingController category;
|
late final TextEditingController description;
|
||||||
late final RxBool discoverable;
|
late final RxBool discoverable;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
|
late final Rx<UpdateStatus> updateStatus;
|
||||||
GameInstance? instance;
|
GameInstance? instance;
|
||||||
|
|
||||||
HostingController() {
|
HostingController() {
|
||||||
_storage = GetStorage("reboot_hosting");
|
_storage = GetStorage("reboot_hosting");
|
||||||
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
|
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
|
||||||
name.addListener(() => _storage.write("name", name.text));
|
name.addListener(() => _storage.write("name", name.text));
|
||||||
category = TextEditingController(text: _storage.read("category") ?? "");
|
description = TextEditingController(text: _storage.read("description") ?? "");
|
||||||
category.addListener(() => _storage.write("category", category.text));
|
description.addListener(() => _storage.write("description", description.text));
|
||||||
discoverable = RxBool(_storage.read("discoverable") ?? false);
|
discoverable = RxBool(_storage.read("discoverable") ?? false);
|
||||||
discoverable.listen((value) => _storage.write("discoverable", value));
|
discoverable.listen((value) => _storage.write("discoverable", value));
|
||||||
|
updateStatus = Rx(UpdateStatus.waiting);
|
||||||
started = RxBool(false);
|
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:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/model/fortnite_version.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/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/ui/widget/home/version_name_input.dart';
|
||||||
|
|
||||||
import '../../util/checks.dart';
|
import '../../util/checks.dart';
|
||||||
import '../widget/shared/file_selector.dart';
|
import '../widget/shared/file_selector.dart';
|
||||||
@@ -38,12 +39,12 @@ class AddLocalVersion extends StatelessWidget {
|
|||||||
height: 16.0
|
height: 16.0
|
||||||
),
|
),
|
||||||
|
|
||||||
TextFormBox(
|
VersionNameInput(
|
||||||
controller: _nameController,
|
controller: _nameController
|
||||||
header: "Name",
|
),
|
||||||
placeholder: "Type the version's name",
|
|
||||||
autofocus: true,
|
const SizedBox(
|
||||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
height: 16.0
|
||||||
),
|
),
|
||||||
|
|
||||||
FileSelector(
|
FileSelector(
|
||||||
@@ -53,6 +54,10 @@ class AddLocalVersion extends StatelessWidget {
|
|||||||
controller: _gamePathController,
|
controller: _gamePathController,
|
||||||
validator: checkGameFolder,
|
validator: checkGameFolder,
|
||||||
folder: true
|
folder: true
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
final BuildController _buildController = Get.find<BuildController>();
|
final BuildController _buildController = Get.find<BuildController>();
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final TextEditingController _pathController = 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 DiskSpace _diskSpace;
|
||||||
late Future _fetchFuture;
|
late Future _fetchFuture;
|
||||||
late Future _diskFuture;
|
late Future _diskFuture;
|
||||||
|
|
||||||
DownloadStatus _status = DownloadStatus.form;
|
|
||||||
String _timeLeft = "00:00:00";
|
|
||||||
double _downloadProgress = 0;
|
|
||||||
CancelableOperation? _manifestDownloadProcess;
|
CancelableOperation? _manifestDownloadProcess;
|
||||||
CancelableOperation? _driveDownloadOperation;
|
|
||||||
Object? _error;
|
Object? _error;
|
||||||
StackTrace? _stackTrace;
|
StackTrace? _stackTrace;
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
_diskSpace = DiskSpace();
|
_diskSpace = DiskSpace();
|
||||||
_diskFuture = _diskSpace.scan()
|
_diskFuture = _diskSpace.scan()
|
||||||
.then((_) => _updateFormDefaults());
|
.then((_) => _updateFormDefaults());
|
||||||
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,57 +61,74 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_pathController.dispose();
|
_pathController.dispose();
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
_buildController.removeOnBuildChangedListener();
|
_cancelDownload();
|
||||||
_onDisposed();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDisposed() {
|
void _cancelDownload() {
|
||||||
if (_status != DownloadStatus.downloading) {
|
if (_status.value != DownloadStatus.extracting && _status.value != DownloadStatus.extracting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_manifestDownloadProcess != null) {
|
if (_manifestDownloadProcess == null) {
|
||||||
_manifestDownloadProcess?.cancel();
|
|
||||||
_buildController.cancelledDownload(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_driveDownloadOperation == null) {
|
Process.run('${assetsDirectory.path}\\builds\\stop.bat', []);
|
||||||
return;
|
_manifestDownloadProcess?.cancel();
|
||||||
}
|
|
||||||
|
|
||||||
_driveDownloadOperation!.cancel();
|
|
||||||
_buildController.cancelledDownload(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => Form(
|
||||||
switch(_status){
|
key: _formKey,
|
||||||
case DownloadStatus.form:
|
child: Obx(() {
|
||||||
return _createFormDialog();
|
switch(_status.value){
|
||||||
case DownloadStatus.downloading:
|
case DownloadStatus.form:
|
||||||
return GenericDialog(
|
return FutureBuilder(
|
||||||
header: _createDownloadBody(),
|
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||||
buttons: _createCloseButton()
|
builder: (context, snapshot) {
|
||||||
);
|
if (snapshot.hasError) {
|
||||||
case DownloadStatus.extracting:
|
WidgetsBinding.instance
|
||||||
return GenericDialog(
|
.addPostFrameCallback((_) =>
|
||||||
header: _createExtractingBody(),
|
_onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||||
buttons: _createCloseButton()
|
}
|
||||||
);
|
|
||||||
case DownloadStatus.error:
|
if (!snapshot.hasData) {
|
||||||
return ErrorDialog(
|
return ProgressDialog(
|
||||||
exception: _error ?? Exception("unknown error"),
|
text: "Fetching builds and disks...",
|
||||||
stackTrace: _stackTrace,
|
onStop: () => Navigator.of(context).pop()
|
||||||
errorMessageBuilder: (exception) => "Cannot download version: $exception"
|
);
|
||||||
);
|
}
|
||||||
case DownloadStatus.done:
|
|
||||||
return const InfoDialog(
|
return FormDialog(
|
||||||
text: "The download was completed successfully!",
|
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() {
|
List<DialogButton> _createFormButtons() {
|
||||||
return [
|
return [
|
||||||
@@ -127,61 +143,56 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
|
|
||||||
void _startDownload(BuildContext context) async {
|
void _startDownload(BuildContext context) async {
|
||||||
try {
|
try {
|
||||||
setState(() => _status = DownloadStatus.downloading);
|
var build = _buildController.selectedBuildRx.value;
|
||||||
|
if(build == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_status.value = DownloadStatus.downloading;
|
||||||
var future = downloadArchiveBuild(
|
var future = downloadArchiveBuild(
|
||||||
_buildController.selectedBuild.link,
|
build.link,
|
||||||
Directory(_pathController.text),
|
Directory(_pathController.text),
|
||||||
_onDownloadProgress,
|
(progress, eta) => _onDownloadProgress(progress, eta, false),
|
||||||
_onUnrar
|
(progress, eta) => _onDownloadProgress(progress, eta, true),
|
||||||
);
|
);
|
||||||
future.then((value) => _onDownloadComplete());
|
future.then((value) => _onDownloadComplete());
|
||||||
|
future.onError((error, stackTrace) => _onDownloadError(error, stackTrace));
|
||||||
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
|
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
|
||||||
} catch (exception, stackTrace) {
|
} catch (exception, stackTrace) {
|
||||||
_onDownloadError(exception, stackTrace);
|
_onDownloadError(exception, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onUnrar() {
|
|
||||||
setState(() => _status = DownloadStatus.extracting);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDownloadComplete() async {
|
Future<void> _onDownloadComplete() async {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
_status.value = DownloadStatus.done;
|
||||||
_status = DownloadStatus.done;
|
_gameController.addVersion(FortniteVersion(
|
||||||
_gameController.addVersion(FortniteVersion(
|
name: _nameController.text,
|
||||||
name: _nameController.text,
|
location: Directory(_pathController.text)
|
||||||
location: Directory(_pathController.text)
|
));
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||||
print("Error");
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
_status.value = DownloadStatus.error;
|
||||||
_status = DownloadStatus.error;
|
_error = error;
|
||||||
_error = error;
|
_stackTrace = stackTrace;
|
||||||
_stackTrace = stackTrace;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadProgress(double progress, String timeLeft) {
|
void _onDownloadProgress(double? progress, String? timeLeft, bool extracting) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
|
||||||
_status = DownloadStatus.downloading;
|
_timeLeft.value = timeLeft;
|
||||||
_timeLeft = timeLeft;
|
_downloadProgress.value = progress;
|
||||||
_downloadProgress = progress;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _createDownloadBody() => Column(
|
Widget _createDownloadBody() => Column(
|
||||||
@@ -204,14 +215,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${_downloadProgress.round()}%",
|
"${(_downloadProgress.value ?? 0).round()}%",
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
),
|
),
|
||||||
|
|
||||||
Text(
|
if(_timeLeft.value != null)
|
||||||
"Time left: $_timeLeft",
|
Text(
|
||||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
"Time left: ${_timeLeft.value}",
|
||||||
)
|
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -221,7 +233,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ProgressBar(value: _downloadProgress.toDouble())
|
child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble())
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(
|
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() {
|
Widget _createFormBody() {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const BuildSelector(),
|
BuildSelector(
|
||||||
const SizedBox(height: 20.0),
|
onSelected: _updateFormDefaults
|
||||||
VersionNameInput(controller: _nameController),
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
|
),
|
||||||
|
|
||||||
|
VersionNameInput(
|
||||||
|
controller: _nameController
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
|
),
|
||||||
|
|
||||||
FileSelector(
|
FileSelector(
|
||||||
label: "Path",
|
label: "Path",
|
||||||
placeholder: "Type the download destination",
|
placeholder: "Type the download destination",
|
||||||
@@ -298,6 +298,10 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
validator: checkDownloadDestination,
|
validator: checkDownloadDestination,
|
||||||
folder: true
|
folder: true
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(
|
||||||
|
height: 16.0
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -319,9 +323,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
await _fetchFuture;
|
await _fetchFuture;
|
||||||
var bestDisk = _diskSpace.disks
|
var bestDisk = _diskSpace.disks
|
||||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||||
|
var build = _buildController.selectedBuildRx.value;
|
||||||
|
if(build== null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
|
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
|
||||||
"${_buildController.selectedBuild.version.toString()}";
|
"${build.version}";
|
||||||
_nameController.text = _buildController.selectedBuild.version.toString();
|
_nameController.text = build.version.toString();
|
||||||
|
_formKey.currentState?.validate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class FormDialog extends AbstractDialog {
|
|||||||
text: entry.text,
|
text: entry.text,
|
||||||
type: entry.type,
|
type: entry.type,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if(!Form.of(context)!.validate()) {
|
if(!Form.of(context).validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import '../../../main.dart';
|
|||||||
import '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. "
|
||||||
"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 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 = "The build you are currently using is corrupted. "
|
const String _corruptedBuildError = "An unknown error happened while launching Fortnite. "
|
||||||
"This means that some critical files are missing for the game to launch. "
|
"Some critical could be missing in your installation. "
|
||||||
"Download the build again from the launcher or, if it's not available there, from another source. "
|
"Download the build again from the launcher, not locally, or from a different source. "
|
||||||
"Occasionally some files might get corrupted if there isn't enough space on your drive.";
|
"Alternatively, something could have gone wrong in the launcher. ";
|
||||||
|
|
||||||
Future<void> showBrokenError() async {
|
Future<void> showBrokenError() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -82,7 +80,7 @@ Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? st
|
|||||||
builder: (context) => ErrorDialog(
|
builder: (context) => ErrorDialog(
|
||||||
exception: error,
|
exception: error,
|
||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
errorMessageBuilder: (exception) => _corruptedBuildError
|
errorMessageBuilder: (exception) => "${_corruptedBuildError}Error message: $exception"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,11 +125,6 @@ extension ServerControllerDialog on ServerController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _showPortTakenDialog(3551);
|
|
||||||
if (!result) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await freeLawinPort();
|
await freeLawinPort();
|
||||||
await stop();
|
await stop();
|
||||||
return _toggle(newResultType);
|
return _toggle(newResultType);
|
||||||
@@ -139,11 +134,6 @@ extension ServerControllerDialog on ServerController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _showPortTakenDialog(8080);
|
|
||||||
if (!result) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await freeMatchmakerPort();
|
await freeMatchmakerPort();
|
||||||
await stop();
|
await stop();
|
||||||
return _toggle(newResultType);
|
return _toggle(newResultType);
|
||||||
@@ -203,14 +193,13 @@ extension ServerControllerDialog on ServerController {
|
|||||||
|
|
||||||
Future<Uri?> _pingRemoteInteractive() async {
|
Future<Uri?> _pingRemoteInteractive() async {
|
||||||
try {
|
try {
|
||||||
var mainFuture = ping(host.text, port.text).then((value) => value != null);
|
var future = ping(host.text, port.text);
|
||||||
var future = _waitFutureOrTime(mainFuture);
|
await showDialog<bool>(
|
||||||
var result = await showDialog<bool>(
|
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
FutureBuilderDialog(
|
FutureBuilderDialog(
|
||||||
future: future,
|
future: future,
|
||||||
closeAutomatically: false,
|
closeAutomatically: true,
|
||||||
loadingMessage: "Pinging remote server...",
|
loadingMessage: "Pinging remote server...",
|
||||||
successfulBody: FutureBuilderDialog.ofMessage(
|
successfulBody: FutureBuilderDialog.ofMessage(
|
||||||
"The server at ${host.text}:${port
|
"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."),
|
.text} doesn't work. Check the hostname and/or the port and try again."),
|
||||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
||||||
)
|
)
|
||||||
) ?? false;
|
);
|
||||||
return result ? await future : null;
|
return await future;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
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() {
|
void _showCannotStopError() {
|
||||||
if(!started.value){
|
if(!started.value){
|
||||||
return;
|
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");
|
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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_rx/src/rx_types/rx_types.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/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/ui/page/launcher_page.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/server_page.dart';
|
||||||
@@ -21,8 +22,9 @@ class HomePage extends StatefulWidget {
|
|||||||
State<HomePage> createState() => _HomePageState();
|
State<HomePage> createState() => _HomePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WindowListener {
|
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
static const double _defaultPadding = 12.0;
|
static const double _kDefaultPadding = 12.0;
|
||||||
|
static const int _kPagesLength = 5;
|
||||||
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
|
||||||
@@ -32,8 +34,11 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
||||||
final RxBool _focused = RxBool(true);
|
final RxBool _focused = RxBool(true);
|
||||||
final RxInt _index = RxInt(0);
|
final RxInt _index = RxInt(0);
|
||||||
final RxBool _nestedNavigation = RxBool(false);
|
final List<GlobalKey<NavigatorState>> _navigators = List.generate(_kPagesLength, (index) => GlobalKey());
|
||||||
final GlobalKey<NavigatorState> _settingsNavigatorKey = GlobalKey();
|
final List<RxBool> _navigationStatus = List.generate(_kPagesLength, (index) => RxBool(false));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -85,38 +90,45 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Obx(() => Stack(
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
NavigationView(
|
LayoutBuilder(
|
||||||
paneBodyBuilder: (body) => Padding(
|
builder: (context, specs) => Obx(() => NavigationView(
|
||||||
padding: const EdgeInsets.all(_defaultPadding),
|
paneBodyBuilder: (pane, body) => Padding(
|
||||||
child: body
|
padding: const EdgeInsets.all(_kDefaultPadding),
|
||||||
),
|
child: body
|
||||||
appBar: NavigationAppBar(
|
),
|
||||||
title: _draggableArea,
|
appBar: NavigationAppBar(
|
||||||
actions: WindowTitleBar(focused: _focused()),
|
title: _draggableArea,
|
||||||
leading: _backButton
|
actions: WindowTitleBar(focused: _focused()),
|
||||||
),
|
leading: _backButton
|
||||||
pane: NavigationPane(
|
),
|
||||||
selected: _selectedIndex,
|
pane: NavigationPane(
|
||||||
onChanged: _onIndexChanged,
|
key: appKey,
|
||||||
displayMode: PaneDisplayMode.auto,
|
selected: _selectedIndex,
|
||||||
items: _items,
|
onChanged: _onIndexChanged,
|
||||||
footerItems: _footerItems,
|
displayMode: specs.biggest.width <= 1536 ? PaneDisplayMode.compact : PaneDisplayMode.open,
|
||||||
autoSuggestBox: _autoSuggestBox,
|
items: _items,
|
||||||
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
|
footerItems: _footerItems,
|
||||||
),
|
autoSuggestBox: _autoSuggestBox,
|
||||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
|
||||||
transitionBuilder: (child, animation) => child
|
),
|
||||||
|
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||||
|
transitionBuilder: (child, animation) => child
|
||||||
|
))
|
||||||
),
|
),
|
||||||
if(_focused() && isWin11)
|
Obx(() => isWin11 && _focused.value ? const WindowBorder() : const SizedBox())
|
||||||
const WindowBorder()
|
|
||||||
]
|
]
|
||||||
));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget get _backButton => Obx(() {
|
Widget get _backButton => Obx(() {
|
||||||
// ignore: unused_local_variable
|
for(var entry in _navigationStatus){
|
||||||
var ignored = _nestedNavigation.value;
|
entry.value;
|
||||||
|
}
|
||||||
|
|
||||||
return PaneItem(
|
return PaneItem(
|
||||||
icon: const Icon(FluentIcons.back, size: 14.0),
|
icon: const Icon(FluentIcons.back, size: 14.0),
|
||||||
body: const SizedBox.shrink(),
|
body: const SizedBox.shrink(),
|
||||||
@@ -128,15 +140,20 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
void Function()? _onBack() {
|
Function()? _onBack() {
|
||||||
var navigator = _settingsNavigatorKey.currentState;
|
var navigator = _navigators[_index.value].currentState;
|
||||||
if(navigator == null || !navigator.mounted || !navigator.canPop()){
|
if(navigator == null || !navigator.mounted || !navigator.canPop()){
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var status = _navigationStatus[_index.value];
|
||||||
|
if(!status.value){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return () async {
|
return () async {
|
||||||
Navigator.pop(navigator.context);
|
Navigator.pop(navigator.context);
|
||||||
_nestedNavigation.value = false;
|
status.value = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,25 +204,25 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Play"),
|
title: const Text("Play"),
|
||||||
icon: const Icon(FluentIcons.game),
|
icon: const Icon(FluentIcons.game),
|
||||||
body: const LauncherPage()
|
body: LauncherPage(_navigators[0], _navigationStatus[0])
|
||||||
),
|
),
|
||||||
|
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Host"),
|
title: const Text("Host"),
|
||||||
icon: const Icon(FluentIcons.server_processes),
|
icon: const Icon(FluentIcons.server_processes),
|
||||||
body: const HostingPage()
|
body: HostingPage(_navigators[1], _navigationStatus[1])
|
||||||
),
|
),
|
||||||
|
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Backend"),
|
title: const Text("Backend"),
|
||||||
icon: const Icon(FluentIcons.user_window),
|
icon: const Icon(FluentIcons.user_window),
|
||||||
body: ServerPage()
|
body: ServerPage(_navigators[2], _navigationStatus[2])
|
||||||
),
|
),
|
||||||
|
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Tutorial"),
|
title: const Text("Tutorial"),
|
||||||
icon: const Icon(FluentIcons.info),
|
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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/hosting_controller.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/launch_button.dart';
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.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: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 {
|
class HostingPage extends StatefulWidget {
|
||||||
const HostingPage(
|
final GlobalKey<NavigatorState> navigatorKey;
|
||||||
{Key? key})
|
final RxBool nestedNavigation;
|
||||||
: super(key: key);
|
const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HostingPage> createState() => _HostingPageState();
|
State<HostingPage> createState() => _HostingPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HostingPageState extends State<HostingPage> {
|
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
|
||||||
@override
|
@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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
Obx(() => SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: InfoBar(
|
child: _hostingController.updateStatus.value == UpdateStatus.error ? _updateError :_rebootGuiInfo,
|
||||||
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
|
)),
|
||||||
severity: InfoBarSeverity.info
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16.0
|
height: 16.0
|
||||||
),
|
),
|
||||||
@@ -48,12 +83,12 @@ class _HostingPageState extends State<HostingPage> {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: "Category",
|
title: "Description",
|
||||||
subtitle: "The category of your game server",
|
subtitle: "The description of your game server",
|
||||||
isChild: true,
|
isChild: true,
|
||||||
content: TextFormBox(
|
content: TextFormBox(
|
||||||
placeholder: "Category",
|
placeholder: "Description",
|
||||||
controller: _hostingController.category
|
controller: _hostingController.description
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
SettingTile(
|
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 Expanded(child: SizedBox()),
|
||||||
const LaunchButton(
|
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();
|
State<InfoPage> createState() => _InfoPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InfoPageState extends State<InfoPage> {
|
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
|
||||||
final List<String> _elseTitles = [
|
final List<String> _elseTitles = [
|
||||||
"Open the home page",
|
"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",
|
"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>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
late final ScrollController _controller;
|
late final ScrollController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance);
|
_controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance);
|
||||||
@@ -71,8 +74,6 @@ class _InfoPageState extends State<InfoPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _createScreen(String? name) {
|
Widget _createScreen(String? name) {
|
||||||
WidgetsBinding.instance
|
|
||||||
.addPostFrameCallback((_) => widget.nestedNavigation.value = name != "home");
|
|
||||||
switch(name){
|
switch(name){
|
||||||
case "home":
|
case "home":
|
||||||
return _homeScreen;
|
return _homeScreen;
|
||||||
@@ -91,7 +92,10 @@ class _InfoPageState extends State<InfoPage> {
|
|||||||
_createCardWidget(
|
_createCardWidget(
|
||||||
text: "Play on someone else's server",
|
text: "Play on someone else's server",
|
||||||
description: "If one of your friends is hosting a game server, click here",
|
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(
|
const SizedBox(
|
||||||
@@ -101,7 +105,10 @@ class _InfoPageState extends State<InfoPage> {
|
|||||||
_createCardWidget(
|
_createCardWidget(
|
||||||
text: "Host your own server",
|
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",
|
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:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/ui/controller/game_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/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/launch_button.dart';
|
||||||
import 'package:reboot_launcher/src/ui/widget/home/version_selector.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: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/checks.dart';
|
||||||
import '../../util/reboot.dart';
|
|
||||||
import '../controller/update_controller.dart';
|
|
||||||
|
|
||||||
class LauncherPage extends StatefulWidget {
|
class LauncherPage extends StatefulWidget {
|
||||||
const LauncherPage(
|
final GlobalKey<NavigatorState> navigatorKey;
|
||||||
{Key? key})
|
final RxBool nestedNavigation;
|
||||||
: super(key: key);
|
const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LauncherPage> createState() => _LauncherPageState();
|
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 GameController _gameController = Get.find<GameController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final BuildController _buildController = Get.find<BuildController>();
|
|
||||||
late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty);
|
late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
Widget build(BuildContext context) => Column(
|
||||||
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(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
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(
|
SettingTile(
|
||||||
title: "Credentials",
|
title: "Credentials",
|
||||||
subtitle: "Your in-game login credentials",
|
subtitle: "Your in-game login credentials",
|
||||||
@@ -144,7 +130,10 @@ class _LauncherPageState extends State<LauncherPage> {
|
|||||||
title: "Browse available servers",
|
title: "Browse available servers",
|
||||||
subtitle: "Discover new game servers that fit your play-style",
|
subtitle: "Discover new game servers that fit your play-style",
|
||||||
content: Button(
|
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")
|
child: const Text("Browse")
|
||||||
),
|
),
|
||||||
isChild: true
|
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/model/server_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/server.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/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_type_selector.dart';
|
||||||
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
|
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../widget/shared/setting_tile.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>();
|
final ServerController _serverController = Get.find<ServerController>();
|
||||||
|
|
||||||
ServerPage({Key? key}) : super(key: key);
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
return Obx(() => Column(
|
return Obx(() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/controller/settings_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/dialog/dialog_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -13,24 +14,36 @@ import '../../util/selector.dart';
|
|||||||
import '../dialog/dialog.dart';
|
import '../dialog/dialog.dart';
|
||||||
import '../widget/shared/setting_tile.dart';
|
import '../widget/shared/setting_tile.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatefulWidget {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
|
||||||
|
|
||||||
SettingsPage({Key? key}) : super(key: key);
|
SettingsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Column(
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
}
|
||||||
children: [
|
|
||||||
SettingTile(
|
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",
|
title: "File settings",
|
||||||
subtitle: "This section contains all the settings related to files used by Fortnite",
|
subtitle: "This section contains all the settings related to files used by Fortnite",
|
||||||
expandedContent: [
|
expandedContent: [
|
||||||
_createFileSetting(
|
_createFileSetting(
|
||||||
title: "Game server",
|
title: "Game server",
|
||||||
description: "This file is injected to create a game server to host matches",
|
description: "This file is injected to create a game server to host matches",
|
||||||
controller: _settingsController.rebootDll
|
controller: _settingsController.rebootDll
|
||||||
),
|
),
|
||||||
_createFileSetting(
|
_createFileSetting(
|
||||||
title: "Unreal engine console",
|
title: "Unreal engine console",
|
||||||
@@ -43,115 +56,111 @@ class SettingsPage extends StatelessWidget {
|
|||||||
controller: _settingsController.authDll
|
controller: _settingsController.authDll
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16.0,
|
height: 16.0,
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: "Automatic updates",
|
title: "Automatic updates",
|
||||||
subtitle: "Choose whether the launcher and its files should be automatically updated",
|
subtitle: "Choose whether the launcher and its files should be automatically updated",
|
||||||
contentWidth: null,
|
contentWidth: null,
|
||||||
content: Obx(() => ToggleSwitch(
|
content: Obx(() => ToggleSwitch(
|
||||||
checked: _settingsController.autoUpdate(),
|
checked: _settingsController.autoUpdate.value,
|
||||||
onChanged: (value) => _settingsController.autoUpdate.value = value
|
onChanged: (value) => _settingsController.autoUpdate.value = value
|
||||||
))
|
)),
|
||||||
),
|
expandedContentSpacing: 0,
|
||||||
const SizedBox(
|
expandedContent: [
|
||||||
height: 16.0,
|
SettingTile(
|
||||||
),
|
title: "Update Mirror",
|
||||||
SettingTile(
|
subtitle: "The URL used to pull the latest update once a day",
|
||||||
title: "Custom launch arguments",
|
content: Obx(() => TextFormBox(
|
||||||
subtitle: "Enter additional arguments to use when launching the game",
|
placeholder: "URL",
|
||||||
content: TextFormBox(
|
controller: _settingsController.updateUrl,
|
||||||
placeholder: "Arguments...",
|
enabled: _settingsController.autoUpdate.value,
|
||||||
controller: _gameController.customLaunchArgs,
|
validator: checkUpdateUrl
|
||||||
)
|
)),
|
||||||
),
|
isChild: true
|
||||||
const SizedBox(
|
)
|
||||||
height: 16.0,
|
]
|
||||||
),
|
),
|
||||||
SettingTile(
|
const SizedBox(
|
||||||
title: "Create a bug report",
|
height: 16.0,
|
||||||
subtitle: "Help me fix bugs by reporting them",
|
),
|
||||||
content: Button(
|
SettingTile(
|
||||||
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues/new/choose")),
|
title: "Custom launch arguments",
|
||||||
child: const Text("Report a bug"),
|
subtitle: "Enter additional arguments to use when launching the game",
|
||||||
)
|
content: TextFormBox(
|
||||||
),
|
placeholder: "Arguments...",
|
||||||
const SizedBox(
|
controller: _gameController.customLaunchArgs,
|
||||||
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: "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';
|
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||||
|
|
||||||
class BuildSelector extends StatefulWidget {
|
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
|
@override
|
||||||
State<BuildSelector> createState() => _BuildSelectorState();
|
State<BuildSelector> createState() => _BuildSelectorState();
|
||||||
@@ -18,14 +19,20 @@ class _BuildSelectorState extends State<BuildSelector> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InfoLabel(
|
return InfoLabel(
|
||||||
label: "Build",
|
label: "Build",
|
||||||
child: ComboBox<FortniteBuild>(
|
child: Obx(() => ComboBox<FortniteBuild>(
|
||||||
placeholder: const Text('Select a fortnite build'),
|
placeholder: const Text('Select a fortnite build'),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: _createItems(),
|
items: _createItems(),
|
||||||
value: _buildController.selectedBuild,
|
value: _buildController.selectedBuildRx.value,
|
||||||
onChanged: (value) =>
|
onChanged: (value) {
|
||||||
value == null ? {} : setState(() => _buildController.selectedBuild = 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/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
import '../../../util/process.dart';
|
import '../../../util/process.dart';
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ class LaunchButton extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LaunchButtonState extends State<LaunchButton> {
|
class _LaunchButtonState extends State<LaunchButton> {
|
||||||
|
static const String _kLoadingRoute = '/loading';
|
||||||
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
||||||
final List<String> _corruptedBuildErrors = [
|
final List<String> _corruptedBuildErrors = [
|
||||||
"when 0 bytes remain",
|
"when 0 bytes remain",
|
||||||
@@ -50,6 +52,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
"UOnlineAccountCommon::ForceLogout"
|
"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 ServerController _serverController = Get.find<ServerController>();
|
||||||
@@ -116,10 +119,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_fail = false;
|
|
||||||
var version = _gameController.selectedVersion!;
|
var version = _gameController.selectedVersion!;
|
||||||
var gamePath = version.executable?.path;
|
if(version.executable?.path == null){
|
||||||
if(gamePath == null){
|
|
||||||
showMissingBuildError(version);
|
showMissingBuildError(version);
|
||||||
_onStop(widget.host);
|
_onStop(widget.host);
|
||||||
return;
|
return;
|
||||||
@@ -131,7 +132,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await compute(patchMatchmaking, version.executable!);
|
|
||||||
await compute(patchHeadless, version.executable!);
|
await compute(patchHeadless, version.executable!);
|
||||||
|
|
||||||
var automaticallyStartedServer = await _startMatchMakingServer();
|
var automaticallyStartedServer = await _startMatchMakingServer();
|
||||||
@@ -141,9 +141,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
await _showServerLaunchingWarning();
|
await _showServerLaunchingWarning();
|
||||||
}
|
}
|
||||||
} catch (exception, stacktrace) {
|
} catch (exception, stacktrace) {
|
||||||
_closeDialogIfOpen(false);
|
_closeLaunchingWidget(false);
|
||||||
showCorruptedBuildError(widget.host, exception, stacktrace);
|
|
||||||
_onStop(widget.host);
|
_onStop(widget.host);
|
||||||
|
showCorruptedBuildError(widget.host, exception, stacktrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
var launcherProcess = await _createLauncherProcess(version);
|
var launcherProcess = await _createLauncherProcess(version);
|
||||||
var eacProcess = await _createEacProcess(version);
|
var eacProcess = await _createEacProcess(version);
|
||||||
var gameProcess = await _createGameProcess(version.executable!.path, host);
|
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){
|
if(host){
|
||||||
_hostingController.instance = instance;
|
_hostingController.instance = instance;
|
||||||
}else{
|
}else{
|
||||||
@@ -161,6 +162,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
_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;
|
||||||
@@ -226,33 +234,50 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeDialogIfOpen(false);
|
_closeLaunchingWidget(false);
|
||||||
_onStop(widget.host);
|
_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!);
|
var route = ModalRoute.of(appKey.currentContext!);
|
||||||
if(route == null || route.isCurrent){
|
if(route == null || route.isCurrent){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(appKey.currentContext!).pop(success);
|
Navigator.of(context).pop(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showServerLaunchingWarning() async {
|
Future<void> _showServerLaunchingWarning() async {
|
||||||
var result = await showDialog<bool>(
|
var result = await showDialog<bool>(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) => ProgressDialog(
|
builder: (context) => ProgressDialog(
|
||||||
|
key: _headlessServerKey,
|
||||||
text: "Launching headless server...",
|
text: "Launching headless server...",
|
||||||
onStop: () =>_onEnd()
|
onStop: () => Navigator.of(context).pop(false)
|
||||||
)
|
)
|
||||||
) ?? false;
|
) ?? false;
|
||||||
|
|
||||||
if(result){
|
if(!result){
|
||||||
|
_onStop(true);
|
||||||
return;
|
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) {
|
void _onGameOutput(String line, bool host) {
|
||||||
@@ -280,7 +305,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fail = true;
|
_fail = true;
|
||||||
_closeDialogIfOpen(false);
|
_closeLaunchingWidget(false);
|
||||||
_showTokenError(host);
|
_showTokenError(host);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -290,7 +315,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
_injectOrShowError(Injectable.console, host);
|
_injectOrShowError(Injectable.console, host);
|
||||||
}else {
|
}else {
|
||||||
_injectOrShowError(Injectable.reboot, host)
|
_injectOrShowError(Injectable.reboot, host)
|
||||||
.then((value) => _closeDialogIfOpen(true));
|
.then((value) => _closeLaunchingWidget(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
_injectOrShowError(Injectable.memoryFix, host);
|
_injectOrShowError(Injectable.memoryFix, host);
|
||||||
@@ -340,6 +365,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setStarted(host, false);
|
_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 {
|
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
|
||||||
@@ -387,12 +419,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
void _onDllFail(File dllPath, bool hosting) {
|
void _onDllFail(File dllPath, bool hosting) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if(_fail){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_fail = true;
|
_fail = true;
|
||||||
_closeDialogIfOpen(false);
|
_closeLaunchingWidget(false);
|
||||||
showMissingDllError(path.basename(dllPath.path));
|
showMissingDllError(path.basename(dllPath.path));
|
||||||
_onStop(hosting);
|
_onStop(hosting);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ class VersionNameInput extends StatelessWidget {
|
|||||||
VersionNameInput({Key? key, required this.controller}) : super(key: key);
|
VersionNameInput({Key? key, required this.controller}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => InfoLabel(
|
||||||
return TextFormBox(
|
label: "Name",
|
||||||
header: "Name",
|
child: TextFormBox(
|
||||||
placeholder: "Type the version's name",
|
controller: controller,
|
||||||
controller: controller,
|
placeholder: "Type the version's name",
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
validator: _validate,
|
validator: _validate,
|
||||||
);
|
autovalidateMode: AutovalidateMode.onUserInteraction
|
||||||
}
|
),
|
||||||
|
);
|
||||||
|
|
||||||
String? _validate(String? text) {
|
String? _validate(String? text) {
|
||||||
if (text == null || text.isEmpty) {
|
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/model/fortnite_version.dart';
|
||||||
import 'package:reboot_launcher/src/ui/dialog/add_local_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/ui/widget/shared/smart_check_box.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/dialog/add_server_version.dart';
|
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
|
||||||
@@ -135,12 +136,8 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_gameController.removeVersion(version);
|
_gameController.removeVersion(version);
|
||||||
if (_gameController.selectedVersion?.name == version.name || _gameController.hasNoVersions) {
|
|
||||||
_gameController.selectedVersion = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_deleteFilesController.value && await version.location.exists()) {
|
if (_deleteFilesController.value && await version.location.exists()) {
|
||||||
version.location.delete(recursive: true);
|
delete(version.location);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -213,12 +210,14 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextFormBox(
|
InfoLabel(
|
||||||
controller: nameController,
|
label: "Name",
|
||||||
header: "Name",
|
child: TextFormBox(
|
||||||
placeholder: "Type the new version name",
|
controller: nameController,
|
||||||
autofocus: true,
|
placeholder: "Type the new version name",
|
||||||
validator: (text) => checkChangeVersion(text)
|
autofocus: true,
|
||||||
|
validator: (text) => checkChangeVersion(text)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -228,6 +227,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
FileSelector(
|
FileSelector(
|
||||||
placeholder: "Type the new game folder",
|
placeholder: "Type the new game folder",
|
||||||
windowTitle: "Select game folder",
|
windowTitle: "Select game folder",
|
||||||
|
label: "Path",
|
||||||
controller: pathController,
|
controller: pathController,
|
||||||
validator: checkGameFolder,
|
validator: checkGameFolder,
|
||||||
folder: true
|
folder: true
|
||||||
|
|||||||
@@ -48,28 +48,16 @@ class _FileSelectorState extends State<FileSelector> {
|
|||||||
) : _buildBody;
|
) : _buildBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget get _buildBody => Row(
|
Widget get _buildBody => TextFormBox(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
controller: widget.controller,
|
||||||
children: [
|
placeholder: widget.placeholder,
|
||||||
Expanded(
|
validator: widget.validator,
|
||||||
child: TextFormBox(
|
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction,
|
||||||
controller: widget.controller,
|
suffix: !widget.allowNavigator ? null : Button(
|
||||||
placeholder: widget.placeholder,
|
onPressed: _onPressed,
|
||||||
validator: widget.validator,
|
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
if (widget.allowNavigator)
|
suffixMode: OverlayVisibilityMode.editing
|
||||||
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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
void _onPressed() {
|
void _onPressed() {
|
||||||
|
|||||||
@@ -26,16 +26,24 @@ class SmartInput extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormBox(
|
if(label != null){
|
||||||
|
return InfoLabel(
|
||||||
|
label: label!,
|
||||||
|
child: _body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _body;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFormBox get _body => TextFormBox(
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
header: label,
|
|
||||||
keyboardType: type,
|
keyboardType: type,
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
autovalidateMode: validatorMode,
|
autovalidateMode: validatorMode,
|
||||||
validator: validator
|
validator: validator
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
import 'package:html/parser.dart' show parse;
|
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/time.dart';
|
||||||
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:unrar_file/unrar_file.dart';
|
|
||||||
|
|
||||||
import 'os.dart';
|
import 'os.dart';
|
||||||
|
|
||||||
@@ -44,21 +45,28 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
|||||||
return results;
|
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 {
|
try {
|
||||||
destination.createSync(recursive: true);
|
destination.createSync(recursive: true);
|
||||||
var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1);
|
var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1);
|
||||||
var extension = path.extension(fileName);
|
var extension = path.extension(fileName);
|
||||||
var tempFile = File("${outputDir.path}//$fileName");
|
var tempFile = File("${outputDir.path}\\$fileName");
|
||||||
var startTime = DateTime.now().millisecondsSinceEpoch;
|
if(tempFile.existsSync()) {
|
||||||
|
tempFile.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
var client = http.Client();
|
var client = http.Client();
|
||||||
var response = await client.send(
|
var request = http.Request("GET", Uri.parse(archiveUrl));
|
||||||
http.Request("GET", Uri.parse(archiveUrl)));
|
request.headers['Connection'] = 'Keep-Alive';
|
||||||
|
var response = await client.send(request);
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
var length = response.contentLength!;
|
var length = response.contentLength!;
|
||||||
var received = 0;
|
var received = 0;
|
||||||
var sink = tempFile.openWrite();
|
var sink = tempFile.openWrite();
|
||||||
@@ -69,17 +77,66 @@ Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Func
|
|||||||
onProgress((received / length) * 100, toETA(eta));
|
onProgress((received / length) * 100, toETA(eta));
|
||||||
return s;
|
return s;
|
||||||
}).pipe(sink);
|
}).pipe(sink);
|
||||||
onRar();
|
|
||||||
if(extension.toLowerCase() == ".zip"){
|
var receiverPort = ReceivePort();
|
||||||
await extractFileToDisk(tempFile.path, destination.path);
|
var file = _CompressedFile(extension, tempFile.path, destination.path, receiverPort.sendPort);
|
||||||
}else if(extension.toLowerCase() == ".rar") {
|
Isolate.spawn<_CompressedFile>(_decompress, file);
|
||||||
await UnrarFile.extract_rar(tempFile.path, destination.path);
|
var completer = Completer();
|
||||||
} else {
|
receiverPort.forEach((element) {
|
||||||
throw Exception("Unknown file extension: $extension");
|
onDecompress(element.progress, element.eta);
|
||||||
}
|
if(element.progress != null && element.progress >= 100){
|
||||||
|
completer.complete(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await completer.future;
|
||||||
|
delete(outputDir);
|
||||||
} catch(message) {
|
} catch(message) {
|
||||||
throw Exception("Cannot download build: $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 "Empty hostname";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? checkUpdateUrl(String? text) {
|
||||||
|
if (text == null || text.isEmpty) {
|
||||||
|
return "Empty URL";
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,9 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../ui/dialog/dialog.dart';
|
import '../ui/dialog/dialog.dart';
|
||||||
|
|
||||||
|
|
||||||
|
String? lastError;
|
||||||
|
|
||||||
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||||
if(exception == null){
|
if(exception == null){
|
||||||
return;
|
return;
|
||||||
@@ -12,6 +15,16 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
|||||||
return;
|
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(
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import 'package:win32/win32.dart';
|
|||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
|
|
||||||
const int appBarSize = 2;
|
const int appBarSize = 2;
|
||||||
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
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 = _regex.firstMatch(Platform.operatingSystemVersion)?.group(1);
|
||||||
if(result == null){
|
if(result == null){
|
||||||
@@ -19,6 +20,33 @@ bool get isWin11 {
|
|||||||
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 {
|
Future<bool> runElevated(String executable, String args) async {
|
||||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
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 {
|
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
|
||||||
Directory? outputDir;
|
Directory? outputDir;
|
||||||
|
var now = DateTime.now();
|
||||||
try {
|
try {
|
||||||
var now = DateTime.now();
|
|
||||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||||
var exists = await rebootDllFile.exists();
|
var exists = await rebootDllFile.exists();
|
||||||
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && 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;
|
return now.millisecondsSinceEpoch;
|
||||||
}catch(message) {
|
}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");
|
throw Exception("Cannot download reboot.zip, invalid zip: $message");
|
||||||
}finally{
|
}finally{
|
||||||
if(outputDir != null) {
|
if(outputDir != null) {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ Future<void> startServer(bool detached) async {
|
|||||||
serverExeFile.path,
|
serverExeFile.path,
|
||||||
[],
|
[],
|
||||||
workingDirectory: serverDirectory.path,
|
workingDirectory: serverDirectory.path,
|
||||||
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
|
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal,
|
||||||
|
runInShell: detached
|
||||||
);
|
);
|
||||||
if(!detached) {
|
if(!detached) {
|
||||||
serverLogFile.createSync(recursive: true);
|
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(
|
return ServerResult(
|
||||||
type: ServerResultType.illegalPortError
|
type: ServerResultType.illegalPortError
|
||||||
);
|
);
|
||||||
@@ -179,9 +187,7 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HttpServer> startRemoteServer(Uri uri) async {
|
Future<HttpServer> startRemoteServer(Uri uri) async => await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||||
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerResult {
|
class ServerResult {
|
||||||
final int? pid;
|
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}';
|
||||||
|
}
|
||||||
|
}
|
||||||
11
pubspec.yaml
11
pubspec.yaml
@@ -1,6 +1,6 @@
|
|||||||
name: reboot_launcher
|
name: reboot_launcher
|
||||||
description: Launcher for project reboot
|
description: Launcher for project reboot
|
||||||
version: "7.0.0"
|
version: "8.0.0"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ dependencies:
|
|||||||
|
|
||||||
bitsdojo_window:
|
bitsdojo_window:
|
||||||
path: ./dependencies/bitsdojo_window-0.1.5
|
path: ./dependencies/bitsdojo_window-0.1.5
|
||||||
fluent_ui: ^4.1.3
|
fluent_ui: ^4.6.2
|
||||||
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
|
http: ^0.13.5
|
||||||
@@ -40,7 +40,8 @@ dependencies:
|
|||||||
jaguar: ^3.1.3
|
jaguar: ^3.1.3
|
||||||
hex: ^0.2.0
|
hex: ^0.2.0
|
||||||
uuid: ^3.0.6
|
uuid: ^3.0.6
|
||||||
unrar_file: ^1.1.0
|
supabase_flutter: ^1.10.0
|
||||||
|
supabase: ^1.9.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -48,11 +49,13 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter_lints: ^2.0.1
|
flutter_lints: ^2.0.1
|
||||||
msix: ^3.6.3
|
msix: ^3.6.3
|
||||||
|
flutter_distributor: ^0.3.4
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- assets/builds/
|
- assets/builds/
|
||||||
|
- assets/browse/
|
||||||
- assets/dlls/
|
- assets/dlls/
|
||||||
- assets/icons/
|
- assets/icons/
|
||||||
- assets/images/
|
- assets/images/
|
||||||
@@ -67,7 +70,7 @@ msix_config:
|
|||||||
display_name: Reboot Launcher
|
display_name: Reboot Launcher
|
||||||
publisher_display_name: Auties00
|
publisher_display_name: Auties00
|
||||||
identity_name: 31868Auties00.RebootLauncher
|
identity_name: 31868Auties00.RebootLauncher
|
||||||
msix_version: 7.0.0.0
|
msix_version: 8.0.0.0
|
||||||
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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
dart compile exe ./lib/watch.dart --output ./assets/browse/watch.exe
|
||||||
flutter_distributor package --platform windows --targets exe
|
flutter_distributor package --platform windows --targets exe
|
||||||
flutter pub run msix:create
|
flutter pub run msix:create
|
||||||
dart compile exe ./lib/cli.dart --output ./dist/cli/reboot.exe
|
dart compile exe ./lib/cli.dart --output ./dist/cli/reboot.exe
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
#include <system_theme/system_theme_plugin.h>
|
#include <system_theme/system_theme_plugin.h>
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
app_links
|
||||||
bitsdojo_window_windows
|
bitsdojo_window_windows
|
||||||
screen_retriever
|
screen_retriever
|
||||||
system_theme
|
system_theme
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ add_executable(${BINARY_NAME} WIN32
|
|||||||
# that need different build settings.
|
# that need different build settings.
|
||||||
apply_standard_settings(${BINARY_NAME})
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the build version.
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||||
|
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||||
|
|
||||||
# Disable Windows macros that collide with C++ standard library functions.
|
# Disable Windows macros that collide with C++ standard library functions.
|
||||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||||
|
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
|
|||||||
// Version
|
// Version
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifdef FLUTTER_BUILD_NUMBER
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
#else
|
#else
|
||||||
#define VERSION_AS_NUMBER 1,0,0
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef FLUTTER_BUILD_NAME
|
#if defined(FLUTTER_VERSION)
|
||||||
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
#else
|
#else
|
||||||
#define VERSION_AS_STRING "1.0.0"
|
#define VERSION_AS_STRING "1.0.0"
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user