This commit is contained in:
Alessandro Autiero
2024-06-01 16:26:00 +02:00
parent d478650e9b
commit efb508bd0c
243 changed files with 486662 additions and 2948 deletions

View File

@@ -1,57 +0,0 @@
import 'dart:io';
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:supabase_flutter/supabase_flutter.dart';
final File _executable = File("${assetsDirectory.path}\\misc\\watch.exe");
extension GameInstanceWatcher on GameInstance {
Future<void> startObserver() async {
if(observerPid != null) {
Process.killPid(observerPid!, ProcessSignal.sigabrt);
}
final hostingController = Get.find<HostingController>();
final gameController = Get.find<GameController>();
watchProcess(gamePid).then((value) async {
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});
}
});
final process = await startProcess(
executable: _executable,
args: [
hostingController.uuid,
gamePid.toString(),
launcherPid?.toString() ?? "-1",
eacPid?.toString() ?? "-1",
hosting.toString()
],
);
observerPid = process.pid;
}
bool get _nestedHosting {
GameInstance? child = this;
while(child != null) {
if(child.hosting) {
return true;
}
child = child.child;
}
return false;
}
}

View File

@@ -9,33 +9,49 @@ 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 {
final Map<String, Future<void>> _operations = {};
Future<void> downloadCriticalDllInteractive(String filePath) {
final old = _operations[filePath];
if(old != null) {
return old;
}
final newRun = _downloadCriticalDllInteractive(filePath);
_operations[filePath] = newRun;
return newRun;
}
Future<void> _downloadCriticalDllInteractive(String filePath) async {
final fileName = path.basename(filePath).toLowerCase();
InfoBarEntry? entry;
try {
final fileName = path.basename(filePath).toLowerCase();
if (fileName == "reboot.dll") {
_updateController.update(true);
await _updateController.update(true);
return;
}
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
await showInfoBar(
entry = showInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
await downloadCriticalDll(fileName, filePath);
await showInfoBar(
entry.close();
entry = await showInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}catch(message) {
entry?.close();
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()),
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
@@ -48,5 +64,7 @@ Future<void> downloadCriticalDllInteractive(String filePath) async {
)
);
await completer.future;
}finally {
_operations.remove(fileName);
}
}
}

View File

