This commit is contained in:
Alessandro Autiero
2024-05-20 17:24:00 +02:00
parent 7c2caed16c
commit 9f5590d41c
562 changed files with 3303 additions and 156787 deletions

View File

@@ -16,7 +16,7 @@ class AuthenticatorController extends ServerController {
String get defaultHost => kDefaultAuthenticatorHost;
@override
String get defaultPort => kDefaultAuthenticatorPort;
String get defaultPort => kDefaultAuthenticatorPort.toString();
@override
Future<bool> get isPortFree => isAuthenticatorPortFree();

View File

@@ -3,17 +3,38 @@ import 'package:reboot_common/common.dart';
class BuildController extends GetxController {
List<FortniteBuild>? _builds;
Rxn<FortniteBuild> selectedBuild;
Rxn<FortniteBuild> _selectedBuild;
Rx<FortniteBuildSource> _selectedBuildSource;
BuildController() : selectedBuild = Rxn();
BuildController() : _selectedBuild = Rxn(),
_selectedBuildSource = Rx(FortniteBuildSource.manifest);
List<FortniteBuild>? get builds => _builds;
FortniteBuild? get selectedBuild => _selectedBuild.value;
set selectedBuild(FortniteBuild? value) {
_selectedBuild.value = value;
if(value != null && value.source != value.source) {
_selectedBuildSource.value = value.source;
}
}
FortniteBuildSource get selectedBuildSource => _selectedBuildSource.value;
set selectedBuildSource(FortniteBuildSource value) {
_selectedBuildSource.value = value;
final selected = selectedBuild;
if(selected == null || selected.source != value) {
final selectable = builds?.firstWhereOrNull((element) => element.source == value);
_selectedBuild.value = selectable;
}
}
set builds(List<FortniteBuild>? builds) {
_builds = builds;
if(builds == null || builds.isEmpty){
return;
}
selectedBuild.value = builds[0];
final selectable = builds?.firstWhereOrNull((element) => element.source == selectedBuildSource);
_selectedBuild.value = selectable;
}
}

View File

@@ -39,14 +39,9 @@ class GameController extends GetxController {
customLaunchArgs.addListener(() =>
_storage.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false);
var serializedInstance = _storage.read("instance");
instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null);
instance.listen((_) => saveInstance());
instance = Rxn();
}
Future<void> saveInstance() =>
_storage.write("instance", jsonEncode(instance.value?.toJson()));
void reset() {
username.text = kDefaultPlayerName;
password.text = "";

View File

@@ -1,10 +1,7 @@
import 'dart:convert';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
@@ -16,8 +13,10 @@ class HostingController extends GetxController {
late final TextEditingController password;
late final RxBool showPassword;
late final RxBool discoverable;
late final RxBool headless;
late final RxBool started;
late final RxBool published;
late final RxBool automaticServer;
late final Rxn<GameInstance> instance;
late final Rxn<Set<Map<String, dynamic>>> servers;
@@ -33,24 +32,27 @@ class HostingController extends GetxController {
password.addListener(() => _storage.write("password", password.text));
discoverable = RxBool(_storage.read("discoverable") ?? true);
discoverable.listen((value) => _storage.write("discoverable", value));
headless = RxBool(_storage.read("headless") ?? true);
headless.listen((value) => _storage.write("headless", value));
started = RxBool(false);
published = RxBool(false);
showPassword = RxBool(false);
var serializedInstance = _storage.read("instance");
instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null);
instance.listen((_) => saveInstance());
var supabase = Supabase.instance.client;
instance = Rxn();
automaticServer = RxBool(_storage.read("auto") ?? true);
automaticServer.listen((value) => _storage.write("auto", value));
final supabase = Supabase.instance.client;
servers = Rxn();
supabase.from('hosts')
supabase.from("hosting")
.stream(primaryKey: ['id'])
.map((event) => _parseValidServers(event))
.listen((event) => servers.value = event);
.listen((event) {
servers.value = event;
published.value = event.any((element) => element["id"] == uuid);
});
}
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet();
Future<void> saveInstance() => _storage.write("instance", jsonEncode(instance.value?.toJson()));
void reset() {
name.text = "";
description.text = "";

View File

@@ -1,5 +1,4 @@
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
class InfoController extends GetxController {
List<String>? links;

View File

@@ -45,7 +45,7 @@ class MatchmakerController extends ServerController {
String get defaultHost => kDefaultMatchmakerHost;
@override
String get defaultPort => kDefaultMatchmakerPort;
String get defaultPort => kDefaultMatchmakerPort.toString();
@override
Future<bool> get isPortFree => isMatchmakerPortFree();

View File

@@ -17,7 +17,6 @@ abstract class ServerController extends GetxController {
late RxBool started;
late RxBool detached;
StreamSubscription? worker;
int? embeddedServerPid;
HttpServer? localServer;
HttpServer? remoteServer;
@@ -93,8 +92,16 @@ abstract class ServerController extends GetxController {
return;
}
yield ServerResult(ServerResultType.starting);
started.value = true;
if(type() != ServerType.local) {
started.value = true;
yield ServerResult(ServerResultType.starting);
}else {
started.value = false;
if(port != defaultPort) {
yield ServerResult(ServerResultType.starting);
}
}
try {
var host = this.host.text.trim();
if (host.isEmpty) {
@@ -117,7 +124,7 @@ abstract class ServerController extends GetxController {
return;
}
if (type() != ServerType.local && await isPortTaken) {
if ((type() != ServerType.local || port != defaultPort) && await isPortTaken) {
yield ServerResult(ServerResultType.freeingPort);
var result = await freePort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
@@ -126,9 +133,15 @@ abstract class ServerController extends GetxController {
return;
}
}
switch(type()){
case ServerType.embedded:
embeddedServerPid = await startEmbeddedInternal();
final pid = await startEmbeddedInternal();
watchProcess(pid).then((value) {
if(started()) {
started.value = false;
}
});
break;
case ServerType.remote:
yield ServerResult(ServerResultType.pingingRemote);
@@ -143,7 +156,7 @@ abstract class ServerController extends GetxController {
break;
case ServerType.local:
if(port != defaultPort) {
localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$defaultPort"));
localServer = await startRemoteAuthenticatorProxy(Uri.parse("http://$defaultHost:$port"));
}
break;
@@ -153,6 +166,8 @@ abstract class ServerController extends GetxController {
var uriResult = await pingServer(defaultHost, defaultPort);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
return;
}
@@ -164,6 +179,8 @@ abstract class ServerController extends GetxController {
error: error,
stackTrace: stackTrace
);
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
}
}
@@ -178,7 +195,7 @@ abstract class ServerController extends GetxController {
try{
switch(type()){
case ServerType.embedded:
Process.killPid(embeddedServerPid!, ProcessSignal.sigabrt);
killProcessByPort(int.parse(defaultPort));
break;
case ServerType.remote:
await remoteServer?.close(force: true);

View File

@@ -1,9 +1,6 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/translations.dart';

View File

@@ -17,36 +17,36 @@ class UpdateController {
timestamp = RxnInt(_storage.read("ts"));
timestamp.listen((value) => _storage.write("ts", value));
var timerIndex = _storage.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.never : UpdateTimer.values.elementAt(timerIndex));
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage.write("timer", value.index));
url = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
url = TextEditingController(text: _storage.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
}
Future<void> update([bool force = false]) async {
if(timer.value == UpdateTimer.never) {
status.value = UpdateStatus.success;
return;
}
showInfoBar(
translations.updatingRebootDll,
loading: true,
duration: null
);
try {
timestamp.value = await downloadRebootDll(
url.text,
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return;
}
showInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
showInfoBar(
translations.updatedRebootDll,
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: snackbarShortDuration
duration: infoBarShortDuration
);
}catch(message) {
var error = message.toString();
@@ -54,12 +54,12 @@ class UpdateController {
error = error.toLowerCase();
status.value = UpdateStatus.error;
showInfoBar(
translations.updateRebootDllError(error.toString()),
duration: snackbarLongDuration,
translations.downloadDllError(error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => update(true),
child: Text(translations.updateRebootDllErrorAction),
child: Text(translations.downloadDllRetry),
)
);
}
@@ -68,7 +68,7 @@ class UpdateController {
void reset() {
timestamp.value = null;
timer.value = UpdateTimer.never;
url.text = rebootDownloadUrl;
url.text = kRebootDownloadUrl;
status.value = UpdateStatus.waiting;
update();
}

View File

@@ -1,17 +1,27 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'dialog_button.dart';
Future<T?> showAppDialog<T extends Object?>({required WidgetBuilder builder}) => fluent.showDialog(
context: appKey.currentContext!,
useRootNavigator: false,
builder: builder
);
bool inDialog = false;
Future<T?> showAppDialog<T extends Object?>({required WidgetBuilder builder}) async {
inDialog = true;
pagesController.add(null);
try {
return await fluent.showDialog(
context: appKey.currentContext!,
useRootNavigator: false,
builder: builder
);
}finally {
inDialog = false;
}
}
abstract class AbstractDialog extends StatelessWidget {
const AbstractDialog({Key? key}) : super(key: key);

View File

@@ -1,39 +1,38 @@
import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:sync/semaphore.dart';
const infoBarLongDuration = Duration(seconds: 4);
const infoBarShortDuration = Duration(seconds: 2);
Semaphore _semaphore = Semaphore();
HashMap<int, OverlayEntry?> _overlays = HashMap();
HashMap<int, _OverlayEntry> _overlays = HashMap();
void restoreMessage(int pageIndex, int lastIndex) {
removeMessageByPage(lastIndex);
var overlay = _overlays[pageIndex];
if(overlay == null) {
final entry = _overlays[pageIndex];
if(entry == null) {
return;
}
Overlay.of(pageKey.currentContext!).insert(overlay);
Overlay.of(pageKey.currentContext!).insert(entry.overlay);
}
OverlayEntry showInfoBar(dynamic text,
{RebootPageType? pageType,
InfoBarSeverity severity = InfoBarSeverity.info,
{InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false,
Duration? duration = snackbarShortDuration,
Duration? duration = infoBarShortDuration,
void Function()? onDismissed,
Widget? action}) {
try {
_semaphore.acquire();
var index = pageType?.index ?? pageIndex.value;
removeMessageByPage(index);
var overlay = OverlayEntry(
removeMessageByPage(pageIndex.value);
final overlay = OverlayEntry(
builder: (context) => Padding(
padding: EdgeInsets.only(
right: 12.0,
left: 12.0,
bottom: pagesWithButtonIndexes.contains(index) ? 72.0 : 16.0
bottom: hasPageButton ? 72.0 : 16.0
),
child: Align(
alignment: AlignmentDirectional.bottomCenter,
@@ -71,19 +70,22 @@ OverlayEntry showInfoBar(dynamic text,
),
)
);
if(index == pageIndex.value) {
Overlay.of(pageKey.currentContext!).insert(overlay);
}
_overlays[index] = overlay;
Overlay.of(pageKey.currentContext!).insert(overlay);
_overlays[pageIndex.value] = _OverlayEntry(
overlay: overlay,
onDismissed: onDismissed
);
if(duration != null) {
Future.delayed(duration).then((_) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if(_overlays[index] == overlay) {
final currentOverlay = _overlays[pageIndex.value];
if(currentOverlay == overlay) {
if(overlay.mounted) {
overlay.remove();
}
_overlays[index] = null;
_overlays.remove(pageIndex.value);
currentOverlay?.onDismissed?.call();
}
});
});
@@ -95,20 +97,23 @@ OverlayEntry showInfoBar(dynamic text,
}
void removeMessageByPage(int index) {
var lastOverlay = _overlays[index];
final lastOverlay = _overlays[index];
if(lastOverlay != null) {
removeMessageByOverlay(lastOverlay);
_overlays[index] = null;
try {
lastOverlay.overlay.remove();
}catch(_) {
// Do not use .isMounted
// This is intended behaviour
}finally {
_overlays.remove(index);
lastOverlay.onDismissed?.call();
}
}
}
void removeMessageByOverlay(OverlayEntry? overlay) {
try {
if(overlay != null) {
overlay.remove();
}
}catch(_) {
// Do not use .isMounted
// This is intended behaviour
}
class _OverlayEntry {
final OverlayEntry overlay;
final void Function()? onDismissed;
_OverlayEntry({required this.overlay, required this.onDismissed});
}

View File

@@ -0,0 +1,24 @@
import 'package:fluent_ui/fluent_ui.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/util/translations.dart';
Future<void> showDllDeletedDialog(Function() onConfirm) => showAppDialog(
builder: (context) => InfoDialog(
text: translations.dllDeletedTitle,
buttons: [
DialogButton(
type: ButtonType.secondary,
text: translations.dllDeletedSecondaryAction,
),
DialogButton(
type: ButtonType.secondary,
text: translations.dllDeletedPrimaryAction,
onTap: () {
Navigator.pop(context);
onConfirm();
},
),
],
)
);

View File

@@ -2,8 +2,8 @@ import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
@@ -12,27 +12,22 @@ 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_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:sync/semaphore.dart';
extension ServerControllerDialog on ServerController {
Future<bool> toggleInteractive(RebootPageType caller, [bool showSuccessMessage = true]) async {
Future<bool> toggleInteractive([bool showSuccessMessage = true]) async {
var stream = toggle();
return await _handleStream(caller, stream, showSuccessMessage);
}
Future<bool> _handleStream(RebootPageType caller, Stream<ServerResult> stream, bool showSuccessMessage) async {
var completer = Completer<bool>();
worker = stream.listen((event) {
switch (event.type) {
case ServerResultType.starting:
showInfoBar(
translations.startingServer(controllerName),
pageType: caller,
severity: InfoBarSeverity.info,
loading: true,
duration: null
@@ -41,8 +36,7 @@ extension ServerControllerDialog on ServerController {
case ServerResultType.startSuccess:
if(showSuccessMessage) {
showInfoBar(
translations.startedServer(controllerName),
pageType: caller,
type.value == ServerType.local ? translations.checkedServer(controllerName) : translations.startedServer(controllerName),
severity: InfoBarSeverity.success
);
}
@@ -50,17 +44,14 @@ extension ServerControllerDialog on ServerController {
break;
case ServerResultType.startError:
showInfoBar(
translations.startServerError(
event.error ?? translations.unknownError, controllerName),
pageType: caller,
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError, controllerName) : translations.startServerError(event.error ?? translations.unknownError, controllerName),
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
break;
case ServerResultType.stopping:
showInfoBar(
translations.stoppingServer,
pageType: caller,
severity: InfoBarSeverity.info,
loading: true,
duration: null
@@ -70,7 +61,6 @@ extension ServerControllerDialog on ServerController {
if(showSuccessMessage) {
showInfoBar(
translations.stoppedServer(controllerName),
pageType: caller,
severity: InfoBarSeverity.success
);
}
@@ -80,36 +70,31 @@ extension ServerControllerDialog on ServerController {
showInfoBar(
translations.stopServerError(
event.error ?? translations.unknownError, controllerName),
pageType: caller,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
break;
case ServerResultType.missingHostError:
showInfoBar(
translations.missingHostNameError(controllerName),
pageType: caller,
severity: InfoBarSeverity.error
);
break;
case ServerResultType.missingPortError:
showInfoBar(
translations.missingPortError(controllerName),
pageType: caller,
severity: InfoBarSeverity.error
);
break;
case ServerResultType.illegalPortError:
showInfoBar(
translations.illegalPortError(controllerName),
pageType: caller,
severity: InfoBarSeverity.error
);
break;
case ServerResultType.freeingPort:
showInfoBar(
translations.freeingPort(defaultPort),
pageType: caller,
loading: true,
duration: null
);
@@ -117,24 +102,21 @@ extension ServerControllerDialog on ServerController {
case ServerResultType.freePortSuccess:
showInfoBar(
translations.freedPort(defaultPort),
pageType: caller,
severity: InfoBarSeverity.success,
duration: snackbarShortDuration
duration: infoBarShortDuration
);
break;
case ServerResultType.freePortError:
showInfoBar(
translations.freePortError(event.error ?? translations.unknownError, controllerName),
pageType: caller,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
break;
case ServerResultType.pingingRemote:
if(started.value) {
showInfoBar(
translations.pingingRemoteServer(controllerName),
pageType: caller,
severity: InfoBarSeverity.info,
loading: true,
duration: null
@@ -142,20 +124,16 @@ extension ServerControllerDialog on ServerController {
}
break;
case ServerResultType.pingingLocal:
if(started.value) {
showInfoBar(
translations.pingingLocalServer(controllerName, type().name),
pageType: caller,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}
showInfoBar(
translations.pingingLocalServer(controllerName, type().name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
break;
case ServerResultType.pingError:
showInfoBar(
translations.pingError(controllerName, type().name),
pageType: caller,
severity: InfoBarSeverity.error
);
break;
@@ -166,19 +144,12 @@ extension ServerControllerDialog on ServerController {
}
});
var result = await completer.future;
if(result && type() == ServerType.embedded) {
watchProcess(embeddedServerPid!).then((value) {
if(started()) {
started.value = false;
}
});
}
return result;
return await completer.future;
}
}
final Semaphore _publishingSemaphore = Semaphore();
extension MatchmakerControllerExtension on MatchmakerController {
void joinLocalHost() {
gameServerAddress.text = kDefaultGameServerHost;
@@ -190,7 +161,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
if(uuid == id) {
showInfoBar(
translations.joinSelfServer,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
@@ -219,7 +190,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
if(!checkPassword(confirmPassword, hashedPassword)) {
showInfoBar(
translations.wrongServerPassword,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
@@ -242,16 +213,16 @@ extension MatchmakerControllerExtension on MatchmakerController {
showInfoBar(
translations.offlineServer,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
var confirmPasswordController = TextEditingController();
var showPassword = RxBool(false);
var showPasswordTrailing = RxBool(false);
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showAppDialog<String?>(
builder: (context) => FormDialog(
content: Column(
@@ -270,15 +241,14 @@ extension MatchmakerControllerExtension on MatchmakerController {
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: Button(
onPressed: () => showPasswordTrailing.value = !showPasswordTrailing.value,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
showPassword.value ? Icons.visibility_off : Icons.visibility,
color: showPassword.value ? null : Colors.transparent
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
),
)
))
@@ -312,7 +282,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
}
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
severity: InfoBarSeverity.success
));
}
@@ -320,40 +290,62 @@ extension MatchmakerControllerExtension on MatchmakerController {
extension HostingControllerExtension on HostingController {
Future<void> publishServer(String author, String version) async {
var passwordText = password.text;
var hasPassword = passwordText.isNotEmpty;
var ip = await Ipify.ipv4();
if(hasPassword) {
ip = aes256Encrypt(ip, passwordText);
}
try {
_publishingSemaphore.acquire();
if(published.value) {
return;
}
var supabase = Supabase.instance.client;
var hosts = supabase.from('hosts');
var payload = {
'name': name.text,
'description': description.text,
'author': author,
'ip': ip,
'version': version,
'password': hasPassword ? hashPassword(passwordText) : null,
'timestamp': DateTime.now().toIso8601String(),
'discoverable': discoverable.value
};
if(published()) {
await hosts.update(payload).eq("id", uuid);
}else {
payload["id"] = uuid;
await hosts.insert(payload);
}
final passwordText = password.text;
final hasPassword = passwordText.isNotEmpty;
var ip = await Ipify.ipv4();
if(hasPassword) {
ip = aes256Encrypt(ip, passwordText);
}
published.value = true;
var supabase = Supabase.instance.client;
var hosts = supabase.from("hosting");
var payload = {
'name': name.text,
'description': description.text,
'author': author,
'ip': ip,
'version': version,
'password': hasPassword ? hashPassword(passwordText) : null,
'timestamp': DateTime.now().toIso8601String(),
'discoverable': discoverable.value
};
if(published()) {
await hosts.update(payload).eq("id", uuid);
}else {
payload["id"] = uuid;
await hosts.insert(payload);
}
published.value = true;
}catch(error) {
published.value = false;
}finally {
_publishingSemaphore.release();
}
}
Future<void> discardServer() async {
var supabase = Supabase.instance.client;
await supabase.from('hosts')
.delete()
.match({'id': uuid});
published.value = false;
try {
_publishingSemaphore.acquire();
if(!published.value) {
return;
}
final supabase = Supabase.instance.client;
await supabase.from("hosting")
.delete()
.match({'id': uuid});
published.value = false;
}catch(_) {
published.value = true;
}finally {
_publishingSemaphore.release();
}
}
}

View File

@@ -1,6 +1,4 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart' as messenger;
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
abstract class RebootPage extends StatefulWidget {
@@ -14,9 +12,7 @@ abstract class RebootPage extends StatefulWidget {
int get index => type.index;
List<PageSetting> get settings;
bool get hasButton;
bool hasButton(String? pageName);
@override
RebootPageState createState();
@@ -49,21 +45,9 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
);
}
OverlayEntry showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) => messenger.showInfoBar(
text,
pageType: widget.type,
severity: severity,
loading: loading,
duration: duration,
action: action
);
ListView get _listView => ListView.builder(
itemCount: settings.length * 2,
itemBuilder: (context, index) => index.isEven ? Align(
alignment: Alignment.center,
child: settings[index ~/ 2],
) : const SizedBox(height: 8.0),
itemCount: settings.length,
itemBuilder: (context, index) => settings[index],
);
@override

View File

@@ -1,26 +0,0 @@
class PageSetting {
final String name;
final String description;
final String? content;
final List<PageSetting>? children;
final int pageIndex;
PageSetting(
{required this.name,
required this.description,
this.content,
this.children,
this.pageIndex = -1});
PageSetting withPageIndex(int pageIndex) => this.pageIndex != -1
? this
: PageSetting(
name: name,
description: description,
content: content,
children: children,
pageIndex: pageIndex);
@override
String toString() => "$name: $description";
}

View File

@@ -0,0 +1,13 @@
class PageSuggestion {
final String name;
final String description;
final String? content;
final int pageIndex;
final String? routeName;
PageSuggestion({required this.name,
required this.description,
this.content,
required this.pageIndex,
this.routeName});
}

View File

@@ -1,15 +1,16 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/server/start_button.dart';
import 'package:reboot_launcher/src/widget/server/type_selector.dart';
import 'package:reboot_launcher/src/widget/server_start_button.dart';
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../dialog/implementation/data.dart';
@@ -27,39 +28,7 @@ class AuthenticatorPage extends RebootPage {
RebootPageType get type => RebootPageType.authenticator;
@override
bool get hasButton => true;
@override
List<PageSetting> get settings => [
PageSetting(
name: translations.authenticatorConfigurationName,
description: translations.authenticatorConfigurationDescription,
children: [
PageSetting(
name: translations.authenticatorConfigurationHostName,
description: translations.authenticatorConfigurationHostDescription
),
PageSetting(
name: translations.authenticatorConfigurationPortName,
description: translations.authenticatorConfigurationPortDescription
),
PageSetting(
name: translations.authenticatorConfigurationDetachedName,
description: translations.authenticatorConfigurationDetachedDescription
)
]
),
PageSetting(
name: translations.authenticatorInstallationDirectoryName,
description: translations.authenticatorInstallationDirectoryDescription,
content: translations.authenticatorInstallationDirectoryContent
),
PageSetting(
name: translations.authenticatorResetDefaultsName,
description: translations.authenticatorResetDefaultsDescription,
content: translations.authenticatorResetDefaultsContent
)
];
bool hasButton(String? pageName) => pageName == null;
@override
RebootPageState<AuthenticatorPage> createState() => _AuthenticatorPageState();
@@ -70,86 +39,126 @@ class _AuthenticatorPageState extends RebootPageState<AuthenticatorPage> {
@override
List<Widget> get settings => [
_configuration,
_type,
_hostName,
_port,
_detached,
_installationDirectory,
_resetDefaults
];
@override
Widget get button => const ServerButton(
authenticator: true
);
Widget get _hostName => Obx(() {
if(_authenticatorController.type.value != ServerType.remote) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.authenticatorConfigurationHostName),
subtitle: Text(translations.authenticatorConfigurationHostDescription),
content: TextFormBox(
placeholder: translations.authenticatorConfigurationHostName,
controller: _authenticatorController.host
)
);
});
Widget get _port => Obx(() {
if(_authenticatorController.type.value == ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
fluentUi.FluentIcons.number_field
),
title: Text(translations.authenticatorConfigurationPortName),
subtitle: Text(translations.authenticatorConfigurationPortDescription),
content: TextFormBox(
placeholder: translations.authenticatorConfigurationPortName,
controller: _authenticatorController.port,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
]
)
);
});
Widget get _detached => Obx(() {
if(_authenticatorController.type.value != ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.developer_board_24_regular
),
title: Text(translations.authenticatorConfigurationDetachedName),
subtitle: Text(translations.authenticatorConfigurationDetachedDescription),
contentWidth: null,
content: Row(
children: [
Text(
_authenticatorController.detached.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _authenticatorController.detached(),
onChanged: (value) => _authenticatorController.detached.value = value
),
],
)
);
});
SettingTile get _resetDefaults => SettingTile(
title: translations.authenticatorResetDefaultsName,
subtitle: translations.authenticatorResetDefaultsDescription,
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.authenticatorResetDefaultsName),
subtitle: Text(translations.authenticatorResetDefaultsDescription),
content: Button(
onPressed: () => showResetDialog(_authenticatorController.reset),
child: Text(translations.authenticatorResetDefaultsContent),
)
);
SettingTile get _installationDirectory => SettingTile(
title: translations.authenticatorInstallationDirectoryName,
subtitle: translations.authenticatorInstallationDirectoryDescription,
content: Button(
onPressed: () => launchUrl(authenticatorDirectory.uri),
child: Text(translations.authenticatorInstallationDirectoryContent)
Widget get _installationDirectory => Obx(() {
if(_authenticatorController.type.value != ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.folder_24_regular
),
title: Text(translations.authenticatorInstallationDirectoryName),
subtitle: Text(translations.authenticatorInstallationDirectoryDescription),
content: Button(
onPressed: () => launchUrl(authenticatorDirectory.uri),
child: Text(translations.authenticatorInstallationDirectoryContent)
)
);
});
Widget get _type => SettingTile(
icon: Icon(
FluentIcons.password_24_regular
),
title: Text(translations.authenticatorTypeName),
subtitle: Text(translations.authenticatorTypeDescription),
content: const ServerTypeSelector(
authenticator: true
)
);
Widget get _configuration => Obx(() => SettingTile(
title: translations.authenticatorConfigurationName,
subtitle: translations.authenticatorConfigurationDescription,
content: const ServerTypeSelector(
authenticator: true
),
expandedContent: [
if(_authenticatorController.type.value == ServerType.remote)
SettingTile(
title: translations.authenticatorConfigurationHostName,
subtitle: translations.authenticatorConfigurationHostDescription,
isChild: true,
content: TextFormBox(
placeholder: translations.authenticatorConfigurationHostName,
controller: _authenticatorController.host
)
),
if(_authenticatorController.type.value != ServerType.embedded)
SettingTile(
title: translations.authenticatorConfigurationPortName,
subtitle: translations.authenticatorConfigurationPortDescription,
isChild: true,
content: TextFormBox(
placeholder: translations.authenticatorConfigurationPortName,
controller: _authenticatorController.port,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
]
)
),
if(_authenticatorController.type.value == ServerType.embedded)
SettingTile(
title: translations.authenticatorConfigurationDetachedName,
subtitle: translations.authenticatorConfigurationDetachedDescription,
contentWidth: null,
isChild: true,
content: Obx(() => Row(
children: [
Text(
_authenticatorController.detached.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _authenticatorController.detached(),
onChanged: (value) => _authenticatorController.detached.value = value
),
],
))
)
],
));
@override
Widget get button => const ServerButton(
authenticator: true
);
}

View File

@@ -1,19 +1,25 @@
import 'dart:collection';
import 'dart:async';
import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/dll.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/home/profile.dart';
import 'package:reboot_launcher/src/widget/os/title_bar.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.dart';
import 'package:window_manager/window_manager.dart';
class HomePage extends StatefulWidget {
@@ -27,12 +33,11 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
static const double _kDefaultPadding = 12.0;
final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController();
final RxBool _focused = RxBool(true);
final Queue<int> _pagesStack = Queue();
bool _hitBack = false;
@override
bool get wantKeepAlive => true;
@@ -42,21 +47,17 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
windowManager.addListener(this);
var lastValue = pageIndex.value;
pageIndex.listen((value) {
if(_hitBack) {
_hitBack = false;
return;
}
if(value == lastValue) {
return;
}
_pagesStack.add(lastValue);
WidgetsBinding.instance.addPostFrameCallback((_) {
restoreMessage(value, lastValue);
lastValue = value;
});
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateController.update();
watchDlls().listen((filePath) => showDllDeletedDialog(() {
downloadCriticalDllInteractive(filePath);
}));
});
super.initState();
}
@@ -64,6 +65,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
void dispose() {
_searchFocusNode.dispose();
_searchController.dispose();
pagesController.close();
windowManager.removeListener(this);
super.dispose();
}
@@ -103,27 +105,17 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() {
_settingsController.language.value;
loadTranslations(context);
return NavigationPaneTheme(
_settingsController.language.value;
loadTranslations(context);
return Obx(() => NavigationPaneTheme(
data: NavigationPaneThemeData(
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
),
child: NavigationView(
paneBodyBuilder: (pane, body) => Navigator(
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Padding(
padding: const EdgeInsets.all(_kDefaultPadding),
child: SizedBox(
key: pageKey,
child: body
)
)
)
],
paneBodyBuilder: (pane, body) => _PaneBody(
padding: _kDefaultPadding,
controller: pagesController,
body: body
),
appBar: NavigationAppBar(
height: 32,
@@ -133,43 +125,64 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
automaticallyImplyLeading: false,
),
pane: NavigationPane(
key: appKey,
selected: pageIndex.value,
onChanged: (index) => pageIndex.value = index,
menuButton: const SizedBox(),
displayMode: PaneDisplayMode.open,
items: _items,
header: const ProfileWidget(),
autoSuggestBox: _autoSuggestBox,
indicator: const StickyNavigationIndicator(
duration: Duration(milliseconds: 500),
curve: Curves.easeOut,
indicatorSize: 3.25
)
selected: pageIndex.value,
onChanged: (index) {
final lastPageIndex = pageIndex.value;
if(lastPageIndex != index) {
pageIndex.value = index;
}else if(pageStack.isNotEmpty) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
pagesController.add(null);
}
},
menuButton: const SizedBox(),
displayMode: PaneDisplayMode.open,
items: _items,
customPane: _CustomPane(),
header: const ProfileWidget(),
autoSuggestBox: _autoSuggestBox,
indicator: const StickyNavigationIndicator(
duration: Duration(milliseconds: 500),
curve: Curves.easeOut,
indicatorSize: 3.25
)
),
contentShape: const RoundedRectangleBorder(),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child
)
),
);
});
}
Widget get _backButton => Obx(() {
pageIndex.value;
return Button(
style: ButtonStyle(
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
backgroundColor: ButtonState.all(Colors.transparent),
border: ButtonState.all(const BorderSide(color: Colors.transparent))
),
onPressed: _pagesStack.isEmpty ? null : () {
_hitBack = true;
pageIndex.value = _pagesStack.removeLast();
},
child: const Icon(FluentIcons.back, size: 12.0),
);
});
Widget get _backButton => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: ButtonState.all(const EdgeInsets.only(top: 6.0)),
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
),
onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) {
Navigator.of(appKey.currentContext!).pop();
}else {
final lastPage = appStack.removeLast();
pageStack.remove(lastPage);
if (lastPage is int) {
hitBack = true;
pageIndex.value = lastPage;
} else {
Navigator.of(pageKey.currentContext!).pop();
}
}
pagesController.add(null);
},
child: const Icon(FluentIcons.back, size: 12.0),
)
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: appWindow.maximizeOrRestore,
@@ -178,34 +191,28 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: AutoSuggestBox<PageSetting>(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0
),
child: AutoSuggestBox<PageSuggestion>(
key: _searchKey,
controller: _searchController,
placeholder: translations.find,
focusNode: _searchFocusNode,
selectionHeightStyle: BoxHeightStyle.max,
itemBuilder: (context, item) => Wrap(
children: [
ListTile(
onPressed: () {
pageIndex.value = item.value.pageIndex;
_searchController.clear();
_searchFocusNode.unfocus();
},
leading: item.child,
title: Text(
item.value.name,
overflow: TextOverflow.clip,
maxLines: 1
),
subtitle: item.value.description.isNotEmpty ? Text(
item.value.description,
overflow: TextOverflow.clip,
maxLines: 1
) : null
),
],
itemBuilder: (context, item) => ListTile(
onPressed: () {
pageIndex.value = item.value.pageIndex;
_searchController.clear();
_searchFocusNode.unfocus();
},
leading: item.child,
title: Text(
item.value.name,
overflow: TextOverflow.clip,
maxLines: 1
)
),
items: _suggestedItems,
autofocus: true,
@@ -221,36 +228,22 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
)
);
List<AutoSuggestBoxItem<PageSetting>> get _suggestedItems => pages.mapMany((page) {
var icon = SizedBox.square(
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
final pageIcon = SizedBox.square(
dimension: 24,
child: Image.asset(page.iconAsset)
);
var outerResults = <AutoSuggestBoxItem<PageSetting>>[];
outerResults.add(AutoSuggestBoxItem(
value: PageSetting(
final results = <AutoSuggestBoxItem<PageSuggestion>>[];
results.add(AutoSuggestBoxItem(
value: PageSuggestion(
name: page.name,
description: "",
pageIndex: page.index
),
label: page.name,
child: icon
child: pageIcon
));
outerResults.addAll(page.settings.mapMany((setting) {
var results = <AutoSuggestBoxItem<PageSetting>>[];
results.add(AutoSuggestBoxItem(
value: setting.withPageIndex(page.index),
label: setting.toString(),
child: icon
));
setting.children?.forEach((childSetting) => results.add(AutoSuggestBoxItem(
value: childSetting.withPageIndex(page.index),
label: childSetting.toString(),
child: icon
)));
return results;
}).toList());
return outerResults;
return results;
}).toList();
List<NavigationPaneItem> get _items => pages.map((page) => _createItem(page)).toList();
@@ -263,4 +256,252 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
),
body: page
);
}
class _PaneBody extends StatefulWidget {
const _PaneBody({
required this.padding,
required this.controller,
required this.body
});
final double padding;
final StreamController<void> controller;
final Widget? body;
@override
State<_PaneBody> createState() => _PaneBodyState();
}
class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin {
final SettingsController _settingsController = Get.find<SettingsController>();
final PageController _pageController = PageController(keepPage: true);
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
pageIndex.listen((index) => _pageController.jumpToPage(index));
}
@override
Widget build(BuildContext context) {
super.build(context);
final themeMode = _settingsController.themeMode.value;
final inactiveColor = themeMode == ThemeMode.dark
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
return Padding(
padding: EdgeInsets.only(
left: widget.padding,
right: widget.padding * 2,
top: widget.padding,
bottom: widget.padding * 2
),
child: Column(
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 1000
),
child: Center(
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: StreamBuilder(
stream: widget.controller.stream,
builder: (context, _) {
final elements = <TextSpan>[];
elements.add(TextSpan(
text: pages[pageIndex.value].name,
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
for(var i = 0; i < pageStack.length; i++) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
}
widget.controller.add(null);
}) : null,
style: TextStyle(
color: pageStack.isNotEmpty ? inactiveColor : null
)
));
for(var i = pageStack.length - 1; i >= 0; i--) {
var innerPage = pageStack.elementAt(i);
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
elements.add(TextSpan(
text: " > ",
style: TextStyle(
color: inactiveColor
)
));
elements.add(TextSpan(
text: innerPage,
recognizer: i == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
for(var j = 0; j < i - 1; j++) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
}
widget.controller.add(null);
}),
style: TextStyle(
color: i == pageStack.length - 1 ? null : inactiveColor
)
));
}
return Text.rich(
TextSpan(
children: elements
),
style: TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w600
),
);
}
),
),
const SizedBox(height: 24.0),
Expanded(
child: PageView.builder(
controller: _pageController,
itemBuilder: (context, index) => Navigator(
onPopPage: (page, data) => true,
observers: [
_NestedPageObserver(
onChanged: (routeName) {
if(routeName != null) {
pageIndex.refresh();
addSubPageToStack(routeName);
widget.controller.add(null);
}
}
)
],
pages: [
MaterialPage(
child: KeyedSubtree(
key: getPageKeyByIndex(index),
child: widget.body ?? const SizedBox.shrink()
)
)
],
),
itemCount: pages.length
),
),
],
),
),
),
)
],
)
);
}
}
class _CustomPane extends NavigationPaneWidget {
@override
Widget build(BuildContext context, NavigationPaneWidgetData data) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
data.appBar,
Expanded(
child: Navigator(
key: appKey,
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Row(
children: [
SizedBox(
width: 310,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
data.pane.header ?? const SizedBox.shrink(),
data.pane.autoSuggestBox ?? const SizedBox.shrink(),
const SizedBox(height: 12.0),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0
),
child: Scrollbar(
controller: data.scrollController,
child: ListView.separated(
controller: data.scrollController,
itemCount: data.pane.items.length,
separatorBuilder: (context, index) => const SizedBox(
height: 4.0
),
itemBuilder: (context, index) {
final item = data.pane.items[index] as PaneItem;
return HoverButton(
onPressed: () => data.pane.onChanged?.call(index),
builder: (context, states) => Container(
height: 36,
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context),
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
transparentWhenNone: true,
),
borderRadius: BorderRadius.all(Radius.circular(6.0))
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0
),
child: Row(
children: [
data.pane.indicator ?? const SizedBox.shrink(),
item.icon,
const SizedBox(width: 12.0),
item.title ?? const SizedBox.shrink()
],
),
),
),
);
},
),
),
),
)
],
),
),
Expanded(
child: data.content
)
],
)
)
],
),
)
],
);
}
class _NestedPageObserver extends NavigatorObserver {
final void Function(String?) onChanged;
_NestedPageObserver({required this.onChanged});
@override
void didPush(Route route, Route? previousRoute) {
if(previousRoute != null) {
WidgetsBinding.instance.addPostFrameCallback((_) => onChanged(route.settings.name));
}
}
}

View File

@@ -1,15 +1,11 @@
import 'dart:convert';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:markdown_widget/config/markdown_generator.dart';
import 'package:reboot_launcher/src/controller/info_controller.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:http/http.dart' as http;
import 'package:reboot_launcher/src/util/tutorial.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
class InfoPage extends RebootPage {
const InfoPage({Key? key}) : super(key: key);
@@ -24,105 +20,68 @@ class InfoPage extends RebootPage {
String get iconAsset => "assets/images/info.png";
@override
bool get hasButton => false;
bool hasButton(String? routeName) => false;
@override
RebootPageType get type => RebootPageType.info;
@override
List<PageSetting> get settings => [];
}
class _InfoPageState extends RebootPageState<InfoPage> {
final InfoController _infoController = Get.find<InfoController>();
late Future<List<String>> _fetchFuture;
late double _height;
@override
void initState() {
_fetchFuture = _infoController.links != null
? Future.value(_infoController.links)
: _initQuery();
super.initState();
}
List<SettingTile> get settings => [
_documentation,
_discord,
_youtubeTutorial,
_reportBug
];
Future<List<String>> _initQuery() async {
var response = await http.get(Uri.parse("https://api.github.com/repos/Auties00/reboot_launcher/contents/documentation/$currentLocale"));
List results = jsonDecode(response.body);
results.sort((first, second) {
var firstIndex = int.parse(first["name"][0]);
var secondIndex = int.parse(second["name"][0]);
return firstIndex > secondIndex ? 1 : firstIndex == secondIndex ? 0 : -1;
});
List<String> parsed = results.map<String>((entry) => entry["download_url"] as String).toList();
return _infoController.links = parsed;
}
Future<String> _readLink(String url) async {
var known = _infoController.linksData[url];
if(known != null) {
return known;
}
var response = await http.get(Uri.parse(url));
return _infoController.linksData[url] = response.body;
}
@override
Widget build(BuildContext context) {
super.build(context);
_height = MediaQuery.of(context).size.height / 3;
return FutureBuilder(
future: _fetchFuture,
builder: (context, linksSnapshot) {
var linksData = linksSnapshot.data;
if(linksData == null) {
return const Center(
child: ProgressRing()
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 1000
),
child: ListView.separated(
shrinkWrap: true,
separatorBuilder: (context, index) => const SizedBox(
height: 16.0
),
itemBuilder: (context, index) => Card(
borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)),
child: _buildBody(linksData, index)
),
itemCount: linksData.length
),
),
)
],
);
}
);
}
Widget _buildBody(List<String> linksData, int index) => FutureBuilder(
future: _readLink(linksData[index]),
builder: (context, linkDataSnapshot) {
var markdownGenerator = MarkdownGenerator();
var result = markdownGenerator.buildWidgets(linkDataSnapshot.data ?? "");
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: result
);
}
SettingTile get _reportBug => SettingTile(
icon: Icon(
FluentIcons.bug_24_regular
),
title: Text(translations.settingsUtilsBugReportName),
subtitle: Text(translations.settingsUtilsBugReportSubtitle) ,
content: Button(
onPressed: openBugReport,
child: Text(translations.settingsUtilsBugReportContent),
)
);
@override
List<SettingTile> get settings => [];
SettingTile get _youtubeTutorial => SettingTile(
icon: Icon(
FluentIcons.video_24_regular
),
title: Text(translations.infoVideoName),
subtitle: Text(translations.infoVideoDescription),
content: Button(
onPressed: openYoutubeTutorial,
child: Text(translations.infoVideoContent)
)
);
SettingTile get _discord => SettingTile(
icon: Icon(
Icons.discord_outlined
),
title: Text(translations.infoDiscordName),
subtitle: Text(translations.infoDiscordDescription),
content: Button(
onPressed: openDiscordServer,
child: Text(translations.infoDiscordContent)
)
);
SettingTile get _documentation => SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(translations.infoDocumentationName),
subtitle: Text(translations.infoDocumentationDescription),
content: Button(
onPressed: openTutorials,
child: Text(translations.infoDocumentationContent)
)
);
@override
Widget? get button => null;

