This commit is contained in:
Alessandro Autiero
2025-09-19 18:14:22 +01:00
parent d53a577f0b
commit 70d83dc1c5
53 changed files with 1043 additions and 1056 deletions

View File

@@ -17,7 +17,7 @@ String? _lastPort;
typedef BackendErrorHandler = void Function(String);
Stream<AuthBackendResult> startAuthBackend({
Stream<AuthBackendEvent> startAuthBackend({
required AuthBackendType type,
required String host,
required String port,
@@ -30,80 +30,80 @@ Stream<AuthBackendResult> startAuthBackend({
host = host.trim();
port = port.trim();
if(type != AuthBackendType.local || port != kDefaultBackendPort.toString()) {
yield AuthBackendResult(AuthBackendResultType.starting);
yield AuthBackendEvent(AuthBackendEventType.starting);
}
if (host.isEmpty) {
yield AuthBackendResult(AuthBackendResultType.startMissingHostError);
yield AuthBackendEvent(AuthBackendEventType.startMissingHostError);
return;
}
if (port.isEmpty) {
yield AuthBackendResult(AuthBackendResultType.startMissingPortError);
yield AuthBackendEvent(AuthBackendEventType.startMissingPortError);
return;
}
final portNumber = int.tryParse(port);
if (portNumber == null) {
yield AuthBackendResult(AuthBackendResultType.startIllegalPortError);
yield AuthBackendEvent(AuthBackendEventType.startIllegalPortError);
return;
}
if ((type != AuthBackendType.local || port != kDefaultBackendPort.toString()) && !(await isAuthBackendPortFree())) {
yield AuthBackendResult(AuthBackendResultType.startFreeingPort);
yield AuthBackendEvent(AuthBackendEventType.startFreeingPort);
final result = await freeAuthBackendPort();
if(!result) {
yield AuthBackendResult(AuthBackendResultType.startFreePortError);
yield AuthBackendEvent(AuthBackendEventType.startFreePortError);
return;
}
yield AuthBackendResult(AuthBackendResultType.startFreePortSuccess);
yield AuthBackendEvent(AuthBackendEventType.startFreePortSuccess);
}
switch(type){
case AuthBackendType.embedded:
process = await _startEmbedded(detached, onError: onError);
yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(process: process));
yield AuthBackendEvent(AuthBackendEventType.startedImplementation, implementation: AuthBackendImplementation(process: process));
break;
case AuthBackendType.remote:
yield AuthBackendResult(AuthBackendResultType.startPingingRemote);
yield AuthBackendEvent(AuthBackendEventType.startPingingRemote);
final uriResult = await _ping(host, portNumber);
if(uriResult == null) {
yield AuthBackendResult(AuthBackendResultType.startPingError);
yield AuthBackendEvent(AuthBackendEventType.startPingError);
return;
}
server = await _startRemote(uriResult);
yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(server: server));
yield AuthBackendEvent(AuthBackendEventType.startedImplementation, implementation: AuthBackendImplementation(server: server));
break;
case AuthBackendType.local:
if(portNumber != kDefaultBackendPort) {
yield AuthBackendResult(AuthBackendResultType.startPingingLocal);
yield AuthBackendEvent(AuthBackendEventType.startPingingLocal);
final uriResult = await _ping(kDefaultBackendHost, portNumber);
if(uriResult == null) {
yield AuthBackendResult(AuthBackendResultType.startPingError);
yield AuthBackendEvent(AuthBackendEventType.startPingError);
return;
}
server = await _startRemote(Uri.parse("http://$kDefaultBackendHost:$port"));
yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(server: server));
yield AuthBackendEvent(AuthBackendEventType.startedImplementation, implementation: AuthBackendImplementation(server: server));
}
break;
}
yield AuthBackendResult(AuthBackendResultType.startPingingLocal);
yield AuthBackendEvent(AuthBackendEventType.startPingingLocal);
final uriResult = await _ping(kDefaultBackendHost, kDefaultBackendPort);
if(uriResult == null) {
yield AuthBackendResult(AuthBackendResultType.startPingError);
yield AuthBackendEvent(AuthBackendEventType.startPingError);
process?.kill(ProcessSignal.sigterm);
server?.close(force: true);
return;
}
yield AuthBackendResult(AuthBackendResultType.startSuccess);
yield AuthBackendEvent(AuthBackendEventType.startSuccess);
}catch(error, stackTrace) {
yield AuthBackendResult(
AuthBackendResultType.startError,
yield AuthBackendEvent(
AuthBackendEventType.startError,
error: error,
stackTrace: stackTrace
);
@@ -141,8 +141,8 @@ Future<Process> _startEmbedded(bool detached, {BackendErrorHandler? onError}) as
Future<HttpServer> _startRemote(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
Stream<AuthBackendResult> stopAuthBackend({required AuthBackendType type, required AuthBackendImplementation? implementation}) async* {
yield AuthBackendResult(AuthBackendResultType.stopping);
Stream<AuthBackendEvent> stopAuthBackend({required AuthBackendType type, required AuthBackendImplementation? implementation}) async* {
yield AuthBackendEvent(AuthBackendEventType.stopping);
try{
switch(type){
case AuthBackendType.embedded:
@@ -158,10 +158,10 @@ Stream<AuthBackendResult> stopAuthBackend({required AuthBackendType type, requir
await implementation?.server?.close(force: true);
break;
}
yield AuthBackendResult(AuthBackendResultType.stopSuccess);
yield AuthBackendEvent(AuthBackendEventType.stopSuccess);
}catch(error, stackTrace){
yield AuthBackendResult(
AuthBackendResultType.stopError,
yield AuthBackendEvent(
AuthBackendEventType.stopError,
error: error,
stackTrace: stackTrace
);

View File

@@ -1,12 +1,12 @@
import 'dart:io';
class AuthBackendResult {
final AuthBackendResultType type;
class AuthBackendEvent {
final AuthBackendEventType type;
final AuthBackendImplementation? implementation;
final Object? error;
final StackTrace? stackTrace;
AuthBackendResult(this.type, {this.implementation, this.error, this.stackTrace});
AuthBackendEvent(this.type, {this.implementation, this.error, this.stackTrace});
@override
String toString() {
@@ -14,14 +14,7 @@ class AuthBackendResult {
}
}
class AuthBackendImplementation {
final Process? process;
final HttpServer? server;
AuthBackendImplementation({this.process, this.server});
}
enum AuthBackendResultType {
enum AuthBackendEventType {
starting,
startMissingHostError,
startMissingPortError,
@@ -43,5 +36,12 @@ enum AuthBackendResultType {
bool get isError => name.endsWith("Error");
bool get isSuccess => this == AuthBackendResultType.startSuccess || this == AuthBackendResultType.stopSuccess;
bool get isSuccess => this == AuthBackendEventType.startSuccess || this == AuthBackendEventType.stopSuccess;
}
class AuthBackendImplementation {
final Process? process;
final HttpServer? server;
AuthBackendImplementation({this.process, this.server});
}

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
@@ -16,7 +15,7 @@ import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/message/error.dart';
import 'package:reboot_launcher/src/pager/pager.dart';
import 'package:reboot_launcher/src/widget/page/pager.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/url_protocol.dart';
import 'package:system_theme/system_theme.dart';

View File

@@ -8,9 +8,9 @@ import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
typedef BackendInteractiveEventHandler = InfoBarEntry? Function(AuthBackendType, AuthBackendResult);
typedef BackendInteractiveEventHandler = SnackBar? Function(AuthBackendType, AuthBackendEvent);
class BackendController extends GetxController {
static const String storageName = "v3_backend_storage";
@@ -27,7 +27,7 @@ class BackendController extends GetxController {
late final RxBool detached;
AuthBackendImplementation? implementation;
StreamSubscription? _worker;
InfoBarEntry? _interactiveEntry;
SnackBar? _interactiveEntry;
BackendController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);

View File

@@ -7,8 +7,8 @@ import 'package:get_storage/get_storage.dart';
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/settings_page.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/page/settings/page.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:version/version.dart';
import 'package:path/path.dart' as path;
@@ -75,7 +75,7 @@ class DllController extends GetxController {
}
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
InfoBarEntry? infoBarEntry;
SnackBar? SnackBar;
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
@@ -94,7 +94,7 @@ class DllController extends GetxController {
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
SnackBar = SnackBar.open(
translations.downloadingDll("reboot"),
loading: true,
duration: null
@@ -111,19 +111,19 @@ class DllController extends GetxController {
).then((values) => values.reduce((first, second) => first && second));
if(!result) {
status.value = UpdateStatus.error;
showRebootInfoBar(
SnackBar.open(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, "reboot"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
infoBarEntry?.close();
SnackBar?.close();
return false;
}
timestamp.value = DateTime.now().millisecondsSinceEpoch;
status.value = UpdateStatus.success;
infoBarEntry?.close();
SnackBar?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
SnackBar = SnackBar.open(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
@@ -132,20 +132,20 @@ class DllController extends GetxController {
_listenToFileEvents(GameDll.gameServer);
return true;
}catch(message) {
infoBarEntry?.close();
SnackBar?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
final completer = Completer<bool>();
infoBarEntry = showRebootInfoBar(
SnackBar = SnackBar.open(
translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
infoBarEntry?.close();
SnackBar?.close();
final result = updateGameServerDll(
force: true,
silent: silent
@@ -216,7 +216,7 @@ class DllController extends GetxController {
Future<bool> download(GameDll dll, String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry;
SnackBar? entry;
try {
if (dll == GameDll.gameServer) {
return await updateGameServerDll(silent: silent);
@@ -232,7 +232,7 @@ class DllController extends GetxController {
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
log("[DLL] Showing dialog while downloading $dll...");
entry = showRebootInfoBar(
entry = SnackBar.open(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
@@ -243,7 +243,7 @@ class DllController extends GetxController {
final result = await downloadDependency(dll, filePath);
if(!result) {
entry?.close();
showRebootInfoBar(
SnackBar.open(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
@@ -254,7 +254,7 @@ class DllController extends GetxController {
entry?.close();
if(!silent) {
log("[DLL] Showing success dialog for $dll");
entry = await showRebootInfoBar(
entry = await SnackBar.open(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
@@ -271,7 +271,7 @@ class DllController extends GetxController {
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer<bool>();
await showRebootInfoBar(
await SnackBar.open(
translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
@@ -317,7 +317,7 @@ class DllController extends GetxController {
.instance
.value
?.kill();
showRebootInfoBar(
SnackBar.open(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error

View File

@@ -7,7 +7,7 @@ import 'package:http/http.dart' as http;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';
import 'package:yaml/yaml.dart';

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
builder: (context) => InfoDialog(

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
Future<void> showDllDeletedDialog() => showRebootDialog(
builder: (context) => InfoDialog(

View File

@@ -9,8 +9,8 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/extensions.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/button/file_selector.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:windows_taskbar/windows_taskbar.dart';

View File

@@ -1,8 +1,8 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
String? lastError;

View File

@@ -6,8 +6,8 @@ import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/button/file_selector.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:version/version.dart';
class ImportVersionDialog extends StatefulWidget {

View File

@@ -5,16 +5,16 @@ 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/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/message/profile.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/backend_page.dart';
import 'package:reboot_launcher/src/pager/pager.dart';
import 'package:reboot_launcher/src/page/host_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/page/play_page.dart';
import 'package:reboot_launcher/src/button/version_selector.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/section/backend/page.dart';
import 'package:reboot_launcher/src/widget/page/pager.dart';
import 'package:reboot_launcher/src/widget/page/host/page.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
import 'package:reboot_launcher/src/widget/section/play/page.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart';
void startOnboarding() {
final gameController = Get.find<GameController>();

View File

@@ -3,7 +3,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
final showPassword = RxBool(false);

View File

@@ -1,325 +0,0 @@
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/util/translations.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'info_bar.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.symmetric(vertical: 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: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
),
onPressed: widget.onTap!,
child: Text(widget.text!),
);
Widget get _secondaryButton => Button(
style: widget.color != null ? ButtonStyle(
backgroundColor: WidgetStateProperty.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
}

View File

@@ -1,78 +0,0 @@
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(String 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(
minHeight: _height
),
child: Mica(
elevation: 1,
child: InfoBar(
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: text is Widget ? text : 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;
}
}

View File

@@ -1,45 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/page/pages.dart';
class InfoBarArea extends StatefulWidget {
const InfoBarArea({super.key});
@override
State<InfoBarArea> createState() => InfoBarAreaState();
}
class InfoBarAreaState extends State<InfoBarArea> {
final Rx<List<Widget>> _children = Rx([]);
void insertChild(Widget child) {
_children.value.add(child);
_children.refresh();
}
bool removeChild(Widget child) {
final result = _children.value.remove(child);
_children.refresh();
return result;
}
@override
Widget build(BuildContext context) => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Obx(() => Padding(
padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: _children.value.map((child) => Padding(
padding: EdgeInsets.only(
top: 12.0
),
child: child
)).toList(growable: false)
),
))
);
}

View File

@@ -1,132 +0,0 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
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_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/button/file_selector.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8;
SettingTile createFileSetting({
required GlobalKey<TextFormBoxState> key,
required String title,
required String description,
required TextEditingController controller,
required void Function() onReset
}) {
final obx = RxnString();
final selecting = RxBool(false);
return SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(title),
subtitle: Text(description),
contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
content: Row(
children: [
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: controller,
validator: (text) {
final result = _checkDll(text);
obx.value = result;
return result;
},
extension: "dll",
folder: false,
validatorMode: AutovalidateMode.always,
allowNavigator: false,
validatorKey: key
),
),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: obx.value == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.selectFile,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _onPressed(selecting, controller),
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
fluentIcons.FluentIcons.open_folder_horizontal
),
)
),
),
)),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: obx.value == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.reset,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: onReset,
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
),
),
))
],
)
);
}
void _onPressed(RxBool selecting, TextEditingController controller) {
if(selecting.value){
return;
}
selecting.value = true;
compute(openFilePicker, "dll")
.then((value) => _updateText(controller, value))
.then((_) => selecting.value = false);
}
void _updateText(TextEditingController controller, String? value) {
final text = value ?? controller.text;
controller.text = text;
controller.selection = TextSelection.collapsed(offset: text.length);
}
String? _checkDll(String? text) {
if (text == null || text.isEmpty) {
return translations.invalidDllPath;
}
final file = File(text);
try {
file.readAsBytesSync();
}catch(_) {
return translations.dllDoesNotExist;
}
if (!text.endsWith(".dll")) {
return translations.invalidDllExtension;
}
return null;
}

View File

@@ -1,35 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
class InfoTile extends StatelessWidget {
final Key? expanderKey;
final Text title;
final Text content;
const InfoTile({
this.expanderKey,
required this.title,
required this.content
});
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(
bottom: 4.0
),
child: Expander(
key: expanderKey,
header: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
FluentIcons.info_24_regular
),
const SizedBox(width: 16.0),
title
],
),
content: content,
),
);
}

View File

@@ -53,7 +53,9 @@ String _sanitize(String value) {
}
List<String> _getArguments(List<String>? arguments) {
if (arguments == null) return ['%s'];
if (arguments == null) {
return ['%s'];
}
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
throw ArgumentError('arguments must contain at least 1 instance of "%s"');

View File

@@ -0,0 +1,20 @@
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/widget/section/sections.dart';
abstract class Dialog extends StatelessWidget {
static Future<T?> open<T extends Object?>({
required WidgetBuilder builder,
bool dismissWithEsc = true
}) => fluent.showDialog(
context: appNavigatorKey.currentContext!,
useRootNavigator: false,
dismissWithEsc: dismissWithEsc,
builder: builder
);
const Dialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context);
}

View File

@@ -0,0 +1,58 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
class DialogButton extends StatefulWidget {
final String? text;
final Function()? onTap;
final DialogButtonType type;
final Color? color;
const DialogButton({
Key? key,
this.text,
this.onTap,
this.color,
required this.type
}): assert(type != DialogButtonType.primary || onTap != null, "OnTap handler cannot be null for primary buttons"),
assert(type != DialogButtonType.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 == DialogButtonType.only ? _onlyButton : _button;
SizedBox get _onlyButton => SizedBox(
width: double.infinity,
child: _button
);
Widget get _button => widget.type == DialogButtonType.primary ? _primaryButton : _secondaryButton;
Widget get _primaryButton => Button(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
),
onPressed: widget.onTap!,
child: Text(widget.text!),
);
Widget get _secondaryButton => Button(
style: widget.color != null ? ButtonStyle(
backgroundColor: WidgetStateProperty.all(widget.color!)
) : null,
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),
);
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
}
enum DialogButtonType {
primary,
secondary,
only
}

View File

@@ -0,0 +1,39 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'dialog.dart';
import 'dialog_button.dart';
import 'info_dialog.dart';
class ErrorDialog extends Dialog {
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);
@override
Widget build(BuildContext context) {
return InfoDialog(
text: errorMessageBuilder(exception),
buttons: [
DialogButton(
type: stackTrace == null ? DialogButtonType.only : DialogButtonType.secondary
),
if(stackTrace != null)
DialogButton(
text: translations.copyErrorDialogTitle,
type: DialogButtonType.primary,
onTap: () async {
FlutterClipboard.controlC("$exception\n$stackTrace");
SnackBar.open(translations.copyErrorDialogSuccess);
Navigator.pop(context);
},
)
],
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'dialog.dart';
import 'dialog_button.dart';
import 'generic_dialog.dart';
class FormDialog extends Dialog {
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) => Form(
child: Builder(
builder: (context) => GenericDialog(
header: content,
buttons: buttons.map((entry) => _createFormButton(entry, context)).toList()
)
)
);
DialogButton _createFormButton(DialogButton entry, BuildContext context) {
if (entry.type != DialogButtonType.primary) {
return entry;
}
return DialogButton(
text: entry.text,
type: entry.type,
onTap: () {
if(!Form.of(context).validate()) {
return;
}
entry.onTap?.call();
}
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'dialog.dart';
import 'dialog_button.dart';
import 'generic_dialog.dart';
class FutureDialog extends Dialog {
final Future future;
final String loadingMessage;
final Widget successfulBody;
final Widget unsuccessfulBody;
final Function(Object) errorMessageBuilder;
final Function()? onError;
final bool closeAutomatically;
const FutureDialog(
{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);
@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 _buildData(errorMessageBuilder(snapshot.error!));
}
if(snapshot.connectionState == ConnectionState.done
&& (snapshot.data == null || (snapshot.data is bool && !snapshot.data))){
return unsuccessfulBody;
}
if (!snapshot.hasData) {
return _loadingBody;
}
if(closeAutomatically){
WidgetsBinding.instance
.addPostFrameCallback((_) => Navigator.of(context).pop(true));
return _loadingBody;
}
return successfulBody;
}
Widget _buildData(String message) {
return Container(
width: double.infinity,
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(
message,
textAlign: TextAlign.center
)
);
}
Widget get _loadingBody => InfoLabel(
label: loadingMessage,
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
width: double.infinity,
child: const ProgressBar()),
);
DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot)=> DialogButton(
text: snapshot.hasData
|| snapshot.hasError
|| (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? translations.defaultDialogSecondaryAction : translations.stopLoadingDialogAction,
type: DialogButtonType.only,
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
);
}

View File

@@ -0,0 +1,21 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'dialog.dart';
import 'dialog_button.dart';
class GenericDialog extends Dialog {
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
);
}

View File

@@ -0,0 +1,33 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'dialog.dart';
import 'dialog_button.dart';
import 'generic_dialog.dart';
class InfoDialog extends Dialog {
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: DialogButtonType.only
);
}

View File

@@ -0,0 +1,36 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'dialog.dart';
import 'dialog_button.dart';
import 'generic_dialog.dart';
class ProgressDialog extends Dialog {
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.symmetric(vertical: 16.0),
width: double.infinity,
child: const ProgressBar()
),
),
buttons: [
if(showButton)
DialogButton(
text: translations.defaultDialogSecondaryAction,
type: DialogButtonType.only,
onTap: onStop
)
]
);
}
}

View File

@@ -17,8 +17,8 @@ class FileSelector extends StatefulWidget {
final bool folder;
final void Function(String)? onSelected;
const FileSelector(
{required this.placeholder,
const FileSelector({
required this.placeholder,
required this.windowTitle,
required this.controller,
required this.folder,
@@ -29,8 +29,7 @@ class FileSelector extends StatefulWidget {
this.extension,
this.validatorMode,
this.onSelected,
Key? key})
: assert(folder || extension != null, "Missing extension for file selector"),
Key? key}) : assert(folder || extension != null, "Missing extension for file selector"),
super(key: key);
@override

View File

@@ -1,7 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/message/onboard.dart';

View File

@@ -1,110 +1,110 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
InfoBarEntry? onBackendResult(AuthBackendType type, AuthBackendResult event) {
SnackBar? onBackendResult(AuthBackendType type, AuthBackendEvent event) {
switch (event.type) {
case AuthBackendResultType.starting:
return showRebootInfoBar(
case AuthBackendEventType.starting:
return SnackBar.open(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.startSuccess:
return showRebootInfoBar(
case AuthBackendEventType.startSuccess:
return SnackBar.open(
type == AuthBackendType.local
? translations.checkedServer
: translations.startedServer,
severity: InfoBarSeverity.success
);
case AuthBackendResultType.startError:
return showRebootInfoBar(
case AuthBackendEventType.startError:
return SnackBar.open(
type == AuthBackendType.local
? translations.localServerError(event.error ?? translations.unknownError)
: translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case AuthBackendResultType.stopping:
return showRebootInfoBar(
case AuthBackendEventType.stopping:
return SnackBar.open(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.stopSuccess:
return showRebootInfoBar(
case AuthBackendEventType.stopSuccess:
return SnackBar.open(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
case AuthBackendResultType.stopError:
return showRebootInfoBar(
case AuthBackendEventType.stopError:
return SnackBar.open(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case AuthBackendResultType.startMissingHostError:
return showRebootInfoBar(
case AuthBackendEventType.startMissingHostError:
return SnackBar.open(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startMissingPortError:
return showRebootInfoBar(
case AuthBackendEventType.startMissingPortError:
return SnackBar.open(
translations.missingPortError,
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startIllegalPortError:
return showRebootInfoBar(
case AuthBackendEventType.startIllegalPortError:
return SnackBar.open(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startFreeingPort:
return showRebootInfoBar(
case AuthBackendEventType.startFreeingPort:
return SnackBar.open(
translations.freeingPort,
loading: true,
duration: null
);
case AuthBackendResultType.startFreePortSuccess:
return showRebootInfoBar(
case AuthBackendEventType.startFreePortSuccess:
return SnackBar.open(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
case AuthBackendResultType.startFreePortError:
return showRebootInfoBar(
case AuthBackendEventType.startFreePortError:
return SnackBar.open(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case AuthBackendResultType.startPingingRemote:
return showRebootInfoBar(
case AuthBackendEventType.startPingingRemote:
return SnackBar.open(
translations.pingingServer(AuthBackendType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.startPingingLocal:
return showRebootInfoBar(
case AuthBackendEventType.startPingingLocal:
return SnackBar.open(
translations.pingingServer(type.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case AuthBackendResultType.startPingError:
return showRebootInfoBar(
case AuthBackendEventType.startPingError:
return SnackBar.open(
translations.pingError(type.name),
severity: InfoBarSeverity.error
);
case AuthBackendResultType.startedImplementation:
case AuthBackendEventType.startedImplementation:
return null;
}
}
void onBackendError(Object error) {
showRebootInfoBar(
SnackBar.open(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,

View File

@@ -5,22 +5,22 @@ import 'package:flutter/services.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/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/widget/section/setting_tile.dart';
import 'package:reboot_launcher/src/message/data.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/button/backend_start_button.dart';
import 'package:reboot_launcher/src/button/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/widget/section/backend/state_toggle.dart';
import 'package:reboot_launcher/src/widget/section/backend/type_selector.dart';
import 'package:url_launcher/url_launcher.dart';
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
class BackendPage extends AbstractPage {
const BackendPage({Key? key}) : super(key: key);
@@ -44,12 +44,12 @@ class BackendPage extends AbstractPage {
class _BackendPageState extends AbstractPageState<BackendPage> {
final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry;
SnackBar? _SnackBar;
@override
void initState() {
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
if(_infoBarEntry == null) {
if(_SnackBar == null) {
return false;
}
@@ -57,8 +57,8 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
_backendController.consoleKey.value = keyEvent.physicalKey;
}
_infoBarEntry?.close();
_infoBarEntry = null;
_SnackBar?.close();
_SnackBar = null;
return true;
});
super.initState();
@@ -87,7 +87,7 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
),
title: Text(translations.matchmakerConfigurationAddressName),
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
content: OverlayTarget(
content: TutorialOverlayTarget(
key: backendGameServerAddressOverlayTargetKey,
child: TextFormBox(
placeholder: translations.matchmakerConfigurationAddressName,
@@ -158,7 +158,7 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
const SizedBox(
width: 16.0
),
OverlayTarget(
TutorialOverlayTarget(
key: backendDetachedOverlayTargetKey,
child: ToggleSwitch(
checked: _backendController.detached(),
@@ -182,11 +182,11 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
title: Text(translations.settingsClientConsoleKeyName),
subtitle: Text(translations.settingsClientConsoleKeyDescription),
contentWidth: null,
content: OverlayTarget(
content: TutorialOverlayTarget(
key: backendUnrealEngineOverlayTargetKey,
child: Button(
onPressed: () {
_infoBarEntry = showRebootInfoBar(
_SnackBar = SnackBar.open(
translations.clickKey,
loading: true,
duration: null
@@ -234,7 +234,7 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
),
title: Text(translations.backendTypeName),
subtitle: Text(translations.backendTypeDescription),
content: ServerTypeSelector(
content: BackendTypeSelector(
overlayKey: backendTypeOverlayTargetKey
)
);

View File

@@ -6,11 +6,9 @@ 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/message/backend.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/section/backend/message.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import '../messenger/info_bar.dart';
class BackendButton extends StatefulWidget {
const BackendButton({Key? key}) : super(key: key);
@@ -50,18 +48,26 @@ class _BackendButtonState extends State<BackendButton> {
alignment: Alignment.center,
child: StreamBuilder(
stream: _textController.stream,
builder: (context, snapshot) => Obx(() => Text(_buttonText))
builder: (context, snapshot) => Obx(() => Text(_text))
),
),
onPressed: () => _backendController.toggle(
eventHandler: (type, event) {
eventHandler: _onEvent,
errorHandler: _onError
)
)
)
);
SnackBar? _onEvent(AuthBackendType type, AuthBackendEvent event) {
_backendController.started.value = event.type.isStart && !event.type.isError;
if(event.type == AuthBackendResultType.startedImplementation) {
if(event.type == AuthBackendEventType.startedImplementation) {
_backendController.implementation = event.implementation;
}
return onBackendResult(type, event);
},
errorHandler: (error) {
}
void _onError(String error) {
if(_backendController.started.value) {
_backendController.stop();
_gameController.instance.value?.kill();
@@ -69,21 +75,15 @@ class _BackendButtonState extends State<BackendButton> {
onBackendError(error);
}
}
)
)
)
);
String get _buttonText {
if(_backendController.type.value == AuthBackendType.local && _backendController.port.text.trim() == kDefaultBackendPort.toString()){
String get _text {
if(_backendController.type.value == AuthBackendType.local
&& _backendController.port.text.trim() == kDefaultBackendPort.toString()){
return translations.checkServer;
}
if(_backendController.started.value){
}else if(_backendController.started.value){
return translations.stopServer;
}
}else {
return translations.startServer;
}
}
}

View File

@@ -3,35 +3,34 @@ 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/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
class ServerTypeSelector extends StatefulWidget {
class BackendTypeSelector extends StatefulWidget {
final Key overlayKey;
const ServerTypeSelector({required this.overlayKey});
const BackendTypeSelector({required this.overlayKey});
@override
State<ServerTypeSelector> createState() => _ServerTypeSelectorState();
State<BackendTypeSelector> createState() => _BackendTypeSelectorState();
}
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
class _BackendTypeSelectorState extends State<BackendTypeSelector> {
late final BackendController _backendController = Get.find<BackendController>();
@override
Widget build(BuildContext context) {
return Obx(() => OverlayTarget(
return Obx(() => TutorialOverlayTarget(
key: widget.overlayKey,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_backendController.type.value.label),
items: AuthBackendType.values
.map((type) => _createItem(type))
.toList()
items: _items
),
));
}
List<MenuFlyoutItem> get _items => AuthBackendType.values
.map((type) => _createItem(type))
.toList();
MenuFlyoutItem _createItem(AuthBackendType type) => MenuFlyoutItem(
text: Text(type.label),
onPressed: () async {
@@ -42,9 +41,9 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
}
extension _ServerTypeExtension on AuthBackendType {
String get label {
return this == AuthBackendType.embedded ? translations.embedded
: this == AuthBackendType.remote ? translations.remote
: translations.local;
}
String get label => switch(this) {
AuthBackendType.embedded => translations.embedded,
AuthBackendType.remote => translations.remote,
AuthBackendType.local => translations.local
};
}

View File

@@ -0,0 +1,13 @@
import 'package:reboot_launcher/src/util/translations.dart';
enum ServerBrowserFilter {
all,
accessible,
playable;
String get translatedName => switch(this) {
ServerBrowserFilter.all => translations.all,
ServerBrowserFilter.accessible => translations.accessible,
ServerBrowserFilter.playable => translations.playable
};
}

View File

@@ -0,0 +1,15 @@
import 'package:reboot_launcher/src/util/translations.dart';
enum ServerBrowserOrder {
timeAscending,
timeDescending,
nameAscending,
nameDescending;
String get translatedName => switch(this) {
ServerBrowserOrder.timeAscending => translations.timeAscending,
ServerBrowserOrder.timeDescending => translations.timeDescending,
ServerBrowserOrder.nameAscending => translations.nameAscending,
ServerBrowserOrder.nameDescending => translations.nameDescending
};
}

View File

@@ -11,16 +11,20 @@ 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/server_browser_controller.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/dialog/dialog.dart';
import 'package:reboot_launcher/src/widget/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/widget/dialog/form_dialog.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/section/browser/filter.dart';
import 'package:reboot_launcher/src/widget/section/browser/order.dart';
import 'package:reboot_launcher/src/widget/section/sections.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/extensions.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/widget/section/setting_tile.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
class BrowsePage extends AbstractPage {
const BrowsePage({Key? key}) : super(key: key);
@@ -49,8 +53,8 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
final TextEditingController _filterController = TextEditingController();
final StreamController<String> _filterControllerStream = StreamController.broadcast();
final Rx<_Filter> _filter = Rx(_Filter.all);
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
final Rx<ServerBrowserFilter> _filter = Rx(ServerBrowserFilter.all);
final Rx<ServerBrowserOrder> _sort = Rx(ServerBrowserOrder.timeDescending);
@override
void initState() {
@@ -76,7 +80,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
if(server != null) {
_joinServer(_hostingController.uuid, server);
}else {
showRebootInfoBar(
SnackBar.open(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
@@ -124,11 +128,11 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
),
Row(
children: [
_buildFilter(context),
_buildFilterSection(context),
const SizedBox(
width: 16.0
),
_buildSort(context),
_buildOrderSection(context),
],
),
const SizedBox(
@@ -142,7 +146,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
}
);
Widget _buildSort(BuildContext context) => Row(
Widget _buildOrderSection(BuildContext context) => Row(
children: [
Icon(
fluentIcons.FluentIcons.arrow_sort_24_regular,
@@ -159,24 +163,27 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
Obx(() => SizedBox(
width: 230,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_sort.value.translatedName,
textAlign: TextAlign.start
),
title: const Spacer(),
items: _Sort.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _sort.value = entry
)).toList()
items: _orders
),
))
],
);
Row _buildFilter(BuildContext context) {
return Row(
List<MenuFlyoutItem> get _orders => ServerBrowserOrder.values
.map(_buildOrder)
.toList();
MenuFlyoutItem _buildOrder(ServerBrowserOrder entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _sort.value = entry
);
Widget _buildFilterSection(BuildContext context) => Row(
children: [
Icon(
fluentIcons.FluentIcons.filter_24_regular,
@@ -193,45 +200,48 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
Obx(() => SizedBox(
width: 125,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_filter.value.translatedName,
textAlign: TextAlign.start
),
title: const Spacer(),
items: _Filter.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _filter.value = entry
)).toList()
items: _filters
),
))
],
);
}
List<MenuFlyoutItem> get _filters => ServerBrowserFilter.values
.map((entry) => _buildFilter(entry))
.toList();
MenuFlyoutItem _buildFilter(ServerBrowserFilter entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _filter.value = entry
);
Widget _buildPopulatedListBody(List<ServerBrowserEntry>? items) => Obx(() {
final filter = _filter.value;
final sorted = items?.where((element) {
switch(filter) {
case _Filter.all:
case ServerBrowserFilter.all:
return true;
case _Filter.accessible:
case ServerBrowserFilter.accessible:
return element.password.isNotEmpty;
case _Filter.playable:
case ServerBrowserFilter.playable:
return _gameController.getVersionByGame(element.version) != null;
}
}).toList();
final sort = _sort.value;
sorted?.sort((first, second) {
switch(sort) {
case _Sort.timeAscending:
case ServerBrowserOrder.timeAscending:
return first.timestamp.compareTo(second.timestamp);
case _Sort.timeDescending:
case ServerBrowserOrder.timeDescending:
return second.timestamp.compareTo(first.timestamp);
case _Sort.nameAscending:
case ServerBrowserOrder.nameAscending:
return first.name.compareTo(second.name);
case _Sort.nameDescending:
case ServerBrowserOrder.nameDescending:
return second.name.compareTo(first.name);
}
});
@@ -369,7 +379,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
Future<void> _joinServer(String uuid, ServerBrowserEntry server) async {
if(!kDebugMode && uuid == server.id) {
showRebootInfoBar(
SnackBar.open(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
@@ -379,7 +389,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
final version = _gameController.getVersionByGame(server.version.toString());
if(version == null) {
showRebootInfoBar(
SnackBar.open(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
@@ -407,7 +417,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
}
if(!checkPassword(confirmPassword, hashedPassword)) {
showRebootInfoBar(
SnackBar.open(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
@@ -425,7 +435,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
}
Future<bool> _isServerValid(String name, String address) async {
final loadingBar = showRebootInfoBar(
final loadingBar = SnackBar.open(
translations.joiningServer(name),
duration: infoBarLongDuration,
loading: true,
@@ -438,7 +448,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
return true;
}
showRebootInfoBar(
SnackBar.open(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
@@ -450,7 +460,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
return await Dialog.open<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
@@ -486,12 +496,12 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
type: DialogButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
type: DialogButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
@@ -507,7 +517,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>().selectedVersion.value = version;
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
WidgetsBinding.instance.addPostFrameCallback((_) => SnackBar.open(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
severity: InfoBarSeverity.success
@@ -520,40 +530,3 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
@override
List<Widget> get settings => [];
}
enum _Filter {
all,
accessible,
playable;
String get translatedName {
switch(this) {
case _Filter.all:
return translations.all;
case _Filter.accessible:
return translations.accessible;
case _Filter.playable:
return translations.playable;
}
}
}
enum _Sort {
timeAscending,
timeDescending,
nameAscending,
nameDescending;
String get translatedName {
switch(this) {
case _Sort.timeAscending:
return translations.timeAscending;
case _Sort.timeDescending:
return translations.timeDescending;
case _Sort.nameAscending:
return translations.nameAscending;
case _Sort.nameDescending:
return translations.nameDescending;
}
}
}

View File

@@ -9,22 +9,22 @@ import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/message/data.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/button/game_start_button.dart';
import 'package:reboot_launcher/src/button/version_selector.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/widget/section/play/state_toggle.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
class HostPage extends AbstractPage {
@@ -100,7 +100,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
),
title: Text(translations.hostGameServerNameName),
subtitle: Text(translations.hostGameServerNameDescription),
content: OverlayTarget(
content: TutorialOverlayTarget(
key: hostInfoNameOverlayTargetKey,
child: TextFormBox(
placeholder: translations.hostGameServerNameName,
@@ -116,7 +116,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
),
title: Text(translations.hostGameServerDescriptionName),
subtitle: Text(translations.hostGameServerDescriptionDescription),
content: OverlayTarget(
content: TutorialOverlayTarget(
key: hostInfoDescriptionOverlayTargetKey,
child: TextFormBox(
placeholder: translations.hostGameServerDescriptionName,
@@ -132,7 +132,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
),
title: Text(translations.hostGameServerPasswordName),
subtitle: Text(translations.hostGameServerPasswordDescription),
content: Obx(() => OverlayTarget(
content: Obx(() => TutorialOverlayTarget(
key: hostInfoPasswordOverlayTargetKey,
child: TextFormBox(
placeholder: translations.hostGameServerPasswordName,
@@ -275,7 +275,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
subtitle: Text(translations.hostShareIpDescription),
content: Button(
onPressed: () async {
InfoBarEntry? entry;
SnackBar? entry;
try {
entry = _showCopyingIp();
final ip = await Ipify.ipv4();
@@ -317,29 +317,29 @@ class _HostingPageState extends AbstractPageState<HostPage> {
}
}
void _showCopiedLink() => showRebootInfoBar(
void _showCopiedLink() => SnackBar.open(
translations.hostShareLinkMessageSuccess,
severity: InfoBarSeverity.success
);
InfoBarEntry _showCopyingIp() => showRebootInfoBar(
SnackBar _showCopyingIp() => SnackBar.open(
translations.hostShareIpMessageLoading,
loading: true,
duration: null
);
void _showCopiedIp() => showRebootInfoBar(
void _showCopiedIp() => SnackBar.open(
translations.hostShareIpMessageSuccess,
severity: InfoBarSeverity.success
);
void _showCannotCopyIp(Object error) => showRebootInfoBar(
void _showCannotCopyIp(Object error) => SnackBar.open(
translations.hostShareIpMessageError(error.toString()),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar(
void _showCannotUpdateGameServer(Object error) => SnackBar.open(
translations.cannotUpdateGameServer(error.toString()),
severity: InfoBarSeverity.success,
duration: infoBarLongDuration

View File

@@ -1,11 +1,11 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/widget/section/setting_tile.dart';
import 'package:reboot_launcher/src/message/onboard.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:url_launcher/url_launcher_string.dart';
class InfoPage extends AbstractPage {

View File

@@ -13,22 +13,21 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/pager/page_suggestion.dart';
import 'package:reboot_launcher/src/widget/page/page_suggestion.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/profile_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
import 'package:reboot_launcher/src/util/updater.dart';
import 'package:reboot_launcher/src/messenger/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar_area.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> profileOverlayKey = GlobalKey();
const double _kDefaultPadding = 12.0;
class RebootPager extends StatefulWidget {
@@ -92,7 +91,7 @@ class _RebootPagerState extends State<RebootPager> with WindowListener, Automati
}
_backendController.gameServerAddress.text = kDefaultGameServerHost;
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
WidgetsBinding.instance.addPostFrameCallback((_) => SnackBar.open(
translations.serverNoLongerAvailableUnnamed,
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration
@@ -106,8 +105,8 @@ class _RebootPagerState extends State<RebootPager> with WindowListener, Automati
void _checkUpdates() {
checkLauncherUpdate(
onUpdate: (latestVersion) {
late InfoBarEntry infoBar;
infoBar = showRebootInfoBar(
late SnackBar infoBar;
infoBar = SnackBar.open(
translations.updateAvailable(latestVersion.toString()),
duration: null,
severity: InfoBarSeverity.warning,
@@ -312,7 +311,7 @@ class _RebootPagerState extends State<RebootPager> with WindowListener, Automati
fit: StackFit.loose,
children: [
_buildBodyContent(),
InfoBarArea(
SnackBarArea(
key: infoBarAreaKey
)
],
@@ -414,10 +413,6 @@ class _RebootPagerState extends State<RebootPager> with WindowListener, Automati
return TextSpan(
text: name,
recognizer: last ? null : (TapGestureRecognizer()..onTap = () {
if(inDialog) {
return;
}
var pops = length - 1 - index;
while(pops-- > 0) {
Navigator.of(pageKey.currentContext!).pop();
@@ -439,7 +434,7 @@ class _RebootPagerState extends State<RebootPager> with WindowListener, Automati
children: [
Obx(() {
pageIndex.value;
return ProfileWidget(
return ProfileTile(
overlayKey: profileOverlayKey
);
}),
@@ -470,7 +465,7 @@ class _RebootPagerState extends State<RebootPager> with WindowListener, Automati
Widget _buildNavigationItem(AbstractPage page) {
final index = page.type.index;
return OverlayTarget(
return TutorialOverlayTarget(
key: getOverlayTargetKeyByPage(index),
child: HoverButton(
onPressed: () {

View File

@@ -3,16 +3,16 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/widget/section/setting_tile.dart';
import 'package:reboot_launcher/src/message/data.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/button/game_start_button.dart';
import 'package:reboot_launcher/src/button/version_selector.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/widget/section/play/state_toggle.dart';
import 'package:reboot_launcher/src/widget/section/version_selector.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
final GlobalKey<TutorialOverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
class PlayPage extends AbstractPage {
const PlayPage({Key? key}) : super(key: key);

View File

@@ -14,11 +14,13 @@ import 'package:reboot_launcher/src/controller/dll_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/server_browser_controller.dart';
import 'package:reboot_launcher/src/message/backend.dart';
import 'package:reboot_launcher/src/widget/dialog/dialog.dart';
import 'package:reboot_launcher/src/widget/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/widget/dialog/info_dialog.dart';
import 'package:reboot_launcher/src/widget/section/backend/message.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
@@ -43,8 +45,8 @@ class _LaunchButtonState extends State<LaunchButton> {
final DllController _dllController = Get.find<DllController>();
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar;
SnackBar? _gameClientInfoBar;
SnackBar? _gameServerInfoBar;
CancelableOperation? _operation;
Completer? _pingOperation;
@@ -106,7 +108,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final backendResult = _backendController.started() || await _backendController.toggle(
eventHandler: (type, event) {
_backendController.started.value = event.type.isStart && !event.type.isError;
if(event.type == AuthBackendResultType.startedImplementation) {
if(event.type == AuthBackendEventType.startedImplementation) {
_backendController.implementation = event.implementation;
}
return onBackendResult(type, event);
@@ -211,16 +213,16 @@ class _LaunchButtonState extends State<LaunchButton> {
return false;
}
final result = await showRebootDialog<bool>(
final result = await Dialog.open<bool>(
builder: (context) => InfoDialog(
text: translations.automaticGameServerDialogContent,
buttons: [
DialogButton(
type: ButtonType.secondary,
type: DialogButtonType.secondary,
text: translations.automaticGameServerDialogIgnore
),
DialogButton(
type: ButtonType.primary,
type: DialogButtonType.primary,
text: translations.automaticGameServerDialogStart,
onTap: () => Navigator.of(context).pop(true),
),
@@ -469,7 +471,7 @@ class _LaunchButtonState extends State<LaunchButton> {
void _onGameClientInjected() {
_gameClientInfoBar?.close();
showRebootInfoBar(
SnackBar.open(
translations.gameClientStarted,
severity: InfoBarSeverity.success,
duration: infoBarLongDuration
@@ -488,7 +490,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final started = await _checkLocalGameServer(gameServerPort);
if(!started) {
if (_hostingController.instance.value?.killed != true) {
showRebootInfoBar(
SnackBar.open(
translations.gameServerStartWarning,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
@@ -500,7 +502,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) {
showRebootInfoBar(
SnackBar.open(
translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration,
@@ -514,7 +516,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final serverBrowserEntry = await _hostingController.createServerBrowserEntry();
await _serverBrowserController.addServer(serverBrowserEntry);
showRebootInfoBar(
SnackBar.open(
translations.gameServerStarted,
severity: InfoBarSeverity.success,
duration: infoBarLongDuration
@@ -526,7 +528,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _checkLocalGameServer(String gameServerPort) async {
try {
_gameServerInfoBar = showRebootInfoBar(
_gameServerInfoBar = SnackBar.open(
translations.waitingForGameServer,
loading: true,
duration: null
@@ -549,7 +551,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _checkPublicGameServer(String gameServerPort) async {
try {
_gameServerInfoBar = showRebootInfoBar(
_gameServerInfoBar = SnackBar.open(
translations.checkingGameServer,
loading: true,
duration: null
@@ -668,21 +670,21 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.normal:
break;
case _StopReason.missingVersionError:
showRebootInfoBar(
SnackBar.open(
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.missingExecutableError:
showRebootInfoBar(
SnackBar.open(
translations.missingExecutableError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.multipleExecutablesError:
showRebootInfoBar(
SnackBar.open(
translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
@@ -691,7 +693,7 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.exitCode:
if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls;
showRebootInfoBar(
SnackBar.open(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
@@ -700,7 +702,7 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.corruptedVersionError:
final injectedDlls = instance?.injectedDlls ?? [];
showRebootInfoBar(
SnackBar.open(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
@@ -711,14 +713,14 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.corruptedDllError:
showRebootInfoBar(
SnackBar.open(
translations.corruptedDllError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.missingCustomDllError:
showRebootInfoBar(
SnackBar.open(
translations.missingCustomDllError(error!),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
@@ -727,7 +729,7 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.tokenError:
_backendController.stop();
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar(
SnackBar.open(
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
@@ -738,21 +740,21 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.crash:
showRebootInfoBar(
SnackBar.open(
translations.fortniteCrashError(host ? translations.gameServer : translations.client),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError:
showRebootInfoBar(
SnackBar.open(
translations.unknownFortniteError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.gameServerPortError:
showRebootInfoBar(
SnackBar.open(
translations.gameServerPortEqualsBackendPort(kDefaultBackendPort),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
@@ -829,14 +831,14 @@ class _LaunchButtonState extends State<LaunchButton> {
return null;
}
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
SnackBar _showLaunchingGameServerWidget() => _gameServerInfoBar = SnackBar.open(
translations.launchingGameServer,
loading: true,
duration: null
);
InfoBarEntry _showLaunchingGameClientWidget(GameVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar(
SnackBar _showLaunchingGameClientWidget(GameVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = SnackBar.open(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
duration: null,

View File

@@ -0,0 +1,142 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
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:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/section/setting_tile.dart';
class FileSetting extends StatefulWidget {
final GlobalKey<TextFormBoxState> validatorKey;
final String title;
final String description;
final TextEditingController controller;
final VoidCallback onReset;
final String extension;
final bool folder;
const FileSetting({
Key? key,
required this.validatorKey,
required this.title,
required this.description,
required this.controller,
required this.onReset,
this.extension = 'dll',
this.folder = false,
}) : super(key: key);
@override
State<FileSetting> createState() => _FileSettingState();
}
class _FileSettingState extends State<FileSetting> {
static const double _kButtonDimensions = 30;
static const double _kButtonSpacing = 8;
String? _validationMessage;
bool _selecting = false;
@override
Widget build(BuildContext context) {
return SettingTile(
icon: const Icon(FluentIcons.document_24_regular),
title: Text(widget.title),
subtitle: Text(widget.description),
contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
content: Row(
children: [
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: widget.controller,
validator: (text) {
final result = _checkDll(text);
setState(() => _validationMessage = result);
return result;
},
extension: widget.extension,
folder: widget.folder,
validatorMode: AutovalidateMode.always,
allowNavigator: false,
validatorKey: widget.validatorKey,
),
),
const SizedBox(width: _kButtonSpacing),
Padding(
padding: EdgeInsets.only(bottom: _validationMessage == null ? 0.0 : 20.0),
child: Tooltip(
message: translations.selectFile,
child: Button(
style: ButtonStyle(padding: WidgetStateProperty.all(EdgeInsets.zero)),
onPressed: _selecting ? null : _onSelectPressed,
child: SizedBox.square(
dimension: _kButtonDimensions,
child: const Icon(fluentIcons.FluentIcons.open_folder_horizontal),
),
),
),
),
const SizedBox(width: _kButtonSpacing),
Padding(
padding: EdgeInsets.only(bottom: _validationMessage == null ? 0.0 : 20.0),
child: Tooltip(
message: translations.reset,
child: Button(
style: ButtonStyle(padding: WidgetStateProperty.all(EdgeInsets.zero)),
onPressed: widget.onReset,
child: SizedBox.square(
dimension: _kButtonDimensions,
child: const Icon(FluentIcons.arrow_reset_24_regular),
),
),
),
),
],
),
);
}
Future<void> _onSelectPressed() async {
if (_selecting) return;
setState(() => _selecting = true);
try {
final picked = await compute(openFilePicker, widget.extension);
_updateText(widget.controller, picked);
} finally {
if (mounted) {
setState(() => _selecting = false);
}
}
}
void _updateText(TextEditingController controller, String? value) {
final text = value ?? controller.text;
controller.text = text;
controller.selection = TextSelection.collapsed(offset: text.length);
}
String? _checkDll(String? text) {
if (text == null || text.isEmpty) {
return translations.invalidDllPath;
}
final file = File(text);
try {
file.readAsBytesSync();
} catch (_) {
return translations.dllDoesNotExist;
}
if (!text.endsWith('.dll')) {
return translations.invalidDllExtension;
}
return null;
}
}

View File

@@ -8,12 +8,11 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/l10n/reboot_localizations.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/tile/file_setting_tile.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/widget/section/settings/file_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:url_launcher/url_launcher.dart';
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
@@ -63,8 +62,8 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
key: settingsConsoleDllInputKey,
FileSetting(
validatorKey: settingsConsoleDllInputKey,
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
@@ -73,10 +72,10 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
_dllController.unrealEngineConsoleDll.text = path;
await _dllController.download(GameDll.console, path, force: true);
settingsConsoleDllInputKey.currentState?.validate();
}
},
),
createFileSetting(
key: settingsAuthDllInputKey,
FileSetting(
validatorKey: settingsAuthDllInputKey,
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
@@ -85,10 +84,10 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
_dllController.backendDll.text = path;
await _dllController.download(GameDll.auth, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
},
),
createFileSetting(
key: settingsMemoryDllInputKey,
FileSetting(
validatorKey: settingsMemoryDllInputKey,
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
@@ -97,7 +96,7 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
_dllController.memoryLeakDll.text = path;
await _dllController.download(GameDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
},
),
_gameServer
],
@@ -124,8 +123,6 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
subtitle: Text(translations.settingsServerTypeDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
@@ -200,8 +197,8 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
)
);
}else {
return createFileSetting(
key: settingsGameServerDllInputKey,
return FileSetting(
validatorKey: settingsGameServerDllInputKey,
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.customGameServerDll,
@@ -210,7 +207,7 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
_dllController.customGameServerDll.text = path;
await _dllController.download(GameDll.gameServer, path);
settingsGameServerDllInputKey.currentState?.validate();
}
},
);
}
});
@@ -295,8 +292,6 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
title: Text(translations.settingsUtilsLanguageName),
subtitle: Text(translations.settingsUtilsLanguageDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_getLocaleName(_settingsController.language.value)),
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
text: Text(_getLocaleName(locale.languageCode)),
@@ -321,8 +316,6 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
title: Text(translations.settingsUtilsThemeName),
subtitle: Text(translations.settingsUtilsThemeDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.themeMode.value.title),
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
text: Text(themeMode.title),

View File

@@ -3,25 +3,25 @@ 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/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/message/profile.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
class ProfileWidget extends StatefulWidget {
final GlobalKey<OverlayTargetState> overlayKey;
const ProfileWidget({required this.overlayKey});
class ProfileTile extends StatefulWidget {
final GlobalKey<TutorialOverlayTargetState> overlayKey;
const ProfileTile({required this.overlayKey});
@override
State<ProfileWidget> createState() => _ProfileWidgetState();
State<ProfileTile> createState() => _ProfileTileState();
}
class _ProfileWidgetState extends State<ProfileWidget> {
class _ProfileTileState extends State<ProfileTile> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
@override
Widget build(BuildContext context) => OverlayTarget(
Widget build(BuildContext context) => TutorialOverlayTarget(
key: widget.overlayKey,
child: HoverButton(
margin: const EdgeInsets.all(8.0),

View File

@@ -3,16 +3,16 @@ import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/pager/page_type.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/backend_page.dart';
import 'package:reboot_launcher/src/page/browser_page.dart';
import 'package:reboot_launcher/src/page/host_page.dart';
import 'package:reboot_launcher/src/page/info_page.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:reboot_launcher/src/page/play_page.dart';
import 'package:reboot_launcher/src/page/settings_page.dart';
import 'package:reboot_launcher/src/messenger/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/section/host/page.dart';
import 'package:reboot_launcher/src/widget/section/info/page.dart';
import 'package:reboot_launcher/src/widget/section/settings/page.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar_area.dart';
import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/widget/section/backend/page.dart';
import 'package:reboot_launcher/src/widget/section/browser/page.dart';
import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/widget/section/play/page.dart';
final StreamController<void> pagesController = StreamController.broadcast();
bool hitBack = false;
@@ -26,7 +26,7 @@ final List<AbstractPage> pages = [
const SettingsPage()
];
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
final List<GlobalKey<TutorialOverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
final RxInt pageIndex = RxInt(PageType.play.index);
@@ -36,7 +36,7 @@ final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey();
final GlobalKey<OverlayState> appOverlayKey = GlobalKey();
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey();
final GlobalKey<SnackBarAreaState> infoBarAreaKey = GlobalKey();
GlobalKey get pageKey => getPageKeyByIndex(pageIndex.value);
@@ -82,6 +82,6 @@ void addSubPageToCurrent(String pageName) {
pagesController.add(null);
}
GlobalKey<OverlayTargetState> getOverlayTargetKeyByPage(int pageIndex) => _flyoutPageControllers[pageIndex];
GlobalKey<TutorialOverlayTargetState> getOverlayTargetKeyByPage(int pageIndex) => _flyoutPageControllers[pageIndex];
GlobalKey<OverlayTargetState> get pageOverlayTargetKey => _flyoutPageControllers[pageIndex.value];
GlobalKey<TutorialOverlayTargetState> get pageOverlayTargetKey => _flyoutPageControllers[pageIndex.value];

View File

@@ -1,7 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:skeletons/skeletons.dart';
class SettingTile extends StatefulWidget {
@@ -78,7 +78,7 @@ class SettingTileState extends State<SettingTile> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if(widget.overlayKey != null)
OverlayTarget(
TutorialOverlayTarget(
key: widget.overlayKey,
child: isSkeleton ? _skeletonIcon : icon,
)

View File

@@ -0,0 +1,81 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
const infoBarLongDuration = Duration(seconds: 4);
const infoBarShortDuration = Duration(seconds: 2);
const _height = 64.0;
class SnackBar {
static SnackBar open(String text, {
InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false,
Duration? duration = infoBarShortDuration,
void Function()? onDismissed,
Widget? action
}) {
final overlayEntry = SnackBar._internal(
overlay: ConstrainedBox(
constraints: BoxConstraints(
minHeight: _height
),
child: Mica(
elevation: 1,
child: InfoBar(
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: 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
)
),
),
onDismissed: onDismissed
);
if(duration != null) {
Future.delayed(duration)
.then((_) => WidgetsBinding.instance.addPostFrameCallback((timeStamp) => overlayEntry.close()));
}
return overlayEntry;
}
final Widget overlay;
final void Function()? onDismissed;
SnackBar._internal({
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;
}
}

View File

@@ -0,0 +1,53 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
class SnackBarArea extends StatefulWidget {
const SnackBarArea({super.key});
@override
State<SnackBarArea> createState() => SnackBarAreaState();
}
class SnackBarAreaState extends State<SnackBarArea> {
final List<Widget> _children = [];
void insertChild(Widget child) {
setState(() {
_children.add(child);
});
}
bool removeChild(Widget child) {
var result = false;
setState(() {
result = _children.remove(child);
});
return result;
}
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Padding(
padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: _snackbars,
),
),
);
}
List<Widget> get _snackbars => _children
.map((child) => _buildSnackbar(child))
.toList(growable: false);
Widget _buildSnackbar(Widget child) => Padding(
padding: const EdgeInsets.only(top: 12.0),
child: child,
);
}

View File

@@ -1,20 +1,22 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/rendering.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/widget/sections.dart';
import 'tutorial_overlay_attach_mode.dart';
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
class OverlayTarget extends StatefulWidget {
class TutorialOverlayTarget extends StatefulWidget {
final Widget child;
const OverlayTarget({super.key, required this.child});
const TutorialOverlayTarget({super.key, required this.child});
@override
State<OverlayTarget> createState() => OverlayTargetState();
State<TutorialOverlayTarget> createState() => TutorialOverlayTargetState();
OverlayTargetState of(BuildContext context) => context.findAncestorStateOfType<OverlayTargetState>()!;
TutorialOverlayTargetState of(BuildContext context) => context.findAncestorStateOfType<TutorialOverlayTargetState>()!;
}
class OverlayTargetState extends State<OverlayTarget> {
class TutorialOverlayTargetState extends State<TutorialOverlayTarget> {
@override
Widget build(BuildContext context) => widget.child;
@@ -23,7 +25,7 @@ class OverlayTargetState extends State<OverlayTarget> {
required WidgetBuilder actionBuilder,
Offset offset = Offset.zero,
bool ignoreTargetPointers = true,
AttachMode attachMode = AttachMode.start
TutorialOverlayAttachMode attachMode = TutorialOverlayAttachMode.start
}) {
final renderBox = context.findRenderObject() as RenderBox;
final position = renderBox.localToGlobal(Offset.zero);
@@ -40,10 +42,10 @@ class OverlayTargetState extends State<OverlayTarget> {
)
),
Positioned(
left: position.dx - (attachMode != AttachMode.start ? renderBox.size.width : 0) + offset.dx,
left: position.dx - (attachMode != TutorialOverlayAttachMode.start ? renderBox.size.width : 0) + offset.dx,
top: position.dy + (renderBox.size.height / 2) + offset.dy,
child: CustomPaint(
painter: _CallOutShape(color, attachMode != AttachMode.start),
painter: _CallOutShape(color, attachMode != TutorialOverlayAttachMode.start),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
@@ -65,12 +67,6 @@ class OverlayTargetState extends State<OverlayTarget> {
}
}
enum AttachMode {
start,
middle,
end;
}
// Harder than one would think
class _CallOutShape extends CustomPainter {
final Color color;

View File

@@ -0,0 +1,5 @@
enum TutorialOverlayAttachMode {
start,
middle,
end;
}

View File

@@ -7,10 +7,10 @@ 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/util/translations.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/message/download_version.dart';
import 'package:reboot_launcher/src/message/import_version.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -19,7 +19,7 @@ class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);
static SettingTile buildTile({
required GlobalKey<OverlayTargetState> key
required GlobalKey<TutorialOverlayTargetState> key
}) => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
@@ -221,7 +221,7 @@ class _VersionSelectorState extends State<VersionSelector> {
}
bool _onExplorerError() {
showRebootInfoBar(
SnackBar.open(
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,