mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-14 03:32:23 +01:00
Reboot v3
This commit is contained in:
78
lib/src/ui/dialog/add_local_version.dart
Normal file
78
lib/src/ui/dialog/add_local_version.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
import '../widget/shared/smart_check_box.dart';
|
||||
import 'dialog.dart';
|
||||
import 'dialog_button.dart';
|
||||
|
||||
class AddLocalVersion extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _gamePathController = TextEditingController();
|
||||
|
||||
AddLocalVersion({Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("Local builds are not guaranteed to work"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
TextFormBox(
|
||||
controller: _nameController,
|
||||
header: "Name",
|
||||
placeholder: "Type the version's name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
),
|
||||
|
||||
FileSelector(
|
||||
label: "Path",
|
||||
placeholder: "Type the game folder",
|
||||
windowTitle: "Select game folder",
|
||||
controller: _gamePathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Save",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_gamePathController.text)
|
||||
));
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
328
lib/src/ui/dialog/add_server_version.dart
Normal file
328
lib/src/ui/dialog/add_server_version.dart
Normal file
@@ -0,0 +1,328 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/util/error.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/build.dart';
|
||||
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
import '../controller/build_controller.dart';
|
||||
import '../widget/home/build_selector.dart';
|
||||
import '../widget/home/version_name_input.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
import 'dialog.dart';
|
||||
import 'dialog_button.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
const AddServerVersion({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddServerVersion> createState() => _AddServerVersionState();
|
||||
}
|
||||
|
||||
class _AddServerVersionState extends State<AddServerVersion> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
DownloadStatus _status = DownloadStatus.form;
|
||||
String _timeLeft = "00:00:00";
|
||||
double _downloadProgress = 0;
|
||||
CancelableOperation? _manifestDownloadProcess;
|
||||
CancelableOperation? _driveDownloadOperation;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_buildController.removeOnBuildChangedListener();
|
||||
_onDisposed();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onDisposed() {
|
||||
if (_status != DownloadStatus.downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_manifestDownloadProcess != null) {
|
||||
_manifestDownloadProcess?.cancel();
|
||||
_buildController.cancelledDownload(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_driveDownloadOperation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_driveDownloadOperation!.cancel();
|
||||
_buildController.cancelledDownload(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch(_status){
|
||||
case DownloadStatus.form:
|
||||
return _createFormDialog();
|
||||
case DownloadStatus.downloading:
|
||||
return GenericDialog(
|
||||
header: _createDownloadBody(),
|
||||
buttons: _createCloseButton()
|
||||
);
|
||||
case DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _createExtractingBody(),
|
||||
buttons: _createCloseButton()
|
||||
);
|
||||
case DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception("unknown error"),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot download version: $exception"
|
||||
);
|
||||
case DownloadStatus.done:
|
||||
return const InfoDialog(
|
||||
text: "The download was completed successfully!",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<DialogButton> _createFormButtons() {
|
||||
return [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: "Download",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
setState(() => _status = DownloadStatus.downloading);
|
||||
var future = downloadArchiveBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
Directory(_pathController.text),
|
||||
_onDownloadProgress,
|
||||
_onUnrar
|
||||
);
|
||||
future.then((value) => _onDownloadComplete());
|
||||
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUnrar() {
|
||||
setState(() => _status = DownloadStatus.extracting);
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.done;
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
print("Error");
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.error;
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDownloadProgress(double progress, String timeLeft) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.downloading;
|
||||
_timeLeft = timeLeft;
|
||||
_downloadProgress = progress;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _createDownloadBody() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Downloading...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _createExtractingBody() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Extracting...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar()
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget _createFormDialog() {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) =>
|
||||
_onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return ProgressDialog(
|
||||
text: "Fetching builds and disks...",
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return FormDialog(
|
||||
content: _createFormBody(),
|
||||
buttons: _createFormButtons()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createFormBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const BuildSelector(),
|
||||
const SizedBox(height: 20.0),
|
||||
VersionNameInput(controller: _nameController),
|
||||
FileSelector(
|
||||
label: "Path",
|
||||
placeholder: "Type the download destination",
|
||||
windowTitle: "Select download destination",
|
||||
controller: _pathController,
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<DialogButton> _createCloseButton() {
|
||||
return [
|
||||
DialogButton(
|
||||
text: "Stop",
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_diskSpace.disks.isEmpty){
|
||||
return;
|
||||
}
|
||||
|
||||
await _fetchFuture;
|
||||
var bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
|
||||
"${_buildController.selectedBuild.version.toString()}";
|
||||
_nameController.text = _buildController.selectedBuild.version.toString();
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||
260
lib/src/ui/dialog/dialog.dart
Normal file
260
lib/src/ui/dialog/dialog.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
|
||||
import 'dialog_button.dart';
|
||||
|
||||
abstract class AbstractDialog extends StatelessWidget {
|
||||
const AbstractDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context);
|
||||
}
|
||||
|
||||
class GenericDialog extends AbstractDialog {
|
||||
final Widget header;
|
||||
final List<DialogButton> buttons;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const GenericDialog({super.key, required this.header, required this.buttons, this.padding});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
MoveWindow(
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
|
||||
ContentDialog(
|
||||
style: ContentDialogThemeData(
|
||||
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
||||
),
|
||||
content: header,
|
||||
actions: buttons
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FormDialog extends AbstractDialog {
|
||||
final Widget content;
|
||||
final List<DialogButton> buttons;
|
||||
|
||||
const FormDialog({super.key, required this.content, required this.buttons});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList();
|
||||
return GenericDialog(
|
||||
header: content,
|
||||
buttons: parsed
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
DialogButton _createFormButton(DialogButton entry, BuildContext context) {
|
||||
if (entry.type != ButtonType.primary) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
return DialogButton(
|
||||
text: entry.text,
|
||||
type: entry.type,
|
||||
onTap: () {
|
||||
if(!Form.of(context)!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.onTap?.call();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InfoDialog extends AbstractDialog {
|
||||
final String text;
|
||||
final List<DialogButton>? buttons;
|
||||
|
||||
const InfoDialog({required this.text, this.buttons, super.key});
|
||||
|
||||
InfoDialog.ofOnly({required this.text, required DialogButton button, super.key})
|
||||
: buttons = [button];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GenericDialog(
|
||||
header: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(text, textAlign: TextAlign.center)
|
||||
),
|
||||
buttons: buttons ?? [_createDefaultButton()],
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 15.0)
|
||||
);
|
||||
}
|
||||
|
||||
DialogButton _createDefaultButton() {
|
||||
return DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.only
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProgressDialog extends AbstractDialog {
|
||||
final String text;
|
||||
final Function()? onStop;
|
||||
|
||||
const ProgressDialog({required this.text, this.onStop, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GenericDialog(
|
||||
header: InfoLabel(
|
||||
label: text,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()
|
||||
),
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.only,
|
||||
onTap: onStop
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FutureBuilderDialog extends AbstractDialog {
|
||||
final Future future;
|
||||
final String loadingMessage;
|
||||
final Widget successfulBody;
|
||||
final Widget unsuccessfulBody;
|
||||
final Function(Object) errorMessageBuilder;
|
||||
final Function()? onError;
|
||||
final bool closeAutomatically;
|
||||
|
||||
const FutureBuilderDialog(
|
||||
{super.key,
|
||||
required this.future,
|
||||
required this.loadingMessage,
|
||||
required this.successfulBody,
|
||||
required this.unsuccessfulBody,
|
||||
required this.errorMessageBuilder,
|
||||
this.onError,
|
||||
this.closeAutomatically = false});
|
||||
|
||||
static Container ofMessage(String message) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
message,
|
||||
textAlign: TextAlign.center
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, snapshot) => GenericDialog(
|
||||
header: _createBody(context, snapshot),
|
||||
buttons: [_createButton(context, snapshot)]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createBody(BuildContext context, AsyncSnapshot snapshot){
|
||||
if (snapshot.hasError) {
|
||||
onError?.call();
|
||||
return ofMessage(errorMessageBuilder(snapshot.error!));
|
||||
}
|
||||
|
||||
if(snapshot.connectionState == ConnectionState.done && (snapshot.data == null || (snapshot.data is bool && !snapshot.data))){
|
||||
return unsuccessfulBody;
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return _createLoadingBody();
|
||||
}
|
||||
|
||||
if(closeAutomatically){
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => Navigator.of(context).pop(true));
|
||||
return _createLoadingBody();
|
||||
}
|
||||
|
||||
return successfulBody;
|
||||
}
|
||||
|
||||
InfoLabel _createLoadingBody() {
|
||||
return InfoLabel(
|
||||
label: loadingMessage,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
}
|
||||
|
||||
DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot){
|
||||
return DialogButton(
|
||||
text: snapshot.hasData
|
||||
|| snapshot.hasError
|
||||
|| (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? "Close" : "Stop",
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorDialog extends AbstractDialog {
|
||||
final Object exception;
|
||||
final StackTrace? stackTrace;
|
||||
final Function(Object) errorMessageBuilder;
|
||||
|
||||
const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace});
|
||||
|
||||
static DialogButton createCopyErrorButton({required Object error, required StackTrace? stackTrace, required Function() onClick, ButtonType type = ButtonType.primary}) => DialogButton(
|
||||
text: "Copy error",
|
||||
type: type,
|
||||
onTap: () async {
|
||||
FlutterClipboard.controlC("An error occurred: $error\nStacktrace:\n $stackTrace");
|
||||
showMessage("Copied error to clipboard");
|
||||
onClick();
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoDialog(
|
||||
text: errorMessageBuilder(exception),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: stackTrace == null ? ButtonType.only : ButtonType.secondary
|
||||
),
|
||||
|
||||
if(stackTrace != null)
|
||||
createCopyErrorButton(
|
||||
error: exception,
|
||||
stackTrace: stackTrace,
|
||||
onClick: () => Navigator.pop(context)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/src/ui/dialog/dialog_button.dart
Normal file
64
lib/src/ui/dialog/dialog_button.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
required this.type})
|
||||
: assert(type != ButtonType.primary || onTap != null,
|
||||
"OnTap handler cannot be null for primary buttons"),
|
||||
assert(type != ButtonType.primary || text != null,
|
||||
"Text cannot be null for primary buttons"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<DialogButton> createState() => _DialogButtonState();
|
||||
}
|
||||
|
||||
class _DialogButtonState extends State<DialogButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.type == ButtonType.only ? _createOnlyButton() : _createButton();
|
||||
}
|
||||
|
||||
SizedBox _createOnlyButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: _createButton()
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createButton() {
|
||||
return widget.type == ButtonType.primary ? _createPrimaryActionButton()
|
||||
: _createSecondaryActionButton();
|
||||
}
|
||||
|
||||
Widget _createPrimaryActionButton() {
|
||||
return FilledButton(
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createSecondaryActionButton() {
|
||||
return Button(
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? "Close"),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDefaultSecondaryActionTap() {
|
||||
Navigator.of(context).pop(null);
|
||||
}
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
98
lib/src/ui/dialog/game_dialogs.dart
Normal file
98
lib/src/ui/dialog/game_dialogs.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import 'dialog.dart';
|
||||
|
||||
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
|
||||
"This means that you cannot currently host this version of the game. "
|
||||
"For a list of supported versions, check #info in the Discord server. "
|
||||
"If you are unsure which version works best, use build 7.40. "
|
||||
"If you are a passionate programmer you can add support by opening a PR on Github. ";
|
||||
|
||||
const String _corruptedBuildError = "The build you are currently using is corrupted. "
|
||||
"This means that some critical files are missing for the game to launch. "
|
||||
"Download the build again from the launcher or, if it's not available there, from another source. "
|
||||
"Occasionally some files might get corrupted if there isn't enough space on your drive.";
|
||||
|
||||
Future<void> showBrokenError() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "The backend server is not working correctly"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showMissingDllError(String name) async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "$name dll is not a valid dll, fix it in the settings tab"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTokenErrorFixable() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "A token error occurred. "
|
||||
"The backend server has been automatically restarted to fix the issue. "
|
||||
"The game has been restarted automatically. "
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTokenErrorCouldNotFix() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "A token error occurred. "
|
||||
"The game couldn't be recovered, open an issue on Discord."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTokenErrorUnfixable() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "A token error occurred. "
|
||||
"This issue cannot be resolved automatically as the server isn't embedded."
|
||||
"Please restart the server manually, then relaunch your game to check if the issue has been fixed. "
|
||||
"Otherwise, open an issue on Discord."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async {
|
||||
if(error == null) {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: server ? _unsupportedServerError : _corruptedBuildError
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => ErrorDialog(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => _corruptedBuildError
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showMissingBuildError(FortniteVersion version) async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "${version.location.path} no longer contains a Fortnite executable. "
|
||||
"This probably means that you deleted it or move it somewhere else."
|
||||
)
|
||||
);
|
||||
}
|
||||
318
lib/src/ui/dialog/server_dialogs.dart
Normal file
318
lib/src/ui/dialog/server_dialogs.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../util/server.dart';
|
||||
import '../controller/server_controller.dart';
|
||||
import 'dialog.dart';
|
||||
import 'dialog_button.dart';
|
||||
|
||||
extension ServerControllerDialog on ServerController {
|
||||
static Semaphore semaphore = Semaphore();
|
||||
|
||||
Future<bool> restart(bool closeLocalPromptAutomatically) async {
|
||||
await resetWinNat();
|
||||
return (!started() || await stop()) && await toggle(closeLocalPromptAutomatically);
|
||||
}
|
||||
|
||||
Future<bool> toggle(bool closeLocalPromptAutomatically) async {
|
||||
try{
|
||||
semaphore.acquire();
|
||||
if (type() == ServerType.local) {
|
||||
return _pingSelfInteractive(closeLocalPromptAutomatically);
|
||||
}
|
||||
|
||||
var result = await _toggle();
|
||||
if(!result){
|
||||
started.value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var ping = await _pingSelfInteractive(true);
|
||||
if(!ping){
|
||||
started.value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}finally{
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _toggle([ServerResultType? lastResultType]) async {
|
||||
if (started.value) {
|
||||
var result = await stop();
|
||||
if (!result) {
|
||||
started.value = true;
|
||||
_showCannotStopError();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
started.value = true;
|
||||
var conditions = await checkServerPreconditions(host.text, port.text, type.value);
|
||||
var result = conditions.type == ServerResultType.canStart ? await _startServer() : conditions;
|
||||
if(result.type == ServerResultType.alreadyStarted) {
|
||||
started.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var handled = await _handleResultType(result, lastResultType);
|
||||
if (!handled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
Future<ServerResult> _startServer() async {
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
startServer(detached());
|
||||
break;
|
||||
case ServerType.remote:
|
||||
var uriResult = await _pingRemoteInteractive();
|
||||
if(uriResult == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer
|
||||
);
|
||||
}
|
||||
|
||||
remoteServer = await startRemoteServer(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
}
|
||||
}catch(error, stackTrace){
|
||||
return ServerResult(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
type: ServerResultType.unknownError
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
type: ServerResultType.canStart
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _handleResultType(ServerResult result, ServerResultType? lastResultType) async {
|
||||
var newResultType = result.type;
|
||||
switch (newResultType) {
|
||||
case ServerResultType.missingHostError:
|
||||
_showMissingHostError();
|
||||
return false;
|
||||
case ServerResultType.missingPortError:
|
||||
_showMissingPortError();
|
||||
return false;
|
||||
case ServerResultType.illegalPortError:
|
||||
_showIllegalPortError();
|
||||
return false;
|
||||
case ServerResultType.cannotPingServer:
|
||||
return false;
|
||||
case ServerResultType.backendPortTakenError:
|
||||
if (lastResultType == ServerResultType.backendPortTakenError) {
|
||||
_showPortTakenError(3551);
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _showPortTakenDialog(3551);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await freeLawinPort();
|
||||
await stop();
|
||||
return _toggle(newResultType);
|
||||
case ServerResultType.matchmakerPortTakenError:
|
||||
if (lastResultType == ServerResultType.matchmakerPortTakenError) {
|
||||
_showPortTakenError(8080);
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _showPortTakenDialog(8080);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await freeMatchmakerPort();
|
||||
await stop();
|
||||
return _toggle(newResultType);
|
||||
case ServerResultType.unknownError:
|
||||
if(lastResultType == ServerResultType.unknownError) {
|
||||
_showUnknownError(result);
|
||||
return false;
|
||||
}
|
||||
|
||||
await resetWinNat();
|
||||
await stop();
|
||||
return _toggle(newResultType);
|
||||
case ServerResultType.alreadyStarted:
|
||||
case ServerResultType.canStart:
|
||||
return true;
|
||||
case ServerResultType.stopped:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
|
||||
try {
|
||||
Future<bool> ping() async {
|
||||
for(var i = 0; i < 3; i++){
|
||||
var result = await pingSelf(port.text);
|
||||
if(result != null){
|
||||
return true;
|
||||
}else {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var future = _waitFutureOrTime(ping());
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: future,
|
||||
loadingMessage: "Pinging ${type().id} server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server works correctly"),
|
||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
||||
errorMessageBuilder: (
|
||||
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? false;
|
||||
return result && await future;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri?> _pingRemoteInteractive() async {
|
||||
try {
|
||||
var mainFuture = ping(host.text, port.text).then((value) => value != null);
|
||||
var future = _waitFutureOrTime(mainFuture);
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: future,
|
||||
closeAutomatically: false,
|
||||
loadingMessage: "Pinging remote server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} works correctly"),
|
||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} doesn't work. Check the hostname and/or the port and try again."),
|
||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
||||
)
|
||||
) ?? false;
|
||||
return result ? await future : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showPortTakenError(int port) async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "Port $port is already in use and the associating process cannot be killed. Kill it manually and try again.",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _showPortTakenDialog(int port) async {
|
||||
return await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
InfoDialog(
|
||||
text: "Port $port is already in use, do you want to kill the associated process?",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
DialogButton(
|
||||
text: "Kill",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
)
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
void _showCannotStopError() {
|
||||
if(!started.value){
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "Cannot stop backend server"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void showUnexpectedServerError() => showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The backend server died unexpectedly",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Open log",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
if(serverLogFile.existsSync()){
|
||||
showMessage("No log is available");
|
||||
}else {
|
||||
launchUrl(serverLogFile.uri);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
|
||||
void _showIllegalPortError() => showMessage("Illegal port for backend server, use only numbers");
|
||||
|
||||
void _showMissingPortError() => showMessage("Missing port for backend server");
|
||||
|
||||
void _showMissingHostError() => showMessage("Missing the host name for backend server");
|
||||
|
||||
Future<Object?> _showUnknownError(ServerResult result) => showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: result.error ?? Exception("Unknown error"),
|
||||
stackTrace: result.stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
|
||||
)
|
||||
);
|
||||
|
||||
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) => Future.wait<bool>([resultFuture, Future.delayed(const Duration(seconds: 1)).then((value) => true)]).then((value) => value.reduce((f, s) => f && s));
|
||||
}
|
||||
13
lib/src/ui/dialog/snackbar.dart
Normal file
13
lib/src/ui/dialog/snackbar.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
|
||||
void showMessage(String text){
|
||||
showSnackbar(
|
||||
appKey.currentContext!,
|
||||
Snackbar(
|
||||
content: Text(text, textAlign: TextAlign.center),
|
||||
extended: true
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user