View File

@@ -1,16 +1,17 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/server/start_button.dart';
import 'package:reboot_launcher/src/widget/server/type_selector.dart';
import 'package:reboot_launcher/src/widget/server_start_button.dart';
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
class MatchmakerPage extends RebootPage {
@@ -26,42 +27,10 @@ class MatchmakerPage extends RebootPage {
String get iconAsset => "assets/images/matchmaker.png";
@override
bool get hasButton => true;
bool hasButton(String? pageName) => pageName == null;
@override
RebootPageType get type => RebootPageType.matchmaker;
@override
List<PageSetting> get settings => [
PageSetting(
name: translations.matchmakerConfigurationName,
description: translations.matchmakerConfigurationDescription,
children: [
PageSetting(
name: translations.matchmakerConfigurationHostName,
description: translations.matchmakerConfigurationHostDescription
),
PageSetting(
name: translations.matchmakerConfigurationPortName,
description: translations.matchmakerConfigurationPortDescription
),
PageSetting(
name: translations.matchmakerConfigurationDetachedName,
description: translations.matchmakerConfigurationDetachedDescription
)
]
),
PageSetting(
name: translations.matchmakerInstallationDirectoryName,
description: translations.matchmakerInstallationDirectoryDescription,
content: translations.matchmakerInstallationDirectoryContent
),
PageSetting(
name: translations.matchmakerResetDefaultsName,
description: translations.matchmakerResetDefaultsDescription,
content: translations.matchmakerResetDefaultsContent
)
];
}
class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
@@ -74,89 +43,138 @@ class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
@override
List<Widget> get settings => [
_configuration,
_type,
_hostName,
_port,
_gameServerAddress,
_detached,
_installationDirectory,
_resetDefaults
];
Widget get _configuration => Obx(() => SettingTile(
title: translations.matchmakerConfigurationName,
subtitle: translations.matchmakerConfigurationDescription,
Widget get _detached => Obx(() {
if(_matchmakerController.type.value != ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.developer_board_24_regular
),
title: Text(translations.matchmakerConfigurationDetachedName),
subtitle: Text(translations.matchmakerConfigurationDetachedDescription),
contentWidth: null,
content: Row(
children: [
Text(
_matchmakerController.detached.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _matchmakerController.detached.value,
onChanged: (value) => _matchmakerController.detached.value = value
),
],
),
);
});
Widget get _gameServerAddress => Obx(() {
if(_matchmakerController.type.value != ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.stream_input_20_regular
),
title: Text(translations.matchmakerConfigurationAddressName),
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
content: TextFormBox(
placeholder: translations.matchmakerConfigurationAddressName,
controller: _matchmakerController.gameServerAddress,
focusNode: _matchmakerController.gameServerAddressFocusNode
)
);
});
Widget get _port => Obx(() {
if(_matchmakerController.type.value == ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
fluentUi.FluentIcons.number_field
),
title: Text(translations.matchmakerConfigurationPortName),
subtitle: Text(translations.matchmakerConfigurationPortDescription),
content: TextFormBox(
placeholder: translations.matchmakerConfigurationPortName,
controller: _matchmakerController.port,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
]
)
);
});
Widget get _hostName => Obx(() {
if(_matchmakerController.type.value == ServerType.remote) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.matchmakerConfigurationHostName),
subtitle: Text(translations.matchmakerConfigurationHostDescription),
content: TextFormBox(
placeholder: translations.matchmakerConfigurationHostName,
controller: _matchmakerController.host
)
);
});
Widget get _type => SettingTile(
icon: Icon(
FluentIcons.people_24_regular
),
title: Text(translations.matchmakerTypeName),
subtitle: Text(translations.matchmakerTypeDescription),
content: const ServerTypeSelector(
authenticator: false
),
expandedContent: [
if(_matchmakerController.type.value == ServerType.remote)
SettingTile(
title: translations.matchmakerConfigurationHostName,
subtitle: translations.matchmakerConfigurationHostDescription,
isChild: true,
content: TextFormBox(
placeholder: translations.matchmakerConfigurationHostName,
controller: _matchmakerController.host
)
),
if(_matchmakerController.type.value != ServerType.embedded)
SettingTile(
title: translations.matchmakerConfigurationPortName,
subtitle: translations.matchmakerConfigurationPortDescription,
isChild: true,
content: TextFormBox(
placeholder: translations.matchmakerConfigurationPortName,
controller: _matchmakerController.port,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
]
)
),
if(_matchmakerController.type.value == ServerType.embedded)
SettingTile(
title: translations.matchmakerConfigurationAddressName,
subtitle: translations.matchmakerConfigurationAddressDescription,
isChild: true,
content: TextFormBox(
placeholder: translations.matchmakerConfigurationAddressName,
controller: _matchmakerController.gameServerAddress,
focusNode: _matchmakerController.gameServerAddressFocusNode
)
),
if(_matchmakerController.type.value == ServerType.embedded)
SettingTile(
title: translations.matchmakerConfigurationDetachedName,
subtitle: translations.matchmakerConfigurationDetachedDescription,
contentWidth: null,
isChild: true,
content: Obx(() => Row(
children: [
Text(
_matchmakerController.detached.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _matchmakerController.detached.value,
onChanged: (value) => _matchmakerController.detached.value = value
),
],
)),
)
]
));
SettingTile get _installationDirectory => SettingTile(
title: translations.matchmakerInstallationDirectoryName,
subtitle: translations.matchmakerInstallationDirectoryDescription,
content: Button(
onPressed: () => launchUrl(matchmakerDirectory.uri),
child: Text(translations.matchmakerInstallationDirectoryContent)
)
);
Widget get _installationDirectory => Obx(() {
if(_matchmakerController.type.value != ServerType.embedded) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.folder_24_regular
),
title: Text(translations.matchmakerInstallationDirectoryName),
subtitle: Text(translations.matchmakerInstallationDirectoryDescription),
content: Button(
onPressed: () => launchUrl(matchmakerDirectory.uri),
child: Text(translations.matchmakerInstallationDirectoryContent)
)
);
});
SettingTile get _resetDefaults => SettingTile(
title: translations.matchmakerResetDefaultsName,
subtitle: translations.matchmakerResetDefaultsDescription,
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.matchmakerResetDefaultsName),
subtitle: Text(translations.matchmakerResetDefaultsDescription),
content: Button(
onPressed: () => showResetDialog(_matchmakerController.reset),
child: Text(translations.matchmakerResetDefaultsContent),

View File

@@ -1,16 +1,16 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/game/start_button.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
class PlayPage extends RebootPage {
@@ -20,7 +20,7 @@ class PlayPage extends RebootPage {
RebootPageState<PlayPage> createState() => _PlayPageState();
@override
bool get hasButton => true;
bool hasButton(String? pageName) => pageName == null;
@override
String get name => translations.playName;
@@ -30,33 +30,6 @@ class PlayPage extends RebootPage {
@override
RebootPageType get type => RebootPageType.play;
@override
List<PageSetting> get settings => [
versionSelectorRebootSetting,
PageSetting(
name: translations.playGameServerName,
description: translations.playGameServerDescription,
content: translations.playGameServerContentLocal,
children: [
PageSetting(
name: translations.playGameServerHostName,
description: translations.playGameServerHostDescription,
content: translations.playGameServerHostName
),
PageSetting(
name: translations.playGameServerBrowserName,
description: translations.playGameServerBrowserDescription,
content: translations.playGameServerBrowserName
),
PageSetting(
name: translations.playGameServerCustomName,
description: translations.playGameServerCustomDescription,
content: translations.playGameServerCustomContent
)
]
)
];
}
class _PlayPageState extends RebootPageState<PlayPage> {
@@ -84,60 +57,39 @@ class _PlayPageState extends RebootPageState<PlayPage> {
@override
List<SettingTile> get settings => [
versionSelectorSettingTile,
_gameServerSelector
versionSelectSettingTile,
_hostSettingTile,
_browseServerTile,
_matchmakerTile
];
SettingTile get _gameServerSelector => SettingTile(
title: translations.playGameServerName,
subtitle: translations.playGameServerDescription,
content: IgnorePointer(
child: Button(
style: ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).resources.controlFillColorDefault)
),
onPressed: () {},
child: Obx(() {
var address = _matchmakerController.gameServerAddress.text;
var owner = _matchmakerController.gameServerOwner.value;
return Text(
isLocalHost(address) ? translations.playGameServerContentLocal : owner != null ? translations.playGameServerContentBrowser(owner) : address,
textAlign: TextAlign.start
);
})
),
),
expandedContent: [
SettingTile(
title: translations.playGameServerHostName,
subtitle: translations.playGameServerHostDescription,
content: Button(
onPressed: () => pageIndex.value = RebootPageType.host.index,
child: Text(translations.playGameServerHostName)
),
isChild: true
),
SettingTile(
title: translations.playGameServerBrowserName,
subtitle: translations.playGameServerBrowserDescription,
content: Button(
onPressed: () => pageIndex.value = RebootPageType.browser.index,
child: Text(translations.playGameServerBrowserName)
),
isChild: true
),
SettingTile(
title: translations.playGameServerCustomName,
subtitle: translations.playGameServerCustomDescription,
content: Button(
onPressed: () {
pageIndex.value = RebootPageType.matchmaker.index;
WidgetsBinding.instance.addPostFrameCallback((_) => _matchmakerController.gameServerAddressFocusNode.requestFocus());
},
child: Text(translations.playGameServerCustomContent)
),
isChild: true
)
]
SettingTile get _matchmakerTile => SettingTile(
onPressed: () {
pageIndex.value = RebootPageType.matchmaker.index;
WidgetsBinding.instance.addPostFrameCallback((_) => _matchmakerController.gameServerAddressFocusNode.requestFocus());
},
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.playGameServerCustomName),
subtitle: Text(translations.playGameServerCustomDescription),
);
SettingTile get _browseServerTile => SettingTile(
onPressed: () => pageIndex.value = RebootPageType.browser.index,
icon: Icon(
FluentIcons.search_24_regular
),
title: Text(translations.playGameServerBrowserName),
subtitle: Text(translations.playGameServerBrowserDescription)
);
SettingTile get _hostSettingTile => SettingTile(
onPressed: () => pageIndex.value = RebootPageType.host.index,
icon: Icon(
FluentIcons.desktop_24_regular
),
title: Text(translations.playGameServerHostName),
subtitle: Text(translations.playGameServerHostDescription),
);
}

View File

@@ -9,11 +9,9 @@ import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:skeletons/skeletons.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
class BrowsePage extends RebootPage {
const BrowsePage({Key? key}) : super(key: key);
@@ -28,13 +26,10 @@ class BrowsePage extends RebootPage {
String get iconAsset => "assets/images/server_browser.png";
@override
bool get hasButton => false;
bool hasButton(String? pageName) => false;
@override
RebootPageState<BrowsePage> createState() => _BrowsePageState();
@override
List<PageSetting> get settings => [];
}
class _BrowsePageState extends RebootPageState<BrowsePage> {
@@ -73,28 +68,22 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
],
);
Widget _buildPageBody(Set<Map<String, dynamic>> data) => Column(
children: [
_searchBar,
const SizedBox(
height: 16,
),
Expanded(
child: StreamBuilder<String?>(
stream: _filterControllerStream.stream,
builder: (context, filterSnapshot) {
var items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
if(items.isEmpty) {
return _noServersByQuery;
}
return _buildPopulatedListBody(items);
}
),
)
],
Widget _buildPageBody(Set<Map<String, dynamic>> data) => StreamBuilder(
stream: _filterControllerStream.stream,
builder: (context, filterSnapshot) {
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
return Column(
children: [
_searchBar,
const SizedBox(
height: 16,
),
Expanded(
child: items.isEmpty ? _noServersByQuery : _buildPopulatedListBody(items)
),
],
);
}
);
Widget _buildPopulatedListBody(Set<Map<String, dynamic>> items) => ListView.builder(
@@ -109,17 +98,16 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
var entry = items.elementAt(index ~/ 2);
var hasPassword = entry["password"] != null;
return SettingTile(
title: "${_formatName(entry)}${entry["author"]}",
subtitle: "${_formatDescription(entry)}${_formatVersion(entry)}",
icon: Icon(
hasPassword ? FluentIcons.lock : FluentIcons.globe
),
title: Text("${_formatName(entry)}${entry["author"]}"),
subtitle: Text("${_formatDescription(entry)}${_formatVersion(entry)}"),
content: Button(
onPressed: () => _matchmakerController.joinServer(_hostingController.uuid, entry),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if(hasPassword)
const Icon(FluentIcons.lock),
if(hasPassword)
const SizedBox(width: 8.0),
Text(_matchmakerController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
],
),
@@ -177,12 +165,20 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
return false;
}
Widget get _searchBar => TextBox(
placeholder: translations.findServer,
controller: _filterController,
autofocus: true,
onChanged: (value) => _filterControllerStream.add(value),
suffix: _searchBarIcon,
Widget get _searchBar => Align(
alignment: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 350
),
child: TextBox(
placeholder: translations.findServer,
controller: _filterController,
autofocus: true,
onChanged: (value) => _filterControllerStream.add(value),
suffix: _searchBarIcon,
),
),
);
Widget get _searchBarIcon => Button(
@@ -191,8 +187,8 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
_filterControllerStream.add("");
},
style: ButtonStyle(
backgroundColor: _filterController.text.isNotEmpty ? null : ButtonState.all(Colors.transparent),
border: _filterController.text.isNotEmpty ? null : ButtonState.all(const BorderSide(color: Colors.transparent))
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
),
child: _searchBarIconData
);

View File

@@ -1,21 +1,20 @@
import 'package:clipboard/clipboard.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/game/start_button.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
import 'package:sync/semaphore.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
class HostPage extends RebootPage {
const HostPage({Key? key}) : super(key: key);
@@ -30,64 +29,15 @@ class HostPage extends RebootPage {
RebootPageType get type => RebootPageType.host;
@override
bool get hasButton => true;
bool hasButton(String? pageName) => pageName == null;
@override
RebootPageState<HostPage> createState() => _HostingPageState();
@override
List<PageSetting> get settings => [
PageSetting(
name: translations.hostGameServerName,
description: translations.hostGameServerDescription,
children: [
PageSetting(
name: translations.hostGameServerNameName,
description: translations.hostGameServerNameDescription
),
PageSetting(
name: translations.hostGameServerDescriptionName,
description: translations.hostGameServerDescriptionDescription
),
PageSetting(
name: translations.hostGameServerPasswordName,
description: translations.hostGameServerDescriptionDescription
),
PageSetting(
name: translations.hostGameServerDiscoverableName,
description: translations.hostGameServerDiscoverableDescription
)
],
),
versionSelectorRebootSetting,
PageSetting(
name: translations.hostShareName,
description: translations.hostShareDescription,
children: [
PageSetting(
name: translations.hostShareLinkName,
description: translations.hostShareLinkDescription,
content: translations.hostShareLinkContent
),
PageSetting(
name: translations.hostShareIpName,
description: translations.hostShareIpDescription,
content: translations.hostShareIpContent
)
],
),
PageSetting(
name: translations.hostResetName,
description: translations.hostResetDescription,
content: translations.hostResetContent
)
];
}
class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final Semaphore _semaphore = Semaphore();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@@ -110,16 +60,20 @@ class _HostingPageState extends RebootPageState<HostPage> {
);
@override
List<SettingTile> get settings => [
List<Widget> get settings => [
_gameServer,
versionSelectorSettingTile,
versionSelectSettingTile,
_headless,
_share,
_resetDefaults
];
SettingTile get _resetDefaults => SettingTile(
title: translations.hostResetName,
subtitle: translations.hostResetDescription,
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.hostResetName),
subtitle: Text(translations.hostResetDescription),
content: Button(
onPressed: () => showResetDialog(_hostingController.reset),
child: Text(translations.hostResetContent),
@@ -127,13 +81,18 @@ class _HostingPageState extends RebootPageState<HostPage> {
);
SettingTile get _gameServer => SettingTile(
title: translations.hostGameServerName,
subtitle: translations.hostGameServerDescription,
expandedContent: [
icon: Icon(
FluentIcons.info_24_regular
),
title: Text(translations.hostGameServerName),
subtitle: Text(translations.hostGameServerDescription),
children: [
SettingTile(
title: translations.hostGameServerNameName,
subtitle: translations.hostGameServerNameDescription,
isChild: true,
icon: Icon(
FluentIcons.textbox_24_regular
),
title: Text(translations.hostGameServerNameName),
subtitle: Text(translations.hostGameServerNameDescription),
content: TextFormBox(
placeholder: translations.hostGameServerNameName,
controller: _hostingController.name,
@@ -141,9 +100,11 @@ class _HostingPageState extends RebootPageState<HostPage> {
)
),
SettingTile(
title: translations.hostGameServerDescriptionName,
subtitle: translations.hostGameServerDescriptionDescription,
isChild: true,
icon: Icon(
FluentIcons.text_description_24_regular
),
title: Text(translations.hostGameServerDescriptionName),
subtitle: Text(translations.hostGameServerDescriptionDescription),
content: TextFormBox(
placeholder: translations.hostGameServerDescriptionName,
controller: _hostingController.description,
@@ -151,9 +112,11 @@ class _HostingPageState extends RebootPageState<HostPage> {
)
),
SettingTile(
title: translations.hostGameServerPasswordName,
subtitle: translations.hostGameServerDescriptionDescription,
isChild: true,
icon: Icon(
FluentIcons.password_24_regular
),
title: Text(translations.hostGameServerPasswordName),
subtitle: Text(translations.hostGameServerDescriptionDescription),
content: Obx(() => TextFormBox(
placeholder: translations.hostGameServerPasswordName,
controller: _hostingController.password,
@@ -172,16 +135,18 @@ class _HostingPageState extends RebootPageState<HostPage> {
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
_hostingController.showPassword.value ? Icons.visibility_off : Icons.visibility,
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
color: _showPasswordTrailing.value ? null : Colors.transparent
),
)
))
),
SettingTile(
title: translations.hostGameServerDiscoverableName,
subtitle: translations.hostGameServerDiscoverableDescription,
isChild: true,
icon: Icon(
FluentIcons.eye_24_regular
),
title: Text(translations.hostGameServerDiscoverableName),
subtitle: Text(translations.hostGameServerDiscoverableDescription),
contentWidth: null,
content: Obx(() => Row(
children: [
@@ -204,14 +169,43 @@ class _HostingPageState extends RebootPageState<HostPage> {
]
);
Widget get _headless => Obx(() => SettingTile(
icon: Icon(
FluentIcons.window_console_20_regular
),
title: Text(translations.hostHeadlessName),
subtitle: Text(translations.hostHeadlessDescription),
contentWidth: null,
content: Row(
children: [
Text(
_hostingController.headless.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _hostingController.headless.value,
onChanged: (value) => _hostingController.headless.value = value
),
],
),
),
);
SettingTile get _share => SettingTile(
title: translations.hostShareName,
subtitle: translations.hostShareDescription,
expandedContent: [
icon: Icon(
FluentIcons.link_24_regular
),
title: Text(translations.hostShareName),
subtitle: Text(translations.hostShareDescription),
children: [
SettingTile(
title: translations.hostShareLinkName,
subtitle: translations.hostShareLinkDescription,
isChild: true,
icon: Icon(
FluentIcons.link_24_regular
),
title: Text(translations.hostShareLinkName),
subtitle: Text(translations.hostShareLinkDescription),
content: Button(
onPressed: () async {
FlutterClipboard.controlC("$kCustomUrlSchema://${_hostingController.uuid}");
@@ -221,9 +215,11 @@ class _HostingPageState extends RebootPageState<HostPage> {
)
),
SettingTile(
title: translations.hostShareIpName,
subtitle: translations.hostShareIpDescription,
isChild: true,
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.hostShareIpName),
subtitle: Text(translations.hostShareIpDescription),
content: Button(
onPressed: () async {
try {
@@ -247,15 +243,12 @@ class _HostingPageState extends RebootPageState<HostPage> {
}
try {
_semaphore.acquire();
_hostingController.publishServer(
_gameController.username.text,
_hostingController.instance.value!.versionName
);
} catch(error) {
_showCannotUpdateGameServer(error);
} finally {
_semaphore.release();
}
}
@@ -278,12 +271,12 @@ class _HostingPageState extends RebootPageState<HostPage> {
void _showCannotCopyIp(Object error) => showInfoBar(
translations.hostShareIpMessageError(error.toString()),
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
void _showCannotUpdateGameServer(Object error) => showInfoBar(
translations.cannotUpdateGameServer(error.toString()),
severity: InfoBarSeverity.success,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
}

View File

@@ -1,21 +1,23 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsPage extends RebootPage {
@@ -31,91 +33,15 @@ class SettingsPage extends RebootPage {
RebootPageType get type => RebootPageType.settings;
@override
bool get hasButton => false;
bool hasButton(String? pageName) => false;
@override
RebootPageState<SettingsPage> createState() => _SettingsPageState();
@override
List<PageSetting> get settings => [
PageSetting(
name: translations.settingsClientName,
description: translations.settingsClientDescription,
children: [
PageSetting(
name: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription
),
PageSetting(
name: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription
),
PageSetting(
name: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription
),
PageSetting(
name: translations.settingsClientArgsName,
description: translations.settingsClientArgsDescription
),
],
),
PageSetting(
name: translations.settingsServerName,
description: translations.settingsServerSubtitle,
children: [
PageSetting(
name: translations.settingsServerFileName,
description: translations.settingsServerFileDescription
),
PageSetting(
name: translations.settingsServerPortName,
description: translations.settingsServerPortDescription
),
PageSetting(
name: translations.settingsServerMirrorName,
description: translations.settingsServerMirrorDescription
),
PageSetting(
name: translations.settingsServerTimerName,
description: translations.settingsServerTimerSubtitle
),
],
),
PageSetting(
name: translations.settingsUtilsName,
description: translations.settingsUtilsSubtitle,
children: [
PageSetting(
name: translations.settingsUtilsThemeName,
description: translations.settingsUtilsThemeDescription,
),
PageSetting(
name: translations.settingsUtilsLanguageName,
description: translations.settingsUtilsLanguageDescription,
),
PageSetting(
name: translations.settingsUtilsInstallationDirectoryName,
description: translations.settingsUtilsInstallationDirectorySubtitle,
content: translations.settingsUtilsInstallationDirectoryContent
),
PageSetting(
name: translations.settingsUtilsBugReportName,
description: translations.settingsUtilsBugReportSubtitle,
content: translations.settingsUtilsBugReportContent
),
PageSetting(
name: translations.settingsUtilsResetDefaultsName,
description: translations.settingsUtilsResetDefaultsSubtitle,
content: translations.settingsUtilsResetDefaultsContent
)
],
)
];
}
class _SettingsPageState extends RebootPageState<SettingsPage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
@@ -126,13 +52,29 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
List<Widget> get settings => [
_clientSettings,
_gameServerSettings,
_launcherUtilities
_launcherSettings,
_installationDirectory
];
SettingTile get _installationDirectory => SettingTile(
icon: Icon(
FluentIcons.folder_24_regular
),
title: Text(translations.settingsUtilsInstallationDirectoryName),
subtitle: Text(translations.settingsUtilsInstallationDirectorySubtitle),
content: Button(
onPressed: () => launchUrl(installationDirectory.uri),
child: Text(translations.settingsUtilsInstallationDirectoryContent),
)
);
SettingTile get _clientSettings => SettingTile(
title: translations.settingsClientName,
subtitle: translations.settingsClientDescription,
expandedContent: [
icon: Icon(
FluentIcons.desktop_24_regular
),
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
_createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
@@ -149,9 +91,11 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
controller: _settingsController.memoryLeakDll
),
SettingTile(
title: translations.settingsClientArgsName,
subtitle: translations.settingsClientArgsDescription,
isChild: true,
icon: Icon(
FluentIcons.text_box_settings_24_regular
),
title: Text(translations.settingsClientArgsName),
subtitle: Text(translations.settingsClientArgsDescription),
content: TextFormBox(
placeholder: translations.settingsClientArgsPlaceholder,
controller: _gameController.customLaunchArgs,
@@ -161,17 +105,23 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
);
SettingTile get _gameServerSettings => SettingTile(
title: translations.settingsServerName,
subtitle: translations.settingsServerSubtitle,
expandedContent: [
icon: Icon(
FluentIcons.server_24_regular
),
title: Text(translations.settingsServerName),
subtitle: Text(translations.settingsServerSubtitle),
children: [
_createFileSetting(
title: translations.settingsServerFileName,
description: translations.settingsServerFileDescription,
controller: _settingsController.gameServerDll
),
SettingTile(
title: translations.settingsServerPortName,
subtitle: translations.settingsServerPortDescription,
icon: Icon(
fluentUi.FluentIcons.number_field
),
title: Text(translations.settingsServerPortName),
subtitle: Text(translations.settingsServerPortDescription),
content: TextFormBox(
placeholder: translations.settingsServerPortName,
controller: _settingsController.gameServerPort,
@@ -179,22 +129,26 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
]
),
isChild: true
)
),
SettingTile(
title: translations.settingsServerMirrorName,
subtitle: translations.settingsServerMirrorDescription,
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
content: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _updateController.url,
validator: checkUpdateUrl
),
isChild: true
)
),
SettingTile(
title: translations.settingsServerTimerName,
subtitle: translations.settingsServerTimerSubtitle,
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
content: Obx(() => DropDownButton(
leading: Text(_updateController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
@@ -205,20 +159,46 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
_updateController.update(true);
}
)).toList()
)),
isChild: true
))
),
SettingTile(
icon: Icon(
FluentIcons.developer_board_24_regular
),
title: Text(translations.playAutomaticServerName),
subtitle: Text(translations.playAutomaticServerDescription),
contentWidth: null,
content: Obx(() => Row(
children: [
Text(
_hostingController.automaticServer.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _hostingController.automaticServer.value,
onChanged: (value) => _hostingController.automaticServer.value = value
),
],
)),
)
],
);
SettingTile get _launcherUtilities => SettingTile(
title: translations.settingsUtilsName,
subtitle: translations.settingsUtilsSubtitle,
expandedContent: [
SettingTile get _launcherSettings => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.settingsUtilsName),
subtitle: Text(translations.settingsUtilsSubtitle),
children: [
SettingTile(
title: translations.settingsUtilsLanguageName,
subtitle: translations.settingsUtilsLanguageDescription,
isChild: true,
icon: Icon(
FluentIcons.local_language_24_regular
),
title: Text(translations.settingsUtilsLanguageName),
subtitle: Text(translations.settingsUtilsLanguageDescription),
content: Obx(() => DropDownButton(
leading: Text(_getLocaleName(_settingsController.language.value)),
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
@@ -228,9 +208,11 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
))
),
SettingTile(
title: translations.settingsUtilsThemeName,
subtitle: translations.settingsUtilsThemeDescription,
isChild: true,
icon: Icon(
FluentIcons.dark_theme_24_regular
),
title: Text(translations.settingsUtilsThemeName),
subtitle: Text(translations.settingsUtilsThemeDescription),
content: Obx(() => DropDownButton(
leading: Text(_settingsController.themeMode.value.title),
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
@@ -240,27 +222,11 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
))
),
SettingTile(
title: translations.settingsUtilsInstallationDirectoryName,
subtitle: translations.settingsUtilsInstallationDirectorySubtitle,
isChild: true,
content: Button(
onPressed: () => launchUrl(installationDirectory.uri),
child: Text(translations.settingsUtilsInstallationDirectoryContent),
)
),
SettingTile(
title: translations.settingsUtilsBugReportName,
subtitle: translations.settingsUtilsBugReportSubtitle,
isChild: true,
content: Button(
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")),
child: Text(translations.settingsUtilsBugReportContent),
)
),
SettingTile(
title: translations.settingsUtilsResetDefaultsName,
subtitle: translations.settingsUtilsResetDefaultsSubtitle,
isChild: true,
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.settingsUtilsResetDefaultsName),
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
content: Button(
onPressed: () => showResetDialog(_settingsController.reset),
child: Text(translations.settingsUtilsResetDefaultsContent),
@@ -278,9 +244,12 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
return locale;
}
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
title: title,
subtitle: description,
SettingTile _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(title),
subtitle: Text(description),
content: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
@@ -288,8 +257,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
validator: checkDll,
extension: "dll",
folder: false
),
isChild: true
)
);
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart';
@@ -11,6 +12,9 @@ import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
import 'package:reboot_launcher/src/page/implementation/settings_page.dart';
final StreamController<void> pagesController = StreamController.broadcast();
bool hitBack = false;
final List<RebootPage> pages = [
const PlayPage(),
const HostPage(),
@@ -25,19 +29,47 @@ final RxInt pageIndex = RxInt(0);
final HashMap<int, GlobalKey> _pageKeys = HashMap();
GlobalKey appKey = GlobalKey();
GlobalKey get pageKey {
var index = pageIndex.value;
var key = _pageKeys[index];
final GlobalKey appKey = GlobalKey();
GlobalKey get pageKey => getPageKeyByIndex(pageIndex.value);
GlobalKey getPageKeyByIndex(int index) {
final key = _pageKeys[index];
if(key != null) {
return key;
}
var result = GlobalKey();
final result = GlobalKey();
_pageKeys[index] = result;
return result;
}
List<int> get pagesWithButtonIndexes => pages.where((page) => page.hasButton)
.map((page) => page.index)
.toList();
bool get hasPageButton => pages[pageIndex.value].hasButton(pageStack.lastOrNull);
final Queue<Object?> appStack = _createAppStack();
Queue _createAppStack() {
final queue = Queue();
var lastValue = pageIndex.value;
pageIndex.listen((index) {
if(!hitBack && lastValue != index) {
queue.add(lastValue);
pagesController.add(null);
}
hitBack = false;
lastValue = index;
});
return queue;
}
final Map<int, Queue<String>> _pagesStack = Map.fromEntries(List.generate(pages.length, (index) => MapEntry(index, Queue<String>())));
Queue<String> get pageStack => _pagesStack[pageIndex.value]!;
void addSubPageToStack(String pageName) {
final index = pageIndex.value;
final identifier = "${index}_$pageName";
appStack.add(identifier);
_pagesStack[index]!.add(identifier);
pagesController.add(null);
}

View File

@@ -10,7 +10,13 @@ const int _keyLength = 32;
String hashPassword(String plaintext) => BCrypt.hashpw(plaintext, BCrypt.gensalt());
bool checkPassword(String password, String hashedText) => BCrypt.checkpw(password, hashedText);
bool checkPassword(String password, String hashedText) {
try {
return BCrypt.checkpw(password, hashedText);
}catch(error) {
return false;
}
}
String aes256Encrypt(String plainText, String password) {
final random = Random.secure();

View File

@@ -6,9 +6,6 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final SupabaseClient _supabase = Supabase.instance.client;
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final File _executable = File("${assetsDirectory.path}\\misc\\watch.exe");
extension GameInstanceWatcher on GameInstance {
@@ -17,18 +14,24 @@ extension GameInstanceWatcher on GameInstance {
Process.killPid(observerPid!, ProcessSignal.sigabrt);
}
final hostingController = Get.find<HostingController>();
final gameController = Get.find<GameController>();
watchProcess(gamePid).then((value) async {
if(hosting) {
_onHostingStopped();
gameController.started.value = false;
gameController.instance.value?.kill();
if(_nestedHosting) {
hostingController.started.value = false;
hostingController.instance.value?.kill();
await Supabase.instance.client.from("hosting")
.delete()
.match({'id': hostingController.uuid});
}
_onGameStopped();
});
observerPid = await startBackgroundProcess(
executable: _executable,
args: [
_hostingController.uuid,
hostingController.uuid,
gamePid.toString(),
launcherPid?.toString() ?? "-1",
eacPid?.toString() ?? "-1",
@@ -37,19 +40,16 @@ extension GameInstanceWatcher on GameInstance {
);
}
void _onGameStopped() {
_gameController.started.value = false;
_gameController.instance.value?.kill();
if(linkedHosting) {
_onHostingStopped();
}
}
bool get _nestedHosting {
GameInstance? child = this;
while(child != null) {
if(child.hosting) {
return true;
}
Future<void> _onHostingStopped() async {
_hostingController.started.value = false;
_hostingController.instance.value?.kill();
await _supabase.from('hosts')
.delete()
.match({'id': _hostingController.uuid});
child = child.child;
}
return false;
}
}

52
gui/lib/src/util/dll.dart Normal file
View File

@@ -0,0 +1,52 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
final UpdateController _updateController = Get.find<UpdateController>();
Future<void> downloadCriticalDllInteractive(String filePath) async {
try {
final fileName = path.basename(filePath);
if (fileName == "reboot.dll") {
_updateController.update(true);
return;
}
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
await showInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
await downloadCriticalDll(fileName, filePath);
await showInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}catch(message) {
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showInfoBar(
translations.downloadDllError(error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
}
}

View File

@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart';
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
bool get isWin11 {
@@ -10,4 +13,7 @@ bool get isWin11 {
var intBuild = int.tryParse(result);
return intBuild != null && intBuild > 22000;
}
}
bool get isDarkMode
=> SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;

View File

@@ -1,3 +1,12 @@
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> openPortTutorial() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/en/PortForwarding.md"));
Future<void> openYoutubeTutorial() => launchUrl(Uri.parse("https://www.youtube.com/watch?v=nrVE2RB0qa4"));
Future<void> openDiscordServer() => launchUrl(Uri.parse("https://discord.gg/reboot"));
Future<void> openTutorials() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale"));
Future<void> openPortTutorial() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"));
Future<void> openBugReport() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues"));

View File

@@ -0,0 +1,8 @@
extension IterableExtension<E> on Iterable<E> {
E? firstWhereOrNull(bool test(E element)) {
for (E element in this) {
if (test(element)) return element;
}
return null;
}
}

View File

@@ -9,8 +9,8 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_name_input.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/version_name_input.dart';
class AddLocalVersion extends StatefulWidget {
const AddLocalVersion({Key? key})

View File

@@ -12,9 +12,8 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_build_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_name_input.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/version_name_input.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import 'package:windows_taskbar/windows_taskbar.dart';
@@ -65,7 +64,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
void _cancelDownload() {
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
_downloadPort?.send("kill");
_downloadPort?.send(kStopBuildDownloadSignal);
}
@override
@@ -125,15 +124,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
void _startDownload(BuildContext context) async {
try {
var build = _buildController.selectedBuild.value;
final build = _buildController.selectedBuild;
if(build == null){
return;
}
_status.value = DownloadStatus.downloading;
var communicationPort = ReceivePort();
final communicationPort = ReceivePort();
communicationPort.listen((message) {
if(message is ArchiveDownloadProgress) {
if(message is FortniteBuildDownloadProgress) {
_onProgress(message.progress, message.minutesLeft, message.extracting);
}else if(message is SendPort) {
_downloadPort = message;
@@ -141,12 +140,12 @@ class _AddServerVersionState extends State<AddServerVersion> {
_onDownloadError(message, null);
}
});
var options = ArchiveDownloadOptions(
build.link,
final options = FortniteBuildDownloadOptions(
build,
Directory(_pathController.text),
communicationPort.sendPort
);
var errorPort = ReceivePort();
final errorPort = ReceivePort();
errorPort.listen((message) => _onDownloadError(message, null));
await Isolate.spawn(
downloadArchiveBuild,
@@ -173,6 +172,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
}
void _onDownloadError(Object? error, StackTrace? stackTrace) {
_cancelDownload();
if (!mounted) {
return;
}
@@ -203,7 +203,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
}
Widget get _progressBody {
var timeLeft = _timeLeft.value;
final timeLeft = _timeLeft.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -256,10 +256,14 @@ class _AddServerVersionState extends State<AddServerVersion> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BuildSelector(
onSelected: _updateFormDefaults
_buildSelectorType(),
const SizedBox(
height: 16.0
),
_buildSelector(),
const SizedBox(
height: 16.0
),
@@ -287,6 +291,70 @@ class _AddServerVersionState extends State<AddServerVersion> {
],
);
Widget _buildSelectorType() => InfoLabel(
label: translations.source,
child: Obx(() => ComboBox<FortniteBuildSource>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: _buildSources,
value: _buildController.selectedBuildSource,
onChanged: (value) {
if(value == null){
return;
}
_buildController.selectedBuildSource = value;
_updateFormDefaults();
}
))
);
Widget _buildSelector() => InfoLabel(
label: translations.build,
child: Obx(() => ComboBox<FortniteBuild>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: _builds,
value: _buildController.selectedBuild,
onChanged: (value) {
if(value == null){
return;
}
_buildController.selectedBuild = value;
_updateFormDefaults();
}
))
);
List<ComboBoxItem<FortniteBuild>> get _builds => _buildController.builds!
.where((element) => element.source == _buildController.selectedBuild?.source)
.map((element) => _buildItem(element))
.toList();
ComboBoxItem<FortniteBuild> _buildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
value: element,
child: Text(element.version.toString())
);
List<ComboBoxItem<FortniteBuildSource>> get _buildSources => FortniteBuildSource.values
.map((element) => _buildSourceItem(element))
.toList();
ComboBoxItem<FortniteBuildSource> _buildSourceItem(FortniteBuildSource element) => ComboBoxItem<FortniteBuildSource>(
value: element,
child: Text(_getBuildSourceName(element))
);
String _getBuildSourceName(FortniteBuildSource element) {
switch(element) {
case FortniteBuildSource.archive:
return translations.archive;
case FortniteBuildSource.manifest:
return translations.manifest;
}
}
List<DialogButton> get _stopButton => [
DialogButton(
text: "Stop",
@@ -300,17 +368,17 @@ class _AddServerVersionState extends State<AddServerVersion> {
}
await _fetchFuture;
var bestDisk = _diskSpace.disks
final bestDisk = _diskSpace.disks
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
var build = _buildController.selectedBuild.value;
if(build== null){
final build = _buildController.selectedBuild;
if(build == null){
return;
}
var pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
_pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
var buildName = build.version.toString();
final buildName = build.version.toString();
_nameController.text = buildName;
_nameController.selection = TextSelection.collapsed(offset: buildName.length);
_formKey.currentState?.validate();

View File

@@ -1,136 +0,0 @@
import 'package:auto_animated_list/auto_animated_list.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:skeletons/skeletons.dart';
class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0;
static const double kDefaultHeaderHeight = 72;
final String? title;
final TextStyle? titleStyle;
final dynamic subtitle;
final TextStyle? subtitleStyle;
final Widget? content;
final double? contentWidth;
final List<Widget>? expandedContent;
final double expandedContentHeaderHeight;
final bool isChild;
const SettingTile(
{Key? key,
this.title,
this.titleStyle,
this.subtitle,
this.subtitleStyle,
this.content,
this.contentWidth = kDefaultContentWidth,
this.expandedContentHeaderHeight = kDefaultHeaderHeight,
this.expandedContent,
this.isChild = false})
: assert((title == null && subtitle == null) || (title != null && subtitle != null), "title and subtitle can only be null together"),
assert(subtitle == null || subtitle is String || subtitle is Widget, "subtitle can only be null, String or Widget"),
assert(subtitle is! Widget || subtitleStyle == null, "subtitleStyle must be null if subtitle is a widget"),
super(key: key);
@override
State<SettingTile> createState() => _SettingTileState();
}
class _SettingTileState extends State<SettingTile> {
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 1000
),
child: () {
if (widget.expandedContent == null || widget.expandedContent?.isEmpty == true) {
return _contentCard;
}
return Expander(
initiallyExpanded: true,
headerShape: (open) => const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
),
header: SizedBox(
height: widget.expandedContentHeaderHeight,
child: _buildTile(false)
),
trailing: _trailing,
content: _expandedContent
);
}()
);
}
Widget get _expandedContent {
var expandedContents = widget.expandedContent!;
var separatedContents = List.generate(expandedContents.length, (index) => expandedContents[index]);
return AutoAnimatedList<Widget>(
scrollDirection: Axis.vertical,
shrinkWrap: true,
items: separatedContents,
itemBuilder: (context, child, index, animation) => FadeTransition(
opacity: animation,
child: child
)
);
}
Widget get _trailing =>
SizedBox(width: widget.contentWidth, child: widget.content);
Widget get _contentCard {
if (widget.isChild) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: _buildTile(true)
);
}
return Card(
borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)),
child: _buildTile(true)
);
}
Widget _buildTile(bool trailing) {
return ListTile(
title: widget.title == null ? _skeletonTitle : _title,
subtitle: widget.title == null ? _skeletonSubtitle : _subtitle,
trailing: trailing ? _trailing : null
);
}
Widget get _title => Text(
widget.title!,
style:
widget.titleStyle ?? FluentTheme.of(context).typography.subtitle
);
Widget get _skeletonTitle => const SkeletonLine(
style: SkeletonLineStyle(
padding: EdgeInsets.only(
right: 24.0
),
height: 18
),
);
Widget get _subtitle => widget.subtitle is Widget ? widget.subtitle : Text(
widget.subtitle!,
style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body
);
Widget get _skeletonSubtitle => const SkeletonLine(
style: SkeletonLineStyle(
padding: EdgeInsets.only(
top: 8.0,
bottom: 8.0,
right: 24.0
),
height: 13
)
);
}

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:async/async.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:process_run/shell.dart';
import 'package:reboot_common/common.dart';
@@ -13,13 +14,15 @@ import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart' as messenger;
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/daemon.dart';
import 'package:reboot_launcher/src/util/dll.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/util/tutorial.dart';
import 'package:reboot_launcher/src/util/watch.dart';
class LaunchButton extends StatefulWidget {
final bool host;
@@ -86,15 +89,15 @@ class _LaunchButtonState extends State<LaunchButton> {
}
_setStarted(widget.host, true);
for (var injectable in _Injectable.values) {
for (final injectable in _Injectable.values) {
if(await _getDllFileOrStop(injectable, widget.host) == null) {
return;
}
}
try {
var version = _gameController.selectedVersion!;
var executable = await version.executable;
final version = _gameController.selectedVersion!;
final executable = await version.executable;
if(executable == null){
_onStop(
reason: _StopReason.missingExecutableError,
@@ -103,7 +106,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
var authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(_pageType, false);
final authenticatorResult = _authenticatorController.started() || await _authenticatorController.toggleInteractive(false);
if(!authenticatorResult){
_onStop(
reason: _StopReason.authenticatorError
@@ -111,7 +114,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
var matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(_pageType, false);
final matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive(false);
if(!matchmakerResult){
_onStop(
reason: _StopReason.matchmakerError
@@ -119,9 +122,9 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
var automaticallyStartedServer = await _startMatchMakingServer(version);
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
if(automaticallyStartedServer || widget.host){
final linkedHostingInstance = await _startMatchMakingServer(version);
await _startGameProcesses(version, widget.host, linkedHostingInstance);
if(linkedHostingInstance != null || widget.host){
_showLaunchingGameServerWidget();
}
} catch (exception, stackTrace) {
@@ -133,45 +136,55 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<bool> _startMatchMakingServer(FortniteVersion version) async {
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version) async {
if(widget.host){
return false;
return null;
}
var matchmakingIp = _matchmakerController.gameServerAddress.text;
final matchmakingIp = _matchmakerController.gameServerAddress.text;
if(!isLocalHost(matchmakingIp)) {
return false;
return null;
}
if(_hostingController.started()){
return false;
return null;
}
if(!_hostingController.automaticServer()) {
return null;
}
_startGameProcesses(version, true, false); // Do not await
final instance = await _startGameProcesses(version, true, null);
_setStarted(true, true);
return true;
return instance;
}
Future<void> _startGameProcesses(FortniteVersion version, bool host, bool linkedHosting) async {
var launcherProcess = await _createLauncherProcess(version);
var eacProcess = await _createEacProcess(version);
var executable = await version.executable;
var gameProcess = await _createGameProcess(executable!.path, host);
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameInstance? linkedHosting) async {
final launcherProcess = await _createLauncherProcess(version);
final eacProcess = await _createEacProcess(version);
final executable = await version.executable;
final gameProcess = await _createGameProcess(executable!.path, host);
if(gameProcess == null) {
return;
return null;
}
var instance = GameInstance(version.name, gameProcess, launcherProcess, eacProcess, host, linkedHosting);
final instance = GameInstance(
versionName: version.name,
gamePid: gameProcess,
launcherPid: launcherProcess,
eacPid: eacProcess,
hosting: host,
child: linkedHosting
);
instance.startObserver();
if(host){
_hostingController.discardServer();
_hostingController.instance.value = instance;
_hostingController.saveInstance();
}else{
_gameController.instance.value = instance;
_gameController.saveInstance();
}
_injectOrShowError(_Injectable.sslBypass, host);
return instance;
}
Future<int?> _createGameProcess(String gamePath, bool host) async {
@@ -179,63 +192,68 @@ class _LaunchButtonState extends State<LaunchButton> {
return null;
}
var gameArgs = createRebootArgs(
final gameArgs = createRebootArgs(
_gameController.username.text,
_gameController.password.text,
host,
_hostingController.headless.value,
_gameController.customLaunchArgs.text
);
var gameProcess = await Process.start(
final gameProcess = await Process.start(
gamePath,
gameArgs
);
gameProcess
..exitCode.then((_) => _onStop(reason: _StopReason.normal))
..outLines.forEach((line) => _onGameOutput(line, host))
..errLines.forEach((line) => _onGameOutput(line, host));
..exitCode.then((_) => _onStop(reason: _StopReason.exitCode))
..outLines.forEach((line) => _onGameOutput(line, host, false))
..errLines.forEach((line) => _onGameOutput(line, host, true));
return gameProcess.pid;
}
Future<int?> _createLauncherProcess(FortniteVersion version) async {
var launcherFile = version.launcher;
final launcherFile = version.launcher;
if (launcherFile == null) {
return null;
}
var launcherProcess = await Process.start(launcherFile.path, []);
var pid = launcherProcess.pid;
final launcherProcess = await Process.start(launcherFile.path, []);
final pid = launcherProcess.pid;
suspend(pid);
return pid;
}
Future<int?> _createEacProcess(FortniteVersion version) async {
var eacFile = version.eacExecutable;
final eacFile = version.eacExecutable;
if (eacFile == null) {
return null;
}
var eacProcess = await Process.start(eacFile.path, []);
var pid = eacProcess.pid;
final eacProcess = await Process.start(eacFile.path, []);
final pid = eacProcess.pid;
suspend(pid);
return pid;
}
void _onGameOutput(String line, bool host) {
if (line.contains(shutdownLine)) {
void _onGameOutput(String line, bool host, bool error) {
if(kDebugMode) {
print("${error ? '[ERROR]' : '[MESSAGE]'} $line");
}
if (line.contains(kShutdownLine)) {
_onStop(
reason: _StopReason.normal
);
return;
}
if(corruptedBuildErrors.any((element) => line.contains(element))){
if(kCorruptedBuildErrors.any((element) => line.contains(element))){
_onStop(
reason: _StopReason.corruptedVersionError
);
return;
}
if(cannotConnectErrors.any((element) => line.contains(element))){
if(kCannotConnectErrors.any((element) => line.contains(element))){
_onStop(
reason: _StopReason.tokenError
);
@@ -251,39 +269,40 @@ class _LaunchButtonState extends State<LaunchButton> {
}
_injectOrShowError(_Injectable.memoryFix, host);
var instance = host ? _hostingController.instance.value : _gameController.instance.value;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
instance?.launched = true;
instance?.tokenError = false;
}
}
Future<void> _onGameServerInjected() async {
var theme = FluentTheme.of(appKey.currentContext!);
final theme = FluentTheme.of(appKey.currentContext!);
showInfoBar(
translations.waitingForGameServer,
loading: true,
duration: null
);
var gameServerPort = _settingsController.gameServerPort.text;
var localPingResult = await pingGameServer(
final gameServerPort = _settingsController.gameServerPort.text;
final localPingResult = await pingGameServer(
"127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 1)
timeout: const Duration(minutes: 2)
);
if(!localPingResult) {
showInfoBar(
translations.gameServerStartWarning,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
return;
}
_matchmakerController.joinLocalHost();
var accessible = await _checkGameServer(theme, gameServerPort);
final accessible = await _checkGameServer(theme, gameServerPort);
if(!accessible) {
showInfoBar(
translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
return;
}
@@ -295,7 +314,7 @@ class _LaunchButtonState extends State<LaunchButton> {
showInfoBar(
translations.gameServerStarted,
severity: InfoBarSeverity.success,
duration: snackbarLongDuration
duration: infoBarLongDuration
);
}
@@ -305,13 +324,13 @@ class _LaunchButtonState extends State<LaunchButton> {
loading: true,
duration: null
);
var publicIp = await Ipify.ipv4();
var externalResult = await pingGameServer("$publicIp:$gameServerPort");
final publicIp = await Ipify.ipv4();
final externalResult = await pingGameServer("$publicIp:$gameServerPort");
if(externalResult) {
return true;
}
var future = pingGameServer(
final future = pingGameServer(
"$publicIp:$gameServerPort",
timeout: const Duration(days: 365)
);
@@ -333,15 +352,12 @@ class _LaunchButtonState extends State<LaunchButton> {
await _operation?.cancel();
await _authenticatorController.worker?.cancel();
await _matchmakerController.worker?.cancel();
var instance = host ? _hostingController.instance.value : _gameController.instance.value;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null){
if(instance.linkedHosting){
_onStop(
_onStop(
reason: _StopReason.normal,
host: true
);
}
);
instance.kill();
if(host){
_hostingController.instance.value = null;
@@ -355,7 +371,10 @@ class _LaunchButtonState extends State<LaunchButton> {
_hostingController.discardServer();
}
messenger.removeMessageByPage(_pageType.index);
if(reason == _StopReason.normal) {
messenger.removeMessageByPage(_pageType.index);
}
switch(reason) {
case _StopReason.authenticatorError:
case _StopReason.matchmakerError:
@@ -365,49 +384,53 @@ class _LaunchButtonState extends State<LaunchButton> {
showInfoBar(
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
);
break;
case _StopReason.missingExecutableError:
showInfoBar(
translations.missingExecutableError,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
);
break;
case _StopReason.exitCode:
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null && !instance.launched) {
showInfoBar(
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
}
break;
case _StopReason.corruptedVersionError:
showInfoBar(
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
);
break;
case _StopReason.missingDllError:
showInfoBar(
translations.missingDllError(error!),
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
);
break;
case _StopReason.corruptedDllError:
showInfoBar(
translations.corruptedDllError(error!),
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
);
break;
case _StopReason.tokenError:
showInfoBar(
translations.tokenError,
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError:
showInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: snackbarLongDuration,
duration: infoBarLongDuration,
);
break;
}
@@ -415,14 +438,14 @@ class _LaunchButtonState extends State<LaunchButton> {
}
Future<void> _injectOrShowError(_Injectable injectable, bool hosting) async {
var instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
final instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
if (instance == null) {
return;
}
try {
var gameProcess = instance.gamePid;
var dllPath = await _getDllFileOrStop(injectable, hosting);
final gameProcess = instance.gamePid;
final dllPath = await _getDllFileOrStop(injectable, hosting);
if(dllPath == null) {
return;
}
@@ -452,17 +475,13 @@ class _LaunchButtonState extends State<LaunchButton> {
}
Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async {
var path = _getDllPath(injectable);
var file = File(path);
final path = _getDllPath(injectable);
final file = File(path);
if(await file.exists()) {
return file;
}
_onStop(
reason: _StopReason.missingDllError,
host: host,
error: path
);
await downloadCriticalDllInteractive(path);
return null;
}
@@ -472,15 +491,6 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
OverlayEntry showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) => messenger.showInfoBar(
text,
pageType: _pageType,
severity: severity,
loading: loading,
duration: duration,
action: action
);
RebootPageType get _pageType => widget.host ? RebootPageType.host : RebootPageType.play;
}
@@ -489,12 +499,11 @@ enum _StopReason {
missingVersionError,
missingExecutableError,
corruptedVersionError,
missingDllError,
corruptedDllError,
authenticatorError,
matchmakerError,
tokenError,
unknownError
unknownError, exitCode
}
enum _Injectable {

View File

@@ -1,100 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/implementation/profile.dart';
class ProfileWidget extends StatefulWidget {
const ProfileWidget({Key? key}) : super(key: key);
@override
State<ProfileWidget> createState() => _ProfileWidgetState();
}
class _ProfileWidgetState extends State<ProfileWidget> {
final GameController _gameController = Get.find<GameController>();
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 12.0
),
child: Button(
style: ButtonStyle(
padding: ButtonState.all(EdgeInsets.zero),
backgroundColor: ButtonState.all(Colors.transparent),
border: ButtonState.all(const BorderSide(color: Colors.transparent))
),
onPressed: () async {
if(await showProfileForm(context)) {
setState(() {});
}
},
child: Row(
children: [
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
shape: BoxShape.circle
),
child: Image.asset("assets/images/user.png")
),
const SizedBox(
width: 12.0,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_username,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w600
),
maxLines: 1
),
Text(
_email,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w100
),
maxLines: 1
)
],
)
],
),
),
);
String get _username {
var username = _gameController.username.text;
if(username.isEmpty) {
return kDefaultPlayerName;
}
var atIndex = username.indexOf("@");
if(atIndex == -1) {
return username.substring(0, 1).toUpperCase() + username.substring(1);
}
var result = username.substring(0, atIndex);
return result.substring(0, 1).toUpperCase() + result.substring(1);
}
String get _email {
var username = _gameController.username.text;
if(username.isEmpty) {
return "$kDefaultPlayerName@projectreboot.dev";
}
if(username.contains("@")) {
return username.toLowerCase();
}
return "$username@projectreboot.dev".toLowerCase();
}
}

View File

@@ -0,0 +1,106 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/implementation/profile.dart';
class ProfileWidget extends StatefulWidget {
const ProfileWidget({Key? key}) : super(key: key);
@override
State<ProfileWidget> createState() => _ProfileWidgetState();
}
class _ProfileWidgetState extends State<ProfileWidget> {
final GameController _gameController = Get.find<GameController>();
@override
Widget build(BuildContext context) => HoverButton(
margin: const EdgeInsets.all(8.0),
onPressed: () async {
if(await showProfileForm(context)) {
setState(() {});
}
},
builder: (context, states) => Container(
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context),
states,
transparentWhenNone: true,
),
borderRadius: BorderRadius.all(Radius.circular(6.0))
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 8.0
),
child: Row(
children: [
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
shape: BoxShape.circle
),
child: Image.asset("assets/images/user.png")
),
const SizedBox(
width: 12.0,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_username,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w600
),
maxLines: 1
),
Text(
_email,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w100
),
maxLines: 1
)
],
)
],
),
),
)
);
String get _username {
var username = _gameController.username.text;
if(username.isEmpty) {
return kDefaultPlayerName;
}
var atIndex = username.indexOf("@");
if(atIndex == -1) {
return username.substring(0, 1).toUpperCase() + username.substring(1);
}
var result = username.substring(0, atIndex);
return result.substring(0, 1).toUpperCase() + result.substring(1);
}
String get _email {
var username = _gameController.username.text;
if(username.isEmpty) {
return "$kDefaultPlayerName@projectreboot.dev";
}
if(username.contains("@")) {
return username.toLowerCase();
}
return "$username@projectreboot.dev".toLowerCase();
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
@@ -5,7 +7,6 @@ import 'package:reboot_launcher/src/controller/authenticator_controller.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/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
class ServerButton extends StatefulWidget {
@@ -18,27 +19,43 @@ class ServerButton extends StatefulWidget {
class _ServerButtonState extends State<ServerButton> {
late final ServerController _controller = widget.authenticator ? Get.find<AuthenticatorController>() : Get.find<MatchmakerController>();
late final StreamController<void> _textController = StreamController.broadcast();
late final void Function() _listener = () => _textController.add(null);
@override
void initState() {
_controller.port.addListener(_listener);
super.initState();
}
@override
void dispose() {
_controller.port.removeListener(_listener);
_textController.close();
super.dispose();
}
@override
Widget build(BuildContext context) => Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
width: double.infinity,
child: Obx(() => SizedBox(
height: 48,
child: Button(
child: Align(
alignment: Alignment.center,
child: Text(_buttonText),
),
onPressed: () => _controller.toggleInteractive(widget.authenticator ? RebootPageType.authenticator : RebootPageType.matchmaker)
),
)),
),
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
height: 48,
width: double.infinity,
child: Button(
child: Align(
alignment: Alignment.center,
child: StreamBuilder(
stream: _textController.stream,
builder: (context, snapshot) => Obx(() => Text(_buttonText))
),
),
onPressed: () => _controller.toggleInteractive()
)
)
);
String get _buttonText {
if(_controller.type.value == ServerType.local){
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == _controller.defaultPort){
return translations.checkServer(_controller.controllerName);
}

View File

@@ -0,0 +1,164 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:skeletons/skeletons.dart';
class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0;
static const double kDefaultHeaderHeight = 72;
final void Function()? onPressed;
final Icon icon;
final Text? title;
final Text? subtitle;
final Widget? content;
final double? contentWidth;
final List<Widget>? children;
const SettingTile({
this.onPressed,
required this.icon,
required this.title,
required this.subtitle,
this.content,
this.contentWidth = kDefaultContentWidth,
this.children
});
@override
State<SettingTile> createState() => _SettingTileState();
}
class _SettingTileState extends State<SettingTile> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
bottom: 4.0
),
child: HoverButton(
onPressed: _buildOnPressed(),
builder: (context, states) => Container(
height: 80,
width: double.infinity,
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context),
states,
transparentWhenNone: true,
),
borderRadius: BorderRadius.all(Radius.circular(4.0))
),
child: Card(
borderRadius: const BorderRadius.all(
Radius.circular(4.0)
),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 12.0,
vertical: 6.0
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
widget.icon,
const SizedBox(width: 16.0),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.title == null ? _skeletonTitle : widget.title!,
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
],
),
const Spacer(),
_trailing
],
),
)
)
),
),
);
}
void Function()? _buildOnPressed() {
if(widget.onPressed != null) {
return widget.onPressed;
}
final children = widget.children;
if (children == null) {
return null;
}
return () async {
await Navigator.of(context).push(PageRouteBuilder(
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
settings: RouteSettings(
name: widget.title?.data
),
pageBuilder: (context, incoming, outgoing) => ListView.builder(
itemCount: children.length,
itemBuilder: (context, index) => children[index]
)
));
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value);
};
}
Widget get _trailing {
final hasContent = widget.content != null;
final hasChildren = widget.children?.isNotEmpty == true;
final hasListener = widget.onPressed != null;
if(hasContent && hasChildren) {
return Row(
children: [
SizedBox(
width: widget.contentWidth,
child: widget.content
),
const SizedBox(width: 16.0),
Icon(
FluentIcons.chevron_right_24_regular
)
],
);
}
if (hasContent) {
return SizedBox(
width: widget.contentWidth,
child: widget.content
);
}
if (hasChildren || hasListener) {
return Icon(
FluentIcons.chevron_right_24_regular
);
}
return const SizedBox.shrink();
}
Widget get _skeletonTitle => const SkeletonLine(
style: SkeletonLineStyle(
padding: EdgeInsets.only(
right: 24.0
),
height: 18
),
);
Widget get _skeletonSubtitle => const SkeletonLine(
style: SkeletonLineStyle(
padding: EdgeInsets.only(
top: 8.0,
bottom: 8.0,
right: 24.0
),
height: 13
)
);
}

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/os/buttons.dart';
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
import 'package:system_theme/system_theme.dart';
class WindowTitleBar extends StatelessWidget {

View File

@@ -1,8 +1,8 @@
import 'package:bitsdojo_window/bitsdojo_window.dart' show appWindow;
import 'package:flutter/material.dart';
import 'icons.dart';
import 'mouse.dart';
import 'title_bar_icons.dart';
import 'title_bar_mouse.dart';
typedef WindowButtonIconBuilder = Widget Function(
WindowButtonContext buttonContext);

View File

@@ -1,50 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart';
class BuildSelector extends StatefulWidget {
final Function() onSelected;
const BuildSelector({Key? key, required this.onSelected}) : super(key: key);
@override
State<BuildSelector> createState() => _BuildSelectorState();
}
class _BuildSelectorState extends State<BuildSelector> {
final BuildController _buildController = Get.find<BuildController>();
@override
Widget build(BuildContext context) {
return InfoLabel(
label: translations.build,
child: Obx(() => ComboBox<FortniteBuild>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: _items,
value: _buildController.selectedBuild.value,
onChanged: (value) {
if(value == null){
return;
}
_buildController.selectedBuild.value = value;
widget.onSelected();
}
))
);
}
List<ComboBoxItem<FortniteBuild>> get _items =>_buildController.builds!
.map((element) => _buildItem(element))
.toList();
ComboBoxItem<FortniteBuild> _buildItem(FortniteBuild element) {
return ComboBoxItem<FortniteBuild>(
value: element,
child: Text(element.version.toString())
);
}
}

View File

@@ -1,48 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/abstract/page_setting.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
SettingTile get versionSelectorSettingTile => SettingTile(
title: translations.addVersionName,
subtitle: translations.addVersionDescription,
content: const VersionSelector(),
expandedContent: [
SettingTile(
title: translations.addLocalBuildName,
subtitle: translations.addLocalBuildDescription,
content: Button(
onPressed: VersionSelector.openAddDialog,
child: Text(translations.addLocalBuildContent)
),
isChild: true
),
SettingTile(
title: translations.downloadBuildName,
subtitle: translations.downloadBuildDescription,
content: Button(
onPressed: VersionSelector.openDownloadDialog,
child: Text(translations.downloadBuildContent)
),
isChild: true
)
]
);
PageSetting get versionSelectorRebootSetting => PageSetting(
name: translations.addVersionName,
description: translations.addVersionDescription,
children: [
PageSetting(
name: translations.addLocalBuildName,
description: translations.addLocalBuildDescription,
content: translations.addLocalBuildContent
),
PageSetting(
name: translations.downloadBuildName,
description: translations.downloadBuildDescription,
content: translations.downloadBuildContent
)
]
);

View File

@@ -11,9 +11,9 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/version/add_local_version.dart';
import 'package:reboot_launcher/src/widget/version/add_server_version.dart';
import 'package:reboot_launcher/src/widget/add_local_version.dart';
import 'package:reboot_launcher/src/widget/add_server_version.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget {
@@ -37,7 +37,12 @@ class _VersionSelectorState extends State<VersionSelector> {
final FlyoutController _flyoutController = FlyoutController();
@override
Widget build(BuildContext context) => Obx(() => _createOptionsMenu(
Widget build(BuildContext context) => Obx(() {
if(_gameController.hasNoVersions) {
return const SizedBox();
}
return _createOptionsMenu(
version: _gameController.selectedVersion,
close: false,
child: FlyoutTarget(
@@ -47,18 +52,13 @@ class _VersionSelectorState extends State<VersionSelector> {
items: _createSelectorItems(context)
),
)
));
);
});
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) => _gameController.hasNoVersions ? [_createDefaultVersionItem()]
: _gameController.versions.value
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) => _gameController.versions.value
.map((version) => _createVersionItem(context, version))
.toList();
MenuFlyoutItem _createDefaultVersionItem() => MenuFlyoutItem(
text: Text(translations.noVersions),
onPressed: () {}
);
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
text: _createOptionsMenu(
version: version,

View File

@@ -0,0 +1,61 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart';
SettingTile get versionSelectSettingTile => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.manageVersionsName),
subtitle: Text(translations.manageVersionsDescription),
content: const VersionSelector(),
children: [
_selectVersionTile,
_addLocalTile,
_downloadTile
],
);
Widget get _selectVersionTile => Obx(() {
final gameController = Get.find<GameController>();
if(gameController.hasNoVersions) {
return const SizedBox();
}
return SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.selectFortniteName),
subtitle: Text(translations.selectFortniteDescription),
content: const VersionSelector()
);
});
SettingTile get _downloadTile => SettingTile(
icon: Icon(
FluentIcons.arrow_download_24_regular
),
title: Text(translations.downloadBuildName),
subtitle: Text(translations.downloadBuildDescription),
content: Button(
onPressed: VersionSelector.openDownloadDialog,
child: Text(translations.downloadBuildContent)
)
);
SettingTile get _addLocalTile => SettingTile(
icon: Icon(
FluentIcons.folder_add_24_regular
),
title: Text(translations.addLocalBuildName),
subtitle: Text(translations.addLocalBuildDescription),
content: Button(
onPressed: VersionSelector.openAddDialog,
child: Text(translations.addLocalBuildContent)
)
);