<feat: New release>

This commit is contained in:
Alessandro Autiero
2023-09-09 12:46:16 +02:00
parent badf41b044
commit 485e757e83
424 changed files with 37224 additions and 818815 deletions

View File

@@ -1,13 +1,13 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:reboot_launcher/src/dialog/message.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'dialog_button.dart';
Future<T?> showDialog<T extends Object?>({required WidgetBuilder builder}) => fluent.showDialog(
context: pageKey.currentContext!,
Future<T?> showAppDialog<T extends Object?>({required WidgetBuilder builder}) => fluent.showDialog(
context: appKey.currentContext!,
useRootNavigator: false,
builder: builder
);
@@ -24,7 +24,7 @@ class GenericDialog extends AbstractDialog {
final List<DialogButton> buttons;
final EdgeInsets? padding;
const GenericDialog({super.key, required this.header, required this.buttons, this.padding});
const GenericDialog({Key? key, required this.header, required this.buttons, this.padding}) : super(key: key);
@override
Widget build(BuildContext context) => ContentDialog(
@@ -40,7 +40,7 @@ class FormDialog extends AbstractDialog {
final Widget content;
final List<DialogButton> buttons;
const FormDialog({super.key, required this.content, required this.buttons});
const FormDialog({Key? key, required this.content, required this.buttons}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -80,10 +80,10 @@ class InfoDialog extends AbstractDialog {
final String text;
final List<DialogButton>? buttons;
const InfoDialog({required this.text, this.buttons, super.key});
const InfoDialog({required this.text, this.buttons, Key? key}) : super(key: key);
InfoDialog.ofOnly({required this.text, required DialogButton button, super.key})
: buttons = [button];
InfoDialog.ofOnly({required this.text, required DialogButton button, Key? key})
: buttons = [button], super(key: key);
@override
Widget build(BuildContext context) {
@@ -109,7 +109,7 @@ class ProgressDialog extends AbstractDialog {
final String text;
final Function()? onStop;
const ProgressDialog({required this.text, this.onStop, super.key});
const ProgressDialog({required this.text, this.onStop, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -143,14 +143,14 @@ class FutureBuilderDialog extends AbstractDialog {
final bool closeAutomatically;
const FutureBuilderDialog(
{super.key,
{Key? key,
required this.future,
required this.loadingMessage,
required this.successfulBody,
required this.unsuccessfulBody,
required this.errorMessageBuilder,
this.onError,
this.closeAutomatically = false});
this.closeAutomatically = false}) : super(key: key);
static Container ofMessage(String message) {
return Container(
@@ -223,14 +223,14 @@ class ErrorDialog extends AbstractDialog {
final StackTrace? stackTrace;
final Function(Object) errorMessageBuilder;
const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace});
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: "Copy error",
type: type,
onTap: () async {
FlutterClipboard.controlC("An error occurred: $error\nStacktrace:\n $stackTrace");
showMessage("Copied error to clipboard");
showInfoBar("Copied error to clipboard");
onClick();
},
);

View File

@@ -1,4 +1,4 @@
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
import 'package:fluent_ui/fluent_ui.dart';
class DialogButton extends StatefulWidget {
final String? text;

View File

@@ -0,0 +1,80 @@
import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:sync/semaphore.dart';
Semaphore _semaphore = Semaphore();
HashMap<int, OverlayEntry?> _overlays = HashMap();
void restoreMessage(int lastIndex) {
removeMessage(lastIndex);
var overlay = _overlays[pageIndex.value];
if(overlay == null) {
return;
}
Overlay.of(pageKey.currentContext!).insert(overlay);
}
void showInfoBar(String text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) {
try {
_semaphore.acquire();
var index = pageIndex.value;
removeMessage(index);
var overlay = showSnackbar(
pageKey.currentContext!,
SizedBox(
width: double.infinity,
child: Mica(
child: InfoBar(
title: Text(text),
isLong: action == null,
isIconVisible: true,
content: action ?? SizedBox(
width: double.infinity,
child: loading ? const ProgressBar() : const SizedBox()
),
severity: severity
),
),
),
margin: EdgeInsets.only(
right: 12.0,
left: 12.0,
bottom: index == 0 || index == 1 || index == 3 || index == 4 ? 72.0 : 16.0
),
duration: duration
);
_overlays[index] = overlay;
if(duration != null) {
Future.delayed(duration).then((_) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if(_overlays[index] == overlay) {
if(overlay.mounted) {
overlay.remove();
}
_overlays[index] = null;
}
});
});
}
}finally {
_semaphore.release();
}
}
void removeMessage(int index) {
try {
var lastOverlay = _overlays[index];
if(lastOverlay != null) {
lastOverlay.remove();
_overlays[index] = null;
}
}catch(_) {
// Do not use .isMounted
// This is intended behaviour
}
}

View File

@@ -0,0 +1,36 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
String? lastError;
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
if(exception == null){
return;
}
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
return;
}
if(lastError == exception.toString()){
return;
}
lastError = exception.toString();
var route = ModalRoute.of(pageKey.currentContext!);
if(route != null && !route.isCurrent){
Navigator.of(pageKey.currentContext!).pop(false);
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showAppDialog(
builder: (context) =>
ErrorDialog(
exception: exception,
stackTrace: stackTrace,
errorMessageBuilder: (exception) => framework ? "An error was thrown by Flutter: $exception" : "An uncaught error was thrown: $exception"
)
));
}

View File

@@ -0,0 +1,69 @@
import 'package:reboot_common/common.dart';
import '../abstract/dialog.dart';
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
"If you are unsure which version works best, use build 7.40. "
"If you are a passionate programmer you can add support by opening a PR on Github. ";
const String _corruptedBuildError = "An unknown occurred while launching Fortnite. "
"Some critical files could be missing in your installation. "
"Download the build again from the launcher, not locally, or from a different source. "
"Alternatively, something could have gone wrong in the launcher. ";
Future<void> showMissingDllError(String name) async {
showAppDialog(
builder: (context) => InfoDialog(
text: "$name dll is not a valid dll, fix it in the settings tab"
)
);
}
Future<void> showTokenErrorFixable() async {
showAppDialog(
builder: (context) => const InfoDialog(
text: "A token error occurred. "
"The backend server has been automatically restarted to fix the issue. "
"The game has been restarted automatically. "
)
);
}
Future<void> showTokenErrorUnfixable() async {
showAppDialog(
builder: (context) => const InfoDialog(
text: "A token error occurred. "
"This issue cannot be resolved automatically as the server isn't embedded."
"Please restart the server manually, then relaunch your game to check if the issue has been fixed. "
"Otherwise, open an issue on Discord."
)
);
}
Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async {
if(error == null) {
showAppDialog(
builder: (context) => InfoDialog(
text: server ? _unsupportedServerError : _corruptedBuildError
)
);
return;
}
showAppDialog(
builder: (context) => ErrorDialog(
exception: error,
stackTrace: stackTrace,
errorMessageBuilder: (exception) => "${_corruptedBuildError}Error message: $exception"
)
);
}
Future<void> showMissingBuildError(FortniteVersion version) async {
showAppDialog(
builder: (context) => InfoDialog(
text: "${version.location.path} no longer contains a Fortnite executable. "
"This probably means that you deleted it or move it somewhere else."
)
);
}

View File

@@ -0,0 +1,83 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
final GameController _gameController = Get.find<GameController>();
Future<bool> showProfileForm(BuildContext context) async{
var showPassword = RxBool(false);
var oldUsername = _gameController.username.text;
var showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
var oldPassword = _gameController.password.text;
var result = await showAppDialog<bool?>(
builder: (context) => Obx(() => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: "Username/Email",
child: TextFormBox(
placeholder: "Type your username or email",
controller: _gameController.username,
autovalidateMode: AutovalidateMode.always,
enableSuggestions: true,
autofocus: true,
autocorrect: false,
)
),
const SizedBox(height: 16.0),
InfoLabel(
label: "Password",
child: TextFormBox(
placeholder: "Type your password, if you have one",
controller: _gameController.password,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
showPassword.value ? Icons.visibility_off : Icons.visibility,
color: showPasswordTrailing.value ? null : Colors.transparent
),
)
)
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: "Cancel",
type: ButtonType.secondary
),
DialogButton(
text: "Save",
type: ButtonType.primary,
onTap: () {
Navigator.of(context).pop(true);
}
)
]
))
) ?? false;
if(result) {
return true;
}
_gameController.username.text = oldUsername;
_gameController.password.text = oldPassword;
return false;
}

View File

@@ -0,0 +1,288 @@
import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
extension ServerControllerDialog on ServerController {
Future<bool> restartInteractive() async {
var stream = restart();
return await _handleStream(stream, false);
}
Future<bool> toggleInteractive([bool showSuccessMessage = true]) async {
var stream = toggle();
return await _handleStream(stream, showSuccessMessage);
}
Future<bool> _handleStream(Stream<ServerResult> stream, bool showSuccessMessage) async {
var completer = Completer<bool>();
stream.listen((event) {
switch (event.type) {
case ServerResultType.starting:
showInfoBar(
"Starting the $controllerName...",
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
break;
case ServerResultType.startSuccess:
if(showSuccessMessage) {
showInfoBar(
"The $controllerName was started successfully",
severity: InfoBarSeverity.success
);
}
completer.complete(true);
break;
case ServerResultType.startError:
showInfoBar(
"An error occurred while starting the $controllerName: ${event.error ?? "unknown error"}",
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
);
break;
case ServerResultType.stopping:
showInfoBar(
"Stopping the $controllerName...",
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
break;
case ServerResultType.stopSuccess:
if(showSuccessMessage) {
showInfoBar(
"The $controllerName was stopped successfully",
severity: InfoBarSeverity.success
);
}
completer.complete(true);
break;
case ServerResultType.stopError:
showInfoBar(
"An error occurred while stopping the $controllerName: ${event.error ?? "unknown error"}",
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
);
break;
case ServerResultType.missingHostError:
showInfoBar(
"Missing hostname in $controllerName configuration",
severity: InfoBarSeverity.error
);
break;
case ServerResultType.missingPortError:
showInfoBar(
"Missing port in $controllerName configuration",
severity: InfoBarSeverity.error
);
break;
case ServerResultType.illegalPortError:
showInfoBar(
"Invalid port in $controllerName configuration",
severity: InfoBarSeverity.error
);
break;
case ServerResultType.freeingPort:
showInfoBar(
"Freeing port $defaultPort...",
loading: true,
duration: null
);
break;
case ServerResultType.freePortSuccess:
showInfoBar(
"Port $defaultPort was freed successfully",
severity: InfoBarSeverity.success,
duration: snackbarShortDuration
);
break;
case ServerResultType.freePortError:
showInfoBar(
"An error occurred while freeing port $defaultPort: ${event.error ?? "unknown error"}",
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
);
break;
case ServerResultType.pingingRemote:
if(started.value) {
showInfoBar(
"Pinging the remote $controllerName...",
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}
break;
case ServerResultType.pingingLocal:
if(started.value) {
showInfoBar(
"Pinging the ${type().name} $controllerName...",
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}
break;
case ServerResultType.pingError:
showInfoBar(
"Cannot ping ${type().name} $controllerName",
severity: InfoBarSeverity.error
);
break;
}
if(event.type.isError) {
completer.complete(false);
}
});
var result = await completer.future;
if(result && type() == ServerType.embedded) {
watchProcess(embeddedServerPid!).then((value) {
if(started()) {
started.value = false;
}
});
}
return result;
}
}
extension MatchmakerControllerExtension on MatchmakerController {
Future<void> joinServer(Map<String, dynamic> entry) async {
var hashedPassword = entry["password"];
var hasPassword = hashedPassword != null;
var embedded = type.value == ServerType.embedded;
var author = entry["author"];
var encryptedIp = entry["ip"];
if(!hasPassword) {
var valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onSuccess(embedded, encryptedIp, author);
return;
}
var confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
showInfoBar(
"Wrong password: please try again",
duration: snackbarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
var decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
var valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onSuccess(embedded, decryptedIp, author);
}
Future<bool> _isServerValid(String address) async {
var result = await pingGameServer(address);
if(result) {
return true;
}
showInfoBar(
"This server isn't online right now: please try again later",
duration: snackbarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
var confirmPasswordController = TextEditingController();
var showPassword = RxBool(false);
var showPasswordTrailing = RxBool(false);
return await showAppDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: "Password",
child: Obx(() => TextFormBox(
placeholder: "Type the server's password",
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: Button(
onPressed: () => showPasswordTrailing.value = !showPasswordTrailing.value,
style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
showPassword.value ? Icons.visibility_off : Icons.visibility,
color: showPassword.value ? null : Colors.transparent
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: "Cancel",
type: ButtonType.secondary
),
DialogButton(
text: "Confirm",
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
);
}
void _onSuccess(bool embedded, String decryptedIp, String author) {
if(embedded) {
gameServerAddress.text = decryptedIp;
pageIndex.value = 0;
}else {
FlutterClipboard.controlC(decryptedIp);
}
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
embedded ? "You joined $author's server successfully!" : "Copied IP to the clipboard",
duration: snackbarLongDuration,
severity: InfoBarSeverity.success
));
}
}

View File

@@ -1,38 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart' hide showDialog;
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:sync/semaphore.dart';
Semaphore _semaphore = Semaphore();
OverlayEntry? _lastOverlay;
void showMessage(String text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration}) {
try {
_semaphore.acquire();
if(_lastOverlay?.mounted == true) {
_lastOverlay?.remove();
}
var pageIndexValue = pageIndex.value;
_lastOverlay = showSnackbar(
pageKey.currentContext!,
InfoBar(
title: Text(text),
isLong: true,
isIconVisible: true,
content: SizedBox(
width: double.infinity,
child: loading ? const ProgressBar() : const SizedBox()
),
severity: severity
),
margin: EdgeInsets.only(
left: 330.0,
right: 16.0,
bottom: pageIndexValue == 0 || pageIndexValue == 1 || pageIndexValue == 3 || pageIndexValue == 4 ? 72 : 16
),
duration: duration
);
}finally {
_semaphore.release();
}
}