Redesigned whole UI

This commit is contained in:
Alessandro Autiero
2023-02-25 01:28:36 +01:00
parent 63c7cc5c5b
commit 760e336bc0
77 changed files with 693 additions and 387816 deletions

View File

@@ -11,6 +11,7 @@ import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart' as server;
late String? username;
late GameType type;
@@ -79,12 +80,13 @@ void main(List<String> args) async {
var serverType = getServerType(result);
var host = result["server-host"] ?? serverJson["${serverType.id}_host"];
var port = result["server-port"] ?? serverJson["${serverType.id}_port"];
var started = await startServer(host, port, serverType, result["matchmaking-address"]);
var started = await startServer(host, port, serverType);
if(!started){
stderr.writeln("Cannot start server!");
return;
}
server.writeMatchmakingIp(result["matchmaking-address"]);
autoRestart = result["auto-restart"];
await startGame();
}

View File

@@ -77,8 +77,8 @@ class _RebootApplicationState extends State<RebootApplication> {
);
}
ThemeData _createTheme(Brightness brightness) {
return ThemeData(
FluentThemeData _createTheme(Brightness brightness) {
return FluentThemeData(
brightness: brightness,
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
visualDensity: VisualDensity.standard,

View File

@@ -38,7 +38,7 @@ Future<void> startGame() async {
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
}
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type))
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type, ""))
..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
}

View File

