diff --git a/Cargo.lock b/Cargo.lock index 74dee62c16..4c6c34043d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -688,6 +688,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-http", "tauri-plugin-log", + "tauri-plugin-opener", "tauri-plugin-upload", "tempdir", "tempfile", @@ -805,6 +806,30 @@ dependencies = [ "zstd-safe 7.1.0", ] +[[package]] +name = "async-executor" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.3.0", + "futures-lite 2.6.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite 2.6.0", +] + [[package]] name = "async-io" version = "2.4.0" @@ -5780,6 +5805,25 @@ dependencies = [ "serde", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -7561,6 +7605,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openai_sora_client" version = "0.0.1" @@ -7935,6 +7991,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -11645,6 +11707,28 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d75fdc9ba0391eafb3337aac9d1b34fd353601dd5d4e7498820733f738991e7" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "open", + "schemars", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "url", + "windows 0.58.0", + "zbus", +] + [[package]] name = "tauri-plugin-upload" version = "2.3.1" @@ -13465,8 +13549,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", ] [[package]] @@ -13575,6 +13659,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -13615,17 +13709,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -13639,6 +13746,17 @@ dependencies = [ "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -13650,6 +13768,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -13684,8 +13813,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -13697,6 +13835,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -14409,9 +14557,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" dependencies = [ "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "derivative", "enumflags2", "event-listener 5.2.0", diff --git a/crates/desktop/artcraft/Cargo.toml b/crates/desktop/artcraft/Cargo.toml index 59f8741d92..bb48f67f6c 100644 --- a/crates/desktop/artcraft/Cargo.toml +++ b/crates/desktop/artcraft/Cargo.toml @@ -81,6 +81,7 @@ tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" } # NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error # types and can't do threading. We need to wait for a new version to unpin from Git. discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" } +tauri-plugin-opener = "2" # # Transparent title bar on MacOS # # NB: See the notes here: diff --git a/crates/desktop/artcraft/capabilities/default.json b/crates/desktop/artcraft/capabilities/default.json index a7b3380d5f..e0a5a74fd1 100644 --- a/crates/desktop/artcraft/capabilities/default.json +++ b/crates/desktop/artcraft/capabilities/default.json @@ -2,7 +2,9 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "enables the default permissions", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", "core:window:allow-close", @@ -25,6 +27,7 @@ "url": "http://localhost:12345" } ] - } + }, + "opener:default" ] -} +} \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/artcraft_error.rs b/crates/desktop/artcraft/src/core/artcraft_error.rs index 72ef8847b2..779cc334f4 100644 --- a/crates/desktop/artcraft/src/core/artcraft_error.rs +++ b/crates/desktop/artcraft/src/core/artcraft_error.rs @@ -1,6 +1,7 @@ use grok_client::error::grok_error::GrokError; use std::error::Error; use std::fmt::{Display, Formatter}; +use std::path::PathBuf; use storyteller_client::error::storyteller_error::StorytellerError; #[derive(Debug)] @@ -8,6 +9,7 @@ pub enum ArtcraftError { AnyhowError(anyhow::Error), DecodeError(base64::DecodeError), IoError(std::io::Error), + ReqwestError(reqwest::Error), // Service errors GrokError(GrokError), StorytellerError(StorytellerError), @@ -15,6 +17,9 @@ pub enum ArtcraftError { RwLockReadError, RwLockWriteError, MutexLockError, + // Download errors + BadDownloadFilename { path: PathBuf }, + CannotDownloadFilePathAlreadyExists { path: PathBuf }, } impl Error for ArtcraftError {} @@ -25,11 +30,14 @@ impl Display for ArtcraftError { Self::AnyhowError(e) => write!(f, "AnyhowError: {:?}", e), Self::DecodeError(e) => write!(f, "DecodeError: {:?}", e), Self::IoError(e) => write!(f, "IoError: {:?}", e), + Self::ReqwestError(e) => write!(f, "ReqwestError: {:?}", e), Self::GrokError(e) => write!(f, "GrokError: {:?}", e), Self::StorytellerError(e) => write!(f, "StorytellerError: {:?}", e), Self::RwLockReadError => write!(f, "RwLockReadError"), Self::RwLockWriteError => write!(f, "RwLockWriteError"), Self::MutexLockError => write!(f, "MutexLockError"), + Self::BadDownloadFilename { path } => write!(f, "BadDownloadFilename: {:?}", path), + Self::CannotDownloadFilePathAlreadyExists { path } => write!(f, "CannotDownloadFilePathAlreadyExists {:?}", path), } } } @@ -52,6 +60,12 @@ impl From for ArtcraftError { } } +impl From for ArtcraftError { + fn from(value: reqwest::Error) -> Self { + Self::ReqwestError(value) + } +} + impl From for ArtcraftError { fn from(value: GrokError) -> Self { Self::GrokError(value) diff --git a/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs b/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs new file mode 100644 index 0000000000..750bdc5553 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/download/download_directory_reveal_command.rs @@ -0,0 +1,70 @@ +use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio; +use crate::core::artcraft_error::ArtcraftError; +use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; +use crate::core::commands::response::shorthand::{Response, ResponseOrErrorMessage, ResponseOrErrorType}; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use crate::core::state::app_preferences::app_preferences::AppPreferences; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::download_url_to_temp_dir::download_url_to_temp_dir; +use crate::core::utils::download_url_to_user_download_dir::download_url_to_user_download_dir; +use anyhow::anyhow; +use log::{error, info}; +use serde_derive::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::str::FromStr; +use storyteller_client::endpoints::media_files::get_media_file::get_media_file; +use tauri::{AppHandle, State}; +use tauri_plugin_opener::OpenerExt; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Serialize)] +pub struct DownloadDirectoryRevealSuccessResponse { +} + +impl SerializeMarker for DownloadDirectoryRevealSuccessResponse {} + +#[tauri::command] +pub async fn download_directory_reveal_command( + app: AppHandle, + app_prefs: State<'_, AppPreferencesManager>, + app_data_root: State<'_, AppDataRoot>, +) -> ResponseOrErrorMessage { + + info!("download_directory_reveal_command called"); + + let result = handle_request( + &app, + &app_prefs, + &app_data_root, + ).await; + + if let Err(err) = result { + format!("Error revealing download dir: {:?}", err); + return Err("error revealing download dir".into()) + } + + Ok(DownloadDirectoryRevealSuccessResponse {}.into()) +} + + +pub async fn handle_request( + app: &AppHandle, + app_prefs: &AppPreferencesManager, + app_data_root: &AppDataRoot, +) -> Result<(), ArtcraftError> { + + let app_prefs = app_prefs.get_clone()?; + + let download_directory = app_prefs + .preferred_download_directory + .download_directory(app_data_root); + + info!("Revealing item in directory: {:?}", download_directory); + + app.opener().reveal_item_in_dir(download_directory) + .map_err(|err| anyhow!("Failed to open directory: {:?}", err))?; + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/commands/download/download_media_file_command.rs b/crates/desktop/artcraft/src/core/commands/download/download_media_file_command.rs new file mode 100644 index 0000000000..de0ad65933 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/download/download_media_file_command.rs @@ -0,0 +1,104 @@ +use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio; +use crate::core::artcraft_error::ArtcraftError; +use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; +use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType}; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use crate::core::state::app_preferences::app_preferences::AppPreferences; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::download_url_to_temp_dir::download_url_to_temp_dir; +use crate::core::utils::download_url_to_user_download_dir::download_url_to_user_download_dir; +use anyhow::anyhow; +use log::{error, info}; +use serde_derive::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::str::FromStr; +use storyteller_client::endpoints::media_files::get_media_file::get_media_file; +use tauri::{AppHandle, State}; +use tokens::tokens::media_files::MediaFileToken; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; + +#[derive(Deserialize, Debug)] +pub struct DownloadMediaFileRequest { + pub media_token: MediaFileToken, +} + +#[derive(Serialize)] +pub struct DownloadMediaFileSuccessResponse { +} + +impl SerializeMarker for DownloadMediaFileSuccessResponse {} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum DownloadMediaFileErrorType { + FilesystemError, + NetworkError, + UnknownError, +} + + +#[tauri::command] +pub async fn download_media_file_command( + request: DownloadMediaFileRequest, + app: AppHandle, + app_prefs: State<'_, AppPreferencesManager>, + app_data_root: State<'_, AppDataRoot>, + app_env_configs: State<'_, AppEnvConfigs>, +) -> ResponseOrErrorType { + + info!("download_media_file_command called"); + + info!("request: {:?}", request); + + let result = handle_request( + request, + &app, + &app_prefs, + &app_data_root, + &app_env_configs, + ).await; + + if let Err(err) = result { + error!("Error downloading media: {:?}", err); + // TODO: This error is semantically incorrect - just trying to get the code done + return Err(CommandErrorResponseWrapper { + status: CommandErrorStatus::ServerError, + error_message: Some("error downloading file".to_string()), + error_type: Some(DownloadMediaFileErrorType::UnknownError), + error_details: None, + }); + } + + Ok(DownloadMediaFileSuccessResponse {}.into()) +} + + +pub async fn handle_request( + request: DownloadMediaFileRequest, + app: &AppHandle, + app_prefs: &AppPreferencesManager, + app_data_root: &AppDataRoot, + app_env_configs: &AppEnvConfigs +) -> Result<(), ArtcraftError> { + + let app_prefs = app_prefs.get_clone()?; + + // TODO: Api should return the extension and suggested filename so we can better construct something. + let media_file = get_media_file( + &app_env_configs.storyteller_host, + &request.media_token, + ).await?; + + let asset_url = media_file.media_file.media_links.cdn_url; + + let download_path = download_url_to_user_download_dir( + &asset_url, + app_data_root, + &app_prefs + ).await?; + + info!("downloaded to: {:?}", download_path); + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/commands/download/download_url_command.rs b/crates/desktop/artcraft/src/core/commands/download/download_url_command.rs new file mode 100644 index 0000000000..7391441b8a --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/download/download_url_command.rs @@ -0,0 +1,125 @@ +use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio; +use crate::core::artcraft_error::ArtcraftError; +use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; +use crate::core::commands::response::shorthand::{Response, ResponseOrErrorType}; +use crate::core::commands::response::success_response_wrapper::SerializeMarker; +use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; +use crate::core::events::warning_events::flash_file_download_error_event::{FlashFileDownloadErrorType, FlashFileDownloadErrorEvent}; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use crate::core::state::app_preferences::app_preferences::AppPreferences; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::utils::download_url_to_temp_dir::download_url_to_temp_dir; +use crate::core::utils::download_url_to_user_download_dir::download_url_to_user_download_dir; +use anyhow::anyhow; +use log::{error, info}; +use serde_derive::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::str::FromStr; +use storyteller_client::endpoints::media_files::get_media_file::get_media_file; +use tauri::{AppHandle, State}; +use url::Url; + +#[derive(Deserialize, Debug)] +pub struct DownloadUrlRequest { + pub url: Url, +} + +#[derive(Serialize)] +pub struct DownloadUrlSuccessResponse { +} + +impl SerializeMarker for DownloadUrlSuccessResponse {} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum DownloadUrlErrorType { + FilesystemError, + NetworkError, + UnknownError, +} + + +#[tauri::command] +pub async fn download_url_command( + request: DownloadUrlRequest, + app: AppHandle, + app_prefs: State<'_, AppPreferencesManager>, + app_data_root: State<'_, AppDataRoot>, + app_env_configs: State<'_, AppEnvConfigs>, +) -> ResponseOrErrorType { + + info!("download_url_command called"); + + info!("request: {:?}", request); + + let result = handle_request( + request, + &app, + &app_prefs, + &app_data_root, + &app_env_configs, + ).await; + + if let Err(err) = result { + error!("Error downloading media: {:?}", err); + + let mut endpoint_message = "unknown error when downloading file"; + let mut error_type = DownloadUrlErrorType::UnknownError; + + let mut flash_error_type = FlashFileDownloadErrorType::UnknownError; + let mut flash_filename = None; + let mut flash_message = Some("Failed to download file".to_string()); + + match err { + ArtcraftError::CannotDownloadFilePathAlreadyExists { path } => { + endpoint_message = "file already downloaded"; + error_type = DownloadUrlErrorType::FilesystemError; + + flash_error_type = FlashFileDownloadErrorType::FileAlreadyDownloaded; + flash_message = Some(format!("File already downloaded: {:?}", path)); + flash_filename = Some(path); + } + _ => {}, // NB: Fall-through + } + + let event = FlashFileDownloadErrorEvent { + filename: flash_filename, + message: flash_message, + error_type: flash_error_type, + }; + + event.send_infallible(&app); + + return Err(CommandErrorResponseWrapper { + status: CommandErrorStatus::ServerError, + error_message: Some(endpoint_message.to_string()), + error_type: Some(error_type), + error_details: None, + }); + } + + Ok(DownloadUrlSuccessResponse {}.into()) +} + + +pub async fn handle_request( + request: DownloadUrlRequest, + app: &AppHandle, + app_prefs: &AppPreferencesManager, + app_data_root: &AppDataRoot, + app_env_configs: &AppEnvConfigs +) -> Result<(), ArtcraftError> { + + let app_prefs = app_prefs.get_clone()?; + + let download_path = download_url_to_user_download_dir( + &request.url, + app_data_root, + &app_prefs + ).await?; + + info!("downloaded to: {:?}", download_path); + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/commands/download/mod.rs b/crates/desktop/artcraft/src/core/commands/download/mod.rs new file mode 100644 index 0000000000..ad9c6330d3 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/download/mod.rs @@ -0,0 +1,3 @@ +pub mod download_directory_reveal_command; +pub mod download_media_file_command; +pub mod download_url_command; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs b/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs index 27c8838aa5..4d68a31447 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/generate_error.rs @@ -169,6 +169,9 @@ impl From for GenerateError { ArtcraftError::RwLockReadError => Self::AnyhowError(anyhow!("Lock read error")), ArtcraftError::RwLockWriteError => Self::AnyhowError(anyhow!("Lock write error")), ArtcraftError::MutexLockError => Self::AnyhowError(anyhow!("Mutex lock error")), + ArtcraftError::ReqwestError(err) => Self::AnyhowError(anyhow!("Reqwest error: {:?}", err)), + ArtcraftError::BadDownloadFilename { path } => Self::AnyhowError(anyhow!("Bad download file name: {:?}", path)), + ArtcraftError::CannotDownloadFilePathAlreadyExists { path } => Self::AnyhowError(anyhow!("Cannot download already existing file: {:?}", path)), } } } diff --git a/crates/desktop/artcraft/src/core/commands/get_app_info_command.rs b/crates/desktop/artcraft/src/core/commands/get_app_info_command.rs index 5a5e6795a1..49e386a677 100644 --- a/crates/desktop/artcraft/src/core/commands/get_app_info_command.rs +++ b/crates/desktop/artcraft/src/core/commands/get_app_info_command.rs @@ -1,11 +1,15 @@ use crate::core::commands::response::shorthand::InfallibleResponse; use crate::core::commands::response::success_response_wrapper::SerializeMarker; use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use crate::core::state::app_preferences::app_preferences_manager::AppPreferencesManager; use crate::core::state::artcraft_platform_info::{ArtcraftOs, ArtcraftPlatformInfo}; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; use crate::core::state::os_platform::OsPlatform; use chrono::{DateTime, Utc}; -use log::info; +use log::{info, warn}; use serde_derive::Serialize; +use std::path::PathBuf; use tauri::State; #[derive(Debug, Serialize)] @@ -22,6 +26,9 @@ pub struct AppInfoResponse { pub os_version: String, pub storyteller_host: String, + + pub artcraft_root_directory: PathBuf, + pub download_directory: PathBuf, } impl SerializeMarker for AppInfoResponse {} @@ -38,8 +45,10 @@ pub enum DetectedOs { #[tauri::command] pub fn get_app_info_command( + app_data_root: State<'_, AppDataRoot>, app_env_configs: State<'_, AppEnvConfigs>, artcraft_platform_info: State<'_, ArtcraftPlatformInfo>, + app_prefs: State<'_, AppPreferencesManager>, ) -> InfallibleResponse { info!("get_app_info_command called..."); @@ -52,6 +61,19 @@ pub fn get_app_info_command( ArtcraftOs::Unknown => DetectedOs::Unknown, }; + let root_directory = app_data_root.path().to_path_buf(); + + let download_directory = match app_prefs.get_clone() { + Ok(app_prefs) => { + app_prefs.preferred_download_directory + .download_directory(&app_data_root) + } + Err(err) => { + warn!("Can't get user download directory: {:?}", err); + app_data_root.downloads_dir().path().to_path_buf() + } + }; + AppInfoResponse { artcraft_version: artcraft_platform_info.artcraft_version.clone(), build_timestamp: artcraft_platform_info.build_timestamp, @@ -61,6 +83,8 @@ pub fn get_app_info_command( os_platform, os_version: artcraft_platform_info.os_version.clone(), storyteller_host, + artcraft_root_directory: root_directory, + download_directory, }.into() } diff --git a/crates/desktop/artcraft/src/core/commands/mod.rs b/crates/desktop/artcraft/src/core/commands/mod.rs index 393d80300b..422160e9d0 100644 --- a/crates/desktop/artcraft/src/core/commands/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/mod.rs @@ -1,5 +1,6 @@ pub mod app_preferences; pub mod app_state; +pub mod download; pub mod enqueue; pub mod flip_image; pub mod get_app_info_command; diff --git a/crates/desktop/artcraft/src/core/events/warning_events/flash_file_download_error_event.rs b/crates/desktop/artcraft/src/core/events/warning_events/flash_file_download_error_event.rs new file mode 100644 index 0000000000..a1bea798a4 --- /dev/null +++ b/crates/desktop/artcraft/src/core/events/warning_events/flash_file_download_error_event.rs @@ -0,0 +1,26 @@ +use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent}; +use enums::tauri::ux::tauri_event_name::TauriEventName; +use serde_derive::Serialize; +use std::path::PathBuf; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct FlashFileDownloadErrorEvent { + pub error_type: FlashFileDownloadErrorType, + pub filename: Option, + pub message: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FlashFileDownloadErrorType { + FileAlreadyDownloaded, + FilesystemError, + NetworkError, + UnknownError, +} + +impl BasicSendableEvent for FlashFileDownloadErrorEvent { + const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::FlashFileDownloadErrorEvent; + const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure; +} diff --git a/crates/desktop/artcraft/src/core/events/warning_events/mod.rs b/crates/desktop/artcraft/src/core/events/warning_events/mod.rs index a8a256fb0d..c7d712d92d 100644 --- a/crates/desktop/artcraft/src/core/events/warning_events/mod.rs +++ b/crates/desktop/artcraft/src/core/events/warning_events/mod.rs @@ -1 +1,2 @@ +pub mod flash_file_download_error_event; pub mod flash_user_input_error_event; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs b/crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs index f8c2a950cb..dd065998f8 100644 --- a/crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs +++ b/crates/desktop/artcraft/src/core/state/app_preferences/preferred_download_directory.rs @@ -1,8 +1,10 @@ use errors::AnyhowError; use serde_derive::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::core::state::data_dir::trait_data_subdir::DataSubdir; const SYSTEM_DEFAULT_SENTINEL_VALUE: &str = "system_default"; @@ -26,6 +28,27 @@ pub enum SystemDownloadDirectory { Documents, } +impl PreferredDownloadDirectory { + /// This doesn't have a fallback. + pub fn maybe_to_download_directory(&self) -> Option { + match self { + PreferredDownloadDirectory::Custom(path) => Some(path.clone()), + PreferredDownloadDirectory::System(SystemDownloadDirectory::Documents) => { + dirs::document_dir() + } + PreferredDownloadDirectory::System(SystemDownloadDirectory::Downloads) => { + dirs::download_dir() + } + } + } + + /// Fallback to the Artcraft directory. + pub fn download_directory(&self, root: &AppDataRoot) -> PathBuf { + let maybe_path = self.maybe_to_download_directory(); + maybe_path.unwrap_or_else(|| root.downloads_dir().path().to_path_buf()) + } +} + impl FromStr for SystemDownloadDirectory { type Err = AnyhowError; diff --git a/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs b/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs new file mode 100644 index 0000000000..7196ef45ae --- /dev/null +++ b/crates/desktop/artcraft/src/core/utils/download_url_to_user_download_dir.rs @@ -0,0 +1,127 @@ +use crate::core::artcraft_error::ArtcraftError; +use crate::core::state::app_preferences::app_preferences::AppPreferences; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use anyhow::anyhow; +use errors::AnyhowResult; +use log::{error, info}; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::PathBuf; +use url::Url; + +pub async fn download_url_to_user_download_dir( + url: &Url, + app_data_root: &AppDataRoot, + app_prefs: &AppPreferences, +) -> Result { + + let url_file_name = { + let path_segments = url.path_segments() + .ok_or_else(|| ArtcraftError::AnyhowError(anyhow!("URL does not have path segments (1): {:?}", url)))?; + + let mut url_file_name = path_segments.last() + .ok_or_else(|| ArtcraftError::AnyhowError(anyhow!("URL does not have path segments (2): {:?}", url)))? + .trim() + .to_string(); + + if !url_file_name.to_lowercase().starts_with("artcraft") { + url_file_name = format!("artcraft_{}", url_file_name); + } + + url_file_name + }; + + check_url_file_name_for_downloadability(&url_file_name)?; + + let download_directory = app_prefs + .preferred_download_directory + .download_directory(app_data_root); + + info!("Pushing path `{}` onto downloaded directory `{:?}`", url_file_name, download_directory); + + let download_filename = { + let mut download_filename = download_directory.clone(); + download_filename.push(&url_file_name); + download_filename + }; + + info!("Download filename: `{:?}`", download_filename); + + if download_filename == download_directory { + return Err(ArtcraftError::AnyhowError(anyhow!("Download filename resolved to directory: {:?}", download_filename)))?; + } + + if download_filename.exists() { + if download_filename.is_dir() { + return Err(ArtcraftError::AnyhowError(anyhow!("Download path exists and resolved to directory: {:?}", download_filename))); + } else { + return Err(ArtcraftError::CannotDownloadFilePathAlreadyExists { path: download_filename }.into()); + } + } + + info!("Downloading bytes from url: {:?}", url); + + let response = reqwest::get(url.clone()).await?; + let response_bytes = response.bytes().await?; + + info!("Writing file: {:?}", download_filename); + + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&download_filename)?; + + file.write_all(&response_bytes)?; + + Ok(download_filename) +} + +// TODO: This likely needs more protections. +fn check_url_file_name_for_downloadability(filename: &str) -> Result<(), ArtcraftError> { + if filename.trim().is_empty() { + error!("Download filename is empty!"); + return Err(ArtcraftError::BadDownloadFilename { path: "".into() }.into()); + } + + if !filename.is_ascii() { + error!("Download filename is not completely ASCII: {:?}", filename); + return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); + } + + if filename.contains("/") + || filename.contains("..") + || filename.contains("\\") + { + error!("Cannot download filename that has relative path components: {:?}", filename); + return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); + } + + if filename.contains("'") + || filename.contains("\"") + || filename.contains("%") + || filename.contains("<") + || filename.contains(">") + || filename.contains("|") + { + error!("Download filename has dangerous path components: {:?}", filename); + return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); + } + + if filename.ends_with(".apk") + || filename.ends_with(".app") + || filename.ends_with(".bat") + || filename.ends_with(".cmd") + || filename.ends_with(".com") + || filename.ends_with(".ps1") + || filename.ends_with(".sh") + || filename.ends_with(".so") + || filename.ends_with("dll") // Don't even risk the '.' + || filename.ends_with("exe") // Don't even risk the '.' + { + error!("Cannot download filename that resembles executable: {:?}", filename); + return Err(ArtcraftError::BadDownloadFilename { path: filename.into() }.into()); + } + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/core/utils/mod.rs b/crates/desktop/artcraft/src/core/utils/mod.rs index 1fc9359db5..f3ac67fd4c 100644 --- a/crates/desktop/artcraft/src/core/utils/mod.rs +++ b/crates/desktop/artcraft/src/core/utils/mod.rs @@ -1,6 +1,7 @@ pub mod clear_all_webview_cookies; pub mod download_media_file_to_temp_dir; pub mod download_url_to_temp_dir; +pub mod download_url_to_user_download_dir; pub mod enum_conversion; pub mod get_url_file_extension; pub mod image; diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index 7a80ebde78..87125cf8ef 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -5,6 +5,9 @@ use tauri::Manager; use crate::core::commands::app_preferences::get_app_preferences_command::get_app_preferences_command; use crate::core::commands::app_preferences::update_app_preference_command::update_app_preferences_command; +use crate::core::commands::download::download_directory_reveal_command::download_directory_reveal_command; +use crate::core::commands::download::download_media_file_command::download_media_file_command; +use crate::core::commands::download::download_url_command::download_url_command; use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command; use crate::core::commands::enqueue::image_edit::enqueue_edit_image_command::enqueue_edit_image_command; use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::enqueue_image_inpaint_command; @@ -127,6 +130,7 @@ pub fn run() { let builder = tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_upload::init()) .setup(move |app| { // TODO(bt): This is broken on windows @@ -190,6 +194,9 @@ pub fn run() { // My first attempt at naively doing this didn't work because the macros can't find their codegen'd targets. let builder = builder.invoke_handler(tauri::generate_handler![ check_sora_session_command, + download_directory_reveal_command, + download_media_file_command, + download_url_command, enqueue_edit_image_command, enqueue_image_bg_removal_command, enqueue_image_inpaint_command, @@ -222,10 +229,10 @@ pub fn run() { storyteller_open_customer_portal_cancel_plan_command, storyteller_open_customer_portal_manage_plan_command, storyteller_open_customer_portal_switch_plan_command, - tasks_nuke_all_command, storyteller_open_customer_portal_update_payment_method_command, storyteller_open_subscription_purchase_command, storyteller_purge_credentials_command, + tasks_nuke_all_command, update_app_preferences_command, worldlabs_clear_credentials_command, worldlabs_get_credential_info_command, diff --git a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs index 6a9066e395..bb2f6664dd 100644 --- a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs +++ b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs @@ -97,6 +97,11 @@ pub enum TauriEventName { /// Flash a user input error message #[serde(rename = "flash_user_input_error_event")] FlashUserInputErrorEvent, + + /// Warning event: + /// File with that name was already downloaded + #[serde(rename = "flash_file_download_error_event")] + FlashFileDownloadErrorEvent, } impl_enum_display_and_debug_using_to_str!(TauriEventName); @@ -124,6 +129,7 @@ impl TauriEventName { Self::ShowProviderBillingModalEvent => "show_provider_billing_modal_event", Self::ShowProviderLoginModalEvent => "show_provider_login_modal_event", Self::FlashUserInputErrorEvent => "flash_user_input_error_event", + Self::FlashFileDownloadErrorEvent => "flash_file_download_error_event", } } @@ -145,6 +151,7 @@ impl TauriEventName { "show_provider_billing_modal_event" => Ok(Self::ShowProviderBillingModalEvent), "show_provider_login_modal_event" => Ok(Self::ShowProviderLoginModalEvent), "flash_user_input_error_event" => Ok(Self::FlashUserInputErrorEvent), + "flash_file_download_error_event" => Ok(Self::FlashFileDownloadErrorEvent), _ => Err(format!("invalid tauri_event_name: {:?}", job_status)), } } @@ -169,6 +176,7 @@ impl TauriEventName { Self::ShowProviderBillingModalEvent, Self::ShowProviderLoginModalEvent, Self::FlashUserInputErrorEvent, + Self::FlashFileDownloadErrorEvent, ]) } } @@ -199,6 +207,7 @@ mod tests { assert_serialization(TauriEventName::ShowProviderBillingModalEvent, "show_provider_billing_modal_event"); assert_serialization(TauriEventName::ShowProviderLoginModalEvent, "show_provider_login_modal_event"); assert_serialization(TauriEventName::FlashUserInputErrorEvent, "flash_user_input_error_event"); + assert_serialization(TauriEventName::FlashFileDownloadErrorEvent, "flash_file_download_error_event"); } #[test] @@ -219,6 +228,7 @@ mod tests { assert_eq!(TauriEventName::ShowProviderBillingModalEvent.to_str(), "show_provider_billing_modal_event"); assert_eq!(TauriEventName::ShowProviderLoginModalEvent.to_str(), "show_provider_login_modal_event"); assert_eq!(TauriEventName::FlashUserInputErrorEvent.to_str(), "flash_user_input_error_event"); + assert_eq!(TauriEventName::FlashFileDownloadErrorEvent.to_str(), "flash_file_download_error_event"); } #[test] @@ -239,12 +249,13 @@ mod tests { assert_eq!(TauriEventName::from_str("show_provider_billing_modal_event").unwrap(), TauriEventName::ShowProviderBillingModalEvent); assert_eq!(TauriEventName::from_str("show_provider_login_modal_event").unwrap(), TauriEventName::ShowProviderLoginModalEvent); assert_eq!(TauriEventName::from_str("flash_user_input_error_event").unwrap(), TauriEventName::FlashUserInputErrorEvent); + assert_eq!(TauriEventName::from_str("flash_file_download_error_event").unwrap(), TauriEventName::FlashFileDownloadErrorEvent); } #[test] fn all_variants() { let mut variants = TauriEventName::all_variants(); - assert_eq!(variants.len(), 16); + assert_eq!(variants.len(), 17); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent)); @@ -261,6 +272,7 @@ mod tests { assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderBillingModalEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderLoginModalEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::FlashUserInputErrorEvent)); + assert_eq!(variants.pop_first(), Some(TauriEventName::FlashFileDownloadErrorEvent)); assert_eq!(variants.pop_first(), None); } } diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx index fef0f6f8e9..857f4f9059 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx @@ -63,6 +63,7 @@ import { APP_DESCRIPTORS } from "~/config/appMenu"; import { AppsQuickMenu } from "./AppsQuickMenu"; import { useRemoveBackgroundStore } from "~/pages/PageRemoveBackground/RemoveBackgroundStore"; import { useImageTo3DWorldStore } from "~/pages/PageImageTo3DWorld/ImageTo3DWorldStore"; +import { DownloadUrl } from "@storyteller/tauri-api"; interface Props { pageName: string; @@ -170,14 +171,17 @@ export const TopBar = ({ pageName }: Props) => { const downloadFile = async (url: string, mediaClass?: string) => { try { - await downloadFileFromUrl(url); + //await downloadFileFromUrl(url); + await DownloadUrl(url); if (mediaClass === FilterMediaClasses.DIMENSIONAL) { toast.success(`Downloaded 3D model`); } else { toast.success(`Downloaded ${mediaClass}`); } } catch (error) { - toast.error("Failed to download file"); + console.error(">>> Failed to download file:", error); + // NB: Rust/Tauri should now flash a toast instead. + //toast.error("Failed to download file"); } }; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEnigma.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEnigma.tsx index 6f4595feb4..1717203cee 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEnigma.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEnigma.tsx @@ -31,7 +31,7 @@ import { actionReminderProps, ActionReminderModal, } from "@storyteller/ui-action-reminder-modal"; -import { useFlashUserInputErrorEvent } from "@storyteller/tauri-events"; +import { useFlashFileDownloadErrorEvent, useFlashUserInputErrorEvent } from "@storyteller/tauri-events"; import { useGenerationCompleteEvent } from "@storyteller/tauri-events"; import { useGenerationEnqueueFailureEvent } from "@storyteller/tauri-events"; import { useGenerationEnqueueSuccessEvent } from "@storyteller/tauri-events"; @@ -98,6 +98,11 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => { toast.error(event.message); }); + useFlashFileDownloadErrorEvent(async (event) => { + console.log("Flash file download error event received:", event); + toast.error(event.message || "File download failed"); + }); + useEffect(() => { console.log("installing event listeners"); InstallSounds(); diff --git a/frontend/libs/components/settings-modal/src/lib/panes/AboutSettingsPane.tsx b/frontend/libs/components/settings-modal/src/lib/panes/AboutSettingsPane.tsx index e5c2014d8e..b94305bacf 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/AboutSettingsPane.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/AboutSettingsPane.tsx @@ -60,6 +60,20 @@ export const AboutSettingsPane = (args: AboutSettingsPaneProps) => {
{appInfo?.os_platform} ({appInfo?.os_version})
+
+ +
{appInfo?.artcraft_root_directory}
+
+ +
+ +
{appInfo?.download_directory}
+
+ ); diff --git a/frontend/libs/components/settings-modal/src/lib/panes/MiscSettingsPane.tsx b/frontend/libs/components/settings-modal/src/lib/panes/MiscSettingsPane.tsx index eb3cdaf95c..d6873046ad 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/MiscSettingsPane.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/MiscSettingsPane.tsx @@ -9,6 +9,9 @@ import { import { PreferenceName, UpdateAppPreferences } from "@storyteller/tauri-api"; import { open } from "@tauri-apps/plugin-dialog"; import { Label } from "@storyteller/ui-label"; +import { DownloadDirectoryReveal } from "@storyteller/tauri-api"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFolder, faMagnifyingGlass, faRotateLeft } from "@fortawesome/pro-solid-svg-icons"; interface MiscSettingsPaneProps {} @@ -71,6 +74,10 @@ export const MiscSettingsPane = (args: MiscSettingsPaneProps) => { await reloadPreferences(); }; + const showDirectory = async () => { + await DownloadDirectoryReveal(); + }; + return (
@@ -85,11 +92,17 @@ export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
- +
); diff --git a/frontend/libs/tauri-api/src/index.ts b/frontend/libs/tauri-api/src/index.ts index 1cc0b07bbd..0e90142e0e 100644 --- a/frontend/libs/tauri-api/src/index.ts +++ b/frontend/libs/tauri-api/src/index.ts @@ -5,6 +5,8 @@ export * from "./lib/artcraft/task_queue/GetTaskQueue.js"; export * from "./lib/artcraft/task_queue/MarkTaskAsDismissed.js"; export * from "./lib/artcraft/task_queue/TasksNukeAll.js"; export * from "./lib/common/CommandStatus.js"; +export * from "./lib/download/DownloadDirectoryReveal.js"; +export * from "./lib/download/DownloadUrl.js"; export * from "./lib/enqueue/EnqueueEditImage.js"; export * from "./lib/enqueue/EnqueueImageBgRemovalCommand.js"; export * from "./lib/enqueue/EnqueueImageInpaint.js"; diff --git a/frontend/libs/tauri-api/src/lib/download/DownloadDirectoryReveal.ts b/frontend/libs/tauri-api/src/lib/download/DownloadDirectoryReveal.ts new file mode 100644 index 0000000000..d794374fed --- /dev/null +++ b/frontend/libs/tauri-api/src/lib/download/DownloadDirectoryReveal.ts @@ -0,0 +1,17 @@ +import { invoke } from "@tauri-apps/api/core"; +import { CommandResult } from "../common/CommandStatus"; + +export interface DownloadDirectoryRevealSuccess extends CommandResult { + payload: DownloadDirectoryRevealPayload; +} + +export interface DownloadDirectoryRevealPayload { +} + +export const DownloadDirectoryReveal = async () : Promise => { + try { + return await invoke("download_directory_reveal_command") as DownloadDirectoryRevealSuccess; + } catch (error) { + throw error; + } +} diff --git a/frontend/libs/tauri-api/src/lib/download/DownloadUrl.ts b/frontend/libs/tauri-api/src/lib/download/DownloadUrl.ts new file mode 100644 index 0000000000..b8d13bd2ad --- /dev/null +++ b/frontend/libs/tauri-api/src/lib/download/DownloadUrl.ts @@ -0,0 +1,21 @@ +import { invoke } from "@tauri-apps/api/core"; +import { CommandResult } from "../common/CommandStatus"; + +export interface DownloadUrlSuccess extends CommandResult { + payload: DownloadUrlPayload; +} + +export interface DownloadUrlPayload { +} + +export const DownloadUrl = async (url: string) : Promise => { + try { + return await invoke("download_url_command", { + request: { + url: url + } + }) as DownloadUrlSuccess; + } catch (error) { + throw error; + } +} diff --git a/frontend/libs/tauri-api/src/lib/system/GetAppInfo.ts b/frontend/libs/tauri-api/src/lib/system/GetAppInfo.ts index a9f824dca0..1ede9d4869 100644 --- a/frontend/libs/tauri-api/src/lib/system/GetAppInfo.ts +++ b/frontend/libs/tauri-api/src/lib/system/GetAppInfo.ts @@ -14,6 +14,8 @@ export interface GetAppInfoPayload { storyteller_host?: string | null; os_platform?: string | null; os_version?: string | null; + artcraft_root_directory: string; + download_directory: string; } // Returns the Success and Error variants directly. diff --git a/frontend/libs/tauri-events/src/index.ts b/frontend/libs/tauri-events/src/index.ts index 4dd36cea48..79eb7764cd 100644 --- a/frontend/libs/tauri-events/src/index.ts +++ b/frontend/libs/tauri-events/src/index.ts @@ -13,3 +13,4 @@ export * from "./lib/events/generation/useGenerationEnqueueFailureEvent"; export * from "./lib/events/generation/useGenerationEnqueueSuccessEvent"; export * from "./lib/events/generation/useGenerationFailedEvent"; export * from "./lib/events/warning/FlashUserInputErrorEvent"; +export * from "./lib/events/warning/FlashFileDownloadErrorEvent"; diff --git a/frontend/libs/tauri-events/src/lib/events/warning/FlashFileDownloadErrorEvent.ts b/frontend/libs/tauri-events/src/lib/events/warning/FlashFileDownloadErrorEvent.ts new file mode 100644 index 0000000000..c944f32139 --- /dev/null +++ b/frontend/libs/tauri-events/src/lib/events/warning/FlashFileDownloadErrorEvent.ts @@ -0,0 +1,43 @@ +import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import { BasicEventWrapper } from '../../common/BasicEventWrapper'; +import { useEffect } from 'react'; + +const EVENT_NAME : string = 'flash_file_download_error_event'; + +export enum FlashFileDownloadErrorType { + FileAlreadyDownloaded = "file_already_downloaded", + FilesystemError = "filesystem_error", + NetworkError = "network_error", + UnknownError = "unknown_error", +} + +export interface FlashFileDownloadErrorEvent { + error_type: FlashFileDownloadErrorType, + message?: string, + filename?: string, +} + +export const useFlashFileDownloadErrorEvent = (asyncCallback: (event: FlashFileDownloadErrorEvent) => Promise) => { + useEffect(() => { + let isUnmounted = false; + let unlisten: Promise; + + const setup = async () => { + unlisten = listen>(EVENT_NAME, async (wrappedEvent) => { + await asyncCallback(wrappedEvent.payload.data); + }); + + if (isUnmounted) { + unlisten.then(f => f()); // Unsubscribe if unmounted early. + } + }; + + setup(); + + return () => { + isUnmounted = true; + unlisten.then(f => f()); + }; + + }, []); +}