@@ -0,0 +1,202 @@
import 'package:flutter/services.dart';
extension UnrealEngineKeyboard on PhysicalKeyboardKey {
static const Map<int, String> _keyboardKeyNames = <int, String>{
0x0007003a: 'F1',
0x0007003b: 'F2',
0x0007003c: 'F3',
0x0007003d: 'F4',
0x0007003e: 'F5',
0x0007003f: 'F6',
0x00070040: 'F7',
0x00070041: 'F8',
0x00070042: 'F9',
0x00070043: 'F10',
0x00070044: 'F11',
0x00070045: 'F12',
0x00070004: 'A',
0x00070005: 'B',
0x00070006: 'C',
0x00070007: 'D',
0x00070008: 'E',
0x00070009: 'F',
0x0007000a: 'G',
0x0007000b: 'H',
0x0007000c: 'I',
0x0007000d: 'J',
0x0007000e: 'K',
0x0007000f: 'L',
0x00070010: 'M',
0x00070011: 'N',
0x00070012: 'O',
0x00070013: 'P',
0x00070014: 'Q',
0x00070015: 'R',
0x00070016: 'S',
0x00070017: 'T',
0x00070018: 'U',
0x00070019: 'V',
0x0007001a: 'W',
0x0007001b: 'X',
0x0007001c: 'Y',
0x0007001d: 'Z',
0x0007001e: 'one',
0x0007001f: 'two',
0x00070020: 'three',
0x00070021: 'four',
0x00070022: 'five',
0x00070023: 'six',
0x00070024: 'seven',
0x00070025: 'eight',
0x00070026: 'nine',
0x00070027: 'zero',
0x00070028: 'Enter',
0x00070029: 'Escape',
0x0007002a: 'Backspace',
0x0007002b: 'Tab',
0x0007002c: 'SpaceBar',
0x0007002d: 'Minus',
0x0007002e: 'Equals',
0x0007002f: 'LeftBracket',
0x00070030: 'RightBracket',
0x00070031: 'Backslash',
0x00070033: 'Semicolon',
0x00070034: 'Quote',
0x00070036: 'Comma',
0x00070037: 'Period',
0x00070038: 'Slash',
0x00070039: 'CapsLock',
0x00070047: 'ScrollLock',
0x00070048: 'Pause',
0x00070049: 'Insert',
0x0007004a: 'Home',
0x0007004b: 'PageUp',
0x0007004c: 'Delete',
0x0007004d: 'End',
0x0007004e: 'PageDown',
0x00070053: 'NumLock',
0x00070054: 'Divide',
0x00070055: 'Multiply',
0x00070056: 'Subtract',
0x00070057: 'Add',
0x00070058: 'Enter',
0x00070059: 'NumPadOne',
0x0007005a: 'NumPadTwo',
0x0007005b: 'NumPadThree',
0x0007005c: 'NumPadFour',
0x0007005d: 'NumPadFive',
0x0007005e: 'NumPadSix',
0x0007005f: 'NumPadSeven',
0x00070060: 'NumPadEight',
0x00070061: 'NumPadNine',
0x00070062: 'NumPadZero',
0x00070063: 'Decimal',
0x00070064: 'Backslash'
};
static const Map<int, String> _keyboardKeyPrettyNames = <int, String>{
0x0007003a: 'F1',
0x0007003b: 'F2',
0x0007003c: 'F3',
0x0007003d: 'F4',
0x0007003e: 'F5',
0x0007003f: 'F6',
0x00070040: 'F7',
0x00070041: 'F8',
0x00070042: 'F9',
0x00070043: 'F10',
0x00070044: 'F11',
0x00070045: 'F12',
0x00070004: 'A',
0x00070005: 'B',
0x00070006: 'C',
0x00070007: 'D',
0x00070008: 'E',
0x00070009: 'F',
0x0007000a: 'G',
0x0007000b: 'H',
0x0007000c: 'I',
0x0007000d: 'J',
0x0007000e: 'K',
0x0007000f: 'L',
0x00070010: 'M',
0x00070011: 'N',
0x00070012: 'O',
0x00070013: 'P',
0x00070014: 'Q',
0x00070015: 'R',
0x00070016: 'S',
0x00070017: 'T',
0x00070018: 'U',
0x00070019: 'V',
0x0007001a: 'W',
0x0007001b: 'X',
0x0007001c: 'Y',
0x0007001d: 'Z',
0x0007001e: '1',
0x0007001f: '2',
0x00070020: '3',
0x00070021: '4',
0x00070022: '5',
0x00070023: '6',
0x00070024: '7',
0x00070025: '8',
0x00070026: '9',
0x00070027: '10',
0x00070028: 'ENTER',
0x00070029: 'ESCAPE',
0x0007002a: 'BACKSPACE',
0x0007002b: 'TAB',
0x0007002c: 'SPACEBAR',
0x0007002d: 'MINUS',
0x0007002e: 'EQUALS',
0x0007002f: 'LEFTBRACKET',
0x00070030: 'RIGHTBRACKET',
0x00070031: 'BACKSLASH',
0x00070033: 'SEMICOLON',
0x00070034: 'QUOTE',
0x00070036: 'COMMA',
0x00070037: 'PERIOD',
0x00070038: 'SLASH',
0x00070039: 'CAPSLOCK',
0x00070047: 'SCROLLLOCK',
0x00070048: 'PAUSE',
0x00070049: 'INSERT',
0x0007004a: 'HOME',
0x0007004b: 'PAGEUP',
0x0007004c: 'DELETE',
0x0007004d: 'END',
0x0007004e: 'PAGEDOWN',
0x00070053: 'NUMLOCK',
0x00070054: 'DIVIDE',
0x00070055: 'MULTIPLY',
0x00070056: 'SUBTRACT',
0x00070057: 'ADD',
0x00070058: 'ENTER',
0x00070059: '1',
0x0007005a: '2',
0x0007005b: '3',
0x0007005c: '4',
0x0007005d: '5',
0x0007005e: '6',
0x0007005f: '7',
0x00070060: '8',
0x00070061: '9',
0x00070062: '0',
0x00070063: 'DECIMAL',
0x00070064: 'BACKSLASH'
};
String? get unrealEngineName => _keyboardKeyNames[this.usbHidUsage];
bool get isUnrealEngineKey => _keyboardKeyNames[this.usbHidUsage] != null;
String? get unrealEnginePrettyName => _keyboardKeyPrettyNames[this.usbHidUsage];
}

23
gui/lib/src/util/log.dart Normal file
View File

@@ -0,0 +1,23 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
final File _loggingFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1);
File _createLoggingFile() {
final file = File("${logsDirectory.path}\\launcher.log");
file.parent.createSync(recursive: true);
if(file.existsSync()) {
file.deleteSync();
}
file.createSync();
return file;
}
void log(String message) async {
await _semaphore.acquire();
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
_semaphore.release();
}

