mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
Release 9.2.0
This commit is contained in:
324
gui/lib/src/messenger/abstract/dialog.dart
Normal file
324
gui/lib/src/messenger/abstract/dialog.dart
Normal file
@@ -0,0 +1,324 @@
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
bool inDialog = false;
|
||||
|
||||
Future<T?> showRebootDialog<T extends Object?>({required WidgetBuilder builder, bool dismissWithEsc = true}) async {
|
||||
inDialog = true;
|
||||
pagesController.add(null);
|
||||
try {
|
||||
return await fluent.showDialog(
|
||||
context: appNavigatorKey.currentContext!,
|
||||
useRootNavigator: false,
|
||||
dismissWithEsc: dismissWithEsc,
|
||||
builder: builder
|
||||
);
|
||||
}finally {
|
||||
inDialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
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({Key? key, required this.header, required this.buttons, this.padding}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => 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({Key? key, required this.content, required this.buttons}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final 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, Key? key}) : super(key: key);
|
||||
|
||||
InfoDialog.ofOnly({required this.text, required DialogButton button, Key? key})
|
||||
: buttons = [button], super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GenericDialog(
|
||||
header: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(text, textAlign: TextAlign.center)
|
||||
),
|
||||
buttons: buttons ?? [_defaultCloseButton],
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 15.0)
|
||||
);
|
||||
}
|
||||
|
||||
DialogButton get _defaultCloseButton =>DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
type: ButtonType.only
|
||||
);
|
||||
}
|
||||
|
||||
class ProgressDialog extends AbstractDialog {
|
||||
final String text;
|
||||
final Function()? onStop;
|
||||
final bool showButton;
|
||||
|
||||
const ProgressDialog({required this.text, this.onStop, this.showButton = true, Key? key}) : super(key: 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: [
|
||||
if(showButton)
|
||||
DialogButton(
|
||||
text: translations.defaultDialogSecondaryAction,
|
||||
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(
|
||||
{Key? key,
|
||||
required this.future,
|
||||
required this.loadingMessage,
|
||||
required this.successfulBody,
|
||||
required this.unsuccessfulBody,
|
||||
required this.errorMessageBuilder,
|
||||
this.onError,
|
||||
this.closeAutomatically = false}) : super(key: key);
|
||||
|
||||
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) ? translations.defaultDialogSecondaryAction : translations.stopLoadingDialogAction,
|
||||
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({Key? key, required this.exception, required this.errorMessageBuilder, this.stackTrace}) : super(key: key);
|
||||
|
||||
static DialogButton createCopyErrorButton({required Object error, required StackTrace? stackTrace, required Function() onClick, ButtonType type = ButtonType.primary}) => DialogButton(
|
||||
text: translations.copyErrorDialogTitle,
|
||||
type: type,
|
||||
onTap: () async {
|
||||
FlutterClipboard.controlC("$error\n$stackTrace");
|
||||
showRebootInfoBar(translations.copyErrorDialogSuccess);
|
||||
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)
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DialogButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Function()? onTap;
|
||||
final ButtonType type;
|
||||
final Color? color;
|
||||
|
||||
const DialogButton(
|
||||
{Key? key,
|
||||
this.text,
|
||||
this.onTap,
|
||||
this.color,
|
||||
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) => widget.type == ButtonType.only ? _onlyButton : _button;
|
||||
|
||||
SizedBox get _onlyButton => SizedBox(
|
||||
width: double.infinity,
|
||||
child: _button
|
||||
);
|
||||
|
||||
Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton;
|
||||
|
||||
Widget get _primaryButton => Button(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
||||
),
|
||||
onPressed: widget.onTap!,
|
||||
child: Text(widget.text!),
|
||||
);
|
||||
|
||||
Widget get _secondaryButton => Button(
|
||||
style: widget.color != null ? ButtonStyle(
|
||||
backgroundColor: ButtonState.all(widget.color!)
|
||||
) : null,
|
||||
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
|
||||
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
|
||||
);
|
||||
|
||||
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
|
||||
}
|
||||
|
||||
enum ButtonType {
|
||||
primary,
|
||||
secondary,
|
||||
only
|
||||
}
|
||||
79
gui/lib/src/messenger/abstract/info_bar.dart
Normal file
79
gui/lib/src/messenger/abstract/info_bar.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
|
||||
const infoBarLongDuration = Duration(seconds: 4);
|
||||
const infoBarShortDuration = Duration(seconds: 2);
|
||||
const _height = 64.0;
|
||||
|
||||
InfoBarEntry showRebootInfoBar(dynamic text, {
|
||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
||||
bool loading = false,
|
||||
Duration? duration = infoBarShortDuration,
|
||||
void Function()? onDismissed,
|
||||
Widget? action
|
||||
}) {
|
||||
final overlay = _buildOverlay(text, action, loading, severity);
|
||||
final overlayEntry = InfoBarEntry(overlay: overlay, onDismissed: onDismissed);
|
||||
if(duration != null) {
|
||||
Future.delayed(duration)
|
||||
.then((_) => WidgetsBinding.instance.addPostFrameCallback((timeStamp) => overlayEntry.close()));
|
||||
}
|
||||
return overlayEntry;
|
||||
}
|
||||
|
||||
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: _height
|
||||
),
|
||||
child: Mica(
|
||||
elevation: 1,
|
||||
child: InfoBar(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(text is Widget)
|
||||
text,
|
||||
if(text is String)
|
||||
Text(text),
|
||||
if(action != null)
|
||||
action
|
||||
],
|
||||
),
|
||||
isLong: false,
|
||||
isIconVisible: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: loading ? const Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
bottom: 2.0,
|
||||
right: 6.0
|
||||
),
|
||||
child: ProgressBar(),
|
||||
) : const SizedBox()
|
||||
),
|
||||
severity: severity
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
class InfoBarEntry {
|
||||
final Widget overlay;
|
||||
final void Function()? onDismissed;
|
||||
|
||||
InfoBarEntry({required this.overlay, required this.onDismissed}) {
|
||||
final context = pageKey.currentContext;
|
||||
if(context != null) {
|
||||
infoBarAreaKey.currentState?.insertChild(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
bool close() {
|
||||
final result = infoBarAreaKey.currentState?.removeChild(overlay) ?? false;
|
||||
if(result) {
|
||||
onDismissed?.call();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
170
gui/lib/src/messenger/abstract/overlay.dart
Normal file
170
gui/lib/src/messenger/abstract/overlay.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
|
||||
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
||||
|
||||
class OverlayTarget extends StatefulWidget {
|
||||
final Widget child;
|
||||
const OverlayTarget({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<OverlayTarget> createState() => OverlayTargetState();
|
||||
|
||||
OverlayTargetState of(BuildContext context) => context.findAncestorStateOfType<OverlayTargetState>()!;
|
||||
}
|
||||
|
||||
class OverlayTargetState extends State<OverlayTarget> {
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
|
||||
void showOverlay({
|
||||
required String text,
|
||||
required WidgetBuilder actionBuilder,
|
||||
Offset offset = Offset.zero,
|
||||
bool ignoreTargetPointers = true,
|
||||
AttachMode attachMode = AttachMode.start
|
||||
}) {
|
||||
final renderBox = context.findRenderObject() as RenderBox;
|
||||
final position = renderBox.localToGlobal(Offset.zero);
|
||||
final color = FluentTheme.of(context).acrylicBackgroundColor;
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: _AbsorbPointer(
|
||||
exclusion: ignoreTargetPointers ? null : renderBox
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
left: position.dx - (attachMode != AttachMode.start ? renderBox.size.width : 0) + offset.dx,
|
||||
top: position.dy + (renderBox.size.height / 2) + offset.dy,
|
||||
child: CustomPaint(
|
||||
painter: _CallOutShape(color, attachMode != AttachMode.start),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(text),
|
||||
const SizedBox(height: 12.0),
|
||||
actionBuilder(context, () => entry.remove())
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
appOverlayKey.currentState?.insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachMode {
|
||||
start,
|
||||
middle,
|
||||
end;
|
||||
}
|
||||
|
||||
// Harder than one would think
|
||||
class _CallOutShape extends CustomPainter {
|
||||
final Color color;
|
||||
final bool end;
|
||||
_CallOutShape(this.color, this.end);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final fillPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final borderPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.25
|
||||
..color = Colors.white;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(10, 0);
|
||||
if(!end) {
|
||||
path.lineTo(12.5, 0);
|
||||
path.lineTo(20, -12.5);
|
||||
path.lineTo(27.5, 0);
|
||||
}else {
|
||||
path.lineTo(size.width - 27.5, 0);
|
||||
path.lineTo(size.width - 20, -12.5);
|
||||
path.lineTo(size.width - 12.5, 0);
|
||||
}
|
||||
|
||||
path.lineTo(size.width - 10, 0);
|
||||
path.arcToPoint(Offset(size.width, 10), radius: Radius.circular(10));
|
||||
path.lineTo(size.width, size.height - 10);
|
||||
path.arcToPoint(Offset(size.width - 10, size.height), radius: Radius.circular(10));
|
||||
path.lineTo(10, size.height);
|
||||
path.arcToPoint(Offset(0, size.height - 10), radius: Radius.circular(10));
|
||||
path.lineTo(0, 10);
|
||||
path.arcToPoint(Offset(10, 0), radius: Radius.circular(10));
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.drawPath(path, borderPaint);
|
||||
canvas.drawShadow(path, color, 1, true);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _AbsorbPointer extends SingleChildRenderObjectWidget {
|
||||
final RenderBox? exclusion;
|
||||
const _AbsorbPointer({
|
||||
required this.exclusion
|
||||
});
|
||||
|
||||
@override
|
||||
_RenderAbsorbPointer createRenderObject(BuildContext context) => _RenderAbsorbPointer(
|
||||
exclusion: exclusion
|
||||
);
|
||||
}
|
||||
|
||||
class _RenderAbsorbPointer extends RenderProxyBox {
|
||||
final RenderBox? exclusion;
|
||||
_RenderAbsorbPointer({
|
||||
required this.exclusion,
|
||||
RenderBox? child
|
||||
}) : super(child);
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, { required Offset position }) {
|
||||
final exclusion = this.exclusion;
|
||||
if(exclusion == null) {
|
||||
return size.contains(position);
|
||||
}
|
||||
|
||||
// 32 is the height of the title bar (need this offset as the overlay area doesn't include it)
|
||||
// Not an optimal solution but it works (calculating it is kind of complicated)
|
||||
position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight);
|
||||
final exclusionPosition = exclusion.localToGlobal(Offset.zero);
|
||||
final exclusionSize = Rect.fromLTRB(
|
||||
exclusionPosition.dx,
|
||||
exclusionPosition.dy,
|
||||
exclusionPosition.dx + exclusion.size.width,
|
||||
exclusionPosition.dy + exclusion.size.height
|
||||
);
|
||||
return !exclusionSize.contains(position);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) => super.visitChildrenForSemantics(visitor);
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.isBlockingUserActions = true;
|
||||
}
|
||||
}
|
||||
23
gui/lib/src/messenger/implementation/data.dart
Normal file
23
gui/lib/src/messenger/implementation/data.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.resetDefaultsDialogTitle,
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: translations.resetDefaultsDialogSecondaryAction,
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
text: translations.resetDefaultsDialogPrimaryAction,
|
||||
onTap: () {
|
||||
onConfirm();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
23
gui/lib/src/messenger/implementation/dll.dart
Normal file
23
gui/lib/src/messenger/implementation/dll.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
|
||||
builder: (context) => InfoDialog(
|
||||
text: translations.dllDeletedTitle,
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: translations.dllDeletedSecondaryAction,
|
||||
),
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: translations.dllDeletedPrimaryAction,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onConfirm();
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
34
gui/lib/src/messenger/implementation/error.dart
Normal file
34
gui/lib/src/messenger/implementation/error.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
String? lastError;
|
||||
|
||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
log("[ERROR] $exception");
|
||||
log("[STACKTRACE] $stackTrace");
|
||||
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
|
||||
return;
|
||||
}
|
||||
|
||||
if(lastError == exception.toString()){
|
||||
return;
|
||||
}
|
||||
|
||||
lastError = exception.toString();
|
||||
final route = ModalRoute.of(pageKey.currentContext!);
|
||||
if(route != null && !route.isCurrent){
|
||||
Navigator.of(pageKey.currentContext!).pop(false);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: exception,
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.uncaughtErrorMessage(exception.toString())
|
||||
)
|
||||
));
|
||||
}
|
||||
346
gui/lib/src/messenger/implementation/onboard.dart
Normal file
346
gui/lib/src/messenger/implementation/onboard.dart
Normal file
@@ -0,0 +1,346 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||
|
||||
void startOnboarding() {
|
||||
final settingsController = Get.find<SettingsController>();
|
||||
settingsController.firstRun.value = false;
|
||||
profileOverlayKey.currentState!.showOverlay(
|
||||
text: translations.startOnboardingText,
|
||||
offset: Offset(27.5, 17.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.startOnboardingActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
await showProfileForm(context);
|
||||
_promptPlayPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayPage() {
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptPlayPageActionLabel,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
_promptPlayVersion();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptPlayVersion() {
|
||||
final gameController = Get.find<GameController>();
|
||||
final hasBuilds = gameController.versions.value.isNotEmpty;
|
||||
gameVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptPlayVersionText,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: hasBuilds ? translations.promptPlayVersionActionLabelHasBuilds : translations.promptPlayVersionActionLabelNoBuilds,
|
||||
onTap: () async {
|
||||
onClose();
|
||||
if(!hasBuilds) {
|
||||
await VersionSelector.openDownloadDialog(closable: false);
|
||||
}
|
||||
_promptServerBrowserPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptServerBrowserPage() {
|
||||
pageIndex.value = RebootPageType.browser.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptServerBrowserPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptServerBrowserPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostPage() {
|
||||
pageIndex.value = RebootPageType.host.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInfo();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptHostInfo() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostInfoOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInfoText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => Row(
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelSkip,
|
||||
themed: false,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = false;
|
||||
_promptHostVersion();
|
||||
}
|
||||
),
|
||||
const SizedBox(width: 12.0),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInfoActionLabelConfigure,
|
||||
onTap: () {
|
||||
onClose();
|
||||
hostingController.discoverable.value = true;
|
||||
hostInfoTileKey.currentState!.openNestedPage();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformation() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.nameFocusNode.requestFocus();
|
||||
hostInfoNameOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationDescription();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationDescription() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.descriptionFocusNode.requestFocus();
|
||||
hostInfoDescriptionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationDescriptionText,
|
||||
attachMode: AttachMode.middle,
|
||||
ignoreTargetPointers: false,
|
||||
offset: Offset(70, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationDescriptionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostInformationPassword();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostInformationPassword() {
|
||||
final hostingController = Get.find<HostingController>();
|
||||
hostingController.passwordFocusNode.requestFocus();
|
||||
hostInfoPasswordOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostInformationPasswordText,
|
||||
ignoreTargetPointers: false,
|
||||
attachMode: AttachMode.middle,
|
||||
offset: Offset(25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostInformationPasswordActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
Navigator.of(hostInfoTileKey.currentContext!).pop();
|
||||
pageStack.removeLast();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostVersion() {
|
||||
hostVersionOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostVersionText,
|
||||
attachMode: AttachMode.end,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostVersionActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptHostShare();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptHostShare() {
|
||||
final backendController = Get.find<BackendController>();
|
||||
hostShareOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptHostShareText,
|
||||
offset: Offset(-10, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptHostShareActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
backendController.type.value = ServerType.embedded;
|
||||
_promptBackendPage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _promptBackendPage() {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendPageText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendPageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendTypePage();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendTypePage() {
|
||||
backendTypeOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendTypePageText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-25, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendTypePageActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendGameServerAddress();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendGameServerAddress() {
|
||||
backendGameServerAddressOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendGameServerAddressText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-100, 0),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendGameServerAddressActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendUnrealEngineKey();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendUnrealEngineKey() {
|
||||
backendUnrealEngineOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendUnrealEngineKeyText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-465, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendUnrealEngineKeyActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptBackendDetached();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptBackendDetached() {
|
||||
backendDetachedOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptBackendDetachedText,
|
||||
attachMode: AttachMode.end,
|
||||
offset: Offset(-410, 2.5),
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptBackendDetachedActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptInfoTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptInfoTab() {
|
||||
pageIndex.value = RebootPageType.info.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptInfoTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptInfoTabActionLabel,
|
||||
onTap: () {
|
||||
onClose();
|
||||
_promptSettingsTab();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _promptSettingsTab() {
|
||||
pageIndex.value = RebootPageType.settings.index;
|
||||
pageOverlayTargetKey.currentState!.showOverlay(
|
||||
text: translations.promptSettingsTabText,
|
||||
actionBuilder: (context, onClose) => _buildActionButton(
|
||||
context: context,
|
||||
label: translations.promptSettingsTabActionLabel,
|
||||
onTap: onClose
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
bool themed = true,
|
||||
required void Function() onTap,
|
||||
}) => Button(
|
||||
style: themed ? ButtonStyle(
|
||||
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
|
||||
) : null,
|
||||
child: Text(label),
|
||||
onPressed: onTap
|
||||
);
|
||||
81
gui/lib/src/messenger/implementation/profile.dart
Normal file
81
gui/lib/src/messenger/implementation/profile.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
|
||||
Future<bool> showProfileForm(BuildContext context) async{
|
||||
final showPassword = RxBool(false);
|
||||
final oldUsername = _gameController.username.text;
|
||||
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
|
||||
final oldPassword = _gameController.password.text;
|
||||
final result = await showRebootDialog<bool?>(
|
||||
builder: (context) => Obx(() => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoLabel(
|
||||
label: translations.usernameOrEmail,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.usernameOrEmailPlaceholder,
|
||||
controller: _gameController.username,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
enableSuggestions: true,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
InfoLabel(
|
||||
label: translations.password,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.passwordPlaceholder,
|
||||
controller: _gameController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
||||
suffix: Button(
|
||||
onPressed: () => showPassword.value = !showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
showPassword.value ? Icons.visibility_off : Icons.visibility,
|
||||
color: showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
const SizedBox(height: 8.0)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: translations.cancelProfileChanges,
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.saveProfileChanges,
|
||||
type: ButtonType.primary,
|
||||
onTap: () => Navigator.of(context).pop(true)
|
||||
)
|
||||
]
|
||||
))
|
||||
) ?? false;
|
||||
if(result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_gameController.username.text = oldUsername;
|
||||
_gameController.password.text = oldPassword;
|
||||
return false;
|
||||
}
|
||||
277
gui/lib/src/messenger/implementation/server.dart
Normal file
277
gui/lib/src/messenger/implementation/server.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
extension ServerControllerDialog on BackendController {
|
||||
Future<bool> toggleInteractive() async {
|
||||
final stream = toggle();
|
||||
final completer = Completer<bool>();
|
||||
InfoBarEntry? entry;
|
||||
worker = stream.listen((event) {
|
||||
entry?.close();
|
||||
entry = _handeEvent(event);
|
||||
if(event.type.isError) {
|
||||
completer.complete(false);
|
||||
}else if(event.type.isSuccess) {
|
||||
completer.complete(true);
|
||||
}
|
||||
});
|
||||
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
InfoBarEntry _handeEvent(ServerResult event) {
|
||||
log("[BACKEND] Handling event: $event");
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
return showRebootInfoBar(
|
||||
translations.startingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.startSuccess:
|
||||
return showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.startError:
|
||||
print(event.stackTrace);
|
||||
return showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.stopping:
|
||||
return showRebootInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.stopSuccess:
|
||||
return showRebootInfoBar(
|
||||
translations.stoppedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.stopError:
|
||||
return showRebootInfoBar(
|
||||
translations.stopServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.missingHostError:
|
||||
return showRebootInfoBar(
|
||||
translations.missingHostNameError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.missingPortError:
|
||||
return showRebootInfoBar(
|
||||
translations.missingPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.illegalPortError:
|
||||
return showRebootInfoBar(
|
||||
translations.illegalPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.freeingPort:
|
||||
return showRebootInfoBar(
|
||||
translations.freeingPort,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.freePortSuccess:
|
||||
return showRebootInfoBar(
|
||||
translations.freedPort,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
case ServerResultType.freePortError:
|
||||
return showRebootInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.pingingRemote:
|
||||
return showRebootInfoBar(
|
||||
translations.pingingServer(ServerType.remote.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingingLocal:
|
||||
return showRebootInfoBar(
|
||||
translations.pingingServer(type.value.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingError:
|
||||
return showRebootInfoBar(
|
||||
translations.pingError(type.value.name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.processError:
|
||||
return showRebootInfoBar(
|
||||
translations.backendProcessError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
|
||||
if(!kDebugMode && uuid == server.id) {
|
||||
showRebootInfoBar(
|
||||
translations.joinSelfServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final gameController = Get.find<GameController>();
|
||||
final version = gameController.getVersionByName(server.version.toString());
|
||||
if(version == null) {
|
||||
showRebootInfoBar(
|
||||
translations.cannotJoinServerVersion(server.version.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final hashedPassword = server.password;
|
||||
final hasPassword = hashedPassword != null;
|
||||
final embedded = type.value == ServerType.embedded;
|
||||
final author = server.author;
|
||||
final encryptedIp = server.ip;
|
||||
if(!hasPassword) {
|
||||
final valid = await _isServerValid(encryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(gameController, embedded, encryptedIp, author, version);
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmPassword = await _askForPassword();
|
||||
if(confirmPassword == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||
showRebootInfoBar(
|
||||
translations.wrongServerPassword,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
||||
final valid = await _isServerValid(decryptedIp);
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onSuccess(gameController, embedded, decryptedIp, author, version);
|
||||
}
|
||||
|
||||
Future<bool> _isServerValid(String address) async {
|
||||
final result = await pingGameServer(address);
|
||||
if(result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
showRebootInfoBar(
|
||||
translations.offlineServer,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<String?> _askForPassword() async {
|
||||
final confirmPasswordController = TextEditingController();
|
||||
final showPassword = RxBool(false);
|
||||
final showPasswordTrailing = RxBool(false);
|
||||
return await showRebootDialog<String?>(
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoLabel(
|
||||
label: translations.serverPassword,
|
||||
child: Obx(() => TextFormBox(
|
||||
placeholder: translations.serverPasswordPlaceholder,
|
||||
controller: confirmPasswordController,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
||||
suffix: !showPasswordTrailing.value ? null : Button(
|
||||
onPressed: () => showPassword.value = !showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
|
||||
),
|
||||
)
|
||||
))
|
||||
),
|
||||
const SizedBox(height: 8.0)
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: translations.serverPasswordCancel,
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: translations.serverPasswordConfirm,
|
||||
type: ButtonType.primary,
|
||||
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
||||
if(embedded) {
|
||||
gameServerAddress.text = decryptedIp;
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
}else {
|
||||
FlutterClipboard.controlC(decryptedIp);
|
||||
}
|
||||
controller.selectedVersion = version;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.success
|
||||
));
|
||||
}
|
||||
}
|
||||
463
gui/lib/src/messenger/implementation/version.dart
Normal file
463
gui/lib/src/messenger/implementation/version.dart
Normal file
@@ -0,0 +1,463 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
|
||||
class AddVersionDialog extends StatefulWidget {
|
||||
final bool closable;
|
||||
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddVersionDialog> createState() => _AddVersionDialogState();
|
||||
}
|
||||
|
||||
class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
|
||||
|
||||
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
|
||||
final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive);
|
||||
final Rxn<FortniteBuild> _build = Rxn();
|
||||
final RxnInt _timeLeft = RxnInt();
|
||||
final Rxn<double> _progress = Rxn();
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future<List<FortniteBuild>> _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
Isolate? _isolate;
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = compute(fetchBuilds, null);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_cancelDownload();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelDownload() {
|
||||
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
_isolate?.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case _DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
final data = snapshot.data;
|
||||
if (data == null) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
showButton: widget.closable,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() => FormDialog(
|
||||
content: _buildFormBody(data),
|
||||
buttons: _formButtons
|
||||
));
|
||||
}
|
||||
);
|
||||
case _DownloadStatus.downloading:
|
||||
case _DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _progressBody,
|
||||
buttons: _stopButton
|
||||
);
|
||||
case _DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception(translations.unknownError),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
|
||||
);
|
||||
case _DownloadStatus.done:
|
||||
return InfoDialog(
|
||||
text: translations.downloadedVersion
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
List<DialogButton> get _formButtons => [
|
||||
if(widget.closable)
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
|
||||
type: widget.closable ? ButtonType.primary : ButtonType.only,
|
||||
color: FluentTheme.of(context).accentColor,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
final topResult = _formKey.currentState?.validate();
|
||||
if(topResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fieldResult = _formFieldKey.currentState?.validate();
|
||||
if(fieldResult != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final source = _source.value;
|
||||
if(source == _BuildSource.local) {
|
||||
Navigator.of(context).pop();
|
||||
_addFortniteVersion(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.downloading;
|
||||
final communicationPort = ReceivePort();
|
||||
communicationPort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
|
||||
}else if(message is SendPort) {
|
||||
_downloadPort = message;
|
||||
}else {
|
||||
_onDownloadError(message, null);
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
Directory(_pathController.text),
|
||||
communicationPort.sendPort
|
||||
);
|
||||
final errorPort = ReceivePort();
|
||||
errorPort.listen((message) => _onDownloadError(message, null));
|
||||
_isolate = await Isolate.spawn(
|
||||
downloadArchiveBuild,
|
||||
options,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadComplete(FortniteBuild build) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.done;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_addFortniteVersion(build);
|
||||
}
|
||||
|
||||
void _addFortniteVersion(FortniteBuild build) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
|
||||
content: build.version,
|
||||
location: Directory(_pathController.text)
|
||||
)));
|
||||
}
|
||||
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
_cancelDownload();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = _DownloadStatus.error;
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(progress >= 100 && extracting) {
|
||||
_onDownloadComplete(build);
|
||||
return;
|
||||
}
|
||||
|
||||
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
|
||||
if(progress >= 0) {
|
||||
WindowsTaskbar.setProgress(progress.round(), 100);
|
||||
}
|
||||
|
||||
_timeLeft.value = timeLeft;
|
||||
_progress.value = progress;
|
||||
}
|
||||
|
||||
Widget get _progressBody {
|
||||
final timeLeft = _timeLeft.value;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translations.buildProgress((_progress.value ?? 0).round()),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSourceSelector(),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
_buildBuildSelector(builds),
|
||||
|
||||
FileSelector(
|
||||
label: translations.gameFolderTitle,
|
||||
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
|
||||
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
|
||||
controller: _pathController,
|
||||
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
String? _checkGameFolder(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyGamePath;
|
||||
}
|
||||
|
||||
final directory = Directory(text);
|
||||
if (!directory.existsSync()) {
|
||||
return translations.directoryDoesNotExist;
|
||||
}
|
||||
|
||||
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
|
||||
return translations.missingShippingExe;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _checkDownloadDestination(text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.invalidDownloadPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
|
||||
label: translations.build,
|
||||
child: FormField<FortniteBuild?>(
|
||||
key: _formFieldKey,
|
||||
validator: (data) => _checkBuild(data),
|
||||
builder: (formContext) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ComboBox<FortniteBuild>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(),
|
||||
value: _build.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_build.value = value;
|
||||
formContext.didChange(value);
|
||||
formContext.validate();
|
||||
_updateFormDefaults();
|
||||
}
|
||||
),
|
||||
if(formContext.hasError)
|
||||
const SizedBox(height: 4.0),
|
||||
if(formContext.hasError)
|
||||
Text(
|
||||
formContext.errorText ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: formContext.hasError ? 8.0 : 16.0
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
String? _checkBuild(FortniteBuild? data) {
|
||||
if(data == null) {
|
||||
return translations.selectBuild;
|
||||
}
|
||||
|
||||
final versions = _gameController.versions.value;
|
||||
if (versions.any((element) => data.version == element.content)) {
|
||||
return translations.versionAlreadyExists;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
|
||||
Widget _buildSourceSelector() => InfoLabel(
|
||||
label: translations.source,
|
||||
child: ComboBox<_BuildSource>(
|
||||
placeholder: Text(translations.selectBuild),
|
||||
isExpanded: true,
|
||||
items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(),
|
||||
value: _source.value,
|
||||
onChanged: (value) {
|
||||
if(value == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_source.value = value;
|
||||
_updateFormDefaults();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>(
|
||||
value: element,
|
||||
child: Text(element.translatedName)
|
||||
);
|
||||
|
||||
|
||||
List<DialogButton> get _stopButton => [
|
||||
DialogButton(
|
||||
text: translations.stopLoadingDialogAction,
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_source.value != _BuildSource.local && _build.value?.available != true) {
|
||||
_build.value = null;
|
||||
}
|
||||
|
||||
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) {
|
||||
await _fetchFuture;
|
||||
final bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
final build = _build.value;
|
||||
if(build == null){
|
||||
return;
|
||||
}
|
||||
|
||||
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
}
|
||||
|
||||
_formKey.currentState?.validate();
|
||||
}
|
||||
}
|
||||
|
||||
enum _DownloadStatus {
|
||||
form,
|
||||
downloading,
|
||||
extracting,
|
||||
error,
|
||||
done
|
||||
}
|
||||
|
||||
enum _BuildSource {
|
||||
local,
|
||||
githubArchive;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _BuildSource.local:
|
||||
return translations.localBuild;
|
||||
case _BuildSource.githubArchive:
|
||||
return translations.githubArchive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user