@@ -1,19 +1,17 @@
import 'dart:io';
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/embedded/server.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart';
import '../model/server_type.dart';
import '../util/server.dart';
import 'game.dart';
import '../util/server.dart' as server;
Future<bool> startServer(String? host, String? port, ServerType type, String? matchmakingIp) async {
Future<bool> startServer(String? host, String? port, ServerType type) async {
stdout.writeln("Starting backend server...");
switch(type){
case ServerType.local:
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
var result = await server.ping(host ?? "127.0.0.1", port ?? "3551");
if(result == null){
throw Exception("Local backend server is not running");
}
@@ -22,11 +20,8 @@ Future<bool> startServer(String? host, String? port, ServerType type, String? ma
return true;
case ServerType.embedded:
stdout.writeln("Starting an embedded server...");
await startEmbeddedServer(
() => matchmakingIp ?? "127.0.0.1"
);
await startEmbeddedMatchmaker();
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
await server.startServer();
var result = await server.ping(host ?? "127.0.0.1", port ?? "3551");
if(result == null){
throw Exception("Cannot start embedded server");
}
@@ -62,7 +57,7 @@ Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
}
try{
var uri = await ping(host, port);
var uri = await server.ping(host, port);
if(uri == null){
return null;
}

View File

@@ -1,9 +1,7 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
@@ -14,12 +12,13 @@ import 'package:reboot_launcher/src/model/game_type.dart';
class GameController extends GetxController {
late final GetStorage _storage;
late final TextEditingController username;
late final TextEditingController version;
late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion;
late final Rx<GameType> type;
late final HashMap<GameType, GameInstance> gameInstancesMap;
late final RxBool started;
late final RxBool autostartGameServer;
late bool updated;
late bool error;
late bool failing;
@@ -50,10 +49,16 @@ class GameController extends GetxController {
username = TextEditingController(text: _readUsername());
username.addListener(() => _storage.write("${type.value == GameType.client ? 'game' : 'host'}_username", username.text));
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? ""));
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
gameInstancesMap= HashMap();
started = RxBool(false);
autostartGameServer = RxBool(_storage.read("auto_start_game_server") ?? true);
autostartGameServer.listen((value) => _storage.write("auto_start_game_server", value));
updated = false;
error = false;
@@ -95,10 +100,10 @@ class GameController extends GetxController {
bool get hasNoVersions => versions.value.isEmpty;
Rxn<FortniteVersion> get selectedVersionObs => _selectedVersion;
GameInstance? get currentGameInstance => gameInstancesMap[type()];
FortniteVersion? get selectedVersion => _selectedVersion();
set selectedVersion(FortniteVersion? version) {
_selectedVersion(version);
_storage.write("version", version?.name);

View File

@@ -6,6 +6,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:jaguar/jaguar.dart';
import '../model/server_type.dart';
import '../util/server.dart';
class ServerController extends GetxController {
static const String _serverName = "127.0.0.1";
@@ -17,13 +18,14 @@ class ServerController extends GetxController {
late final Rx<ServerType> type;
late final RxBool warning;
late RxBool started;
Jaguar? embeddedServer;
Jaguar? embeddedMatchmaker;
late RxBool loginAutomatically;
HttpServer? remoteServer;
ServerController() {
_storage = GetStorage("server");
started = RxBool(false);
loginAutomatically = RxBool(_storage.read("login_automatically") ?? false);
loginAutomatically.listen((value) => _storage.write("login_automatically", value));
type = Rx(ServerType.values.elementAt(_storage.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
@@ -35,17 +37,12 @@ class ServerController extends GetxController {
stop();
});
host = TextEditingController(text: _readHost());
host.addListener(() => _storage.write("${type.value.id}_host", host.text));
port = TextEditingController(text: _readPort());
port.addListener(() => _storage.write("${type.value.id}_port", port.text));
warning = RxBool(_storage.read("lawin_value") ?? true);
warning.listen((value) => _storage.write("lawin_value", value));
started = RxBool(false);
}
String _readHost() {
@@ -63,8 +60,7 @@ class ServerController extends GetxController {
try{
switch(type()){
case ServerType.embedded:
await embeddedServer?.close();
await embeddedMatchmaker?.close();
stopServer();
break;
case ServerType.remote:
await remoteServer?.close(force: true);

View File

@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/model/tutorial_page.dart';
import 'package:ini/ini.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'dart:ui';
import '../util/reboot.dart';
@@ -15,12 +18,7 @@ class SettingsController extends GetxController {
late final TextEditingController consoleDll;
late final TextEditingController authDll;
late final TextEditingController matchmakingIp;
late final Rx<PaneDisplayMode> displayType;
late final RxBool automaticallyStartMatchmaker;
late final RxBool doNotAskAgain;
late final RxBool advancedMode;
late final RxBool autoUpdate;
late Rx<TutorialPage> tutorialPage;
late double width;
late double height;
late double? offsetX;
@@ -43,29 +41,18 @@ class SettingsController extends GetxController {
matchmakingIp.addListener(() async {
var text = matchmakingIp.text;
_storage.write("ip", text);
writeMatchmakingIp(text);
});
automaticallyStartMatchmaker = RxBool(_storage.read("start_matchmaker_automatically") ?? false);
automaticallyStartMatchmaker.listen((value) => _storage.write("start_matchmaker_automatically", value));
doNotAskAgain = RxBool(_storage.read("do_not_ask_again") ?? false);
doNotAskAgain.listen((value) => _storage.write("do_not_ask_again", value));
width = _storage.read("width") ?? window.physicalSize.width;
height = _storage.read("height") ?? window.physicalSize.height;
width = _storage.read("width") ?? 912;
height = _storage.read("height") ?? 660;
offsetX = _storage.read("offset_x");
offsetY = _storage.read("offset_y");
advancedMode = RxBool(_storage.read("advanced") ?? false);
advancedMode.listen((value) async => _storage.write("advanced", value));
autoUpdate = RxBool(_storage.read("auto_update") ?? false);
autoUpdate.listen((value) async => _storage.write("auto_update", value));
displayType = Rx(PaneDisplayMode.top);
scrollingDistance = 0.0;
tutorialPage = Rx(TutorialPage.start);
}
TextEditingController _createController(String key, String name) {

View File

@@ -40,7 +40,6 @@ class AddLocalVersion extends StatelessWidget {
),
FileSelector(
label: "Location",
placeholder: "Type the game folder",
windowTitle: "Select game folder",
controller: _gamePathController,

View File

@@ -273,7 +273,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
VersionNameInput(controller: _nameController),
const SizedBox(height: 16.0),
FileSelector(
label: "Destination",
placeholder: "Type the download destination",
windowTitle: "Select download destination",
controller: _pathController,

View File

@@ -6,14 +6,12 @@ import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/embedded/server.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:sync/semaphore.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../main.dart';
import '../page/home_page.dart';
import '../util/server.dart';
extension ServerControllerDialog on ServerController {
@@ -81,10 +79,7 @@ extension ServerControllerDialog on ServerController {
try{
switch(type()){
case ServerType.embedded:
embeddedServer = await startEmbeddedServer(
() => Get.find<SettingsController>().matchmakingIp.text,
);
embeddedMatchmaker = await startEmbeddedMatchmaker();
startServer();
break;
case ServerType.remote:
var uriResult = await _pingRemoteInteractive();

View File

@@ -1,333 +0,0 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:jaguar/http/context/context.dart';
import 'package:reboot_launcher/src/embedded/utils.dart';
import '../util/os.dart';
final Directory _profiles = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\profiles");
const String _token = "reboot_token";
const String _clientId = "reboot_client";
const String _device = "reboot_device";
const String _sessionId = "3c3662bcb661d6de679c636744c66b62";
List<Map<String, Object>> getAccounts(Context context) {
return context.query.getList("accountId").map(getAccount).toList();
}
Map<String, Object> getAccount(String account) {
return {"id": account, "displayName": _parseUsername(account), "externalAuths": {}};
}
Map<String, Object> getAccountInfo(Context context) {
var usernameId = context.pathParams.get("accountId")!;
var accountName = _parseUsername(usernameId);
return {
"id": usernameId,
"displayName": accountName,
"name": "Reboot",
"email": usernameId,
"failedLoginAttempts": 0,
"lastLogin": "2022-11-08T18:55:52.341Z",
"numberOfDisplayNameChanges": 0,
"ageGroup": "UNKNOWN",
"headless": false,
"country": "US",
"lastName": "Server",
"preferredLanguage": "en",
"canUpdateDisplayName": false,
"tfaEnabled": false,
"emailVerified": true,
"minorVerified": false,
"minorExpected": false,
"minorStatus": "UNKNOWN"
};
}
List<Map<String, Object>> getExternalAuths(Context context) => [];
Future<Map<String, Object>> getOAuthToken(Context context) async {
var usernameId = await _getUsername(context);
var accountName = _parseUsername(usernameId);
return {
"access_token": _token,
"expires_in": 28800,
"expires_at": "9999-12-02T01:12:01.100Z",
"token_type": "bearer",
"refresh_token": _token,
"refresh_expires": 86400,
"refresh_expires_at": "9999-12-02T01:12:01.100Z",
"account_id": usernameId,
"client_id": _clientId,
"internal_client": true,
"client_service": "fortnite",
"displayName": accountName,
"app": "fortnite",
"in_app_id": usernameId,
"device_id": _device
};
}
Future<String> _getUsername(Context context) async {
var params = await parseBody(context);
var username = params["username"];
return username ?? "unknown@projectreboot.dev";
}
Map<String, Object> verifyOAuthToken(Context context) {
return {
"token": _token,
"session_id": _sessionId,
"token_type": "bearer",
"client_id": _clientId,
"internal_client": true,
"client_service": "fortnite",
"account_id": "unknown",
"expires_in": 28800,
"expires_at": "9999-12-02T01:12:01.100Z",
"auth_method": "exchange_code",
"display_name": "unknown",
"app": "fortnite",
"in_app_id": "unknown",
"device_id": _device
};
}
List<Map<String, Object>> getExchange(Context context) => [];
List<String> getSsoDomains(Context context) => [
"unrealengine.com",
"unrealtournament.com",
"fortnite.com",
"epicgames.com"
];
String tryPlayOnPlatform(Context context) => "true";
List<Map<String, Object>> getFeatures(Context context) => [];
Map<String, Object?> getProfile(Context context){
var profileId = context.query.get("profileId");
if (profileId == null) {
return {"Error": "Profile not defined."};
}
var profileJson = _getProfileJson(profileId, context);
var profileFile = _getProfileFile(context);
var baseRevision = profileJson["rvn"] ?? 0;
var queryRevision = context.query.getInt("rvn") ?? -1;
var profileChanges = _getFullProfileUpdate(context, profileId, profileJson, queryRevision, baseRevision);
if(profileId == "athena" && !profileFile.existsSync()) {
profileFile.writeAsStringSync(json.encode(profileJson), flush: true);
}
return {
"profileRevision": baseRevision,
"profileId": profileId,
"profileChangesBaseRevision": baseRevision,
"profileChanges": profileChanges,
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
"serverTime": "2022-11-08T18:55:52.341Z",
"responseVersion": 1
};
}
Map<String, dynamic> _getProfileJson(String profileId, Context context) {
if(profileId == "athena"){
var profile = _getProfileFile(context);
if(profile.existsSync()){
return json.decode(profile.readAsStringSync());
}
var body = loadEmbedded("profiles/$profileId.json").readAsStringSync();
return json.decode(body);
}
var profileJson = json.decode(loadEmbedded("profiles/$profileId.json").readAsStringSync());
return profileJson;
}
Future<Map<String, Object>> equipItem(Context context) async {
var profileFile = _getProfileFile(context);
var profileJson = json.decode(profileFile.readAsStringSync());
var baseRevision = profileJson["rvn"] ?? 0;
var queryRevision = context.query.getInt("rvn") ?? -1;
var body = json.decode(utf8.decode(await context.body));
var variant = _getReturnVariant(body, profileJson);
var change = _getStatsChanged(body, profileJson);
var profileChanges = _getProfileChanges(queryRevision, baseRevision, profileJson, change, body, variant);
profileFile.writeAsStringSync(json.encode(profileJson));
return {
"profileRevision": baseRevision,
"profileId": "athena",
"profileChangesBaseRevision": baseRevision,
"profileChanges": profileChanges,
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
"serverTime": "2022-11-08T18:55:52.341Z",
"responseVersion": 1
};
}
List<dynamic> _getProfileChanges(int queryRevision, baseRevision, profileJson, bool change, body, bool variant) {
var changes = [];
if (change) {
var category = ("favorite_${body["slotName"] ?? "character"}")
.toLowerCase();
if (category == "favorite_itemwrap") {
category += "s";
}
profileJson["rvn"] = (profileJson["rvn"] ?? 0) + 1;
profileJson["commandRevision"] = (profileJson["commandRevision"] ?? 0) + 1;
changes.add({
"changeType": "statModified",
"name": category,
"value": profileJson["stats"]["attributes"][category]
});
if (variant) {
changes.add({
"changeType": "itemAttrChanged",
"itemId": body["itemToSlot"],
"attributeName": "variants",
"attributeValue": profileJson["items"][body["itemToSlot"]]["attributes"]["variants"]
});
}
}
if(queryRevision != baseRevision){
return [{
"changeType": "fullProfileUpdate",
"profile": profileJson
}];
}
return changes;
}
bool _getStatsChanged(body, profileJson) {
var slotName = body["slotName"];
if (slotName == null) {
return false;
}
switch (slotName) {
case "Character":
profileJson["stats"]["attributes"]["favorite_character"] =
body["itemToSlot"] ?? "";
return true;
case "Backpack":
profileJson["stats"]["attributes"]["favorite_backpack"] =
body["itemToSlot"] ?? "";
return true;
case "Pickaxe":
profileJson["stats"]["attributes"]["favorite_pickaxe"] =
body["itemToSlot"] ?? "";
return true;
case "Glider":
profileJson["stats"]["attributes"]["favorite_glider"] =
body["itemToSlot"] ?? "";
return true;
case "SkyDiveContrail":
profileJson["stats"]["attributes"]["favorite_skydivecontrail"] =
body["itemToSlot"] ?? "";
return true;
case "MusicPack":
profileJson["stats"]["attributes"]["favorite_musicpack"] =
body["itemToSlot"] ?? "";
return true;
case "LoadingScreen":
profileJson["stats"]["attributes"]["favorite_loadingscreen"] =
body["itemToSlot"] ?? "";
return true;
case "Dance":
var index = body["indexWithinSlot"] ?? 0;
if (index >= 0) {
profileJson["stats"]["attributes"]["favorite_dance"][index] =
body["itemToSlot"] ?? "";
}
return true;
case "ItemWrap":
var index = body["indexWithinSlot"] ?? 0;
if (index < 0) {
for (var i = 0; i < 7; i++) {
profileJson["stats"]["attributes"]["favorite_itemwraps"][i] =
body["itemToSlot"] ?? "";
}
} else {
profileJson["stats"]["attributes"]["favorite_itemwraps"][index] =
body["itemToSlot"] ?? "";
}
return true;
default:
return false;
}
}
bool _getReturnVariant(body, profileJson) {
var variantUpdates = body["variantUpdates"] ?? [];
if(!variantUpdates.toString().contains("active")){
return false;
}
try {
var variantJson = profileJson["items"][body["itemToSlot"]]["attributes"]["variants"] ?? [];
if (variantJson.isEmpty) {
variantJson = variantUpdates;
}
for (var i in variantJson) {
try {
if (variantJson[i]["channel"].toLowerCase() == body["variantUpdates"][i]["channel"].toLowerCase()) {
profileJson["items"][body["itemToSlot"]]["attributes"]["variants"][i]["active"] = body["variantUpdates"][i]["active"] ?? "";
}
} catch (_) {
// Ignored
}
}
return true;
} catch (_) {
// Ignored
}
return false;
}
List<Map<String, Object?>> _getFullProfileUpdate(Context context, String profileName, Map<String, dynamic> profileJson, int queryRevision, int baseRevision) {
if (queryRevision == baseRevision) {
return [];
}
if (profileName == "athena") {
var season = parseSeason(context);
profileJson["stats"]["attributes"]["season_num"] = season;
profileJson["stats"]["attributes"]["book_purchased"] = true;
profileJson["stats"]["attributes"]["book_level"] = 100;
profileJson["stats"]["attributes"]["season_match_boost"] = 100;
profileJson["stats"]["attributes"]["season_friend_match_boost"] = 100;
}
return [{
"changeType": "fullProfileUpdate",
"profile": profileJson
}];
}
String _parseUsername(String username) =>
username.contains("@") ? username.split("@")[0] : username;
File _getProfileFile(Context context) {
if(!_profiles.existsSync()){
_profiles.createSync(recursive: true);
}
return File("${_profiles.path}\\ClientProfile.json");
}

View File

@@ -1,43 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:jaguar/jaguar.dart';
class EmbeddedErrorWriter extends ErrorWriter {
static const String _errorName404 = "errors.com.lawinserver.common.not_found";
static const String _errorName500 = "errors.com.lawinserver.common.error";
static const String _errorCode = "1004";
@override
FutureOr<Response> make404(Context ctx) {
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
ctx.response.headers.set('X-Epic-Error-Name', _errorName404);
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
return Response.json(
statusCode: 204,
{
"errorCode": _errorName404,
"errorMessage": "Sorry the resource you were trying to find could not be found",
"numericErrorCode": _errorCode,
"originatingService": "any",
"intent": "prod"
}
);
}
@override
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
ctx.response.headers.set('X-Epic-Error-Name', _errorName500);
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
return Response.json(
statusCode: 500,
{
"errorCode": _errorName500,
"errorMessage": "Sorry the resource you were trying to find threw an error",
"numericErrorCode": _errorCode,
"originatingService": "any",
"intent": "prod"
}
);
}
}

View File

@@ -1,33 +0,0 @@
import 'package:jaguar/http/context/context.dart';
Map<String, Object?> getFortniteStatus(Context context) => {
"serviceInstanceId": "fortnite",
"status": "UP",
"message": "Fortnite is online",
"maintenanceUri": null,
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
"allowedActions": [],
"banned": false,
"launcherInfoDTO": {
"appName": "Fortnite",
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
"namespace": "fn"
}
};
List<Map<String, Object?>> getBulkStatus(Context context) => [
{
"serviceInstanceId": "fortnite",
"status": "UP",
"message": "fortnite is up.",
"maintenanceUri": null,
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
"allowedActions": ["PLAY", "DOWNLOAD"],
"banned": false,
"launcherInfoDTO": {
"appName": "Fortnite",
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
"namespace": "fn"
}
}
];

View File

@@ -1,144 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:jaguar/http/context/context.dart';
import 'package:uuid/uuid.dart';
import 'package:jaguar/jaguar.dart';
String _build = "0";
String? _customIp;
Map<String, Object> getPlayerTicket(Context context){
var bucketId = context.query.get("bucketId");
if(bucketId == null){
return {"Error": "Missing bucket id"};
}
_build = bucketId.split(":")[0];
_customIp = context.query.get("player.option.customKey");
return {
"serviceUrl": "ws://127.0.0.1:8080",
"ticketType": "mms-player",
"payload": "69=",
"signature": "420="
};
}
Map<String, Object?> getSessionAccount(Context context) => {
"accountId": context.pathParams.get("accountId"),
"sessionId": context.pathParams.get("sessionId"),
"key": "AOJEv8uTFmUh7XM2328kq9rlAzeQ5xzWzPIiyKn2s7s="
};
Future<Map<String, Object?>> getMatch(Context context, String Function() ipQuery) async {
var ipAndPort = _customIp ?? ipQuery().trim();
var ip = ipAndPort.contains(":") ? ipAndPort.split(":")[0] : ipAndPort;
var port = ipAndPort.contains(":") ? int.parse(ipAndPort.split(":")[1]) : 7777;
return {
"id": context.pathParams.get("sessionId"),
"ownerId": _randomUUID(),
"ownerName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
"serverName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
"serverAddress": ip,
"serverPort": port,
"maxPublicPlayers": 220,
"openPublicPlayers": 175,
"maxPrivatePlayers": 0,
"openPrivatePlayers": 0,
"attributes": {
"REGION_s": "EU",
"GAMEMODE_s": "FORTATHENA",
"ALLOWBROADCASTING_b": true,
"SUBREGION_s": "GB",
"DCID_s": "FORTNITE-LIVEEUGCEC1C2E30UBRCORE0A-14840880",
"tenant_s": "Fortnite",
"MATCHMAKINGPOOL_s": "Any",
"STORMSHIELDDEFENSETYPE_i": 0,
"HOTFIXVERSION_i": 0,
"PLAYLISTNAME_s": "Playlist_DefaultSolo",
"SESSIONKEY_s": _randomUUID(),
"TENANT_s": "Fortnite",
"BEACONPORT_i": 15009
},
"publicPlayers": [],
"privatePlayers": [],
"totalPlayers": 45,
"allowJoinInProgress": false,
"shouldAdvertise": false,
"isDedicated": false,
"usesStats": false,
"allowInvites": false,
"usesPresence": false,
"allowJoinViaPresence": true,
"allowJoinViaPresenceFriendsOnly": false,
"buildUniqueId": _build,
"lastUpdated": "2022-11-08T18:55:52.341Z",
"started": false
};
}
List<Map<String, Object>> getMatchmakingRequests() => [];
void queueMatchmaking(WebSocket ws) {
var now = DateTime.now();
var ticketId = md5.convert(utf8.encode("1$now")).toString();
var matchId = md5.convert(utf8.encode("2$now")).toString();
var sessionId = md5.convert(utf8.encode("3$now")).toString();
ws.addUtf8Text(utf8.encode(
jsonEncode({
"payload": {
"state": "Connecting"
},
"name": "StatusUpdate"
})
));
ws.addUtf8Text(utf8.encode(
jsonEncode({
"payload": {
"totalPlayers": 1,
"connectedPlayers": 1,
"state": "Waiting"
},
"name": "StatusUpdate"
})
));
ws.addUtf8Text(utf8.encode(
jsonEncode({
"payload": {
"ticketId": ticketId,
"queuedPlayers": 0,
"estimatedWaitSec": 0,
"status": {},
"state": "Queued"
},
"name": "StatusUpdate"
})
));
ws.addUtf8Text(utf8.encode(
jsonEncode({
"payload": {
"matchId": matchId,
"state": "SessionAssignment"
},
"name": "StatusUpdate"
})
));
ws.addUtf8Text(utf8.encode(
jsonEncode({
"payload": {
"matchId": matchId,
"sessionId": sessionId,
"joinDelaySec": 1
},
"name": "Play"
})
));
}
String _randomUUID() => const Uuid().v4().replaceAll("-", "").toUpperCase();

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
import 'package:jaguar/http/context/context.dart';
import 'package:reboot_launcher/src/embedded/utils.dart';
Map<String, Object?> getPrivacy(Context context) => {
"accountId": context.pathParams.get("accountId"),
"optOutOfPublicLeaderboards": false
};
Future<Map<String, Object?>> postPrivacy(Context context) async {
var body = await parseBody(context);
return {
"accountId": context.pathParams.get("accountId"),
"optOutOfPublicLeaderboards": body["optOutOfPublicLeaderboards"]
};
}

View File

@@ -1,120 +0,0 @@
import "dart:async";
import "dart:io";
import "package:jaguar/jaguar.dart";
import "package:reboot_launcher/src/embedded/auth.dart";
import 'package:reboot_launcher/src/embedded/misc.dart';
import 'package:reboot_launcher/src/embedded/privacy.dart';
import "package:reboot_launcher/src/embedded/storage.dart";
import 'package:reboot_launcher/src/embedded/storefront.dart';
import "package:reboot_launcher/src/embedded/version.dart";
import '../util/server.dart';
import "error.dart";
import "lightswitch.dart";
import 'matchmaking.dart';
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
var server = Jaguar(port: 3551, errorWriter: EmbeddedErrorWriter());
// Version
server.getJson("unknown", (context) => Response(body: "lawinserver"));
server.getJson("/fortnite/api/version", getVersion);
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
server.getJson("/fortnite/api/versioncheck*", hasUpdate);
// Auth
server.getJson("/account/api/public/account/displayName/:accountId", getAccountInfo);
server.getJson("/account/api/public/account/:accountId", getAccountInfo);
server.getJson("/account/api/public/account/:accountId/externalAuths", getExternalAuths);
server.getJson("/account/api/public/account", getAccounts);
server.delete("/account/api/oauth/sessions/kill/*", (context) => Response(statusCode: 204));
server.getJson("/account/api/oauth/verify", verifyOAuthToken);
server.postJson("/account/api/oauth/token", getOAuthToken);
server.postJson("/account/api/oauth/exchange", getExchange);
server.getJson("/account/api/epicdomains/ssodomains", getSsoDomains);
server.post("/fortnite/api/game/v2/tryPlayOnPlatform/account/*", tryPlayOnPlatform);
server.post("/datarouter/api/v1/public/data/*", (context) => Response(statusCode: 204));
server.getJson("/fortnite/api/game/v2/enabled_features", getFeatures);
server.postJson("/fortnite/api/game/v2/grant_access/*", (context) => Response(statusCode: 204));
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/EquipBattleRoyaleCustomization", equipItem);
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/*", getProfile);
// Storage
server.getJson("/fortnite/api/cloudstorage/system", getStorageSettings);
server.get("/fortnite/api/cloudstorage/system/:file", getStorageSetting);
server.getJson("/fortnite/api/cloudstorage/user/:accountId", getStorageAccount);
server.getJson("/fortnite/api/cloudstorage/user/:accountId/:file", getStorageFile);
server.put("/fortnite/api/cloudstorage/user/:accountId/:file", addStorageFile);
// Status
server.getJson("/lightswitch/api/service/Fortnite/status", getFortniteStatus);
server.getJson("/lightswitch/api/service/bulk/status", getBulkStatus);
// Keychain and catalog
server.get("/fortnite/api/storefront/v2/catalog", getCatalog);
server.get("/fortnite/api/storefront/v2/keychain", getKeyChain);
server.get("/catalog/api/shared/bulk/offers", getOffers);
// Matchmaking
server.get("/fortnite/api/matchmaking/session/findPlayer/*", (context) => Response(statusCode: 200));
server.getJson("/fortnite/api/game/v2/matchmakingservice/ticket/player/*", getPlayerTicket);
server.getJson("/fortnite/api/game/v2/matchmaking/account/:accountId/session/:sessionId", getSessionAccount);
server.getJson("/fortnite/api/matchmaking/session/:sessionId", (context) => getMatch(context, ipQuery));
server.post("/fortnite/api/matchmaking/session/:accountId/join", (context) => Response(statusCode: 204));
server.postJson("/fortnite/api/matchmaking/session/matchMakingRequest", (context) => getMatchmakingRequests);
// Misc
server.getJson("/api/v1/events/Fortnite/download/*", getDownload);
server.getJson("/fortnite/api/receipts/v1/account/:accountId/receipts", getReceipts);
server.getJson("/content/api/pages/*", getContentPages);
server.getJson("/friends/api/v1/:accountId/settings", getFriendsSettings);
server.getJson("/friends/api/v1/:accountId/blocklist", getFriendsBlocklist);
server.getJson("/friends/api/public/blocklist/:accountId", getFriendsBlocklist);
server.getJson("/friends/api/public/friends/:accountId", getFriendsList);
server.getJson("/friends/api/public/list/fortnite/:accountId/recentPlayers", getRecentPlayers);
server.getJson("/fortnite/api/calendar/v1/timeline", getTimeline);
server.getJson("/fortnite/api/game/v2/events/tournamentandhistory/:accountId/EU/WindowsClient", getTournamentHistory);
server.get("/waitingroom/api/waitingroom", (context) => Response(statusCode: 204));
server.postJson("/api/v1/user/setting", (context) => []);
server.getJson("/eulatracking/api/public/agreements/fn/account/*", (context) => Response(statusCode: 204));
server.getJson("/socialban/api/public/v1/:accountId", getSocialBan);
server.getJson("/party/api/v1/Fortnite/user/*", getParty);
server.getJson("/friends/api/v1/*/settings", (context) => {});
server.getJson("/friends/api/v1/*/blocklist", (context) => {});
server.getJson("/friends/api/public/friends", (context) => []);
server.getJson("/friends/api/v1/:accountId/summary", (context) => []);
server.getJson("/friends/api/public/list/fortnite/*/recentPlayers", (context) => []);
server.getJson("/friends/api/public/blocklist/*", getBlockedFriends);
// Privacy
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
await server.serve(logRequests: true);
server.log.onRecord.listen((line) {
stdout.writeln(line);
serverLogFile.writeAsString("$line\n", mode: FileMode.append);
});
server.onException.add((ctx, exception, trace) {
stderr.writeln("An error occurred: $exception");
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
});
return server;
}
Future<Jaguar> startEmbeddedMatchmaker() async {
var server = Jaguar(port: 8080);
WebSocket? ws;
server.wsStream(
"/",
(_, input) => ws = input,
after: [(_) => queueMatchmaking(ws!)]
);
await server.serve(logRequests: true);
return server;
}

View File

@@ -1,96 +0,0 @@
import 'package:path/path.dart' as path;
import 'dart:io';
import 'package:jaguar/jaguar.dart';
import 'package:jaguar/http/context/context.dart';
import 'package:crypto/crypto.dart';
import 'package:reboot_launcher/src/embedded/utils.dart';
import '../util/os.dart';
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
List getStorageSettings(Context context) =>
loadEmbeddedDirectory("config")
.listSync()
.map((e) => File(e.path))
.map(_getStorageSetting)
.toList();
Map<String, Object> _getStorageSetting(File file){
var name = path.basename(file.path);
var bytes = file.readAsBytesSync();
return {
"uniqueFilename": name,
"filename": name,
"hash": sha1.convert(bytes).toString(),
"hash256": sha256.convert(bytes).toString(),
"length": bytes.length,
"contentType": "application/octet-stream",
"uploaded": "2020-02-23T18:35:53.967Z",
"storageType": "S3",
"storageIds": {},
"doNotCache": true
};
}
Response getStorageSetting(Context context) {
var file = loadEmbedded("config\\${context.pathParams.get("file")}");
return Response(body: file.readAsStringSync());
}
Response getStorageFile(Context context) {
if (context.pathParams.get("file")?.toLowerCase() != "clientsettings.sav") {
return Response.json(
{"error": "File not found"},
statusCode: 404
);
}
var file = _getSettingsFile(context);
return Response(
body: file.existsSync() ? file.readAsBytesSync() : null,
headers: {"content-type": "application/octet-stream"}
);
}
List<Map<String, Object?>> getStorageAccount(Context context) {
var file = _getSettingsFile(context);
if (!file.existsSync()) {
return [];
}
var content = file.readAsBytesSync();
return [{
"uniqueFilename": "ClientSettings.Sav",
"filename": "ClientSettings.Sav",
"hash": sha1.convert(content).toString(),
"hash256": sha256.convert(content).toString(),
"length": content.length,
"contentType": "application/octet-stream",
"uploaded": "2020-02-23T18:35:53.967Z",
"storageType": "S3",
"storageIds": {},
"accountId": context.pathParams.get("accountId"),
"doNotCache": true
}];
}
Future<Response> addStorageFile(Context context) async {
if(!_settings.existsSync()){
await _settings.create(recursive: true);
}
var file = _getSettingsFile(context);
await file.writeAsBytes(await context.body);
return Response(statusCode: 204);
}
File _getSettingsFile(Context context) {
if(!_settings.existsSync()){
_settings.createSync(recursive: true);
}
return File("${_settings.path}\\ClientSettings.Sav");
}

View File

@@ -1,19 +0,0 @@
import 'package:jaguar/http/context/context.dart';
import 'package:jaguar/http/response/response.dart';
import 'package:reboot_launcher/src/util/os.dart';
final String _keyChain = loadEmbedded("responses/keychain.json").readAsStringSync();
final String _catalog = loadEmbedded("responses/catalog.json").readAsStringSync();
Response getCatalog(Context context) {
if (context.headers.value("user-agent")?.contains("2870186") == true) {
return Response(statusCode: 404);
}
return Response(body: _catalog, headers: {"content-type": "application/json"});
}
Response getKeyChain(Context context) => Response(body: _keyChain, headers: {"content-type": "application/json"});
Map<String, Object> getOffers(Context context) => {};

View File

@@ -1,42 +0,0 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:math';
import 'package:jaguar/http/context/context.dart';
const String _chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final Random _random = Random();
String randomString(int length) => String.fromCharCodes(
Iterable.generate(length, (_) => _chars.codeUnitAt(_random.nextInt(_chars.length))));
double parseSeasonBuild(Context context){
String? userAgent = context.headers.value("user-agent");
if (userAgent == null) {
return 1.0;
}
try {
var build = userAgent.split("Release-")[1].split("-")[0];
if (build.split(".").length == 3) {
var value = build.split(".");
return double.parse("${value[0]}.${value[1]}${value[2]}");
}
return double.parse(build);
} catch (_) {
return 2.0;
}
}
int parseSeason(Context context) => int.parse(parseSeasonBuild(context).toString().split(".")[0]);
Future<HashMap<String, String?>> parseBody(Context context) async {
var params = HashMap<String, String?>();
utf8.decode(await context.req.body)
.split("&")
.map((entry) => MapEntry(entry.substring(0, entry.indexOf("=")), entry.substring(entry.indexOf("=") + 1)))
.forEach((element) => params[element.key] = Uri.decodeQueryComponent(element.value));
return params;
}

View File

@@ -1,41 +0,0 @@
import 'package:jaguar/http/context/context.dart';
import 'package:reboot_launcher/src/util/time.dart';
Map<String, Object> getVersion(Context context) => {
"app": "fortnite",
"serverDate": "2022-11-08T18:55:52.341Z",
"overridePropertiesVersion": "unknown",
"cln": "17951730",
"build": "444",
"moduleName": "Fortnite-Core",
"buildDate": "2021-10-27T21:00:51.697Z",
"version": "18.30",
"branch": "Release-18.30",
"modules": {
"Epic-LightSwitch-AccessControlCore": {
"cln": "17237679",
"build": "b2130",
"buildDate": "2021-08-19T18:56:08.144Z",
"version": "1.0.0",
"branch": "trunk"
},
"epic-xmpp-api-v1-base": {
"cln": "5131a23c1470acbd9c94fae695ef7d899c1a41d6",
"build": "b3595",
"buildDate": "2019-07-30T09:11:06.587Z",
"version": "0.0.1",
"branch": "master"
},
"epic-common-core": {
"cln": "17909521",
"build": "3217",
"buildDate": "2021-10-25T18:41:12.486Z",
"version": "3.0",
"branch": "TRUNK"
}
}
};
Map<String, Object> hasUpdate(Context context) => {
"type": "NO_UPDATE"
};

View File

@@ -1,12 +1,15 @@
import 'dart:io';
import 'game_type.dart';
class GameInstance {
final Process gameProcess;
final Process? launcherProcess;
final Process? eacProcess;
bool tokenError;
bool hasChildServer;
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess)
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.hasChildServer)
: tokenError = false;
void kill() {

View File

@@ -19,14 +19,8 @@ enum GameType {
}
String get name {
return this == GameType.client ? "Client"
: this == GameType.server ? "Server"
: "Headless Server";
}
String get message {
return this == GameType.client ? "A fortnite client will be launched to play multiplayer games"
: this == GameType.server ? "A fortnite client will be launched to host multiplayer games"
: "A fortnite client will be launched in the background to host multiplayer games";
return this == GameType.client ? "Game client"
: this == GameType.server ? "Game server"
: "Headless game server";
}
}

View File

@@ -19,7 +19,7 @@ enum ServerType {
}
String get name {
return this == ServerType.embedded ? "Embedded"
return this == ServerType.embedded ? "Embedded (Lawin)"
: this == ServerType.remote ? "Remote"
: "Local";
}

View File

@@ -1,5 +0,0 @@
enum TutorialPage {
start,
someoneElse,
yourOwn
}

View File

@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
@@ -19,7 +17,6 @@ import 'package:window_manager/window_manager.dart';
import '../controller/settings_controller.dart';
import '../model/server_type.dart';
import '../model/tutorial_page.dart';
import 'info_page.dart';
class HomePage extends StatefulWidget {
@@ -30,10 +27,7 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State<HomePage> with WindowListener {
static const double _headerSize = 48.0;
static const double _sectionSize = 100.0;
static const double _defaultPadding = 12.0;
static const int _headerButtonCount = 3;
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
@@ -46,18 +40,12 @@ class _HomePageState extends State<HomePage> with WindowListener {
final RxBool _focused = RxBool(true);
final RxInt _index = RxInt(0);
bool _navigated = false;
bool _shouldMaximize = false;
@override
void initState() {
windowManager.addListener(this);
_searchController.addListener(_onSearch);
_onEasyMode();
_settingsController.advancedMode.listen((advanced) {
_onEasyMode();
_index.value = _index.value + (advanced ? 1 : -1);
});
super.initState();
}
@@ -73,15 +61,6 @@ class _HomePageState extends State<HomePage> with WindowListener {
.cast<NavigationPaneItem>();
}
void _onEasyMode() {
if(_settingsController.advancedMode.value){
return;
}
_gameController.type.value = GameType.client;
_serverController.type.value = ServerType.embedded;
}
@override
void dispose() {
windowManager.removeListener(this);
@@ -100,6 +79,12 @@ class _HomePageState extends State<HomePage> with WindowListener {
_focused.value = false;
}
@override
void onWindowResized() {
_settingsController.saveWindowSize();
super.onWindowResized();
}
@override
void onWindowMoved() {
_settingsController.saveWindowOffset(appWindow.position);
@@ -131,52 +116,52 @@ class _HomePageState extends State<HomePage> with WindowListener {
),
],
);
},
}
);
}
@override
Widget build(BuildContext context) {
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
return _calculateSize();
},
child: SizeChangedLayoutNotifier(
child: Obx(_getViewStack)
)
);
}
Widget _getViewStack() {
var view = _createNavigationView();
return Stack(
children: [
view,
if(_settingsController.displayType() == PaneDisplayMode.top)
Align(
alignment: Alignment.topRight,
child: WindowTitleBar(focused: _focused())
Widget build(BuildContext context) => Obx(() => Stack(
children: [
NavigationView(
paneBodyBuilder: (body) => Padding(
padding: const EdgeInsets.all(_defaultPadding),
child: body
),
if(_settingsController.displayType() == PaneDisplayMode.top)
_createTopDisplayGestures(view.pane?.items.length ?? 0),
if(_focused() && isWin11)
const WindowBorder()
]
);
appBar: NavigationAppBar(
title: _draggableArea,
actions: WindowTitleBar(focused: _focused())
),
pane: NavigationPane(
selected: _selectedIndex,
onChanged: _onIndexChanged,
displayMode: PaneDisplayMode.auto,
items: _items,
footerItems: _footerItems,
autoSuggestBox: _autoSuggestBox,
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child
),
if(_focused() && isWin11)
const WindowBorder()
]
));
void _onIndexChanged(int index) {
_index.value = index;
_navigated = true;
}
Padding _createTopDisplayGestures(int size) => Padding(
padding: EdgeInsets.only(
left: _sectionSize * size,
right: _headerSize * _headerButtonCount,
),
child: SizedBox(
height: _headerSize,
child: _createWindowGestures()
)
TextBox get _autoSuggestBox => TextBox(
key: _searchKey,
controller: _searchController,
placeholder: 'Search',
focusNode: _searchFocusNode
);
GestureDetector _createWindowGestures({Widget? child}) => GestureDetector(
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: () {
if(!_shouldMaximize){
return;
@@ -187,89 +172,9 @@ class _HomePageState extends State<HomePage> with WindowListener {
},
onDoubleTapDown: (details) => _shouldMaximize = true,
onHorizontalDragStart: (event) => appWindow.startDragging(),
onVerticalDragStart: (event) => appWindow.startDragging(),
child: child
onVerticalDragStart: (event) => appWindow.startDragging()
);
NavigationView _createNavigationView() {
return NavigationView(
paneBodyBuilder: (body) => _createPage(body),
pane: NavigationPane(
size: const NavigationPaneSize(
topHeight: _headerSize
),
selected: _selectedIndex,
onChanged: _onIndexChanged,
displayMode: _settingsController.displayType(),
items: _createItems(),
indicator: const EndNavigationIndicator(),
footerItems: _createFooterItems(),
header: _settingsController.displayType() != PaneDisplayMode.open ? null : const SizedBox(height: _defaultPadding),
autoSuggestBox: _createAutoSuggestBox(),
autoSuggestBoxReplacement: _settingsController.displayType() == PaneDisplayMode.top ? null : const Icon(FluentIcons.search),
),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: _settingsController.displayType() == PaneDisplayMode.top ? null : (child, animation) => child
);
}
void _onIndexChanged(int index) {
_index.value = index;
_navigated = true;
}
TextBox? _createAutoSuggestBox() {
if (_settingsController.displayType() == PaneDisplayMode.top) {
return null;
}
return TextBox(
key: _searchKey,
controller: _searchController,
placeholder: 'Search',
focusNode: _searchFocusNode
);
}
Widget _createPage(Widget? body) {
if(_settingsController.displayType() == PaneDisplayMode.top){
return Padding(
padding: const EdgeInsets.all(_defaultPadding),
child: body
);
}
return Column(
children: [
Row(
children: [
Expanded(
child: _createWindowGestures(
child: Container(
height: _headerSize,
color: Colors.transparent
)
)
),
WindowTitleBar(focused: _focused())
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: _defaultPadding,
right: _defaultPadding,
bottom: _defaultPadding
),
child: body
)
)
],
);
}
int? get _selectedIndex {
var searchItems = _searchItems();
if (searchItems == null) {
@@ -288,10 +193,9 @@ class _HomePageState extends State<HomePage> with WindowListener {
return indexOnScreen;
}
List<NavigationPaneItem> get _allItems => [..._createItems(), ..._createFooterItems()];
List<NavigationPaneItem> get _allItems => [..._items, ..._footerItems];
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
if(_settingsController.displayType() != PaneDisplayMode.top)
List<NavigationPaneItem> get _footerItems => searchValue.isNotEmpty ? [] : [
PaneItem(
title: const Text("Settings"),
icon: const Icon(FluentIcons.settings),
@@ -299,19 +203,18 @@ class _HomePageState extends State<HomePage> with WindowListener {
)
];
List<NavigationPaneItem> _createItems() => _searchItems() ?? [
List<NavigationPaneItem> get _items => _searchItems() ?? [
PaneItem(
title: const Text("Home"),
icon: const Icon(FluentIcons.game),
body: const LauncherPage()
),
if(_settingsController.advancedMode.value)
PaneItem(
title: const Text("Backend"),
icon: const Icon(FluentIcons.server_enviroment),
body: ServerPage()
),
PaneItem(
title: const Text("Backend"),
icon: const Icon(FluentIcons.server_enviroment),
body: ServerPage()
),
PaneItem(
title: const Text("Tutorial"),
@@ -319,50 +222,15 @@ class _HomePageState extends State<HomePage> with WindowListener {
body: const InfoPage(),
onTap: _onTutorial
),
if(_settingsController.displayType() == PaneDisplayMode.top)
PaneItem(
title: const Text("Settings"),
icon: const Icon(FluentIcons.settings),
body: SettingsPage()
)
];
void _onTutorial() {
if(!_navigated){
setState(() {
_settingsController.tutorialPage.value = TutorialPage.start;
_settingsController.scrollingDistance = 0;
});
setState(() => _settingsController.scrollingDistance = 0);
}
_navigated = false;
}
bool _calculateSize() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_settingsController.saveWindowSize();
var width = window.physicalSize.width;
PaneDisplayMode? newType;
if (width <= 1000) {
newType = PaneDisplayMode.top;
} else if (width >= 1500) {
newType = PaneDisplayMode.open;
} else if (width > 1000) {
newType = PaneDisplayMode.compact;
}
if(newType == null || newType == _settingsController.displayType()){
return;
}
_settingsController.displayType.value = newType;
_searchItems.value = null;
_searchController.text = "";
});
return true;
}
String get searchValue => _searchController.text;
}

View File

@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import '../controller/settings_controller.dart';
import '../model/tutorial_page.dart';
import '../widget/shared/fluent_card.dart';
class InfoPage extends StatefulWidget {
const InfoPage({Key? key}) : super(key: key);
@@ -36,6 +36,7 @@ class _InfoPageState extends State<InfoPage> {
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
];
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
final SettingsController _settingsController = Get.find<SettingsController>();
late final ScrollController _controller;
@@ -55,17 +56,52 @@ class _InfoPageState extends State<InfoPage> {
}
@override
Widget build(BuildContext context) {
switch(_settingsController.tutorialPage()) {
case TutorialPage.start:
return _createHomeScreen();
case TutorialPage.someoneElse:
Widget build(BuildContext context) => Navigator(
key: _navigatorKey,
initialRoute: "home",
onGenerateRoute: (settings) {
var screen = _createScreen(settings.name);
return FluentPageRoute(
builder: (context) => screen,
settings: settings
);
},
);
Widget _createScreen(String? name) {
switch(name){
case "home":
return _homeScreen;
case "else":
return _createInstructions(false);
case TutorialPage.yourOwn:
case "own":
return _createInstructions(true);
default:
throw Exception("Unknown page: $name");
}
}
Widget get _homeScreen => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_createCardWidget(
text: "Play on someone else's server",
description: "If one of your friends is hosting a game server, click here",
onClick: () => _navigatorKey.currentState?.pushNamed("else")
),
const SizedBox(
width: 8.0,
),
_createCardWidget(
text: "Host your own server",
description: "If you want to create your own server to invite your friends or to play around by yourself, click here",
onClick: () => _navigatorKey.currentState?.pushNamed("own")
)
]
);
SizedBox _createInstructions(bool own) {
var titles = own ? _ownTitles : _elseTitles;
var codeName = own ? "own" : "else";
@@ -76,8 +112,7 @@ class _InfoPageState extends State<InfoPage> {
padding: const EdgeInsets.only(
right: 20.0
),
child: Card(
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
child: FluentCard(
child: ListTile(
title: SelectableText("${index + 1}. ${titles[index]}"),
subtitle: Padding(
@@ -93,67 +128,42 @@ class _InfoPageState extends State<InfoPage> {
);
}
Widget _createHomeScreen() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_createCardWidget(
text: "Play on someone else's server",
description: "If one of your friends is hosting a game server, click here",
onClick: () => setState(() => _settingsController.tutorialPage.value = TutorialPage.someoneElse)
),
const SizedBox(
width: 8.0,
),
_createCardWidget(
text: "Host your own server",
description: "If you want to create your own server to invite your friends or to play around by yourself, click here",
onClick: () => setState(() => _settingsController.tutorialPage.value = TutorialPage.yourOwn)
)
]
);
}
Widget _createCardWidget({required String text, required String description, required Function() onClick}) {
return Expanded(
child: SizedBox(
Widget _createCardWidget({required String text, required String description, required Function() onClick}) => Expanded(
child: SizedBox(
height: double.infinity,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onClick,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
text,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onClick,
child: FluentCard(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
text,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
const SizedBox(
height: 8.0,
),
const SizedBox(
height: 8.0,
),
Text(
description,
textAlign: TextAlign.center
),
],
Text(
description,
textAlign: TextAlign.center
),
],
)
)
)
)
)
)
)
)
)
);
}
)
);
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
@@ -9,23 +8,18 @@ import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/model/reboot_download.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/widget/home/username_box.dart';
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/widget/shared/file_selector.dart';
import 'package:reboot_launcher/src/widget/shared/setting_tile.dart';
import '../dialog/dialog_button.dart';
import '../model/server_type.dart';
import '../util/checks.dart';
import '../util/reboot.dart';
import '../widget/shared/smart_input.dart';
import 'home_page.dart';
class LauncherPage extends StatefulWidget {
const LauncherPage(
@@ -38,7 +32,6 @@ class LauncherPage extends StatefulWidget {
class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>();
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final BuildController _buildController = Get.find<BuildController>();
@@ -152,28 +145,98 @@ class _LauncherPageState extends State<LauncherPage> {
);
Widget get _homeScreen => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(_gameController.error)
_updateError,
UsernameBox(),
Tooltip(
message:
"The hostname of the server that hosts the multiplayer matches",
child: Obx(() => SmartInput(
label: "Matchmaking Host",
placeholder:
"Type the hostname of the server that hosts the multiplayer matches",
controller: _settingsController.matchmakingIp,
validatorMode: AutovalidateMode.always,
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _gameController.error ? _updateError : const SizedBox(),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: SizedBox(height: _gameController.error ? 16.0 : 0.0),
),
SettingTile(
title: "Username",
subtitle: "Enter the name that others will see once you are in-game",
content: TextFormBox(
placeholder: "username",
controller: _gameController.username,
validator: checkMatchmaking,
enabled: _serverController.type() == ServerType.embedded)
autovalidateMode: AutovalidateMode.always
)
),
const VersionSelector(),
if(_settingsController.advancedMode.value)
GameTypeSelector(),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Matchmaking host",
subtitle: "Enter the IP address of the game server hosting the match",
content: TextFormBox(
placeholder: "ip:port",
controller: _settingsController.matchmakingIp,
validator: checkMatchmaking,
autovalidateMode: AutovalidateMode.always
),
expandedContent: [
ListTile(
title: const Text(
"Automatically start a game server",
style: TextStyle(
fontSize: 14
),
),
subtitle: const Text("Choose whether an headless server should be automatically started when matchmaking is on localhost"),
trailing: Obx(() => ToggleSwitch(
checked: _gameController.autostartGameServer(),
onChanged: (value) => _gameController.autostartGameServer.value = value
))
),
],
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version",
subtitle: "Select the version of Fortnite you want to play with your friends",
content: const VersionSelector(),
expandedContent: [
ListTile(
title: const Text(
"Add a version from this PC's local storage",
style: TextStyle(
fontSize: 14
),
),
trailing: Button(
onPressed: () => VersionSelector.openAddDialog(context),
child: const Text("Add build "),
),
),
ListTile(
title: const Text(
"Download any version from the cloud",
style: TextStyle(
fontSize: 14
),
),
trailing: Button(
onPressed: () => VersionSelector.openDownloadDialog(context),
child: const Text("Download"),
),
),
]
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Instance type",
subtitle: "Select the type of instance you want to launch",
content: GameTypeSelector()
),
const Expanded(child: SizedBox()),
const LaunchButton()
],
);

View File

@@ -6,36 +6,78 @@ import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/server/port_input.dart';
import 'package:reboot_launcher/src/widget/server/server_button.dart';
import '../model/server_type.dart';
import '../widget/shared/setting_tile.dart';
class ServerPage extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
ServerPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(_serverController.warning.value)
GestureDetector(
onTap: () => _serverController.warning.value = false,
child: const MouseRegion(
cursor: SystemMouseCursors.click,
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("The backend server handles authentication and parties, not game hosting"),
severity: InfoBarSeverity.info
),
),
),
const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("The backend server handles authentication and parties, not game hosting"),
severity: InfoBarSeverity.info
),
HostInput(),
PortInput(),
ServerTypeSelector(),
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Host",
subtitle: "Enter the host of the backend server",
content: TextFormBox(
placeholder: "host",
controller: _serverController.host,
enabled: _isRemote
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Host",
subtitle: "Enter the port of the backend server",
content: TextFormBox(
placeholder: "host",
controller: _serverController.port,
enabled: _isRemote
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Type",
subtitle: "Select the type of backend to use",
content: ServerTypeSelector()
),
const SizedBox(
height: 16.0,
),
Align(
alignment: Alignment.bottomCenter,
child: SettingTile(
title: "Login automatically",
subtitle: "Choose whether the game client should login automatically using random credentials",
contentWidth: null,
content: Obx(() => ToggleSwitch(
checked: _serverController.loginAutomatically(),
onChanged: (value) => _serverController.loginAutomatically.value = value
))
),
),
const Expanded(child: SizedBox()),
const ServerButton()
]
));
}
bool get _isRemote => _serverController.type.value == ServerType.remote;
}

View File

@@ -1,121 +1,110 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
import 'package:url_launcher/url_launcher.dart';
import '../util/checks.dart';
import '../widget/setting/url_updater.dart';
import '../widget/shared/file_selector.dart';
import '../widget/shared/setting_tile.dart';
class SettingsPage extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
SettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) =>
_settingsController.advancedMode.value ? _advancedSettings : _easySettings;
Widget get _advancedSettings => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const RebootUpdaterInput(),
_createFileSelector(),
_createConsoleSelector(),
_createGameSelector(),
_createVersionInfo(),
_createAdvancedSwitch()
]
);
Widget get _easySettings => SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircleAvatar(
radius: 48,
backgroundImage: AssetImage("assets/images/auties.png")),
SettingTile(
title: "File settings",
subtitle: "This section contains all the settings related to files used by Fortnite",
expandedContent: [
_createFileSetting(
title: "Game server",
description: "This file is injected to create a game server to host matches",
controller: _settingsController.rebootDll
),
_createFileSetting(
title: "Unreal engine console",
description: "This file is injected to unlock the Unreal Engine Console in-game",
controller: _settingsController.consoleDll
),
_createFileSetting(
title: "Authentication patcher",
description: "This file is injected to redirect all HTTP requests to the local backend",
controller: _settingsController.authDll
),
],
),
const SizedBox(
height: 16.0,
),
const Text("Made by Auties00"),
const SizedBox(
height: 4.0,
SettingTile(
title: "Automatic updates",
subtitle: "Choose whether the launcher and its files should be automatically updated",
contentWidth: null,
content: Obx(() => ToggleSwitch(
checked: _settingsController.autoUpdate(),
onChanged: (value) => _settingsController.autoUpdate.value = value
))
),
_versionText,
const SizedBox(
height: 8.0,
height: 16.0,
),
Button(
child: const Text("Switch to advanced mode"),
onPressed: () => _settingsController.advancedMode.value = true
)
],
),
SettingTile(
title: "Custom launch arguments",
subtitle: "Enter additional arguments to use when launching the game",
content: TextFormBox(
placeholder: "args",
controller: _gameController.customLaunchArgs,
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Create a bug report",
subtitle: "Help me fix bugs by reporting them",
content: Button(
onPressed: () => launchUrl(Uri.parse("https://discord.com/channels/998020695223193670/1031262639457828910")),
child: const Text("Report a bug"),
)
),
]
);
Widget _createAdvancedSwitch() => SmartSwitch(
label: "Advanced Mode",
value: _settingsController.advancedMode
);
Widget _createVersionInfo() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Version Status"),
const SizedBox(height: 6.0),
Button(
child: _versionText,
onPressed: () => launchUrl(safeBinariesDirectory.uri)
)
],
);
Widget _createGameSelector() => Tooltip(
message: "The dll that is injected to make the game work",
child: FileSelector(
label: "Cranium DLL",
placeholder:
"Type the path to the dll used for authentication",
controller: _settingsController.authDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => ListTile(
title: Text(title),
subtitle: Text(description),
trailing: SizedBox(
width: 256,
child: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: "path",
controller: controller,
validator: checkDll,
autovalidateMode: AutovalidateMode.always
),
),
const SizedBox(
width: 8.0,
),
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: () { },
child: const Icon(FluentIcons.open_folder_horizontal),
),
)
],
)
)
);
Widget _createConsoleSelector() => Tooltip(
message: "The dll that is injected when a client is launched",
child: FileSelector(
label: "Console DLL",
placeholder: "Type the path to the console dll",
controller: _settingsController.consoleDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always),
);
Widget _createFileSelector() => Tooltip(
message: "The dll that is injected when a server is launched",
child: FileSelector(
label: "Reboot DLL",
placeholder: "Type the path to the reboot dll",
controller: _settingsController.rebootDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always),
);
Widget get _versionText => const Text("6.4${kDebugMode ? '-DEBUG' : '-RELEASE'}");
}