View File

@@ -6,22 +6,26 @@ import 'package:reboot_common/common.dart';
const Duration _timeout = Duration(seconds: 2);
Future<bool> _pingGameServer(String hostname, int port) async {
var socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
var dataToSend = utf8.encode(DateTime.now().toIso8601String());
socket.send(dataToSend, InternetAddress(hostname), port);
await for (var event in socket) {
switch(event) {
case RawSocketEvent.read:
return true;
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
case RawSocketEvent.write:
break;
RawDatagramSocket? socket;
try {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
final dataToSend = utf8.encode(DateTime.now().toIso8601String());
socket.send(dataToSend, InternetAddress(hostname), port);
await for (var event in socket) {
switch(event) {
case RawSocketEvent.read:
case RawSocketEvent.write:
return true;
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
}
}
}
return false;
return false;
}finally {
socket?.close();
}
}
Future<bool> get _timeoutFuture => Future.delayed(_timeout).then((value) => false);

View File

@@ -2,18 +2,399 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart';
import 'dart:collection';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
bool get isWin11 {
var result = _winBuildRegex.firstMatch(Platform.operatingSystemVersion)?.group(1);
if(result == null){
return false;
int? get windowsBuild {
final result = _winBuildRegex.firstMatch(Platform.operatingSystemVersion)?.group(1);
if (result == null) {
return null;
}
var intBuild = int.tryParse(result);
return int.tryParse(result);
}
bool get isWin11 {
final intBuild = windowsBuild;
return intBuild != null && intBuild > 22000;
}
bool get isDarkMode
=> SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
bool get isDarkMode =>
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
class _ServiceProvider10 extends IUnknown {
static const String _CLSID = "{C2F03A33-21F5-47FA-B4BB-156362A2F239}";
static const String _IID = "{6D5140C1-7436-11CE-8034-00AA006009FA}";
_ServiceProvider10._internal(Pointer<COMObject> ptr) : super(ptr);
factory _ServiceProvider10.createInstance() =>
_ServiceProvider10._internal(COMObject.createFromID(_CLSID, _IID));
Pointer<COMObject> queryService(String classId, String instanceId) {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<GUID>, Pointer<GUID>,
Pointer<COMObject>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<GUID>, Pointer<GUID>,
Pointer<COMObject>)>()(ptr.ref.lpVtbl,
GUIDFromString(classId), GUIDFromString(instanceId), result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result;
}
}
class IVirtualDesktop extends IUnknown {
static const String _CLSID = "{3F07F4BE-B107-441A-AF0F-39D82529072C}";
IVirtualDesktop._internal(super.ptr);
String getName() {
final result = calloc<HSTRING>();
final code = (ptr.ref.vtable + 5)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return convertFromHString(result.value);
}
}
class IApplicationView extends IUnknown {
// static const String _CLSID = "{372E1D3B-38D3-42E4-A15B-8AB2B178F513}";
IApplicationView._internal(super.ptr);
}
class _IObjectArray extends IUnknown {
_IObjectArray(super.ptr);
int getCount() {
final result = calloc<Int32>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result.value;
}
Pointer<COMObject> getAt(int index, String guid) {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 4)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Int32 index, Pointer<GUID>,
Pointer<COMObject>)>>>()
.value
.asFunction<
int Function(
Pointer, int index, Pointer<GUID>, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, index, GUIDFromString(guid), result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result;
}
}
typedef _IObjectMapper<T> = T Function(Pointer<COMObject>);
class _IObjectArrayList<T> extends ListBase<T> {
final _IObjectArray _array;
final String _guid;
final _IObjectMapper<T> _mapper;
_IObjectArrayList(
{required _IObjectArray array,
required String guid,
required _IObjectMapper<T> mapper})
: _array = array,
_guid = guid,
_mapper = mapper;
@override
int get length => _array.getCount();
@override
set length(int newLength) {
throw UnsupportedError("Immutable list");
}
@override
T operator [](int index) => _mapper(_array.getAt(index, _guid));
@override
void operator []=(int index, T value) {
throw UnsupportedError("Immutable list");
}
}
class _IVirtualDesktopManagerInternal extends IUnknown {
static const String _CLSID = "{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}";
static const String _IID_WIN10 = "{F31574D6-B682-4CDC-BD56-1827860ABEC6}";
static const String _IID_WIN_21H2 = "{B2F925B9-5A0F-4D2E-9F4D-2B1507593C10}";
static const String _IID_WIN_23H2 = "{A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E}";
static const String _IID_WIN_23H2_3085 = "{53F5CA0B-158F-4124-900C-057158060B27}";
_IVirtualDesktopManagerInternal._internal(super.ptr);
int getDesktopsCount() {
final result = calloc<Int32>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result.value;
}
List<IVirtualDesktop> getDesktops() {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 7)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
final array = _IObjectArray(result);
return _IObjectArrayList(
array: array,
guid: IVirtualDesktop._CLSID,
mapper: (comObject) => IVirtualDesktop._internal(comObject));
}
void moveWindowToDesktop(IApplicationView view, IVirtualDesktop desktop) {
final code = (ptr.ref.vtable + 4)
.cast<
Pointer<
NativeFunction<
Int32 Function(Pointer, COMObject, COMObject)>>>()
.value
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
ptr.ref.lpVtbl, view.ptr.ref, desktop.ptr.ref);
if (code != 0) {
throw WindowsException(code);
}
}
IVirtualDesktop createDesktop() {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 10)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return IVirtualDesktop._internal(result);
}
void removeDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback) {
final code = (ptr.ref.vtable + 12)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, COMObject, COMObject)>>>()
.value
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, fallback.ptr.ref);
if (code != 0) {
throw WindowsException(code);
}
}
void setDesktopName(IVirtualDesktop desktop, String newName) {
final code =
(ptr.ref.vtable + 15)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, COMObject, Int8)>>>()
.value
.asFunction<int Function(Pointer, COMObject, int)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
if (code != 0) {
throw WindowsException(code);
}
}
}
class _IApplicationViewCollection extends IUnknown {
static const String _CLSID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
static const String _IID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
_IApplicationViewCollection._internal(super.ptr);
IApplicationView getViewForHWnd(int HWnd) {
final result = calloc<COMObject>();
final code =
(ptr.ref.vtable + 6)
.cast<
Pointer<
NativeFunction<
HRESULT Function(
Pointer, IntPtr, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, int, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, HWnd, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return IApplicationView._internal(result);
}
}
final class _Process extends Struct {
@Uint32()
external int pid;
@Uint32()
external int HWnd;
static int _filter(int HWnd, int lParam) {
final structure = Pointer.fromAddress(lParam).cast<_Process>();
final pidPointer = calloc<Uint32>();
GetWindowThreadProcessId(HWnd, pidPointer);
final pid = pidPointer.value;
free(pidPointer);
if (pid != structure.ref.pid) {
return TRUE;
}
structure.ref.HWnd = HWnd;
return FALSE;
}
static int getHWndFromPid(int pid) {
final result = calloc<_Process>();
result.ref.pid = pid;
EnumWindows(
Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
final HWnd = result.ref.HWnd;
calloc.free(result);
return HWnd;
}
}
class VirtualDesktopManager {
static VirtualDesktopManager? _instance;
final _IVirtualDesktopManagerInternal windowManager;
final _IApplicationViewCollection applicationViewCollection;
VirtualDesktopManager._internal(this.windowManager, this.applicationViewCollection);
factory VirtualDesktopManager.getInstance() {
if (_instance != null) {
return _instance!;
}
final hr = CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) {
throw WindowsException(hr);
}
final shell = _ServiceProvider10.createInstance();
final windowManager = _createWindowManager(shell);
final applicationViewCollection = _IApplicationViewCollection._internal(
shell.queryService(_IApplicationViewCollection._CLSID,
_IApplicationViewCollection._IID));
return _instance =
VirtualDesktopManager._internal(windowManager, applicationViewCollection);
}
static _IVirtualDesktopManagerInternal _createWindowManager(_ServiceProvider10 shell) {
final build = windowsBuild;
if(build == null || build < 19044) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN10));
}else if(build >= 19044 && build < 22631) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_21H2));
}else if(build >= 22631 && build < 22631) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_23H2));
}else {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_23H2_3085));
}
}
int getDesktopsCount() => windowManager.getDesktopsCount();
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
void moveWindowToDesktop(int pid, IVirtualDesktop desktop) {
final HWnd = _Process.getHWndFromPid(pid);
final window = applicationViewCollection.getViewForHWnd(HWnd);
windowManager.moveWindowToDesktop(window, desktop);
}
IVirtualDesktop createDesktop() => windowManager.createDesktop();
void removeDesktop(IVirtualDesktop desktop, [IVirtualDesktop? fallback]) {
fallback ??= getDesktops().first;
return windowManager.removeDesktop(desktop, fallback);
}
void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName);
}

View File

@@ -1,12 +0,0 @@
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
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"));