Reboot v3

This commit is contained in:
Alessandro Autiero
2023-05-24 23:19:36 +02:00
parent 760e336bc0
commit eb6381912c
129 changed files with 396379 additions and 1380 deletions

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

View 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
}

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

View 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";
}
}

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

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

View 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";
}
}

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

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

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

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

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

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