View File

@@ -1,19 +1,13 @@
import 'dart:io';
import 'dart:math';
import 'package:html/parser.dart' show parse;
import 'package:http/http.dart' as http;
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/model/fortnite_build.dart';
import 'package:reboot_launcher/src/util/time.dart';
import 'package:reboot_launcher/src/util/version.dart' as parser;
import 'package:version/version.dart';
import 'os.dart';
const _userAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36";
final _manifestSourceUrl = Uri.parse(
"https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md");
@@ -67,56 +61,3 @@ Future<Process> downloadManifestBuild(
return process;
}
Future<void> downloadArchiveBuild(String archiveUrl, String destination,
Function(double, String) onProgress, Function() onDecompress) async {
var uuid = Random.secure().nextInt(1000000);
var extension = archiveUrl.substring(archiveUrl.lastIndexOf("."));
var tempFile = File(
"$destination\\.temp\\FortniteBuild$uuid$extension"
);
await tempFile.parent.create(recursive: true);
try {
var client = http.Client();
var request = http.Request("GET", Uri.parse(archiveUrl));
request.headers["User-Agent"] = _userAgent;
var response = await client.send(request);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
}
var startTime = DateTime.now();
var lastRemaining = -1;
var length = response.contentLength!;
var received = 0;
var sink = tempFile.openWrite();
var lastEta = toETA(0);
await response.stream.map((entry) {
received += entry.length;
var percentage = (received / length) * 100;
var elapsed = DateTime.now().difference(startTime).inMilliseconds;
var newRemaining = (elapsed * length / received - elapsed).round();
if(lastRemaining < 0 || lastRemaining - newRemaining <= -10000 || lastRemaining > newRemaining) {
lastEta = toETA(lastRemaining = newRemaining);
}
onProgress(percentage, lastEta);
return entry;
}).pipe(sink);
onDecompress();
var output = Directory(destination);
await output.create(recursive: true);
await loadBinary("winrar.exe", true);
var shell = Shell(
commandVerbose: false,
commentVerbose: false,
workingDirectory: safeBinariesDirectory.path
);
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
} finally {
if (await tempFile.parent.exists()) {
tempFile.parent.delete(recursive: true);
}
}
}

