Reboot v3

This commit is contained in:
Alessandro Autiero
2023-06-03 18:30:50 +02:00
parent 5eafcae616
commit 30f1b0f162
44 changed files with 1041 additions and 613 deletions

BIN
assets/browse/watch.exe Normal file

Binary file not shown.

View File

@@ -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

Binary file not shown.

BIN
assets/dlls/reboot.dll Normal file

Binary file not shown.

View File

@@ -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"];

View File

@@ -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,
),
); );
} }

View File

@@ -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");
} }

View File

@@ -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);
}
} }
} }

View File

@@ -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();
} }

View File

@@ -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);
} }

View File

@@ -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;
}
} }
} }

View File

@@ -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
) )
], ],
), ),

View File

@@ -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();
} }
} }

View File

@@ -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;
} }

View File

@@ -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"
) )
); );
} }

View File

@@ -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");

View 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'),
)
);
}
),
),
)
],
),
);
}
)
);
}
}

View File

@@ -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])
), ),
]; ];

View File

@@ -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
),
),
);
} }

View File

@@ -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;
}
) )
] ]
); );

View File

@@ -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
)
),
),
);
} }

View File

@@ -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: [

View File

@@ -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
); );
} }

View File

@@ -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();
}
))
); );
} }

View File

@@ -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);
}); });

View File

@@ -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) {

View File

@@ -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

View File

@@ -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() {

View File

@@ -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
); );
}
} }

View File

@@ -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);
}

View File

@@ -70,3 +70,11 @@ String? checkMatchmaking(String? text) {
return null; return null;
} }
String? checkUpdateUrl(String? text) {
if (text == null || text.isEmpty) {
return "Empty URL";
}
return null;
}

View File

@@ -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) =>

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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
View 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
View 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}';
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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")

View File

@@ -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