mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-14 11:39:17 +01:00
Reboot v3
This commit is contained in:
44
lib/src/ui/widget/home/build_selector.dart
Normal file
44
lib/src/ui/widget/home/build_selector.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
|
||||
class BuildSelector extends StatefulWidget {
|
||||
|
||||
const BuildSelector({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BuildSelector> createState() => _BuildSelectorState();
|
||||
}
|
||||
|
||||
class _BuildSelectorState extends State<BuildSelector> {
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoLabel(
|
||||
label: "Build",
|
||||
child: ComboBox<FortniteBuild>(
|
||||
placeholder: const Text('Select a fortnite build'),
|
||||
isExpanded: true,
|
||||
items: _createItems(),
|
||||
value: _buildController.selectedBuild,
|
||||
onChanged: (value) =>
|
||||
value == null ? {} : setState(() => _buildController.selectedBuild = value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
List<ComboBoxItem<FortniteBuild>> _createItems() {
|
||||
return _buildController.builds!
|
||||
.map((element) => _createItem(element))
|
||||
.toList();
|
||||
}
|
||||
|
||||
ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) {
|
||||
return ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
}
|
||||
}
|
||||
407
lib/src/ui/widget/home/launch_button.dart
Normal file
407
lib/src/ui/widget/home/launch_button.dart
Normal file
@@ -0,0 +1,407 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
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/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/game_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/injector.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:reboot_launcher/src/../main.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||
|
||||
import '../../../util/process.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
final bool host;
|
||||
|
||||
const LaunchButton({Key? key, required this.host}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LaunchButton> createState() => _LaunchButtonState();
|
||||
}
|
||||
|
||||
class _LaunchButtonState extends State<LaunchButton> {
|
||||
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
||||
final List<String> _corruptedBuildErrors = [
|
||||
"when 0 bytes remain",
|
||||
"Pak chunk signature verification failed!"
|
||||
];
|
||||
final List<String> _errorStrings = [
|
||||
"port 3551 failed: Connection refused",
|
||||
"Unable to login to Fortnite servers",
|
||||
"HTTP 400 response from ",
|
||||
"Network failure when attempting to check platform restrictions",
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final File _logFile = File("${assetsDirectory.path}\\logs\\game.log");
|
||||
bool _fail = false;
|
||||
Future? _executor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(() => SizedBox(
|
||||
height: 48,
|
||||
child: Button(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_hasStarted ? _stopMessage : _startMessage
|
||||
),
|
||||
),
|
||||
onPressed: () => _executor = _start()
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
bool get _hasStarted => widget.host ? _hostingController.started() : _gameController.started();
|
||||
|
||||
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started;
|
||||
|
||||
String get _startMessage => widget.host ? "Start hosting" : "Launch fortnite";
|
||||
|
||||
String get _stopMessage => widget.host ? "Stop hosting" : "Close fortnite";
|
||||
|
||||
Future<void> _start() async {
|
||||
if (_hasStarted) {
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
_setStarted(widget.host, true);
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
if(_serverController.type() != ServerType.local){
|
||||
showMessage("Missing username");
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("No username: expecting self sign in");
|
||||
}
|
||||
|
||||
if (_gameController.selectedVersion == null) {
|
||||
showMessage("No version is selected");
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var element in Injectable.values) {
|
||||
if(await _getDllPath(element, widget.host) == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_fail = false;
|
||||
var version = _gameController.selectedVersion!;
|
||||
var gamePath = version.executable?.path;
|
||||
if(gamePath == null){
|
||||
showMissingBuildError(version);
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = _serverController.started() || await _serverController.toggle(true);
|
||||
if(!result){
|
||||
_onStop(widget.host);
|
||||
return;
|
||||
}
|
||||
|
||||
await compute(patchMatchmaking, version.executable!);
|
||||
await compute(patchHeadless, version.executable!);
|
||||
|
||||
var automaticallyStartedServer = await _startMatchMakingServer();
|
||||
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
|
||||
|
||||
if(widget.host){
|
||||
await _showServerLaunchingWarning();
|
||||
}
|
||||
} catch (exception, stacktrace) {
|
||||
_closeDialogIfOpen(false);
|
||||
showCorruptedBuildError(widget.host, exception, stacktrace);
|
||||
_onStop(widget.host);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startGameProcesses(FortniteVersion version, bool host, bool hasChildServer) async {
|
||||
_setStarted(host, true);
|
||||
var launcherProcess = await _createLauncherProcess(version);
|
||||
var eacProcess = await _createEacProcess(version);
|
||||
var gameProcess = await _createGameProcess(version.executable!.path, host);
|
||||
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
|
||||
if(host){
|
||||
_hostingController.instance = instance;
|
||||
}else{
|
||||
_gameController.instance = instance;
|
||||
}
|
||||
_injectOrShowError(Injectable.sslBypass, host);
|
||||
}
|
||||
|
||||
Future<bool> _startMatchMakingServer() async {
|
||||
if(widget.host){
|
||||
return false;
|
||||
}
|
||||
|
||||
var matchmakingIp = _settingsController.matchmakingIp.text;
|
||||
if(!matchmakingIp.contains("127.0.0.1") && !matchmakingIp.contains("localhost")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var version = _gameController.selectedVersion!;
|
||||
await _startGameProcesses(version, true, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<Process> _createGameProcess(String gamePath, bool host) async {
|
||||
var gameArgs = createRebootArgs(_safeUsername, host, _gameController.customLaunchArgs.text);
|
||||
var gameProcess = await Process.start(gamePath, gameArgs);
|
||||
gameProcess
|
||||
..exitCode.then((_) => _onEnd())
|
||||
..outLines.forEach((line) => _onGameOutput(line, host))
|
||||
..errLines.forEach((line) => _onGameOutput(line, host));
|
||||
return gameProcess;
|
||||
}
|
||||
|
||||
String get _safeUsername {
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
return kDefaultPlayerName;
|
||||
}
|
||||
|
||||
var username = _gameController.username.text.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
|
||||
if(username.isEmpty){
|
||||
return kDefaultPlayerName;
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
|
||||
var launcherFile = version.launcher;
|
||||
if (launcherFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var launcherProcess = await Process.start(launcherFile.path, []);
|
||||
suspend(launcherProcess.pid);
|
||||
return launcherProcess;
|
||||
}
|
||||
|
||||
Future<Process?> _createEacProcess(FortniteVersion version) async {
|
||||
var eacFile = version.eacExecutable;
|
||||
if (eacFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var eacProcess = await Process.start(eacFile.path, []);
|
||||
suspend(eacProcess.pid);
|
||||
return eacProcess;
|
||||
}
|
||||
|
||||
void _onEnd() {
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_closeDialogIfOpen(false);
|
||||
_onStop(widget.host);
|
||||
}
|
||||
|
||||
void _closeDialogIfOpen(bool success) {
|
||||
var route = ModalRoute.of(appKey.currentContext!);
|
||||
if(route == null || route.isCurrent){
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(appKey.currentContext!).pop(success);
|
||||
}
|
||||
|
||||
Future<void> _showServerLaunchingWarning() async {
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => ProgressDialog(
|
||||
text: "Launching headless server...",
|
||||
onStop: () =>_onEnd()
|
||||
)
|
||||
) ?? false;
|
||||
|
||||
if(result){
|
||||
return;
|
||||
}
|
||||
|
||||
_onStop(widget.host);
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool host) {
|
||||
_logFile.createSync(recursive: true);
|
||||
_logFile.writeAsString("$line\n", mode: FileMode.append);
|
||||
if (line.contains(_shutdownLine)) {
|
||||
_onStop(host);
|
||||
return;
|
||||
}
|
||||
|
||||
if(_corruptedBuildErrors.any((element) => line.contains(element))){
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
showCorruptedBuildError(host);
|
||||
_onStop(host);
|
||||
return;
|
||||
}
|
||||
|
||||
if(_errorStrings.any((element) => line.contains(element))){
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
_showTokenError(host);
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Region ")){
|
||||
if(!host){
|
||||
_injectOrShowError(Injectable.console, host);
|
||||
}else {
|
||||
_injectOrShowError(Injectable.reboot, host)
|
||||
.then((value) => _closeDialogIfOpen(true));
|
||||
}
|
||||
|
||||
_injectOrShowError(Injectable.memoryFix, host);
|
||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
||||
instance?.tokenError = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showTokenError(bool host) async {
|
||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
||||
if(_serverController.type() != ServerType.embedded) {
|
||||
showTokenErrorUnfixable();
|
||||
instance?.tokenError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenError = instance?.tokenError;
|
||||
instance?.tokenError = true;
|
||||
await _serverController.restart(true);
|
||||
if (tokenError == true) {
|
||||
showTokenErrorCouldNotFix();
|
||||
return;
|
||||
}
|
||||
|
||||
showTokenErrorFixable();
|
||||
_onStop(host);
|
||||
_start();
|
||||
}
|
||||
|
||||
void _onStop(bool host) async {
|
||||
if(_executor != null){
|
||||
await _executor;
|
||||
}
|
||||
|
||||
var instance = host ? _hostingController.instance : _gameController.instance;
|
||||
if(instance != null){
|
||||
if(instance.hasChildServer){
|
||||
_onStop(true);
|
||||
}
|
||||
|
||||
instance.kill();
|
||||
if(host){
|
||||
_hostingController.instance = null;
|
||||
}else {
|
||||
_gameController.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
_setStarted(host, false);
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
|
||||
var instance = hosting ? _hostingController.instance : _gameController.instance;
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var gameProcess = instance.gameProcess;
|
||||
var dllPath = await _getDllPath(injectable, hosting);
|
||||
if(dllPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await injectDll(gameProcess.pid, dllPath.path);
|
||||
} catch (exception) {
|
||||
showMessage("Cannot inject $injectable.dll: $exception");
|
||||
_onStop(hosting);
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllPath(Injectable injectable, bool hosting) async {
|
||||
Future<File> getPath(Injectable injectable) async {
|
||||
switch(injectable){
|
||||
case Injectable.reboot:
|
||||
return File(_settingsController.rebootDll.text);
|
||||
case Injectable.console:
|
||||
return File(_settingsController.consoleDll.text);
|
||||
case Injectable.sslBypass:
|
||||
return File(_settingsController.authDll.text);
|
||||
case Injectable.memoryFix:
|
||||
return File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
|
||||
}
|
||||
}
|
||||
|
||||
var dllPath = await getPath(injectable);
|
||||
if(dllPath.existsSync()) {
|
||||
return dllPath;
|
||||
}
|
||||
|
||||
_onDllFail(dllPath, hosting);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onDllFail(File dllPath, bool hosting) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showMissingDllError(path.basename(dllPath.path));
|
||||
_onStop(hosting);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum Injectable {
|
||||
console,
|
||||
sslBypass,
|
||||
reboot,
|
||||
memoryFix
|
||||
}
|
||||
33
lib/src/ui/widget/home/version_name_input.dart
Normal file
33
lib/src/ui/widget/home/version_name_input.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
|
||||
class VersionNameInput extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController controller;
|
||||
|
||||
VersionNameInput({Key? key, required this.controller}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormBox(
|
||||
header: "Name",
|
||||
placeholder: "Type the version's name",
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
validator: _validate,
|
||||
);
|
||||
}
|
||||
|
||||
String? _validate(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return 'Empty version name';
|
||||
}
|
||||
|
||||
if (_gameController.versions.value.any((element) => element.name == text)) {
|
||||
return 'This version already exists';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
271
lib/src/ui/widget/home/version_selector.dart
Normal file
271
lib/src/ui/widget/home/version_selector.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/util/checks.dart';
|
||||
import '../shared/file_selector.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
const VersionSelector({Key? key}) : super(key: key);
|
||||
|
||||
static void openDownloadDialog(BuildContext context) async {
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => const AddServerVersion()
|
||||
);
|
||||
}
|
||||
|
||||
static void openAddDialog(BuildContext context) async {
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AddLocalVersion());
|
||||
}
|
||||
|
||||
@override
|
||||
State<VersionSelector> createState() => _VersionSelectorState();
|
||||
}
|
||||
|
||||
class _VersionSelectorState extends State<VersionSelector> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final CheckboxController _deleteFilesController = CheckboxController();
|
||||
final FlyoutController _flyoutController = FlyoutController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => _createOptionsMenu(
|
||||
version: _gameController.selectedVersion,
|
||||
close: false,
|
||||
child: FlyoutTarget(
|
||||
controller: _flyoutController,
|
||||
child: DropDownButton(
|
||||
leading: Text(_gameController.selectedVersion?.name ?? "Select a version"),
|
||||
items: _createSelectorItems(context)
|
||||
),
|
||||
)
|
||||
));
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) => _gameController.hasNoVersions ? [_createDefaultVersionItem()]
|
||||
: _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
|
||||
MenuFlyoutItem _createDefaultVersionItem() => MenuFlyoutItem(
|
||||
text: const Text("Please create or download a version"),
|
||||
onPressed: () {}
|
||||
);
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
|
||||
text: _createOptionsMenu(
|
||||
version: version,
|
||||
close: true,
|
||||
child: Text(version.name),
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version
|
||||
);
|
||||
|
||||
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(version == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _flyoutController.showFlyout<ContextualOption?>(
|
||||
builder: (context) => MenuFlyout(
|
||||
items: ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.toList()
|
||||
)
|
||||
);
|
||||
_handleResult(result, version, close);
|
||||
},
|
||||
child: child
|
||||
);
|
||||
|
||||
void _handleResult(ContextualOption? result, FortniteVersion version, bool close) async {
|
||||
switch (result) {
|
||||
case ContextualOption.openExplorer:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
launchUrl(version.location.uri)
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
break;
|
||||
case ContextualOption.modify:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
await _openRenameDialog(context, version);
|
||||
break;
|
||||
case ContextualOption.delete:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _openDeleteDialog(context, version) ?? false;
|
||||
if(!mounted || !result){
|
||||
return;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
_gameController.removeVersion(version);
|
||||
if (_gameController.selectedVersion?.name == version.name || _gameController.hasNoVersions) {
|
||||
_gameController.selectedVersion = null;
|
||||
}
|
||||
|
||||
if (_deleteFilesController.value && await version.location.exists()) {
|
||||
version.location.delete(recursive: true);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createOption(BuildContext context, ContextualOption entry) {
|
||||
return MenuFlyoutItem(
|
||||
text: Text(entry.name),
|
||||
onPressed: () => Navigator.of(context).pop(entry)
|
||||
);
|
||||
}
|
||||
|
||||
bool _onExplorerError() {
|
||||
showSnackbar(
|
||||
context,
|
||||
const Snackbar(
|
||||
content: Text("This version doesn't exist on the local machine", textAlign: TextAlign.center),
|
||||
extended: true
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("Are you sure you want to delete this version?")),
|
||||
|
||||
const SizedBox(height: 12.0),
|
||||
|
||||
SmartCheckBox(
|
||||
controller: _deleteFilesController,
|
||||
content: const Text("Delete version files from disk")
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Keep'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Delete'),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
|
||||
var nameController = TextEditingController(text: version.name);
|
||||
var pathController = TextEditingController(text: version.location.path);
|
||||
return showDialog<String?>(
|
||||
context: context,
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormBox(
|
||||
controller: nameController,
|
||||
header: "Name",
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkChangeVersion(text)
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
placeholder: "Type the new game folder",
|
||||
windowTitle: "Select game folder",
|
||||
controller: pathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Save",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.updateVersion(version, (version) {
|
||||
version.name = nameController.text;
|
||||
version.location = Directory(pathController.text);
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ContextualOption {
|
||||
openExplorer,
|
||||
modify,
|
||||
delete;
|
||||
|
||||
String get name {
|
||||
return this == ContextualOption.openExplorer ? "Open in explorer"
|
||||
: this == ContextualOption.modify ? "Modify"
|
||||
: "Delete";
|
||||
}
|
||||
}
|
||||
27
lib/src/ui/widget/os/window_border.dart
Normal file
27
lib/src/ui/widget/os/window_border.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
|
||||
class WindowBorder extends StatelessWidget {
|
||||
const WindowBorder({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 1 / appWindow.scaleFactor
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: SystemTheme.accentColor.accent,
|
||||
width: appBarSize.toDouble()
|
||||
)
|
||||
)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
51
lib/src/ui/widget/os/window_buttons.dart
Normal file
51
lib/src/ui/widget/os/window_buttons.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
|
||||
class WindowTitleBar extends StatelessWidget {
|
||||
final bool focused;
|
||||
|
||||
const WindowTitleBar({Key? key, required this.focused}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var lightMode = FluentTheme.of(context).brightness.isLight;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
MinimizeWindowButton(
|
||||
colors: WindowButtonColors(
|
||||
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
|
||||
iconMouseDown: lightMode ? Colors.black : Colors.white,
|
||||
iconMouseOver: lightMode ? Colors.black : Colors.white,
|
||||
normal: Colors.transparent,
|
||||
mouseOver: _color,
|
||||
mouseDown: _color.withOpacity(0.7)),
|
||||
),
|
||||
MaximizeWindowButton(
|
||||
colors: WindowButtonColors(
|
||||
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
|
||||
iconMouseDown: lightMode ? Colors.black : Colors.white,
|
||||
iconMouseOver: lightMode ? Colors.black : Colors.white,
|
||||
normal: Colors.transparent,
|
||||
mouseOver: _color,
|
||||
mouseDown: _color.withOpacity(0.7)),
|
||||
),
|
||||
CloseWindowButton(
|
||||
colors: WindowButtonColors(
|
||||
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
|
||||
iconMouseDown: lightMode ? Colors.black : Colors.white,
|
||||
iconMouseOver: lightMode ? Colors.black : Colors.white,
|
||||
normal: Colors.transparent,
|
||||
mouseOver: Colors.red,
|
||||
mouseDown: Colors.red.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color get _color =>
|
||||
SystemTheme.accentColor.accent;
|
||||
}
|
||||
46
lib/src/ui/widget/server/server_button.dart
Normal file
46
lib/src/ui/widget/server/server_button.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
|
||||
class ServerButton extends StatefulWidget {
|
||||
const ServerButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ServerButton> createState() => _ServerButtonState();
|
||||
}
|
||||
|
||||
class _ServerButtonState extends State<ServerButton> {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Align(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(() => SizedBox(
|
||||
height: 48,
|
||||
child: Button(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(_buttonText),
|
||||
),
|
||||
onPressed: () => _serverController.toggle(false)
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
String get _buttonText {
|
||||
if(_serverController.type.value == ServerType.local){
|
||||
return "Check backend";
|
||||
}
|
||||
|
||||
if(_serverController.started.value){
|
||||
return "Stop backend";
|
||||
}
|
||||
|
||||
return "Start backend";
|
||||
}
|
||||
}
|
||||
34
lib/src/ui/widget/server/server_type_selector.dart
Normal file
34
lib/src/ui/widget/server/server_type_selector.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
|
||||
class ServerTypeSelector extends StatelessWidget {
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
ServerTypeSelector({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropDownButton(
|
||||
leading: Text(_serverController.type.value.name),
|
||||
items: ServerType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createItem(ServerType type) {
|
||||
return MenuFlyoutItem(
|
||||
text: Tooltip(
|
||||
message: type.message,
|
||||
child: Text(type.name)
|
||||
),
|
||||
onPressed: () async {
|
||||
await _serverController.stop();
|
||||
_serverController.type(type);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
93
lib/src/ui/widget/shared/file_selector.dart
Normal file
93
lib/src/ui/widget/shared/file_selector.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/util/selector.dart';
|
||||
|
||||
class FileSelector extends StatefulWidget {
|
||||
final String placeholder;
|
||||
final String windowTitle;
|
||||
final bool allowNavigator;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?) validator;
|
||||
final AutovalidateMode? validatorMode;
|
||||
final String? extension;
|
||||
final String? label;
|
||||
final bool folder;
|
||||
|
||||
const FileSelector(
|
||||
{required this.placeholder,
|
||||
required this.windowTitle,
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.folder,
|
||||
this.label,
|
||||
this.extension,
|
||||
this.validatorMode,
|
||||
this.allowNavigator = true,
|
||||
Key? key})
|
||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<FileSelector> createState() => _FileSelectorState();
|
||||
}
|
||||
|
||||
class _FileSelectorState extends State<FileSelector> {
|
||||
bool _selecting = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.label != null ? InfoLabel(
|
||||
label: widget.label!,
|
||||
child: _buildBody,
|
||||
) : _buildBody;
|
||||
}
|
||||
|
||||
Widget get _buildBody => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
controller: widget.controller,
|
||||
placeholder: widget.placeholder,
|
||||
validator: widget.validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
||||
)
|
||||
),
|
||||
if (widget.allowNavigator)
|
||||
const SizedBox(width: 16.0),
|
||||
if (widget.allowNavigator)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
void _onPressed() {
|
||||
if(_selecting){
|
||||
showMessage("Folder selector is already opened");
|
||||
return;
|
||||
}
|
||||
|
||||
_selecting = true;
|
||||
if(widget.folder) {
|
||||
compute(openFolderPicker, widget.windowTitle)
|
||||
.then((value) => widget.controller.text = value ?? widget.controller.text)
|
||||
.then((_) => _selecting = false);
|
||||
return;
|
||||
}
|
||||
|
||||
compute(openFilePicker, widget.extension!)
|
||||
.then((value) => widget.controller.text = value ?? widget.controller.text)
|
||||
.then((_) => _selecting = false);
|
||||
}
|
||||
}
|
||||
16
lib/src/ui/widget/shared/fluent_card.dart
Normal file
16
lib/src/ui/widget/shared/fluent_card.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class FluentCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
const FluentCard({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Mica(
|
||||
elevation: 1,
|
||||
child: Card(
|
||||
backgroundColor: FluentTheme.of(context).menuColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)),
|
||||
child: child
|
||||
)
|
||||
);
|
||||
}
|
||||
90
lib/src/ui/widget/shared/setting_tile.dart
Normal file
90
lib/src/ui/widget/shared/setting_tile.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/ui/widget/shared/fluent_card.dart';
|
||||
|
||||
class SettingTile extends StatefulWidget {
|
||||
static const double kDefaultContentWidth = 200.0;
|
||||
static const double kDefaultSpacing = 8.0;
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget? content;
|
||||
final double? contentWidth;
|
||||
final List<Widget>? expandedContent;
|
||||
final double expandedContentSpacing;
|
||||
final bool isChild;
|
||||
|
||||
const SettingTile(
|
||||
{Key? key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.content,
|
||||
this.contentWidth = kDefaultContentWidth,
|
||||
this.expandedContentSpacing = kDefaultSpacing,
|
||||
this.expandedContent,
|
||||
this.isChild = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<SettingTile> createState() => _SettingTileState();
|
||||
}
|
||||
|
||||
class _SettingTileState extends State<SettingTile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if(widget.expandedContent == null){
|
||||
return _contentCard;
|
||||
}
|
||||
|
||||
return Mica(
|
||||
elevation: 1,
|
||||
child: Expander(
|
||||
initiallyExpanded: true,
|
||||
contentBackgroundColor: FluentTheme.of(context).menuColor,
|
||||
headerShape: (open) => const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
|
||||
),
|
||||
header: _header,
|
||||
headerHeight: 72,
|
||||
trailing: _trailing,
|
||||
content: _content
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _content {
|
||||
var contents = widget.expandedContent!;
|
||||
var items = List.generate(contents.length * 2, (index) => index % 2 == 0 ? contents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing));
|
||||
return Column(
|
||||
children: items
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _trailing => SizedBox(
|
||||
width: widget.contentWidth,
|
||||
child: widget.content
|
||||
);
|
||||
|
||||
Widget get _header => ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle)
|
||||
);
|
||||
|
||||
Widget get _contentCard {
|
||||
if (widget.isChild) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: _contentCardBody
|
||||
);
|
||||
}
|
||||
|
||||
return FluentCard(
|
||||
child: _contentCardBody,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _contentCardBody => ListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle),
|
||||
trailing: _trailing
|
||||
);
|
||||
}
|
||||
27
lib/src/ui/widget/shared/smart_check_box.dart
Normal file
27
lib/src/ui/widget/shared/smart_check_box.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class SmartCheckBox extends StatefulWidget {
|
||||
final CheckboxController controller;
|
||||
final Widget? content;
|
||||
const SmartCheckBox({Key? key, required this.controller, this.content}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SmartCheckBox> createState() => _SmartCheckBoxState();
|
||||
}
|
||||
|
||||
class _SmartCheckBoxState extends State<SmartCheckBox> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Checkbox(
|
||||
checked: widget.controller.value,
|
||||
onChanged: (checked) => setState(() => widget.controller.value = checked ?? false),
|
||||
content: widget.content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxController {
|
||||
bool value;
|
||||
|
||||
CheckboxController({this.value = false});
|
||||
}
|
||||
41
lib/src/ui/widget/shared/smart_input.dart
Normal file
41
lib/src/ui/widget/shared/smart_input.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class SmartInput extends StatelessWidget {
|
||||
final String? label;
|
||||
final String placeholder;
|
||||
final TextEditingController controller;
|
||||
final TextInputType type;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
final bool readOnly;
|
||||
final AutovalidateMode validatorMode;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
const SmartInput(
|
||||
{Key? key,
|
||||
required this.placeholder,
|
||||
required this.controller,
|
||||
this.label,
|
||||
this.onTap,
|
||||
this.enabled = true,
|
||||
this.readOnly = false,
|
||||
this.type = TextInputType.text,
|
||||
this.validatorMode = AutovalidateMode.disabled,
|
||||
this.validator})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormBox(
|
||||
enabled: enabled,
|
||||
controller: controller,
|
||||
header: label,
|
||||
keyboardType: type,
|
||||
placeholder: placeholder,
|
||||
onTap: onTap,
|
||||
readOnly: readOnly,
|
||||
autovalidateMode: validatorMode,
|
||||
validator: validator
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user