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.
|
The launcher uses the [Tauri](https://tauri.app/) framework.
|
||||||
|
|
||||||
- [Usage](#usage)
|
- [OpenGOAL Launcher](#opengoal-launcher)
|
||||||
- [Asking for help](#asking-for-help)
|
- [Usage](#usage)
|
||||||
- [Development](#development)
|
- [Asking for help](#asking-for-help)
|
||||||
- [Windows](#windows)
|
- [Development](#development)
|
||||||
- [Linux (Ubuntu 22.04)](#linux-ubuntu-2204)
|
- [Windows](#windows)
|
||||||
- [Building and Running](#building-and-running)
|
- [Linux (Ubuntu 22.04)](#linux-ubuntu-2204)
|
||||||
|
- [macOS](#macos)
|
||||||
|
- [Building and Running](#building-and-running)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -53,6 +55,12 @@ nvm install lts/hydrogen # installs latest nodejs 18.X
|
||||||
npm install -g yarn
|
npm install -g yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g yarn
|
||||||
|
```
|
||||||
|
|
||||||
### Building and Running
|
### Building and Running
|
||||||
|
|
||||||
To build and run the application locally, all you have to do is run:
|
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",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
|
|
|
@ -43,6 +43,7 @@ zip = { version = "2.2.0", features = ["deflate-zlib-ng"] }
|
||||||
zip-extract = "0.2.1"
|
zip-extract = "0.2.1"
|
||||||
tempfile = "3.12.0"
|
tempfile = "3.12.0"
|
||||||
native-dialog = "0.7.0"
|
native-dialog = "0.7.0"
|
||||||
|
tokio-util = "0.7.12"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.52.0"
|
winreg = "0.52.0"
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
io::ErrorKind,
|
||||||
|
os::windows::process::CommandExt,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Stdio,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
use tokio::{io::AsyncWriteExt, process::Command};
|
||||||
|
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
|
@ -16,7 +17,10 @@ use tauri::Manager;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::LauncherConfig,
|
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,
|
TAURI_APP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +40,12 @@ struct CommonConfigData {
|
||||||
tooling_version: Version,
|
tooling_version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize)]
|
||||||
|
struct ToastPayload {
|
||||||
|
toast: String,
|
||||||
|
level: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn common_prelude(
|
fn common_prelude(
|
||||||
config: &tokio::sync::MutexGuard<LauncherConfig>,
|
config: &tokio::sync::MutexGuard<LauncherConfig>,
|
||||||
) -> Result<CommonConfigData, CommandError> {
|
) -> 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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct InstallStepOutput {
|
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]
|
#[tauri::command]
|
||||||
pub async fn extract_and_validate_iso(
|
pub async fn extract_and_validate_iso(
|
||||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||||
|
@ -307,23 +280,34 @@ pub async fn extract_and_validate_iso(
|
||||||
args.push(game_name.clone());
|
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);
|
log::info!("Running extractor with args: {:?}", args);
|
||||||
|
|
||||||
let mut command = Command::new(exec_info.executable_path);
|
let mut command = Command::new(exec_info.executable_path);
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(exec_info.executable_dir)
|
.stdout(Stdio::piped())
|
||||||
.stdout(log_file.try_clone()?)
|
.stderr(Stdio::piped())
|
||||||
.stderr(log_file.try_clone()?);
|
.current_dir(exec_info.executable_dir);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
command.creation_flags(0x08000000);
|
||||||
}
|
}
|
||||||
let output = command.output()?;
|
let mut child = command.spawn()?;
|
||||||
match output.status.code() {
|
|
||||||
|
// 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) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
log::info!("extraction and validation was successful");
|
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);
|
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||||
log::error!("extraction and validation was not successful. 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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some(message.msg.clone()),
|
msg: Some(message.msg.clone()),
|
||||||
|
@ -347,8 +329,6 @@ pub async fn extract_and_validate_iso(
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("extraction and validation was not successful. No status code");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some("Unexpected error occurred".to_owned()),
|
msg: Some("Unexpected error occurred".to_owned()),
|
||||||
|
@ -393,7 +373,6 @@ pub async fn run_decompiler(
|
||||||
.to_string();
|
.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 command = Command::new(exec_info.executable_path);
|
||||||
|
|
||||||
let mut decomp_config_overrides = vec![];
|
let mut decomp_config_overrides = vec![];
|
||||||
|
@ -443,15 +422,35 @@ pub async fn run_decompiler(
|
||||||
|
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.stdout(log_file.try_clone()?)
|
.stdout(Stdio::piped())
|
||||||
.stderr(log_file)
|
.stderr(Stdio::piped())
|
||||||
.current_dir(exec_info.executable_dir);
|
.current_dir(exec_info.executable_dir);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
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) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
log::info!("decompilation was successful");
|
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);
|
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||||
log::error!("decompilation was not successful. Code {code}");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some(message.msg.clone()),
|
msg: Some(message.msg.clone()),
|
||||||
|
@ -475,8 +472,6 @@ pub async fn run_decompiler(
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("decompilation was not successful. No status code");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some("Unexpected error occurred".to_owned()),
|
msg: Some("Unexpected error occurred".to_owned()),
|
||||||
|
@ -520,7 +515,6 @@ pub async fn run_compiler(
|
||||||
.to_string();
|
.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let log_file = create_log_file(&app_handle, "extractor.log", !truncate_logs)?;
|
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
source_path,
|
source_path,
|
||||||
"--compile".to_string(),
|
"--compile".to_string(),
|
||||||
|
@ -538,15 +532,25 @@ pub async fn run_compiler(
|
||||||
let mut command = Command::new(exec_info.executable_path);
|
let mut command = Command::new(exec_info.executable_path);
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.stdout(log_file.try_clone().unwrap())
|
.stdout(Stdio::piped())
|
||||||
.stderr(log_file)
|
.stderr(Stdio::piped())
|
||||||
.current_dir(exec_info.executable_dir);
|
.current_dir(exec_info.executable_dir);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
command.creation_flags(0x08000000);
|
||||||
}
|
}
|
||||||
let output = command.output()?;
|
let mut child = command.spawn()?;
|
||||||
match output.status.code() {
|
|
||||||
|
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) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
log::info!("compilation was successful");
|
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);
|
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||||
log::error!("compilation was not successful. Code {code}");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some(message.msg.clone()),
|
msg: Some(message.msg.clone()),
|
||||||
|
@ -570,8 +572,6 @@ pub async fn run_compiler(
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("compilation was not successful. No status code");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some("Unexpected error occurred".to_owned()),
|
msg: Some("Unexpected error occurred".to_owned()),
|
||||||
|
@ -583,33 +583,66 @@ pub async fn run_compiler(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn open_repl(
|
pub async fn open_repl(
|
||||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
game_name: String,
|
game_name: String,
|
||||||
) -> Result<(), CommandError> {
|
) -> 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_lock = config.lock().await;
|
||||||
let config_info = common_prelude(&config_lock)?;
|
let config_info = common_prelude(&config_lock)?;
|
||||||
|
|
||||||
let data_folder = get_data_dir(&config_info, &game_name, false)?;
|
let data_folder = get_data_dir(&config_info, &game_name, false)?;
|
||||||
let exec_info = get_exec_location(&config_info, "goalc")?;
|
let exec_info = get_exec_location(&config_info, "goalc")?;
|
||||||
let mut command = Command::new("cmd");
|
let mut command;
|
||||||
command
|
|
||||||
.args([
|
|
||||||
"/K",
|
|
||||||
"start",
|
|
||||||
&bin_ext("goalc"),
|
|
||||||
"--proj-path",
|
|
||||||
&data_folder.to_string_lossy(),
|
|
||||||
])
|
|
||||||
.current_dir(exec_info.executable_dir);
|
|
||||||
#[cfg(windows)]
|
#[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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -658,7 +691,7 @@ pub async fn run_game_gpu_test(
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
command.creation_flags(0x08000000);
|
||||||
}
|
}
|
||||||
let output = command.output()?;
|
let output = command.output().await?;
|
||||||
match output.status.code() {
|
match output.status.code() {
|
||||||
Some(code) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
|
@ -769,13 +802,26 @@ pub async fn launch_game(
|
||||||
let config_info = common_prelude(&config_lock)?;
|
let config_info = common_prelude(&config_lock)?;
|
||||||
|
|
||||||
let mut exec_info = get_exec_location(&config_info, "gk")?;
|
let mut exec_info = get_exec_location(&config_info, "gk")?;
|
||||||
if executable_location.is_some() {
|
if let Some(custom_exec_location) = executable_location {
|
||||||
let exec_path = PathBuf::from_str(executable_location.unwrap().as_str());
|
match PathBuf::from_str(custom_exec_location.as_str()) {
|
||||||
if exec_path.is_ok() {
|
Ok(exec_path) => {
|
||||||
exec_info = ExecutableLocation {
|
let path_copy = exec_path.clone();
|
||||||
executable_dir: exec_path.clone().unwrap().parent().unwrap().to_path_buf(),
|
if path_copy.parent().is_none() {
|
||||||
executable_path: exec_path.clone().unwrap(),
|
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,
|
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 = std::process::Command::new(exec_info.executable_path);
|
||||||
let mut command = Command::new(exec_info.executable_path);
|
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.stdout(log_file.try_clone().unwrap())
|
.stdout(log_file.try_clone().unwrap())
|
||||||
|
@ -801,7 +846,7 @@ pub async fn launch_game(
|
||||||
.current_dir(exec_info.executable_dir);
|
.current_dir(exec_info.executable_dir);
|
||||||
#[cfg(windows)]
|
#[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
|
// Start the process here so if there is an error, we can return immediately
|
||||||
let mut child = command.spawn()?;
|
let mut child = command.spawn()?;
|
||||||
|
@ -809,9 +854,22 @@ pub async fn launch_game(
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let start_time = Instant::now(); // get the start time of the game
|
let start_time = Instant::now(); // get the start time of the game
|
||||||
// start waiting for the game to exit
|
// start waiting for the game to exit
|
||||||
if let Err(err) = child.wait() {
|
match child.wait() {
|
||||||
log::error!("Error occured when waiting for game to exit: {}", err);
|
Ok(status_code) => {
|
||||||
return;
|
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
|
// 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 {
|
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 {
|
match config_lock.requirements.avx {
|
||||||
None => {
|
None => {
|
||||||
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
|
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||||
config_lock.requirements.avx = Some(true);
|
{
|
||||||
} else {
|
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.requirements.avx = Some(false);
|
||||||
}
|
}
|
||||||
config_lock.save_config().map_err(|err| {
|
config_lock.save_config().map_err(|err| {
|
||||||
|
|
|
@ -3,10 +3,11 @@ use std::os::windows::process::CommandExt;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Stdio,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::{io::AsyncWriteExt, process::Command};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{binaries::InstallStepOutput, CommandError},
|
commands::{binaries::InstallStepOutput, CommandError},
|
||||||
|
@ -14,6 +15,7 @@ use crate::{
|
||||||
util::{
|
util::{
|
||||||
file::{create_dir, delete_dir, to_image_base64},
|
file::{create_dir, delete_dir, to_image_base64},
|
||||||
network::download_file,
|
network::download_file,
|
||||||
|
process::{create_log_file, create_std_log_file, watch_process},
|
||||||
tar::{extract_and_delete_tar_ball, extract_tar_ball},
|
tar::{extract_and_delete_tar_ball, extract_tar_ball},
|
||||||
zip::{extract_and_delete_zip_file, extract_zip_file},
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct LauncherErrorCode {
|
struct LauncherErrorCode {
|
||||||
msg: String,
|
msg: String,
|
||||||
|
@ -336,23 +313,37 @@ pub async fn extract_iso_for_mod_install(
|
||||||
game_name.clone(),
|
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);
|
log::info!("Running extractor with args: {:?}", args);
|
||||||
|
|
||||||
let mut command = Command::new(exec_info.executable_path);
|
let mut command = Command::new(exec_info.executable_path);
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(exec_info.executable_dir)
|
.current_dir(exec_info.executable_dir)
|
||||||
.stdout(log_file.try_clone()?)
|
.stdout(Stdio::piped())
|
||||||
.stderr(log_file.try_clone()?);
|
.stderr(Stdio::piped());
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
command.creation_flags(0x08000000);
|
||||||
}
|
}
|
||||||
let output = command.output()?;
|
let mut child = command.spawn()?;
|
||||||
match output.status.code() {
|
|
||||||
|
// 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) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
log::info!("extraction and validation was successful");
|
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}"),
|
msg: format!("Unexpected error occured with code {code}"),
|
||||||
};
|
};
|
||||||
log::error!("extraction and validation was not successful. 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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some(default_error.msg.clone()),
|
msg: Some(default_error.msg.clone()),
|
||||||
|
@ -374,8 +363,6 @@ pub async fn extract_iso_for_mod_install(
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("extraction and validation was not successful. No status code");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some("Unexpected error occurred".to_owned()),
|
msg: Some("Unexpected error occurred".to_owned()),
|
||||||
|
@ -434,26 +421,38 @@ pub async fn decompile_for_mod_install(
|
||||||
game_name.clone(),
|
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);
|
log::info!("Running extractor with args: {:?}", args);
|
||||||
|
|
||||||
let mut command = Command::new(exec_info.executable_path);
|
let mut command = Command::new(exec_info.executable_path);
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(exec_info.executable_dir)
|
.current_dir(exec_info.executable_dir)
|
||||||
.stdout(log_file.try_clone()?)
|
.stdout(Stdio::piped())
|
||||||
.stderr(log_file.try_clone()?);
|
.stderr(Stdio::piped());
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
command.creation_flags(0x08000000);
|
||||||
}
|
}
|
||||||
let output = command.output()?;
|
let mut child = command.spawn()?;
|
||||||
match output.status.code() {
|
|
||||||
|
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) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
log::info!("extraction and validation was successful");
|
log::info!("decompilation was successful");
|
||||||
return Ok(InstallStepOutput {
|
return Ok(InstallStepOutput {
|
||||||
success: true,
|
success: true,
|
||||||
msg: None,
|
msg: None,
|
||||||
|
@ -462,18 +461,14 @@ pub async fn decompile_for_mod_install(
|
||||||
let default_error = LauncherErrorCode {
|
let default_error = LauncherErrorCode {
|
||||||
msg: format!("Unexpected error occured with code {code}"),
|
msg: format!("Unexpected error occured with code {code}"),
|
||||||
};
|
};
|
||||||
log::error!("extraction and validation was not successful. Code {code}");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some(default_error.msg.clone()),
|
msg: Some(default_error.msg.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("extraction and validation was not successful. No status code");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some("Unexpected error occurred".to_owned()),
|
msg: Some("Unexpected error occurred".to_owned()),
|
||||||
|
@ -532,26 +527,36 @@ pub async fn compile_for_mod_install(
|
||||||
game_name.clone(),
|
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);
|
log::info!("Running extractor with args: {:?}", args);
|
||||||
|
|
||||||
let mut command = Command::new(exec_info.executable_path);
|
let mut command = Command::new(exec_info.executable_path);
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(exec_info.executable_dir)
|
.current_dir(exec_info.executable_dir)
|
||||||
.stdout(log_file.try_clone()?)
|
.stdout(Stdio::piped())
|
||||||
.stderr(log_file.try_clone()?);
|
.stderr(Stdio::piped());
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
command.creation_flags(0x08000000);
|
command.creation_flags(0x08000000);
|
||||||
}
|
}
|
||||||
let output = command.output()?;
|
let mut child = command.spawn()?;
|
||||||
match output.status.code() {
|
|
||||||
|
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) => {
|
Some(code) => {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
log::info!("extraction and validation was successful");
|
log::info!("compilation was successful");
|
||||||
return Ok(InstallStepOutput {
|
return Ok(InstallStepOutput {
|
||||||
success: true,
|
success: true,
|
||||||
msg: None,
|
msg: None,
|
||||||
|
@ -560,18 +565,14 @@ pub async fn compile_for_mod_install(
|
||||||
let default_error = LauncherErrorCode {
|
let default_error = LauncherErrorCode {
|
||||||
msg: format!("Unexpected error occured with code {code}"),
|
msg: format!("Unexpected error occured with code {code}"),
|
||||||
};
|
};
|
||||||
log::error!("extraction and validation was not successful. Code {code}");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some(default_error.msg.clone()),
|
msg: Some(default_error.msg.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::error!("extraction and validation was not successful. No status code");
|
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 {
|
Ok(InstallStepOutput {
|
||||||
success: false,
|
success: false,
|
||||||
msg: Some("Unexpected error occurred".to_owned()),
|
msg: Some("Unexpected error occurred".to_owned()),
|
||||||
|
@ -686,13 +687,17 @@ pub async fn launch_mod(
|
||||||
&mod_name,
|
&mod_name,
|
||||||
&source_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);
|
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);
|
let mut command = Command::new(exec_info.executable_path);
|
||||||
command
|
command
|
||||||
.args(args)
|
.args(args)
|
||||||
|
|
|
@ -153,7 +153,6 @@ fn main() {
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::binaries::extract_and_validate_iso,
|
commands::binaries::extract_and_validate_iso,
|
||||||
commands::binaries::get_end_of_logs,
|
|
||||||
commands::binaries::get_launch_game_string,
|
commands::binaries::get_launch_game_string,
|
||||||
commands::binaries::launch_game,
|
commands::binaries::launch_game,
|
||||||
commands::binaries::open_repl,
|
commands::binaries::open_repl,
|
||||||
|
|
|
@ -2,5 +2,6 @@ pub mod file;
|
||||||
pub mod game_milestones;
|
pub mod game_milestones;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
|
pub mod process;
|
||||||
pub mod tar;
|
pub mod tar;
|
||||||
pub mod zip;
|
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)?)
|
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<()> {
|
pub fn touch_file(path: &PathBuf) -> std::io::Result<()> {
|
||||||
match std::fs::OpenOptions::new()
|
match std::fs::OpenOptions::new()
|
||||||
.create(true)
|
.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
|
// Other Imports
|
||||||
import { onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { Router, Route } from "svelte-navigator";
|
import { Router, Route } from "svelte-navigator";
|
||||||
import Game from "./routes/Game.svelte";
|
import Game from "./routes/Game.svelte";
|
||||||
import Settings from "./routes/Settings.svelte";
|
import Settings from "./routes/Settings.svelte";
|
||||||
|
@ -15,8 +15,11 @@
|
||||||
import { isLoading } from "svelte-i18n";
|
import { isLoading } from "svelte-i18n";
|
||||||
import { getLocale, setLocale } from "$lib/rpc/config";
|
import { getLocale, setLocale } from "$lib/rpc/config";
|
||||||
import GameFeature from "./routes/GameFeature.svelte";
|
import GameFeature from "./routes/GameFeature.svelte";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { toastStore } from "$lib/stores/ToastStore";
|
||||||
|
|
||||||
let revokeSpecificActions = false;
|
let revokeSpecificActions = false;
|
||||||
|
let toastListener: any = undefined;
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -32,6 +35,16 @@
|
||||||
if (locale !== null) {
|
if (locale !== null) {
|
||||||
setLocale(locale);
|
setLocale(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toastListener = await listen("toast_msg", (event) => {
|
||||||
|
toastStore.makeToast(event.payload.toast, event.payload.level);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (toastListener !== undefined) {
|
||||||
|
toastListener();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isInDebugMode()) {
|
if (!isInDebugMode()) {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"features_textures_moveDown_buttonAlt": "move texture pack down in order",
|
"features_textures_moveDown_buttonAlt": "move texture pack down in order",
|
||||||
"features_textures_moveUp_buttonAlt": "move texture pack up in order",
|
"features_textures_moveUp_buttonAlt": "move texture pack up in order",
|
||||||
"features_textures_replacedCount": "Textures replaced",
|
"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_update_mod": "Update",
|
||||||
"gameControls_active": "(Active)",
|
"gameControls_active": "(Active)",
|
||||||
"gameControls_always_use_newest": "Always Use Newest",
|
"gameControls_always_use_newest": "Always Use Newest",
|
||||||
|
@ -69,6 +70,7 @@
|
||||||
"header_updateAvailable": "Update Available!",
|
"header_updateAvailable": "Update Available!",
|
||||||
"help_button_downloadPackage": "Download Support Package",
|
"help_button_downloadPackage": "Download Support Package",
|
||||||
"help_button_openLogFolder": "Open Log Folder",
|
"help_button_openLogFolder": "Open Log Folder",
|
||||||
|
"help_button_defaultKeybinds": "Default Keybinds",
|
||||||
"help_button_reportGameIssue": "Report Game Issue",
|
"help_button_reportGameIssue": "Report Game Issue",
|
||||||
"help_button_reportLauncherIssue": "Report Launcher 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.",
|
"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_changes": "Changes",
|
||||||
"settings_versions_table_header_date": "Date",
|
"settings_versions_table_header_date": "Date",
|
||||||
"settings_versions_table_header_version": "Version",
|
"settings_versions_table_header_version": "Version",
|
||||||
|
"settings_versions_noReleasesFound": "No releases could be retrieved from GitHub!",
|
||||||
"setup_button_continue": "Continue",
|
"setup_button_continue": "Continue",
|
||||||
"setup_button_getSupportPackage": "Get Support Package",
|
"setup_button_getSupportPackage": "Get Support Package",
|
||||||
"setup_button_installViaISO": "Install via ISO",
|
"setup_button_installViaISO": "Install via ISO",
|
||||||
|
@ -219,5 +222,7 @@
|
||||||
"toasts_modSourceUnreachable": "Mod source unreachable",
|
"toasts_modSourceUnreachable": "Mod source unreachable",
|
||||||
"toasts_couldNotRemoveModSource": "Unable to remove mod source",
|
"toasts_couldNotRemoveModSource": "Unable to remove mod source",
|
||||||
"toasts_modSourceDuplicateName": "Mod source has the same display name as one you already have added",
|
"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">
|
<script lang="ts">
|
||||||
import { getInternalName, SupportedGame } from "$lib/constants";
|
import { getInternalName, SupportedGame } from "$lib/constants";
|
||||||
import { openDir } from "$lib/rpc/window";
|
import { openDir } from "$lib/rpc/window";
|
||||||
import IconArrowLeft from "~icons/mdi/arrow-left";
|
|
||||||
import IconCog from "~icons/mdi/cog";
|
import IconCog from "~icons/mdi/cog";
|
||||||
import { configDir, join } from "@tauri-apps/api/path";
|
import { configDir, join } from "@tauri-apps/api/path";
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
|
@ -27,7 +26,6 @@
|
||||||
import { navigate } from "svelte-navigator";
|
import { navigate } from "svelte-navigator";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { toastStore } from "$lib/stores/ToastStore";
|
import { toastStore } from "$lib/stores/ToastStore";
|
||||||
import { launchMod } from "$lib/rpc/features";
|
|
||||||
|
|
||||||
export let activeGame: SupportedGame;
|
export let activeGame: SupportedGame;
|
||||||
|
|
||||||
|
@ -172,13 +170,11 @@
|
||||||
launchGameWithCustomExecutable(getInternalName(activeGame));
|
launchGameWithCustomExecutable(getInternalName(activeGame));
|
||||||
}}>Launch with Custom Executable</DropdownItem
|
}}>Launch with Custom Executable</DropdownItem
|
||||||
>
|
>
|
||||||
{#if !isLinux}
|
<DropdownItem
|
||||||
<DropdownItem
|
on:click={async () => {
|
||||||
on:click={async () => {
|
openREPL(getInternalName(activeGame));
|
||||||
openREPL(getInternalName(activeGame));
|
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
||||||
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
|
|
|
@ -330,13 +330,11 @@
|
||||||
launchMod(getInternalName(activeGame), true, modName, modSource);
|
launchMod(getInternalName(activeGame), true, modName, modSource);
|
||||||
}}>{$_("gameControls_button_playInDebug")}</DropdownItem
|
}}>{$_("gameControls_button_playInDebug")}</DropdownItem
|
||||||
>
|
>
|
||||||
{#if !isLinux}
|
<DropdownItem
|
||||||
<DropdownItem
|
on:click={async () => {
|
||||||
on:click={async () => {
|
openREPLForMod(getInternalName(activeGame), modName, modSource);
|
||||||
openREPLForMod(getInternalName(activeGame), modName, modSource);
|
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
||||||
}}>{$_("gameControls_button_openREPL")}</DropdownItem
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
|
|
|
@ -261,6 +261,12 @@
|
||||||
{packAddingError}
|
{packAddingError}
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-row font-bold mt-3">
|
||||||
|
<Alert color="red" class="flex-grow">
|
||||||
|
{$_("features_textures_largePackWarning")}
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-row font-bold mt-3">
|
<div class="flex flex-row font-bold mt-3">
|
||||||
<h2>{$_("features_textures_listHeading")}</h2>
|
<h2>{$_("features_textures_listHeading")}</h2>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import type { Job } from "$lib/utils/jobs";
|
import type { Job } from "$lib/utils/jobs";
|
||||||
import { getInternalName, type SupportedGame } from "$lib/constants";
|
import { getInternalName, type SupportedGame } from "$lib/constants";
|
||||||
import {
|
import {
|
||||||
getEndOfLogs,
|
|
||||||
runCompiler,
|
runCompiler,
|
||||||
runDecompiler,
|
runDecompiler,
|
||||||
updateDataDirectory,
|
updateDataDirectory,
|
||||||
|
@ -60,7 +59,6 @@
|
||||||
]);
|
]);
|
||||||
progressTracker.start();
|
progressTracker.start();
|
||||||
let resp = await runDecompiler("", getInternalName(activeGame), true);
|
let resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -84,7 +82,6 @@
|
||||||
]);
|
]);
|
||||||
progressTracker.start();
|
progressTracker.start();
|
||||||
let resp = await runCompiler("", getInternalName(activeGame), true);
|
let resp = await runCompiler("", getInternalName(activeGame), true);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -116,7 +113,6 @@
|
||||||
]);
|
]);
|
||||||
progressTracker.start();
|
progressTracker.start();
|
||||||
let resp = await updateDataDirectory(getInternalName(activeGame));
|
let resp = await updateDataDirectory(getInternalName(activeGame));
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -124,7 +120,6 @@
|
||||||
}
|
}
|
||||||
progressTracker.proceed();
|
progressTracker.proceed();
|
||||||
resp = await runDecompiler("", getInternalName(activeGame), true);
|
resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -132,7 +127,6 @@
|
||||||
}
|
}
|
||||||
progressTracker.proceed();
|
progressTracker.proceed();
|
||||||
resp = await runCompiler("", getInternalName(activeGame));
|
resp = await runCompiler("", getInternalName(activeGame));
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -198,7 +192,6 @@
|
||||||
}
|
}
|
||||||
progressTracker.proceed();
|
progressTracker.proceed();
|
||||||
resp = await runDecompiler("", getInternalName(activeGame), true);
|
resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -248,7 +241,6 @@
|
||||||
modSourceName,
|
modSourceName,
|
||||||
sourcePath,
|
sourcePath,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -266,7 +258,6 @@
|
||||||
modName,
|
modName,
|
||||||
modSourceName,
|
modSourceName,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -278,7 +269,6 @@
|
||||||
modName,
|
modName,
|
||||||
modSourceName,
|
modSourceName,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -338,7 +328,6 @@
|
||||||
modSourceName,
|
modSourceName,
|
||||||
sourcePath,
|
sourcePath,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -368,7 +357,6 @@
|
||||||
modName,
|
modName,
|
||||||
modSourceName,
|
modSourceName,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -380,7 +368,6 @@
|
||||||
modName,
|
modName,
|
||||||
modSourceName,
|
modSourceName,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -420,7 +407,6 @@
|
||||||
modName,
|
modName,
|
||||||
modSourceName,
|
modSourceName,
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -449,8 +435,6 @@
|
||||||
modName,
|
modName,
|
||||||
modSourceName,
|
modSourceName,
|
||||||
);
|
);
|
||||||
// TODO - stream logs
|
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -494,9 +478,7 @@
|
||||||
|
|
||||||
<div class="flex flex-col justify-content">
|
<div class="flex flex-col justify-content">
|
||||||
<Progress />
|
<Progress />
|
||||||
{#if $progressTracker.logs !== undefined}
|
<LogViewer />
|
||||||
<LogViewer />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if $progressTracker.overallStatus === "success"}
|
{#if $progressTracker.overallStatus === "success"}
|
||||||
<div class="flex flex-col justify-end items-end mt-auto">
|
<div class="flex flex-col justify-end items-end mt-auto">
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import { Alert, Button } from "flowbite-svelte";
|
import { Alert, Button } from "flowbite-svelte";
|
||||||
import {
|
import {
|
||||||
extractAndValidateISO,
|
extractAndValidateISO,
|
||||||
getEndOfLogs,
|
|
||||||
runCompiler,
|
runCompiler,
|
||||||
runDecompiler,
|
runDecompiler,
|
||||||
} from "$lib/rpc/binaries";
|
} from "$lib/rpc/binaries";
|
||||||
|
@ -92,7 +91,6 @@
|
||||||
sourcePath,
|
sourcePath,
|
||||||
getInternalName(activeGame),
|
getInternalName(activeGame),
|
||||||
);
|
);
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -100,7 +98,6 @@
|
||||||
}
|
}
|
||||||
progressTracker.proceed();
|
progressTracker.proceed();
|
||||||
resp = await runDecompiler(sourcePath, getInternalName(activeGame));
|
resp = await runDecompiler(sourcePath, getInternalName(activeGame));
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -108,7 +105,6 @@
|
||||||
}
|
}
|
||||||
progressTracker.proceed();
|
progressTracker.proceed();
|
||||||
resp = await runCompiler(sourcePath, getInternalName(activeGame));
|
resp = await runCompiler(sourcePath, getInternalName(activeGame));
|
||||||
progressTracker.updateLogs(await getEndOfLogs());
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
progressTracker.halt();
|
progressTracker.halt();
|
||||||
installationError = resp.msg;
|
installationError = resp.msg;
|
||||||
|
@ -129,11 +125,9 @@
|
||||||
{#if !requirementsMet}
|
{#if !requirementsMet}
|
||||||
<Requirements {activeGame} on:recheckRequirements={checkRequirements} />
|
<Requirements {activeGame} on:recheckRequirements={checkRequirements} />
|
||||||
{:else if installing}
|
{:else if installing}
|
||||||
<div class="flex flex-col justify-content">
|
<div class="flex flex-col justify-content shrink">
|
||||||
<Progress />
|
<Progress />
|
||||||
{#if $progressTracker.logs !== undefined}
|
<LogViewer />
|
||||||
<LogViewer />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{#if $progressTracker.overallStatus === "success"}
|
{#if $progressTracker.overallStatus === "success"}
|
||||||
<div class="flex flex-col justify-end items-end mt-auto">
|
<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 { progressTracker } from "$lib/stores/ProgressStore";
|
||||||
import IconDocument from "~icons/mdi/file-document-outline";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { Accordion, AccordionItem } from "flowbite-svelte";
|
|
||||||
import { ansiSpan } from "ansi-to-span";
|
import { ansiSpan } from "ansi-to-span";
|
||||||
import escapeHtml from "escape-html";
|
import escapeHtml from "escape-html";
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
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) {
|
function convertLogColors(text) {
|
||||||
return ansiSpan(escapeHtml(text)).replaceAll("\n", "<br/>");
|
return ansiSpan(escapeHtml(text)).replaceAll("\n", "<br/>");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Accordion class="log-accordian p-0 mb-2">
|
{#if $progressTracker.logs}
|
||||||
<AccordionItem class="bg-slate-900 rounded p-[1rem]">
|
<pre
|
||||||
<span slot="header" class="text-sm font-semibold text-white flex gap-2">
|
class="rounded p-2 bg-[#141414] text-[11px] max-h-[300px] overflow-auto text-pretty font-mono"
|
||||||
<IconDocument />
|
bind:this={logElement}>{@html convertLogColors($progressTracker.logs)}</pre>
|
||||||
<span>{$_("setup_logs_header")}</span>
|
{/if}
|
||||||
</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>
|
|
||||||
|
|
|
@ -75,7 +75,10 @@
|
||||||
$VersionStore.activeVersionType === "official"
|
$VersionStore.activeVersionType === "official"
|
||||||
) {
|
) {
|
||||||
const latestToolingVersion = await getLatestOfficialRelease();
|
const latestToolingVersion = await getLatestOfficialRelease();
|
||||||
if ($VersionStore.activeVersionName !== latestToolingVersion.version) {
|
if (
|
||||||
|
latestToolingVersion !== undefined &&
|
||||||
|
$VersionStore.activeVersionName !== latestToolingVersion.version
|
||||||
|
) {
|
||||||
// Check that we havn't already downloaded it
|
// Check that we havn't already downloaded it
|
||||||
let alreadyHaveRelease = false;
|
let alreadyHaveRelease = false;
|
||||||
const downloadedOfficialVersions =
|
const downloadedOfficialVersions =
|
||||||
|
@ -127,11 +130,6 @@
|
||||||
{$VersionStore.activeVersionName === null
|
{$VersionStore.activeVersionName === null
|
||||||
? "not set!"
|
? "not set!"
|
||||||
: $VersionStore.activeVersionName}
|
: $VersionStore.activeVersionName}
|
||||||
{#if $VersionStore.activeVersionType === "unofficial"}
|
|
||||||
(unf)
|
|
||||||
{:else if $VersionStore.activeVersionType === "devel"}
|
|
||||||
(dev)
|
|
||||||
{/if}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -60,3 +60,7 @@
|
||||||
.font-mono {
|
.font-mono {
|
||||||
font-family: "Noto Sans Mono", monospace !important;
|
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) {
|
if (!async) {
|
||||||
await initPromise;
|
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";
|
import { invoke_rpc } from "./rpc";
|
||||||
|
|
||||||
interface InstallationOutput {
|
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(
|
export async function extractAndValidateISO(
|
||||||
pathToIso: string,
|
pathToIso: string,
|
||||||
gameName: string,
|
gameName: string,
|
||||||
|
@ -81,7 +77,7 @@ export async function launchGameWithCustomExecutable(
|
||||||
gameName: string,
|
gameName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Get custom executable location
|
// Get custom executable location
|
||||||
const customExecutable = await filePrompt(["exe"], "executables", "pick exe");
|
const customExecutable = await filePromptNoFilters("Select custom 'gk'");
|
||||||
if (customExecutable !== null) {
|
if (customExecutable !== null) {
|
||||||
return await invoke_rpc(
|
return await invoke_rpc(
|
||||||
"launch_game",
|
"launch_game",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { invoke_rpc } from "./rpc";
|
import { invoke_rpc } from "./rpc";
|
||||||
|
|
||||||
export type VersionFolders = null | "official" | "unofficial" | "devel";
|
export type VersionFolders = null | "official";
|
||||||
|
|
||||||
export async function listDownloadedVersions(
|
export async function listDownloadedVersions(
|
||||||
versionFolder: VersionFolders,
|
versionFolder: VersionFolders,
|
||||||
|
|
|
@ -16,7 +16,7 @@ interface ProgressTracker {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
overallStatus: ProgressStatus;
|
overallStatus: ProgressStatus;
|
||||||
steps: ProgressStep[];
|
steps: ProgressStep[];
|
||||||
logs: string;
|
logs: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storeValue: ProgressTracker = {
|
const storeValue: ProgressTracker = {
|
||||||
|
@ -66,9 +66,12 @@ function createProgressTracker() {
|
||||||
val.steps[val.currentStep].status = "failed";
|
val.steps[val.currentStep].status = "failed";
|
||||||
return val;
|
return val;
|
||||||
}),
|
}),
|
||||||
updateLogs: (logs: string) =>
|
appendLogs: (logs: string) =>
|
||||||
update((val) => {
|
update((val) => {
|
||||||
val.logs = logs;
|
if (val.logs === undefined) {
|
||||||
|
val.logs = "";
|
||||||
|
}
|
||||||
|
val.logs += logs;
|
||||||
return val;
|
return val;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,22 @@
|
||||||
import { open, save } from "@tauri-apps/api/dialog";
|
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(
|
export async function filePrompt(
|
||||||
extensions: string[],
|
extensions: string[],
|
||||||
name: 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 { arch, platform } from "@tauri-apps/api/os";
|
||||||
import { listOfficialReleases } from "./github";
|
import { listOfficialReleases } from "./github";
|
||||||
|
import { init } from "svelte-i18n";
|
||||||
|
import { initLocales } from "$lib/i18n/i18n";
|
||||||
|
|
||||||
vi.mock("@tauri-apps/api/os");
|
vi.mock("@tauri-apps/api/os");
|
||||||
global.fetch = vi.fn();
|
global.fetch = vi.fn();
|
||||||
|
|
||||||
function createFetchResponse(data: any) {
|
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) {
|
function createFakeGithubReleaseAsset(assetName) {
|
||||||
|
@ -121,6 +137,10 @@ function createFakeGithubRelease(assetNames: string[]) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initLocales(true);
|
||||||
|
});
|
||||||
|
|
||||||
describe("listOfficialReleases", () => {
|
describe("listOfficialReleases", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
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 { arch, platform } from "@tauri-apps/api/os";
|
||||||
import semver from "semver";
|
import { unwrapFunctionStore, format } from "svelte-i18n";
|
||||||
|
|
||||||
|
const $format = unwrapFunctionStore(format);
|
||||||
|
|
||||||
export interface ReleaseInfo {
|
export interface ReleaseInfo {
|
||||||
releaseType: "official" | "unofficial" | "devel";
|
releaseType: "official";
|
||||||
version: string;
|
version: string;
|
||||||
date: string | undefined;
|
date: string | undefined;
|
||||||
githubLink: 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(
|
function isWindowsRelease(
|
||||||
platform: string,
|
platform: string,
|
||||||
architecture: string,
|
architecture: string,
|
||||||
assetName: string,
|
assetName: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return platform === "win32" && assetName.startsWith("opengoal-windows-v");
|
||||||
platform === "win32" &&
|
|
||||||
(assetName.startsWith("opengoal-windows-v") ||
|
|
||||||
(assetName.startsWith("opengoal-v") && assetName.includes("windows")))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLinuxRelease(
|
function isLinuxRelease(
|
||||||
|
@ -44,11 +41,7 @@ function isLinuxRelease(
|
||||||
architecture: string,
|
architecture: string,
|
||||||
assetName: string,
|
assetName: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return platform === "linux" && assetName.startsWith("opengoal-linux-v");
|
||||||
platform === "linux" &&
|
|
||||||
(assetName.startsWith("opengoal-linux-v") ||
|
|
||||||
(assetName.startsWith("opengoal-v") && assetName.includes("linux")))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDownloadLinkForCurrentPlatform(
|
async function getDownloadLinkForCurrentPlatform(
|
||||||
|
@ -99,35 +92,53 @@ async function parseGithubRelease(githubRelease: any): Promise<ReleaseInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listOfficialReleases(): Promise<ReleaseInfo[]> {
|
export async function listOfficialReleases(): Promise<ReleaseInfo[]> {
|
||||||
return listReleases("official", "open-goal/jak-project");
|
const nextUrlPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i;
|
||||||
}
|
|
||||||
|
|
||||||
export async function listReleases(
|
|
||||||
releaseType: string,
|
|
||||||
repo: string,
|
|
||||||
): Promise<ReleaseInfo[]> {
|
|
||||||
let releases = [];
|
let releases = [];
|
||||||
// TODO - handle rate limiting
|
let urlToHit =
|
||||||
// TODO - long term - handle pagination (more than 100 releases)
|
"https://api.github.com/repos/open-goal/jak-project/releases?per_page=100";
|
||||||
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();
|
|
||||||
|
|
||||||
for (const release of githubReleases) {
|
while (urlToHit !== undefined) {
|
||||||
releases.push(await parseGithubRelease(release));
|
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));
|
return releases.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLatestOfficialRelease(): Promise<ReleaseInfo> {
|
export async function getLatestOfficialRelease(): Promise<
|
||||||
// TODO - handle rate limiting
|
ReleaseInfo | undefined
|
||||||
|
> {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
"https://api.github.com/repos/open-goal/jak-project/releases/latest",
|
"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();
|
const githubRelease = await resp.json();
|
||||||
return await parseGithubRelease(githubRelease);
|
return await parseGithubRelease(githubRelease);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { appConfigDir } from "@tauri-apps/api/path";
|
import { appConfigDir } from "@tauri-apps/api/path";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
import { navigate } from "svelte-navigator";
|
||||||
|
|
||||||
let appDir: string | undefined = undefined;
|
let appDir: string | undefined = undefined;
|
||||||
let downloadingPackage = false;
|
let downloadingPackage = false;
|
||||||
|
@ -46,6 +47,12 @@
|
||||||
}}>{$_("help_button_openLogFolder")}</Button
|
}}>{$_("help_button_openLogFolder")}</Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
<p class="mt-3 text-sm">
|
<p class="mt-3 text-sm">
|
||||||
{$_("help_description_createAnIssue")}
|
{$_("help_description_createAnIssue")}
|
||||||
|
|
|
@ -50,7 +50,6 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
// TODO - "no releases found"
|
|
||||||
|
|
||||||
// Merge that with the actual current releases on github
|
// Merge that with the actual current releases on github
|
||||||
const githubReleases = await listOfficialReleases();
|
const githubReleases = await listOfficialReleases();
|
||||||
|
@ -111,8 +110,6 @@
|
||||||
if (success) {
|
if (success) {
|
||||||
$VersionStore.activeVersionType = "official";
|
$VersionStore.activeVersionType = "official";
|
||||||
$VersionStore.activeVersionName = $VersionStore.selectedVersions.official;
|
$VersionStore.activeVersionName = $VersionStore.selectedVersions.official;
|
||||||
$VersionStore.selectedVersions.unofficial = null;
|
|
||||||
$VersionStore.selectedVersions.devel = null;
|
|
||||||
toastStore.makeToast($_("toasts_savedToolingVersion"), "info");
|
toastStore.makeToast($_("toasts_savedToolingVersion"), "info");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import IconDownload from "~icons/mdi/download";
|
import IconDownload from "~icons/mdi/download";
|
||||||
import IconDeleteForever from "~icons/mdi/delete-forever";
|
import IconDeleteForever from "~icons/mdi/delete-forever";
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Radio,
|
Radio,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
@ -60,144 +61,152 @@
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
{#if releaseList === undefined || releaseList.length <= 0}
|
||||||
<TableHead>
|
<Alert color="red" class="dark:bg-slate-900 flex-grow">
|
||||||
<TableHeadCell>
|
{$_("settings_versions_noReleasesFound")}
|
||||||
<span class="sr-only"></span>
|
</Alert>
|
||||||
</TableHeadCell>
|
{:else}
|
||||||
<TableHeadCell>
|
<Table>
|
||||||
<span class="sr-only"></span>
|
<TableHead>
|
||||||
</TableHeadCell>
|
<TableHeadCell>
|
||||||
<TableHeadCell
|
<span class="sr-only"></span>
|
||||||
>{$_("settings_versions_table_header_version")}</TableHeadCell
|
</TableHeadCell>
|
||||||
>
|
<TableHeadCell>
|
||||||
<TableHeadCell>{$_("settings_versions_table_header_date")}</TableHeadCell>
|
<span class="sr-only"></span>
|
||||||
<TableHeadCell
|
</TableHeadCell>
|
||||||
>{$_("settings_versions_table_header_changes")}</TableHeadCell
|
<TableHeadCell
|
||||||
>
|
>{$_("settings_versions_table_header_version")}</TableHeadCell
|
||||||
</TableHead>
|
>
|
||||||
<TableBody tableBodyClass="divide-y">
|
<TableHeadCell
|
||||||
{#each releaseList as release (release.version)}
|
>{$_("settings_versions_table_header_date")}</TableHeadCell
|
||||||
<TableBodyRow>
|
>
|
||||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
<TableHeadCell
|
||||||
{#if release.isDownloaded}
|
>{$_("settings_versions_table_header_changes")}</TableHeadCell
|
||||||
<Radio
|
>
|
||||||
class="disabled:cursor-not-allowed p-0"
|
</TableHead>
|
||||||
bind:group={$VersionStore.selectedVersions[releaseType]}
|
<TableBody tableBodyClass="divide-y">
|
||||||
on:change={() => dispatch("versionChange")}
|
{#each releaseList as release (release.version)}
|
||||||
value={release.version}
|
<TableBodyRow>
|
||||||
disabled={!release.isDownloaded}
|
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
||||||
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 release.isDownloaded}
|
{#if release.isDownloaded}
|
||||||
<IconDeleteForever
|
<Radio
|
||||||
class="text-xl"
|
class="disabled:cursor-not-allowed p-0"
|
||||||
color="red"
|
bind:group={$VersionStore.selectedVersions[releaseType]}
|
||||||
aria-label={$_(
|
on:change={() => dispatch("versionChange")}
|
||||||
"settings_versions_icon_removeVersion_altText",
|
value={release.version}
|
||||||
)}
|
disabled={!release.isDownloaded}
|
||||||
/>
|
name={`${releaseType}-release`}
|
||||||
{: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}
|
{/if}
|
||||||
</Button>
|
</TableBodyCell>
|
||||||
{#if release.invalid}
|
<TableBodyCell
|
||||||
<Tooltip color="red">
|
class="px-6 py-2 whitespace-nowrap font-medium"
|
||||||
{#if release.invalidationReasons.length > 0}
|
style="line-height: 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
|
<Button
|
||||||
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
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 () => {
|
on:click={async () => {
|
||||||
dispatch("redownloadVersion", {
|
if (release.isDownloaded) {
|
||||||
version: release.version,
|
dispatch("removeVersion", { version: release.version });
|
||||||
downloadUrl: release.downloadUrl,
|
} else {
|
||||||
});
|
dispatch("downloadVersion", {
|
||||||
|
version: release.version,
|
||||||
|
downloadUrl: release.downloadUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if release.pendingAction}
|
{#if release.isDownloaded}
|
||||||
<Spinner color="yellow" size={"6"} />
|
<IconDeleteForever
|
||||||
{:else}
|
|
||||||
<IconRefresh
|
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
|
color="red"
|
||||||
aria-label={$_(
|
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}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{#if release.invalid}
|
||||||
</TableBodyCell>
|
<Tooltip color="red">
|
||||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium"
|
{#if release.invalidationReasons.length > 0}
|
||||||
>{release.version}</TableBodyCell
|
{$_("settings_versions_invalidReleaseGeneric")}
|
||||||
>
|
{#each release.invalidationReasons as reason}
|
||||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
<br />
|
||||||
{#if release.date}
|
- {reason}
|
||||||
{new Date(release.date).toLocaleDateString()}
|
{/each}
|
||||||
{/if}
|
{:else}
|
||||||
</TableBodyCell>
|
{$_("settings_versions_invalidReleaseGeneric")}
|
||||||
<TableBodyCell class="px-6 py-2 whitespace-nowrap font-medium">
|
{/if}
|
||||||
{#if release.githubLink}
|
</Tooltip>
|
||||||
<a
|
{/if}
|
||||||
class="inline-block"
|
{#if release.isDownloaded && release.releaseType == "official"}
|
||||||
href={release.githubLink}
|
<Button
|
||||||
target="_blank"
|
class="py-0 dark:bg-transparent hover:dark:bg-transparent focus:ring-0 focus:ring-offset-0 disabled:opacity-50"
|
||||||
rel="noreferrer"
|
disabled={release.pendingAction}
|
||||||
>
|
on:click={async () => {
|
||||||
<IconGitHub
|
dispatch("redownloadVersion", {
|
||||||
class="text-xl"
|
version: release.version,
|
||||||
aria-label={$_(
|
downloadUrl: release.downloadUrl,
|
||||||
"settings_versions_icon_githubRelease_altText",
|
});
|
||||||
)}
|
}}
|
||||||
/>
|
>
|
||||||
</a>
|
{#if release.pendingAction}
|
||||||
{/if}
|
<Spinner color="yellow" size={"6"} />
|
||||||
</TableBodyCell>
|
{:else}
|
||||||
</TableBodyRow>
|
<IconRefresh
|
||||||
{/each}
|
class="text-xl"
|
||||||
</TableBody>
|
aria-label={$_(
|
||||||
</Table>
|
"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}
|
{/if}
|
||||||
|
|
Loading…
Reference in a new issue