mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65fbe5fd35 | ||
|
|
aab42703bb | ||
|
|
9246d28b26 | ||
|
|
d9cbd3e8c7 | ||
|
|
ba4871118b | ||
|
|
d9018028ab | ||
|
|
d2f6f56881 | ||
|
|
e35b29e317 |
Generated
+162
-8
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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<std::io::Error> for ArtcraftError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ArtcraftError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::ReqwestError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GrokError> for ArtcraftError {
|
||||
fn from(value: GrokError) -> Self {
|
||||
Self::GrokError(value)
|
||||
|
||||
+70
@@ -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<DownloadDirectoryRevealSuccessResponse> {
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -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<DownloadMediaFileSuccessResponse, DownloadMediaFileErrorType> {
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -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<DownloadUrlSuccessResponse, DownloadUrlErrorType> {
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod download_directory_reveal_command;
|
||||
pub mod download_media_file_command;
|
||||
pub mod download_url_command;
|
||||
@@ -169,6 +169,9 @@ impl From<ArtcraftError> 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AppInfoResponse> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+26
@@ -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<PathBuf>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod flash_file_download_error_event;
|
||||
pub mod flash_user_input_error_event;
|
||||
+24
-1
@@ -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<PathBuf> {
|
||||
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;
|
||||
|
||||
|
||||
@@ -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<PathBuf, ArtcraftError> {
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -60,6 +60,20 @@ export const AboutSettingsPane = (args: AboutSettingsPaneProps) => {
|
||||
<div>{appInfo?.os_platform} ({appInfo?.os_version})</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label>
|
||||
Artcraft Data Directory
|
||||
</Label>
|
||||
<div>{appInfo?.artcraft_root_directory}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label>
|
||||
Downloads Directory
|
||||
</Label>
|
||||
<div>{appInfo?.download_directory}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-4 text-base-fg">
|
||||
<div className="space-y-2">
|
||||
@@ -85,11 +92,17 @@ export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" onClick={openDirectoryPicker}>
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
Choose Directory
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={clearDirectory}>
|
||||
<Button variant="destructive" onClick={clearDirectory}>
|
||||
<FontAwesomeIcon icon={faRotateLeft} />
|
||||
Use Default
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={showDirectory}>
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
Show Directory
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<DownloadDirectoryRevealSuccess> => {
|
||||
try {
|
||||
return await invoke("download_directory_reveal_command") as DownloadDirectoryRevealSuccess;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -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<DownloadUrlSuccess> => {
|
||||
try {
|
||||
return await invoke("download_url_command", {
|
||||
request: {
|
||||
url: url
|
||||
}
|
||||
}) as DownloadUrlSuccess;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<void>) => {
|
||||
useEffect(() => {
|
||||
let isUnmounted = false;
|
||||
let unlisten: Promise<UnlistenFn>;
|
||||
|
||||
const setup = async () => {
|
||||
unlisten = listen<BasicEventWrapper<FlashFileDownloadErrorEvent>>(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());
|
||||
};
|
||||
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user