mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
Refactored GUI
This commit is contained in:
@@ -1,18 +1,21 @@
|
|||||||
export 'package:reboot_common/src/constant/backend.dart';
|
export 'package:reboot_common/src/backend/auth_backend_constants.dart';
|
||||||
export 'package:reboot_common/src/constant/game.dart';
|
export 'package:reboot_common/src/backend/auth_backend_helper.dart';
|
||||||
export 'package:reboot_common/src/constant/supabase.dart';
|
export 'package:reboot_common/src/backend/auth_backend_result.dart';
|
||||||
export 'package:reboot_common/src/model/fortnite_build.dart';
|
export 'package:reboot_common/src/backend/auth_backend_type.dart';
|
||||||
export 'package:reboot_common/src/model/fortnite_version.dart';
|
|
||||||
export 'package:reboot_common/src/model/game_instance.dart';
|
export 'package:reboot_common/src/browser/server_browser_client.dart';
|
||||||
export 'package:reboot_common/src/model/server_result.dart';
|
export 'package:reboot_common/src/browser/server_browser_entry.dart';
|
||||||
export 'package:reboot_common/src/model/server_type.dart';
|
export 'package:reboot_common/src/browser/server_browser_event.dart';
|
||||||
export 'package:reboot_common/src/model/update_status.dart';
|
export 'package:reboot_common/src/browser/server_browser_state.dart';
|
||||||
export 'package:reboot_common/src/model/update_timer.dart';
|
|
||||||
export 'package:reboot_common/src/model/fortnite_server.dart';
|
export 'package:reboot_common/src/game/game_build.dart';
|
||||||
export 'package:reboot_common/src/model/dll.dart';
|
export 'package:reboot_common/src/game/game_constants.dart';
|
||||||
export 'package:reboot_common/src/util/backend.dart';
|
export 'package:reboot_common/src/game/game_dll.dart';
|
||||||
export 'package:reboot_common/src/util/downloader.dart';
|
export 'package:reboot_common/src/game/game_downloader.dart';
|
||||||
export 'package:reboot_common/src/util/os.dart';
|
export 'package:reboot_common/src/game/game_instance.dart';
|
||||||
export 'package:reboot_common/src/util/log.dart';
|
export 'package:reboot_common/src/game/game_metadata.dart';
|
||||||
export 'package:reboot_common/src/util/game.dart';
|
export 'package:reboot_common/src/game/game_version.dart';
|
||||||
|
|
||||||
export 'package:reboot_common/src/util/extensions.dart';
|
export 'package:reboot_common/src/util/extensions.dart';
|
||||||
|
export 'package:reboot_common/src/util/logger.dart';
|
||||||
|
export 'package:reboot_common/src/util/os.dart';
|
||||||
|
|||||||
@@ -15,93 +15,95 @@ final Semaphore _semaphore = Semaphore();
|
|||||||
String? _lastIp;
|
String? _lastIp;
|
||||||
String? _lastPort;
|
String? _lastPort;
|
||||||
|
|
||||||
Stream<ServerResult> startBackend({
|
typedef BackendErrorHandler = void Function(String);
|
||||||
required ServerType type,
|
|
||||||
|
Stream<AuthBackendResult> startAuthBackend({
|
||||||
|
required AuthBackendType type,
|
||||||
required String host,
|
required String host,
|
||||||
required String port,
|
required String port,
|
||||||
required bool detached,
|
required bool detached,
|
||||||
required void Function(String) onError
|
required BackendErrorHandler? onError
|
||||||
}) async* {
|
}) async* {
|
||||||
Process? process;
|
Process? process;
|
||||||
HttpServer? server;
|
HttpServer? server;
|
||||||
try {
|
try {
|
||||||
host = host.trim();
|
host = host.trim();
|
||||||
port = port.trim();
|
port = port.trim();
|
||||||
if(type != ServerType.local || port != kDefaultBackendPort.toString()) {
|
if(type != AuthBackendType.local || port != kDefaultBackendPort.toString()) {
|
||||||
yield ServerResult(ServerResultType.starting);
|
yield AuthBackendResult(AuthBackendResultType.starting);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.isEmpty) {
|
if (host.isEmpty) {
|
||||||
yield ServerResult(ServerResultType.startMissingHostError);
|
yield AuthBackendResult(AuthBackendResultType.startMissingHostError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port.isEmpty) {
|
if (port.isEmpty) {
|
||||||
yield ServerResult(ServerResultType.startMissingPortError);
|
yield AuthBackendResult(AuthBackendResultType.startMissingPortError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final portNumber = int.tryParse(port);
|
final portNumber = int.tryParse(port);
|
||||||
if (portNumber == null) {
|
if (portNumber == null) {
|
||||||
yield ServerResult(ServerResultType.startIllegalPortError);
|
yield AuthBackendResult(AuthBackendResultType.startIllegalPortError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type != ServerType.local || port != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
if ((type != AuthBackendType.local || port != kDefaultBackendPort.toString()) && !(await isAuthBackendPortFree())) {
|
||||||
yield ServerResult(ServerResultType.startFreeingPort);
|
yield AuthBackendResult(AuthBackendResultType.startFreeingPort);
|
||||||
final result = await freeBackendPort();
|
final result = await freeAuthBackendPort();
|
||||||
if(!result) {
|
if(!result) {
|
||||||
yield ServerResult(ServerResultType.startFreePortError);
|
yield AuthBackendResult(AuthBackendResultType.startFreePortError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.startFreePortSuccess);
|
yield AuthBackendResult(AuthBackendResultType.startFreePortSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(type){
|
switch(type){
|
||||||
case ServerType.embedded:
|
case AuthBackendType.embedded:
|
||||||
process = await startEmbeddedBackend(detached, onError: onError);
|
process = await _startEmbedded(detached, onError: onError);
|
||||||
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(process: process));
|
yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(process: process));
|
||||||
break;
|
break;
|
||||||
case ServerType.remote:
|
case AuthBackendType.remote:
|
||||||
yield ServerResult(ServerResultType.startPingingRemote);
|
yield AuthBackendResult(AuthBackendResultType.startPingingRemote);
|
||||||
final uriResult = await pingBackend(host, portNumber);
|
final uriResult = await _ping(host, portNumber);
|
||||||
if(uriResult == null) {
|
if(uriResult == null) {
|
||||||
yield ServerResult(ServerResultType.startPingError);
|
yield AuthBackendResult(AuthBackendResultType.startPingError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
server = await startRemoteBackendProxy(uriResult);
|
server = await _startRemote(uriResult);
|
||||||
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
|
yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(server: server));
|
||||||
break;
|
break;
|
||||||
case ServerType.local:
|
case AuthBackendType.local:
|
||||||
if(portNumber != kDefaultBackendPort) {
|
if(portNumber != kDefaultBackendPort) {
|
||||||
yield ServerResult(ServerResultType.startPingingLocal);
|
yield AuthBackendResult(AuthBackendResultType.startPingingLocal);
|
||||||
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
|
final uriResult = await _ping(kDefaultBackendHost, portNumber);
|
||||||
if(uriResult == null) {
|
if(uriResult == null) {
|
||||||
yield ServerResult(ServerResultType.startPingError);
|
yield AuthBackendResult(AuthBackendResultType.startPingError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
server = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$port"));
|
server = await _startRemote(Uri.parse("http://$kDefaultBackendHost:$port"));
|
||||||
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
|
yield AuthBackendResult(AuthBackendResultType.startedImplementation, implementation: AuthBackendImplementation(server: server));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.startPingingLocal);
|
yield AuthBackendResult(AuthBackendResultType.startPingingLocal);
|
||||||
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
|
final uriResult = await _ping(kDefaultBackendHost, kDefaultBackendPort);
|
||||||
if(uriResult == null) {
|
if(uriResult == null) {
|
||||||
yield ServerResult(ServerResultType.startPingError);
|
yield AuthBackendResult(AuthBackendResultType.startPingError);
|
||||||
process?.kill(ProcessSignal.sigterm);
|
process?.kill(ProcessSignal.sigterm);
|
||||||
server?.close(force: true);
|
server?.close(force: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.startSuccess);
|
yield AuthBackendResult(AuthBackendResultType.startSuccess);
|
||||||
}catch(error, stackTrace) {
|
}catch(error, stackTrace) {
|
||||||
yield ServerResult(
|
yield AuthBackendResult(
|
||||||
ServerResultType.startError,
|
AuthBackendResultType.startError,
|
||||||
error: error,
|
error: error,
|
||||||
stackTrace: stackTrace
|
stackTrace: stackTrace
|
||||||
);
|
);
|
||||||
@@ -110,34 +112,7 @@ Stream<ServerResult> startBackend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<ServerResult> stopBackend({required ServerType type, required ServerImplementation? implementation}) async* {
|
Future<Process> _startEmbedded(bool detached, {BackendErrorHandler? onError}) async {
|
||||||
yield ServerResult(ServerResultType.stopping);
|
|
||||||
try{
|
|
||||||
switch(type){
|
|
||||||
case ServerType.embedded:
|
|
||||||
final process = implementation?.process;
|
|
||||||
if(process != null) {
|
|
||||||
Process.killPid(process.pid, ProcessSignal.sigterm);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
await implementation?.server?.close(force: true);
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
await implementation?.server?.close(force: true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
yield ServerResult(ServerResultType.stopSuccess);
|
|
||||||
}catch(error, stackTrace){
|
|
||||||
yield ServerResult(
|
|
||||||
ServerResultType.stopError,
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
|
|
||||||
final process = await startProcess(
|
final process = await startProcess(
|
||||||
executable: backendStartExecutable,
|
executable: backendStartExecutable,
|
||||||
window: detached,
|
window: detached,
|
||||||
@@ -164,22 +139,38 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
|
|||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
|
Future<HttpServer> _startRemote(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
|
||||||
|
|
||||||
Future<bool> isBackendPortFree() async => await pingBackend(kDefaultBackendHost, kDefaultBackendPort) == null;
|
Stream<AuthBackendResult> stopAuthBackend({required AuthBackendType type, required AuthBackendImplementation? implementation}) async* {
|
||||||
|
yield AuthBackendResult(AuthBackendResultType.stopping);
|
||||||
Future<bool> freeBackendPort() async {
|
try{
|
||||||
await killProcessByPort(kDefaultBackendPort);
|
switch(type){
|
||||||
await killProcessByPort(kDefaultXmppPort);
|
case AuthBackendType.embedded:
|
||||||
final standardResult = await isBackendPortFree();
|
final process = implementation?.process;
|
||||||
if(standardResult) {
|
if(process != null) {
|
||||||
return true;
|
Process.killPid(process.pid, ProcessSignal.sigterm);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AuthBackendType.remote:
|
||||||
|
await implementation?.server?.close(force: true);
|
||||||
|
break;
|
||||||
|
case AuthBackendType.local:
|
||||||
|
await implementation?.server?.close(force: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yield AuthBackendResult(AuthBackendResultType.stopSuccess);
|
||||||
|
}catch(error, stackTrace){
|
||||||
|
yield AuthBackendResult(
|
||||||
|
AuthBackendResultType.stopError,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
Future<bool> isAuthBackendPortFree() async => await _ping(kDefaultBackendHost, kDefaultBackendPort) == null;
|
||||||
|
|
||||||
|
Future<Uri?> _ping(String host, int port, [bool https=false]) async {
|
||||||
final hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
final hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||||
final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||||
try{
|
try{
|
||||||
@@ -198,7 +189,7 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
|||||||
return uri;
|
return uri;
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
log("[BACKEND] Cannot ping backend: $error");
|
log("[BACKEND] Cannot ping backend: $error");
|
||||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
return https || declaredScheme != null || isLocalHost(host) ? null : await _ping(host, port, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +227,18 @@ Stream<String?> watchMatchmakingIp() async* {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> writeMatchmakingIp(String text) async {
|
Future<bool> freeAuthBackendPort() async {
|
||||||
|
await killProcessByPort(kDefaultBackendPort);
|
||||||
|
await killProcessByPort(kDefaultXmppPort);
|
||||||
|
final standardResult = await isAuthBackendPortFree();
|
||||||
|
if(standardResult) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeAuthBackendMatchmakingIp(String text) async {
|
||||||
final exists = await matchmakerConfigFile.exists();
|
final exists = await matchmakerConfigFile.exists();
|
||||||
if(!exists) {
|
if(!exists) {
|
||||||
return;
|
return;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
class ServerResult {
|
class AuthBackendResult {
|
||||||
final ServerResultType type;
|
final AuthBackendResultType type;
|
||||||
final ServerImplementation? implementation;
|
final AuthBackendImplementation? implementation;
|
||||||
final Object? error;
|
final Object? error;
|
||||||
final StackTrace? stackTrace;
|
final StackTrace? stackTrace;
|
||||||
|
|
||||||
ServerResult(this.type, {this.implementation, this.error, this.stackTrace});
|
AuthBackendResult(this.type, {this.implementation, this.error, this.stackTrace});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -14,14 +14,14 @@ class ServerResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerImplementation {
|
class AuthBackendImplementation {
|
||||||
final Process? process;
|
final Process? process;
|
||||||
final HttpServer? server;
|
final HttpServer? server;
|
||||||
|
|
||||||
ServerImplementation({this.process, this.server});
|
AuthBackendImplementation({this.process, this.server});
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ServerResultType {
|
enum AuthBackendResultType {
|
||||||
starting,
|
starting,
|
||||||
startMissingHostError,
|
startMissingHostError,
|
||||||
startMissingPortError,
|
startMissingPortError,
|
||||||
@@ -39,9 +39,9 @@ enum ServerResultType {
|
|||||||
stopSuccess,
|
stopSuccess,
|
||||||
stopError;
|
stopError;
|
||||||
|
|
||||||
bool get isStart => name.contains("start");
|
bool get isStart => name.startsWith("start");
|
||||||
|
|
||||||
bool get isError => name.contains("Error");
|
bool get isError => name.endsWith("Error");
|
||||||
|
|
||||||
bool get isSuccess => this == ServerResultType.startSuccess || this == ServerResultType.stopSuccess;
|
bool get isSuccess => this == AuthBackendResultType.startSuccess || this == AuthBackendResultType.stopSuccess;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
enum ServerType {
|
enum AuthBackendType {
|
||||||
embedded,
|
embedded,
|
||||||
remote,
|
remote,
|
||||||
local
|
local
|
||||||
209
common/lib/src/browser/server_browser_client.dart
Normal file
209
common/lib/src/browser/server_browser_client.dart
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
|
class ServerBrowserClient {
|
||||||
|
static const String _pingEvent = 'ping';
|
||||||
|
static const String _addEvent = 'add';
|
||||||
|
static const String _removeEvent = 'remove';
|
||||||
|
static const Duration _pingInterval = const Duration(seconds: 30);
|
||||||
|
static const Duration _reconnectDelay = const Duration(seconds: 10);
|
||||||
|
static const Duration _timeout = const Duration(seconds: 10);
|
||||||
|
|
||||||
|
final String _serverUrl;
|
||||||
|
final StreamController<ServerBrowserEvent> _eventsController = StreamController.broadcast();
|
||||||
|
|
||||||
|
WebSocket? _socket;
|
||||||
|
Timer? _reconnectTimer;
|
||||||
|
Timer? _pingTimer;
|
||||||
|
Completer _pingCompleter = Completer();
|
||||||
|
ServerBrowserState _state = ServerBrowserState.disconnected;
|
||||||
|
|
||||||
|
ServerBrowserClient({required String serverUrl})
|
||||||
|
: _serverUrl = serverUrl;
|
||||||
|
|
||||||
|
Future<void> connect() async {
|
||||||
|
if (_state != ServerBrowserState.disconnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setState(ServerBrowserState.connecting);
|
||||||
|
_reconnectTimer?.cancel();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final socket = await WebSocket.connect(_serverUrl)
|
||||||
|
.timeout(_timeout);
|
||||||
|
_socket = socket;
|
||||||
|
_setState(ServerBrowserState.connected);
|
||||||
|
socket.listen(
|
||||||
|
_handleMessage,
|
||||||
|
onDone: () => _handleDisconnection(_state == ServerBrowserState.connected),
|
||||||
|
onError: (error) {
|
||||||
|
_eventsController.add(new ServerBrowserErrorEvent(
|
||||||
|
error: 'An unhandled error was thrown: $error'
|
||||||
|
));
|
||||||
|
_handleDisconnection(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
_startHeartbeat();
|
||||||
|
} catch (e) {
|
||||||
|
_eventsController.add(new ServerBrowserErrorEvent(
|
||||||
|
error: 'Cannot connect: $e'
|
||||||
|
));
|
||||||
|
_handleDisconnection(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleMessage(dynamic data) {
|
||||||
|
try {
|
||||||
|
final message = jsonDecode(data);
|
||||||
|
final type = message['type'];
|
||||||
|
final payload = message['data'];
|
||||||
|
switch (type) {
|
||||||
|
case _pingEvent:
|
||||||
|
if(!_pingCompleter.isCompleted) {
|
||||||
|
_pingCompleter.complete(null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _addEvent:
|
||||||
|
if (payload is List) {
|
||||||
|
final entries = payload
|
||||||
|
.map((entry) => ServerBrowserEntry.fromJson(entry))
|
||||||
|
.toList(growable: false);
|
||||||
|
_eventsController.add(new ServerBrowserAddEvent(
|
||||||
|
entries: entries,
|
||||||
|
));
|
||||||
|
}else {
|
||||||
|
_eventsController.add(new ServerBrowserErrorEvent(
|
||||||
|
error: 'Invalid add event payload: ${payload?.runtimeType}'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _removeEvent:
|
||||||
|
if (payload is List) {
|
||||||
|
final entries = payload
|
||||||
|
.map((entry) => entry['id'] as String?)
|
||||||
|
.whereType<String>()
|
||||||
|
.toList(growable: false);
|
||||||
|
_eventsController.add(new ServerBrowserRemoveEvent(
|
||||||
|
entries: entries
|
||||||
|
));
|
||||||
|
}else {
|
||||||
|
_eventsController.add(new ServerBrowserErrorEvent(
|
||||||
|
error: 'Invalid remove event payload: ${payload?.runtimeType}'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
_eventsController.add(new ServerBrowserErrorEvent(
|
||||||
|
error: 'Invalid event type: $type'
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
_eventsController.add(new ServerBrowserErrorEvent(
|
||||||
|
error: 'An error occurred while processing an event: $error'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDisconnection(bool reconnect) {
|
||||||
|
_setState(ServerBrowserState.disconnected);
|
||||||
|
_cleanup();
|
||||||
|
if (reconnect) {
|
||||||
|
_reconnectTimer = Timer(_reconnectDelay, () => connect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startHeartbeat() {
|
||||||
|
_pingTimer?.cancel();
|
||||||
|
_pingTimer = Timer.periodic(_pingInterval, (timer) async {
|
||||||
|
final socket = _socket;
|
||||||
|
if(socket == null || _state != ServerBrowserState.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.add(jsonEncode({'type': _pingEvent}));
|
||||||
|
await _pingCompleter.future
|
||||||
|
.timeout(_timeout);
|
||||||
|
_pingCompleter = Completer();
|
||||||
|
} catch (error) {
|
||||||
|
_pingCompleter = Completer();
|
||||||
|
_handleDisconnection(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cleanup() {
|
||||||
|
_socket?.close();
|
||||||
|
_socket = null;
|
||||||
|
_pingTimer?.cancel();
|
||||||
|
_pingTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setState(ServerBrowserState newState) {
|
||||||
|
if (_state != newState) {
|
||||||
|
_state = newState;
|
||||||
|
_eventsController.add(ServerBrowserStateEvent(
|
||||||
|
state: newState
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addEntry(ServerBrowserEntry entry) async {
|
||||||
|
if (_state != ServerBrowserState.connected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final socket = _socket;
|
||||||
|
if(socket == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = {
|
||||||
|
'type': _addEvent,
|
||||||
|
'data': entry.toJson()
|
||||||
|
};
|
||||||
|
socket.add(jsonEncode(message));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeEntry(String id) async {
|
||||||
|
if (_state != ServerBrowserState.connected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final socket = _socket;
|
||||||
|
if(socket == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = {
|
||||||
|
'type': _removeEvent,
|
||||||
|
'data': id
|
||||||
|
};
|
||||||
|
socket.add(jsonEncode(message));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<ServerBrowserEvent> addListener(void Function(ServerBrowserEvent) onData) {
|
||||||
|
return _eventsController.stream.listen(onData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
_reconnectTimer?.cancel();
|
||||||
|
_cleanup();
|
||||||
|
_setState(ServerBrowserState.disconnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
disconnect();
|
||||||
|
_eventsController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
class FortniteServer {
|
class ServerBrowserEntry {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
final String author;
|
final String author;
|
||||||
final String ip;
|
final String ip;
|
||||||
final String version;
|
final String version;
|
||||||
final String? password;
|
final String password;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
final bool discoverable;
|
|
||||||
|
|
||||||
FortniteServer({
|
ServerBrowserEntry({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
@@ -18,10 +17,9 @@ class FortniteServer {
|
|||||||
required this.version,
|
required this.version,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
required this.discoverable
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory FortniteServer.fromJson(json) => FortniteServer(
|
factory ServerBrowserEntry.fromJson(json) => ServerBrowserEntry(
|
||||||
id: json["id"],
|
id: json["id"],
|
||||||
name: json["name"],
|
name: json["name"],
|
||||||
description: json["description"],
|
description: json["description"],
|
||||||
@@ -29,8 +27,7 @@ class FortniteServer {
|
|||||||
ip: json["ip"],
|
ip: json["ip"],
|
||||||
version: json["version"],
|
version: json["version"],
|
||||||
password: json["password"],
|
password: json["password"],
|
||||||
timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now(),
|
timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now()
|
||||||
discoverable: json["discoverable"] ?? false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@@ -41,7 +38,6 @@ class FortniteServer {
|
|||||||
"ip": ip,
|
"ip": ip,
|
||||||
"version": version,
|
"version": version,
|
||||||
"password": password,
|
"password": password,
|
||||||
"timestamp": timestamp.toString(),
|
"timestamp": timestamp.toString()
|
||||||
"discoverable": discoverable
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
30
common/lib/src/browser/server_browser_event.dart
Normal file
30
common/lib/src/browser/server_browser_event.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_common/src/browser/server_browser_state.dart';
|
||||||
|
|
||||||
|
sealed class ServerBrowserEvent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ServerBrowserStateEvent extends ServerBrowserEvent {
|
||||||
|
final ServerBrowserState state;
|
||||||
|
|
||||||
|
ServerBrowserStateEvent({required this.state});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ServerBrowserAddEvent extends ServerBrowserEvent {
|
||||||
|
final List<ServerBrowserEntry> entries;
|
||||||
|
|
||||||
|
ServerBrowserAddEvent({required this.entries});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ServerBrowserRemoveEvent extends ServerBrowserEvent {
|
||||||
|
final List<String> entries;
|
||||||
|
|
||||||
|
ServerBrowserRemoveEvent({required this.entries});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ServerBrowserErrorEvent extends ServerBrowserEvent {
|
||||||
|
final String error;
|
||||||
|
|
||||||
|
ServerBrowserErrorEvent({required this.error});
|
||||||
|
}
|
||||||
5
common/lib/src/browser/server_browser_state.dart
Normal file
5
common/lib/src/browser/server_browser_state.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
enum ServerBrowserState {
|
||||||
|
disconnected,
|
||||||
|
connecting,
|
||||||
|
connected
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co';
|
|
||||||
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M';
|
|
||||||
64
common/lib/src/game/game_build.dart
Normal file
64
common/lib/src/game/game_build.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
class GameBuild {
|
||||||
|
final String gameVersion;
|
||||||
|
final String link;
|
||||||
|
final bool available;
|
||||||
|
|
||||||
|
GameBuild({
|
||||||
|
required this.gameVersion,
|
||||||
|
required this.link,
|
||||||
|
required this.available
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameBuildDownloadProgress {
|
||||||
|
final double progress;
|
||||||
|
final int? timeLeft;
|
||||||
|
final bool extracting;
|
||||||
|
final int speed;
|
||||||
|
|
||||||
|
GameBuildDownloadProgress({
|
||||||
|
required this.progress,
|
||||||
|
required this.extracting,
|
||||||
|
required this.timeLeft,
|
||||||
|
required this.speed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameBuildDownloadOptions {
|
||||||
|
GameBuild build;
|
||||||
|
Directory destination;
|
||||||
|
SendPort port;
|
||||||
|
|
||||||
|
GameBuildDownloadOptions(this.build, this.destination, this.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameBuildManifestChunk {
|
||||||
|
List<int> chunksIds;
|
||||||
|
String file;
|
||||||
|
int fileSize;
|
||||||
|
|
||||||
|
GameBuildManifestChunk._internal(this.chunksIds, this.file, this.fileSize);
|
||||||
|
|
||||||
|
factory GameBuildManifestChunk.fromJson(json) => GameBuildManifestChunk._internal(
|
||||||
|
List<int>.from(json["ChunksIds"] as List),
|
||||||
|
json["File"],
|
||||||
|
json["FileSize"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameBuildManifestFile {
|
||||||
|
String name;
|
||||||
|
List<GameBuildManifestChunk> chunks;
|
||||||
|
int size;
|
||||||
|
|
||||||
|
GameBuildManifestFile._internal(this.name, this.chunks, this.size);
|
||||||
|
|
||||||
|
factory GameBuildManifestFile.fromJson(json) => GameBuildManifestFile._internal(
|
||||||
|
json["Name"],
|
||||||
|
List<GameBuildManifestChunk>.from(json["Chunks"].map((chunk) => GameBuildManifestChunk.fromJson(chunk))),
|
||||||
|
json["Size"]
|
||||||
|
);
|
||||||
|
}
|
||||||
10
common/lib/src/game/game_dll.dart
Normal file
10
common/lib/src/game/game_dll.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
enum GameDll {
|
||||||
|
console,
|
||||||
|
auth,
|
||||||
|
gameServer,
|
||||||
|
memoryLeak
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InjectableDllVersionAware on GameDll {
|
||||||
|
bool get isVersionDependent => this == GameDll.gameServer;
|
||||||
|
}
|
||||||
@@ -30,96 +30,97 @@ final int _ariaPort = 6800;
|
|||||||
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
|
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
|
||||||
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
|
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
|
||||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||||
final List<FortniteBuild> downloadableBuilds = [
|
final List<GameBuild> downloadableBuilds = [
|
||||||
FortniteBuild(gameVersion: "1.7.2", link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
|
GameBuild(gameVersion: "1.7.2", link: "https://builds.rebootfn.org/1.7.2.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "1.8", link: "https://public.simplyblk.xyz/1.8.rar", available: true),
|
GameBuild(gameVersion: "1.8", link: "https://builds.rebootfn.org/1.8.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "1.8.1", link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
|
GameBuild(gameVersion: "1.8.1", link: "https://builds.rebootfn.org/1.8.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "1.8.2", link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
|
GameBuild(gameVersion: "1.8.2", link: "https://builds.rebootfn.org/1.8.2.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "1.9", link: "https://public.simplyblk.xyz/1.9.rar", available: true),
|
GameBuild(gameVersion: "1.9", link: "https://builds.rebootfn.org/1.9.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "1.9.1", link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
|
GameBuild(gameVersion: "1.9.1", link: "https://builds.rebootfn.org/1.9.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "1.10", link: "https://public.simplyblk.xyz/1.10.rar", available: true),
|
GameBuild(gameVersion: "1.10", link: "https://builds.rebootfn.org/1.10.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "1.11", link: "https://public.simplyblk.xyz/1.11.zip", available: true),
|
GameBuild(gameVersion: "1.11", link: "https://builds.rebootfn.org/1.11.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "2.1.0", link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
|
GameBuild(gameVersion: "2.1.0", link: "https://builds.rebootfn.org/2.1.0.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "2.2.0", link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
|
GameBuild(gameVersion: "2.2.0", link: "https://builds.rebootfn.org/2.2.0.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "2.3", link: "https://public.simplyblk.xyz/2.3.rar", available: true),
|
GameBuild(gameVersion: "2.3", link: "https://builds.rebootfn.org/2.3.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "2.4.0", link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
|
GameBuild(gameVersion: "2.4.0", link: "https://builds.rebootfn.org/2.4.0.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "2.4.2", link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
|
GameBuild(gameVersion: "2.4.2", link: "https://builds.rebootfn.org/2.4.2.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "2.5.0", link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
|
GameBuild(gameVersion: "2.5.0", link: "https://builds.rebootfn.org/2.5.0.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "3.0", link: "https://public.simplyblk.xyz/3.0.zip", available: true),
|
GameBuild(gameVersion: "3.0", link: "https://builds.rebootfn.org/3.0.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "3.1", link: "https://public.simplyblk.xyz/3.1.rar", available: true),
|
GameBuild(gameVersion: "3.1", link: "https://builds.rebootfn.org/3.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "3.1.1", link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
|
GameBuild(gameVersion: "3.1.1", link: "https://builds.rebootfn.org/3.1.1.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "3.2", link: "https://public.simplyblk.xyz/3.2.zip", available: true),
|
GameBuild(gameVersion: "3.2", link: "https://builds.rebootfn.org/3.2.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "3.3", link: "https://public.simplyblk.xyz/3.3.rar", available: true),
|
GameBuild(gameVersion: "3.3", link: "https://builds.rebootfn.org/3.3.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "3.5", link: "https://public.simplyblk.xyz/3.5.rar", available: true),
|
GameBuild(gameVersion: "3.5", link: "https://builds.rebootfn.org/3.5.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "3.6", link: "https://public.simplyblk.xyz/3.6.zip", available: true),
|
GameBuild(gameVersion: "3.6", link: "https://builds.rebootfn.org/3.6.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "4.0", link: "https://public.simplyblk.xyz/4.0.zip", available: true),
|
GameBuild(gameVersion: "4.0", link: "https://builds.rebootfn.org/4.0.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "4.1", link: "https://public.simplyblk.xyz/4.1.zip", available: true),
|
GameBuild(gameVersion: "4.1", link: "https://builds.rebootfn.org/4.1.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "4.2", link: "https://public.simplyblk.xyz/4.2.zip", available: true),
|
GameBuild(gameVersion: "4.2", link: "https://builds.rebootfn.org/4.2.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "4.4", link: "https://public.simplyblk.xyz/4.4.rar", available: true),
|
GameBuild(gameVersion: "4.4", link: "https://builds.rebootfn.org/4.4.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "4.5", link: "https://public.simplyblk.xyz/4.5.rar", available: true),
|
GameBuild(gameVersion: "4.5", link: "https://builds.rebootfn.org/4.5.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "5.00", link: "https://public.simplyblk.xyz/5.00.rar", available: true),
|
GameBuild(gameVersion: "5.00", link: "https://builds.rebootfn.org/5.00.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "5.0.1", link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
|
GameBuild(gameVersion: "5.0.1", link: "https://builds.rebootfn.org/5.0.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "5.10", link: "https://public.simplyblk.xyz/5.10.rar", available: true),
|
GameBuild(gameVersion: "5.10", link: "https://builds.rebootfn.org/5.10.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "5.21", link: "https://public.simplyblk.xyz/5.21.rar", available: true),
|
GameBuild(gameVersion: "5.21", link: "https://builds.rebootfn.org/5.21.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "5.30", link: "https://public.simplyblk.xyz/5.30.rar", available: true),
|
GameBuild(gameVersion: "5.30", link: "https://builds.rebootfn.org/5.30.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "5.40", link: "https://public.simplyblk.xyz/5.40.rar", available: true),
|
GameBuild(gameVersion: "5.40", link: "https://builds.rebootfn.org/5.40.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.00", link: "https://public.simplyblk.xyz/6.00.rar", available: true),
|
GameBuild(gameVersion: "6.00", link: "https://builds.rebootfn.org/6.00.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.01", link: "https://public.simplyblk.xyz/6.01.rar", available: true),
|
GameBuild(gameVersion: "6.01", link: "https://builds.rebootfn.org/6.01.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.1.1", link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
|
GameBuild(gameVersion: "6.1.1", link: "https://builds.rebootfn.org/6.1.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.02", link: "https://public.simplyblk.xyz/6.02.rar", available: true),
|
GameBuild(gameVersion: "6.02", link: "https://builds.rebootfn.org/6.02.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.2.1", link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
|
GameBuild(gameVersion: "6.2.1", link: "https://builds.rebootfn.org/6.2.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.10", link: "https://public.simplyblk.xyz/6.10.rar", available: true),
|
GameBuild(gameVersion: "6.10", link: "https://builds.rebootfn.org/6.10.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.10.1", link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
|
GameBuild(gameVersion: "6.10.1", link: "https://builds.rebootfn.org/6.10.1.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.10.2", link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
|
GameBuild(gameVersion: "6.10.2", link: "https://builds.rebootfn.org/6.10.2.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.21", link: "https://public.simplyblk.xyz/6.21.rar", available: true),
|
GameBuild(gameVersion: "6.21", link: "https://builds.rebootfn.org/6.21.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.22", link: "https://public.simplyblk.xyz/6.22.rar", available: true),
|
GameBuild(gameVersion: "6.22", link: "https://builds.rebootfn.org/6.22.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.30", link: "https://public.simplyblk.xyz/6.30.rar", available: true),
|
GameBuild(gameVersion: "6.30", link: "https://builds.rebootfn.org/6.30.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "6.31", link: "https://public.simplyblk.xyz/6.31.rar", available: true),
|
GameBuild(gameVersion: "6.31", link: "https://builds.rebootfn.org/6.31.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "7.00", link: "https://public.simplyblk.xyz/7.00.rar", available: true),
|
GameBuild(gameVersion: "7.00", link: "https://builds.rebootfn.org/7.00.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "7.10", link: "https://public.simplyblk.xyz/7.10.rar", available: true),
|
GameBuild(gameVersion: "7.10", link: "https://builds.rebootfn.org/7.10.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "7.20", link: "https://public.simplyblk.xyz/7.20.rar", available: true),
|
GameBuild(gameVersion: "7.20", link: "https://builds.rebootfn.org/7.20.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "7.30", link: "https://public.simplyblk.xyz/7.30.zip", available: true),
|
GameBuild(gameVersion: "7.30", link: "https://builds.rebootfn.org/7.30.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "7.40", link: "https://public.simplyblk.xyz/7.40.rar", available: true),
|
GameBuild(gameVersion: "7.40", link: "https://builds.rebootfn.org/7.40.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "8.00", link: "https://public.simplyblk.xyz/8.00.zip", available: true),
|
GameBuild(gameVersion: "8.00", link: "https://builds.rebootfn.org/8.00.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "8.20", link: "https://public.simplyblk.xyz/8.20.rar", available: true),
|
GameBuild(gameVersion: "8.20", link: "https://builds.rebootfn.org/8.20.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "8.30", link: "https://public.simplyblk.xyz/8.30.rar", available: true),
|
GameBuild(gameVersion: "8.30", link: "https://builds.rebootfn.org/8.30.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "8.40", link: "https://public.simplyblk.xyz/8.40.zip", available: true),
|
GameBuild(gameVersion: "8.40", link: "https://builds.rebootfn.org/8.40.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "8.50", link: "https://public.simplyblk.xyz/8.50.zip", available: true),
|
GameBuild(gameVersion: "8.50", link: "https://builds.rebootfn.org/8.50.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "8.51", link: "https://public.simplyblk.xyz/8.51.rar", available: true),
|
GameBuild(gameVersion: "8.51", link: "https://builds.rebootfn.org/8.51.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "9.00", link: "https://public.simplyblk.xyz/9.00.zip", available: true),
|
GameBuild(gameVersion: "9.00", link: "https://builds.rebootfn.org/9.00.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "9.01", link: "https://public.simplyblk.xyz/9.01.zip", available: true),
|
GameBuild(gameVersion: "9.01", link: "https://builds.rebootfn.org/9.01.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "9.10", link: "https://public.simplyblk.xyz/9.10.rar", available: true),
|
GameBuild(gameVersion: "9.10", link: "https://builds.rebootfn.org/9.10.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "9.21", link: "https://public.simplyblk.xyz/9.21.zip", available: true),
|
GameBuild(gameVersion: "9.21", link: "https://builds.rebootfn.org/9.21.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "9.30", link: "https://public.simplyblk.xyz/9.30.zip", available: true),
|
GameBuild(gameVersion: "9.30", link: "https://builds.rebootfn.org/9.30.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "9.40", link: "https://public.simplyblk.xyz/9.40.zip", available: true),
|
GameBuild(gameVersion: "9.40", link: "https://builds.rebootfn.org/9.40.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "9.41", link: "https://public.simplyblk.xyz/9.41.rar", available: true),
|
GameBuild(gameVersion: "9.41", link: "https://builds.rebootfn.org/9.41.rar", available: true),
|
||||||
FortniteBuild(gameVersion: "10.00", link: "https://public.simplyblk.xyz/10.00.zip", available: true),
|
GameBuild(gameVersion: "10.00", link: "https://builds.rebootfn.org/10.00.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "10.10", link: "https://public.simplyblk.xyz/10.10.zip", available: true),
|
GameBuild(gameVersion: "10.10", link: "https://builds.rebootfn.org/10.10.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "10.20", link: "https://public.simplyblk.xyz/10.20.zip", available: true),
|
GameBuild(gameVersion: "10.20", link: "https://builds.rebootfn.org/10.20.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "10.31", link: "https://public.simplyblk.xyz/10.31.zip", available: true),
|
GameBuild(gameVersion: "10.31", link: "https://builds.rebootfn.org/10.31.zip", available: true),
|
||||||
FortniteBuild(gameVersion: "10.40", link: "https://public.simplyblk.xyz/10.40.rar", available: true),
|
GameBuild(gameVersion: "10.40", link: "https://builds.rebootfn.org/10.40.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "11.00", link: "https://public.simplyblk.xyz/11.00.zip", available: true),
|
GameBuild(gameVersion: "11.00", link: "https://builds.rebootfn.org/11.00.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "11.31", link: "https://public.simplyblk.xyz/11.31.rar", available: true),
|
GameBuild(gameVersion: "11.31", link: "https://builds.rebootfn.org/11.31.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "12.00", link: "https://public.simplyblk.xyz/12.00.rar", available: true),
|
GameBuild(gameVersion: "12.00", link: "https://builds.rebootfn.org/12.00.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "12.21", link: "https://public.simplyblk.xyz/12.21.zip", available: true),
|
GameBuild(gameVersion: "12.21", link: "https://builds.rebootfn.org/12.21.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "12.50", link: "https://public.simplyblk.xyz/12.50.zip", available: true),
|
GameBuild(gameVersion: "Fortnite 12.41", link: "https://builds.rebootfn.org/Fortnite%2012.41.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "12.61", link: "https://public.simplyblk.xyz/12.61.zip", available: true),
|
GameBuild(gameVersion: "12.50", link: "https://builds.rebootfn.org/12.50.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "13.00", link: "https://public.simplyblk.xyz/13.00.rar", available: true),
|
GameBuild(gameVersion: "12.61", link: "https://builds.rebootfn.org/12.61.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "13.40", link: "https://public.simplyblk.xyz/13.40.zip", available: true),
|
GameBuild(gameVersion: "13.00", link: "https://builds.rebootfn.org/13.00.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "14.00", link: "https://public.simplyblk.xyz/14.00.rar", available: true),
|
GameBuild(gameVersion: "13.40", link: "https://builds.rebootfn.org/13.40.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "14.40", link: "https://public.simplyblk.xyz/14.40.rar", available: true),
|
GameBuild(gameVersion: "14.00", link: "https://builds.rebootfn.org/14.00.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "14.60", link: "https://public.simplyblk.xyz/14.60.rar", available: true),
|
GameBuild(gameVersion: "14.40", link: "https://builds.rebootfn.org/14.40.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "15.30", link: "https://public.simplyblk.xyz/15.30.rar", available: true),
|
GameBuild(gameVersion: "14.60", link: "https://builds.rebootfn.org/14.60.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "16.40", link: "https://public.simplyblk.xyz/16.40.rar", available: true),
|
GameBuild(gameVersion: "15.30", link: "https://builds.rebootfn.org/15.30.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "17.30", link: "https://public.simplyblk.xyz/17.30.zip", available: true),
|
GameBuild(gameVersion: "16.40", link: "https://builds.rebootfn.org/16.40.rar", available: false),
|
||||||
FortniteBuild(gameVersion: "17.50", link: "https://public.simplyblk.xyz/17.50.zip", available: true),
|
GameBuild(gameVersion: "17.30", link: "https://builds.rebootfn.org/17.30.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "18.40", link: "https://public.simplyblk.xyz/18.40.zip", available: true),
|
GameBuild(gameVersion: "17.50", link: "https://builds.rebootfn.org/17.50.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "19.10", link: "https://public.simplyblk.xyz/19.10.rar", available: true),
|
GameBuild(gameVersion: "18.40", link: "https://builds.rebootfn.org/18.40.zip", available: false),
|
||||||
FortniteBuild(gameVersion: "20.40", link: "https://public.simplyblk.xyz/20.40.zip", available: true),
|
GameBuild(gameVersion: "19.10", link: "https://builds.rebootfn.org/19.10.rar", available: false),
|
||||||
|
GameBuild(gameVersion: "20.40", link: "https://builds.rebootfn.org/20.40.zip", available: false)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
Future<void> downloadArchiveBuild(GameBuildDownloadOptions options) async {
|
||||||
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||||
final outputFile = File("${options.destination.path}\\.build\\$fileName");
|
final outputFile = File("${options.destination.path}\\.build\\$fileName");
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
@@ -259,7 +260,7 @@ Future<bool> _isAriaRunning() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
|
Future<String> _startAriaDownload(GameBuildDownloadOptions options, File outputFile) async {
|
||||||
http.Response? addDownloadResponse;
|
http.Response? addDownloadResponse;
|
||||||
try {
|
try {
|
||||||
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
|
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
|
||||||
@@ -311,7 +312,7 @@ Future<void> stopDownloadServer() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
|
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, GameBuildDownloadOptions options) async {
|
||||||
Process? process;
|
Process? process;
|
||||||
switch (extension.toLowerCase()) {
|
switch (extension.toLowerCase()) {
|
||||||
case ".zip":
|
case ".zip":
|
||||||
@@ -437,7 +438,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
|
|
||||||
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
|
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
|
||||||
if(percentage == 0) {
|
if(percentage == 0) {
|
||||||
port.send(FortniteBuildDownloadProgress(
|
port.send(GameBuildDownloadProgress(
|
||||||
progress: percentage,
|
progress: percentage,
|
||||||
extracting: extracting,
|
extracting: extracting,
|
||||||
timeLeft: null,
|
timeLeft: null,
|
||||||
@@ -446,7 +447,7 @@ void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, b
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
port.send(FortniteBuildDownloadProgress(
|
port.send(GameBuildDownloadProgress(
|
||||||
progress: percentage,
|
progress: percentage,
|
||||||
extracting: extracting,
|
extracting: extracting,
|
||||||
timeLeft: minutesLeft,
|
timeLeft: minutesLeft,
|
||||||
@@ -454,13 +455,13 @@ void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, b
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onError(Object? error, FortniteBuildDownloadOptions options) {
|
void _onError(Object? error, GameBuildDownloadOptions options) {
|
||||||
if(error != null) {
|
if(error != null) {
|
||||||
options.port.send(error.toString());
|
options.port.send(error.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
Completer<dynamic> _setupLifecycle(GameBuildDownloadOptions options) {
|
||||||
var stopped = Completer();
|
var stopped = Completer();
|
||||||
var lifecyclePort = ReceivePort();
|
var lifecyclePort = ReceivePort();
|
||||||
lifecyclePort.listen((message) {
|
lifecyclePort.listen((message) {
|
||||||
@@ -473,8 +474,6 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
|||||||
return stopped;
|
return stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
||||||
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||||
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
|
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
|
||||||
@@ -482,16 +481,16 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
|
|||||||
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> downloadDependency(InjectableDll dll, String outputPath) async {
|
Future<bool> downloadDependency(GameDll dll, String outputPath) async {
|
||||||
String? name;
|
String? name;
|
||||||
switch(dll) {
|
switch(dll) {
|
||||||
case InjectableDll.console:
|
case GameDll.console:
|
||||||
name = "console.dll";
|
name = "console.dll";
|
||||||
case InjectableDll.auth:
|
case GameDll.auth:
|
||||||
name = "cobalt.dll";
|
name = "cobalt.dll";
|
||||||
case InjectableDll.memoryLeak:
|
case GameDll.memoryLeak:
|
||||||
name = "memory.dll";
|
name = "memory.dll";
|
||||||
case InjectableDll.gameServer:
|
case GameDll.gameServer:
|
||||||
name = null;
|
name = null;
|
||||||
}
|
}
|
||||||
if(name == null) {
|
if(name == null) {
|
||||||
@@ -9,7 +9,7 @@ class GameInstance {
|
|||||||
final int gamePid;
|
final int gamePid;
|
||||||
final int? launcherPid;
|
final int? launcherPid;
|
||||||
final int? eacPid;
|
final int? eacPid;
|
||||||
final List<InjectableDll> injectedDlls;
|
final List<GameDll> injectedDlls;
|
||||||
final bool headless;
|
final bool headless;
|
||||||
bool launched;
|
bool launched;
|
||||||
bool tokenError;
|
bool tokenError;
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
class FortniteVersion {
|
class GameVersion {
|
||||||
String name;
|
String name;
|
||||||
String gameVersion;
|
String gameVersion;
|
||||||
Directory location;
|
Directory location;
|
||||||
|
|
||||||
FortniteVersion.fromJson(json)
|
GameVersion.fromJson(json)
|
||||||
: name = json["name"],
|
: name = json["name"],
|
||||||
gameVersion = json["gameVersion"],
|
gameVersion = json["gameVersion"],
|
||||||
location = Directory(json["location"]);
|
location = Directory(json["location"]);
|
||||||
|
|
||||||
FortniteVersion({required this.name, required this.gameVersion, required this.location});
|
GameVersion({required this.name, required this.gameVersion, required this.location});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'name': name,
|
'name': name,
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
enum InjectableDll {
|
|
||||||
console,
|
|
||||||
auth,
|
|
||||||
gameServer,
|
|
||||||
memoryLeak
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InjectableDllVersionAware on InjectableDll {
|
|
||||||
bool get isVersionDependent => this == InjectableDll.gameServer;
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
class FortniteBuild {
|
|
||||||
final String gameVersion;
|
|
||||||
final String link;
|
|
||||||
final bool available;
|
|
||||||
|
|
||||||
FortniteBuild({
|
|
||||||
required this.gameVersion,
|
|
||||||
required this.link,
|
|
||||||
required this.available
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class FortniteBuildDownloadProgress {
|
|
||||||
final double progress;
|
|
||||||
final int? timeLeft;
|
|
||||||
final bool extracting;
|
|
||||||
final int speed;
|
|
||||||
|
|
||||||
FortniteBuildDownloadProgress({
|
|
||||||
required this.progress,
|
|
||||||
required this.extracting,
|
|
||||||
required this.timeLeft,
|
|
||||||
required this.speed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class FortniteBuildDownloadOptions {
|
|
||||||
FortniteBuild build;
|
|
||||||
Directory destination;
|
|
||||||
SendPort port;
|
|
||||||
|
|
||||||
FortniteBuildDownloadOptions(this.build, this.destination, this.port);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FortniteBuildManifestChunk {
|
|
||||||
List<int> chunksIds;
|
|
||||||
String file;
|
|
||||||
int fileSize;
|
|
||||||
|
|
||||||
FortniteBuildManifestChunk._internal(this.chunksIds, this.file, this.fileSize);
|
|
||||||
|
|
||||||
factory FortniteBuildManifestChunk.fromJson(json) => FortniteBuildManifestChunk._internal(
|
|
||||||
List<int>.from(json["ChunksIds"] as List),
|
|
||||||
json["File"],
|
|
||||||
json["FileSize"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class FortniteBuildManifestFile {
|
|
||||||
String name;
|
|
||||||
List<FortniteBuildManifestChunk> chunks;
|
|
||||||
int size;
|
|
||||||
|
|
||||||
FortniteBuildManifestFile._internal(this.name, this.chunks, this.size);
|
|
||||||
|
|
||||||
factory FortniteBuildManifestFile.fromJson(json) => FortniteBuildManifestFile._internal(
|
|
||||||
json["Name"],
|
|
||||||
List<FortniteBuildManifestChunk>.from(json["Chunks"].map((chunk) => FortniteBuildManifestChunk.fromJson(chunk))),
|
|
||||||
json["Size"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
enum UpdateStatus {
|
|
||||||
waiting,
|
|
||||||
started,
|
|
||||||
success,
|
|
||||||
error
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
enum UpdateTimer {
|
|
||||||
never,
|
|
||||||
hour,
|
|
||||||
day,
|
|
||||||
week
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
@@ -41,7 +40,7 @@ String? get antiVirusName {
|
|||||||
final hr = CoCreateInstance(
|
final hr = CoCreateInstance(
|
||||||
rclsid,
|
rclsid,
|
||||||
nullptr,
|
nullptr,
|
||||||
CLSCTX.CLSCTX_INPROC_SERVER,
|
CLSCTX_INPROC_SERVER,
|
||||||
riid,
|
riid,
|
||||||
pLoc.cast(),
|
pLoc.cast(),
|
||||||
);
|
);
|
||||||
@@ -84,7 +83,7 @@ String? get antiVirusName {
|
|||||||
final hr3 = service.execQuery(
|
final hr3 = service.execQuery(
|
||||||
wql,
|
wql,
|
||||||
query,
|
query,
|
||||||
WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_FORWARD_ONLY | WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_RETURN_IMMEDIATELY,
|
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
|
||||||
nullptr,
|
nullptr,
|
||||||
pEnumerator.cast(),
|
pEnumerator.cast(),
|
||||||
);
|
);
|
||||||
@@ -125,7 +124,7 @@ String? get antiVirusName {
|
|||||||
|
|
||||||
calloc.free(propName);
|
calloc.free(propName);
|
||||||
|
|
||||||
if (SUCCEEDED(hr5) && vtProp.ref.vt == VARENUM.VT_BSTR) {
|
if (SUCCEEDED(hr5) && vtProp.ref.vt == VT_BSTR) {
|
||||||
final bstr = vtProp.ref.bstrVal;
|
final bstr = vtProp.ref.bstrVal;
|
||||||
result = bstr.toDartString();
|
result = bstr.toDartString();
|
||||||
}
|
}
|
||||||
@@ -225,7 +224,7 @@ bool killProcessByPort(int port) {
|
|||||||
_TCP_TABLE_OWNER_PID_LISTENER,
|
_TCP_TABLE_OWNER_PID_LISTENER,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
if (result == WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) {
|
if (result == ERROR_INSUFFICIENT_BUFFER) {
|
||||||
calloc.free(pTcpTable);
|
calloc.free(pTcpTable);
|
||||||
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
|
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
|
||||||
result = _getExtendedTcpTable(
|
result = _getExtendedTcpTable(
|
||||||
@@ -247,7 +246,7 @@ bool killProcessByPort(int port) {
|
|||||||
final pid = row.dwOwningPid;
|
final pid = row.dwOwningPid;
|
||||||
calloc.free(pTcpTable);
|
calloc.free(pTcpTable);
|
||||||
calloc.free(dwSize);
|
calloc.free(dwSize);
|
||||||
final hProcess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE, FALSE, pid);
|
final hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
|
||||||
if (hProcess != NULL) {
|
if (hProcess != NULL) {
|
||||||
final result = TerminateProcess(hProcess, 0);
|
final result = TerminateProcess(hProcess, 0);
|
||||||
CloseHandle(hProcess);
|
CloseHandle(hProcess);
|
||||||
@@ -345,8 +344,8 @@ Future<bool> startElevatedProcess({required String executable, required String a
|
|||||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||||
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
|
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
|
||||||
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
|
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
||||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||||
return ShellExecuteEx(shellInput) == 1;
|
return ShellExecuteEx(shellInput) == 1;
|
||||||
@@ -402,7 +401,7 @@ final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
|||||||
int Function(int hWnd)>('NtSuspendProcess');
|
int Function(int hWnd)>('NtSuspendProcess');
|
||||||
|
|
||||||
bool suspend(int pid) {
|
bool suspend(int pid) {
|
||||||
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||||
try {
|
try {
|
||||||
return _NtSuspendProcess(processHandle) == 0;
|
return _NtSuspendProcess(processHandle) == 0;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -411,7 +410,7 @@ bool suspend(int pid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool resume(int pid) {
|
bool resume(int pid) {
|
||||||
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
|
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
|
||||||
try {
|
try {
|
||||||
return _NtResumeProcess(processHandle) == 0;
|
return _NtResumeProcess(processHandle) == 0;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -90,8 +90,8 @@
|
|||||||
"settingsClientArgsName": "Custom launch arguments",
|
"settingsClientArgsName": "Custom launch arguments",
|
||||||
"settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
|
"settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
|
||||||
"settingsClientArgsPlaceholder": "Arguments...",
|
"settingsClientArgsPlaceholder": "Arguments...",
|
||||||
"settingsServerName": "Internal files",
|
"settingsServerName": "Game server",
|
||||||
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
"settingsServerSubtitle": "Creates the game server on top of a Fortnite instance",
|
||||||
"settingsServerOptionsName": "Options",
|
"settingsServerOptionsName": "Options",
|
||||||
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
||||||
"settingsServerTypeName": "Game server type",
|
"settingsServerTypeName": "Game server type",
|
||||||
@@ -182,6 +182,7 @@
|
|||||||
"joinSelfServer": "You can't join your own server",
|
"joinSelfServer": "You can't join your own server",
|
||||||
"cannotJoinServerVersion": "You can't join this server: download Fortnite {version}",
|
"cannotJoinServerVersion": "You can't join this server: download Fortnite {version}",
|
||||||
"wrongServerPassword": "Wrong password: please try again",
|
"wrongServerPassword": "Wrong password: please try again",
|
||||||
|
"joiningServer": "Joining {name}...",
|
||||||
"offlineServer": "This server isn't online right now: please try again later",
|
"offlineServer": "This server isn't online right now: please try again later",
|
||||||
"serverPassword": "Password",
|
"serverPassword": "Password",
|
||||||
"serverPasswordPlaceholder": "Type the server's password",
|
"serverPasswordPlaceholder": "Type the server's password",
|
||||||
@@ -340,7 +341,7 @@
|
|||||||
"promptServerBrowserPageActionLabel": "Next",
|
"promptServerBrowserPageActionLabel": "Next",
|
||||||
"promptHostPageText": "The Host tab is used to host a game server.\nWhen you usually play Fortnite, you connect to an Epic Games' game server.\nTo play using Reboot, you'll need to host the game server yourself, or join someone else's.\nOtherwise, you will be sent back to the lobby when trying to join a game.",
|
"promptHostPageText": "The Host tab is used to host a game server.\nWhen you usually play Fortnite, you connect to an Epic Games' game server.\nTo play using Reboot, you'll need to host the game server yourself, or join someone else's.\nOtherwise, you will be sent back to the lobby when trying to join a game.",
|
||||||
"promptHostPageActionLabel": "Next",
|
"promptHostPageActionLabel": "Next",
|
||||||
"promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't want other players to join your server, you can skip this part",
|
"promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't to use the Server Browser, you can skip this part for now and come back later.",
|
||||||
"promptHostInfoActionLabelSkip": "Skip",
|
"promptHostInfoActionLabelSkip": "Skip",
|
||||||
"promptHostInfoActionLabelConfigure": "Configure",
|
"promptHostInfoActionLabelConfigure": "Configure",
|
||||||
"promptHostInformationText": "Choose the name for your server",
|
"promptHostInformationText": "Choose the name for your server",
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/error.dart';
|
import 'package:reboot_launcher/src/message/error.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/home_page.dart';
|
import 'package:reboot_launcher/src/pager/pager.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/url_protocol.dart';
|
import 'package:reboot_launcher/src/util/url_protocol.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@@ -35,207 +35,140 @@ bool appWithNoStorage = false;
|
|||||||
void main() {
|
void main() {
|
||||||
log("[APP] Called");
|
log("[APP] Called");
|
||||||
runZonedGuarded(
|
runZonedGuarded(
|
||||||
() => _startApp(),
|
() => _startApp(),
|
||||||
(error, stack) => onError(error, stack, false),
|
(error, stack) => onError(error, stack, false),
|
||||||
zoneSpecification: ZoneSpecification(
|
zoneSpecification: ZoneSpecification(
|
||||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If anything fails here, the app won't start
|
||||||
|
// Be extremely careful
|
||||||
Future<void> _startApp() async {
|
Future<void> _startApp() async {
|
||||||
_overrideHttpCertificate();
|
final errors = <String>[];
|
||||||
final errors = <Object>[];
|
Future<T?> runCatching<T>({
|
||||||
|
required FutureOr<T> Function() callable,
|
||||||
|
required String Function(Object) errorFormatter
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return callable();
|
||||||
|
}catch(error) {
|
||||||
|
errors.add(errorFormatter(error));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[APP] Starting application");
|
||||||
try {
|
try {
|
||||||
log("[APP] Starting application");
|
|
||||||
final pathError = await _initPath();
|
|
||||||
if(pathError != null) {
|
|
||||||
errors.add(pathError);
|
|
||||||
}
|
|
||||||
|
|
||||||
final databaseError = await _initDatabase();
|
|
||||||
if(databaseError != null) {
|
|
||||||
errors.add(databaseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
final notificationsError = await _initNotifications();
|
|
||||||
if(notificationsError != null) {
|
|
||||||
errors.add(notificationsError);
|
|
||||||
}
|
|
||||||
|
|
||||||
final versionError = await _initVersion();
|
|
||||||
if(versionError != null) {
|
|
||||||
errors.add(versionError);
|
|
||||||
}
|
|
||||||
|
|
||||||
final storageErrors = await _initStorage();
|
|
||||||
errors.addAll(storageErrors);
|
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await runCatching(
|
||||||
_initWindow();
|
callable: () => installationDirectory.create(
|
||||||
|
recursive: true
|
||||||
final urlError = await _initUrlHandler();
|
),
|
||||||
if(urlError != null) {
|
errorFormatter: (error) => "Cannot create installation directory: $error"
|
||||||
errors.add(urlError);
|
);
|
||||||
}
|
await runCatching(
|
||||||
}catch(uncaughtError) {
|
callable: () => localNotifier.setup(
|
||||||
errors.add(uncaughtError);
|
appName: 'Reboot Launcher',
|
||||||
} finally{
|
shortcutPolicy: ShortcutPolicy.ignore
|
||||||
|
),
|
||||||
|
errorFormatter: (error) => "Cannot create installation directory: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () async {
|
||||||
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
appVersion = Version.parse(packageInfo.version);
|
||||||
|
},
|
||||||
|
errorFormatter: (error) => "Cannot parse version: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () async {
|
||||||
|
await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
|
||||||
|
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
|
||||||
|
},
|
||||||
|
errorFormatter: (error) {
|
||||||
|
appWithNoStorage = true;
|
||||||
|
return "Cannot access storage: $error";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () => Get.put(GameController(), permanent: true),
|
||||||
|
errorFormatter: (error) => "Cannot create game controller: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () => Get.put(BackendController(), permanent: true),
|
||||||
|
errorFormatter: (error) => "Cannot create backend controller: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () => Get.put(HostingController(), permanent: true),
|
||||||
|
errorFormatter: (error) => "Cannot create backend controller: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () => Get.put(ServerBrowserController(), permanent: true),
|
||||||
|
errorFormatter: (error) => "Cannot create browser controller: $error"
|
||||||
|
);
|
||||||
|
final settingsController = await runCatching(
|
||||||
|
callable: () => Get.put(SettingsController(), permanent: true),
|
||||||
|
errorFormatter: (error) => "Cannot create settings controller: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () => Get.put(DllController(), permanent: true),
|
||||||
|
errorFormatter: (error) => "Cannot create dll controller: $error"
|
||||||
|
);
|
||||||
|
await runCatching(
|
||||||
|
callable: () async {
|
||||||
|
try {
|
||||||
|
await SystemTheme.accentColor.load();
|
||||||
|
await windowManager.ensureInitialized();
|
||||||
|
await Window.initialize();
|
||||||
|
if(settingsController != null) {
|
||||||
|
final size = Size(settingsController.width, settingsController.height);
|
||||||
|
await windowManager.setSize(size);
|
||||||
|
final offsetX = settingsController.offsetX;
|
||||||
|
final offsetY = settingsController.offsetY;
|
||||||
|
if (offsetX != null && offsetY != null) {
|
||||||
|
final position = Offset(
|
||||||
|
offsetX,
|
||||||
|
offsetY
|
||||||
|
);
|
||||||
|
await windowManager.setPosition(position);
|
||||||
|
} else {
|
||||||
|
await windowManager.setAlignment(Alignment.center);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await windowManager.setPreventClose(true);
|
||||||
|
await windowManager.setResizable(true);
|
||||||
|
if(isWin11) {
|
||||||
|
await Window.setEffect(
|
||||||
|
effect: WindowEffect.acrylic,
|
||||||
|
color: Colors.green,
|
||||||
|
dark: isDarkMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
windowManager.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorFormatter: (error) => "Cannot configure window: $error"
|
||||||
|
);
|
||||||
|
runCatching(
|
||||||
|
callable: () => registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']),
|
||||||
|
errorFormatter: (error) => "Cannot configure custom url scheme: $error"
|
||||||
|
);
|
||||||
|
}catch(error) {
|
||||||
|
errors.add("Uncaught error: $error");
|
||||||
|
}finally {
|
||||||
log("[APP] Started applications with errors: $errors");
|
log("[APP] Started applications with errors: $errors");
|
||||||
runApp(RebootApplication(errors: errors));
|
runApp(RebootApplication(errors: errors));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyHttpOverrides extends HttpOverrides {
|
|
||||||
@override
|
|
||||||
HttpClient createHttpClient(SecurityContext? context){
|
|
||||||
return super.createHttpClient(context)
|
|
||||||
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _overrideHttpCertificate() {
|
|
||||||
HttpOverrides.global = _MyHttpOverrides(); // Not safe, but necessary
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Object?> _initNotifications() async {
|
|
||||||
try {
|
|
||||||
await localNotifier.setup(
|
|
||||||
appName: 'Reboot Launcher',
|
|
||||||
shortcutPolicy: ShortcutPolicy.ignore
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}catch(error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Object?> _initDatabase() async {
|
|
||||||
try {
|
|
||||||
await Supabase.initialize(
|
|
||||||
url: supabaseUrl,
|
|
||||||
anonKey: supabaseAnonKey
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}catch(error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Object?> _initPath() async {
|
|
||||||
try {
|
|
||||||
await installationDirectory.create(recursive: true);
|
|
||||||
return null;
|
|
||||||
}catch(error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Object?> _initVersion() async {
|
|
||||||
try {
|
|
||||||
final packageInfo = await PackageInfo.fromPlatform();
|
|
||||||
appVersion = Version.parse(packageInfo.version);
|
|
||||||
return null;
|
|
||||||
}catch(error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Object?> _initUrlHandler() async {
|
|
||||||
try {
|
|
||||||
registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
|
|
||||||
return null;
|
|
||||||
}catch(error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initWindow() async {
|
|
||||||
try {
|
|
||||||
await SystemTheme.accentColor.load();
|
|
||||||
await windowManager.ensureInitialized();
|
|
||||||
await Window.initialize();
|
|
||||||
var settingsController = Get.find<SettingsController>();
|
|
||||||
var size = Size(settingsController.width, settingsController.height);
|
|
||||||
await windowManager.setSize(size);
|
|
||||||
var offsetX = settingsController.offsetX;
|
|
||||||
var offsetY = settingsController.offsetY;
|
|
||||||
if(offsetX != null && offsetY != null) {
|
|
||||||
final position = Offset(
|
|
||||||
offsetX,
|
|
||||||
offsetY
|
|
||||||
);
|
|
||||||
await windowManager.setPosition(position);
|
|
||||||
}else {
|
|
||||||
await windowManager.setAlignment(Alignment.center);
|
|
||||||
}
|
|
||||||
await windowManager.setPreventClose(true);
|
|
||||||
await windowManager.setResizable(true);
|
|
||||||
if(isWin11) {
|
|
||||||
await Window.setEffect(
|
|
||||||
effect: WindowEffect.acrylic,
|
|
||||||
color: Colors.green,
|
|
||||||
dark: isDarkMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}catch(error, stackTrace) {
|
|
||||||
onError(error, stackTrace, false);
|
|
||||||
}finally {
|
|
||||||
windowManager.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Object>> _initStorage() async {
|
|
||||||
final errors = <Object>[];
|
|
||||||
try {
|
|
||||||
await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
|
|
||||||
await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
|
|
||||||
await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
|
|
||||||
await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
|
|
||||||
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
|
|
||||||
}catch(error) {
|
|
||||||
appWithNoStorage = true;
|
|
||||||
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Get.put(GameController());
|
|
||||||
}catch(error) {
|
|
||||||
errors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Get.put(BackendController());
|
|
||||||
}catch(error) {
|
|
||||||
errors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final controller = HostingController();
|
|
||||||
Get.put(controller);
|
|
||||||
controller.discardServer();
|
|
||||||
}catch(error) {
|
|
||||||
errors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Get.put(SettingsController());
|
|
||||||
}catch(error) {
|
|
||||||
errors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Get.put(DllController());
|
|
||||||
}catch(error) {
|
|
||||||
errors.add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RebootApplication extends StatefulWidget {
|
class RebootApplication extends StatefulWidget {
|
||||||
final List<Object> errors;
|
final List<String> errors;
|
||||||
const RebootApplication({Key? key, required this.errors}) : super(key: key);
|
const RebootApplication({Key? key, required this.errors}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -248,15 +181,12 @@ class _RebootApplicationState extends State<RebootApplication> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(widget.errors));
|
// Not pretty but make sure the errors are shown
|
||||||
}
|
Future.delayed(const Duration(seconds: 5)).then((_) {
|
||||||
|
for(final error in widget.errors) {
|
||||||
void _handleErrors(List<Object?> errors) {
|
|
||||||
for(final error in errors) {
|
|
||||||
if(error != null) {
|
|
||||||
onError(error, null, false);
|
onError(error, null, false);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -272,7 +202,7 @@ class _RebootApplicationState extends State<RebootApplication> {
|
|||||||
color: SystemTheme.accentColor.accent.toAccentColor(),
|
color: SystemTheme.accentColor.accent.toAccentColor(),
|
||||||
darkTheme: _createTheme(Brightness.dark),
|
darkTheme: _createTheme(Brightness.dark),
|
||||||
theme: _createTheme(Brightness.light),
|
theme: _createTheme(Brightness.light),
|
||||||
home: const HomePage()
|
home: const RebootPager()
|
||||||
));
|
));
|
||||||
|
|
||||||
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
|
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
|
||||||
|
|||||||
89
gui/lib/src/button/backend_start_button.dart
Normal file
89
gui/lib/src/button/backend_start_button.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/message/backend.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../messenger/info_bar.dart';
|
||||||
|
|
||||||
|
class BackendButton extends StatefulWidget {
|
||||||
|
const BackendButton({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BackendButton> createState() => _BackendButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackendButtonState extends State<BackendButton> {
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
final StreamController<void> _textController = StreamController.broadcast();
|
||||||
|
late final void Function() _listener = () => _textController.add(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_backendController.port.addListener(_listener);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_backendController.port.removeListener(_listener);
|
||||||
|
_textController.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Align(
|
||||||
|
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: () => _backendController.toggle(
|
||||||
|
eventHandler: (type, event) {
|
||||||
|
_backendController.started.value = event.type.isStart && !event.type.isError;
|
||||||
|
if(event.type == AuthBackendResultType.startedImplementation) {
|
||||||
|
_backendController.implementation = event.implementation;
|
||||||
|
}
|
||||||
|
return onBackendResult(type, event);
|
||||||
|
},
|
||||||
|
errorHandler: (error) {
|
||||||
|
if(_backendController.started.value) {
|
||||||
|
_backendController.stop();
|
||||||
|
_gameController.instance.value?.kill();
|
||||||
|
_hostingController.instance.value?.kill();
|
||||||
|
onBackendError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
String get _buttonText {
|
||||||
|
if(_backendController.type.value == AuthBackendType.local && _backendController.port.text.trim() == kDefaultBackendPort.toString()){
|
||||||
|
return translations.checkServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_backendController.started.value){
|
||||||
|
return translations.stopServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations.startServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
|
||||||
typedef FileSelectorValidator = String? Function(String?);
|
typedef FileSelectorValidator = String? Function(String?);
|
||||||
@@ -13,10 +13,12 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
import 'package:reboot_launcher/src/message/backend.dart';
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
@@ -39,6 +41,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
|
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
|
||||||
|
|
||||||
InfoBarEntry? _gameClientInfoBar;
|
InfoBarEntry? _gameClientInfoBar;
|
||||||
InfoBarEntry? _gameServerInfoBar;
|
InfoBarEntry? _gameServerInfoBar;
|
||||||
@@ -91,8 +94,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
log("[${host ? 'HOST' : 'GAME'}] Setting started...");
|
log("[${host ? 'HOST' : 'GAME'}] Setting started...");
|
||||||
_setStarted(host, true);
|
_setStarted(host, true);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
log("[${host ? 'HOST' : 'GAME'}] Set started");
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
|
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${GameDll.values}");
|
||||||
for (final injectable in InjectableDll.values) {
|
for (final injectable in GameDll.values) {
|
||||||
if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
|
if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,23 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
||||||
final backendResult = _backendController.started() || await _backendController.toggle();
|
final backendResult = _backendController.started() || await _backendController.toggle(
|
||||||
|
eventHandler: (type, event) {
|
||||||
|
_backendController.started.value = event.type.isStart && !event.type.isError;
|
||||||
|
if(event.type == AuthBackendResultType.startedImplementation) {
|
||||||
|
_backendController.implementation = event.implementation;
|
||||||
|
}
|
||||||
|
return onBackendResult(type, event);
|
||||||
|
},
|
||||||
|
errorHandler: (error) {
|
||||||
|
if(_backendController.started.value) {
|
||||||
|
_backendController.stop();
|
||||||
|
_gameController.instance.value?.kill();
|
||||||
|
_hostingController.instance.value?.kill();
|
||||||
|
onBackendError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
if(!backendResult){
|
if(!backendResult){
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||||
_onStop(
|
_onStop(
|
||||||
@@ -154,14 +173,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool forceLinkedHosting) async {
|
Future<GameInstance?> _startMatchMakingServer(GameVersion version, bool host, bool headless, bool forceLinkedHosting) async {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||||
if(host){
|
if(host){
|
||||||
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
if(!forceLinkedHosting && _backendController.type.value == AuthBackendType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -212,7 +231,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
|
Future<GameInstance?> _startGameProcesses(GameVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
|
||||||
final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
|
final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
|
||||||
final eacProcess = await _createPausedProcess(version, host, kEacExe);
|
final eacProcess = await _createPausedProcess(version, host, kEacExe);
|
||||||
final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
|
final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
|
||||||
@@ -232,17 +251,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
child: linkedHosting
|
child: linkedHosting
|
||||||
);
|
);
|
||||||
if(host){
|
if(host){
|
||||||
_hostingController.discardServer();
|
_serverBrowserController.removeServer(_hostingController.uuid);
|
||||||
_hostingController.instance.value = instance;
|
_hostingController.instance.value = instance;
|
||||||
}else{
|
}else{
|
||||||
_gameController.instance.value = instance;
|
_gameController.instance.value = instance;
|
||||||
}
|
}
|
||||||
await _injectOrShowError(InjectableDll.auth, host);
|
await _injectOrShowError(GameDll.auth, host);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
|
Future<int?> _createGameProcess(GameVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
|
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
|
||||||
try {
|
try {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Deleting $kGFSDKAftermathLibDll...");
|
log("[${host ? 'HOST' : 'GAME'}] Deleting $kGFSDKAftermathLibDll...");
|
||||||
@@ -335,7 +354,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return gameProcess.pid;
|
return gameProcess.pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
|
Future<int?> _createPausedProcess(GameVersion version, bool host, String executableName) async {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
|
log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
|
||||||
final executables = await findFiles(version.location, executableName);
|
final executables = await findFiles(version.location, executableName);
|
||||||
if(executables.isEmpty){
|
if(executables.isEmpty){
|
||||||
@@ -366,7 +385,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMatchEnd(FortniteVersion version) {
|
void _onMatchEnd(GameVersion version) {
|
||||||
if(_hostingController.autoRestart.value) {
|
if(_hostingController.autoRestart.value) {
|
||||||
final notification = LocalNotification(
|
final notification = LocalNotification(
|
||||||
title: translations.gameServerEnd,
|
title: translations.gameServerEnd,
|
||||||
@@ -409,17 +428,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
instance.launched = true;
|
instance.launched = true;
|
||||||
instance.tokenError = false;
|
instance.tokenError = false;
|
||||||
if(_isChapterOne(instance.version)) {
|
if(_isChapterOne(instance.version)) {
|
||||||
await _injectOrShowError(InjectableDll.memoryLeak, host);
|
await _injectOrShowError(GameDll.memoryLeak, host);
|
||||||
}
|
}
|
||||||
if(!host){
|
if(!host){
|
||||||
await _injectOrShowError(InjectableDll.console, host);
|
await _injectOrShowError(GameDll.console, host);
|
||||||
_onGameClientInjected();
|
_onGameClientInjected();
|
||||||
}else {
|
}else {
|
||||||
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
|
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
|
||||||
if(gameServerPort != null) {
|
if(gameServerPort != null) {
|
||||||
await killProcessByPort(gameServerPort);
|
await killProcessByPort(gameServerPort);
|
||||||
}
|
}
|
||||||
await _injectOrShowError(InjectableDll.gameServer, host);
|
await _injectOrShowError(GameDll.gameServer, host);
|
||||||
_onGameServerInjected();
|
_onGameServerInjected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,7 +481,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_backendController.joinLocalhost();
|
_backendController.gameServerAddress.text = kDefaultGameServerHost;
|
||||||
|
|
||||||
final accessible = await _checkPublicGameServer(gameServerPort);
|
final accessible = await _checkPublicGameServer(gameServerPort);
|
||||||
if (!accessible) {
|
if (!accessible) {
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
@@ -477,10 +497,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _hostingController.publishServer(
|
final serverBrowserEntry = await _hostingController.createServerBrowserEntry();
|
||||||
_hostingController.accountUsername.text,
|
await _serverBrowserController.addServer(serverBrowserEntry);
|
||||||
_hostingController.instance.value!.version.toString(),
|
|
||||||
);
|
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.gameServerStarted,
|
translations.gameServerStarted,
|
||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
@@ -599,7 +617,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
|
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
|
||||||
if(host) {
|
if(host) {
|
||||||
_hostingController.discardServer();
|
_serverBrowserController.removeServer(_hostingController.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(reason == _StopReason.normal) {
|
if(reason == _StopReason.normal) {
|
||||||
@@ -692,7 +710,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case _StopReason.tokenError:
|
case _StopReason.tokenError:
|
||||||
_backendController.stop(interactive: false);
|
_backendController.stop();
|
||||||
final injectedDlls = instance?.injectedDlls;
|
final injectedDlls = instance?.injectedDlls;
|
||||||
showRebootInfoBar(
|
showRebootInfoBar(
|
||||||
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
|
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
|
||||||
@@ -729,7 +747,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _injectOrShowError(InjectableDll injectable, bool hosting) async {
|
Future<void> _injectOrShowError(GameDll injectable, bool hosting) async {
|
||||||
final instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
|
final instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
log("[${hosting ? 'HOST' : 'GAME'}] No instance found to inject ${injectable.name}");
|
log("[${hosting ? 'HOST' : 'GAME'}] No instance found to inject ${injectable.name}");
|
||||||
@@ -762,7 +780,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
|
Future<File?> _getDllFileOrStop(String version, GameDll injectable, bool host) async {
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||||
final (file, customDll) = _dllController.getInjectableData(version, injectable);
|
final (file, customDll) = _dllController.getInjectableData(version, injectable);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
|
||||||
@@ -802,7 +820,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
|
|
||||||
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
|
InfoBarEntry _showLaunchingGameClientWidget(GameVersion version, bool headless, bool linkedHosting) {
|
||||||
return _gameClientInfoBar = showRebootInfoBar(
|
return _gameClientInfoBar = showRebootInfoBar(
|
||||||
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -818,7 +836,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
),
|
),
|
||||||
child: Button(
|
child: Button(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_backendController.joinLocalhost();
|
_backendController.gameServerAddress.text = kDefaultGameServerHost;
|
||||||
|
|
||||||
if(!_hostingController.started.value) {
|
if(!_hostingController.started.value) {
|
||||||
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
|
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
|
||||||
_gameClientInfoBar?.close();
|
_gameClientInfoBar?.close();
|
||||||
@@ -2,9 +2,9 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
|
|
||||||
class ServerTypeSelector extends StatefulWidget {
|
class ServerTypeSelector extends StatefulWidget {
|
||||||
final Key overlayKey;
|
final Key overlayKey;
|
||||||
@@ -15,7 +15,7 @@ class ServerTypeSelector extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||||
late final BackendController _controller = Get.find<BackendController>();
|
late final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -24,27 +24,27 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
|||||||
child: DropDownButton(
|
child: DropDownButton(
|
||||||
onOpen: () => inDialog = true,
|
onOpen: () => inDialog = true,
|
||||||
onClose: () => inDialog = false,
|
onClose: () => inDialog = false,
|
||||||
leading: Text(_controller.type.value.label),
|
leading: Text(_backendController.type.value.label),
|
||||||
items: ServerType.values
|
items: AuthBackendType.values
|
||||||
.map((type) => _createItem(type))
|
.map((type) => _createItem(type))
|
||||||
.toList()
|
.toList()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem(
|
MenuFlyoutItem _createItem(AuthBackendType type) => MenuFlyoutItem(
|
||||||
text: Text(type.label),
|
text: Text(type.label),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _controller.stop(interactive: false);
|
await _backendController.stop();
|
||||||
_controller.type.value = type;
|
_backendController.type.value = type;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _ServerTypeExtension on ServerType {
|
extension _ServerTypeExtension on AuthBackendType {
|
||||||
String get label {
|
String get label {
|
||||||
return this == ServerType.embedded ? translations.embedded
|
return this == AuthBackendType.embedded ? translations.embedded
|
||||||
: this == ServerType.remote ? translations.remote
|
: this == AuthBackendType.remote ? translations.remote
|
||||||
: translations.local;
|
: translations.local;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,13 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
import 'package:reboot_launcher/src/message/download_version.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version/download_version.dart';
|
import 'package:reboot_launcher/src/message/import_version.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/version/import_version.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class VersionSelector extends StatefulWidget {
|
class VersionSelector extends StatefulWidget {
|
||||||
@@ -38,7 +38,7 @@ class VersionSelector extends StatefulWidget {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<void> openImportDialog(FortniteVersion? version) => showRebootDialog<bool>(
|
static Future<void> openImportDialog(GameVersion? version) => showRebootDialog<bool>(
|
||||||
builder: (context) => ImportVersionDialog(
|
builder: (context) => ImportVersionDialog(
|
||||||
version: version,
|
version: version,
|
||||||
closable: true,
|
closable: true,
|
||||||
@@ -83,7 +83,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener(
|
Widget _createOptionsMenu({required GameVersion? version, required bool close, required Widget child}) => Listener(
|
||||||
onPointerDown: (event) async {
|
onPointerDown: (event) async {
|
||||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||||
return;
|
return;
|
||||||
@@ -134,7 +134,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuFlyoutItem _createVersionItem(FortniteVersion version) => MenuFlyoutItem(
|
MenuFlyoutItem _createVersionItem(GameVersion version) => MenuFlyoutItem(
|
||||||
text: Listener(
|
text: Listener(
|
||||||
onPointerDown: (event) async {
|
onPointerDown: (event) async {
|
||||||
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
|
||||||
@@ -154,7 +154,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
onPressed: () => _gameController.selectedVersion.value = version
|
onPressed: () => _gameController.selectedVersion.value = version
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _openVersionOptions(FortniteVersion version) async {
|
Future<void> _openVersionOptions(GameVersion version) async {
|
||||||
final result = await _flyoutController.showFlyout<_ContextualOption?>(
|
final result = await _flyoutController.showFlyout<_ContextualOption?>(
|
||||||
builder: (context) => MenuFlyout(
|
builder: (context) => MenuFlyout(
|
||||||
items: _ContextualOption.values
|
items: _ContextualOption.values
|
||||||
@@ -167,7 +167,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
_handleResult(result, version, true);
|
_handleResult(result, version, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleResult(_ContextualOption? result, FortniteVersion version, bool close) async {
|
void _handleResult(_ContextualOption? result, GameVersion version, bool close) async {
|
||||||
if(!mounted){
|
if(!mounted){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> _openDeleteDialog(FortniteVersion version) {
|
Future<bool?> _openDeleteDialog(GameVersion version) {
|
||||||
return showRebootDialog<bool>(
|
return showRebootDialog<bool>(
|
||||||
builder: (context) => ContentDialog(
|
builder: (context) => ContentDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
@@ -1,27 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:clipboard/clipboard.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/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/keyboard.dart';
|
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import 'hosting_controller.dart';
|
typedef BackendInteractiveEventHandler = InfoBarEntry? Function(AuthBackendType, AuthBackendResult);
|
||||||
|
|
||||||
class BackendController extends GetxController {
|
class BackendController extends GetxController {
|
||||||
static const String storageName = "v3_backend_storage";
|
static const String storageName = "v3_backend_storage";
|
||||||
@@ -30,20 +19,20 @@ class BackendController extends GetxController {
|
|||||||
late final GetStorage? _storage;
|
late final GetStorage? _storage;
|
||||||
late final TextEditingController host;
|
late final TextEditingController host;
|
||||||
late final TextEditingController port;
|
late final TextEditingController port;
|
||||||
late final Rx<ServerType> type;
|
late final Rx<AuthBackendType> type;
|
||||||
late final TextEditingController gameServerAddress;
|
late final TextEditingController gameServerAddress;
|
||||||
late final FocusNode gameServerAddressFocusNode;
|
late final FocusNode gameServerAddressFocusNode;
|
||||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool detached;
|
late final RxBool detached;
|
||||||
late final List<InfoBarEntry> _infoBars;
|
AuthBackendImplementation? implementation;
|
||||||
StreamSubscription? _worker;
|
StreamSubscription? _worker;
|
||||||
ServerImplementation? _implementation;
|
InfoBarEntry? _interactiveEntry;
|
||||||
|
|
||||||
BackendController() {
|
BackendController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
started = RxBool(false);
|
started = RxBool(false);
|
||||||
type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
|
type = Rx(AuthBackendType.values.elementAt(_storage?.read("type") ?? 0));
|
||||||
type.listen((value) {
|
type.listen((value) {
|
||||||
host.text = _readHost();
|
host.text = _readHost();
|
||||||
port.text = _readPort();
|
port.text = _readPort();
|
||||||
@@ -58,9 +47,9 @@ class BackendController extends GetxController {
|
|||||||
detached = RxBool(_storage?.read("detached") ?? false);
|
detached = RxBool(_storage?.read("detached") ?? false);
|
||||||
detached.listen((value) => _storage?.write("detached", value));
|
detached.listen((value) => _storage?.write("detached", value));
|
||||||
final address = _storage?.read("game_server_address");
|
final address = _storage?.read("game_server_address");
|
||||||
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
|
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? kDefaultBackendHost : address);
|
||||||
var lastValue = gameServerAddress.text;
|
var lastValue = gameServerAddress.text;
|
||||||
writeMatchmakingIp(lastValue);
|
writeAuthBackendMatchmakingIp(lastValue);
|
||||||
gameServerAddress.addListener(() {
|
gameServerAddress.addListener(() {
|
||||||
var newValue = gameServerAddress.text;
|
var newValue = gameServerAddress.text;
|
||||||
if(newValue.trim().toLowerCase() == lastValue.trim().toLowerCase()) {
|
if(newValue.trim().toLowerCase() == lastValue.trim().toLowerCase()) {
|
||||||
@@ -70,7 +59,7 @@ class BackendController extends GetxController {
|
|||||||
lastValue = newValue;
|
lastValue = newValue;
|
||||||
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
||||||
_storage?.write("game_server_address", newValue);
|
_storage?.write("game_server_address", newValue);
|
||||||
writeMatchmakingIp(newValue);
|
writeAuthBackendMatchmakingIp(newValue);
|
||||||
});
|
});
|
||||||
watchMatchmakingIp().listen((event) {
|
watchMatchmakingIp().listen((event) {
|
||||||
if(event != null && gameServerAddress.text != event) {
|
if(event != null && gameServerAddress.text != event) {
|
||||||
@@ -101,7 +90,6 @@ class BackendController extends GetxController {
|
|||||||
_storage?.write("console_key", newValue.usbHidUsage);
|
_storage?.write("console_key", newValue.usbHidUsage);
|
||||||
_writeConsoleKey(newValue);
|
_writeConsoleKey(newValue);
|
||||||
});
|
});
|
||||||
_infoBars = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
|
||||||
@@ -116,7 +104,7 @@ class BackendController extends GetxController {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.value != ServerType.remote) {
|
if (type.value != AuthBackendType.remote) {
|
||||||
return kDefaultBackendHost;
|
return kDefaultBackendHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,71 +113,56 @@ class BackendController extends GetxController {
|
|||||||
|
|
||||||
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||||
|
|
||||||
void joinLocalhost() {
|
|
||||||
gameServerAddress.text = kDefaultGameServerHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() async {
|
void reset() async {
|
||||||
type.value = ServerType.values.elementAt(0);
|
type.value = AuthBackendType.values.elementAt(0);
|
||||||
for (final type in ServerType.values) {
|
for (final type in AuthBackendType.values) {
|
||||||
_storage?.write("${type.name}_host", null);
|
_storage?.write("${type.name}_host", null);
|
||||||
_storage?.write("${type.name}_port", null);
|
_storage?.write("${type.name}_port", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
|
host.text = type.value != AuthBackendType.remote ? kDefaultBackendHost : "";
|
||||||
port.text = kDefaultBackendPort.toString();
|
port.text = kDefaultBackendPort.toString();
|
||||||
gameServerAddress.text = "127.0.0.1";
|
gameServerAddress.text = kDefaultBackendHost;
|
||||||
consoleKey.value = _kDefaultConsoleKey;
|
consoleKey.value = _kDefaultConsoleKey;
|
||||||
detached.value = false;
|
detached.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> toggle() {
|
Future<bool> toggle({
|
||||||
|
BackendInteractiveEventHandler? eventHandler,
|
||||||
|
BackendErrorHandler? errorHandler
|
||||||
|
}) {
|
||||||
if(started.value) {
|
if(started.value) {
|
||||||
return stop(interactive: true);
|
return stop(
|
||||||
|
eventHandler: eventHandler
|
||||||
|
);
|
||||||
}else {
|
}else {
|
||||||
return start(interactive: true);
|
return start(
|
||||||
|
eventHandler: eventHandler,
|
||||||
|
errorHandler: errorHandler
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> start({required bool interactive}) async {
|
Future<bool> start({
|
||||||
|
BackendInteractiveEventHandler? eventHandler,
|
||||||
|
BackendErrorHandler? errorHandler
|
||||||
|
}) async {
|
||||||
if(started.value) {
|
if(started.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancel();
|
_cancel();
|
||||||
final stream = startBackend(
|
final stream = startAuthBackend(
|
||||||
type: type.value,
|
type: type.value,
|
||||||
host: host.text,
|
host: host.text,
|
||||||
port: port.text,
|
port: port.text,
|
||||||
detached: detached.value,
|
detached: detached.value,
|
||||||
onError: (errorMessage) {
|
onError: errorHandler
|
||||||
if(started.value) {
|
|
||||||
stop(interactive: false);
|
|
||||||
Get.find<GameController>()
|
|
||||||
.instance
|
|
||||||
.value
|
|
||||||
?.kill();
|
|
||||||
Get.find<HostingController>()
|
|
||||||
.instance
|
|
||||||
.value
|
|
||||||
?.kill();
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.backendErrorMessage,
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
action: Button(
|
|
||||||
onPressed: () => launchUrl(launcherLogFile.uri),
|
|
||||||
child: Text(translations.openLog),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
final completer = Completer<bool>();
|
final completer = Completer<bool>();
|
||||||
InfoBarEntry? entry;
|
|
||||||
_worker = stream.listen((event) {
|
_worker = stream.listen((event) {
|
||||||
entry?.close();
|
_interactiveEntry?.close();
|
||||||
entry = _handeEvent(event, interactive);
|
_interactiveEntry = eventHandler?.call(type.value, event);
|
||||||
if(event.type.isError) {
|
if(event.type.isError) {
|
||||||
completer.complete(false);
|
completer.complete(false);
|
||||||
}else if(event.type.isSuccess) {
|
}else if(event.type.isSuccess) {
|
||||||
@@ -199,21 +172,22 @@ class BackendController extends GetxController {
|
|||||||
return await completer.future;
|
return await completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> stop({required bool interactive}) async {
|
Future<bool> stop({
|
||||||
|
BackendInteractiveEventHandler? eventHandler
|
||||||
|
}) async {
|
||||||
if(!started.value) {
|
if(!started.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cancel();
|
_cancel();
|
||||||
final stream = stopBackend(
|
final stream = stopAuthBackend(
|
||||||
type: type.value,
|
type: type.value,
|
||||||
implementation: _implementation
|
implementation: implementation
|
||||||
);
|
);
|
||||||
final completer = Completer<bool>();
|
final completer = Completer<bool>();
|
||||||
InfoBarEntry? entry;
|
|
||||||
_worker = stream.listen((event) {
|
_worker = stream.listen((event) {
|
||||||
entry?.close();
|
_interactiveEntry?.close();
|
||||||
entry = _handeEvent(event, interactive);
|
_interactiveEntry = eventHandler?.call(type.value, event);
|
||||||
if(event.type.isError) {
|
if(event.type.isError) {
|
||||||
completer.complete(false);
|
completer.complete(false);
|
||||||
}else if(event.type.isSuccess) {
|
}else if(event.type.isSuccess) {
|
||||||
@@ -225,334 +199,6 @@ class BackendController extends GetxController {
|
|||||||
|
|
||||||
void _cancel() {
|
void _cancel() {
|
||||||
_worker?.cancel(); // Do not await or it will hang
|
_worker?.cancel(); // Do not await or it will hang
|
||||||
_infoBars.forEach((infoBar) => infoBar.close());
|
_interactiveEntry?.close();
|
||||||
_infoBars.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
|
|
||||||
log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})");
|
|
||||||
started.value = event.type.isStart && !event.type.isError;
|
|
||||||
switch (event.type) {
|
|
||||||
case ServerResultType.starting:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.startingServer,
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startSuccess:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
|
||||||
severity: InfoBarSeverity.success
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.stopping:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.stoppingServer,
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.stopSuccess:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.stoppedServer,
|
|
||||||
severity: InfoBarSeverity.success
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.stopError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.stopServerError(event.error ?? translations.unknownError),
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startMissingHostError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.missingHostNameError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startMissingPortError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.missingPortError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startIllegalPortError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.illegalPortError,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startFreeingPort:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.freeingPort,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startFreePortSuccess:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.freedPort,
|
|
||||||
severity: InfoBarSeverity.success,
|
|
||||||
duration: infoBarShortDuration
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startFreePortError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.freePortError(event.error ?? translations.unknownError),
|
|
||||||
severity: InfoBarSeverity.error,
|
|
||||||
duration: infoBarLongDuration
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startPingingRemote:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.pingingServer(ServerType.remote.name),
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startPingingLocal:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.pingingServer(type.value.name),
|
|
||||||
severity: InfoBarSeverity.info,
|
|
||||||
loading: true,
|
|
||||||
duration: null
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startPingError:
|
|
||||||
if(interactive) {
|
|
||||||
return _showRebootInfoBar(
|
|
||||||
translations.pingError(type.value.name),
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ServerResultType.startedImplementation:
|
|
||||||
_implementation = event.implementation;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> joinServer(String uuid, FortniteServer server) async {
|
|
||||||
if(!kDebugMode && uuid == server.id) {
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.joinSelfServer,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final version = Get.find<GameController>()
|
|
||||||
.getVersionByGame(server.version.toString());
|
|
||||||
if(version == null) {
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.cannotJoinServerVersion(server.version.toString()),
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final hashedPassword = server.password;
|
|
||||||
final hasPassword = hashedPassword != null;
|
|
||||||
final embedded = type.value == ServerType.embedded;
|
|
||||||
final author = server.author;
|
|
||||||
final encryptedIp = server.ip;
|
|
||||||
if(!hasPassword) {
|
|
||||||
final valid = await _isServerValid(encryptedIp);
|
|
||||||
if(!valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onServerJoined(embedded, encryptedIp, author, version);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final confirmPassword = await _askForPassword();
|
|
||||||
if(confirmPassword == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!checkPassword(confirmPassword, hashedPassword)) {
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.wrongServerPassword,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
|
||||||
final valid = await _isServerValid(decryptedIp);
|
|
||||||
if(!valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onServerJoined(embedded, decryptedIp, author, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _isServerValid(String address) async {
|
|
||||||
final result = await pingGameServer(address);
|
|
||||||
if(result) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showRebootInfoBar(
|
|
||||||
translations.offlineServer,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> _askForPassword() async {
|
|
||||||
final confirmPasswordController = TextEditingController();
|
|
||||||
final showPassword = RxBool(false);
|
|
||||||
final showPasswordTrailing = RxBool(false);
|
|
||||||
return await showRebootDialog<String?>(
|
|
||||||
builder: (context) => FormDialog(
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
InfoLabel(
|
|
||||||
label: translations.serverPassword,
|
|
||||||
child: Obx(() => TextFormBox(
|
|
||||||
placeholder: translations.serverPasswordPlaceholder,
|
|
||||||
controller: confirmPasswordController,
|
|
||||||
autovalidateMode: AutovalidateMode.always,
|
|
||||||
obscureText: !showPassword.value,
|
|
||||||
enableSuggestions: false,
|
|
||||||
autofocus: true,
|
|
||||||
autocorrect: false,
|
|
||||||
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
|
||||||
suffix: !showPasswordTrailing.value ? null : Button(
|
|
||||||
onPressed: () => showPassword.value = !showPassword.value,
|
|
||||||
style: ButtonStyle(
|
|
||||||
shape: WidgetStateProperty.all(const CircleBorder()),
|
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
|
|
||||||
),
|
|
||||||
)
|
|
||||||
))
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8.0)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
buttons: [
|
|
||||||
DialogButton(
|
|
||||||
text: translations.serverPasswordCancel,
|
|
||||||
type: ButtonType.secondary
|
|
||||||
),
|
|
||||||
|
|
||||||
DialogButton(
|
|
||||||
text: translations.serverPasswordConfirm,
|
|
||||||
type: ButtonType.primary,
|
|
||||||
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onServerJoined(bool embedded, String decryptedIp, String author, FortniteVersion version) {
|
|
||||||
if(embedded) {
|
|
||||||
gameServerAddress.text = decryptedIp;
|
|
||||||
pageIndex.value = RebootPageType.play.index;
|
|
||||||
}else {
|
|
||||||
FlutterClipboard.controlC(decryptedIp);
|
|
||||||
}
|
|
||||||
Get.find<GameController>().selectedVersion.value = version;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
|
|
||||||
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.success
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoBarEntry _showRebootInfoBar(dynamic text, {
|
|
||||||
InfoBarSeverity severity = InfoBarSeverity.info,
|
|
||||||
bool loading = false,
|
|
||||||
Duration? duration = infoBarShortDuration,
|
|
||||||
void Function()? onDismissed,
|
|
||||||
Widget? action
|
|
||||||
}) {
|
|
||||||
final result = showRebootInfoBar(
|
|
||||||
text,
|
|
||||||
severity: severity,
|
|
||||||
loading: loading,
|
|
||||||
duration: duration,
|
|
||||||
onDismissed: onDismissed,
|
|
||||||
action: action
|
|
||||||
);
|
|
||||||
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
|
|
||||||
_infoBars.add(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> restart() async {
|
|
||||||
if(started.value) {
|
|
||||||
await stop(interactive: false);
|
|
||||||
await start(interactive: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,13 @@ import 'dart:io';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
|
||||||
|
|
||||||
class DllController extends GetxController {
|
class DllController extends GetxController {
|
||||||
static const String storageName = "v3_dll_storage";
|
static const String storageName = "v3_dll_storage";
|
||||||
@@ -24,25 +21,20 @@ class DllController extends GetxController {
|
|||||||
late final TextEditingController backendDll;
|
late final TextEditingController backendDll;
|
||||||
late final TextEditingController memoryLeakDll;
|
late final TextEditingController memoryLeakDll;
|
||||||
late final TextEditingController gameServerPort;
|
late final TextEditingController gameServerPort;
|
||||||
late final Rx<UpdateTimer> timer;
|
|
||||||
late final TextEditingController beforeS20Mirror;
|
late final TextEditingController beforeS20Mirror;
|
||||||
late final TextEditingController aboveS20Mirror;
|
late final TextEditingController aboveS20Mirror;
|
||||||
late final RxBool customGameServer;
|
late final RxBool customGameServer;
|
||||||
late final RxnInt timestamp;
|
late final RxnInt timestamp;
|
||||||
late final Rx<UpdateStatus> status;
|
late final Rx<UpdateStatus> status;
|
||||||
late final Map<InjectableDll, StreamSubscription?> _subscriptions;
|
|
||||||
|
|
||||||
DllController() {
|
DllController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
customGameServerDll = _createController("game_server", InjectableDll.gameServer);
|
customGameServerDll = _createController("game_server", GameDll.gameServer);
|
||||||
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
unrealEngineConsoleDll = _createController("unreal_engine_console", GameDll.console);
|
||||||
backendDll = _createController("backend", InjectableDll.auth);
|
backendDll = _createController("backend", GameDll.auth);
|
||||||
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
|
memoryLeakDll = _createController("memory_leak", GameDll.memoryLeak);
|
||||||
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
|
||||||
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
|
||||||
final timerIndex = _storage?.read("timer");
|
|
||||||
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
|
||||||
timer.listen((value) => _storage?.write("timer", value.index));
|
|
||||||
beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
|
beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
|
||||||
beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
|
beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
|
||||||
aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
|
aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
|
||||||
@@ -52,24 +44,22 @@ class DllController extends GetxController {
|
|||||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||||
timestamp = RxnInt(_storage?.read("ts"));
|
timestamp = RxnInt(_storage?.read("ts"));
|
||||||
timestamp.listen((value) => _storage?.write("ts", value));
|
timestamp.listen((value) => _storage?.write("ts", value));
|
||||||
_subscriptions = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingController _createController(String key, InjectableDll dll) {
|
TextEditingController _createController(String key, GameDll dll) {
|
||||||
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
|
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
|
||||||
controller.addListener(() => _storage?.write(key, controller.text));
|
controller.addListener(() => _storage?.write(key, controller.text));
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetGame() {
|
void resetGame() {
|
||||||
customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
|
customGameServerDll.text = getDefaultDllPath(GameDll.gameServer);
|
||||||
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
|
unrealEngineConsoleDll.text = getDefaultDllPath(GameDll.console);
|
||||||
backendDll.text = getDefaultDllPath(InjectableDll.auth);
|
backendDll.text = getDefaultDllPath(GameDll.auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetServer() {
|
void resetServer() {
|
||||||
gameServerPort.text = kDefaultGameServerPort;
|
gameServerPort.text = kDefaultGameServerPort;
|
||||||
timer.value = UpdateTimer.hour;
|
|
||||||
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
|
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
|
||||||
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
|
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
|
||||||
status.value = UpdateStatus.waiting;
|
status.value = UpdateStatus.waiting;
|
||||||
@@ -83,18 +73,15 @@ class DllController extends GetxController {
|
|||||||
try {
|
try {
|
||||||
if(customGameServer.value) {
|
if(customGameServer.value) {
|
||||||
status.value = UpdateStatus.success;
|
status.value = UpdateStatus.success;
|
||||||
_listenToFileEvents(InjectableDll.gameServer);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final needsUpdate = await hasRebootDllUpdate(
|
final needsUpdate = await hasRebootDllUpdate(
|
||||||
timestamp.value,
|
timestamp.value,
|
||||||
hours: timer.value.hours,
|
|
||||||
force: force
|
force: force
|
||||||
);
|
);
|
||||||
if(!needsUpdate) {
|
if(!needsUpdate) {
|
||||||
status.value = UpdateStatus.success;
|
status.value = UpdateStatus.success;
|
||||||
_listenToFileEvents(InjectableDll.gameServer);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +121,6 @@ class DllController extends GetxController {
|
|||||||
duration: infoBarShortDuration
|
duration: infoBarShortDuration
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_listenToFileEvents(InjectableDll.gameServer);
|
|
||||||
return true;
|
return true;
|
||||||
}catch(message) {
|
}catch(message) {
|
||||||
infoBarEntry?.close();
|
infoBarEntry?.close();
|
||||||
@@ -164,22 +150,22 @@ class DllController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(File, bool) getInjectableData(String version, InjectableDll dll) {
|
(File, bool) getInjectableData(String version, GameDll dll) {
|
||||||
final defaultPath = canonicalize(getDefaultDllPath(dll));
|
final defaultPath = canonicalize(getDefaultDllPath(dll));
|
||||||
switch(dll){
|
switch(dll){
|
||||||
case InjectableDll.gameServer:
|
case GameDll.gameServer:
|
||||||
if(customGameServer.value) {
|
if(customGameServer.value) {
|
||||||
return (File(customGameServerDll.text), true);
|
return (File(customGameServerDll.text), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
||||||
case InjectableDll.console:
|
case GameDll.console:
|
||||||
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
|
||||||
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
|
||||||
case InjectableDll.auth:
|
case GameDll.auth:
|
||||||
final backendFile = File(backendDll.text);
|
final backendFile = File(backendDll.text);
|
||||||
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
return (backendFile, canonicalize(backendFile.path) != defaultPath);
|
||||||
case InjectableDll.memoryLeak:
|
case GameDll.memoryLeak:
|
||||||
final memoryFile = File(memoryLeakDll.text);
|
final memoryFile = File(memoryLeakDll.text);
|
||||||
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
|
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
|
||||||
}
|
}
|
||||||
@@ -193,43 +179,42 @@ class DllController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingController getDllEditingController(InjectableDll dll) {
|
TextEditingController getDllEditingController(GameDll dll) {
|
||||||
switch(dll) {
|
switch(dll) {
|
||||||
case InjectableDll.console:
|
case GameDll.console:
|
||||||
return unrealEngineConsoleDll;
|
return unrealEngineConsoleDll;
|
||||||
case InjectableDll.auth:
|
case GameDll.auth:
|
||||||
return backendDll;
|
return backendDll;
|
||||||
case InjectableDll.gameServer:
|
case GameDll.gameServer:
|
||||||
return customGameServerDll;
|
return customGameServerDll;
|
||||||
case InjectableDll.memoryLeak:
|
case GameDll.memoryLeak:
|
||||||
return memoryLeakDll;
|
return memoryLeakDll;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getDefaultDllPath(InjectableDll dll) {
|
String getDefaultDllPath(GameDll dll) {
|
||||||
switch(dll) {
|
switch(dll) {
|
||||||
case InjectableDll.console:
|
case GameDll.console:
|
||||||
return "${dllsDirectory.path}\\console.dll";
|
return "${dllsDirectory.path}\\console.dll";
|
||||||
case InjectableDll.auth:
|
case GameDll.auth:
|
||||||
return "${dllsDirectory.path}\\cobalt.dll";
|
return "${dllsDirectory.path}\\cobalt.dll";
|
||||||
case InjectableDll.gameServer:
|
case GameDll.gameServer:
|
||||||
return "${dllsDirectory.path}\\reboot.dll";
|
return "${dllsDirectory.path}\\reboot.dll";
|
||||||
case InjectableDll.memoryLeak:
|
case GameDll.memoryLeak:
|
||||||
return "${dllsDirectory.path}\\memory.dll";
|
return "${dllsDirectory.path}\\memory.dll";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
|
Future<bool> download(GameDll dll, String filePath, {bool silent = false, bool force = false}) async {
|
||||||
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
|
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
|
||||||
InfoBarEntry? entry;
|
InfoBarEntry? entry;
|
||||||
try {
|
try {
|
||||||
if (dll == InjectableDll.gameServer) {
|
if (dll == GameDll.gameServer) {
|
||||||
return await updateGameServerDll(silent: silent);
|
return await updateGameServerDll(silent: silent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!force && File(filePath).existsSync()) {
|
if(!force && File(filePath).existsSync()) {
|
||||||
log("[DLL] $dll already exists");
|
log("[DLL] $dll already exists");
|
||||||
_listenToFileEvents(dll);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +252,6 @@ class DllController extends GetxController {
|
|||||||
}else {
|
}else {
|
||||||
log("[DLL] Not showing success dialog for $dll");
|
log("[DLL] Not showing success dialog for $dll");
|
||||||
}
|
}
|
||||||
_listenToFileEvents(dll);
|
|
||||||
return true;
|
return true;
|
||||||
}catch(message) {
|
}catch(message) {
|
||||||
log("[DLL] An error occurred while downloading $dll: $message");
|
log("[DLL] An error occurred while downloading $dll: $message");
|
||||||
@@ -294,7 +278,7 @@ class DllController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> downloadAndGuardDependencies() async {
|
Future<void> downloadAndGuardDependencies() async {
|
||||||
for(final injectable in InjectableDll.values) {
|
for(final injectable in GameDll.values) {
|
||||||
final controller = getDllEditingController(injectable);
|
final controller = getDllEditingController(injectable);
|
||||||
final defaultPath = getDefaultDllPath(injectable);
|
final defaultPath = getDefaultDllPath(injectable);
|
||||||
|
|
||||||
@@ -303,76 +287,11 @@ class DllController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenToFileEvents(InjectableDll injectable) {
|
|
||||||
final controller = getDllEditingController(injectable);
|
|
||||||
final defaultPath = getDefaultDllPath(injectable);
|
|
||||||
|
|
||||||
void onFileEvent(FileSystemEvent event, String filePath) {
|
|
||||||
if (!path.equals(event.path, filePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(path.equals(filePath, defaultPath)) {
|
|
||||||
Get.find<GameController>()
|
|
||||||
.instance
|
|
||||||
.value
|
|
||||||
?.kill();
|
|
||||||
Get.find<HostingController>()
|
|
||||||
.instance
|
|
||||||
.value
|
|
||||||
?.kill();
|
|
||||||
showRebootInfoBar(
|
|
||||||
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateInput(injectable);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription subscribe(String filePath) => File(filePath)
|
|
||||||
.parent
|
|
||||||
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
|
|
||||||
.listen((event) => onFileEvent(event, filePath));
|
|
||||||
|
|
||||||
controller.addListener(() {
|
|
||||||
_subscriptions[injectable]?.cancel();
|
|
||||||
_subscriptions[injectable] = subscribe(controller.text);
|
|
||||||
});
|
|
||||||
_subscriptions[injectable] = subscribe(controller.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateInput(InjectableDll injectable) {
|
|
||||||
switch(injectable) {
|
|
||||||
case InjectableDll.console:
|
|
||||||
settingsConsoleDllInputKey.currentState?.validate();
|
|
||||||
break;
|
|
||||||
case InjectableDll.auth:
|
|
||||||
settingsAuthDllInputKey.currentState?.validate();
|
|
||||||
break;
|
|
||||||
case InjectableDll.gameServer:
|
|
||||||
settingsGameServerDllInputKey.currentState?.validate();
|
|
||||||
break;
|
|
||||||
case InjectableDll.memoryLeak:
|
|
||||||
settingsMemoryDllInputKey.currentState?.validate();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _UpdateTimerExtension on UpdateTimer {
|
enum UpdateStatus {
|
||||||
int get hours {
|
waiting,
|
||||||
switch(this) {
|
started,
|
||||||
case UpdateTimer.never:
|
success,
|
||||||
return -1;
|
error
|
||||||
case UpdateTimer.hour:
|
|
||||||
return 1;
|
|
||||||
case UpdateTimer.day:
|
|
||||||
return 24;
|
|
||||||
case UpdateTimer.week:
|
|
||||||
return 24 * 7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -16,8 +16,8 @@ class GameController extends GetxController {
|
|||||||
late final TextEditingController username;
|
late final TextEditingController username;
|
||||||
late final TextEditingController password;
|
late final TextEditingController password;
|
||||||
late final TextEditingController customLaunchArgs;
|
late final TextEditingController customLaunchArgs;
|
||||||
late final Rx<List<FortniteVersion>> versions;
|
late final Rx<List<GameVersion>> versions;
|
||||||
late final Rxn<FortniteVersion> selectedVersion;
|
late final Rxn<GameVersion> selectedVersion;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final Rxn<GameInstance> instance;
|
late final Rxn<GameInstance> instance;
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class GameController extends GetxController {
|
|||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
||||||
final decodedVersions = decodedVersionsJson
|
final decodedVersions = decodedVersionsJson
|
||||||
.map((entry) => FortniteVersion.fromJson(entry))
|
.map((entry) => GameVersion.fromJson(entry))
|
||||||
.toList();
|
.toList();
|
||||||
versions = Rx(decodedVersions);
|
versions = Rx(decodedVersions);
|
||||||
versions.listen((data) => _saveVersions());
|
versions.listen((data) => _saveVersions());
|
||||||
@@ -52,12 +52,12 @@ class GameController extends GetxController {
|
|||||||
instance.value = null;
|
instance.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteVersion? getVersionByName(String name) {
|
GameVersion? getVersionByName(String name) {
|
||||||
name = name.trim();
|
name = name.trim();
|
||||||
return versions.value.firstWhereOrNull((element) => element.name == name);
|
return versions.value.firstWhereOrNull((element) => element.name == name);
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteVersion? getVersionByGame(String gameVersion) {
|
GameVersion? getVersionByGame(String gameVersion) {
|
||||||
gameVersion = gameVersion.trim();
|
gameVersion = gameVersion.trim();
|
||||||
final parsedGameVersion = Version.parse(gameVersion);
|
final parsedGameVersion = Version.parse(gameVersion);
|
||||||
return versions.value.firstWhereOrNull((element) {
|
return versions.value.firstWhereOrNull((element) {
|
||||||
@@ -72,12 +72,12 @@ class GameController extends GetxController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void addVersion(FortniteVersion version) {
|
void addVersion(GameVersion version) {
|
||||||
versions.update((val) => val?.add(version));
|
versions.update((val) => val?.add(version));
|
||||||
selectedVersion.value = version;
|
selectedVersion.value = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeVersion(FortniteVersion version) {
|
void removeVersion(GameVersion version) {
|
||||||
final index = versions.value.indexOf(version);
|
final index = versions.value.indexOf(version);
|
||||||
versions.update((val) => val?.removeAt(index));
|
versions.update((val) => val?.removeAt(index));
|
||||||
if(hasNoVersions) {
|
if(hasNoVersions) {
|
||||||
@@ -96,5 +96,5 @@ class GameController extends GetxController {
|
|||||||
|
|
||||||
bool get hasNoVersions => versions.value.isEmpty;
|
bool get hasNoVersions => versions.value.isEmpty;
|
||||||
|
|
||||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) => versions.update((val) => function(version));
|
void updateVersion(GameVersion version, Function(GameVersion) function) => versions.update((val) => function(version));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dart_ipify/dart_ipify.dart';
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/util/cryptography.dart';
|
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
||||||
import 'package:sync/semaphore.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../util/cryptography.dart';
|
||||||
|
|
||||||
class HostingController extends GetxController {
|
class HostingController extends GetxController {
|
||||||
static const String storageName = "v3_hosting_storage";
|
static const String storageName = "v3_hosting_storage";
|
||||||
|
|
||||||
@@ -25,15 +22,12 @@ class HostingController extends GetxController {
|
|||||||
late final TextEditingController password;
|
late final TextEditingController password;
|
||||||
late final FocusNode passwordFocusNode;
|
late final FocusNode passwordFocusNode;
|
||||||
late final RxBool showPassword;
|
late final RxBool showPassword;
|
||||||
late final RxBool discoverable;
|
|
||||||
late final RxBool headless;
|
late final RxBool headless;
|
||||||
late final RxBool autoRestart;
|
late final RxBool autoRestart;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool published;
|
late final RxBool published;
|
||||||
late final Rxn<GameInstance> instance;
|
late final Rxn<GameInstance> instance;
|
||||||
late final Rxn<Set<FortniteServer>> servers;
|
|
||||||
late final TextEditingController customLaunchArgs;
|
late final TextEditingController customLaunchArgs;
|
||||||
late final Semaphore _semaphore;
|
|
||||||
|
|
||||||
HostingController() {
|
HostingController() {
|
||||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||||
@@ -52,8 +46,6 @@ class HostingController extends GetxController {
|
|||||||
nameFocusNode = FocusNode();
|
nameFocusNode = FocusNode();
|
||||||
descriptionFocusNode = FocusNode();
|
descriptionFocusNode = FocusNode();
|
||||||
passwordFocusNode = FocusNode();
|
passwordFocusNode = FocusNode();
|
||||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
|
||||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
|
||||||
headless = RxBool(_storage?.read("headless") ?? true);
|
headless = RxBool(_storage?.read("headless") ?? true);
|
||||||
headless.listen((value) => _storage?.write("headless", value));
|
headless.listen((value) => _storage?.write("headless", value));
|
||||||
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
|
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
|
||||||
@@ -62,119 +54,38 @@ class HostingController extends GetxController {
|
|||||||
published = RxBool(false);
|
published = RxBool(false);
|
||||||
showPassword = RxBool(false);
|
showPassword = RxBool(false);
|
||||||
instance = Rxn();
|
instance = Rxn();
|
||||||
servers = Rxn();
|
|
||||||
_listenServers();
|
|
||||||
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
|
||||||
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
|
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
|
||||||
_semaphore = Semaphore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenServers([int attempt = 0]) {
|
Future<ServerBrowserEntry> createServerBrowserEntry() async {
|
||||||
log("[SUPABASE] Listening...");
|
final passwordText = password.text;
|
||||||
final supabase = Supabase.instance.client;
|
final hasPassword = passwordText.isNotEmpty;
|
||||||
supabase.from("hosting_v2")
|
var ip = await Ipify.ipv4();
|
||||||
.stream(primaryKey: ['id'])
|
if(hasPassword) {
|
||||||
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
|
ip = aes256Encrypt(ip, passwordText);
|
||||||
.listen(
|
}
|
||||||
_onNewServer,
|
return ServerBrowserEntry(
|
||||||
onError: (error) async {
|
id: uuid,
|
||||||
log("[SUPABASE] Error: ${error}");
|
name: name.text,
|
||||||
await Future.delayed(Duration(seconds: attempt * 5));
|
description: description.text,
|
||||||
_listenServers(attempt + 1);
|
author: accountUsername.text,
|
||||||
},
|
ip: ip,
|
||||||
cancelOnError: true
|
version: instance.value!.version.toString(),
|
||||||
|
password: hasPassword ? hashPassword(passwordText) : "",
|
||||||
|
timestamp: DateTime.now()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNewServer(Set<FortniteServer> event) {
|
|
||||||
log("[SUPABASE] New event: ${event}");
|
|
||||||
servers.value = event;
|
|
||||||
published.value = event.any((element) => element.id == uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> publishServer(String author, String version) async {
|
|
||||||
try {
|
|
||||||
_semaphore.acquire();
|
|
||||||
log("[SERVER] Publishing server...");
|
|
||||||
if(published.value) {
|
|
||||||
log("[SERVER] Already published");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final passwordText = password.text;
|
|
||||||
final hasPassword = passwordText.isNotEmpty;
|
|
||||||
var ip = await Ipify.ipv4();
|
|
||||||
if(hasPassword) {
|
|
||||||
ip = aes256Encrypt(ip, passwordText);
|
|
||||||
}
|
|
||||||
|
|
||||||
final supabase = Supabase.instance.client;
|
|
||||||
final hosts = supabase.from("hosting_v2");
|
|
||||||
final payload = FortniteServer(
|
|
||||||
id: uuid,
|
|
||||||
name: name.text,
|
|
||||||
description: description.text,
|
|
||||||
author: author,
|
|
||||||
ip: ip,
|
|
||||||
version: version,
|
|
||||||
password: hasPassword ? hashPassword(passwordText) : null,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
discoverable: discoverable.value
|
|
||||||
).toJson();
|
|
||||||
log("[SERVER] Payload: ${jsonEncode(payload)}");
|
|
||||||
if(published()) {
|
|
||||||
await hosts.update(payload)
|
|
||||||
.eq("id", uuid);
|
|
||||||
}else {
|
|
||||||
await hosts.insert(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
published.value = true;
|
|
||||||
log("[SERVER] Published");
|
|
||||||
}catch(error) {
|
|
||||||
log("[SERVER] Cannot publish server: $error");
|
|
||||||
published.value = false;
|
|
||||||
}finally {
|
|
||||||
_semaphore.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> discardServer() async {
|
|
||||||
try {
|
|
||||||
_semaphore.acquire();
|
|
||||||
log("[SERVER] Discarding server...");
|
|
||||||
final supabase = Supabase.instance.client;
|
|
||||||
await supabase.from("hosting_v2")
|
|
||||||
.delete()
|
|
||||||
.match({'id': uuid});
|
|
||||||
servers.value?.removeWhere((element) => element.id == uuid);
|
|
||||||
log("[SERVER] Discarded server");
|
|
||||||
}catch(error) {
|
|
||||||
log("[SERVER] Cannot discard server: $error");
|
|
||||||
}finally {
|
|
||||||
published.value = false;
|
|
||||||
_semaphore.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
accountUsername.text = kDefaultHostName;
|
accountUsername.text = kDefaultHostName;
|
||||||
accountPassword.text = "";
|
accountPassword.text = "";
|
||||||
name.text = "";
|
name.text = "";
|
||||||
description.text = "";
|
description.text = "";
|
||||||
showPassword.value = false;
|
showPassword.value = false;
|
||||||
discoverable.value = false;
|
|
||||||
instance.value = null;
|
instance.value = null;
|
||||||
headless.value = true;
|
headless.value = true;
|
||||||
autoRestart.value = true;
|
autoRestart.value = true;
|
||||||
customLaunchArgs.text = "";
|
customLaunchArgs.text = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteServer? findServerById(String uuid) {
|
|
||||||
try {
|
|
||||||
return servers.value?.firstWhere((element) => element.id == uuid);
|
|
||||||
} on StateError catch(_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
74
gui/lib/src/controller/server_browser_controller.dart
Normal file
74
gui/lib/src/controller/server_browser_controller.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:sync/semaphore.dart';
|
||||||
|
|
||||||
|
final class ServerBrowserController extends GetxController {
|
||||||
|
static const String _url = "ws://192.99.216.42:8080";
|
||||||
|
|
||||||
|
final Rxn<List<ServerBrowserEntry>> servers;
|
||||||
|
final Map<String, ServerBrowserEntry> _entries;
|
||||||
|
final ServerBrowserClient _client;
|
||||||
|
final Semaphore _semaphore;
|
||||||
|
|
||||||
|
ServerBrowserController() :
|
||||||
|
servers = Rxn(),
|
||||||
|
_entries = {},
|
||||||
|
_client = ServerBrowserClient(serverUrl: _url)..connect(), // The client should always be connected
|
||||||
|
_semaphore = Semaphore() {
|
||||||
|
addEventsListener((data) {
|
||||||
|
switch(data) {
|
||||||
|
case ServerBrowserStateEvent():
|
||||||
|
break;
|
||||||
|
case ServerBrowserAddEvent():
|
||||||
|
for(final entry in data.entries) {
|
||||||
|
_entries[entry.id] = entry;
|
||||||
|
}
|
||||||
|
_updateServers();
|
||||||
|
break;
|
||||||
|
case ServerBrowserRemoveEvent():
|
||||||
|
for(final entry in data.entries) {
|
||||||
|
_entries.remove(entry);
|
||||||
|
}
|
||||||
|
_updateServers();
|
||||||
|
break;
|
||||||
|
case ServerBrowserErrorEvent():
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateServers() {
|
||||||
|
servers.value = servers.value == null
|
||||||
|
? _entries.values.toList(growable: false)
|
||||||
|
: [...?servers.value, ..._entries.values];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addServer(ServerBrowserEntry entry) async {
|
||||||
|
try {
|
||||||
|
_semaphore.acquire();
|
||||||
|
await _client.addEntry(entry);
|
||||||
|
} finally {
|
||||||
|
_semaphore.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeServer(String uuid) async {
|
||||||
|
try {
|
||||||
|
_semaphore.acquire();
|
||||||
|
await _client.removeEntry(uuid);
|
||||||
|
} finally {
|
||||||
|
_semaphore.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<ServerBrowserEvent> addEventsListener(void Function(ServerBrowserEvent) onData) {
|
||||||
|
return _client.addListener(onData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerBrowserEntry? getServerById(String uuid) {
|
||||||
|
return _entries[uuid];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import 'package:get_storage/get_storage.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
@@ -49,50 +49,4 @@ class SettingsController extends GetxController {
|
|||||||
_storage?.write("offset_x", offsetX);
|
_storage?.write("offset_x", offsetX);
|
||||||
_storage?.write("offset_y", offsetY);
|
_storage?.write("offset_y", offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notifyLauncherUpdate() async {
|
|
||||||
if (appVersion == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pubspec = await _getPubspecYaml();
|
|
||||||
if (pubspec == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final latestVersion = Version.parse(pubspec["version"]);
|
|
||||||
if (latestVersion <= appVersion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
late InfoBarEntry infoBar;
|
|
||||||
infoBar = showRebootInfoBar(
|
|
||||||
translations.updateAvailable(latestVersion.toString()),
|
|
||||||
duration: null,
|
|
||||||
severity: InfoBarSeverity.warning,
|
|
||||||
action: Button(
|
|
||||||
child: Text(translations.updateAvailableAction),
|
|
||||||
onPressed: () {
|
|
||||||
infoBar.close();
|
|
||||||
launchUrl(Uri.parse(
|
|
||||||
"https://github.com/Auties00/reboot_launcher/releases"));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<dynamic> _getPubspecYaml() async {
|
|
||||||
try {
|
|
||||||
final pubspecResponse = await http.get(Uri.parse(
|
|
||||||
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
|
||||||
if (pubspecResponse.statusCode != 200) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadYaml(pubspecResponse.body);
|
|
||||||
} catch (error) {
|
|
||||||
log("[UPDATER] Cannot check for updates: $error");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
116
gui/lib/src/message/backend.dart
Normal file
116
gui/lib/src/message/backend.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
InfoBarEntry? onBackendResult(AuthBackendType type, AuthBackendResult event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case AuthBackendResultType.starting:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.startingServer,
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startSuccess:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
type == AuthBackendType.local
|
||||||
|
? translations.checkedServer
|
||||||
|
: translations.startedServer,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
type == AuthBackendType.local
|
||||||
|
? translations.localServerError(event.error ?? translations.unknownError)
|
||||||
|
: translations.startServerError(event.error ?? translations.unknownError),
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.stopping:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.stoppingServer,
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.stopSuccess:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.stoppedServer,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.stopError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.stopServerError(event.error ?? translations.unknownError),
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startMissingHostError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.missingHostNameError,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startMissingPortError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.missingPortError,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startIllegalPortError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.illegalPortError,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startFreeingPort:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.freeingPort,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startFreePortSuccess:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.freedPort,
|
||||||
|
severity: InfoBarSeverity.success,
|
||||||
|
duration: infoBarShortDuration
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startFreePortError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.freePortError(event.error ?? translations.unknownError),
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startPingingRemote:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.pingingServer(AuthBackendType.remote.name),
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startPingingLocal:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.pingingServer(type.name),
|
||||||
|
severity: InfoBarSeverity.info,
|
||||||
|
loading: true,
|
||||||
|
duration: null
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startPingError:
|
||||||
|
return showRebootInfoBar(
|
||||||
|
translations.pingError(type.name),
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
case AuthBackendResultType.startedImplementation:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBackendError(Object error) {
|
||||||
|
showRebootInfoBar(
|
||||||
|
translations.backendErrorMessage,
|
||||||
|
severity: InfoBarSeverity.error,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
action: Button(
|
||||||
|
onPressed: () => launchUrl(launcherLogFile.uri),
|
||||||
|
child: Text(translations.openLog),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
|
||||||
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
|
||||||
Future<void> showDllDeletedDialog() => showRebootDialog(
|
Future<void> showDllDeletedDialog() => showRebootDialog(
|
||||||
builder: (context) => InfoDialog(
|
builder: (context) => InfoDialog(
|
||||||
@@ -6,11 +6,11 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/util/extensions.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/util/types.dart';
|
import 'package:reboot_launcher/src/button/file_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||||
|
|
||||||
@@ -30,19 +30,13 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
|
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
|
||||||
|
|
||||||
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
|
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
|
||||||
final Rxn<FortniteBuild> _build = Rxn();
|
final Rxn<GameBuild> _build = Rxn();
|
||||||
final RxnInt _timeLeft = RxnInt();
|
final RxnInt _timeLeft = RxnInt();
|
||||||
final Rxn<double> _progress = Rxn();
|
final Rxn<double> _progress = Rxn();
|
||||||
final RxInt _speed = RxInt(0);
|
final RxInt _speed = RxInt(0);
|
||||||
|
|
||||||
SendPort? _downloadPort;
|
SendPort? _downloadPort;
|
||||||
Object? _error;
|
Object? _error;
|
||||||
StackTrace? _stackTrace;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -133,33 +127,33 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
_status.value = _DownloadStatus.downloading;
|
_status.value = _DownloadStatus.downloading;
|
||||||
final communicationPort = ReceivePort();
|
final communicationPort = ReceivePort();
|
||||||
communicationPort.listen((message) {
|
communicationPort.listen((message) {
|
||||||
if(message is FortniteBuildDownloadProgress) {
|
if(message is GameBuildDownloadProgress) {
|
||||||
_onProgress(build, message);
|
_onProgress(build, message);
|
||||||
}else if(message is SendPort) {
|
}else if(message is SendPort) {
|
||||||
_downloadPort = message;
|
_downloadPort = message;
|
||||||
}else {
|
}else {
|
||||||
_onDownloadError(message, null);
|
_onDownloadError(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final options = FortniteBuildDownloadOptions(
|
final options = GameBuildDownloadOptions(
|
||||||
build,
|
build,
|
||||||
Directory(_pathController.text),
|
Directory(_pathController.text),
|
||||||
communicationPort.sendPort
|
communicationPort.sendPort
|
||||||
);
|
);
|
||||||
final errorPort = ReceivePort();
|
final errorPort = ReceivePort();
|
||||||
errorPort.listen((message) => _onDownloadError(message, null));
|
errorPort.listen((message) => _onDownloadError(message));
|
||||||
await Isolate.spawn(
|
await Isolate.spawn(
|
||||||
downloadArchiveBuild,
|
downloadArchiveBuild,
|
||||||
options,
|
options,
|
||||||
onError: errorPort.sendPort,
|
onError: errorPort.sendPort,
|
||||||
errorsAreFatal: true
|
errorsAreFatal: true
|
||||||
);
|
);
|
||||||
} catch (exception, stackTrace) {
|
} catch (exception) {
|
||||||
_onDownloadError(exception, stackTrace);
|
_onDownloadError(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDownloadComplete(FortniteBuild build) async {
|
Future<void> _onDownloadComplete(GameBuild build) async {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,7 +168,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
_status.value = _DownloadStatus.done;
|
_status.value = _DownloadStatus.done;
|
||||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||||
|
|
||||||
final version = FortniteVersion(
|
final version = GameVersion(
|
||||||
name: name,
|
name: name,
|
||||||
gameVersion: build.gameVersion,
|
gameVersion: build.gameVersion,
|
||||||
location: location
|
location: location
|
||||||
@@ -182,7 +176,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
_gameController.addVersion(version);
|
_gameController.addVersion(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
void _onDownloadError(Object? error) {
|
||||||
_cancelDownload();
|
_cancelDownload();
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -191,10 +185,9 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
_status.value = _DownloadStatus.error;
|
_status.value = _DownloadStatus.error;
|
||||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||||
_error = error;
|
_error = error;
|
||||||
_stackTrace = stackTrace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
|
void _onProgress(GameBuild build, GameBuildDownloadProgress message) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -282,6 +275,8 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_buildSelector,
|
||||||
|
|
||||||
InfoLabel(
|
InfoLabel(
|
||||||
label: translations.versionName,
|
label: translations.versionName,
|
||||||
child: TextFormBox(
|
child: TextFormBox(
|
||||||
@@ -296,8 +291,6 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
height: 16.0
|
height: 16.0
|
||||||
),
|
),
|
||||||
|
|
||||||
_buildSelector,
|
|
||||||
|
|
||||||
FileSelector(
|
FileSelector(
|
||||||
label: translations.gameFolderTitle,
|
label: translations.gameFolderTitle,
|
||||||
placeholder: translations.buildInstallationDirectoryPlaceholder,
|
placeholder: translations.buildInstallationDirectoryPlaceholder,
|
||||||
@@ -336,13 +329,13 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
|
|
||||||
Widget get _buildSelector => InfoLabel(
|
Widget get _buildSelector => InfoLabel(
|
||||||
label: translations.build,
|
label: translations.build,
|
||||||
child: FormField<FortniteBuild?>(
|
child: FormField<GameBuild?>(
|
||||||
key: _formFieldKey,
|
key: _formFieldKey,
|
||||||
validator: (data) => _checkBuild(data),
|
validator: (data) => _checkBuild(data),
|
||||||
builder: (formContext) => Column(
|
builder: (formContext) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ComboBox<FortniteBuild>(
|
ComboBox<GameBuild>(
|
||||||
placeholder: Text(translations.selectBuild),
|
placeholder: Text(translations.selectBuild),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
items: downloadableBuilds.where((build) => build.available)
|
items: downloadableBuilds.where((build) => build.available)
|
||||||
@@ -377,7 +370,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
String? _checkBuild(FortniteBuild? data) {
|
String? _checkBuild(GameBuild? data) {
|
||||||
if(data == null) {
|
if(data == null) {
|
||||||
return translations.selectBuild;
|
return translations.selectBuild;
|
||||||
}
|
}
|
||||||
@@ -385,7 +378,7 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
|
ComboBoxItem<GameBuild> _buildBuildItem(GameBuild element) => ComboBoxItem<GameBuild>(
|
||||||
value: element,
|
value: element,
|
||||||
child: Text(element.gameVersion)
|
child: Text(element.gameVersion)
|
||||||
);
|
);
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
|
|
||||||
String? lastError;
|
String? lastError;
|
||||||
|
|
||||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||||
|
log("[ERROR_HANDLER] Called");
|
||||||
log("[ERROR] $exception");
|
log("[ERROR] $exception");
|
||||||
log("[STACKTRACE] $stackTrace");
|
log("[STACKTRACE] $stackTrace");
|
||||||
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
|
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
|
||||||
|
log("[ERROR_HANDLER] Not mounted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(lastError == exception.toString()){
|
if(lastError == exception.toString()){
|
||||||
|
log("[ERROR_HANDLER] Duplicate");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,16 +2,16 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
import 'package:reboot_launcher/src/button/file_selector.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
class ImportVersionDialog extends StatefulWidget {
|
class ImportVersionDialog extends StatefulWidget {
|
||||||
final FortniteVersion? version;
|
final GameVersion? version;
|
||||||
final bool closable;
|
final bool closable;
|
||||||
const ImportVersionDialog({Key? key, required this.version, required this.closable}) : super(key: key);
|
const ImportVersionDialog({Key? key, required this.version, required this.closable}) : super(key: key);
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ class _ImportVersionDialogState extends State<ImportVersionDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(widget.version == null) {
|
if(widget.version == null) {
|
||||||
final version = FortniteVersion(
|
final version = GameVersion(
|
||||||
name: name,
|
name: name,
|
||||||
gameVersion: gameVersion,
|
gameVersion: gameVersion,
|
||||||
location: shippingExes.first.parent
|
location: shippingExes.first.parent
|
||||||
@@ -5,16 +5,16 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/profile.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/page/home_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/page/host_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/page/play_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
import 'package:reboot_launcher/src/message/profile.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/backend_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/pager.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/host_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/play_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/button/version_selector.dart';
|
||||||
|
|
||||||
void startOnboarding() {
|
void startOnboarding() {
|
||||||
final gameController = Get.find<GameController>();
|
final gameController = Get.find<GameController>();
|
||||||
@@ -36,7 +36,7 @@ void startOnboarding() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _promptPlayPage() {
|
void _promptPlayPage() {
|
||||||
pageIndex.value = RebootPageType.play.index;
|
pageIndex.value = PageType.play.index;
|
||||||
pageOverlayTargetKey.currentState!.showOverlay(
|
pageOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptPlayPageText,
|
text: translations.promptPlayPageText,
|
||||||
actionBuilder: (context, onClose) => _buildActionButton(
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
@@ -72,7 +72,7 @@ void _promptPlayVersion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _promptServerBrowserPage() {
|
void _promptServerBrowserPage() {
|
||||||
pageIndex.value = RebootPageType.browser.index;
|
pageIndex.value = PageType.browser.index;
|
||||||
pageOverlayTargetKey.currentState!.showOverlay(
|
pageOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptServerBrowserPageText,
|
text: translations.promptServerBrowserPageText,
|
||||||
actionBuilder: (context, onClose) => _buildActionButton(
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
@@ -87,7 +87,7 @@ void _promptServerBrowserPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _promptHostAccount() {
|
void _promptHostAccount() {
|
||||||
pageIndex.value = RebootPageType.host.index;
|
pageIndex.value = PageType.host.index;
|
||||||
profileOverlayKey.currentState!.showOverlay(
|
profileOverlayKey.currentState!.showOverlay(
|
||||||
text: translations.hostAccountText,
|
text: translations.hostAccountText,
|
||||||
offset: Offset(27.5, 17.5),
|
offset: Offset(27.5, 17.5),
|
||||||
@@ -118,7 +118,6 @@ void _promptHostPage() {
|
|||||||
|
|
||||||
|
|
||||||
void _promptHostInfo() {
|
void _promptHostInfo() {
|
||||||
final hostingController = Get.find<HostingController>();
|
|
||||||
hostInfoOverlayTargetKey.currentState!.showOverlay(
|
hostInfoOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptHostInfoText,
|
text: translations.promptHostInfoText,
|
||||||
offset: Offset(-10, 2.5),
|
offset: Offset(-10, 2.5),
|
||||||
@@ -130,7 +129,6 @@ void _promptHostInfo() {
|
|||||||
themed: false,
|
themed: false,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onClose();
|
onClose();
|
||||||
hostingController.discoverable.value = false;
|
|
||||||
_promptHostVersion();
|
_promptHostVersion();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -140,7 +138,6 @@ void _promptHostInfo() {
|
|||||||
label: translations.promptHostInfoActionLabelConfigure,
|
label: translations.promptHostInfoActionLabelConfigure,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onClose();
|
onClose();
|
||||||
hostingController.discoverable.value = true;
|
|
||||||
hostInfoTileKey.currentState!.openNestedPage();
|
hostInfoTileKey.currentState!.openNestedPage();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation());
|
||||||
}
|
}
|
||||||
@@ -202,7 +199,7 @@ void _promptHostInformationPassword() {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
onClose();
|
onClose();
|
||||||
Navigator.of(hostInfoTileKey.currentContext!).pop();
|
Navigator.of(hostInfoTileKey.currentContext!).pop();
|
||||||
pageStack.removeLast();
|
currentPageStack.removeLast();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion());
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -234,7 +231,7 @@ void _promptHostShare() {
|
|||||||
label: translations.promptHostShareActionLabel,
|
label: translations.promptHostShareActionLabel,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onClose();
|
onClose();
|
||||||
backendController.type.value = ServerType.embedded;
|
backendController.type.value = AuthBackendType.embedded;
|
||||||
_promptBackendPage();
|
_promptBackendPage();
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -243,7 +240,7 @@ void _promptHostShare() {
|
|||||||
|
|
||||||
|
|
||||||
void _promptBackendPage() {
|
void _promptBackendPage() {
|
||||||
pageIndex.value = RebootPageType.backend.index;
|
pageIndex.value = PageType.backend.index;
|
||||||
pageOverlayTargetKey.currentState!.showOverlay(
|
pageOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptBackendPageText,
|
text: translations.promptBackendPageText,
|
||||||
actionBuilder: (context, onClose) => _buildActionButton(
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
@@ -322,7 +319,7 @@ void _promptBackendDetached() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _promptInfoTab() {
|
void _promptInfoTab() {
|
||||||
pageIndex.value = RebootPageType.info.index;
|
pageIndex.value = PageType.info.index;
|
||||||
pageOverlayTargetKey.currentState!.showOverlay(
|
pageOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptInfoTabText,
|
text: translations.promptInfoTabText,
|
||||||
actionBuilder: (context, onClose) => _buildActionButton(
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
@@ -337,7 +334,7 @@ void _promptInfoTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _promptSettingsTab() {
|
void _promptSettingsTab() {
|
||||||
pageIndex.value = RebootPageType.settings.index;
|
pageIndex.value = PageType.settings.index;
|
||||||
pageOverlayTargetKey.currentState!.showOverlay(
|
pageOverlayTargetKey.currentState!.showOverlay(
|
||||||
text: translations.promptSettingsTabText,
|
text: translations.promptSettingsTabText,
|
||||||
actionBuilder: (context, onClose) => _buildActionButton(
|
actionBuilder: (context, onClose) => _buildActionButton(
|
||||||
@@ -2,8 +2,8 @@ import 'package:email_validator/email_validator.dart';
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
|
||||||
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
|
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
|
||||||
final showPassword = RxBool(false);
|
final showPassword = RxBool(false);
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
|
||||||
|
import 'info_bar.dart';
|
||||||
|
|
||||||
bool inDialog = false;
|
bool inDialog = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/home_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
|
||||||
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
typedef WidgetBuilder = Widget Function(BuildContext, void Function());
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/message/data.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/server/server_start_button.dart';
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
|
import 'package:reboot_launcher/src/message/data.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/button/backend_start_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/button/server_type_selector.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
||||||
@@ -22,7 +22,7 @@ final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = G
|
|||||||
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
||||||
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
||||||
|
|
||||||
class BackendPage extends RebootPage {
|
class BackendPage extends AbstractPage {
|
||||||
const BackendPage({Key? key}) : super(key: key);
|
const BackendPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -32,16 +32,16 @@ class BackendPage extends RebootPage {
|
|||||||
String get iconAsset => "assets/images/backend.png";
|
String get iconAsset => "assets/images/backend.png";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageType get type => RebootPageType.backend;
|
PageType get type => PageType.backend;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasButton(String? pageName) => pageName == null;
|
bool hasButton(String? pageName) => pageName == null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageState<BackendPage> createState() => _BackendPageState();
|
AbstractPageState<BackendPage> createState() => _BackendPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BackendPageState extends RebootPageState<BackendPage> {
|
class _BackendPageState extends AbstractPageState<BackendPage> {
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
|
||||||
InfoBarEntry? _infoBarEntry;
|
InfoBarEntry? _infoBarEntry;
|
||||||
@@ -77,7 +77,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
Widget get _gameServerAddress => Obx(() {
|
Widget get _gameServerAddress => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.embedded) {
|
if(_backendController.type.value != AuthBackendType.embedded) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Widget get _hostName => Obx(() {
|
Widget get _hostName => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.remote) {
|
if(_backendController.type.value != AuthBackendType.remote) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Widget get _port => Obx(() {
|
Widget get _port => Obx(() {
|
||||||
if(_backendController.type.value == ServerType.embedded) {
|
if(_backendController.type.value == AuthBackendType.embedded) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Widget get _detached => Obx(() {
|
Widget get _detached => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.embedded) {
|
if(_backendController.type.value != AuthBackendType.embedded) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,12 +162,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
key: backendDetachedOverlayTargetKey,
|
key: backendDetachedOverlayTargetKey,
|
||||||
child: ToggleSwitch(
|
child: ToggleSwitch(
|
||||||
checked: _backendController.detached(),
|
checked: _backendController.detached(),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async => _backendController.detached.value = value
|
||||||
_backendController.detached.value = value;
|
|
||||||
if(_backendController.started.value) {
|
|
||||||
await _backendController.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -176,7 +171,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Widget get _unrealEngineConsoleKey => Obx(() {
|
Widget get _unrealEngineConsoleKey => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.embedded) {
|
if(_backendController.type.value != AuthBackendType.embedded) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +211,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget get _installationDirectory => Obx(() {
|
Widget get _installationDirectory => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.embedded) {
|
if(_backendController.type.value != AuthBackendType.embedded) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,5 +240,5 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get button => const ServerButton();
|
Widget get button => const BackendButton();
|
||||||
}
|
}
|
||||||
559
gui/lib/src/page/browser_page.dart
Normal file
559
gui/lib/src/page/browser_page.dart
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:app_links/app_links.dart';
|
||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentIcons show FluentIcons;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/cryptography.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/extensions.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
|
|
||||||
|
class BrowsePage extends AbstractPage {
|
||||||
|
const BrowsePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => translations.browserName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PageType get type => PageType.browser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get iconAsset => "assets/images/server_browser.png";
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hasButton(String? pageName) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AbstractPageState<BrowsePage> createState() => _BrowsePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BrowsePageState extends AbstractPageState<BrowsePage> {
|
||||||
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
|
||||||
|
final TextEditingController _filterController = TextEditingController();
|
||||||
|
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||||
|
|
||||||
|
final Rx<_Filter> _filter = Rx(_Filter.all);
|
||||||
|
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_initAppLink();
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initAppLink() async {
|
||||||
|
final appLinks = AppLinks();
|
||||||
|
final initialUrl = await appLinks.getInitialLink();
|
||||||
|
if(initialUrl != null) {
|
||||||
|
_onAppLink(initialUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
appLinks.uriLinkStream.listen(_onAppLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAppLink(Uri uri) {
|
||||||
|
final uuid = uri.host;
|
||||||
|
final server = _serverBrowserController.getServerById(uuid);
|
||||||
|
if(server != null) {
|
||||||
|
_joinServer(_hostingController.uuid, server);
|
||||||
|
}else {
|
||||||
|
showRebootInfoBar(
|
||||||
|
translations.noServerFound,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Obx(() {
|
||||||
|
final servers = _serverBrowserController.servers.value;
|
||||||
|
return servers?.isEmpty == true
|
||||||
|
? _noServers
|
||||||
|
: _buildPageBody(servers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _noServers => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translations.noServersAvailableTitle,
|
||||||
|
style: FluentTheme.of(context).typography.titleLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.noServersAvailableSubtitle,
|
||||||
|
style: FluentTheme.of(context).typography.body
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildPageBody(List<ServerBrowserEntry>? data) => StreamBuilder(
|
||||||
|
stream: _filterControllerStream.stream,
|
||||||
|
builder: (context, filterSnapshot) {
|
||||||
|
final items = data
|
||||||
|
?.where((entry) => _isValidItem(entry, filterSnapshot.data))
|
||||||
|
.toList(growable: false);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_searchBar,
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildFilter(context),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16.0
|
||||||
|
),
|
||||||
|
_buildSort(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _buildPopulatedListBody(items)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildSort(BuildContext context) => Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
fluentIcons.FluentIcons.arrow_sort_24_regular,
|
||||||
|
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
Text(
|
||||||
|
"Sort by: ",
|
||||||
|
style: TextStyle(
|
||||||
|
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
Obx(() => SizedBox(
|
||||||
|
width: 230,
|
||||||
|
child: DropDownButton(
|
||||||
|
onOpen: () => inDialog = true,
|
||||||
|
onClose: () => inDialog = false,
|
||||||
|
leading: Text(
|
||||||
|
_sort.value.translatedName,
|
||||||
|
textAlign: TextAlign.start
|
||||||
|
),
|
||||||
|
title: const Spacer(),
|
||||||
|
items: _Sort.values.map((entry) => MenuFlyoutItem(
|
||||||
|
text: Text(entry.translatedName),
|
||||||
|
onPressed: () => _sort.value = entry
|
||||||
|
)).toList()
|
||||||
|
),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Row _buildFilter(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
fluentIcons.FluentIcons.filter_24_regular,
|
||||||
|
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
Text(
|
||||||
|
"Filter by: ",
|
||||||
|
style: TextStyle(
|
||||||
|
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
Obx(() => SizedBox(
|
||||||
|
width: 125,
|
||||||
|
child: DropDownButton(
|
||||||
|
onOpen: () => inDialog = true,
|
||||||
|
onClose: () => inDialog = false,
|
||||||
|
leading: Text(
|
||||||
|
_filter.value.translatedName,
|
||||||
|
textAlign: TextAlign.start
|
||||||
|
),
|
||||||
|
title: const Spacer(),
|
||||||
|
items: _Filter.values.map((entry) => MenuFlyoutItem(
|
||||||
|
text: Text(entry.translatedName),
|
||||||
|
onPressed: () => _filter.value = entry
|
||||||
|
)).toList()
|
||||||
|
),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPopulatedListBody(List<ServerBrowserEntry>? items) => Obx(() {
|
||||||
|
final filter = _filter.value;
|
||||||
|
final sorted = items?.where((element) {
|
||||||
|
switch(filter) {
|
||||||
|
case _Filter.all:
|
||||||
|
return true;
|
||||||
|
case _Filter.accessible:
|
||||||
|
return element.password.isNotEmpty;
|
||||||
|
case _Filter.playable:
|
||||||
|
return _gameController.getVersionByGame(element.version) != null;
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
final sort = _sort.value;
|
||||||
|
sorted?.sort((first, second) {
|
||||||
|
switch(sort) {
|
||||||
|
case _Sort.timeAscending:
|
||||||
|
return first.timestamp.compareTo(second.timestamp);
|
||||||
|
case _Sort.timeDescending:
|
||||||
|
return second.timestamp.compareTo(first.timestamp);
|
||||||
|
case _Sort.nameAscending:
|
||||||
|
return first.name.compareTo(second.name);
|
||||||
|
case _Sort.nameDescending:
|
||||||
|
return second.name.compareTo(first.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(sorted?.isEmpty == true) {
|
||||||
|
return _noServersByQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: sorted?.length,
|
||||||
|
physics: sorted == null ? const NeverScrollableScrollPhysics() : const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final entry = sorted?.elementAt(index);
|
||||||
|
if (entry == null) {
|
||||||
|
return const SettingTile();
|
||||||
|
} else {
|
||||||
|
final hasPassword = entry.password.isNotEmpty;
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"${_formatName(entry)} • ${entry.author}",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"${_formatDescription(entry)} • ${_formatVersion(entry)}",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis
|
||||||
|
),
|
||||||
|
content: Button(
|
||||||
|
onPressed: () => _joinServer(_hostingController.uuid, entry),
|
||||||
|
child: Text(
|
||||||
|
_backendController.type.value == AuthBackendType.embedded
|
||||||
|
? translations.joinServer
|
||||||
|
: translations.copyIp),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget get _noServersByQuery => Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translations.noServersAvailableByQueryTitle,
|
||||||
|
style: FluentTheme.of(context).typography.titleLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translations.noServersAvailableByQuerySubtitle,
|
||||||
|
style: FluentTheme.of(context).typography.body
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
bool _isValidItem(ServerBrowserEntry entry, String? filter) =>
|
||||||
|
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||||
|
|
||||||
|
bool _filterServer(ServerBrowserEntry element, String filter) {
|
||||||
|
filter = filter.toLowerCase();
|
||||||
|
|
||||||
|
final uri = Uri.tryParse(filter);
|
||||||
|
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element.id.toLowerCase().contains(filter.toLowerCase())
|
||||||
|
|| element.name.toLowerCase().contains(filter)
|
||||||
|
|| element.author.toLowerCase().contains(filter)
|
||||||
|
|| element.description.toLowerCase().contains(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
onPressed: _filterController.text.isEmpty ? null : () {
|
||||||
|
_filterController.clear();
|
||||||
|
_filterControllerStream.add("");
|
||||||
|
},
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
|
shape: WidgetStateProperty.all(Border())
|
||||||
|
),
|
||||||
|
child: _searchBarIconData
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _searchBarIconData {
|
||||||
|
final color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||||
|
if (_filterController.text.isNotEmpty) {
|
||||||
|
return Icon(
|
||||||
|
FluentIcons.clear,
|
||||||
|
size: 8.0,
|
||||||
|
color: color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transform.flip(
|
||||||
|
flipX: true,
|
||||||
|
child: Icon(
|
||||||
|
FluentIcons.search,
|
||||||
|
size: 12.0,
|
||||||
|
color: color
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatName(ServerBrowserEntry server) {
|
||||||
|
final result = server.name;
|
||||||
|
return result.isEmpty ? translations.defaultServerName : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDescription(ServerBrowserEntry server) {
|
||||||
|
final result = server.description;
|
||||||
|
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatVersion(ServerBrowserEntry server) => "Fortnite ${server.version.toString()}";
|
||||||
|
|
||||||
|
Future<void> _joinServer(String uuid, ServerBrowserEntry server) async {
|
||||||
|
if(!kDebugMode && uuid == server.id) {
|
||||||
|
showRebootInfoBar(
|
||||||
|
translations.joinSelfServer,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final version = _gameController.getVersionByGame(server.version.toString());
|
||||||
|
if(version == null) {
|
||||||
|
showRebootInfoBar(
|
||||||
|
translations.cannotJoinServerVersion(server.version.toString()),
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hashedPassword = server.password;
|
||||||
|
final embedded = _backendController.type.value == AuthBackendType.embedded;
|
||||||
|
final author = server.author;
|
||||||
|
final encryptedIp = server.ip;
|
||||||
|
if(hashedPassword.isEmpty) {
|
||||||
|
final valid = await _isServerValid(server.name, encryptedIp);
|
||||||
|
if(!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onServerJoined(embedded, encryptedIp, author, version);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final confirmPassword = await _askForPassword();
|
||||||
|
if(confirmPassword == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!checkPassword(confirmPassword, hashedPassword)) {
|
||||||
|
showRebootInfoBar(
|
||||||
|
translations.wrongServerPassword,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
|
||||||
|
final valid = await _isServerValid(server.name, decryptedIp);
|
||||||
|
if(!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onServerJoined(embedded, decryptedIp, author, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _isServerValid(String name, String address) async {
|
||||||
|
final loadingBar = showRebootInfoBar(
|
||||||
|
translations.joiningServer(name),
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
loading: true,
|
||||||
|
severity: InfoBarSeverity.info
|
||||||
|
);
|
||||||
|
final result = await pingGameServer(address)
|
||||||
|
.withMinimumDuration(const Duration(seconds: 1));
|
||||||
|
loadingBar.close();
|
||||||
|
if(result) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showRebootInfoBar(
|
||||||
|
translations.offlineServer,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _askForPassword() async {
|
||||||
|
final confirmPasswordController = TextEditingController();
|
||||||
|
final showPassword = RxBool(false);
|
||||||
|
final showPasswordTrailing = RxBool(false);
|
||||||
|
return await showRebootDialog<String?>(
|
||||||
|
builder: (context) => FormDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InfoLabel(
|
||||||
|
label: translations.serverPassword,
|
||||||
|
child: Obx(() => TextFormBox(
|
||||||
|
placeholder: translations.serverPasswordPlaceholder,
|
||||||
|
controller: confirmPasswordController,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
obscureText: !showPassword.value,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofocus: true,
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
|
||||||
|
suffix: !showPasswordTrailing.value ? null : Button(
|
||||||
|
onPressed: () => showPassword.value = !showPassword.value,
|
||||||
|
style: ButtonStyle(
|
||||||
|
shape: WidgetStateProperty.all(const CircleBorder()),
|
||||||
|
backgroundColor: WidgetStateProperty.all(Colors.transparent)
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
showPassword.value ? fluentIcons.FluentIcons.eye_off_24_regular : fluentIcons.FluentIcons.eye_24_regular
|
||||||
|
),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
buttons: [
|
||||||
|
DialogButton(
|
||||||
|
text: translations.serverPasswordCancel,
|
||||||
|
type: ButtonType.secondary
|
||||||
|
),
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text: translations.serverPasswordConfirm,
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onServerJoined(bool embedded, String decryptedIp, String author, GameVersion version) {
|
||||||
|
if(embedded) {
|
||||||
|
_backendController.gameServerAddress.text = decryptedIp;
|
||||||
|
pageIndex.value = PageType.play.index;
|
||||||
|
}else {
|
||||||
|
FlutterClipboard.controlC(decryptedIp);
|
||||||
|
}
|
||||||
|
Get.find<GameController>().selectedVersion.value = version;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||||
|
embedded ? translations.joinedServer(author) : translations.copiedIp,
|
||||||
|
duration: infoBarLongDuration,
|
||||||
|
severity: InfoBarSeverity.success
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? get button => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get settings => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _Filter {
|
||||||
|
all,
|
||||||
|
accessible,
|
||||||
|
playable;
|
||||||
|
|
||||||
|
String get translatedName {
|
||||||
|
switch(this) {
|
||||||
|
case _Filter.all:
|
||||||
|
return translations.all;
|
||||||
|
case _Filter.accessible:
|
||||||
|
return translations.accessible;
|
||||||
|
case _Filter.playable:
|
||||||
|
return translations.playable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _Sort {
|
||||||
|
timeAscending,
|
||||||
|
timeDescending,
|
||||||
|
nameAscending,
|
||||||
|
nameDescending;
|
||||||
|
|
||||||
|
String get translatedName {
|
||||||
|
switch(this) {
|
||||||
|
case _Sort.timeAscending:
|
||||||
|
return translations.timeAscending;
|
||||||
|
case _Sort.timeDescending:
|
||||||
|
return translations.timeDescending;
|
||||||
|
case _Sort.nameAscending:
|
||||||
|
return translations.nameAscending;
|
||||||
|
case _Sort.nameDescending:
|
||||||
|
return translations.nameDescending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,17 +7,17 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
|
import 'package:reboot_launcher/src/message/data.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/data.dart';
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
import 'package:reboot_launcher/src/button/game_start_button.dart';
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
import 'package:reboot_launcher/src/button/version_selector.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||||
@@ -27,7 +27,7 @@ final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey
|
|||||||
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
|
||||||
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
|
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
|
||||||
|
|
||||||
class HostPage extends RebootPage {
|
class HostPage extends AbstractPage {
|
||||||
const HostPage({Key? key}) : super(key: key);
|
const HostPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -37,17 +37,17 @@ class HostPage extends RebootPage {
|
|||||||
String get iconAsset => "assets/images/host.png";
|
String get iconAsset => "assets/images/host.png";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageType get type => RebootPageType.host;
|
PageType get type => PageType.host;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasButton(String? pageName) => pageName == null;
|
bool hasButton(String? pageName) => pageName == null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageState<HostPage> createState() => _HostingPageState();
|
AbstractPageState<HostPage> createState() => _HostingPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HostingPageState extends RebootPageState<HostPage> {
|
class _HostingPageState extends AbstractPageState<HostPage> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
|
|
||||||
@@ -159,31 +159,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.eye_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.hostGameServerDiscoverableName),
|
|
||||||
subtitle: Text(translations.hostGameServerDiscoverableDescription),
|
|
||||||
contentWidth: null,
|
|
||||||
content: Obx(() => Row(
|
|
||||||
children: [
|
|
||||||
Obx(() => Text(
|
|
||||||
_hostingController.discoverable.value ? translations.on : translations.off
|
|
||||||
)),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0
|
|
||||||
),
|
|
||||||
ToggleSwitch(
|
|
||||||
checked: _hostingController.discoverable(),
|
|
||||||
onChanged: (value) async {
|
|
||||||
_hostingController.discoverable.value = value;
|
|
||||||
await _updateServer();
|
|
||||||
}
|
|
||||||
),
|
|
||||||
],
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -339,15 +314,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_hostingController.publishServer(
|
final server = await _hostingController.createServerBrowserEntry();
|
||||||
_hostingController.accountUsername.text,
|
_serverBrowserController.addServer(server);
|
||||||
_hostingController.instance.value!.version.toString()
|
|
||||||
);
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
_showCannotUpdateGameServer(error);
|
_showCannotUpdateGameServer(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _showCopiedLink() => showRebootInfoBar(
|
void _showCopiedLink() => showRebootInfoBar(
|
||||||
translations.hostShareLinkMessageSuccess,
|
translations.hostShareLinkMessageSuccess,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/onboard.dart';
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
|
import 'package:reboot_launcher/src/message/onboard.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class InfoPage extends RebootPage {
|
class InfoPage extends AbstractPage {
|
||||||
const InfoPage({Key? key}) : super(key: key);
|
const InfoPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageState<InfoPage> createState() => _InfoPageState();
|
AbstractPageState<InfoPage> createState() => _InfoPageState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get name => translations.infoName;
|
String get name => translations.infoName;
|
||||||
@@ -24,10 +24,10 @@ class InfoPage extends RebootPage {
|
|||||||
bool hasButton(String? routeName) => false;
|
bool hasButton(String? routeName) => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageType get type => RebootPageType.info;
|
PageType get type => PageType.info;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
class _InfoPageState extends AbstractPageState<InfoPage> {
|
||||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
||||||
static const String _kDiscordInviteUrl = "https://discord.gg/rebootmp";
|
static const String _kDiscordInviteUrl = "https://discord.gg/rebootmp";
|
||||||
|
|
||||||
@@ -3,21 +3,21 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
import 'package:reboot_launcher/src/page/backend_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
import 'package:reboot_launcher/src/page/browser_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
|
import 'package:reboot_launcher/src/page/host_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/browser_page.dart';
|
import 'package:reboot_launcher/src/page/info_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/host_page.dart';
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/info_page.dart';
|
import 'package:reboot_launcher/src/page/play_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/play_page.dart';
|
import 'package:reboot_launcher/src/page/settings_page.dart';
|
||||||
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar_area.dart';
|
||||||
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
|
|
||||||
|
|
||||||
final StreamController<void> pagesController = StreamController.broadcast();
|
final StreamController<void> pagesController = StreamController.broadcast();
|
||||||
bool hitBack = false;
|
bool hitBack = false;
|
||||||
|
|
||||||
final List<RebootPage> pages = [
|
final List<AbstractPage> pages = [
|
||||||
const PlayPage(),
|
const PlayPage(),
|
||||||
const HostPage(),
|
const HostPage(),
|
||||||
const BrowsePage(),
|
const BrowsePage(),
|
||||||
@@ -28,7 +28,7 @@ final List<RebootPage> pages = [
|
|||||||
|
|
||||||
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
|
final List<GlobalKey<OverlayTargetState>> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey());
|
||||||
|
|
||||||
final RxInt pageIndex = RxInt(RebootPageType.play.index);
|
final RxInt pageIndex = RxInt(PageType.play.index);
|
||||||
|
|
||||||
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
final HashMap<int, GlobalKey> _pageKeys = HashMap();
|
||||||
|
|
||||||
@@ -51,7 +51,9 @@ GlobalKey getPageKeyByIndex(int index) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasPageButton => pages[pageIndex.value].hasButton(pageStack.lastOrNull);
|
bool get hasPageButton => currentPage.hasButton(currentPageStack.lastOrNull);
|
||||||
|
|
||||||
|
AbstractPage get currentPage => pages[pageIndex.value];
|
||||||
|
|
||||||
final Queue<Object?> appStack = _createAppStack();
|
final Queue<Object?> appStack = _createAppStack();
|
||||||
Queue _createAppStack() {
|
Queue _createAppStack() {
|
||||||
@@ -71,13 +73,12 @@ Queue _createAppStack() {
|
|||||||
|
|
||||||
final Map<int, Queue<String>> _pagesStack = Map.fromEntries(List.generate(pages.length, (index) => MapEntry(index, Queue<String>())));
|
final Map<int, Queue<String>> _pagesStack = Map.fromEntries(List.generate(pages.length, (index) => MapEntry(index, Queue<String>())));
|
||||||
|
|
||||||
Queue<String> get pageStack => _pagesStack[pageIndex.value]!;
|
Queue<String> get currentPageStack => _pagesStack[pageIndex.value]!;
|
||||||
|
|
||||||
void addSubPageToStack(String pageName) {
|
void addSubPageToCurrent(String pageName) {
|
||||||
final index = pageIndex.value;
|
final index = pageIndex.value;
|
||||||
final identifier = "${index}_$pageName";
|
appStack.add(pageName);
|
||||||
appStack.add(identifier);
|
_pagesStack[index]!.add(pageName);
|
||||||
_pagesStack[index]!.add(identifier);
|
|
||||||
pagesController.add(null);
|
pagesController.add(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/data.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
import 'package:reboot_launcher/src/message/data.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/button/game_start_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/button/version_selector.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||||
|
|
||||||
class PlayPage extends RebootPage {
|
class PlayPage extends AbstractPage {
|
||||||
const PlayPage({Key? key}) : super(key: key);
|
const PlayPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageState<PlayPage> createState() => _PlayPageState();
|
AbstractPageState<PlayPage> createState() => _PlayPageState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasButton(String? pageName) => pageName == null;
|
bool hasButton(String? pageName) => pageName == null;
|
||||||
@@ -30,10 +30,10 @@ class PlayPage extends RebootPage {
|
|||||||
String get iconAsset => "assets/images/play.png";
|
String get iconAsset => "assets/images/play.png";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageType get type => RebootPageType.play;
|
PageType get type => PageType.play;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
class _PlayPageState extends AbstractPageState<PlayPage> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:async/async.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||||
@@ -9,12 +8,12 @@ import 'package:reboot_common/common.dart';
|
|||||||
import 'package:reboot_launcher/l10n/reboot_localizations.dart';
|
import 'package:reboot_launcher/l10n/reboot_localizations.dart';
|
||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
|
import 'package:reboot_launcher/src/tile/file_setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
|
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
|
||||||
@@ -22,7 +21,7 @@ final GlobalKey<TextFormBoxState> settingsAuthDllInputKey = GlobalKey();
|
|||||||
final GlobalKey<TextFormBoxState> settingsMemoryDllInputKey = GlobalKey();
|
final GlobalKey<TextFormBoxState> settingsMemoryDllInputKey = GlobalKey();
|
||||||
final GlobalKey<TextFormBoxState> settingsGameServerDllInputKey = GlobalKey();
|
final GlobalKey<TextFormBoxState> settingsGameServerDllInputKey = GlobalKey();
|
||||||
|
|
||||||
class SettingsPage extends RebootPage {
|
class SettingsPage extends AbstractPage {
|
||||||
const SettingsPage({Key? key}) : super(key: key);
|
const SettingsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -32,16 +31,16 @@ class SettingsPage extends RebootPage {
|
|||||||
String get iconAsset => "assets/images/settings.png";
|
String get iconAsset => "assets/images/settings.png";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageType get type => RebootPageType.settings;
|
PageType get type => PageType.settings;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool hasButton(String? pageName) => false;
|
bool hasButton(String? pageName) => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageState<SettingsPage> createState() => _SettingsPageState();
|
AbstractPageState<SettingsPage> createState() => _SettingsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
class _SettingsPageState extends AbstractPageState<SettingsPage> {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
int? _downloadFromMirrorId;
|
int? _downloadFromMirrorId;
|
||||||
@@ -70,9 +69,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|||||||
description: translations.settingsClientConsoleDescription,
|
description: translations.settingsClientConsoleDescription,
|
||||||
controller: _dllController.unrealEngineConsoleDll,
|
controller: _dllController.unrealEngineConsoleDll,
|
||||||
onReset: () async {
|
onReset: () async {
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.console);
|
final path = _dllController.getDefaultDllPath(GameDll.console);
|
||||||
_dllController.unrealEngineConsoleDll.text = path;
|
_dllController.unrealEngineConsoleDll.text = path;
|
||||||
await _dllController.download(InjectableDll.console, path, force: true);
|
await _dllController.download(GameDll.console, path, force: true);
|
||||||
settingsConsoleDllInputKey.currentState?.validate();
|
settingsConsoleDllInputKey.currentState?.validate();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -82,9 +81,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|||||||
description: translations.settingsClientAuthDescription,
|
description: translations.settingsClientAuthDescription,
|
||||||
controller: _dllController.backendDll,
|
controller: _dllController.backendDll,
|
||||||
onReset: () async {
|
onReset: () async {
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
|
final path = _dllController.getDefaultDllPath(GameDll.auth);
|
||||||
_dllController.backendDll.text = path;
|
_dllController.backendDll.text = path;
|
||||||
await _dllController.download(InjectableDll.auth, path, force: true);
|
await _dllController.download(GameDll.auth, path, force: true);
|
||||||
settingsAuthDllInputKey.currentState?.validate();
|
settingsAuthDllInputKey.currentState?.validate();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -94,14 +93,24 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|||||||
description: translations.settingsClientMemoryDescription,
|
description: translations.settingsClientMemoryDescription,
|
||||||
controller: _dllController.memoryLeakDll,
|
controller: _dllController.memoryLeakDll,
|
||||||
onReset: () async {
|
onReset: () async {
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
|
final path = _dllController.getDefaultDllPath(GameDll.memoryLeak);
|
||||||
_dllController.memoryLeakDll.text = path;
|
_dllController.memoryLeakDll.text = path;
|
||||||
await _dllController.download(InjectableDll.memoryLeak, path, force: true);
|
await _dllController.download(GameDll.memoryLeak, path, force: true);
|
||||||
settingsAuthDllInputKey.currentState?.validate();
|
settingsAuthDllInputKey.currentState?.validate();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
_gameServer
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
SettingTile get _gameServer => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.document_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerName),
|
||||||
|
subtitle: Text(translations.settingsServerSubtitle),
|
||||||
|
children: [
|
||||||
_internalFilesServerType,
|
_internalFilesServerType,
|
||||||
_internalFilesUpdateTimer,
|
|
||||||
_internalFilesServerSource,
|
_internalFilesServerSource,
|
||||||
_internalFilesNewServerSource,
|
_internalFilesNewServerSource,
|
||||||
],
|
],
|
||||||
@@ -197,9 +206,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|||||||
description: translations.settingsServerFileDescription,
|
description: translations.settingsServerFileDescription,
|
||||||
controller: _dllController.customGameServerDll,
|
controller: _dllController.customGameServerDll,
|
||||||
onReset: () async {
|
onReset: () async {
|
||||||
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
|
final path = _dllController.getDefaultDllPath(GameDll.gameServer);
|
||||||
_dllController.customGameServerDll.text = path;
|
_dllController.customGameServerDll.text = path;
|
||||||
await _dllController.download(InjectableDll.gameServer, path);
|
await _dllController.download(GameDll.gameServer, path);
|
||||||
settingsGameServerDllInputKey.currentState?.validate();
|
settingsGameServerDllInputKey.currentState?.validate();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -279,35 +288,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget get _internalFilesUpdateTimer => Obx(() {
|
|
||||||
if(_dllController.customGameServer.value) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.timer_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerTimerName),
|
|
||||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
|
||||||
contentWidth: SettingTile.kDefaultContentWidth + 30,
|
|
||||||
content: Obx(() => DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(_dllController.timer.value.text),
|
|
||||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
|
||||||
text: Text(entry.text),
|
|
||||||
onPressed: () {
|
|
||||||
_dllController.timer.value = entry;
|
|
||||||
_dllController.updateGameServerDll(
|
|
||||||
force: true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)).toList()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
SettingTile get _language => SettingTile(
|
SettingTile get _language => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.local_language_24_regular
|
FluentIcons.local_language_24_regular
|
||||||
@@ -376,13 +356,3 @@ extension _ThemeModeExtension on ThemeMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _UpdateTimerExtension on UpdateTimer {
|
|
||||||
String get text {
|
|
||||||
if (this == UpdateTimer.never) {
|
|
||||||
return translations.updateGameServerDllNever;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations.updateGameServerDllEvery(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/onboard.dart';
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:reboot_launcher/src/message/onboard.dart';
|
||||||
|
|
||||||
abstract class RebootPage extends StatefulWidget {
|
|
||||||
const RebootPage({super.key});
|
abstract class AbstractPage extends StatefulWidget {
|
||||||
|
const AbstractPage({super.key});
|
||||||
|
|
||||||
String get name;
|
String get name;
|
||||||
|
|
||||||
String get iconAsset;
|
String get iconAsset;
|
||||||
|
|
||||||
RebootPageType get type;
|
PageType get type;
|
||||||
|
|
||||||
int get index => type.index;
|
int get index => type.index;
|
||||||
|
|
||||||
bool hasButton(String? pageName);
|
bool hasButton(String? pageName);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RebootPageState createState();
|
AbstractPageState createState();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
abstract class AbstractPageState<T extends AbstractPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -5,9 +5,11 @@ class PageSuggestion {
|
|||||||
final int pageIndex;
|
final int pageIndex;
|
||||||
final String? routeName;
|
final String? routeName;
|
||||||
|
|
||||||
PageSuggestion({required this.name,
|
PageSuggestion({
|
||||||
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
this.content,
|
this.content,
|
||||||
required this.pageIndex,
|
required this.pageIndex,
|
||||||
this.routeName});
|
this.routeName
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
enum RebootPageType {
|
enum PageType {
|
||||||
play,
|
play,
|
||||||
host,
|
host,
|
||||||
browser,
|
browser,
|
||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart' show MaterialPage;
|
import 'package:flutter/material.dart' show MaterialPage;
|
||||||
@@ -12,37 +11,39 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
import 'package:reboot_launcher/src/controller/dll_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_browser_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
import 'package:reboot_launcher/src/pager/page_suggestion.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/message/dll.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_suggestion.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
|
import 'package:reboot_launcher/src/tile/profile_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/profile_tile.dart';
|
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:reboot_launcher/src/messenger/info_bar.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/abstract_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/updater.dart';
|
||||||
|
import 'package:reboot_launcher/src/messenger/info_bar_area.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
|
||||||
|
const double _kDefaultPadding = 12.0;
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class RebootPager extends StatefulWidget {
|
||||||
static const double kDefaultPadding = 12.0;
|
|
||||||
|
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const RebootPager({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HomePage> createState() => _HomePageState();
|
State<RebootPager> createState() => _RebootPagerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
class _RebootPagerState extends State<RebootPager> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
|
final ServerBrowserController _serverBrowserController = Get.find<ServerBrowserController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final DllController _dllController = Get.find<DllController>();
|
final DllController _dllController = Get.find<DllController>();
|
||||||
final GlobalKey _searchKey = GlobalKey();
|
final GlobalKey _searchKey = GlobalKey();
|
||||||
@@ -61,7 +62,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
_syncPageViewWithNavigator();
|
_syncPageViewWithNavigator();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_checkUpdates();
|
_checkUpdates();
|
||||||
_initAppLink();
|
|
||||||
_checkGameServer();
|
_checkGameServer();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -79,30 +79,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initAppLink() async {
|
|
||||||
final appLinks = AppLinks();
|
|
||||||
final initialUrl = await appLinks.getInitialLink();
|
|
||||||
if(initialUrl != null) {
|
|
||||||
_joinServer(initialUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
appLinks.uriLinkStream.listen(_joinServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _joinServer(Uri uri) {
|
|
||||||
final uuid = uri.host;
|
|
||||||
final server = _hostingController.findServerById(uuid);
|
|
||||||
if(server != null) {
|
|
||||||
_backendController.joinServer(_hostingController.uuid, server);
|
|
||||||
}else {
|
|
||||||
showRebootInfoBar(
|
|
||||||
translations.noServerFound,
|
|
||||||
duration: infoBarLongDuration,
|
|
||||||
severity: InfoBarSeverity.error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _checkGameServer() async {
|
Future<void> _checkGameServer() async {
|
||||||
try {
|
try {
|
||||||
final address = _backendController.gameServerAddress.text;
|
final address = _backendController.gameServerAddress.text;
|
||||||
@@ -115,7 +91,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_backendController.joinLocalhost();
|
_backendController.gameServerAddress.text = kDefaultGameServerHost;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||||
translations.serverNoLongerAvailableUnnamed,
|
translations.serverNoLongerAvailableUnnamed,
|
||||||
severity: InfoBarSeverity.warning,
|
severity: InfoBarSeverity.warning,
|
||||||
@@ -128,7 +104,23 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _checkUpdates() {
|
void _checkUpdates() {
|
||||||
_settingsController.notifyLauncherUpdate();
|
checkLauncherUpdate(
|
||||||
|
onUpdate: (latestVersion) {
|
||||||
|
late InfoBarEntry infoBar;
|
||||||
|
infoBar = showRebootInfoBar(
|
||||||
|
translations.updateAvailable(latestVersion.toString()),
|
||||||
|
duration: null,
|
||||||
|
severity: InfoBarSeverity.warning,
|
||||||
|
action: Button(
|
||||||
|
child: Text(translations.updateAvailableAction),
|
||||||
|
onPressed: () {
|
||||||
|
infoBar.close();
|
||||||
|
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if(!dllsDirectory.existsSync()) {
|
if(!dllsDirectory.existsSync()) {
|
||||||
dllsDirectory.createSync(recursive: true);
|
dllsDirectory.createSync(recursive: true);
|
||||||
@@ -146,15 +138,13 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _hostingController.discardServer();
|
await _serverBrowserController.removeServer(_hostingController.uuid);
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
log("[HOSTING] Cannot discard server on exit: $error");
|
log("[HOSTING] Cannot discard server on exit: $error");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(_backendController.started.value) {
|
await _backendController.stop();
|
||||||
await _backendController.toggle();
|
|
||||||
}
|
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
log("[BACKEND] Cannot stop backend on exit: $error");
|
log("[BACKEND] Cannot stop backend on exit: $error");
|
||||||
}
|
}
|
||||||
@@ -300,10 +290,10 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
Widget _buildBody() => Expanded(
|
Widget _buildBody() => Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: HomePage.kDefaultPadding,
|
left: _kDefaultPadding,
|
||||||
right: HomePage.kDefaultPadding * 2,
|
right: _kDefaultPadding * 2,
|
||||||
top: HomePage.kDefaultPadding,
|
top: _kDefaultPadding,
|
||||||
bottom: HomePage.kDefaultPadding * 2
|
bottom: _kDefaultPadding * 2
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -339,6 +329,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildBodyContent() => PageView.builder(
|
Widget _buildBodyContent() => PageView.builder(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemBuilder: (context, index) => Navigator(
|
itemBuilder: (context, index) => Navigator(
|
||||||
onPopPage: (page, data) => true,
|
onPopPage: (page, data) => true,
|
||||||
@@ -347,8 +338,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
onChanged: (routeName) {
|
onChanged: (routeName) {
|
||||||
if(routeName != null) {
|
if(routeName != null) {
|
||||||
pageIndex.refresh();
|
pageIndex.refresh();
|
||||||
addSubPageToStack(routeName);
|
addSubPageToCurrent(routeName);
|
||||||
pagesController.add(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -375,12 +365,22 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
stream: pagesController.stream,
|
stream: pagesController.stream,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final elements = <TextSpan>[];
|
final elements = <TextSpan>[];
|
||||||
elements.add(_buildBodyHeaderRootPage(inactiveColor));
|
final subPagesLength = currentPageStack.length;
|
||||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
final pagesLength = subPagesLength + 1;
|
||||||
var innerPage = pageStack.elementAt(i);
|
elements.add(_buildBodyHeaderNestedPage(
|
||||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
name: currentPage.name,
|
||||||
|
index: 0,
|
||||||
|
length: pagesLength,
|
||||||
|
inactiveColor: inactiveColor
|
||||||
|
));
|
||||||
|
for(var index = 0; index < subPagesLength; index++) {
|
||||||
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
|
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
|
||||||
elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor));
|
elements.add(_buildBodyHeaderNestedPage(
|
||||||
|
name: currentPageStack.elementAt(index),
|
||||||
|
index: index + 1,
|
||||||
|
length: pagesLength,
|
||||||
|
inactiveColor: inactiveColor
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text.rich(
|
return Text.rich(
|
||||||
@@ -397,26 +397,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan(
|
|
||||||
text: pages[pageIndex.value].name,
|
|
||||||
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
|
|
||||||
if(inDialog) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var i = 0; i < pageStack.length; i++) {
|
|
||||||
Navigator.of(pageKey.currentContext!).pop();
|
|
||||||
final element = pageStack.removeLast();
|
|
||||||
appStack.remove(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
pagesController.add(null);
|
|
||||||
}) : null,
|
|
||||||
style: TextStyle(
|
|
||||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
|
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
|
||||||
text: " > ",
|
text: " > ",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -424,24 +404,33 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan(
|
TextSpan _buildBodyHeaderNestedPage({
|
||||||
text: nestedPageName,
|
required String name,
|
||||||
recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
required int index,
|
||||||
|
required int length,
|
||||||
|
required Color inactiveColor
|
||||||
|
}) {
|
||||||
|
final last = index == length - 1;
|
||||||
|
return TextSpan(
|
||||||
|
text: name,
|
||||||
|
recognizer: last ? null : (TapGestureRecognizer()..onTap = () {
|
||||||
if(inDialog) {
|
if(inDialog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(var j = 0; j < nestedPageIndex - 1; j++) {
|
var pops = length - 1 - index;
|
||||||
|
while(pops-- > 0) {
|
||||||
Navigator.of(pageKey.currentContext!).pop();
|
Navigator.of(pageKey.currentContext!).pop();
|
||||||
final element = pageStack.removeLast();
|
final element = currentPageStack.removeLast();
|
||||||
appStack.remove(element);
|
appStack.remove(element);
|
||||||
}
|
}
|
||||||
pagesController.add(null);
|
pagesController.add(null);
|
||||||
}),
|
}),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor
|
color: last ? null : inactiveColor
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildLateralView() => SizedBox(
|
Widget _buildLateralView() => SizedBox(
|
||||||
width: 310,
|
width: 310,
|
||||||
@@ -479,7 +468,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildNavigationItem(RebootPage page) {
|
Widget _buildNavigationItem(AbstractPage page) {
|
||||||
final index = page.type.index;
|
final index = page.type.index;
|
||||||
return OverlayTarget(
|
return OverlayTarget(
|
||||||
key: getOverlayTargetKeyByPage(index),
|
key: getOverlayTargetKeyByPage(index),
|
||||||
@@ -488,9 +477,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
final lastPageIndex = pageIndex.value;
|
final lastPageIndex = pageIndex.value;
|
||||||
if(lastPageIndex != index) {
|
if(lastPageIndex != index) {
|
||||||
pageIndex.value = index;
|
pageIndex.value = index;
|
||||||
}else if(pageStack.isNotEmpty) {
|
}else if(currentPageStack.isNotEmpty) {
|
||||||
Navigator.of(pageKey.currentContext!).pop();
|
Navigator.of(pageKey.currentContext!).pop();
|
||||||
final element = pageStack.removeLast();
|
final element = currentPageStack.removeLast();
|
||||||
appStack.remove(element);
|
appStack.remove(element);
|
||||||
pagesController.add(null);
|
pagesController.add(null);
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
|
import 'package:reboot_launcher/src/button/file_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
import 'package:reboot_launcher/src/tile/setting_tile.dart';
|
||||||
|
|
||||||
const double _kButtonDimensions = 30;
|
const double _kButtonDimensions = 30;
|
||||||
const double _kButtonSpacing = 8;
|
const double _kButtonSpacing = 8;
|
||||||
@@ -19,7 +19,7 @@ class InfoTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Expander(
|
child: Expander(
|
||||||
key: expanderKey,
|
key: expanderKey,
|
||||||
header: Row(
|
header: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
@@ -3,9 +3,9 @@ import 'package:get/get.dart';
|
|||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/pager/page_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/message/profile.dart';
|
||||||
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
import 'package:reboot_launcher/src/messenger/overlay.dart';
|
||||||
import 'package:reboot_launcher/src/widget/message/profile.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
|
|
||||||
class ProfileWidget extends StatefulWidget {
|
class ProfileWidget extends StatefulWidget {
|
||||||
@@ -113,6 +113,6 @@ class _ProfileWidgetState extends State<ProfileWidget> {
|
|||||||
return "$username@projectreboot.dev".toLowerCase();
|
return "$username@projectreboot.dev".toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username;
|
TextEditingController get _username => pageIndex.value == PageType.host.index ? _hostingController.accountUsername : _gameController.username;
|
||||||
TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password;
|
TextEditingController get _password => pageIndex.value == PageType.host.index ? _hostingController.accountPassword : _gameController.password;
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ class SettingTile extends StatefulWidget {
|
|||||||
static const double kDefaultContentWidth = 200.0;
|
static const double kDefaultContentWidth = 200.0;
|
||||||
|
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
final Icon icon;
|
final Icon? icon;
|
||||||
final Text? title;
|
final Text? title;
|
||||||
final Text? subtitle;
|
final Text? subtitle;
|
||||||
final Widget? content;
|
final Widget? content;
|
||||||
@@ -19,10 +19,10 @@ class SettingTile extends StatefulWidget {
|
|||||||
|
|
||||||
const SettingTile({
|
const SettingTile({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.icon,
|
||||||
|
this.title,
|
||||||
|
this.subtitle,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
required this.icon,
|
|
||||||
required this.title,
|
|
||||||
required this.subtitle,
|
|
||||||
this.content,
|
this.content,
|
||||||
this.contentWidth = kDefaultContentWidth,
|
this.contentWidth = kDefaultContentWidth,
|
||||||
this.overlayKey,
|
this.overlayKey,
|
||||||
@@ -62,6 +62,10 @@ class SettingTileState extends State<SettingTile> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Card _buildBody() {
|
Card _buildBody() {
|
||||||
|
final icon = widget.icon;
|
||||||
|
final title = widget.title;
|
||||||
|
final subtitle = widget.subtitle;
|
||||||
|
final isSkeleton = icon == null || title == null || subtitle == null;
|
||||||
return Card(
|
return Card(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(6.0)
|
Radius.circular(6.0)
|
||||||
@@ -76,10 +80,10 @@ class SettingTileState extends State<SettingTile> {
|
|||||||
if(widget.overlayKey != null)
|
if(widget.overlayKey != null)
|
||||||
OverlayTarget(
|
OverlayTarget(
|
||||||
key: widget.overlayKey,
|
key: widget.overlayKey,
|
||||||
child: widget.icon,
|
child: isSkeleton ? _skeletonIcon : icon,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
widget.icon,
|
isSkeleton ? _skeletonIcon : icon,
|
||||||
|
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
|
|
||||||
@@ -87,12 +91,14 @@ class SettingTileState extends State<SettingTile> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
widget.title == null ? _skeletonTitle : widget.title!,
|
isSkeleton ? _skeletonTitle : title,
|
||||||
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
|
isSkeleton ? _skeletonSubtitle : subtitle
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 16.0),
|
||||||
|
|
||||||
_trailing
|
_trailing
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -100,6 +106,12 @@ class SettingTileState extends State<SettingTile> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SkeletonAvatar get _skeletonIcon => const SkeletonAvatar(style: SkeletonAvatarStyle(
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
shape: BoxShape.circle
|
||||||
|
));
|
||||||
|
|
||||||
void Function()? _buildOnPressed() {
|
void Function()? _buildOnPressed() {
|
||||||
if(widget.onPressed != null) {
|
if(widget.onPressed != null) {
|
||||||
return widget.onPressed;
|
return widget.onPressed;
|
||||||
@@ -17,3 +17,13 @@ extension StringExtension on String {
|
|||||||
return substring(index + leading.length);
|
return substring(index + leading.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FutureExtension<T> on Future<T> {
|
||||||
|
Future<T> withMinimumDuration(Duration duration) async {
|
||||||
|
final result = await Future.wait([
|
||||||
|
Future.delayed(duration),
|
||||||
|
this
|
||||||
|
]);
|
||||||
|
return result.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:collection';
|
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
|||||||
40
gui/lib/src/util/updater.dart
Normal file
40
gui/lib/src/util/updater.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/main.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
Future<void> checkLauncherUpdate({
|
||||||
|
required void Function(Version) onUpdate
|
||||||
|
}) async {
|
||||||
|
if (appVersion == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pubspec = await _getPubspecYaml();
|
||||||
|
if (pubspec == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final latestVersion = Version.parse(pubspec["version"]);
|
||||||
|
if (latestVersion <= appVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(latestVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _getPubspecYaml() async {
|
||||||
|
try {
|
||||||
|
final pubspecResponse = await http.get(Uri.parse(
|
||||||
|
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||||
|
if (pubspecResponse.statusCode != 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadYaml(pubspecResponse.body);
|
||||||
|
} catch (error) {
|
||||||
|
log("[UPDATER] Cannot check for updates: $error");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons;
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/messenger/dialog.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
|
|
||||||
|
|
||||||
class BrowsePage extends RebootPage {
|
|
||||||
const BrowsePage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get name => translations.browserName;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RebootPageType get type => RebootPageType.browser;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get iconAsset => "assets/images/server_browser.png";
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool hasButton(String? pageName) => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
|
||||||
final TextEditingController _filterController = TextEditingController();
|
|
||||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
|
||||||
|
|
||||||
final Rx<_Filter> _filter = Rx(_Filter.all);
|
|
||||||
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Obx(() {
|
|
||||||
final data = _hostingController.servers.value
|
|
||||||
?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable)
|
|
||||||
.toSet();
|
|
||||||
if(data == null || data.isEmpty == true) {
|
|
||||||
return _noServers;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _buildPageBody(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget get _noServers => Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
translations.noServersAvailableTitle,
|
|
||||||
style: FluentTheme.of(context).typography.titleLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
translations.noServersAvailableSubtitle,
|
|
||||||
style: FluentTheme.of(context).typography.body
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildPageBody(Set<FortniteServer> 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: 24,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildFilter(context),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16.0
|
|
||||||
),
|
|
||||||
_buildSort(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _buildPopulatedListBody(items)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSort(BuildContext context) => Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
fluentUiIcons.FluentIcons.arrow_sort_24_regular,
|
|
||||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Text(
|
|
||||||
"Sort by: ",
|
|
||||||
style: TextStyle(
|
|
||||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Obx(() => SizedBox(
|
|
||||||
width: 230,
|
|
||||||
child: DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(
|
|
||||||
_sort.value.translatedName,
|
|
||||||
textAlign: TextAlign.start
|
|
||||||
),
|
|
||||||
title: const Spacer(),
|
|
||||||
items: _Sort.values.map((entry) => MenuFlyoutItem(
|
|
||||||
text: Text(entry.translatedName),
|
|
||||||
onPressed: () => _sort.value = entry
|
|
||||||
)).toList()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
Row _buildFilter(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
fluentUiIcons.FluentIcons.filter_24_regular,
|
|
||||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Text(
|
|
||||||
"Filter by: ",
|
|
||||||
style: TextStyle(
|
|
||||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Obx(() => SizedBox(
|
|
||||||
width: 125,
|
|
||||||
child: DropDownButton(
|
|
||||||
onOpen: () => inDialog = true,
|
|
||||||
onClose: () => inDialog = false,
|
|
||||||
leading: Text(
|
|
||||||
_filter.value.translatedName,
|
|
||||||
textAlign: TextAlign.start
|
|
||||||
),
|
|
||||||
title: const Spacer(),
|
|
||||||
items: _Filter.values.map((entry) => MenuFlyoutItem(
|
|
||||||
text: Text(entry.translatedName),
|
|
||||||
onPressed: () => _filter.value = entry
|
|
||||||
)).toList()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPopulatedListBody(Set<FortniteServer> items) => Obx(() {
|
|
||||||
final filter = _filter.value;
|
|
||||||
final sorted = items.where((element) {
|
|
||||||
switch(filter) {
|
|
||||||
case _Filter.all:
|
|
||||||
return true;
|
|
||||||
case _Filter.accessible:
|
|
||||||
return element.password == null;
|
|
||||||
case _Filter.playable:
|
|
||||||
return _gameController.getVersionByGame(element.version) != null;
|
|
||||||
}
|
|
||||||
}).toList();
|
|
||||||
final sort = _sort.value;
|
|
||||||
sorted.sort((first, second) {
|
|
||||||
switch(sort) {
|
|
||||||
case _Sort.timeAscending:
|
|
||||||
return first.timestamp.compareTo(second.timestamp);
|
|
||||||
case _Sort.timeDescending:
|
|
||||||
return second.timestamp.compareTo(first.timestamp);
|
|
||||||
case _Sort.nameAscending:
|
|
||||||
return first.name.compareTo(second.name);
|
|
||||||
case _Sort.nameDescending:
|
|
||||||
return second.name.compareTo(first.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(sorted.isEmpty) {
|
|
||||||
return _noServersByQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
itemCount: sorted.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final entry = sorted.elementAt(index);
|
|
||||||
final hasPassword = entry.password != null;
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
"${_formatName(entry)} • ${entry.author}",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
"${_formatDescription(entry)} • ${_formatVersion(entry)}",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis
|
|
||||||
),
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
|
||||||
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget get _noServersByQuery => Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
translations.noServersAvailableByQueryTitle,
|
|
||||||
style: FluentTheme.of(context).typography.titleLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
translations.noServersAvailableByQuerySubtitle,
|
|
||||||
style: FluentTheme.of(context).typography.body
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
bool _isValidItem(FortniteServer entry, String? filter) =>
|
|
||||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
|
||||||
|
|
||||||
bool _filterServer(FortniteServer element, String filter) {
|
|
||||||
filter = filter.toLowerCase();
|
|
||||||
|
|
||||||
final uri = Uri.tryParse(filter);
|
|
||||||
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return element.id.toLowerCase().contains(filter.toLowerCase())
|
|
||||||
|| element.name.toLowerCase().contains(filter)
|
|
||||||
|| element.author.toLowerCase().contains(filter)
|
|
||||||
|| element.description.toLowerCase().contains(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
onPressed: _filterController.text.isEmpty ? null : () {
|
|
||||||
_filterController.clear();
|
|
||||||
_filterControllerStream.add("");
|
|
||||||
},
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
|
||||||
shape: WidgetStateProperty.all(Border())
|
|
||||||
),
|
|
||||||
child: _searchBarIconData
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _searchBarIconData {
|
|
||||||
final color = FluentTheme.of(context).resources.textFillColorPrimary;
|
|
||||||
if (_filterController.text.isNotEmpty) {
|
|
||||||
return Icon(
|
|
||||||
FluentIcons.clear,
|
|
||||||
size: 8.0,
|
|
||||||
color: color
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Transform.flip(
|
|
||||||
flipX: true,
|
|
||||||
child: Icon(
|
|
||||||
FluentIcons.search,
|
|
||||||
size: 12.0,
|
|
||||||
color: color
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatName(FortniteServer server) {
|
|
||||||
final result = server.name;
|
|
||||||
return result.isEmpty ? translations.defaultServerName : result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDescription(FortniteServer server) {
|
|
||||||
final result = server.description;
|
|
||||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}";
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? get button => null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> get settings => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
enum _Filter {
|
|
||||||
all,
|
|
||||||
accessible,
|
|
||||||
playable;
|
|
||||||
|
|
||||||
String get translatedName {
|
|
||||||
switch(this) {
|
|
||||||
case _Filter.all:
|
|
||||||
return translations.all;
|
|
||||||
case _Filter.accessible:
|
|
||||||
return translations.accessible;
|
|
||||||
case _Filter.playable:
|
|
||||||
return translations.playable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum _Sort {
|
|
||||||
timeAscending,
|
|
||||||
timeDescending,
|
|
||||||
nameAscending,
|
|
||||||
nameDescending;
|
|
||||||
|
|
||||||
String get translatedName {
|
|
||||||
switch(this) {
|
|
||||||
case _Sort.timeAscending:
|
|
||||||
return translations.timeAscending;
|
|
||||||
case _Sort.timeDescending:
|
|
||||||
return translations.timeDescending;
|
|
||||||
case _Sort.nameAscending:
|
|
||||||
return translations.nameAscending;
|
|
||||||
case _Sort.nameDescending:
|
|
||||||
return translations.nameDescending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
|
|
||||||
class ServerButton extends StatefulWidget {
|
|
||||||
const ServerButton({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ServerButton> createState() => _ServerButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerButtonState extends State<ServerButton> {
|
|
||||||
late final BackendController _controller = Get.find<BackendController>();
|
|
||||||
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(
|
|
||||||
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.toggle()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
String get _buttonText {
|
|
||||||
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == kDefaultBackendPort.toString()){
|
|
||||||
return translations.checkServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_controller.started.value){
|
|
||||||
return translations.stopServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations.startServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,7 +57,6 @@ dependencies:
|
|||||||
port_forwarder: ^1.0.0
|
port_forwarder: ^1.0.0
|
||||||
|
|
||||||
# Server browser
|
# Server browser
|
||||||
supabase_flutter: ^2.7.0
|
|
||||||
dart_ipify: ^1.1.1
|
dart_ipify: ^1.1.1
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:server_browser_backend/server_browser_backend.dart';
|
import 'package:server_browser_backend/server_browser_backend.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
final server = WebSocketServer();
|
final server = ServerBrowserBackend();
|
||||||
await server.start(port: 8080);
|
await server.start(port: 8080);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/server_entry.dart';
|
export 'src/server_browser_entry.dart';
|
||||||
export 'src/web_socket.dart';
|
export 'src/web_socket.dart';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class ServerEntry {
|
class ServerBrowserEntry {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
@@ -9,7 +9,7 @@ class ServerEntry {
|
|||||||
final String author;
|
final String author;
|
||||||
final bool discoverable;
|
final bool discoverable;
|
||||||
|
|
||||||
ServerEntry({
|
ServerBrowserEntry({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
@@ -35,8 +35,8 @@ class ServerEntry {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static ServerEntry fromJson(Map<String, dynamic> json) {
|
static ServerBrowserEntry fromJson(Map<String, dynamic> json) {
|
||||||
return ServerEntry(
|
return ServerBrowserEntry(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
name: json['name'],
|
name: json['name'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:server_browser_backend/src/server_entry.dart';
|
import 'package:server_browser_backend/src/server_browser_entry.dart';
|
||||||
|
|
||||||
class WebSocketServer {
|
class ServerBrowserBackend {
|
||||||
static const String addEvent = 'add';
|
static const String addEvent = 'add';
|
||||||
static const String removeEvent = 'remove';
|
static const String removeEvent = 'remove';
|
||||||
|
static const String pingEvent = 'ping';
|
||||||
|
|
||||||
final Map<String, ServerEntry> _entries = {};
|
final Map<String, ServerBrowserEntry> _entries = {};
|
||||||
final Set<WebSocket> _clients = {};
|
final Set<WebSocket> _clients = {};
|
||||||
|
|
||||||
late HttpServer _server;
|
late HttpServer _server;
|
||||||
@@ -54,8 +55,11 @@ class WebSocketServer {
|
|||||||
type = data['type'];
|
type = data['type'];
|
||||||
final payload = data['data'];
|
final payload = data['data'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case pingEvent:
|
||||||
|
client.add(json.encode({"type": pingEvent}));
|
||||||
|
break;
|
||||||
case addEvent:
|
case addEvent:
|
||||||
final entry = ServerEntry.fromJson(payload);
|
final entry = ServerBrowserEntry.fromJson(payload);
|
||||||
_entries[entry.id] = entry;
|
_entries[entry.id] = entry;
|
||||||
_broadcastEvent(addEvent, entry.toJson());
|
_broadcastEvent(addEvent, entry.toJson());
|
||||||
break;
|
break;
|
||||||
|
|||||||
1
server_browser_backend/package.bat
Normal file
1
server_browser_backend/package.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dart compile exe bin/main.dart -o dist/server.exe
|
||||||
2
server_browser_backend/package.sh
Normal file
2
server_browser_backend/package.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Run me from WSL
|
||||||
|
dart compile exe bin/main.dart -o dist/server
|
||||||
@@ -5,11 +5,11 @@ import 'package:test/test.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('WebSocket Server Tests', () {
|
group('WebSocket Server Tests', () {
|
||||||
late WebSocketServer server;
|
late ServerBrowserBackend server;
|
||||||
final int testPort = 8081;
|
final int testPort = 8081;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
server = WebSocketServer();
|
server = ServerBrowserBackend();
|
||||||
await server.start(port: testPort);
|
await server.start(port: testPort);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user