View File

@@ -67,7 +67,7 @@ Directory get safeBinariesDirectory =>
Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher");
Directory get embeddedBackendDirectory =>
Directory("${safeBinariesDirectory.path}\\backend");
Directory("${safeBinariesDirectory.path}\\backend-lawin");
File loadEmbedded(String file) {
var safeBinary = File("${embeddedBackendDirectory.path}\\$file");

View File

@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:ini/ini.dart';
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
@@ -12,6 +14,41 @@ import 'package:http/http.dart' as http;
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
Future<void> writeMatchmakingIp(String text) async {
var file = File("${embeddedBackendDirectory.path}\\Config\\config.ini");
if(!file.existsSync()){
return;
}
var splitIndex = text.indexOf(":");
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : "7777";
var config = Config.fromString(file.readAsStringSync());
config.set("GameServer", "ip", ip);
config.set("GameServer", "port", port);
file.writeAsStringSync(config.toString());
}
Future<void> startServer() async {
if(!embeddedBackendDirectory.existsSync()){
var serverZip = await loadBinary("server.zip", true);
await extractFileToDisk(serverZip.path, embeddedBackendDirectory.path);
}
var process = await Process.start(
"${embeddedBackendDirectory.path}\\lawinserver-win.exe",
[],
workingDirectory: embeddedBackendDirectory.path
);
process.outLines.forEach((element) => serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
process.errLines.forEach((element) => serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
}
Future<void> stopServer() async {
var releaseBat = await loadBinary("kill_both_ports.bat", false);
await Process.run(releaseBat.path, []);
}
Future<bool> isLawinPortFree() async {
return http.get(Uri.parse("http://127.0.0.1:3551/unknown"))
.timeout(const Duration(milliseconds: 500))
@@ -44,7 +81,7 @@ Future<void> freeMatchmakerPort() async {
}
}
List<String> createRebootArgs(String username, GameType type) {
List<String> createRebootArgs(String username, GameType type, String additionalArgs) {
var args = [
"-epicapp=Fortnite",
"-epicenv=Prod",
@@ -73,6 +110,10 @@ List<String> createRebootArgs(String username, GameType type) {
]);
}
if(additionalArgs.isNotEmpty){
args.addAll(additionalArgs.split(" "));
}
return args;
}

View File

@@ -12,33 +12,16 @@ class GameTypeSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Tooltip(
message: "The type of Fortnite instance to launch",
child: _createAdvancedSelector(),
);
return Obx(() => DropDownButton(
leading: Text(_gameController.type.value.name),
items: GameType.values
.map((type) => _createItem(type))
.toList()
));
}
Widget _createAdvancedSelector() => InfoLabel(
label: "Type",
child: SizedBox(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_gameController.type.value.name),
items: GameType.values
.map((type) => _createItem(type))
.toList())
)
)
);
MenuFlyoutItem _createItem(GameType type) => MenuFlyoutItem(
text: SizedBox(
width: double.infinity,
child: Tooltip(
message: type.message,
child: Text(type.name)
)
),
text: Text(type.name),
onPressed: () {
_gameController.type(type);
_gameController.started.value = _gameController.currentGameInstance != null;

View File

@@ -25,9 +25,7 @@ import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/game_instance.dart';
import '../../page/home_page.dart';
import '../../util/process.dart';
import '../shared/smart_check_box.dart';
class LaunchButton extends StatefulWidget {
const LaunchButton(
@@ -66,21 +64,24 @@ class _LaunchButtonState extends State<LaunchButton> {
}
@override
Widget build(BuildContext context) {
return Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
width: double.infinity,
child: Obx(() => Tooltip(
message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance",
child: Button(
onPressed: () => _start(_gameController.type()),
child: Text(_gameController.started() ? "Close" : "Launch")
Widget build(BuildContext context) => Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
width: double.infinity,
child: Obx(() => SizedBox(
height: 48,
child: Button(
child: Align(
alignment: Alignment.center,
child: Text(
_gameController.started() ? "Close fortnite" : "Launch fortnite"
),
),
)),
),
);
}
onPressed: () => _start(_gameController.type()),
),
)),
),
);
void _start(GameType type) async {
if (_gameController.started()) {
@@ -99,7 +100,7 @@ class _LaunchButtonState extends State<LaunchButton> {
showMessage("No username: expecting self sign in");
}
if (_gameController.selectedVersionObs.value == null) {
if (_gameController.selectedVersion == null) {
showMessage("No version is selected");
_onStop(type);
return;
@@ -115,7 +116,7 @@ class _LaunchButtonState extends State<LaunchButton> {
_fail = false;
await _resetLogFile();
var version = _gameController.selectedVersionObs.value!;
var version = _gameController.selectedVersion!;
var gamePath = version.executable?.path;
if(gamePath == null){
showMissingBuildError(version);
@@ -132,8 +133,8 @@ class _LaunchButtonState extends State<LaunchButton> {
await compute(patchMatchmaking, version.executable!);
await compute(patchHeadless, version.executable!);
await _startMatchMakingServer();
await _startGameProcesses(version, type);
var automaticallyStartedServer = await _startMatchMakingServer();
await _startGameProcesses(version, type, automaticallyStartedServer);
if(type == GameType.headlessServer){
await _showServerLaunchingWarning();
@@ -145,96 +146,40 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<void> _startGameProcesses(FortniteVersion version, GameType type) async {
Future<void> _startGameProcesses(FortniteVersion version, GameType type, bool hasChildServer) async {
var launcherProcess = await _createLauncherProcess(version);
var eacProcess = await _createEacProcess(version);
var gameProcess = await _createGameProcess(version.executable!.path, type);
_gameController.gameInstancesMap[type] = GameInstance(gameProcess, launcherProcess, eacProcess);
_gameController.gameInstancesMap[type] = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
_injectOrShowError(Injectable.cranium, type);
}
Future<void> _startMatchMakingServer() async {
Future<bool> _startMatchMakingServer() async {
if(_gameController.type() != GameType.client){
return;
return false;
}
var matchmakingIp = _settingsController.matchmakingIp.text;
if(!matchmakingIp.contains("127.0.0.1") && !matchmakingIp.contains("localhost")) {
return;
return false;
}
var headlessServer = _gameController.gameInstancesMap[GameType.headlessServer] != null;
var server = _gameController.gameInstancesMap[GameType.server] != null;
if(headlessServer || server){
return;
if(!_gameController.autostartGameServer()){
return false;
}
var result = await _askToStartMatchMakingServer();
if(result != true){
return;
}
var version = _gameController.selectedVersionObs.value!;
var version = _gameController.selectedVersion!;
await _startGameProcesses(
version,
GameType.headlessServer
GameType.headlessServer,
false
);
}
Future<bool> _askToStartMatchMakingServer() async {
if(_settingsController.doNotAskAgain()) {
return _settingsController.automaticallyStartMatchmaker();
}
var controller = CheckboxController();
var result = await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
ContentDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: double.infinity,
child: Text(
"The matchmaking ip is set to the local machine, but no server is running. "
"If you want to start a match for your friends or just test out Reboot, you need to start a server, either now from this prompt or later manually.",
textAlign: TextAlign.start,
)
),
const SizedBox(height: 12.0),
SmartCheckBox(
controller: controller,
content: const Text("Don't ask again")
)
],
),
actions: [
Button(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Ignore'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Start a server'),
)
],
)
);
_settingsController.doNotAskAgain.value = controller.value;
if(result != null){
_settingsController.automaticallyStartMatchmaker.value = result;
}
return result ?? false;
return true;
}
Future<Process> _createGameProcess(String gamePath, GameType type) async {
var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type));
var gameArgs = createRebootArgs(_gameController.username.text, type, _gameController.customLaunchArgs.text);
var gameProcess = await Process.start(gamePath, gameArgs);
gameProcess
..exitCode.then((_) => _onEnd(type))
..outLines.forEach((line) => _onGameOutput(line, type))
@@ -369,9 +314,21 @@ class _LaunchButtonState extends State<LaunchButton> {
_start(type);
}
void _onStop(GameType type) {
_gameController.gameInstancesMap[type]?.kill();
_gameController.gameInstancesMap.remove(type);
void _onStop(GameType? type) {
if(type == null){
return;
}
var value = _gameController.gameInstancesMap[type];
if(value != null){
if(value.hasChildServer){
_onStop(GameType.headlessServer);
}
value.kill();
_gameController.gameInstancesMap.remove(type);
}
if(type == _gameController.type()) {
_gameController.started.value = false;
}

View File

@@ -1,23 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
class UsernameBox extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
UsernameBox({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() => Tooltip(
message: _gameController.type.value != GameType.client ? "The username of the game hoster" : "The in-game username of your player",
child: SmartInput(
label: "Username",
placeholder: "Type your ${_gameController.type.value != GameType.client ? 'hosting' : "in-game"} username",
controller: _gameController.username
),
));
}
}

View File

@@ -19,6 +19,19 @@ import '../shared/file_selector.dart';
class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);
static void openDownloadDialog(BuildContext context) async {
await showDialog<bool>(
context: context,
builder: (dialogContext) => const AddServerVersion()
);
}
static void openAddDialog(BuildContext context) async {
await showDialog<bool>(
context: context,
builder: (context) => AddLocalVersion());
}
@override
State<VersionSelector> createState() => _VersionSelectorState();
}
@@ -26,79 +39,44 @@ class VersionSelector extends StatefulWidget {
class _VersionSelectorState extends State<VersionSelector> {
final GameController _gameController = Get.find<GameController>();
final CheckboxController _deleteFilesController = CheckboxController();
final FlyoutController _flyoutController = FlyoutController();
@override
Widget build(BuildContext context) {
return InfoLabel(
label: "Version",
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Row(
children: [
Expanded(child: _createSelector(context)),
const SizedBox(
width: 16,
),
Tooltip(
message: "Add a local fortnite build to the versions list",
child: Button(
child: const Icon(FluentIcons.open_file),
onPressed: () => _openAddLocalVersionDialog(context)),
),
const SizedBox(
width: 16,
),
Tooltip(
message: "Download a fortnite build from the archive",
child: Button(
child: const Icon(FluentIcons.download),
onPressed: () => _openDownloadVersionDialog(context)),
),
],
)));
}
Widget _createSelector(BuildContext context) {
return Tooltip(
message: "The version of Fortnite to launch",
child: Obx(() => _createOptionsMenu(
version: _gameController.selectedVersionObs(),
close: false,
child: DropDownButton(
leading: Text(_gameController.selectedVersionObs.value?.name
?? "Select a version"),
items: _createSelectorItems(context)
)
))
);
}
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
return _gameController.hasNoVersions ? [_createDefaultVersionItem()]
: _gameController.versions.value
.map((version) => _createVersionItem(context, version))
.toList();
}
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) {
return MenuFlyoutItem(
text: _createOptionsMenu(
version: version,
close: true,
child: SizedBox(
width: double.infinity,
child: Text(version.name)
Widget build(BuildContext context) => Obx(() => _createOptionsMenu(
version: _gameController.selectedVersion,
close: false,
child: FlyoutTarget(
controller: _flyoutController,
child: DropDownButton(
leading: Text(_gameController.selectedVersion?.name ?? "Select a version"),
items: _createSelectorItems(context)
),
),
onPressed: () => _gameController.selectedVersion = version
);
}
)
));
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) {
return Listener(
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) => _gameController.hasNoVersions ? [_createDefaultVersionItem()]
: _gameController.versions.value
.map((version) => _createVersionItem(context, version))
.toList();
MenuFlyoutItem _createDefaultVersionItem() => MenuFlyoutItem(
text: const SizedBox(
width: double.infinity, child: Text("No versions available. Add it using the buttons on the right.")),
trailing: const Expanded(child: SizedBox()),
onPressed: () {});
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
text: _createOptionsMenu(
version: version,
close: true,
child: Text(version.name),
),
onPressed: () => _gameController.selectedVersion = version
);
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse ||
event.buttons != kSecondaryMouseButton) {
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
return;
}
@@ -106,43 +84,19 @@ class _VersionSelectorState extends State<VersionSelector> {
return;
}
await _openMenu(context, version, event.position, close);
var result = await _flyoutController.showFlyout<ContextualOption?>(
builder: (context) => MenuFlyout(
items: ContextualOption.values
.map((entry) => _createOption(context, entry))
.toList()
)
);
_handleResult(result, version, close);
},
child: child
);
}
);
MenuFlyoutItem _createDefaultVersionItem() {
return MenuFlyoutItem(
text: const SizedBox(
width: double.infinity, child: Text("No versions available. Add it using the buttons on the right.")),
trailing: const Expanded(child: SizedBox()),
onPressed: () {});
}
void _openDownloadVersionDialog(BuildContext context) async {
await showDialog<bool>(
context: context,
builder: (dialogContext) => const AddServerVersion()
);
}
void _openAddLocalVersionDialog(BuildContext context) async {
await showDialog<bool>(
context: context,
builder: (context) => AddLocalVersion());
}
Future<void> _openMenu(
BuildContext context, FortniteVersion version, Offset offset, bool close) async {
var controller = FlyoutController();
var result = await controller.showFlyout(
builder: (context) => MenuFlyout(
items: ContextualOption.values
.map((entry) => _createOption(context, entry))
.toList()
)
);
void _handleResult(ContextualOption? result, FortniteVersion version, bool close) async {
switch (result) {
case ContextualOption.openExplorer:
if(!mounted){
@@ -156,7 +110,6 @@ class _VersionSelectorState extends State<VersionSelector> {
launchUrl(version.location.uri)
.onError((error, stackTrace) => _onExplorerError());
break;
case ContextualOption.modify:
if(!mounted){
return;
@@ -168,7 +121,6 @@ class _VersionSelectorState extends State<VersionSelector> {
await _openRenameDialog(context, version);
break;
case ContextualOption.delete:
if(!mounted){
return;
@@ -184,8 +136,8 @@ class _VersionSelectorState extends State<VersionSelector> {
}
_gameController.removeVersion(version);
if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) {
_gameController.selectedVersionObs.value = null;
if (_gameController.selectedVersion?.name == version.name || _gameController.hasNoVersions) {
_gameController.selectedVersion = null;
}
if (_deleteFilesController.value && await version.location.exists()) {
@@ -193,7 +145,6 @@ class _VersionSelectorState extends State<VersionSelector> {
}
break;
default:
break;
}
@@ -276,7 +227,6 @@ class _VersionSelectorState extends State<VersionSelector> {
),
FileSelector(
label: "Location",
placeholder: "Type the new game folder",
windowTitle: "Select game folder",
controller: pathController,

View File

@@ -15,49 +15,32 @@ class _ServerButtonState extends State<ServerButton> {
final ServerController _serverController = Get.find<ServerController>();
@override
Widget build(BuildContext context) {
return Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
width: double.infinity,
child: Obx(() => Tooltip(
message: _helpMessage,
child: Button(
onPressed: () async => _serverController.toggle(),
child: Text(_buttonText())),
)),
),
);
}
Widget build(BuildContext context) => Align(
alignment: AlignmentDirectional.bottomCenter,
child: SizedBox(
width: double.infinity,
child: Obx(() => SizedBox(
height: 48,
child: Button(
child: Align(
alignment: Alignment.center,
child: Text(_buttonText),
),
onPressed: () => _serverController.toggle()
),
)),
),
);
String _buttonText() {
String get _buttonText {
if(_serverController.type.value == ServerType.local){
return "Check";
return "Check backend";
}
if(_serverController.started.value){
return "Stop";
return "Stop backend";
}
return "Start";
}
String get _helpMessage {
switch(_serverController.type.value){
case ServerType.embedded:
if (_serverController.started.value) {
return "Stop the backend server currently running";
}
return "Start a new local backend server";
case ServerType.remote:
if (_serverController.started.value) {
return "Stop the reverse proxy currently running";
}
return "Start a reverse proxy targeting the remote backend server";
case ServerType.local:
return "Check if a local backend server is running";
}
return "Start backend";
}
}

View File

@@ -10,30 +10,19 @@ class ServerTypeSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Tooltip(
message: "Determines the type of backend server to use",
child: InfoLabel(
label: "Type",
child: SizedBox(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_serverController.type.value.name),
items: ServerType.values
.map((type) => _createItem(type))
.toList()))
),
),
return DropDownButton(
leading: Text(_serverController.type.value.name),
items: ServerType.values
.map((type) => _createItem(type))
.toList()
);
}
MenuFlyoutItem _createItem(ServerType type) {
return MenuFlyoutItem(
text: SizedBox(
width: double.infinity,
child: Tooltip(
message: type.message,
child: Text(type.name)
)
text: Tooltip(
message: type.message,
child: Text(type.name)
),
onPressed: () async {
await _serverController.stop();

View File

@@ -51,12 +51,10 @@ class _RebootUpdaterInputState extends State<RebootUpdaterInput> {
const SizedBox(width: 16.0),
Tooltip(
message: _settingsController.autoUpdate.value ? "Disable automatic updates" : "Enable automatic updates",
child: Obx(() => Padding(
padding: _valid() ? EdgeInsets.zero : const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: () => _settingsController.autoUpdate.value = !_settingsController.autoUpdate.value,
child: Icon(_settingsController.autoUpdate.value ? FluentIcons.disable_updates : FluentIcons.refresh)
))
child: Obx(() => Button(
onPressed: () => _settingsController.autoUpdate.value = !_settingsController.autoUpdate.value,
child: Icon(_settingsController.autoUpdate.value ? FluentIcons.disable_updates : FluentIcons.refresh)
)
)
)
],

View File

@@ -9,7 +9,6 @@ import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/util/selector.dart';
class FileSelector extends StatefulWidget {
final String label;
final String placeholder;
final String windowTitle;
final bool allowNavigator;
@@ -20,8 +19,7 @@ class FileSelector extends StatefulWidget {
final bool folder;
const FileSelector(
{required this.label,
required this.placeholder,
{required this.placeholder,
required this.windowTitle,
required this.controller,
required this.validator,
@@ -38,50 +36,32 @@ class FileSelector extends StatefulWidget {
}
class _FileSelectorState extends State<FileSelector> {
final RxBool _valid = RxBool(true);
late String? Function(String?) validator;
bool _selecting = false;
@override
void initState() {
validator = (value) {
var result = widget.validator(value);
WidgetsBinding.instance.addPostFrameCallback((_) => _valid.value = result == null);
return result;
};
super.initState();
}
@override
Widget build(BuildContext context) {
return InfoLabel(
label: widget.label,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
)
),
if (widget.allowNavigator) const SizedBox(width: 16.0),
if (widget.allowNavigator)
Tooltip(
message: "Select a ${widget.folder ? 'folder' : 'file'}",
child: Obx(() => Padding(
padding: _valid() ? EdgeInsets.zero : const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
))
)
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
)
),
if (widget.allowNavigator)
const SizedBox(width: 16.0),
if (widget.allowNavigator)
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
)
],
)
)
],
);
}

View File

@@ -1,190 +1,16 @@
import 'package:fluent_ui/fluent_ui.dart';
class FluentCard extends StatefulWidget {
const FluentCard({
Key? key,
this.leading,
required this.content,
this.icon,
this.trailing,
this.animationCurve,
this.animationDuration,
this.onPressed,
this.onStateChanged,
this.isButton = false,
this.headerHeight = 68.5,
this.headerBackgroundColor,
this.contentBackgroundColor,
}) : super(key: key);
static Color backgroundColor(ThemeData style, Set<ButtonStates> states, [bool isClickable = true]) {
if (style.brightness == Brightness.light) {
if (!states.isDisabled && isClickable) {
if (states.isPressing) return const ColorConst.withOpacity(0xf9f9f9, 0.2);
if (states.isHovering) return const ColorConst.withOpacity(0xf9f9f9, 0.4);
}
return const ColorConst.withOpacity(0xFFFFFF, 0.7);
} else {
if (!states.isDisabled && isClickable) {
if (states.isPressing) return const ColorConst.withOpacity(0xFFFFFF, 0.03);
if (states.isHovering) return const ColorConst.withOpacity(0xFFFFFF, 0.082);
}
return const ColorConst.withOpacity(0xFFFFFF, 0.05);
}
}
static Color borderColor(ThemeData style, Set<ButtonStates> states, [bool isClickable = true]) {
if (style.brightness == Brightness.light) {
if (isClickable && states.isHovering && !states.isPressing) return const Color(0xFF212121).withOpacity(0.22);
return const Color(0xFF212121).withOpacity(0.17);
} else {
if (isClickable && states.isPressing) return Colors.white.withOpacity(0.062);
if (isClickable && states.isHovering) return Colors.white.withOpacity(0.02);
return Colors.black.withOpacity(0.52);
}
}
/// The leading widget.
///
/// See also:
///
/// * [Icon]
/// * [RadioButton]
/// * [Checkbox]
final Widget? leading;
/// The card content
///
/// Usually a [Text]
final Widget content;
/// The icon of the toggle button.
final Widget? icon;
/// Disable when onPressed is null, always show chevron icon in the right
final bool isButton;
/// The trailing widget. It's positioned at the right of [content]
/// and at the left of [icon].
///
/// See also:
///
/// * [ToggleSwitch]
final Widget? trailing;
/// Makes the card clickable
/// is null by default
final VoidCallback? onPressed;
/// The expand-collapse animation duration. If null, defaults to
/// [FluentTheme.fastAnimationDuration]
final Duration? animationDuration;
/// The expand-collapse animation curve. If null, defaults to
/// [FluentTheme.animationCurve]
final Curve? animationCurve;
/// A callback called when the current state is changed. `true` when
/// open and `false` when closed.
final ValueChanged<bool>? onStateChanged;
/// The height of the header.
///
/// Defaults to 48.0
final double headerHeight;
/// The background color of the header. If null, [ThemeData.scaffoldBackgroundColor]
/// is used
final Color? headerBackgroundColor;
/// The content color of the header. If null, [ThemeData.acrylicBackgroundColor]
/// is used
final Color? contentBackgroundColor;
class FluentCard extends StatelessWidget {
final Widget child;
const FluentCard({Key? key, required this.child}) : super(key: key);
@override
FluentCardState createState() => FluentCardState();
}
class FluentCardState extends State<FluentCard>
with SingleTickerProviderStateMixin {
late ThemeData theme;
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.animationDuration ?? const Duration(milliseconds: 150),
);
}
static void emptyPressMethod() {}
static const double borderSize = 0.5;
static final Color darkBorderColor = Colors.black.withOpacity(0.8);
static const Duration expanderAnimationDuration = Duration(milliseconds: 70);
/// If this widget acts as a button and is disabled, gray out all text and icons
Widget buttonStyled(Widget child) => !widget.isButton || widget.onPressed != null ? child : IconTheme.merge(
data: IconThemeData(color: theme.disabledColor),
child: DefaultTextStyle.merge(style: TextStyle(color: theme.disabledColor), child: child)
Widget build(BuildContext context) => Mica(
elevation: 1,
child: Card(
backgroundColor: FluentTheme.of(context).menuColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)),
child: child
)
);
@override
Widget build(BuildContext context) {
assert(debugCheckHasFluentTheme(context));
final isLtr = Directionality.of(context) == TextDirection.ltr;
theme = FluentTheme.of(context);
bool isDark = theme.brightness == Brightness.dark;
return buttonStyled(HoverButton(
onPressed: widget.onPressed ?? (widget.isButton ? null : emptyPressMethod),
builder: (context, states) {
return AnimatedContainer(
duration: expanderAnimationDuration,
height: widget.headerHeight,
decoration: BoxDecoration(
color: FluentCard.backgroundColor(theme, states, widget.onPressed != null),
border: Border.all(
width: borderSize,
color: FluentCard.borderColor(theme, states, widget.onPressed != null),
),
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
),
padding: const EdgeInsetsDirectional.only(start: 16.0),
alignment: Alignment.centerLeft,
child: Row(mainAxisSize: MainAxisSize.min, children: [
if (widget.leading != null) Padding(
padding: const EdgeInsetsDirectional.only(end: 17.0),
child: widget.leading!,
),
Expanded(child: widget.content),
if (widget.trailing != null) Padding(
padding: const EdgeInsetsDirectional.only(start: 20.0, end: 13.5),
child: widget.trailing!,
),
if (widget.icon != null || widget.isButton) Container(
margin: EdgeInsetsDirectional.only(
start: widget.trailing != null ? 8.0 : 20.0,
end: 8.0,
top: 8.0,
bottom: 8.0,
),
padding: const EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
child: widget.icon ?? Icon(isLtr ? isDark ? FluentIcons.chevron_right : FluentIcons.chevron_right_med :
isDark ? FluentIcons.chevron_left : FluentIcons.chevron_left_med, size: 11),
),
]),
);
},
));
}
}
class ColorConst extends Color {
const ColorConst.withOpacity(int value, double opacity) : super(
( (((opacity * 0xff ~/ 1) & 0xff) << 24) | ((0x00ffffff & value)) ) & 0xFFFFFFFF);
}

View File

@@ -0,0 +1,67 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/widget/shared/fluent_card.dart';
class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0;
final String title;
final String subtitle;
final Widget? content;
final double? contentWidth;
final List<Widget>? expandedContent;
const SettingTile(
{Key? key,
required this.title,
required this.subtitle,
this.content,
this.contentWidth = kDefaultContentWidth,
this.expandedContent})
: super(key: key);
@override
State<SettingTile> createState() => _SettingTileState();
}
class _SettingTileState extends State<SettingTile> {
@override
Widget build(BuildContext context) {
if(widget.expandedContent == null){
return _contentCard;
}
return Mica(
elevation: 1,
child: Expander(
initiallyExpanded: true,
contentBackgroundColor: FluentTheme.of(context).menuColor,
headerShape: (open) => const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
),
header: ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle)
),
headerHeight: 72,
trailing: SizedBox(
width: widget.contentWidth,
child: widget.content
),
content: Column(
children: widget.expandedContent!
)
),
);
}
Widget get _contentCard => FluentCard(
child: ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
trailing: SizedBox(
width: widget.contentWidth,
child: widget.content
),
),
);
}