mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
10.0.8
This commit is contained in:
@@ -1,60 +1,95 @@
|
||||
import 'package:interact/interact.dart';
|
||||
import 'package:reboot_cli/cli.dart';
|
||||
import 'package:tint/tint.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:interact_cli/interact_cli.dart';
|
||||
import 'package:reboot_cli/src/controller/config.dart';
|
||||
import 'package:reboot_cli/src/util/console.dart';
|
||||
import 'package:reboot_cli/src/util/extensions.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:tint/tint.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
const Command _buildList = Command(name: 'list', parameters: [], subCommands: []);
|
||||
const Command _buildImport = Command(name: 'import', parameters: ['version', 'path'], subCommands: []);
|
||||
const Command _buildDownload = Command(name: 'download', parameters: ['version', 'path'], subCommands: []);
|
||||
const Command _build = Command(name: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]);
|
||||
const Command _build = Command(name: 'versions', parameters: [], subCommands: [_buildList, _buildImport, _buildDownload]);
|
||||
const Command _play = Command(name: 'play', parameters: [], subCommands: []);
|
||||
const Command _host = Command(name: 'host', parameters: [], subCommands: []);
|
||||
const Command _backend = Command(name: 'backend', parameters: [], subCommands: []);
|
||||
final List<String> _versions = downloadableBuilds.map((build) => build.version.toString()).toList(growable: false);
|
||||
|
||||
void main(List<String> args) {
|
||||
_welcome();
|
||||
void main(List<String> args) async {
|
||||
enableLoggingToConsole = false;
|
||||
useDefaultPath = true;
|
||||
|
||||
final parser = Parser(commands: [_build, _play, _host, _backend]);
|
||||
print("""
|
||||
🎮 Reboot Launcher
|
||||
🔥 Launch, manage, and play Fortnite using Project Reboot!
|
||||
🚀 Developed by Auties00 - Version 10.0.7
|
||||
""".green());
|
||||
|
||||
final parser = ConsoleParser(
|
||||
commands: [
|
||||
_build,
|
||||
_play,
|
||||
_host,
|
||||
_backend
|
||||
]
|
||||
);
|
||||
final command = parser.parse(args);
|
||||
print(command);
|
||||
_handleRootCommand(command);
|
||||
await _handleRootCommand(command);
|
||||
}
|
||||
|
||||
void _handleRootCommand(CommandCall? call) {
|
||||
switch(call == null ? null : call.name) {
|
||||
case 'build':
|
||||
_handleBuildCommand(call?.subCall);
|
||||
Future<void> _handleRootCommand(CommandCall? command) async {
|
||||
if(command == null) {
|
||||
await _askRootCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command.name) {
|
||||
case 'versions':
|
||||
await _handleBuildCommand(command.subCall);
|
||||
break;
|
||||
case 'play':
|
||||
_handleBuildCommand(call?.subCall);
|
||||
_handlePlayCommand(command.subCall);
|
||||
break;
|
||||
case 'host':
|
||||
_handleBuildCommand(call?.subCall);
|
||||
_handleHostCommand(command.subCall);
|
||||
break;
|
||||
case 'backend':
|
||||
_handleBuildCommand(call?.subCall);
|
||||
_handleBackendCommand(command.subCall);
|
||||
break;
|
||||
default:
|
||||
_askRootCommand();
|
||||
await _askRootCommand();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _askRootCommand() {
|
||||
Future<void> _askRootCommand() async {
|
||||
final commands = [_build.name, _play.name, _host.name, _backend.name];
|
||||
final commandSelector = Select.withTheme(
|
||||
prompt: ' Select a command:',
|
||||
options: commands,
|
||||
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '')
|
||||
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '', successSuffix: '', errorPrefix: '❌')
|
||||
);
|
||||
_handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
|
||||
await _handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
|
||||
}
|
||||
|
||||
void _handleBuildCommand(CommandCall? call) {
|
||||
switch(call == null ? null : call.name) {
|
||||
Future<void> _handleBuildCommand(CommandCall? call) async {
|
||||
if(call == null) {
|
||||
_askBuildCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
switch(call.name) {
|
||||
case 'import':
|
||||
_handleBuildImportCommand(call!);
|
||||
await _handleBuildImportCommand(call);
|
||||
break;
|
||||
case 'download':
|
||||
_handleBuildDownloadCommand(call!);
|
||||
_handleBuildDownloadCommand(call);
|
||||
break;
|
||||
case 'list':
|
||||
_handleBuildListCommand(call);
|
||||
break;
|
||||
default:
|
||||
_askBuildCommand();
|
||||
@@ -62,23 +97,283 @@ void _handleBuildCommand(CommandCall? call) {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleBuildImportCommand(CommandCall call) {
|
||||
final version = call.parameters['path'];
|
||||
final path = call.parameters['path'];
|
||||
print(version);
|
||||
print(path);
|
||||
void _handleBuildListCommand(CommandCall commandCall) {
|
||||
final versions = readVersions();
|
||||
final versionSelector = Select.withTheme(
|
||||
prompt: ' Select a version:',
|
||||
options: versions.map((version) => version.content.toString()).toList(growable: false),
|
||||
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '', successSuffix: '', errorPrefix: '❌')
|
||||
);
|
||||
final version = versions[versionSelector.interact()];
|
||||
final actionSelector = Select.withTheme(
|
||||
prompt: ' Select an action:',
|
||||
options: ['Play', 'Host', 'Delete', 'Open in Explorer'],
|
||||
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '', successSuffix: '', errorPrefix: '❌')
|
||||
);
|
||||
actionSelector.interact();
|
||||
}
|
||||
|
||||
void _handleBuildDownloadCommand(CommandCall call) {
|
||||
Future<void> _handleBuildImportCommand(CommandCall call) async {
|
||||
final version = _getOrPromptVersion(call);
|
||||
if(version == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final path = await _getOrPromptPath(call, true);
|
||||
if(path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fortniteVersion = FortniteVersion(
|
||||
name: "dummy",
|
||||
content: Version.parse(version),
|
||||
location: Directory(path)
|
||||
);
|
||||
writeVersion(fortniteVersion);
|
||||
print('');
|
||||
print('✅ Imported build: ${version.green()}');
|
||||
}
|
||||
|
||||
String? _getOrPromptVersion(CommandCall call) {
|
||||
final version = call.parameters['version'];
|
||||
if(version != null) {
|
||||
final result = version.trim();
|
||||
if (_versions.contains(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
print('');
|
||||
print("❌ Unknown version: $result");
|
||||
return null;
|
||||
}
|
||||
|
||||
stdout.write('❓ Type a version: ');
|
||||
final result = runAutoComplete(_autocompleteVersion).trim();
|
||||
if(_versions.contains(result)) {
|
||||
print('✅ Type a version: ${result.green()}');
|
||||
return result;
|
||||
}
|
||||
|
||||
print('');
|
||||
print("❌ Unknown version: $version");
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _getOrPromptPath(CommandCall call, bool existing) async {
|
||||
var path = call.parameters['path'];
|
||||
if(path != null) {
|
||||
final result = path.trim();
|
||||
final check = await _checkBuildPath(result, existing);
|
||||
if(!check) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
stdout.write('❓ Type a path: ');
|
||||
final result = runAutoComplete(_autocompletePath).trim();
|
||||
final check = await _checkBuildPath(result, existing);
|
||||
if(!check) {
|
||||
return null;
|
||||
}
|
||||
|
||||
print('✅ Type a path: ${result.green()}');
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<bool> _checkBuildPath(String path, bool existing) async {
|
||||
final directory = Directory(path);
|
||||
if(!directory.existsSync()) {
|
||||
if(existing) {
|
||||
print('');
|
||||
print("❌ Unknown path: $path");
|
||||
return false;
|
||||
}else {
|
||||
directory.createSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
final checker = Spinner.withTheme(
|
||||
icon: '✅',
|
||||
rightPrompt: (status) => status != SpinnerStateType.inProgress ? 'Finished looking for FortniteClient-Win64-Shipping.exe' : 'Looking for FortniteClient-Win64-Shipping.exe...',
|
||||
theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '❌', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
|
||||
).interact();
|
||||
final result = await Future.wait([
|
||||
Future.delayed(const Duration(seconds: 1)).then((_) => true),
|
||||
Isolate.run(() => FortniteVersionExtension.findFiles(directory, "FortniteClient-Win64-Shipping.exe") != null)
|
||||
]).then((values) => values.reduce((first, second) => first && second));
|
||||
checker.done();
|
||||
if(!result) {
|
||||
print("❌ Cannot find FortniteClient-Win64-Shipping.exe: $path");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String? _autocompleteVersion(String input) => input.isEmpty ? null : _versions.firstWhereOrNull((version) => version.toLowerCase().startsWith(input.toLowerCase()));
|
||||
|
||||
String? _autocompletePath(String path) {
|
||||
try {
|
||||
if (path.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String separator = Platform.isWindows ? '\\' : '/';
|
||||
path = path.replaceAll('\\', separator);
|
||||
|
||||
if (FileSystemEntity.isFileSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(FileSystemEntity.isDirectorySync(path)) {
|
||||
return path.endsWith(separator) ? null : path + separator;
|
||||
}
|
||||
|
||||
final lastSeparatorIndex = path.lastIndexOf(separator);
|
||||
String directoryPath;
|
||||
String partialName;
|
||||
String prefixPath;
|
||||
if (lastSeparatorIndex == -1) {
|
||||
directoryPath = '';
|
||||
partialName = path;
|
||||
prefixPath = '';
|
||||
} else {
|
||||
directoryPath = path.substring(0, lastSeparatorIndex);
|
||||
partialName = path.substring(lastSeparatorIndex + 1);
|
||||
prefixPath = path.substring(0, lastSeparatorIndex + 1);
|
||||
if (directoryPath.isEmpty && lastSeparatorIndex == 0) {
|
||||
directoryPath = separator;
|
||||
} else if (directoryPath.isEmpty) {
|
||||
directoryPath = '.';
|
||||
}
|
||||
}
|
||||
|
||||
final dir = Directory(directoryPath);
|
||||
if (!dir.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final entries = dir.listSync();
|
||||
final matches = <FileSystemEntity>[];
|
||||
for (var entry in entries) {
|
||||
final name = entry.path.split(separator).last;
|
||||
if (name.startsWith(partialName)) {
|
||||
matches.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
matches.sort((a, b) {
|
||||
final aIsDir = a is Directory;
|
||||
final bIsDir = b is Directory;
|
||||
|
||||
if (aIsDir != bIsDir) {
|
||||
return aIsDir ? -1 : 1;
|
||||
}
|
||||
|
||||
final aName = a.path.split(separator).last;
|
||||
final bName = b.path.split(separator).last;
|
||||
|
||||
if (aName.length != bName.length) {
|
||||
return aName.length - bName.length;
|
||||
}
|
||||
|
||||
return aName.compareTo(bName);
|
||||
});
|
||||
|
||||
final bestMatch = matches.first;
|
||||
final bestMatchName = bestMatch.path.split(separator).last;
|
||||
|
||||
var result = prefixPath + bestMatchName;
|
||||
if (bestMatch is Directory) {
|
||||
result = result.endsWith(separator) ? result : result + separator;
|
||||
}
|
||||
return result;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _handleBuildDownloadCommand(CommandCall call) async {
|
||||
final version = _getOrPromptVersion(call);
|
||||
if(version == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final parsedVersion = Version.parse(version);
|
||||
final build = downloadableBuilds.firstWhereOrNull((build) => build.version == parsedVersion);
|
||||
if(build == null) {
|
||||
print('');
|
||||
print("❌ Cannot find mirror for version: $parsedVersion");
|
||||
return;
|
||||
}
|
||||
|
||||
final path = await _getOrPromptPath(call, false);
|
||||
if(path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double progress = 0;
|
||||
bool extracting = false;
|
||||
final downloader = Spinner.withTheme(
|
||||
icon: '✅',
|
||||
rightPrompt: (status) => status != SpinnerStateType.inProgress ? 'Finished ${extracting ? 'extracting' : 'downloading'} ${parsedVersion.toString()}' : '${extracting ? 'Extracting' : 'Downloading'} ${parsedVersion.toString()} (${progress.round()}%)...',
|
||||
theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '❌', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
|
||||
).interact();
|
||||
final parsedDirectory = Directory(path);
|
||||
final receivePort = ReceivePort();
|
||||
SendPort? sendPort;
|
||||
receivePort.listen((message) {
|
||||
if(message is FortniteBuildDownloadProgress) {
|
||||
if(message.progress >= 100) {
|
||||
sendPort?.send(kStopBuildDownloadSignal);
|
||||
stopDownloadServer();
|
||||
downloader.done();
|
||||
receivePort.close();
|
||||
final fortniteVersion = FortniteVersion(
|
||||
name: "dummy",
|
||||
content: parsedVersion,
|
||||
location: parsedDirectory
|
||||
);
|
||||
writeVersion(fortniteVersion);
|
||||
print('');
|
||||
print('✅ Downloaded build: ${version.green()}');
|
||||
}else {
|
||||
progress = message.progress;
|
||||
extracting = message.extracting;
|
||||
}
|
||||
}else if(message is SendPort) {
|
||||
sendPort = message;
|
||||
}else {
|
||||
sendPort?.send(kStopBuildDownloadSignal);
|
||||
stopDownloadServer();
|
||||
downloader.done();
|
||||
receivePort.close();
|
||||
print("❌ Cannot download build: $message");
|
||||
}
|
||||
});
|
||||
final options = FortniteBuildDownloadOptions(
|
||||
build,
|
||||
parsedDirectory,
|
||||
receivePort.sendPort
|
||||
);
|
||||
await downloadArchiveBuild(options);
|
||||
}
|
||||
|
||||
void _askBuildCommand() {
|
||||
final commands = [_buildImport.name, _buildDownload.name];
|
||||
final commands = [_buildList.name, _buildImport.name, _buildDownload.name];
|
||||
final commandSelector = Select.withTheme(
|
||||
prompt: ' Select a build command:',
|
||||
prompt: ' Select a version command:',
|
||||
options: commands,
|
||||
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '')
|
||||
theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '', successSuffix: '', errorPrefix: '❌')
|
||||
);
|
||||
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
|
||||
}
|
||||
@@ -93,10 +388,4 @@ void _handleHostCommand(CommandCall? call) {
|
||||
|
||||
void _handleBackendCommand(CommandCall? call) {
|
||||
|
||||
}
|
||||
|
||||
void _welcome() => print("""
|
||||
🎮 Reboot Launcher
|
||||
🔥 Launch, manage, and play Fortnite using Project Reboot!
|
||||
🚀 Developed by Auties00 - Version 10.0.7
|
||||
""".green());
|
||||
}
|
||||
0
cli/lib/src/command/backend.dart
Normal file
0
cli/lib/src/command/backend.dart
Normal file
0
cli/lib/src/command/commands.dart
Normal file
0
cli/lib/src/command/commands.dart
Normal file
0
cli/lib/src/command/config.dart
Normal file
0
cli/lib/src/command/config.dart
Normal file
0
cli/lib/src/command/host.dart
Normal file
0
cli/lib/src/command/host.dart
Normal file
0
cli/lib/src/command/play.dart
Normal file
0
cli/lib/src/command/play.dart
Normal file
0
cli/lib/src/command/versions.dart
Normal file
0
cli/lib/src/command/versions.dart
Normal file
24
cli/lib/src/controller/config.dart
Normal file
24
cli/lib/src/controller/config.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
List<FortniteVersion> readVersions() {
|
||||
final file = _versionsFile;
|
||||
if(!file.existsSync()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync());
|
||||
return decodedVersionsJson
|
||||
.map((entry) => FortniteVersion.fromJson(entry))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void writeVersion(FortniteVersion version) {
|
||||
final versions = readVersions();
|
||||
versions.add(version);
|
||||
_versionsFile.writeAsString(jsonEncode(versions.map((version) => version.toJson()).toList()));
|
||||
}
|
||||
|
||||
File get _versionsFile => File('${installationDirectory.path}/versions.json');
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'package:dart_console/dart_console.dart';
|
||||
|
||||
class Parser {
|
||||
typedef AutoComplete = String? Function(String);
|
||||
|
||||
class ConsoleParser {
|
||||
final List<Command> commands;
|
||||
|
||||
Parser({required this.commands});
|
||||
ConsoleParser({required this.commands});
|
||||
|
||||
CommandCall? parse(List<String> args) {
|
||||
var position = 0;
|
||||
@@ -86,4 +89,72 @@ class CommandCall {
|
||||
|
||||
@override
|
||||
String toString() => 'CommandCall{name: $name, parameters: $parameters, subCall: $subCall}';
|
||||
}
|
||||
}
|
||||
|
||||
String runAutoComplete(AutoComplete completion) {
|
||||
final console = Console();
|
||||
console.rawMode = true;
|
||||
final position = console.cursorPosition!;
|
||||
|
||||
var currentInput = '';
|
||||
var running = true;
|
||||
var result = '';
|
||||
|
||||
while (running) {
|
||||
final key = console.readKey();
|
||||
switch (key.controlChar) {
|
||||
case ControlCharacter.ctrlC:
|
||||
running = false;
|
||||
break;
|
||||
case ControlCharacter.enter:
|
||||
_eraseUntil(console, position);
|
||||
console.write(currentInput);
|
||||
console.writeLine();
|
||||
result = currentInput;
|
||||
running = false;
|
||||
break;
|
||||
case ControlCharacter.tab:
|
||||
final suggestion = completion(currentInput);
|
||||
if (suggestion != null) {
|
||||
_eraseUntil(console, position);
|
||||
currentInput = suggestion;
|
||||
console.write(currentInput);
|
||||
}
|
||||
break;
|
||||
case ControlCharacter.backspace:
|
||||
if (currentInput.isNotEmpty) {
|
||||
currentInput = currentInput.substring(0, currentInput.length - 1);
|
||||
_eraseUntil(console, position);
|
||||
console.write(currentInput);
|
||||
_showSuggestion(console, position, currentInput, completion);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
currentInput += key.char;
|
||||
console.write(key.char);
|
||||
_showSuggestion(console, position, currentInput, completion);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _eraseUntil(Console console, Coordinate position) {
|
||||
console.cursorPosition = position;
|
||||
stdout.write('\x1b[K');
|
||||
}
|
||||
|
||||
void _showSuggestion(Console console, Coordinate position, String input, AutoComplete completion) {
|
||||
final suggestion = completion(input);
|
||||
if(suggestion == null) {
|
||||
_eraseUntil(console, position);
|
||||
console.write(input);
|
||||
}else if(suggestion.length > input.length) {
|
||||
final remaining = suggestion.substring(input.length);
|
||||
final cursorPosition = console.cursorPosition;
|
||||
console.setForegroundColor(ConsoleColor.brightBlack);
|
||||
console.write(remaining);
|
||||
console.resetColorAttributes();
|
||||
console.cursorPosition = cursorPosition;
|
||||
}
|
||||
}
|
||||
10
cli/lib/src/util/extensions.dart
Normal file
10
cli/lib/src/util/extensions.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
extension IterableExtension<E> on Iterable<E> {
|
||||
E? firstWhereOrNull(bool test(E element)) {
|
||||
for (final element in this) {
|
||||
if (test(element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,15 @@ version: "10.0.7"
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <=3.5.3"
|
||||
sdk: ">=3.0.0 <=3.5.3"
|
||||
|
||||
dependencies:
|
||||
reboot_common:
|
||||
path: ./../common
|
||||
tint: ^2.0.1
|
||||
interact: ^2.2.0
|
||||
interact_cli: ^2.4.0
|
||||
args: ^2.6.0
|
||||
version: ^3.0.2
|
||||
|
||||
dependency_overrides:
|
||||
xml: ^6.3.0
|
||||
|
||||
Reference in New Issue
Block a user