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

View File

@@ -1,12 +1,12 @@
import 'dart:io'; import 'dart:io';
class AuthBackendResult { class AuthBackendEvent {
final AuthBackendResultType type; final AuthBackendEventType type;
final AuthBackendImplementation? implementation; final AuthBackendImplementation? implementation;
final Object? error; final Object? error;
final StackTrace? stackTrace; final StackTrace? stackTrace;
AuthBackendResult(this.type, {this.implementation, this.error, this.stackTrace}); AuthBackendEvent(this.type, {this.implementation, this.error, this.stackTrace});
@override @override
String toString() { String toString() {
@@ -14,14 +14,7 @@ class AuthBackendResult {
} }
} }
class AuthBackendImplementation { enum AuthBackendEventType {
final Process? process;
final HttpServer? server;
AuthBackendImplementation({this.process, this.server});
}
enum AuthBackendResultType {
starting, starting,
startMissingHostError, startMissingHostError,
startMissingPortError, startMissingPortError,
@@ -43,5 +36,12 @@ enum AuthBackendResultType {
bool get isError => name.endsWith("Error"); 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:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.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/server_browser_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/message/error.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/os.dart';
import 'package:reboot_launcher/src/util/url_protocol.dart'; import 'package:reboot_launcher/src/util/url_protocol.dart';
import 'package:system_theme/system_theme.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_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/keyboard.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 { class BackendController extends GetxController {
static const String storageName = "v3_backend_storage"; static const String storageName = "v3_backend_storage";
@@ -27,7 +27,7 @@ class BackendController extends GetxController {
late final RxBool detached; late final RxBool detached;
AuthBackendImplementation? implementation; AuthBackendImplementation? implementation;
StreamSubscription? _worker; StreamSubscription? _worker;
InfoBarEntry? _interactiveEntry; SnackBar? _interactiveEntry;
BackendController() { BackendController() {
_storage = appWithNoStorage ? null : GetStorage(storageName); _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:path/path.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/page/settings_page.dart'; import 'package:reboot_launcher/src/widget/page/settings/page.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:path/path.dart' as path; 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 { Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
InfoBarEntry? infoBarEntry; SnackBar? SnackBar;
try { try {
if(customGameServer.value) { if(customGameServer.value) {
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
@@ -94,7 +94,7 @@ class DllController extends GetxController {
} }
if(!silent) { if(!silent) {
infoBarEntry = showRebootInfoBar( SnackBar = SnackBar.open(
translations.downloadingDll("reboot"), translations.downloadingDll("reboot"),
loading: true, loading: true,
duration: null duration: null
@@ -111,19 +111,19 @@ class DllController extends GetxController {
).then((values) => values.reduce((first, second) => first && second)); ).then((values) => values.reduce((first, second) => first && second));
if(!result) { if(!result) {
status.value = UpdateStatus.error; status.value = UpdateStatus.error;
showRebootInfoBar( SnackBar.open(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, "reboot"), translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, "reboot"),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
infoBarEntry?.close(); SnackBar?.close();
return false; return false;
} }
timestamp.value = DateTime.now().millisecondsSinceEpoch; timestamp.value = DateTime.now().millisecondsSinceEpoch;
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
infoBarEntry?.close(); SnackBar?.close();
if(!silent) { if(!silent) {
infoBarEntry = showRebootInfoBar( SnackBar = SnackBar.open(
translations.downloadDllSuccess("reboot"), translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
@@ -132,20 +132,20 @@ class DllController extends GetxController {
_listenToFileEvents(GameDll.gameServer); _listenToFileEvents(GameDll.gameServer);
return true; return true;
}catch(message) { }catch(message) {
infoBarEntry?.close(); SnackBar?.close();
var error = message.toString(); var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase(); error = error.toLowerCase();
status.value = UpdateStatus.error; status.value = UpdateStatus.error;
final completer = Completer<bool>(); final completer = Completer<bool>();
infoBarEntry = showRebootInfoBar( SnackBar = SnackBar.open(
translations.downloadDllError(error.toString(), "reboot.dll"), translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(false), onDismissed: () => completer.complete(false),
action: Button( action: Button(
onPressed: () async { onPressed: () async {
infoBarEntry?.close(); SnackBar?.close();
final result = updateGameServerDll( final result = updateGameServerDll(
force: true, force: true,
silent: silent silent: silent
@@ -216,7 +216,7 @@ class DllController extends GetxController {
Future<bool> download(GameDll dll, String filePath, {bool silent = false, bool force = false}) async { 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)"); log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry; SnackBar? entry;
try { try {
if (dll == GameDll.gameServer) { if (dll == GameDll.gameServer) {
return await updateGameServerDll(silent: silent); return await updateGameServerDll(silent: silent);
@@ -232,7 +232,7 @@ class DllController extends GetxController {
final fileNameWithoutExtension = basenameWithoutExtension(filePath); final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) { if(!silent) {
log("[DLL] Showing dialog while downloading $dll..."); log("[DLL] Showing dialog while downloading $dll...");
entry = showRebootInfoBar( entry = SnackBar.open(
translations.downloadingDll(fileNameWithoutExtension), translations.downloadingDll(fileNameWithoutExtension),
loading: true, loading: true,
duration: null duration: null
@@ -243,7 +243,7 @@ class DllController extends GetxController {
final result = await downloadDependency(dll, filePath); final result = await downloadDependency(dll, filePath);
if(!result) { if(!result) {
entry?.close(); entry?.close();
showRebootInfoBar( SnackBar.open(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name), translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -254,7 +254,7 @@ class DllController extends GetxController {
entry?.close(); entry?.close();
if(!silent) { if(!silent) {
log("[DLL] Showing success dialog for $dll"); log("[DLL] Showing success dialog for $dll");
entry = await showRebootInfoBar( entry = await SnackBar.open(
translations.downloadDllSuccess(fileNameWithoutExtension), translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
@@ -271,7 +271,7 @@ class DllController extends GetxController {
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase(); error = error.toLowerCase();
final completer = Completer<bool>(); final completer = Completer<bool>();
await showRebootInfoBar( await SnackBar.open(
translations.downloadDllError(error.toString(), dll.name), translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
@@ -317,7 +317,7 @@ class DllController extends GetxController {
.instance .instance
.value .value
?.kill(); ?.kill();
showRebootInfoBar( SnackBar.open(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name), translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error 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_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/translations.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:url_launcher/url_launcher.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.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( Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
builder: (context) => InfoDialog( builder: (context) => InfoDialog(

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/translations.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( Future<void> showDllDeletedDialog() => showRebootDialog(
builder: (context) => InfoDialog( 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/extensions.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/button/file_selector.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:windows_taskbar/windows_taskbar.dart'; import 'package:windows_taskbar/windows_taskbar.dart';

View File

@@ -1,8 +1,8 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/translations.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';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/widget/sections.dart';
String? lastError; String? lastError;

View File

@@ -6,8 +6,8 @@ import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/button/file_selector.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
class ImportVersionDialog extends StatefulWidget { 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/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_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/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/util/translations.dart';
import 'package:reboot_launcher/src/message/profile.dart'; import 'package:reboot_launcher/src/message/profile.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/page/backend_page.dart'; import 'package:reboot_launcher/src/widget/section/backend/page.dart';
import 'package:reboot_launcher/src/pager/pager.dart'; import 'package:reboot_launcher/src/widget/page/pager.dart';
import 'package:reboot_launcher/src/page/host_page.dart'; import 'package:reboot_launcher/src/widget/page/host/page.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/widget/sections.dart';
import 'package:reboot_launcher/src/page/play_page.dart'; import 'package:reboot_launcher/src/widget/section/play/page.dart';
import 'package:reboot_launcher/src/button/version_selector.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart';
void startOnboarding() { void startOnboarding() {
final gameController = Get.find<GameController>(); 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:flutter/material.dart' show Icons;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/translations.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{ Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
final showPassword = RxBool(false); 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) { 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'))) { if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
throw ArgumentError('arguments must contain at least 1 instance of "%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,20 +17,19 @@ class FileSelector extends StatefulWidget {
final bool folder; final bool folder;
final void Function(String)? onSelected; final void Function(String)? onSelected;
const FileSelector( const FileSelector({
{required this.placeholder, required this.placeholder,
required this.windowTitle, required this.windowTitle,
required this.controller, required this.controller,
required this.folder, required this.folder,
required this.allowNavigator, required this.allowNavigator,
this.validator, this.validator,
this.validatorKey, this.validatorKey,
this.label, this.label,
this.extension, this.extension,
this.validatorMode, this.validatorMode,
this.onSelected, this.onSelected,
Key? key}) Key? key}) : assert(folder || extension != null, "Missing extension for file selector"),
: assert(folder || extension != null, "Missing extension for file selector"),
super(key: key); super(key: key);
@override @override

View File

@@ -1,7 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_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/util/translations.dart';
import 'package:reboot_launcher/src/message/onboard.dart'; import 'package:reboot_launcher/src/message/onboard.dart';

View File

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

View File

@@ -5,22 +5,22 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.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/keyboard.dart';
import 'package:reboot_launcher/src/util/translations.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/message/data.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart'; import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/button/backend_start_button.dart'; import 'package:reboot_launcher/src/widget/section/backend/state_toggle.dart';
import 'package:reboot_launcher/src/button/server_type_selector.dart'; import 'package:reboot_launcher/src/widget/section/backend/type_selector.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
class BackendPage extends AbstractPage { class BackendPage extends AbstractPage {
const BackendPage({Key? key}) : super(key: key); const BackendPage({Key? key}) : super(key: key);
@@ -44,12 +44,12 @@ class BackendPage extends AbstractPage {
class _BackendPageState extends AbstractPageState<BackendPage> { class _BackendPageState extends AbstractPageState<BackendPage> {
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry; SnackBar? _SnackBar;
@override @override
void initState() { void initState() {
ServicesBinding.instance.keyboard.addHandler((keyEvent) { ServicesBinding.instance.keyboard.addHandler((keyEvent) {
if(_infoBarEntry == null) { if(_SnackBar == null) {
return false; return false;
} }
@@ -57,8 +57,8 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
_backendController.consoleKey.value = keyEvent.physicalKey; _backendController.consoleKey.value = keyEvent.physicalKey;
} }
_infoBarEntry?.close(); _SnackBar?.close();
_infoBarEntry = null; _SnackBar = null;
return true; return true;
}); });
super.initState(); super.initState();
@@ -87,7 +87,7 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
), ),
title: Text(translations.matchmakerConfigurationAddressName), title: Text(translations.matchmakerConfigurationAddressName),
subtitle: Text(translations.matchmakerConfigurationAddressDescription), subtitle: Text(translations.matchmakerConfigurationAddressDescription),
content: OverlayTarget( content: TutorialOverlayTarget(
key: backendGameServerAddressOverlayTargetKey, key: backendGameServerAddressOverlayTargetKey,
child: TextFormBox( child: TextFormBox(
placeholder: translations.matchmakerConfigurationAddressName, placeholder: translations.matchmakerConfigurationAddressName,
@@ -158,7 +158,7 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
const SizedBox( const SizedBox(
width: 16.0 width: 16.0
), ),
OverlayTarget( TutorialOverlayTarget(
key: backendDetachedOverlayTargetKey, key: backendDetachedOverlayTargetKey,
child: ToggleSwitch( child: ToggleSwitch(
checked: _backendController.detached(), checked: _backendController.detached(),
@@ -182,11 +182,11 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
title: Text(translations.settingsClientConsoleKeyName), title: Text(translations.settingsClientConsoleKeyName),
subtitle: Text(translations.settingsClientConsoleKeyDescription), subtitle: Text(translations.settingsClientConsoleKeyDescription),
contentWidth: null, contentWidth: null,
content: OverlayTarget( content: TutorialOverlayTarget(
key: backendUnrealEngineOverlayTargetKey, key: backendUnrealEngineOverlayTargetKey,
child: Button( child: Button(
onPressed: () { onPressed: () {
_infoBarEntry = showRebootInfoBar( _SnackBar = SnackBar.open(
translations.clickKey, translations.clickKey,
loading: true, loading: true,
duration: null duration: null
@@ -234,7 +234,7 @@ class _BackendPageState extends AbstractPageState<BackendPage> {
), ),
title: Text(translations.backendTypeName), title: Text(translations.backendTypeName),
subtitle: Text(translations.backendTypeDescription), subtitle: Text(translations.backendTypeDescription),
content: ServerTypeSelector( content: BackendTypeSelector(
overlayKey: backendTypeOverlayTargetKey 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/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_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/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:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import '../messenger/info_bar.dart';
class BackendButton extends StatefulWidget { class BackendButton extends StatefulWidget {
const BackendButton({Key? key}) : super(key: key); const BackendButton({Key? key}) : super(key: key);
@@ -50,40 +48,42 @@ class _BackendButtonState extends State<BackendButton> {
alignment: Alignment.center, alignment: Alignment.center,
child: StreamBuilder( child: StreamBuilder(
stream: _textController.stream, stream: _textController.stream,
builder: (context, snapshot) => Obx(() => Text(_buttonText)) builder: (context, snapshot) => Obx(() => Text(_text))
), ),
), ),
onPressed: () => _backendController.toggle( onPressed: () => _backendController.toggle(
eventHandler: (type, event) { eventHandler: _onEvent,
_backendController.started.value = event.type.isStart && !event.type.isError; errorHandler: _onError
if(event.type == AuthBackendResultType.startedImplementation) {
_backendController.implementation = event.implementation;
}
return onBackendResult(type, event);
},
errorHandler: (error) {
if(_backendController.started.value) {
_backendController.stop();
_gameController.instance.value?.kill();
_hostingController.instance.value?.kill();
onBackendError(error);
}
}
) )
) )
) )
); );
SnackBar? _onEvent(AuthBackendType type, AuthBackendEvent event) {
_backendController.started.value = event.type.isStart && !event.type.isError;
if(event.type == AuthBackendEventType.startedImplementation) {
_backendController.implementation = event.implementation;
}
return onBackendResult(type, event);
}
String get _buttonText { void _onError(String error) {
if(_backendController.type.value == AuthBackendType.local && _backendController.port.text.trim() == kDefaultBackendPort.toString()){ if(_backendController.started.value) {
_backendController.stop();
_gameController.instance.value?.kill();
_hostingController.instance.value?.kill();
onBackendError(error);
}
}
String get _text {
if(_backendController.type.value == AuthBackendType.local
&& _backendController.port.text.trim() == kDefaultBackendPort.toString()){
return translations.checkServer; return translations.checkServer;
} }else if(_backendController.started.value){
if(_backendController.started.value){
return translations.stopServer; return translations.stopServer;
}else {
return translations.startServer;
} }
return translations.startServer;
} }
} }

View File

@@ -3,35 +3,34 @@ import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
class ServerTypeSelector extends StatefulWidget { class BackendTypeSelector extends StatefulWidget {
final Key overlayKey; final Key overlayKey;
const ServerTypeSelector({required this.overlayKey}); const BackendTypeSelector({required this.overlayKey});
@override @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>(); late final BackendController _backendController = Get.find<BackendController>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() => OverlayTarget( return Obx(() => TutorialOverlayTarget(
key: widget.overlayKey, key: widget.overlayKey,
child: DropDownButton( child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_backendController.type.value.label), leading: Text(_backendController.type.value.label),
items: AuthBackendType.values items: _items
.map((type) => _createItem(type))
.toList()
), ),
)); ));
} }
List<MenuFlyoutItem> get _items => AuthBackendType.values
.map((type) => _createItem(type))
.toList();
MenuFlyoutItem _createItem(AuthBackendType type) => MenuFlyoutItem( MenuFlyoutItem _createItem(AuthBackendType type) => MenuFlyoutItem(
text: Text(type.label), text: Text(type.label),
onPressed: () async { onPressed: () async {
@@ -42,9 +41,9 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
} }
extension _ServerTypeExtension on AuthBackendType { extension _ServerTypeExtension on AuthBackendType {
String get label { String get label => switch(this) {
return this == AuthBackendType.embedded ? translations.embedded AuthBackendType.embedded => translations.embedded,
: this == AuthBackendType.remote ? translations.remote AuthBackendType.remote => translations.remote,
: translations.local; 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/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_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/server_browser_controller.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/widget/dialog/dialog.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/widget/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/pager/page_type.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/cryptography.dart';
import 'package:reboot_launcher/src/util/extensions.dart'; import 'package:reboot_launcher/src/util/extensions.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.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/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
class BrowsePage extends AbstractPage { class BrowsePage extends AbstractPage {
const BrowsePage({Key? key}) : super(key: key); const BrowsePage({Key? key}) : super(key: key);
@@ -49,8 +53,8 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
final TextEditingController _filterController = TextEditingController(); final TextEditingController _filterController = TextEditingController();
final StreamController<String> _filterControllerStream = StreamController.broadcast(); final StreamController<String> _filterControllerStream = StreamController.broadcast();
final Rx<_Filter> _filter = Rx(_Filter.all); final Rx<ServerBrowserFilter> _filter = Rx(ServerBrowserFilter.all);
final Rx<_Sort> _sort = Rx(_Sort.timeDescending); final Rx<ServerBrowserOrder> _sort = Rx(ServerBrowserOrder.timeDescending);
@override @override
void initState() { void initState() {
@@ -76,7 +80,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
if(server != null) { if(server != null) {
_joinServer(_hostingController.uuid, server); _joinServer(_hostingController.uuid, server);
}else { }else {
showRebootInfoBar( SnackBar.open(
translations.noServerFound, translations.noServerFound,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -124,11 +128,11 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
), ),
Row( Row(
children: [ children: [
_buildFilter(context), _buildFilterSection(context),
const SizedBox( const SizedBox(
width: 16.0 width: 16.0
), ),
_buildSort(context), _buildOrderSection(context),
], ],
), ),
const SizedBox( const SizedBox(
@@ -142,7 +146,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
} }
); );
Widget _buildSort(BuildContext context) => Row( Widget _buildOrderSection(BuildContext context) => Row(
children: [ children: [
Icon( Icon(
fluentIcons.FluentIcons.arrow_sort_24_regular, fluentIcons.FluentIcons.arrow_sort_24_regular,
@@ -159,24 +163,27 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
Obx(() => SizedBox( Obx(() => SizedBox(
width: 230, width: 230,
child: DropDownButton( child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text( leading: Text(
_sort.value.translatedName, _sort.value.translatedName,
textAlign: TextAlign.start textAlign: TextAlign.start
), ),
title: const Spacer(), title: const Spacer(),
items: _Sort.values.map((entry) => MenuFlyoutItem( items: _orders
text: Text(entry.translatedName),
onPressed: () => _sort.value = entry
)).toList()
), ),
)) ))
], ],
); );
Row _buildFilter(BuildContext context) { List<MenuFlyoutItem> get _orders => ServerBrowserOrder.values
return Row( .map(_buildOrder)
.toList();
MenuFlyoutItem _buildOrder(ServerBrowserOrder entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _sort.value = entry
);
Widget _buildFilterSection(BuildContext context) => Row(
children: [ children: [
Icon( Icon(
fluentIcons.FluentIcons.filter_24_regular, fluentIcons.FluentIcons.filter_24_regular,
@@ -193,45 +200,48 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
Obx(() => SizedBox( Obx(() => SizedBox(
width: 125, width: 125,
child: DropDownButton( child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text( leading: Text(
_filter.value.translatedName, _filter.value.translatedName,
textAlign: TextAlign.start textAlign: TextAlign.start
), ),
title: const Spacer(), title: const Spacer(),
items: _Filter.values.map((entry) => MenuFlyoutItem( items: _filters
text: Text(entry.translatedName),
onPressed: () => _filter.value = entry
)).toList()
), ),
)) ))
], ],
); );
}
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(() { Widget _buildPopulatedListBody(List<ServerBrowserEntry>? items) => Obx(() {
final filter = _filter.value; final filter = _filter.value;
final sorted = items?.where((element) { final sorted = items?.where((element) {
switch(filter) { switch(filter) {
case _Filter.all: case ServerBrowserFilter.all:
return true; return true;
case _Filter.accessible: case ServerBrowserFilter.accessible:
return element.password.isNotEmpty; return element.password.isNotEmpty;
case _Filter.playable: case ServerBrowserFilter.playable:
return _gameController.getVersionByGame(element.version) != null; return _gameController.getVersionByGame(element.version) != null;
} }
}).toList(); }).toList();
final sort = _sort.value; final sort = _sort.value;
sorted?.sort((first, second) { sorted?.sort((first, second) {
switch(sort) { switch(sort) {
case _Sort.timeAscending: case ServerBrowserOrder.timeAscending:
return first.timestamp.compareTo(second.timestamp); return first.timestamp.compareTo(second.timestamp);
case _Sort.timeDescending: case ServerBrowserOrder.timeDescending:
return second.timestamp.compareTo(first.timestamp); return second.timestamp.compareTo(first.timestamp);
case _Sort.nameAscending: case ServerBrowserOrder.nameAscending:
return first.name.compareTo(second.name); return first.name.compareTo(second.name);
case _Sort.nameDescending: case ServerBrowserOrder.nameDescending:
return second.name.compareTo(first.name); return second.name.compareTo(first.name);
} }
}); });
@@ -369,7 +379,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
Future<void> _joinServer(String uuid, ServerBrowserEntry server) async { Future<void> _joinServer(String uuid, ServerBrowserEntry server) async {
if(!kDebugMode && uuid == server.id) { if(!kDebugMode && uuid == server.id) {
showRebootInfoBar( SnackBar.open(
translations.joinSelfServer, translations.joinSelfServer,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -379,7 +389,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
final version = _gameController.getVersionByGame(server.version.toString()); final version = _gameController.getVersionByGame(server.version.toString());
if(version == null) { if(version == null) {
showRebootInfoBar( SnackBar.open(
translations.cannotJoinServerVersion(server.version.toString()), translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -407,7 +417,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
} }
if(!checkPassword(confirmPassword, hashedPassword)) { if(!checkPassword(confirmPassword, hashedPassword)) {
showRebootInfoBar( SnackBar.open(
translations.wrongServerPassword, translations.wrongServerPassword,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -425,7 +435,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
} }
Future<bool> _isServerValid(String name, String address) async { Future<bool> _isServerValid(String name, String address) async {
final loadingBar = showRebootInfoBar( final loadingBar = SnackBar.open(
translations.joiningServer(name), translations.joiningServer(name),
duration: infoBarLongDuration, duration: infoBarLongDuration,
loading: true, loading: true,
@@ -438,7 +448,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
return true; return true;
} }
showRebootInfoBar( SnackBar.open(
translations.offlineServer, translations.offlineServer,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -450,7 +460,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
final confirmPasswordController = TextEditingController(); final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false); final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false); final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>( return await Dialog.open<String?>(
builder: (context) => FormDialog( builder: (context) => FormDialog(
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -486,12 +496,12 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
buttons: [ buttons: [
DialogButton( DialogButton(
text: translations.serverPasswordCancel, text: translations.serverPasswordCancel,
type: ButtonType.secondary type: DialogButtonType.secondary
), ),
DialogButton( DialogButton(
text: translations.serverPasswordConfirm, text: translations.serverPasswordConfirm,
type: ButtonType.primary, type: DialogButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text) onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
) )
] ]
@@ -507,7 +517,7 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
FlutterClipboard.controlC(decryptedIp); FlutterClipboard.controlC(decryptedIp);
} }
Get.find<GameController>().selectedVersion.value = version; Get.find<GameController>().selectedVersion.value = version;
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar( WidgetsBinding.instance.addPostFrameCallback((_) => SnackBar.open(
embedded ? translations.joinedServer(author) : translations.copiedIp, embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
@@ -519,41 +529,4 @@ class _BrowsePageState extends AbstractPageState<BrowsePage> {
@override @override
List<Widget> get settings => []; 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/dll_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_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/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/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/message/data.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart'; import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/button/game_start_button.dart'; import 'package:reboot_launcher/src/widget/section/play/state_toggle.dart';
import 'package:reboot_launcher/src/button/version_selector.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey(); final GlobalKey<TutorialOverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey(); final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
class HostPage extends AbstractPage { class HostPage extends AbstractPage {
@@ -100,7 +100,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
), ),
title: Text(translations.hostGameServerNameName), title: Text(translations.hostGameServerNameName),
subtitle: Text(translations.hostGameServerNameDescription), subtitle: Text(translations.hostGameServerNameDescription),
content: OverlayTarget( content: TutorialOverlayTarget(
key: hostInfoNameOverlayTargetKey, key: hostInfoNameOverlayTargetKey,
child: TextFormBox( child: TextFormBox(
placeholder: translations.hostGameServerNameName, placeholder: translations.hostGameServerNameName,
@@ -116,7 +116,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
), ),
title: Text(translations.hostGameServerDescriptionName), title: Text(translations.hostGameServerDescriptionName),
subtitle: Text(translations.hostGameServerDescriptionDescription), subtitle: Text(translations.hostGameServerDescriptionDescription),
content: OverlayTarget( content: TutorialOverlayTarget(
key: hostInfoDescriptionOverlayTargetKey, key: hostInfoDescriptionOverlayTargetKey,
child: TextFormBox( child: TextFormBox(
placeholder: translations.hostGameServerDescriptionName, placeholder: translations.hostGameServerDescriptionName,
@@ -132,7 +132,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
), ),
title: Text(translations.hostGameServerPasswordName), title: Text(translations.hostGameServerPasswordName),
subtitle: Text(translations.hostGameServerPasswordDescription), subtitle: Text(translations.hostGameServerPasswordDescription),
content: Obx(() => OverlayTarget( content: Obx(() => TutorialOverlayTarget(
key: hostInfoPasswordOverlayTargetKey, key: hostInfoPasswordOverlayTargetKey,
child: TextFormBox( child: TextFormBox(
placeholder: translations.hostGameServerPasswordName, placeholder: translations.hostGameServerPasswordName,
@@ -275,7 +275,7 @@ class _HostingPageState extends AbstractPageState<HostPage> {
subtitle: Text(translations.hostShareIpDescription), subtitle: Text(translations.hostShareIpDescription),
content: Button( content: Button(
onPressed: () async { onPressed: () async {
InfoBarEntry? entry; SnackBar? entry;
try { try {
entry = _showCopyingIp(); entry = _showCopyingIp();
final ip = await Ipify.ipv4(); final ip = await Ipify.ipv4();
@@ -317,29 +317,29 @@ class _HostingPageState extends AbstractPageState<HostPage> {
} }
} }
void _showCopiedLink() => showRebootInfoBar( void _showCopiedLink() => SnackBar.open(
translations.hostShareLinkMessageSuccess, translations.hostShareLinkMessageSuccess,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
InfoBarEntry _showCopyingIp() => showRebootInfoBar( SnackBar _showCopyingIp() => SnackBar.open(
translations.hostShareIpMessageLoading, translations.hostShareIpMessageLoading,
loading: true, loading: true,
duration: null duration: null
); );
void _showCopiedIp() => showRebootInfoBar( void _showCopiedIp() => SnackBar.open(
translations.hostShareIpMessageSuccess, translations.hostShareIpMessageSuccess,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
void _showCannotCopyIp(Object error) => showRebootInfoBar( void _showCannotCopyIp(Object error) => SnackBar.open(
translations.hostShareIpMessageError(error.toString()), translations.hostShareIpMessageError(error.toString()),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar( void _showCannotUpdateGameServer(Object error) => SnackBar.open(
translations.cannotUpdateGameServer(error.toString()), translations.cannotUpdateGameServer(error.toString()),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarLongDuration duration: infoBarLongDuration

View File

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

View File

@@ -3,16 +3,16 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_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/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/message/data.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart'; import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/button/game_start_button.dart'; import 'package:reboot_launcher/src/widget/section/play/state_toggle.dart';
import 'package:reboot_launcher/src/button/version_selector.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 { class PlayPage extends AbstractPage {
const PlayPage({Key? key}) : super(key: key); 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/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_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/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/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
@@ -43,8 +45,8 @@ class _LaunchButtonState extends State<LaunchButton> {
final DllController _dllController = Get.find<DllController>(); final DllController _dllController = Get.find<DllController>();
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>(); final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
InfoBarEntry? _gameClientInfoBar; SnackBar? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar; SnackBar? _gameServerInfoBar;
CancelableOperation? _operation; CancelableOperation? _operation;
Completer? _pingOperation; Completer? _pingOperation;
@@ -106,7 +108,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final backendResult = _backendController.started() || await _backendController.toggle( final backendResult = _backendController.started() || await _backendController.toggle(
eventHandler: (type, event) { eventHandler: (type, event) {
_backendController.started.value = event.type.isStart && !event.type.isError; _backendController.started.value = event.type.isStart && !event.type.isError;
if(event.type == AuthBackendResultType.startedImplementation) { if(event.type == AuthBackendEventType.startedImplementation) {
_backendController.implementation = event.implementation; _backendController.implementation = event.implementation;
} }
return onBackendResult(type, event); return onBackendResult(type, event);
@@ -211,16 +213,16 @@ class _LaunchButtonState extends State<LaunchButton> {
return false; return false;
} }
final result = await showRebootDialog<bool>( final result = await Dialog.open<bool>(
builder: (context) => InfoDialog( builder: (context) => InfoDialog(
text: translations.automaticGameServerDialogContent, text: translations.automaticGameServerDialogContent,
buttons: [ buttons: [
DialogButton( DialogButton(
type: ButtonType.secondary, type: DialogButtonType.secondary,
text: translations.automaticGameServerDialogIgnore text: translations.automaticGameServerDialogIgnore
), ),
DialogButton( DialogButton(
type: ButtonType.primary, type: DialogButtonType.primary,
text: translations.automaticGameServerDialogStart, text: translations.automaticGameServerDialogStart,
onTap: () => Navigator.of(context).pop(true), onTap: () => Navigator.of(context).pop(true),
), ),
@@ -469,7 +471,7 @@ class _LaunchButtonState extends State<LaunchButton> {
void _onGameClientInjected() { void _onGameClientInjected() {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
showRebootInfoBar( SnackBar.open(
translations.gameClientStarted, translations.gameClientStarted,
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarLongDuration duration: infoBarLongDuration
@@ -488,7 +490,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final started = await _checkLocalGameServer(gameServerPort); final started = await _checkLocalGameServer(gameServerPort);
if(!started) { if(!started) {
if (_hostingController.instance.value?.killed != true) { if (_hostingController.instance.value?.killed != true) {
showRebootInfoBar( SnackBar.open(
translations.gameServerStartWarning, translations.gameServerStartWarning,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
@@ -500,7 +502,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final accessible = await _checkPublicGameServer(gameServerPort); final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) { if (!accessible) {
showRebootInfoBar( SnackBar.open(
translations.gameServerStartLocalWarning, translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning, severity: InfoBarSeverity.warning,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -514,7 +516,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final serverBrowserEntry = await _hostingController.createServerBrowserEntry(); final serverBrowserEntry = await _hostingController.createServerBrowserEntry();
await _serverBrowserController.addServer(serverBrowserEntry); await _serverBrowserController.addServer(serverBrowserEntry);
showRebootInfoBar( SnackBar.open(
translations.gameServerStarted, translations.gameServerStarted,
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarLongDuration duration: infoBarLongDuration
@@ -526,7 +528,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _checkLocalGameServer(String gameServerPort) async { Future<bool> _checkLocalGameServer(String gameServerPort) async {
try { try {
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = SnackBar.open(
translations.waitingForGameServer, translations.waitingForGameServer,
loading: true, loading: true,
duration: null duration: null
@@ -549,7 +551,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _checkPublicGameServer(String gameServerPort) async { Future<bool> _checkPublicGameServer(String gameServerPort) async {
try { try {
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = SnackBar.open(
translations.checkingGameServer, translations.checkingGameServer,
loading: true, loading: true,
duration: null duration: null
@@ -668,21 +670,21 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.normal: case _StopReason.normal:
break; break;
case _StopReason.missingVersionError: case _StopReason.missingVersionError:
showRebootInfoBar( SnackBar.open(
translations.missingVersionError, translations.missingVersionError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.missingExecutableError: case _StopReason.missingExecutableError:
showRebootInfoBar( SnackBar.open(
translations.missingExecutableError, translations.missingExecutableError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.multipleExecutablesError: case _StopReason.multipleExecutablesError:
showRebootInfoBar( SnackBar.open(
translations.multipleExecutablesError(error ?? translations.unknown), translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -691,7 +693,7 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.exitCode: case _StopReason.exitCode:
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls; final injectedDlls = instance.injectedDlls;
showRebootInfoBar( SnackBar.open(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -700,7 +702,7 @@ class _LaunchButtonState extends State<LaunchButton> {
break; break;
case _StopReason.corruptedVersionError: case _StopReason.corruptedVersionError:
final injectedDlls = instance?.injectedDlls ?? []; final injectedDlls = instance?.injectedDlls ?? [];
showRebootInfoBar( SnackBar.open(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -711,14 +713,14 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.corruptedDllError: case _StopReason.corruptedDllError:
showRebootInfoBar( SnackBar.open(
translations.corruptedDllError(error ?? translations.unknownError), translations.corruptedDllError(error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.missingCustomDllError: case _StopReason.missingCustomDllError:
showRebootInfoBar( SnackBar.open(
translations.missingCustomDllError(error!), translations.missingCustomDllError(error!),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -727,7 +729,7 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.tokenError: case _StopReason.tokenError:
_backendController.stop(); _backendController.stop();
final injectedDlls = instance?.injectedDlls; final injectedDlls = instance?.injectedDlls;
showRebootInfoBar( SnackBar.open(
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -738,21 +740,21 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.crash: case _StopReason.crash:
showRebootInfoBar( SnackBar.open(
translations.fortniteCrashError(host ? translations.gameServer : translations.client), translations.fortniteCrashError(host ? translations.gameServer : translations.client),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.unknownError: case _StopReason.unknownError:
showRebootInfoBar( SnackBar.open(
translations.unknownFortniteError(error ?? translations.unknownError), translations.unknownFortniteError(error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.gameServerPortError: case _StopReason.gameServerPortError:
showRebootInfoBar( SnackBar.open(
translations.gameServerPortEqualsBackendPort(kDefaultBackendPort), translations.gameServerPortEqualsBackendPort(kDefaultBackendPort),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -829,14 +831,14 @@ class _LaunchButtonState extends State<LaunchButton> {
return null; return null;
} }
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar( SnackBar _showLaunchingGameServerWidget() => _gameServerInfoBar = SnackBar.open(
translations.launchingGameServer, translations.launchingGameServer,
loading: true, loading: true,
duration: null duration: null
); );
InfoBarEntry _showLaunchingGameClientWidget(GameVersion version, bool headless, bool linkedHosting) { SnackBar _showLaunchingGameClientWidget(GameVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar( return _gameClientInfoBar = SnackBar.open(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly, linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true, loading: true,
duration: null, 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/l10n/reboot_localizations.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_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/util/translations.dart';
import 'package:reboot_launcher/src/tile/file_setting_tile.dart'; import 'package:reboot_launcher/src/widget/section/settings/file_tile.dart';
import 'package:reboot_launcher/src/tile/setting_tile.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey(); final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
@@ -63,42 +62,42 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
title: Text(translations.settingsClientName), title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription), subtitle: Text(translations.settingsClientDescription),
children: [ children: [
createFileSetting( FileSetting(
key: settingsConsoleDllInputKey, validatorKey: settingsConsoleDllInputKey,
title: translations.settingsClientConsoleName, title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription, description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll, controller: _dllController.unrealEngineConsoleDll,
onReset: () async { onReset: () async {
final path = _dllController.getDefaultDllPath(GameDll.console); final path = _dllController.getDefaultDllPath(GameDll.console);
_dllController.unrealEngineConsoleDll.text = path; _dllController.unrealEngineConsoleDll.text = path;
await _dllController.download(GameDll.console, path, force: true); await _dllController.download(GameDll.console, path, force: true);
settingsConsoleDllInputKey.currentState?.validate(); settingsConsoleDllInputKey.currentState?.validate();
} },
), ),
createFileSetting( FileSetting(
key: settingsAuthDllInputKey, validatorKey: settingsAuthDllInputKey,
title: translations.settingsClientAuthName, title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription, description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll, controller: _dllController.backendDll,
onReset: () async { onReset: () async {
final path = _dllController.getDefaultDllPath(GameDll.auth); final path = _dllController.getDefaultDllPath(GameDll.auth);
_dllController.backendDll.text = path; _dllController.backendDll.text = path;
await _dllController.download(GameDll.auth, path, force: true); await _dllController.download(GameDll.auth, path, force: true);
settingsAuthDllInputKey.currentState?.validate(); settingsAuthDllInputKey.currentState?.validate();
} },
), ),
createFileSetting( FileSetting(
key: settingsMemoryDllInputKey, validatorKey: settingsMemoryDllInputKey,
title: translations.settingsClientMemoryName, title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription, description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll, controller: _dllController.memoryLeakDll,
onReset: () async { onReset: () async {
final path = _dllController.getDefaultDllPath(GameDll.memoryLeak); final path = _dllController.getDefaultDllPath(GameDll.memoryLeak);
_dllController.memoryLeakDll.text = path; _dllController.memoryLeakDll.text = path;
await _dllController.download(GameDll.memoryLeak, path, force: true); await _dllController.download(GameDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate(); settingsAuthDllInputKey.currentState?.validate();
} },
), ),
_gameServer _gameServer
], ],
); );
@@ -124,8 +123,6 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
subtitle: Text(translations.settingsServerTypeDescription), subtitle: Text(translations.settingsServerTypeDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30, contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: { items: {
false: translations.settingsServerTypeEmbeddedName, false: translations.settingsServerTypeEmbeddedName,
@@ -200,18 +197,18 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
) )
); );
}else { }else {
return createFileSetting( return FileSetting(
key: settingsGameServerDllInputKey, validatorKey: settingsGameServerDllInputKey,
title: translations.settingsOldServerFileName, title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription, description: translations.settingsServerFileDescription,
controller: _dllController.customGameServerDll, controller: _dllController.customGameServerDll,
onReset: () async { onReset: () async {
final path = _dllController.getDefaultDllPath(GameDll.gameServer); final path = _dllController.getDefaultDllPath(GameDll.gameServer);
_dllController.customGameServerDll.text = path; _dllController.customGameServerDll.text = path;
await _dllController.download(GameDll.gameServer, path); await _dllController.download(GameDll.gameServer, path);
settingsGameServerDllInputKey.currentState?.validate(); settingsGameServerDllInputKey.currentState?.validate();
} },
); );
} }
}); });
@@ -295,8 +292,6 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
title: Text(translations.settingsUtilsLanguageName), title: Text(translations.settingsUtilsLanguageName),
subtitle: Text(translations.settingsUtilsLanguageDescription), subtitle: Text(translations.settingsUtilsLanguageDescription),
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_getLocaleName(_settingsController.language.value)), leading: Text(_getLocaleName(_settingsController.language.value)),
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem( items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
text: Text(_getLocaleName(locale.languageCode)), text: Text(_getLocaleName(locale.languageCode)),
@@ -321,8 +316,6 @@ class _SettingsPageState extends AbstractPageState<SettingsPage> {
title: Text(translations.settingsUtilsThemeName), title: Text(translations.settingsUtilsThemeName),
subtitle: Text(translations.settingsUtilsThemeDescription), subtitle: Text(translations.settingsUtilsThemeDescription),
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.themeMode.value.title), leading: Text(_settingsController.themeMode.value.title),
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem( items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
text: Text(themeMode.title), text: Text(themeMode.title),

View File

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

View File

@@ -3,16 +3,16 @@ import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/pager/page_type.dart'; import 'package:reboot_launcher/src/widget/section/host/page.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/widget/section/info/page.dart';
import 'package:reboot_launcher/src/page/backend_page.dart'; import 'package:reboot_launcher/src/widget/section/settings/page.dart';
import 'package:reboot_launcher/src/page/browser_page.dart'; import 'package:reboot_launcher/src/widget/snackbar/snackbar_area.dart';
import 'package:reboot_launcher/src/page/host_page.dart'; import 'package:reboot_launcher/src/widget/page/page_type.dart';
import 'package:reboot_launcher/src/page/info_page.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:reboot_launcher/src/pager/abstract_page.dart'; import 'package:reboot_launcher/src/widget/section/backend/page.dart';
import 'package:reboot_launcher/src/page/play_page.dart'; import 'package:reboot_launcher/src/widget/section/browser/page.dart';
import 'package:reboot_launcher/src/page/settings_page.dart'; import 'package:reboot_launcher/src/widget/page/abstract_page.dart';
import 'package:reboot_launcher/src/messenger/info_bar_area.dart'; import 'package:reboot_launcher/src/widget/section/play/page.dart';
final StreamController<void> pagesController = StreamController.broadcast(); final StreamController<void> pagesController = StreamController.broadcast();
bool hitBack = false; bool hitBack = false;
@@ -26,7 +26,7 @@ final List<AbstractPage> pages = [
const SettingsPage() 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); final RxInt pageIndex = RxInt(PageType.play.index);
@@ -36,7 +36,7 @@ final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey();
final GlobalKey<OverlayState> appOverlayKey = GlobalKey(); final GlobalKey<OverlayState> appOverlayKey = GlobalKey();
final GlobalKey<InfoBarAreaState> infoBarAreaKey = GlobalKey(); final GlobalKey<SnackBarAreaState> infoBarAreaKey = GlobalKey();
GlobalKey get pageKey => getPageKeyByIndex(pageIndex.value); GlobalKey get pageKey => getPageKeyByIndex(pageIndex.value);
@@ -82,6 +82,6 @@ void addSubPageToCurrent(String pageName) {
pagesController.add(null); 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:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/widget/sections.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/widget/tutorial/tutorial_overlay.dart';
import 'package:skeletons/skeletons.dart'; import 'package:skeletons/skeletons.dart';
class SettingTile extends StatefulWidget { class SettingTile extends StatefulWidget {
@@ -78,7 +78,7 @@ class SettingTileState extends State<SettingTile> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if(widget.overlayKey != null) if(widget.overlayKey != null)
OverlayTarget( TutorialOverlayTarget(
key: widget.overlayKey, key: widget.overlayKey,
child: isSkeleton ? _skeletonIcon : icon, 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:fluent_ui/fluent_ui.dart';
import 'package:flutter/rendering.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()); typedef WidgetBuilder = Widget Function(BuildContext, void Function());
class OverlayTarget extends StatefulWidget { class TutorialOverlayTarget extends StatefulWidget {
final Widget child; final Widget child;
const OverlayTarget({super.key, required this.child}); const TutorialOverlayTarget({super.key, required this.child});
@override @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 @override
Widget build(BuildContext context) => widget.child; Widget build(BuildContext context) => widget.child;
@@ -23,7 +25,7 @@ class OverlayTargetState extends State<OverlayTarget> {
required WidgetBuilder actionBuilder, required WidgetBuilder actionBuilder,
Offset offset = Offset.zero, Offset offset = Offset.zero,
bool ignoreTargetPointers = true, bool ignoreTargetPointers = true,
AttachMode attachMode = AttachMode.start TutorialOverlayAttachMode attachMode = TutorialOverlayAttachMode.start
}) { }) {
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
final position = renderBox.localToGlobal(Offset.zero); final position = renderBox.localToGlobal(Offset.zero);
@@ -40,10 +42,10 @@ class OverlayTargetState extends State<OverlayTarget> {
) )
), ),
Positioned( 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, top: position.dy + (renderBox.size.height / 2) + offset.dy,
child: CustomPaint( child: CustomPaint(
painter: _CallOutShape(color, attachMode != AttachMode.start), painter: _CallOutShape(color, attachMode != TutorialOverlayAttachMode.start),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
@@ -65,12 +67,6 @@ class OverlayTargetState extends State<OverlayTarget> {
} }
} }
enum AttachMode {
start,
middle,
end;
}
// Harder than one would think // Harder than one would think
class _CallOutShape extends CustomPainter { class _CallOutShape extends CustomPainter {
final Color color; 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_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/translations.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/messenger/dialog.dart'; import 'package:reboot_launcher/src/widget/overlay/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/widget/snackbar/snackbar.dart';
import 'package:reboot_launcher/src/messenger/overlay.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/download_version.dart';
import 'package:reboot_launcher/src/message/import_version.dart'; import 'package:reboot_launcher/src/message/import_version.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -19,7 +19,7 @@ class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key); const VersionSelector({Key? key}) : super(key: key);
static SettingTile buildTile({ static SettingTile buildTile({
required GlobalKey<OverlayTargetState> key required GlobalKey<TutorialOverlayTargetState> key
}) => SettingTile( }) => SettingTile(
icon: Icon( icon: Icon(
FluentIcons.play_24_regular FluentIcons.play_24_regular
@@ -221,7 +221,7 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
bool _onExplorerError() { bool _onExplorerError() {
showRebootInfoBar( SnackBar.open(
translations.missingVersionError, translations.missingVersionError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,