mirror of
https://github.com/open-goal/launcher.git
synced 2024-10-20 04:57:38 -04:00
Merge remote-tracking branch 'origin/main' into v/267
This commit is contained in:
commit
a529fb9734
20
README.md
20
README.md
|
@ -6,12 +6,14 @@ Our attempt at distributing the [OpenGOAL](https://github.com/open-goal/jak-proj
|
|||
|
||||
The launcher uses the [Tauri](https://tauri.app/) framework.
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Asking for help](#asking-for-help)
|
||||
- [Development](#development)
|
||||
- [Windows](#windows)
|
||||
- [Linux (Ubuntu 22.04)](#linux-ubuntu-2204)
|
||||
- [Building and Running](#building-and-running)
|
||||
- [OpenGOAL Launcher](#opengoal-launcher)
|
||||
- [Usage](#usage)
|
||||
- [Asking for help](#asking-for-help)
|
||||
- [Development](#development)
|
||||
- [Windows](#windows)
|
||||
- [Linux (Ubuntu 22.04)](#linux-ubuntu-2204)
|
||||
- [macOS](#macos)
|
||||
- [Building and Running](#building-and-running)
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -53,6 +55,12 @@ nvm install lts/hydrogen # installs latest nodejs 18.X
|
|||
npm install -g yarn
|
||||
```
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
npm install -g yarn
|
||||
```
|
||||
|
||||
### Building and Running
|
||||
|
||||
To build and run the application locally, all you have to do is run:
|
||||
|
|
BIN
docs/default-keybinds.png
Normal file
BIN
docs/default-keybinds.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 131 KiB |
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
@ -3193,6 +3193,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"ts-rs",
|
||||
"walkdir",
|
||||
"wgpu",
|
||||
|
|
|
@ -43,6 +43,7 @@ zip = { version = "2.2.0", features = ["deflate-zlib-ng"] }
|
|||
zip-extract = "0.2.1"
|
||||
tempfile = "3.12.0"
|
||||
native-dialog = "0.7.0"
|
||||
tokio-util = "0.7.12"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52.0"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::ErrorKind,
|
||||
os::windows::process::CommandExt,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
process::Stdio,
|
||||
str::FromStr,
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
use log::{info, warn};
|
||||
use semver::Version;
|
||||
|
@ -16,7 +17,10 @@ use tauri::Manager;
|
|||
|
||||
use crate::{
|
||||
config::LauncherConfig,
|
||||
util::file::{create_dir, overwrite_dir, read_last_lines_from_file},
|
||||
util::{
|
||||
file::{create_dir, overwrite_dir},
|
||||
process::{create_log_file, create_std_log_file, watch_process},
|
||||
},
|
||||
TAURI_APP,
|
||||
};
|
||||
|
||||
|
@ -36,6 +40,12 @@ struct CommonConfigData {
|
|||
tooling_version: Version,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct ToastPayload {
|
||||
toast: String,
|
||||
level: String,
|
||||
}
|
||||
|
||||
fn common_prelude(
|
||||
config: &tokio::sync::MutexGuard<LauncherConfig>,
|
||||
) -> Result<CommonConfigData, CommandError> {
|
||||
|
@ -205,31 +215,6 @@ fn get_exec_location(
|
|||
})
|
||||
}
|
||||
|
||||
fn create_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: &str,
|
||||
append: bool,
|
||||
) -> Result<std::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = std::fs::OpenOptions::new();
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstallStepOutput {
|
||||
|
@ -253,18 +238,6 @@ pub async fn update_data_directory(
|
|||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_end_of_logs(app_handle: tauri::AppHandle) -> Result<String, CommandError> {
|
||||
Ok(read_last_lines_from_file(
|
||||
&app_handle
|
||||
.path_resolver()
|
||||
.app_log_dir()
|
||||
.unwrap()
|
||||
.join("extractor.log"),
|
||||
250,
|
||||
)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extract_and_validate_iso(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
|
@ -307,23 +280,34 @@ pub async fn extract_and_validate_iso(
|
|||
args.push(game_name.clone());
|
||||
}
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let mut log_file =
|
||||
create_log_file(&app_handle, format!("extractor-{game_name}.log"), true).await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
|
@ -338,8 +322,6 @@ pub async fn extract_and_validate_iso(
|
|||
};
|
||||
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(message.msg.clone()),
|
||||
|
@ -347,8 +329,6 @@ pub async fn extract_and_validate_iso(
|
|||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -393,7 +373,6 @@ pub async fn run_decompiler(
|
|||
.to_string();
|
||||
}
|
||||
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", !truncate_logs)?;
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
|
||||
let mut decomp_config_overrides = vec![];
|
||||
|
@ -443,15 +422,35 @@ pub async fn run_decompiler(
|
|||
|
||||
command
|
||||
.args(args)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file = create_log_file(
|
||||
&app_handle,
|
||||
format!("extractor-{game_name}.log"),
|
||||
!truncate_logs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
|
||||
// Ensure all remaining data is flushed to the file
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("decompilation was successful");
|
||||
|
@ -466,8 +465,6 @@ pub async fn run_decompiler(
|
|||
};
|
||||
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||
log::error!("decompilation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(message.msg.clone()),
|
||||
|
@ -475,8 +472,6 @@ pub async fn run_decompiler(
|
|||
}
|
||||
None => {
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -520,7 +515,6 @@ pub async fn run_compiler(
|
|||
.to_string();
|
||||
}
|
||||
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", !truncate_logs)?;
|
||||
let mut args = vec![
|
||||
source_path,
|
||||
"--compile".to_string(),
|
||||
|
@ -538,15 +532,25 @@ pub async fn run_compiler(
|
|||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.stdout(log_file.try_clone().unwrap())
|
||||
.stderr(log_file)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file = create_log_file(
|
||||
&app_handle,
|
||||
format!("extractor-{game_name}.log"),
|
||||
!truncate_logs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
log_file.flush().await?;
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("compilation was successful");
|
||||
|
@ -561,8 +565,6 @@ pub async fn run_compiler(
|
|||
};
|
||||
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||
log::error!("compilation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(message.msg.clone()),
|
||||
|
@ -570,8 +572,6 @@ pub async fn run_compiler(
|
|||
}
|
||||
None => {
|
||||
log::error!("compilation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -583,33 +583,66 @@ pub async fn run_compiler(
|
|||
#[tauri::command]
|
||||
pub async fn open_repl(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
game_name: String,
|
||||
) -> Result<(), CommandError> {
|
||||
// TODO - explore a linux option though this is very annoying because without doing a ton of research
|
||||
// we seem to have to handle various terminals. Which honestly we should probably do on windows too
|
||||
//
|
||||
// So maybe we can make a menu where the user will specify what terminal to use / what launch-options to use
|
||||
let config_lock = config.lock().await;
|
||||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let data_folder = get_data_dir(&config_info, &game_name, false)?;
|
||||
let exec_info = get_exec_location(&config_info, "goalc")?;
|
||||
let mut command = Command::new("cmd");
|
||||
command
|
||||
.args([
|
||||
"/K",
|
||||
"start",
|
||||
&bin_ext("goalc"),
|
||||
"--proj-path",
|
||||
&data_folder.to_string_lossy(),
|
||||
])
|
||||
.current_dir(exec_info.executable_dir);
|
||||
let mut command;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
command = std::process::Command::new("cmd");
|
||||
command
|
||||
.args([
|
||||
"/K",
|
||||
"start",
|
||||
&bin_ext("goalc"),
|
||||
"--proj-path",
|
||||
&data_folder.to_string_lossy(),
|
||||
])
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.creation_flags(0x08000000);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
command = std::process::Command::new("xdg-terminal-exec");
|
||||
command
|
||||
.args(["./goalc", "--proj-path", &data_folder.to_string_lossy()])
|
||||
.current_dir(exec_info.executable_dir);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
command = std::process::Command::new("osascript");
|
||||
command
|
||||
.args([
|
||||
"-e",
|
||||
"'tell app \"Terminal\" to do script",
|
||||
format!("\"cd {:?}\" &&", exec_info.executable_dir).as_str(),
|
||||
"./goalc",
|
||||
"--proj-path",
|
||||
&data_folder.to_string_lossy(),
|
||||
])
|
||||
.current_dir(exec_info.executable_dir);
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
if let ErrorKind::NotFound = e.kind() {
|
||||
let _ = app_handle.emit_all(
|
||||
"toast_msg",
|
||||
ToastPayload {
|
||||
toast: format!("'{:?}' not found in PATH!", command.get_program()),
|
||||
level: "error".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
return Err(CommandError::BinaryExecution(
|
||||
"Unable to launch REPL".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
command.spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -658,7 +691,7 @@ pub async fn run_game_gpu_test(
|
|||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
let output = command.output().await?;
|
||||
match output.status.code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
|
@ -769,13 +802,26 @@ pub async fn launch_game(
|
|||
let config_info = common_prelude(&config_lock)?;
|
||||
|
||||
let mut exec_info = get_exec_location(&config_info, "gk")?;
|
||||
if executable_location.is_some() {
|
||||
let exec_path = PathBuf::from_str(executable_location.unwrap().as_str());
|
||||
if exec_path.is_ok() {
|
||||
exec_info = ExecutableLocation {
|
||||
executable_dir: exec_path.clone().unwrap().parent().unwrap().to_path_buf(),
|
||||
executable_path: exec_path.clone().unwrap(),
|
||||
};
|
||||
if let Some(custom_exec_location) = executable_location {
|
||||
match PathBuf::from_str(custom_exec_location.as_str()) {
|
||||
Ok(exec_path) => {
|
||||
let path_copy = exec_path.clone();
|
||||
if path_copy.parent().is_none() {
|
||||
return Err(CommandError::BinaryExecution(format!(
|
||||
"Failed to resolve custom binary parent directory"
|
||||
)));
|
||||
}
|
||||
exec_info = ExecutableLocation {
|
||||
executable_dir: exec_path.clone().parent().unwrap().to_path_buf(),
|
||||
executable_path: exec_path.clone(),
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(CommandError::BinaryExecution(format!(
|
||||
"Failed to resolve custom binary location {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -790,10 +836,9 @@ pub async fn launch_game(
|
|||
exec_info.executable_path,
|
||||
);
|
||||
|
||||
let log_file = create_log_file(&app_handle, "game.log", false)?;
|
||||
let log_file = create_std_log_file(&app_handle, format!("game-{game_name}.log"), false)?;
|
||||
|
||||
// TODO - log rotation here would be nice too, and for it to be game specific
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
let mut command = std::process::Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.stdout(log_file.try_clone().unwrap())
|
||||
|
@ -801,7 +846,7 @@ pub async fn launch_game(
|
|||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
std::os::windows::process::CommandExt::creation_flags(&mut command, 0x08000000);
|
||||
}
|
||||
// Start the process here so if there is an error, we can return immediately
|
||||
let mut child = command.spawn()?;
|
||||
|
@ -809,9 +854,22 @@ pub async fn launch_game(
|
|||
tokio::spawn(async move {
|
||||
let start_time = Instant::now(); // get the start time of the game
|
||||
// start waiting for the game to exit
|
||||
if let Err(err) = child.wait() {
|
||||
log::error!("Error occured when waiting for game to exit: {}", err);
|
||||
return;
|
||||
match child.wait() {
|
||||
Ok(status_code) => {
|
||||
if !status_code.code().is_some() || status_code.code().unwrap() != 0 {
|
||||
let _ = app_handle.emit_all(
|
||||
"toast_msg",
|
||||
ToastPayload {
|
||||
toast: "Game crashed unexpectedly!".to_string(),
|
||||
level: "error".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Error occured when waiting for game to exit: {}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// once the game exits pass the time the game started to the track_playtine function
|
||||
if let Err(err) = track_playtime(start_time, game_name).await {
|
||||
|
|
|
@ -195,9 +195,17 @@ pub async fn is_avx_requirement_met(
|
|||
}
|
||||
match config_lock.requirements.avx {
|
||||
None => {
|
||||
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
|
||||
config_lock.requirements.avx = Some(true);
|
||||
} else {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
{
|
||||
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
|
||||
config_lock.requirements.avx = Some(true);
|
||||
} else {
|
||||
config_lock.requirements.avx = Some(false);
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
{
|
||||
// TODO - macOS check if on atleast sequoia and rosetta 2 is installed
|
||||
config_lock.requirements.avx = Some(false);
|
||||
}
|
||||
config_lock.save_config().map_err(|err| {
|
||||
|
|
|
@ -3,10 +3,11 @@ use std::os::windows::process::CommandExt;
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
use crate::{
|
||||
commands::{binaries::InstallStepOutput, CommandError},
|
||||
|
@ -14,6 +15,7 @@ use crate::{
|
|||
util::{
|
||||
file::{create_dir, delete_dir, to_image_base64},
|
||||
network::download_file,
|
||||
process::{create_log_file, create_std_log_file, watch_process},
|
||||
tar::{extract_and_delete_tar_ball, extract_tar_ball},
|
||||
zip::{extract_and_delete_zip_file, extract_zip_file},
|
||||
},
|
||||
|
@ -252,31 +254,6 @@ fn get_mod_exec_location(
|
|||
})
|
||||
}
|
||||
|
||||
fn create_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: &str,
|
||||
append: bool,
|
||||
) -> Result<std::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = std::fs::OpenOptions::new();
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct LauncherErrorCode {
|
||||
msg: String,
|
||||
|
@ -336,23 +313,37 @@ pub async fn extract_iso_for_mod_install(
|
|||
game_name.clone(),
|
||||
];
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let mut log_file = create_log_file(
|
||||
&app_handle,
|
||||
format!("extractor-{game_name}-{mod_name}.log"),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
|
@ -365,8 +356,6 @@ pub async fn extract_iso_for_mod_install(
|
|||
msg: format!("Unexpected error occured with code {code}"),
|
||||
};
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(default_error.msg.clone()),
|
||||
|
@ -374,8 +363,6 @@ pub async fn extract_iso_for_mod_install(
|
|||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -434,26 +421,38 @@ pub async fn decompile_for_mod_install(
|
|||
game_name.clone(),
|
||||
];
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file =
|
||||
create_log_file(&app_handle, format!("extractor-{game_name}.log"), false).await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
|
||||
// Ensure all remaining data is flushed to the file
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
log::info!("decompilation was successful");
|
||||
return Ok(InstallStepOutput {
|
||||
success: true,
|
||||
msg: None,
|
||||
|
@ -462,18 +461,14 @@ pub async fn decompile_for_mod_install(
|
|||
let default_error = LauncherErrorCode {
|
||||
msg: format!("Unexpected error occured with code {code}"),
|
||||
};
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("decompilation was not successful. Code {code}");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(default_error.msg.clone()),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -532,26 +527,36 @@ pub async fn compile_for_mod_install(
|
|||
game_name.clone(),
|
||||
];
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file =
|
||||
create_log_file(&app_handle, format!("extractor-{game_name}.log"), false).await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("compilation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
log::info!("compilation was successful");
|
||||
return Ok(InstallStepOutput {
|
||||
success: true,
|
||||
msg: None,
|
||||
|
@ -560,18 +565,14 @@ pub async fn compile_for_mod_install(
|
|||
let default_error = LauncherErrorCode {
|
||||
msg: format!("Unexpected error occured with code {code}"),
|
||||
};
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("compilation was not successful. Code {code}");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(default_error.msg.clone()),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("compilation was not successful. No status code");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -686,13 +687,17 @@ pub async fn launch_mod(
|
|||
&mod_name,
|
||||
&source_name,
|
||||
)?;
|
||||
let args = generate_launch_mod_args(game_name, in_debug, config_dir, false)?;
|
||||
let args = generate_launch_mod_args(game_name.clone(), in_debug, config_dir, false)?;
|
||||
|
||||
log::info!("Launching gk args: {:?}", args);
|
||||
|
||||
let log_file = create_log_file(&app_handle, "mod.log", false)?;
|
||||
let log_file = create_std_log_file(
|
||||
&app_handle,
|
||||
format!("game-{game_name}-{mod_name}.log"),
|
||||
false,
|
||||
)?;
|
||||
|
||||
// TODO - log rotation here would be nice too, and for it to be game/mod specific
|
||||
// TODO - log rotation here would be nice too
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
|
|
|
@ -153,7 +153,6 @@ fn main() {
|
|||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::binaries::extract_and_validate_iso,
|
||||
commands::binaries::get_end_of_logs,
|
||||
commands::binaries::get_launch_game_string,
|
||||
commands::binaries::launch_game,
|
||||
commands::binaries::open_repl,
|
||||
|
|
|
@ -2,5 +2,6 @@ pub mod file;
|
|||
pub mod game_milestones;
|
||||
pub mod network;
|
||||
pub mod os;
|
||||
pub mod process;
|
||||
pub mod tar;
|
||||
pub mod zip;
|
||||
|
|
|
@ -47,24 +47,6 @@ pub fn read_lines_in_file(path: &PathBuf) -> Result<String, Box<dyn std::error::
|
|||
Ok(std::fs::read_to_string(path)?)
|
||||
}
|
||||
|
||||
pub fn read_last_lines_from_file(path: &PathBuf, lines: usize) -> Result<String, std::io::Error> {
|
||||
if !path.exists() {
|
||||
return Ok("".to_owned());
|
||||
}
|
||||
let buf = rev_buf_reader::RevBufReader::new(std::fs::File::open(path)?);
|
||||
Ok(
|
||||
buf
|
||||
.lines()
|
||||
.take(lines)
|
||||
.map(|l| l.unwrap_or("".to_owned()))
|
||||
.collect::<Vec<String>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn touch_file(path: &PathBuf) -> std::io::Result<()> {
|
||||
match std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
|
|
119
src-tauri/src/util/process.rs
Normal file
119
src-tauri/src/util/process.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
use std::{process::ExitStatus, sync::Arc, time::Duration};
|
||||
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use crate::commands::CommandError;
|
||||
|
||||
use super::file::create_dir;
|
||||
use tauri::Manager;
|
||||
|
||||
pub async fn create_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: String,
|
||||
append: bool,
|
||||
) -> Result<tokio::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = tokio::fs::OpenOptions::new();
|
||||
file_options.read(true);
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name)).await?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct LogPayload {
|
||||
logs: String,
|
||||
}
|
||||
|
||||
pub async fn watch_process(
|
||||
log_file: &mut tokio::fs::File,
|
||||
child: &mut tokio::process::Child,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> Result<Option<ExitStatus>, CommandError> {
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
|
||||
let mut stdout_reader = tokio::io::BufReader::new(stdout).lines();
|
||||
let mut stderr_reader = tokio::io::BufReader::new(stderr).lines();
|
||||
let combined_buffer = Arc::new(Mutex::new(String::new()));
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(25));
|
||||
|
||||
let mut process_status = None;
|
||||
loop {
|
||||
let buffer_clone = Arc::clone(&combined_buffer);
|
||||
tokio::select! {
|
||||
Ok(Some(line)) = stdout_reader.next_line() => {
|
||||
let formatted_line = format!("{line}\n");
|
||||
log_file.write_all(formatted_line.as_bytes()).await?;
|
||||
if formatted_line != "\n" {
|
||||
let mut buf = buffer_clone.lock().await;
|
||||
buf.push_str(&formatted_line);
|
||||
}
|
||||
},
|
||||
Ok(Some(line)) = stderr_reader.next_line() => {
|
||||
let formatted_line = format!("{line}\n");
|
||||
log_file.write_all(formatted_line.as_bytes()).await?;
|
||||
if formatted_line != "\n" {
|
||||
let mut buf = buffer_clone.lock().await;
|
||||
buf.push_str(&formatted_line);
|
||||
}
|
||||
},
|
||||
_ = interval.tick() => {
|
||||
log_file.flush().await?;
|
||||
{
|
||||
let mut buf = buffer_clone.lock().await;
|
||||
let _ = app_handle.emit_all("log_update", LogPayload { logs: buf.clone() });
|
||||
buf.clear();
|
||||
}
|
||||
},
|
||||
// Wait for the child process to finish
|
||||
status = child.wait() => {
|
||||
process_status = Some(status?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(process_status);
|
||||
}
|
||||
|
||||
pub fn create_std_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: String,
|
||||
append: bool,
|
||||
) -> Result<std::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = std::fs::OpenOptions::new();
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name))?;
|
||||
Ok(file)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
// Other Imports
|
||||
import { onMount } from "svelte";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { Router, Route } from "svelte-navigator";
|
||||
import Game from "./routes/Game.svelte";
|
||||
import Settings from "./routes/Settings.svelte";
|
||||
|
@ -15,8 +15,11 @@
|
|||
import { isLoading } from "svelte-i18n";
|
||||
import { getLocale, setLocale } from "$lib/rpc/config";
|
||||
import GameFeature from "./routes/GameFeature.svelte";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { toastStore } from "$lib/stores/ToastStore";
|
||||
|
||||
let revokeSpecificActions = false;
|
||||
let toastListener: any = undefined;
|
||||
|
||||
// Events
|
||||
onMount(async () => {
|
||||
|
@ -32,6 +35,16 @@
|
|||
if (locale !== null) {
|
||||
setLocale(locale);
|
||||
}
|
||||
|
||||
toastListener = await listen("toast_msg", (event) => {
|
||||
toastStore.makeToast(event.payload.toast, event.payload.level);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (toastListener !== undefined) {
|
||||
toastListener();
|
||||
}
|
||||
});
|
||||
|
||||
if (!isInDebugMode()) {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"features_textures_moveDown_buttonAlt": "move texture pack down in order",
|
||||
"features_textures_moveUp_buttonAlt": "move texture pack up in order",
|
||||
"features_textures_replacedCount": "Textures replaced",
|
||||
"features_textures_largePackWarning": "Very large texture packs (hundreds of megabytes) may fail to install or impact game performance.",
|
||||
"gameControls_update_mod": "Update",
|
||||
"gameControls_active": "(Active)",
|
||||
"gameControls_always_use_newest": "Always Use Newest",
|
||||
|
@ -69,6 +70,7 @@
|
|||
"header_updateAvailable": "Update Available!",
|
||||
"help_button_downloadPackage": "Download Support Package",
|
||||
"help_button_openLogFolder": "Open Log Folder",
|
||||
"help_button_defaultKeybinds": "Default Keybinds",
|
||||
"help_button_reportGameIssue": "Report Game Issue",
|
||||
"help_button_reportLauncherIssue": "Report Launcher Issue",
|
||||
"help_description_createAnIssue": "You can either ask a question on our Discord, or create a GitHub issue with as much detail as possible.",
|
||||
|
@ -139,6 +141,7 @@
|
|||
"settings_versions_table_header_changes": "Changes",
|
||||
"settings_versions_table_header_date": "Date",
|
||||
"settings_versions_table_header_version": "Version",
|
||||
"settings_versions_noReleasesFound": "No releases could be retrieved from GitHub!",
|
||||
"setup_button_continue": "Continue",
|
||||
"setup_button_getSupportPackage": "Get Support Package",
|
||||
"setup_button_installViaISO": "Install via ISO",
|
||||
|
@ -219,5 +222,7 @@
|
|||
"toasts_modSourceUnreachable": "Mod source unreachable",
|
||||
"toasts_couldNotRemoveModSource": "Unable to remove mod source",
|
||||
"toasts_modSourceDuplicateName": "Mod source has the same display name as one you already have added",
|
||||
"toasts_unableToRetrieveModDownloadURL": "Unable to retrieve mod download URL"
|
||||
"toasts_unableToRetrieveModDownloadURL": "Unable to retrieve mod download URL",
|
||||
"toasts_githubRateLimit": "Unable to hit GitHub's API, you are rate-limited",
|
||||
"toasts_githubUnexpectedError": "Unexpected error when hitting GitHub's API"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { getInternalName, SupportedGame } from "$lib/constants";
|
||||
import { openDir } from "$lib/rpc/window";
|
||||
import IconArrowLeft from "~icons/mdi/arrow-left";
|
||||
import IconCog from "~icons/mdi/cog";
|
||||
import { configDir, join } from "@tauri-apps/api/path";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
@ -27,7 +26,6 @@
|
|||
import { navigate } from "svelte-navigator";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { toastStore } from "$lib/stores/ToastStore";
|
||||
import { launchMod } from "$lib/rpc/features";
|
||||
|
||||
export let activeGame: SupportedGame;
|
||||
|
||||
|
@ -172,13 +170,11 @@
|
|||
launchGameWithCustomExecutable(getInternalName(activeGame));
|
||||
}}>Launch with Custom Executable</DropdownItem
|
||||
>
|
||||
{#if !isLinux}
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
openREPL(getInternalName(activeGame));
|
||||
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
||||
>
|
||||
{/if}
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
openREPL(getInternalName(activeGame));
|
||||
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
||||
>
|
||||
<DropdownDivider />
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
|
|
|
@ -330,13 +330,11 @@
|
|||
launchMod(getInternalName(activeGame), true, modName, modSource);
|
||||
}}>{$_("gameControls_button_playInDebug")}</DropdownItem
|
||||
>
|
||||
{#if !isLinux}
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
openREPLForMod(getInternalName(activeGame), modName, modSource);
|
||||
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
||||
>
|
||||
{/if}
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
openREPLForMod(getInternalName(activeGame), modName, modSource);
|
||||
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
||||
>
|
||||
<DropdownDivider />
|
||||
<DropdownItem
|
||||
on:click={async () => {
|
||||
|
|
|
@ -261,6 +261,12 @@
|
|||
{packAddingError}
|
||||
</Alert>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row font-bold mt-3">
|
||||
<Alert color="red" class="flex-grow">
|
||||
{$_("features_textures_largePackWarning")}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row font-bold mt-3">
|
||||
<h2>{$_("features_textures_listHeading")}</h2>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import type { Job } from "$lib/utils/jobs";
|
||||
import { getInternalName, type SupportedGame } from "$lib/constants";
|
||||
import {
|
||||
getEndOfLogs,
|
||||
runCompiler,
|
||||
runDecompiler,
|
||||
updateDataDirectory,
|
||||
|
@ -60,7 +59,6 @@
|
|||
]);
|
||||
progressTracker.start();
|
||||
let resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -84,7 +82,6 @@
|
|||
]);
|
||||
progressTracker.start();
|
||||
let resp = await runCompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -116,7 +113,6 @@
|
|||
]);
|
||||
progressTracker.start();
|
||||
let resp = await updateDataDirectory(getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -124,7 +120,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -132,7 +127,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runCompiler("", getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -198,7 +192,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -248,7 +241,6 @@
|
|||
modSourceName,
|
||||
sourcePath,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -266,7 +258,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -278,7 +269,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -338,7 +328,6 @@
|
|||
modSourceName,
|
||||
sourcePath,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -368,7 +357,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -380,7 +368,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -420,7 +407,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -449,8 +435,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
// TODO - stream logs
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -494,9 +478,7 @@
|
|||
|
||||
<div class="flex flex-col justify-content">
|
||||
<Progress />
|
||||
{#if $progressTracker.logs !== undefined}
|
||||
<LogViewer />
|
||||
{/if}
|
||||
<LogViewer />
|
||||
</div>
|
||||
{#if $progressTracker.overallStatus === "success"}
|
||||
<div class="flex flex-col justify-end items-end mt-auto">
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { Alert, Button } from "flowbite-svelte";
|
||||
import {
|
||||
extractAndValidateISO,
|
||||
getEndOfLogs,
|
||||
runCompiler,
|
||||
runDecompiler,
|
||||
} from "$lib/rpc/binaries";
|
||||
|
@ -92,7 +91,6 @@
|
|||
sourcePath,
|
||||
getInternalName(activeGame),
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -100,7 +98,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runDecompiler(sourcePath, getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -108,7 +105,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runCompiler(sourcePath, getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -129,11 +125,9 @@
|
|||
{#if !requirementsMet}
|
||||
<Requirements {activeGame} on:recheckRequirements={checkRequirements} />
|
||||
{:else if installing}
|
||||
<div class="flex flex-col justify-content">
|
||||
<div class="flex flex-col justify-content shrink">
|
||||
<Progress />
|
||||
{#if $progressTracker.logs !== undefined}
|
||||
<LogViewer />
|
||||
{/if}
|
||||
<LogViewer />
|
||||
</div>
|
||||
{#if $progressTracker.overallStatus === "success"}
|
||||
<div class="flex flex-col justify-end items-end mt-auto">
|
||||
|
|
|
@ -1,31 +1,40 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { progressTracker } from "$lib/stores/ProgressStore";
|
||||
import IconDocument from "~icons/mdi/file-document-outline";
|
||||
import { Accordion, AccordionItem } from "flowbite-svelte";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { ansiSpan } from "ansi-to-span";
|
||||
import escapeHtml from "escape-html";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let logListener: any = undefined;
|
||||
let logElement;
|
||||
|
||||
const scrollToBottom = async (node) => {
|
||||
node.scroll({ top: node.scrollHeight, behavior: "instant" });
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
logListener = await listen("log_update", (event) => {
|
||||
progressTracker.appendLogs(event.payload.logs);
|
||||
if (logElement) {
|
||||
scrollToBottom(logElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (logListener !== undefined) {
|
||||
logListener();
|
||||
}
|
||||
});
|
||||
|
||||
function convertLogColors(text) {
|
||||
return ansiSpan(escapeHtml(text)).replaceAll("\n", "<br/>");
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion class="log-accordian p-0 mb-2">
|
||||
<AccordionItem class="bg-slate-900 rounded p-[1rem]">
|
||||
<span slot="header" class="text-sm font-semibold text-white flex gap-2">
|
||||
<IconDocument />
|
||||
<span>{$_("setup_logs_header")}</span>
|
||||
</span>
|
||||
<div
|
||||
slot="default"
|
||||
class="bg-slate-900 px-4 max-h-52 overflow-y-scroll scrollbar"
|
||||
>
|
||||
<p class="py-4 text-clip overflow-hidden font-mono log-output">
|
||||
...{$_("setup_logs_truncation")}:
|
||||
<br />
|
||||
{@html convertLogColors($progressTracker.logs)}
|
||||
</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{#if $progressTracker.logs}
|
||||
<pre
|
||||
class="rounded p-2 bg-[#141414] text-[11px] max-h-[300px] overflow-auto text-pretty font-mono"
|
||||
bind:this={logElement}>{@html convertLogColors($progressTracker.logs)}</pre>
|
||||
{/if}
|
||||
|
|
|
@ -75,7 +75,10 @@
|
|||
$VersionStore.activeVersionType === "official"
|
||||
) {
|
||||
const latestToolingVersion = await getLatestOfficialRelease();
|
||||
if ($VersionStore.activeVersionName !== latestToolingVersion.version) {
|
||||
if (
|
||||
latestToolingVersion !== undefined &&
|
||||
$VersionStore.activeVersionName !== latestToolingVersion.version
|
||||
) {
|
||||
// Check that we havn't already downloaded it
|
||||
let alreadyHaveRelease = false;
|
||||
const downloadedOfficialVersions =
|
||||
|
@ -127,11 +130,6 @@
|
|||
{$VersionStore.activeVersionName === null
|
||||
? "not set!"
|
||||
: $VersionStore.activeVersionName}
|
||||
{#if $VersionStore.activeVersionType === "unofficial"}
|
||||
(unf)
|
||||
{:else if $VersionStore.activeVersionType === "devel"}
|
||||
(dev)
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -60,3 +60,7 @@
|
|||
.font-mono {
|
||||
font-family: "Noto Sans Mono", monospace !important;
|
||||
}
|
||||
|
||||
.font-mono span {
|
||||
font-family: "Noto Sans Mono", monospace !important;
|
||||
}
|
||||
|
|
|
@ -215,5 +215,7 @@ export async function initLocales(async: boolean) {
|
|||
});
|
||||
if (!async) {
|
||||
await initPromise;
|
||||
} else {
|
||||
return initPromise;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { filePrompt } from "$lib/utils/file-dialogs";
|
||||
import { filePrompt, filePromptNoFilters } from "$lib/utils/file-dialogs";
|
||||
import { invoke_rpc } from "./rpc";
|
||||
|
||||
interface InstallationOutput {
|
||||
|
@ -18,10 +18,6 @@ export async function updateDataDirectory(
|
|||
);
|
||||
}
|
||||
|
||||
export async function getEndOfLogs(): Promise<string> {
|
||||
return await invoke_rpc("get_end_of_logs", {}, () => "");
|
||||
}
|
||||
|
||||
export async function extractAndValidateISO(
|
||||
pathToIso: string,
|
||||
gameName: string,
|
||||
|
@ -81,7 +77,7 @@ export async function launchGameWithCustomExecutable(
|
|||
gameName: string,
|
||||
): Promise<void> {
|
||||
// Get custom executable location
|
||||
const customExecutable = await filePrompt(["exe"], "executables", "pick exe");
|
||||
const customExecutable = await filePromptNoFilters("Select custom 'gk'");
|
||||
if (customExecutable !== null) {
|
||||
return await invoke_rpc(
|
||||
"launch_game",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { invoke_rpc } from "./rpc";
|
||||
|
||||
export type VersionFolders = null | "official" | "unofficial" | "devel";
|
||||
export type VersionFolders = null | "official";
|
||||
|
||||
export async function listDownloadedVersions(
|
||||
versionFolder: VersionFolders,
|
||||
|
|
|
@ -16,7 +16,7 @@ interface ProgressTracker {
|
|||
currentStep: number;
|
||||
overallStatus: ProgressStatus;
|
||||
steps: ProgressStep[];
|
||||
logs: string;
|
||||
logs: string | undefined;
|
||||
}
|
||||
|
||||
const storeValue: ProgressTracker = {
|
||||
|
@ -66,9 +66,12 @@ function createProgressTracker() {
|
|||
val.steps[val.currentStep].status = "failed";
|
||||
return val;
|
||||
}),
|
||||
updateLogs: (logs: string) =>
|
||||
appendLogs: (logs: string) =>
|
||||
update((val) => {
|
||||
val.logs = logs;
|
||||
if (val.logs === undefined) {
|
||||
val.logs = "";
|
||||
}
|
||||
val.logs += logs;
|
||||
return val;
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,5 +1,22 @@
|
|||
import { open, save } from "@tauri-apps/api/dialog";
|
||||
|
||||
export async function filePromptNoFilters(
|
||||
title: string,
|
||||
): Promise<string | null> {
|
||||
const path = await open({
|
||||
title: title,
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: undefined,
|
||||
});
|
||||
|
||||
if (Array.isArray(path) || path === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export async function filePrompt(
|
||||
extensions: string[],
|
||||
name: string,
|
||||
|
|
|
@ -1,12 +1,28 @@
|
|||
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
type Mock,
|
||||
} from "vitest";
|
||||
import { arch, platform } from "@tauri-apps/api/os";
|
||||
import { listOfficialReleases } from "./github";
|
||||
import { init } from "svelte-i18n";
|
||||
import { initLocales } from "$lib/i18n/i18n";
|
||||
|
||||
vi.mock("@tauri-apps/api/os");
|
||||
global.fetch = vi.fn();
|
||||
|
||||
function createFetchResponse(data: any) {
|
||||
return { json: () => new Promise((resolve) => resolve(data)) };
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Map(),
|
||||
json: () => new Promise((resolve) => resolve(data)),
|
||||
};
|
||||
}
|
||||
|
||||
function createFakeGithubReleaseAsset(assetName) {
|
||||
|
@ -121,6 +137,10 @@ function createFakeGithubRelease(assetNames: string[]) {
|
|||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initLocales(true);
|
||||
});
|
||||
|
||||
describe("listOfficialReleases", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { toastStore } from "$lib/stores/ToastStore";
|
||||
import { arch, platform } from "@tauri-apps/api/os";
|
||||
import semver from "semver";
|
||||
import { unwrapFunctionStore, format } from "svelte-i18n";
|
||||
|
||||
const $format = unwrapFunctionStore(format);
|
||||
|
||||
export interface ReleaseInfo {
|
||||
releaseType: "official" | "unofficial" | "devel";
|
||||
releaseType: "official";
|
||||
version: string;
|
||||
date: string | undefined;
|
||||
githubLink: string | undefined;
|
||||
|
@ -26,17 +28,12 @@ function isIntelMacOsRelease(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO - go back and fix old asset names so windows/linux can be simplified
|
||||
function isWindowsRelease(
|
||||
platform: string,
|
||||
architecture: string,
|
||||
assetName: string,
|
||||
): boolean {
|
||||
return (
|
||||
platform === "win32" &&
|
||||
(assetName.startsWith("opengoal-windows-v") ||
|
||||
(assetName.startsWith("opengoal-v") && assetName.includes("windows")))
|
||||
);
|
||||
return platform === "win32" && assetName.startsWith("opengoal-windows-v");
|
||||
}
|
||||
|
||||
function isLinuxRelease(
|
||||
|
@ -44,11 +41,7 @@ function isLinuxRelease(
|
|||
architecture: string,
|
||||
assetName: string,
|
||||
): boolean {
|
||||
return (
|
||||
platform === "linux" &&
|
||||
(assetName.startsWith("opengoal-linux-v") ||
|
||||
(assetName.startsWith("opengoal-v") && assetName.includes("linux")))
|
||||
);
|
||||
return platform === "linux" && assetName.startsWith("opengoal-linux-v");
|
||||
}
|
||||
|
||||
async function getDownloadLinkForCurrentPlatform(
|
||||
|
@ -99,35 +92,53 @@ async function parseGithubRelease(githubRelease: any): Promise<ReleaseInfo> {
|
|||
}
|
||||
|
||||
export async function listOfficialReleases(): Promise<ReleaseInfo[]> {
|
||||
return listReleases("official", "open-goal/jak-project");
|
||||
}
|
||||
|
||||
export async function listReleases(
|
||||
releaseType: string,
|
||||
repo: string,
|
||||
): Promise<ReleaseInfo[]> {
|
||||
const nextUrlPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i;
|
||||
let releases = [];
|
||||
// TODO - handle rate limiting
|
||||
// TODO - long term - handle pagination (more than 100 releases)
|
||||
const resp = await fetch(
|
||||
"https://api.github.com/repos/open-goal/jak-project/releases?per_page=100",
|
||||
);
|
||||
// TODO - handle error
|
||||
const githubReleases = await resp.json();
|
||||
let urlToHit =
|
||||
"https://api.github.com/repos/open-goal/jak-project/releases?per_page=100";
|
||||
|
||||
for (const release of githubReleases) {
|
||||
releases.push(await parseGithubRelease(release));
|
||||
while (urlToHit !== undefined) {
|
||||
const resp = await fetch(urlToHit);
|
||||
if (resp.status === 403 || resp.status === 429) {
|
||||
toastStore.makeToast($format("toasts_githubRateLimit"), "error");
|
||||
return [];
|
||||
} else if (!resp.ok) {
|
||||
toastStore.makeToast($format("toasts_githubUnexpectedError"), "error");
|
||||
return [];
|
||||
}
|
||||
|
||||
const githubReleases = await resp.json();
|
||||
for (const release of githubReleases) {
|
||||
releases.push(await parseGithubRelease(release));
|
||||
}
|
||||
|
||||
if (
|
||||
resp.headers.has("link") &&
|
||||
resp.headers.get("link").includes(`rel=\"next\"`)
|
||||
) {
|
||||
// we must paginate!
|
||||
urlToHit = resp.headers.get("link").match(nextUrlPattern)[0];
|
||||
} else {
|
||||
urlToHit = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return releases.sort((a, b) => b.date.localeCompare(a.date));
|
||||
}
|
||||
|
||||
export async function getLatestOfficialRelease(): Promise<ReleaseInfo> {
|
||||
// TODO - handle rate limiting
|
||||
export async function getLatestOfficialRelease(): Promise<
|
||||
ReleaseInfo | undefined
|
||||
> {
|
||||
const resp = await fetch(
|
||||
"https://api.github.com/repos/open-goal/jak-project/releases/latest",
|
||||
);
|
||||
// TODO - handle error
|
||||
if (resp.status === 403 || resp.status === 429) {
|
||||
toastStore.makeToast($format("toasts_githubRateLimit"), "error");
|
||||
return undefined;
|
||||
} else if (!resp.ok) {
|
||||
toastStore.makeToast($format("toasts_githubUnexpectedError"), "error");
|
||||
return undefined;
|
||||
}
|
||||
const githubRelease = await resp.json();
|
||||
return await parseGithubRelease(githubRelease);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { onMount } from "svelte";
|
||||
import { appConfigDir } from "@tauri-apps/api/path";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { navigate } from "svelte-navigator";
|
||||
|
||||
let appDir: string | undefined = undefined;
|
||||
let downloadingPackage = false;
|
||||
|
@ -46,6 +47,12 @@
|
|||
}}>{$_("help_button_openLogFolder")}</Button
|
||||
>
|
||||
{/if}
|
||||
<Button
|
||||
class="flex items-center border-solid rounded bg-white hover:bg-orange-400 text-sm text-slate-900 font-semibold px-4 py-2"
|
||||
href="https://raw.githubusercontent.com/open-goal/launcher/refs/heads/main/docs/default-keybinds.png"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{$_("help_button_defaultKeybinds")}</Button
|
||||
>
|
||||
</div>
|
||||
<p class="mt-3 text-sm">
|
||||
{$_("help_description_createAnIssue")}
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
},
|
||||
];
|
||||
}
|
||||
// TODO - "no releases found"
|
||||
|
||||
// Merge that with the actual current releases on github
|
||||
const githubReleases = await listOfficialReleases();
|
||||
|
@ -111,8 +110,6 @@
|
|||
if (success) {
|
||||
$VersionStore.activeVersionType = "official";
|
||||
$VersionStore.activeVersionName = $VersionStore.selectedVersions.official;
|
||||
$VersionStore.selectedVersions.unofficial = null;
|
||||
$VersionStore.selectedVersions.devel = null;
|
||||
toastStore.makeToast($_("toasts_savedToolingVersion"), "info");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import IconDownload from "~icons/mdi/download";
|
||||
import IconDeleteForever from "~icons/mdi/delete-forever";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Radio,
|
||||
Spinner,
|
||||
|
@ -60,144 +61,152 @@
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_version")}</TableHeadCell
|
||||
>
|
||||
<TableHeadCell>{$_("settings_versions_table_header_date")}</TableHeadCell>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_changes")}</TableHeadCell
|
||||
>
|
||||
</TableHead>
|
||||
<TableBody tableBodyClass="divide-y">
|
||||
{#each releaseList as release (release.version)}
|
||||
<TableBodyRow>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.isDownloaded}
|
||||
<Radio
|
||||
class="disabled:cursor-not-allowed p-0"
|
||||
bind:group={$VersionStore.selectedVersions[releaseType]}
|
||||
on:change={() => dispatch("versionChange")}
|
||||
value={release.version}
|
||||
disabled={!release.isDownloaded}
|
||||
name={`${releaseType}-release`}
|
||||
/>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell
|
||||
class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
style="line-height: 0;"
|
||||
>
|
||||
<Button
|
||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||
disabled={release.pendingAction ||
|
||||
(!release.isDownloaded &&
|
||||
release.downloadUrl !== undefined &&
|
||||
release.invalid)}
|
||||
on:click={async () => {
|
||||
if (release.isDownloaded) {
|
||||
dispatch("removeVersion", { version: release.version });
|
||||
} else {
|
||||
dispatch("downloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if releaseList === undefined || releaseList.length <= 0}
|
||||
<Alert color="red" class="dark:bg-slate-900 flex-grow">
|
||||
{$_("settings_versions_noReleasesFound")}
|
||||
</Alert>
|
||||
{:else}
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell>
|
||||
<span class="sr-only"></span>
|
||||
</TableHeadCell>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_version")}</TableHeadCell
|
||||
>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_date")}</TableHeadCell
|
||||
>
|
||||
<TableHeadCell
|
||||
>{$_("settings_versions_table_header_changes")}</TableHeadCell
|
||||
>
|
||||
</TableHead>
|
||||
<TableBody tableBodyClass="divide-y">
|
||||
{#each releaseList as release (release.version)}
|
||||
<TableBodyRow>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.isDownloaded}
|
||||
<IconDeleteForever
|
||||
class="text-xl"
|
||||
color="red"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_removeVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{:else if release.downloadUrl === undefined}
|
||||
<span>{$_("settings_versions_incompatibleVersion")}</span>
|
||||
{:else if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else if release.releaseType === "official" && release.downloadUrl !== undefined}
|
||||
<IconDownload
|
||||
class="text-xl"
|
||||
color="#00d500"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_downloadVersion_altText",
|
||||
)}
|
||||
<Radio
|
||||
class="disabled:cursor-not-allowed p-0"
|
||||
bind:group={$VersionStore.selectedVersions[releaseType]}
|
||||
on:change={() => dispatch("versionChange")}
|
||||
value={release.version}
|
||||
disabled={!release.isDownloaded}
|
||||
name={`${releaseType}-release`}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if release.invalid}
|
||||
<Tooltip color="red">
|
||||
{#if release.invalidationReasons.length > 0}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{#each release.invalidationReasons as reason}
|
||||
<br />
|
||||
- {reason}
|
||||
{/each}
|
||||
{:else}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{/if}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if release.isDownloaded && release.releaseType == "official"}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell
|
||||
class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
style="line-height: 0;"
|
||||
>
|
||||
<Button
|
||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||
disabled={release.pendingAction}
|
||||
disabled={release.pendingAction ||
|
||||
(!release.isDownloaded &&
|
||||
release.downloadUrl !== undefined &&
|
||||
release.invalid)}
|
||||
on:click={async () => {
|
||||
dispatch("redownloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
if (release.isDownloaded) {
|
||||
dispatch("removeVersion", { version: release.version });
|
||||
} else {
|
||||
dispatch("downloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else}
|
||||
<IconRefresh
|
||||
{#if release.isDownloaded}
|
||||
<IconDeleteForever
|
||||
class="text-xl"
|
||||
color="red"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_redownloadVersion_altText",
|
||||
"settings_versions_icon_removeVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{:else if release.downloadUrl === undefined}
|
||||
<span>{$_("settings_versions_incompatibleVersion")}</span>
|
||||
{:else if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else if release.releaseType === "official" && release.downloadUrl !== undefined}
|
||||
<IconDownload
|
||||
class="text-xl"
|
||||
color="#00d500"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_downloadVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
>{release.version}</TableBodyCell
|
||||
>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.date}
|
||||
{new Date(release.date).toLocaleDateString()}
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.githubLink}
|
||||
<a
|
||||
class="inline-block"
|
||||
href={release.githubLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<IconGitHub
|
||||
class="text-xl"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_githubRelease_altText",
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
</TableBodyRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{#if release.invalid}
|
||||
<Tooltip color="red">
|
||||
{#if release.invalidationReasons.length > 0}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{#each release.invalidationReasons as reason}
|
||||
<br />
|
||||
- {reason}
|
||||
{/each}
|
||||
{:else}
|
||||
{$_("settings_versions_invalidReleaseGeneric")}
|
||||
{/if}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if release.isDownloaded && release.releaseType == "official"}
|
||||
<Button
|
||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||
disabled={release.pendingAction}
|
||||
on:click={async () => {
|
||||
dispatch("redownloadVersion", {
|
||||
version: release.version,
|
||||
downloadUrl: release.downloadUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{#if release.pendingAction}
|
||||
<Spinner color="yellow" size={"6"} />
|
||||
{:else}
|
||||
<IconRefresh
|
||||
class="text-xl"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_redownloadVersion_altText",
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium"
|
||||
>{release.version}</TableBodyCell
|
||||
>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.date}
|
||||
{new Date(release.date).toLocaleDateString()}
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||
{#if release.githubLink}
|
||||
<a
|
||||
class="inline-block"
|
||||
href={release.githubLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<IconGitHub
|
||||
class="text-xl"
|
||||
aria-label={$_(
|
||||
"settings_versions_icon_githubRelease_altText",
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
</TableBodyRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
Loading…
Reference in a new issue