cleanup
This commit is contained in:
parent
ae8c50a0c7
commit
775841e6c7
|
|
@ -1,125 +0,0 @@
|
|||
use crate::commands::generate::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
|
||||
use crate::commands::background_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
|
||||
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::provider_priority::ProviderPriorityStore;
|
||||
use crate::utils::save_base64_image_to_temp_dir::save_base64_image_to_temp_dir;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_client::api_defs::generate::image::bg_removal::remove_image_background::RemoveImageBackgroundRequest;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use uuid_utils::uuid::generate_random_uuid;
|
||||
use log::{error, info};
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::endpoints::generate::image::bg_removal::remove_image_background::remove_image_background;
|
||||
use artcraft_client::endpoints::media_files::upload_image_media_file_from_file::{upload_image_media_file_from_file, UploadImageFromFileArgs};
|
||||
use tauri::AppHandle;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn handle_generic_bg_removal_artcraft(
|
||||
request: &EnqueueImageBgRemovalCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
let creds = match storyteller_creds_manager.get_credentials()? {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials));
|
||||
},
|
||||
};
|
||||
|
||||
info!("Calling Artcraft bg removal ...");
|
||||
|
||||
let uuid_idempotency_token = generate_random_uuid();
|
||||
|
||||
let image_media_token = match &request.image_media_token {
|
||||
Some(image_media_token) => image_media_token.clone(),
|
||||
None => {
|
||||
upload_image_from_base64_bytes(&request, &app_data_root, &app_env_configs, &creds).await?
|
||||
}
|
||||
};
|
||||
|
||||
let request = RemoveImageBackgroundRequest {
|
||||
uuid_idempotency_token,
|
||||
media_file_token: Some(image_media_token),
|
||||
};
|
||||
|
||||
let result = remove_image_background(
|
||||
&app_env_configs.storyteller_host,
|
||||
Some(&creds),
|
||||
request,
|
||||
).await;
|
||||
|
||||
let success_result = match result {
|
||||
Ok(enqueued) => {
|
||||
// TODO(bt,2025-07-05): Enqueue job token?
|
||||
info!("Successfully enqueued Artcraft background removal. Job token: {}",
|
||||
enqueued.inference_job_token);
|
||||
enqueued
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to use Artcraft background removal: {:?}", err);
|
||||
return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
task_type: TaskType::BackgroundRemoval,
|
||||
provider_job_id: Some(success_result.inference_job_token.to_string()),
|
||||
model: None,
|
||||
maybe_queue_status_url: None,
|
||||
maybe_prompt_token: None,
|
||||
maybe_queue_response_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn upload_image_from_base64_bytes(
|
||||
request: &EnqueueImageBgRemovalCommand,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
creds: &StorytellerCredentialSet
|
||||
) -> Result<MediaFileToken, GenerateError> {
|
||||
let temp_file;
|
||||
|
||||
if let Some(base64_bytes) = &request.base64_image {
|
||||
info!("Saving base64 image to temp dir ...");
|
||||
temp_file = save_base64_image_to_temp_dir(&app_data_root, base64_bytes)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to save base64 image to temp dir: {:?}", err);
|
||||
GenerateError::BadInput(BadInputReason::Base64DecodeError)
|
||||
})?;
|
||||
} else {
|
||||
return Err(GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided));
|
||||
};
|
||||
|
||||
info!("Uploading image media file from temp file: {:?}", temp_file.path());
|
||||
|
||||
let result =
|
||||
upload_image_media_file_from_file(UploadImageFromFileArgs {
|
||||
api_host: &app_env_configs.storyteller_host,
|
||||
maybe_creds: Some(&creds),
|
||||
path: temp_file,
|
||||
is_intermediate_system_file: true, // NB: Probably not essential to keep this.
|
||||
maybe_prompt_token: None, // NB: Not used for bg removal.
|
||||
maybe_batch_token: None, // NB: Not used for bg removal.
|
||||
maybe_generation_provider: None,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image media file: {:?}", err);
|
||||
GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err))
|
||||
})?;
|
||||
|
||||
Ok(result.media_file_token)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
pub mod handle_generic_bg_removal_artcraft;
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
use crate::commands::generate::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::commands::generate::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::commands::background_removal::artcraft::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
|
||||
use crate::commands::generate::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
|
||||
use crate::commands::response::shorthand::{Response, ResponseOrErrorType};
|
||||
use crate::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
|
||||
use crate::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
|
||||
use crate::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
|
||||
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::events::sendable_event_trait::SendableEvent;
|
||||
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
|
||||
use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir;
|
||||
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::state::task_database::TaskDatabase;
|
||||
use crate::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::utils::simple_http_download::simple_http_download;
|
||||
use crate::services::sora::state::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use enums::tauri::ux::tauri_command_caller::TauriCommandCaller;
|
||||
use errors::AnyhowError;
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_database::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use std::time::Duration;
|
||||
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use artcraft_client::error::storyteller_error::StorytellerError;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EnqueueImageBgRemovalCommand {
|
||||
// /// The bg removal model to use (optional).
|
||||
// pub model: Option<TODO>,
|
||||
|
||||
/// Image media file; the image to remove the background from.
|
||||
pub image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Base64-encoded image
|
||||
pub base64_image: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// Name of the frontend caller.
|
||||
/// We'll use this to selectively trigger events.
|
||||
pub frontend_caller: Option<TauriCommandCaller>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined identifier that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_id: Option<String>,
|
||||
|
||||
/// OPTIONAL.
|
||||
/// A frontend-defined payload that we'll send back to the frontend
|
||||
/// as a Tauri event on task completion.
|
||||
pub frontend_subscriber_payload: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EnqueueImageBgRemovalErrorType {
|
||||
/// No model available for image generation
|
||||
NoProviderAvailable,
|
||||
|
||||
/// No image was provided for background removal
|
||||
MissingImage,
|
||||
|
||||
/// Bad base64 image data
|
||||
Base64DecodeError,
|
||||
|
||||
/// Generic bad request error
|
||||
BadRequest,
|
||||
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EnqueueImageBgRemovalSuccessResponse {
|
||||
}
|
||||
|
||||
impl SerializeMarker for EnqueueImageBgRemovalSuccessResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enqueue_image_bg_removal_command(
|
||||
app: AppHandle,
|
||||
request: EnqueueImageBgRemovalCommand,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
) -> ResponseOrErrorType<EnqueueImageBgRemovalSuccessResponse, EnqueueImageBgRemovalErrorType> {
|
||||
|
||||
info!("enqueue_image_bg_removal_command called");
|
||||
|
||||
let result = handle_request(
|
||||
&request,
|
||||
&app,
|
||||
&app_data_root,
|
||||
&app_env_configs,
|
||||
&artcraft_usage_tracker,
|
||||
&provider_priority_store,
|
||||
&task_database,
|
||||
&storyteller_creds_manager,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("Error removing background: {:?}", err);
|
||||
|
||||
notify_frontend_of_errors(&app, &err).await;
|
||||
|
||||
// TODO: Derive from err. Make service provider optional.
|
||||
let event = GenerationEnqueueFailureEvent {
|
||||
action: GenerationAction::RemoveBackground,
|
||||
service: GenerationServiceProvider::Artcraft, // FIXME: This is wrong.
|
||||
model: None,
|
||||
reason: None,
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
Err(error_to_tauri_response(err))
|
||||
}
|
||||
Ok(event) => {
|
||||
let event = GenerationEnqueueSuccessEvent {
|
||||
action: event.to_frontend_event_action(),
|
||||
service: event.to_frontend_event_service(),
|
||||
model: None, // FIXME: This isn't right, though we probably don't care for simple bg removal.
|
||||
};
|
||||
|
||||
if let Err(err) = event.send(&app) {
|
||||
error!("Failed to emit event: {:?}", err); // Fail open.
|
||||
}
|
||||
|
||||
CreditsBalanceChangedEvent{}.send_infallible(&app);
|
||||
|
||||
Ok(EnqueueImageBgRemovalSuccessResponse {}.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request(
|
||||
request: &EnqueueImageBgRemovalCommand,
|
||||
app: &AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
artcraft_usage_tracker: &ArtcraftUsageTracker,
|
||||
provider_priority_store: &ProviderPriorityStore,
|
||||
task_database: &TaskDatabase,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
|
||||
// TODO(bt,2025-07-07): Other model/provider routing...
|
||||
|
||||
let success_event = handle_generic_bg_removal_artcraft(
|
||||
request,
|
||||
app,
|
||||
app_data_root,
|
||||
app_env_configs,
|
||||
storyteller_creds_manager,
|
||||
).await?;
|
||||
|
||||
let result = success_event
|
||||
.insert_into_task_database_with_frontend_payload(
|
||||
task_database,
|
||||
request.frontend_caller,
|
||||
request.frontend_subscriber_id.as_deref(),
|
||||
request.frontend_subscriber_payload.as_deref()
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Failed to create task in database: {:?}", err);
|
||||
// NB: Fail open, but find a way to flag this.
|
||||
}
|
||||
|
||||
if let Err(err) = artcraft_usage_tracker.record_image_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
|
||||
// NB: Fail open.
|
||||
warn!("Failed to report usage: {:?}", err);
|
||||
}
|
||||
|
||||
Ok(success_event)
|
||||
}
|
||||
|
||||
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueImageBgRemovalErrorType, ()> {
|
||||
let mut status = CommandErrorStatus::ServerError;
|
||||
let mut error_type = EnqueueImageBgRemovalErrorType::ServerError;
|
||||
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
|
||||
|
||||
match error {
|
||||
GenerateError::NoProviderAvailable => {
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_type = EnqueueImageBgRemovalErrorType::NoProviderAvailable;
|
||||
error_message = "No configured provider available for background removal".to_string();
|
||||
}
|
||||
GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided) => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageBgRemovalErrorType::MissingImage;
|
||||
error_message = "No image provided for background removal".to_string();
|
||||
}
|
||||
GenerateError::BadInput(BadInputReason::Base64DecodeError) => {
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_type = EnqueueImageBgRemovalErrorType::Base64DecodeError;
|
||||
error_message = "Failed to decode base64 image data".to_string();
|
||||
}
|
||||
_ => {}, // Other cases fall through.
|
||||
}
|
||||
|
||||
CommandErrorResponseWrapper {
|
||||
status,
|
||||
error_message: Some(error_message.to_string()),
|
||||
error_type: Some(error_type),
|
||||
error_details: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
pub mod enqueue_image_bg_removal_command;
|
||||
pub (super) mod artcraft;
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::ImageReader;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn flip_image(image: &str) -> Result<String, String> {
|
||||
println!("infer_image called; processing image...");
|
||||
|
||||
let bytes = BASE64_STANDARD.decode(image)
|
||||
.map_err(|err| format!("Base64 decode error: {}", err))?;
|
||||
|
||||
let image = ImageReader::new(Cursor::new(bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|err| format!("Image format error: {}", err))?
|
||||
.decode()
|
||||
.map_err(|err| format!("Image decode error: {}", err))?;
|
||||
|
||||
let rotated_image = image.rotate180();
|
||||
|
||||
let mut buffer = Vec::new(); // TODO: Preallocate, better data structure
|
||||
let mut writer = Cursor::new(&mut buffer);
|
||||
|
||||
let encoder = PngEncoder::new_with_quality
|
||||
(&mut writer, CompressionType::Fast, FilterType::Adaptive);
|
||||
|
||||
rotated_image.write_with_encoder(encoder)
|
||||
.map_err(|err| format!("Image encoding error: {}", err))?;
|
||||
|
||||
let encoded = BASE64_STANDARD.encode(&buffer);
|
||||
|
||||
println!("Encoded: {:?}", encoded.split_at(10).0);
|
||||
|
||||
Ok(encoded)
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
use crate::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::events::functional_events::media_file_deleted_event::MediaFileDeletedEvent;
|
||||
use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use crate::state::task_database::TaskDatabase;
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use artcraft_client::endpoints::media_files::delete_media_file::delete_media_file;
|
||||
use tauri::{AppHandle, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MediaFileDeleteRequest {
|
||||
/// REQUIRED.
|
||||
/// Media file to delete. The user must own the file.
|
||||
pub media_file_token: MediaFileToken,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MediaFileDeleteSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
impl SerializeMarker for MediaFileDeleteSuccessResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn media_file_delete_command(
|
||||
app: AppHandle,
|
||||
request: MediaFileDeleteRequest,
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
) -> ResponseOrErrorMessage<MediaFileDeleteSuccessResponse> {
|
||||
|
||||
info!("media_file_delete_command called");
|
||||
|
||||
let result = handle_request(
|
||||
request,
|
||||
&app,
|
||||
&app_env_configs,
|
||||
&storyteller_creds_manager,
|
||||
).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("media_file_delete_command failed: {:?}", err);
|
||||
return Err("delete failed".into())
|
||||
}
|
||||
|
||||
Ok(MediaFileDeleteSuccessResponse {
|
||||
success: true,
|
||||
}.into())
|
||||
}
|
||||
|
||||
pub async fn handle_request(
|
||||
request: MediaFileDeleteRequest,
|
||||
app: &AppHandle,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
storyteller_creds_manager: &StorytellerCredentialManager,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let creds = storyteller_creds_manager.get_credentials()?;
|
||||
|
||||
let _result = delete_media_file(
|
||||
&app_env_configs.storyteller_host,
|
||||
creds.as_ref(),
|
||||
&request.media_file_token
|
||||
).await?;
|
||||
|
||||
MediaFileDeletedEvent {
|
||||
media_file_token: request.media_file_token,
|
||||
}.send_infallible(app);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
pub mod media_file_delete_command;
|
||||
|
|
@ -1,15 +1,7 @@
|
|||
pub mod app_preferences;
|
||||
pub mod app_state;
|
||||
pub mod background_removal;
|
||||
pub mod cost_estimate;
|
||||
pub mod credentials;
|
||||
pub mod download;
|
||||
pub mod flip_image;
|
||||
pub mod generate;
|
||||
pub mod get_app_info_command;
|
||||
pub mod load_without_cors_command;
|
||||
pub mod media_files;
|
||||
pub mod platform_info_command;
|
||||
pub mod providers;
|
||||
pub mod response;
|
||||
pub mod service;
|
||||
pub mod task_queue;
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
use crate::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use log::{error, info};
|
||||
use serde_derive::Serialize;
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetProviderOrderResponse {
|
||||
pub providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for GetProviderOrderResponse{}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_provider_order_command(
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
) -> ResponseOrErrorMessage<GetProviderOrderResponse> {
|
||||
info!("get_provider_order_command called");
|
||||
|
||||
let providers = provider_priority_store.get_priority()
|
||||
.map_err(|err| {
|
||||
error!("Failed to get provider order: {:?}", err);
|
||||
"Failed to retrieve provider order"
|
||||
})?;
|
||||
|
||||
Ok(GetProviderOrderResponse {
|
||||
providers,
|
||||
}.into())
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
pub mod get_provider_order_command;
|
||||
pub mod set_provider_order_command;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
use crate::commands::response::shorthand::SuccessOrErrorMessage;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::provider_priority::{Provider, ProviderPriorityStore};
|
||||
use log::{error, info};
|
||||
use serde_derive::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetProviderOrderRequest {
|
||||
pub providers: Vec<Provider>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_provider_order_command(
|
||||
request: SetProviderOrderRequest,
|
||||
provider_priority_store: State<'_, ProviderPriorityStore>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> SuccessOrErrorMessage {
|
||||
info!("set_provider_order_command called");
|
||||
|
||||
provider_priority_store.set_priority(&request.providers)
|
||||
.map_err(|err| {
|
||||
error!("Failed to set provider order: {:?}", err);
|
||||
"Failed to set provider order"
|
||||
})?;
|
||||
|
||||
provider_priority_store.persist_to_filesystem(&app_data_root)
|
||||
.map_err(|err| {
|
||||
error!("Failed to persist provider order to filesystem: {:?}", err);
|
||||
"Failed to persist provider order"
|
||||
})?;
|
||||
|
||||
Ok(().into())
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
pub mod deprecated;
|
||||
pub mod provider_list_command;
|
||||
pub mod provider_set_api_key_command;
|
||||
pub mod provider_clear_command;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
use crate::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::providers::credentials::provider_credential_key::ProviderCredentialKey;
|
||||
use crate::providers::credentials::provider_credential_loading_cache::ProviderCredentialLoadingCache;
|
||||
use log::{error, info};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProviderClearRequest {
|
||||
pub provider_credential: ProviderCredentialKey,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProviderClearResponse {}
|
||||
|
||||
impl SerializeMarker for ProviderClearResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn provider_clear_command(
|
||||
request: ProviderClearRequest,
|
||||
credential_cache: State<'_, ProviderCredentialLoadingCache>,
|
||||
) -> ResponseOrErrorMessage<ProviderClearResponse> {
|
||||
info!("provider_clear_command called for key: {:?}", request.provider_credential);
|
||||
|
||||
credential_cache.delete_credentials(request.provider_credential)
|
||||
.map_err(|err| {
|
||||
error!("Failed to clear provider credential: {:?}", err);
|
||||
"Failed to clear provider credential"
|
||||
})?;
|
||||
|
||||
Ok(ProviderClearResponse {}.into())
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
use crate::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::providers::credentials::payload::provider_credential_payload::ProviderCredentialPayload;
|
||||
use crate::providers::credentials::provider_credential_key::ProviderCredentialKey;
|
||||
use crate::providers::credentials::provider_credential_loading_cache::ProviderCredentialLoadingCache;
|
||||
use crate::providers::credentials::provider_credential_type::ProviderCredentialType;
|
||||
use log::{info, warn};
|
||||
use serde_derive::Serialize;
|
||||
use tauri::State;
|
||||
|
||||
/// All known credential keys. Add new ones here as providers are added.
|
||||
const ALL_KEYS: &[ProviderCredentialKey] = &[
|
||||
ProviderCredentialKey::FalApiKey,
|
||||
ProviderCredentialKey::ReplicateApiKey,
|
||||
ProviderCredentialKey::GrokWebLogin,
|
||||
ProviderCredentialKey::HiggsfieldWebLogin,
|
||||
ProviderCredentialKey::MidjourneyLogin,
|
||||
ProviderCredentialKey::RunwayWebLogin,
|
||||
];
|
||||
|
||||
const REDACTED_KEY_VISIBLE_CHARS: usize = 6;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProviderListResponse {
|
||||
pub providers: Vec<ProviderListEntry>,
|
||||
}
|
||||
|
||||
impl SerializeMarker for ProviderListResponse {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProviderListEntry {
|
||||
pub provider_credential: ProviderCredentialKey,
|
||||
pub credential_type: ProviderCredentialType,
|
||||
pub has_credentials: bool,
|
||||
pub maybe_details: Option<ProviderCredentialDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProviderCredentialDetails {
|
||||
/// For API keys: the first few characters followed by asterisks.
|
||||
pub maybe_key_start: Option<String>,
|
||||
/// For API keys: the full key value.
|
||||
pub maybe_full_key: Option<String>,
|
||||
/// For web logins: the email address if available.
|
||||
pub maybe_email_address: Option<String>,
|
||||
/// For web logins: the username if available.
|
||||
pub maybe_username: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn provider_list_command(
|
||||
credential_cache: State<'_, ProviderCredentialLoadingCache>,
|
||||
) -> ResponseOrErrorMessage<ProviderListResponse> {
|
||||
info!("provider_list_command called");
|
||||
|
||||
let mut providers = Vec::with_capacity(ALL_KEYS.len());
|
||||
|
||||
for &key in ALL_KEYS {
|
||||
let maybe_payload = match credential_cache.get_credentials(key) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
warn!("Error checking credential for {:?}: {:?}", key, err);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let has_credentials = maybe_payload.is_some();
|
||||
|
||||
let maybe_details = maybe_payload.map(|payload| match payload {
|
||||
ProviderCredentialPayload::ApiKey(data) => {
|
||||
ProviderCredentialDetails {
|
||||
maybe_key_start: Some(redact_key(data.as_str())),
|
||||
maybe_full_key: Some(data.as_str().to_string()),
|
||||
maybe_email_address: None,
|
||||
maybe_username: None,
|
||||
}
|
||||
}
|
||||
ProviderCredentialPayload::WebLogin(data) => {
|
||||
ProviderCredentialDetails {
|
||||
maybe_key_start: None,
|
||||
maybe_full_key: None,
|
||||
maybe_email_address: data.email_address,
|
||||
maybe_username: data.username,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
providers.push(ProviderListEntry {
|
||||
provider_credential: key,
|
||||
credential_type: key.get_type(),
|
||||
has_credentials,
|
||||
maybe_details,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ProviderListResponse { providers }.into())
|
||||
}
|
||||
|
||||
fn redact_key(key: &str) -> String {
|
||||
if key.len() <= REDACTED_KEY_VISIBLE_CHARS {
|
||||
"********".to_string()
|
||||
} else {
|
||||
let visible = &key[..REDACTED_KEY_VISIBLE_CHARS];
|
||||
format!("{}********", visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
use crate::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::commands::response::success_response_wrapper::SerializeMarker;
|
||||
use crate::providers::credentials::payload::api_key::ApiKeyData;
|
||||
use crate::providers::credentials::payload::provider_credential_payload::ProviderCredentialPayload;
|
||||
use crate::providers::credentials::provider_credential_key::ProviderCredentialKey;
|
||||
use crate::providers::credentials::provider_credential_loading_cache::ProviderCredentialLoadingCache;
|
||||
use log::{error, info};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProviderSetApiKeyRequest {
|
||||
pub provider_credential: ProviderCredentialKey,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProviderSetApiKeyResponse {}
|
||||
|
||||
impl SerializeMarker for ProviderSetApiKeyResponse {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn provider_set_api_key_command(
|
||||
request: ProviderSetApiKeyRequest,
|
||||
credential_cache: State<'_, ProviderCredentialLoadingCache>,
|
||||
) -> ResponseOrErrorMessage<ProviderSetApiKeyResponse> {
|
||||
info!("provider_set_api_key_command called for provider: {:?}", request.provider_credential);
|
||||
|
||||
let api_key_data = ApiKeyData::from_str(&request.api_key);
|
||||
let payload = ProviderCredentialPayload::ApiKey(api_key_data);
|
||||
|
||||
credential_cache.save_credentials(request.provider_credential, payload)
|
||||
.map_err(|err| {
|
||||
error!("Failed to save API key: {:?}", err);
|
||||
"Failed to save API key"
|
||||
})?;
|
||||
|
||||
Ok(ProviderSetApiKeyResponse {}.into())
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pub mod app_preferences;
|
||||
pub mod app_state;
|
||||
pub mod get_app_info_command;
|
||||
pub mod load_without_cors_command;
|
||||
pub mod platform_info_command;
|
||||
|
|
@ -23,8 +23,8 @@ use crate::commands::credentials::edit_api_credential_command::edit_api_credenti
|
|||
use crate::commands::credentials::edit_web_credential_command::edit_web_credential_command;
|
||||
use crate::commands::credentials::list_credentials_command::list_credentials_command;
|
||||
use crate::commands::credentials::open_web_login_command::open_web_login_command;
|
||||
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
|
||||
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
|
||||
use crate::commands::service::app_preferences::get_app_preferences_command::get_app_preferences_command;
|
||||
use crate::commands::service::app_preferences::update_app_preference_command::update_app_preferences_command;
|
||||
use crate::commands::cost_estimate::estimate_audio_cost_command::estimate_audio_cost_command;
|
||||
use crate::commands::cost_estimate::estimate_image_cost_command::estimate_image_cost_command;
|
||||
use crate::commands::cost_estimate::estimate_mesh_cost_command::estimate_mesh_cost_command;
|
||||
|
|
@ -35,22 +35,14 @@ use crate::commands::generate::models::video::list_video_models_command::list_vi
|
|||
use crate::commands::download::download_directory_reveal_command::download_directory_reveal_command;
|
||||
use crate::commands::download::download_media_file_command::download_media_file_command;
|
||||
use crate::commands::download::download_url_command::download_url_command;
|
||||
use crate::commands::background_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command;
|
||||
use crate::commands::generate::generate_audio::generate_audio_command::generate_audio_command;
|
||||
use crate::commands::generate::generate_image::generate_image_command::generate_image_command;
|
||||
use crate::commands::generate::generate_mesh::generate_mesh_command::generate_mesh_command;
|
||||
use crate::commands::generate::generate_splat::generate_splat_command::generate_splat_command;
|
||||
use crate::commands::generate::generate_video::generate_video_command::generate_video_command;
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::get_app_info_command::get_app_info_command;
|
||||
use crate::commands::load_without_cors_command::load_without_cors_command;
|
||||
use crate::commands::media_files::media_file_delete_command::media_file_delete_command;
|
||||
use crate::commands::platform_info_command::platform_info_command;
|
||||
use crate::commands::providers::deprecated::get_provider_order_command::get_provider_order_command;
|
||||
use crate::commands::providers::deprecated::set_provider_order_command::set_provider_order_command;
|
||||
use crate::commands::providers::provider_clear_command::provider_clear_command;
|
||||
use crate::commands::providers::provider_list_command::provider_list_command;
|
||||
use crate::commands::providers::provider_set_api_key_command::provider_set_api_key_command;
|
||||
use crate::commands::service::get_app_info_command::get_app_info_command;
|
||||
use crate::commands::service::load_without_cors_command::load_without_cors_command;
|
||||
use crate::commands::service::platform_info_command::platform_info_command;
|
||||
use crate::commands::task_queue::get_task_queue_command::get_task_queue_command;
|
||||
use crate::commands::task_queue::mark_task_as_dismissed_command::mark_task_as_dismissed_command;
|
||||
use crate::commands::task_queue::tasks_nuke_all_command::tasks_nuke_all_command;
|
||||
|
|
@ -221,7 +213,6 @@ pub fn run() {
|
|||
download_directory_reveal_command,
|
||||
download_media_file_command,
|
||||
download_url_command,
|
||||
enqueue_image_bg_removal_command,
|
||||
estimate_audio_cost_command,
|
||||
estimate_image_cost_command,
|
||||
estimate_mesh_cost_command,
|
||||
|
|
@ -229,7 +220,6 @@ pub fn run() {
|
|||
estimate_video_cost_command,
|
||||
list_image_models_command,
|
||||
list_video_models_command,
|
||||
flip_image,
|
||||
generate_audio_command,
|
||||
generate_image_command,
|
||||
generate_mesh_command,
|
||||
|
|
@ -237,16 +227,10 @@ pub fn run() {
|
|||
generate_video_command,
|
||||
get_app_info_command,
|
||||
get_app_preferences_command,
|
||||
get_provider_order_command,
|
||||
get_task_queue_command,
|
||||
provider_clear_command,
|
||||
provider_list_command,
|
||||
provider_set_api_key_command,
|
||||
load_without_cors_command,
|
||||
mark_task_as_dismissed_command,
|
||||
media_file_delete_command,
|
||||
platform_info_command,
|
||||
set_provider_order_command,
|
||||
tasks_nuke_all_command,
|
||||
update_app_preferences_command,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
import { MediaFileDelete } from "@storyteller/tauri-api";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
export const galleryModalDeleteMedia = (id: string) => MediaFileDelete(id);
|
||||
|
||||
export const galleryModalSubscribeToMediaEvents = (handlers: {
|
||||
onGenerationComplete: () => void;
|
||||
onMediaDeleted: (mediaId: string) => void;
|
||||
}) => {
|
||||
const unlistenGen = listen("generation-complete-event", () => {
|
||||
handlers.onGenerationComplete();
|
||||
});
|
||||
const unlistenDel = listen<any>("media_file_deleted_event", (event) => {
|
||||
const token: string | undefined =
|
||||
event?.payload?.data?.media_file_token ??
|
||||
event?.payload?.media_file_token;
|
||||
if (token) handlers.onMediaDeleted(token);
|
||||
});
|
||||
return () => {
|
||||
unlistenGen.then((fn) => fn());
|
||||
unlistenDel.then((fn) => fn());
|
||||
};
|
||||
};
|
||||
|
|
@ -19,7 +19,6 @@ export type AppId =
|
|||
| "IMAGE_WATERMARK_REMOVAL"
|
||||
| "IMAGE_TO_3D_OBJECT"
|
||||
| "IMAGE_TO_3D_WORLD"
|
||||
| "REMOVE_BACKGROUND"
|
||||
| "ANGLES"
|
||||
| "STORYBOARD"
|
||||
| "BACKGROUND_CHANGE"
|
||||
|
|
@ -78,7 +77,6 @@ export const goToApp = (action?: string) => {
|
|||
"IMAGE_WATERMARK_REMOVAL",
|
||||
"IMAGE_TO_3D_OBJECT",
|
||||
"IMAGE_TO_3D_WORLD",
|
||||
"REMOVE_BACKGROUND",
|
||||
"ANGLES",
|
||||
"STORYBOARD",
|
||||
"BACKGROUND_CHANGE",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import { VideoWatermarkRemover } from "./PageVideoWatermarkRemover";
|
|||
import { ImageWatermarkRemover } from "./PageImageWatermarkRemover";
|
||||
import { ImageTo3DObject } from "./PageImageTo3DObject";
|
||||
import { ImageTo3DWorld } from "./PageImageTo3DWorld";
|
||||
import { RemoveBackground } from "./PageRemoveBackground";
|
||||
import { Angles } from "./PageAngles";
|
||||
import { Storyboard } from "./PageStoryboard";
|
||||
import { PageBackgroundChange } from "./PageBackgroundChange";
|
||||
|
|
@ -211,12 +210,6 @@ const TabBody = () => {
|
|||
<ImageTo3DWorld />
|
||||
</div>
|
||||
);
|
||||
case "REMOVE_BACKGROUND":
|
||||
return (
|
||||
<div>
|
||||
<RemoveBackground />
|
||||
</div>
|
||||
);
|
||||
case "ANGLES":
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,769 +0,0 @@
|
|||
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faWandMagicSparkles,
|
||||
faImages,
|
||||
faPlus,
|
||||
faEye,
|
||||
faDownload,
|
||||
faUpload,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { downloadFileFromUrl } from "@storyteller/api";
|
||||
import toast from "react-hot-toast";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { UploadEntryCard } from "../../components/media/UploadEntryCard";
|
||||
import {
|
||||
useRemoveBackgroundStore,
|
||||
ProcessedImage,
|
||||
} from "./RemoveBackgroundStore";
|
||||
import {
|
||||
EnqueueImageBgRemoval,
|
||||
useCanvasBgRemovedEvent,
|
||||
} from "@storyteller/tauri-api";
|
||||
import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
|
||||
const convertFileToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
if (reader.result) {
|
||||
resolve(reader.result as string);
|
||||
} else {
|
||||
reject(new Error("Failed to convert file to base64."));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error("Error reading file."));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
export const RemoveBackground = () => {
|
||||
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
const [isHoldingCompare, setIsHoldingCompare] = useState(false);
|
||||
const [animationComplete, setAnimationComplete] = useState(true);
|
||||
const [isLoadingImage, setIsLoadingImage] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const clipLayerRef = useRef<HTMLDivElement>(null);
|
||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||
const revealProgressRef = useRef(100);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const processingExternalUrlRef = useRef<string | null>(null);
|
||||
|
||||
const store = useRemoveBackgroundStore();
|
||||
const {
|
||||
images,
|
||||
activeImageId,
|
||||
isProcessing,
|
||||
currentOriginalUrl,
|
||||
pendingJobId,
|
||||
imageDimensions,
|
||||
pendingExternalUrl,
|
||||
setIsProcessing,
|
||||
setCurrentOriginalUrl,
|
||||
setPendingJobId,
|
||||
setImageDimensions,
|
||||
setPendingExternalUrl,
|
||||
addImage,
|
||||
setActiveImage,
|
||||
getActiveImage,
|
||||
} = store;
|
||||
|
||||
const activeImage = getActiveImage();
|
||||
|
||||
const pendingJobIdRef = useRef(pendingJobId);
|
||||
const currentOriginalUrlRef = useRef(currentOriginalUrl);
|
||||
|
||||
useEffect(() => {
|
||||
pendingJobIdRef.current = pendingJobId;
|
||||
}, [pendingJobId]);
|
||||
|
||||
useEffect(() => {
|
||||
currentOriginalUrlRef.current = currentOriginalUrl;
|
||||
}, [currentOriginalUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const processExternalUrl = async () => {
|
||||
if (!pendingExternalUrl) return;
|
||||
if (processingExternalUrlRef.current === pendingExternalUrl) return;
|
||||
|
||||
processingExternalUrlRef.current = pendingExternalUrl;
|
||||
const imageUrl = pendingExternalUrl;
|
||||
setPendingExternalUrl(null);
|
||||
setIsLoadingImage(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], "external-image.png", {
|
||||
type: blob.type,
|
||||
});
|
||||
const base64Image = await convertFileToBase64(file);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageDimensions({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => reject(new Error("Failed to load image"));
|
||||
img.src = imageUrl;
|
||||
});
|
||||
|
||||
setActiveImage(null);
|
||||
setCurrentOriginalUrl(imageUrl);
|
||||
setIsLoadingImage(false);
|
||||
setIsProcessing(true);
|
||||
|
||||
const jobId = uuidv4();
|
||||
setPendingJobId(jobId);
|
||||
|
||||
await EnqueueImageBgRemoval({
|
||||
base64_image: base64Image,
|
||||
frontend_caller: "mini_app",
|
||||
frontend_subscriber_id: jobId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing external image:", error);
|
||||
toast.error("Failed to process image");
|
||||
setIsLoadingImage(false);
|
||||
setIsProcessing(false);
|
||||
setPendingJobId(null);
|
||||
} finally {
|
||||
processingExternalUrlRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
processExternalUrl();
|
||||
}, [
|
||||
pendingExternalUrl,
|
||||
setPendingExternalUrl,
|
||||
setActiveImage,
|
||||
setCurrentOriginalUrl,
|
||||
setIsProcessing,
|
||||
setPendingJobId,
|
||||
setImageDimensions,
|
||||
]);
|
||||
|
||||
const addMenuItems: PopoverItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "Upload Image",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
|
||||
action: "upload",
|
||||
},
|
||||
{
|
||||
label: "Choose from Library",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
|
||||
action: "library",
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleAddMenuSelect = useCallback((item: PopoverItem) => {
|
||||
if (item.action === "upload") {
|
||||
fileInputRef.current?.click();
|
||||
} else if (item.action === "library") {
|
||||
setIsGalleryModalOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startRevealAnimation = useCallback(() => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
revealProgressRef.current = 0;
|
||||
setAnimationComplete(false);
|
||||
|
||||
const duration = 800;
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const progressPercent = eased * 100;
|
||||
|
||||
revealProgressRef.current = progressPercent;
|
||||
|
||||
if (clipLayerRef.current) {
|
||||
clipLayerRef.current.style.clipPath = `inset(0 0 0 ${progressPercent}%)`;
|
||||
}
|
||||
if (progressBarRef.current) {
|
||||
progressBarRef.current.style.left = `${progressPercent}%`;
|
||||
progressBarRef.current.style.display =
|
||||
progressPercent < 100 ? "block" : "none";
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
} else {
|
||||
setAnimationComplete(true);
|
||||
animationFrameRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useCanvasBgRemovedEvent(async (event) => {
|
||||
if (event.maybe_frontend_subscriber_id !== pendingJobIdRef.current) return;
|
||||
|
||||
const newImage: ProcessedImage = {
|
||||
id: uuidv4(),
|
||||
originalUrl: currentOriginalUrlRef.current,
|
||||
processedUrl: event.image_cdn_url,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const loadImage = (src: string): Promise<HTMLImageElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => reject(new Error(`Failed to load: ${src}`));
|
||||
img.src = src;
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const [originalImg] = await Promise.all([
|
||||
loadImage(currentOriginalUrlRef.current),
|
||||
loadImage(event.image_cdn_url),
|
||||
]);
|
||||
|
||||
addImage(newImage);
|
||||
setIsProcessing(false);
|
||||
setPendingJobId(null);
|
||||
toast.success("Saved to Library");
|
||||
|
||||
setImageDimensions({
|
||||
width: originalImg.naturalWidth,
|
||||
height: originalImg.naturalHeight,
|
||||
});
|
||||
|
||||
startRevealAnimation();
|
||||
} catch (error) {
|
||||
console.error("Error loading images:", error);
|
||||
addImage(newImage);
|
||||
setIsProcessing(false);
|
||||
setPendingJobId(null);
|
||||
toast.success("Saved to Library");
|
||||
}
|
||||
});
|
||||
|
||||
const resetAnimationState = useCallback(() => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = null;
|
||||
}
|
||||
revealProgressRef.current = 0;
|
||||
setAnimationComplete(true);
|
||||
setIsHoldingCompare(false);
|
||||
}, []);
|
||||
|
||||
const handleLocalImageSelect = useCallback(
|
||||
async (files: FileList) => {
|
||||
const file = files[0];
|
||||
if (!file || !file.type.startsWith("image/")) return;
|
||||
|
||||
setIsLoadingImage(true);
|
||||
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(file);
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageDimensions({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => reject(new Error("Failed to load image"));
|
||||
img.src = objectUrl;
|
||||
});
|
||||
|
||||
setActiveImage(null);
|
||||
resetAnimationState();
|
||||
setCurrentOriginalUrl(objectUrl);
|
||||
setIsLoadingImage(false);
|
||||
setIsProcessing(true);
|
||||
|
||||
const jobId = uuidv4();
|
||||
setPendingJobId(jobId);
|
||||
|
||||
await EnqueueImageBgRemoval({
|
||||
base64_image: base64Image,
|
||||
frontend_caller: "mini_app",
|
||||
frontend_subscriber_id: jobId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing image:", error);
|
||||
toast.error("Failed to process image");
|
||||
setIsLoadingImage(false);
|
||||
setIsProcessing(false);
|
||||
setPendingJobId(null);
|
||||
}
|
||||
},
|
||||
[
|
||||
setActiveImage,
|
||||
resetAnimationState,
|
||||
setCurrentOriginalUrl,
|
||||
setIsProcessing,
|
||||
setPendingJobId,
|
||||
setImageDimensions,
|
||||
],
|
||||
);
|
||||
|
||||
const handleImageSelect = useCallback((id: string) => {
|
||||
setSelectedGalleryImages((prev) => {
|
||||
if (prev.includes(id)) return prev.filter((x) => x !== id);
|
||||
return [id];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleGallerySelect = useCallback(
|
||||
async (selectedItems: GalleryItem[]) => {
|
||||
const item = selectedItems[0];
|
||||
if (!item || !item.fullImage) {
|
||||
toast.error("No image selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = item.fullImage;
|
||||
setIsGalleryModalOpen(false);
|
||||
setSelectedGalleryImages([]);
|
||||
setIsLoadingImage(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], "library-image.png", { type: blob.type });
|
||||
|
||||
const base64Image = await convertFileToBase64(file);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageDimensions({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => reject(new Error("Failed to load image"));
|
||||
img.src = imageUrl;
|
||||
});
|
||||
|
||||
setActiveImage(null);
|
||||
resetAnimationState();
|
||||
setCurrentOriginalUrl(imageUrl);
|
||||
setIsLoadingImage(false);
|
||||
setIsProcessing(true);
|
||||
|
||||
const jobId = uuidv4();
|
||||
setPendingJobId(jobId);
|
||||
|
||||
await EnqueueImageBgRemoval({
|
||||
base64_image: base64Image,
|
||||
frontend_caller: "mini_app",
|
||||
frontend_subscriber_id: jobId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing gallery image:", error);
|
||||
toast.error("Failed to process image");
|
||||
setIsLoadingImage(false);
|
||||
setIsProcessing(false);
|
||||
setPendingJobId(null);
|
||||
}
|
||||
},
|
||||
[
|
||||
setActiveImage,
|
||||
resetAnimationState,
|
||||
setCurrentOriginalUrl,
|
||||
setIsProcessing,
|
||||
setPendingJobId,
|
||||
setImageDimensions,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const currentActiveImage = getActiveImage();
|
||||
if (!currentActiveImage) {
|
||||
toast.error("No image to download");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await downloadFileFromUrl(currentActiveImage.processedUrl);
|
||||
toast.success("Image saved to Downloads folder");
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error);
|
||||
toast.error("Failed to download image");
|
||||
}
|
||||
}, [getActiveImage]);
|
||||
|
||||
const handleCompareMouseDown = useCallback(() => {
|
||||
setIsHoldingCompare(true);
|
||||
if (clipLayerRef.current) {
|
||||
clipLayerRef.current.style.clipPath = "inset(0 0 0 0)";
|
||||
}
|
||||
if (progressBarRef.current) {
|
||||
progressBarRef.current.style.display = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCompareMouseUp = useCallback(() => {
|
||||
setIsHoldingCompare(false);
|
||||
if (clipLayerRef.current) {
|
||||
clipLayerRef.current.style.clipPath = `inset(0 0 0 ${revealProgressRef.current}%)`;
|
||||
}
|
||||
if (progressBarRef.current && revealProgressRef.current < 100) {
|
||||
progressBarRef.current.style.display = "block";
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isHoldingCompare) {
|
||||
document.addEventListener("mouseup", handleCompareMouseUp);
|
||||
document.addEventListener("mouseleave", handleCompareMouseUp);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("mouseup", handleCompareMouseUp);
|
||||
document.removeEventListener("mouseleave", handleCompareMouseUp);
|
||||
};
|
||||
}, [isHoldingCompare, handleCompareMouseUp]);
|
||||
|
||||
const handleThumbnailClick = useCallback(
|
||||
(img: ProcessedImage) => {
|
||||
setActiveImage(img.id);
|
||||
revealProgressRef.current = 100;
|
||||
setAnimationComplete(true);
|
||||
if (clipLayerRef.current) {
|
||||
clipLayerRef.current.style.clipPath = "inset(0 0 0 100%)";
|
||||
}
|
||||
if (progressBarRef.current) {
|
||||
progressBarRef.current.style.display = "none";
|
||||
}
|
||||
const loadImg = new Image();
|
||||
loadImg.onload = () => {
|
||||
setImageDimensions({
|
||||
width: loadImg.naturalWidth,
|
||||
height: loadImg.naturalHeight,
|
||||
});
|
||||
};
|
||||
loadImg.src = img.originalUrl;
|
||||
},
|
||||
[setActiveImage, setImageDimensions],
|
||||
);
|
||||
|
||||
const hasImages = images.length > 0;
|
||||
const showUploadScreen = !hasImages && !isProcessing;
|
||||
|
||||
const imageContainerStyle = useMemo(() => {
|
||||
if (!imageDimensions) {
|
||||
return { width: "600px", height: "450px" };
|
||||
}
|
||||
|
||||
const horizontalPadding = 128 + 32;
|
||||
const verticalPadding = 128 + 150;
|
||||
|
||||
const availableWidth = windowSize.width - horizontalPadding;
|
||||
const availableHeight = windowSize.height - 56 - verticalPadding;
|
||||
|
||||
const maxWidth = Math.min(availableWidth, 1400);
|
||||
const maxHeight = Math.max(availableHeight, 200);
|
||||
const imageAspect = imageDimensions.width / imageDimensions.height;
|
||||
|
||||
let width = maxWidth;
|
||||
let height = width / imageAspect;
|
||||
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
width = height * imageAspect;
|
||||
}
|
||||
|
||||
width = Math.max(width, 200);
|
||||
height = Math.max(height, 150);
|
||||
|
||||
return { width: `${width}px`, height: `${height}px` };
|
||||
}, [imageDimensions, windowSize.width, windowSize.height]);
|
||||
|
||||
const handleFileInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
handleLocalImageSelect(e.target.files);
|
||||
e.target.value = "";
|
||||
}
|
||||
},
|
||||
[handleLocalImageSelect],
|
||||
);
|
||||
|
||||
const handleOpenGallery = useCallback(() => {
|
||||
setIsGalleryModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseGallery = useCallback(() => {
|
||||
setIsGalleryModalOpen(false);
|
||||
setSelectedGalleryImages([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-panel text-base-fg">
|
||||
<div className="flex flex-1 items-center justify-center overflow-y-auto p-16">
|
||||
<main className="flex h-full w-full flex-col items-center justify-center">
|
||||
{showUploadScreen ? (
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="relative aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<UploadEntryCard
|
||||
icon={faWandMagicSparkles}
|
||||
title="Remove Background"
|
||||
description="Instantly remove backgrounds from your images with AI-powered precision."
|
||||
accentBackgroundClass="bg-violet-500/40"
|
||||
accentBorderClass="border-violet-400/30"
|
||||
accept="image/*"
|
||||
onFilesSelected={handleLocalImageSelect}
|
||||
primaryLabel="Select Image"
|
||||
secondaryLabel="Pick from Library"
|
||||
secondaryIcon={faImages}
|
||||
onSecondaryClick={handleOpenGallery}
|
||||
disabled={isLoadingImage}
|
||||
/>
|
||||
{isLoadingImage && (
|
||||
<div className="bg-ui-panel/80 absolute inset-0 flex items-center justify-center backdrop-blur-sm">
|
||||
<LoadingSpinner className="h-12 w-12" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full max-w-[1400px] flex-col items-center">
|
||||
{(activeImage || isProcessing) && (
|
||||
<div className="flex shrink-0 gap-3 pb-4">
|
||||
<Button
|
||||
variant="action"
|
||||
icon={faEye}
|
||||
onMouseDown={handleCompareMouseDown}
|
||||
disabled={!activeImage || isProcessing}
|
||||
className={twMerge(
|
||||
"border-ui-controls-border select-none border-2 px-6 py-2.5 text-sm font-semibold transition-all",
|
||||
isHoldingCompare
|
||||
? "border-primary bg-primary/20"
|
||||
: "border-ui-controls-border",
|
||||
(!activeImage || isProcessing) &&
|
||||
"cursor-not-allowed opacity-50",
|
||||
)}
|
||||
>
|
||||
{isHoldingCompare
|
||||
? "Showing Original"
|
||||
: "Hold to Compare"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={faDownload}
|
||||
onClick={handleDownload}
|
||||
disabled={!activeImage || isProcessing}
|
||||
className={twMerge(
|
||||
"select-none border-2 border-primary px-6 py-2.5 text-sm font-semibold transition-all",
|
||||
(!activeImage || isProcessing) &&
|
||||
"cursor-not-allowed opacity-50",
|
||||
)}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl border border-ui-panel-border shadow-xl"
|
||||
style={imageContainerStyle}
|
||||
>
|
||||
{isProcessing && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
{currentOriginalUrl && (
|
||||
<img
|
||||
src={currentOriginalUrl}
|
||||
alt="Processing"
|
||||
className="absolute inset-0 h-full w-full object-contain opacity-30"
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10 flex flex-col items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-primary-500/30 border-t-primary-500" />
|
||||
<FontAwesomeIcon
|
||||
icon={faWandMagicSparkles}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-2xl text-primary-400"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-white">
|
||||
Removing Background...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeImage && (
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)
|
||||
`,
|
||||
backgroundSize: "16px 16px",
|
||||
backgroundPosition:
|
||||
"0 0, 0 8px, 8px -8px, -8px 0px",
|
||||
backgroundColor: "#2a2a2a",
|
||||
}}
|
||||
/>
|
||||
|
||||
<img
|
||||
src={activeImage.processedUrl}
|
||||
alt="Background Removed"
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={clipLayerRef}
|
||||
className={twMerge(
|
||||
"absolute inset-0",
|
||||
animationComplete &&
|
||||
"transition-[clip-path] duration-300",
|
||||
)}
|
||||
style={{
|
||||
clipPath: `inset(0 0 0 ${revealProgressRef.current}%)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={activeImage.originalUrl}
|
||||
alt="Original"
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className="absolute bottom-0 top-0 w-1 bg-primary-500 shadow-lg shadow-primary-500/50"
|
||||
style={{
|
||||
left: `${revealProgressRef.current}%`,
|
||||
transform: "translateX(-50%)",
|
||||
display:
|
||||
animationComplete ||
|
||||
revealProgressRef.current >= 100
|
||||
? "none"
|
||||
: "block",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex shrink-0 items-center gap-3 rounded-xl border border-ui-panel-border bg-ui-background p-2">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
<PopoverMenu
|
||||
items={addMenuItems}
|
||||
onSelect={handleAddMenuSelect}
|
||||
mode="button"
|
||||
position="top"
|
||||
showIconsInList
|
||||
buttonClassName={twMerge(
|
||||
"h-14 w-14 border-2 border-dashed border-ui-panel-border bg-ui-controls/50",
|
||||
isProcessing && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
triggerIcon={
|
||||
<FontAwesomeIcon icon={faPlus} className="text-xl" />
|
||||
}
|
||||
/>
|
||||
|
||||
{images.map((img) => (
|
||||
<button
|
||||
key={img.id}
|
||||
onClick={() => handleThumbnailClick(img)}
|
||||
className={twMerge(
|
||||
"relative h-14 w-14 overflow-hidden rounded-lg border-2 transition-all",
|
||||
img.id === activeImageId
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-transparent hover:border-primary/50",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={img.processedUrl}
|
||||
alt="Processed"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GalleryModal
|
||||
isOpen={isGalleryModalOpen}
|
||||
onClose={handleCloseGallery}
|
||||
mode="select"
|
||||
selectedItemIds={selectedGalleryImages}
|
||||
onSelectItem={handleImageSelect}
|
||||
maxSelections={1}
|
||||
onUseSelected={handleGallerySelect}
|
||||
onDownloadClicked={downloadFileFromUrl}
|
||||
forceFilter="image"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
export interface ProcessedImage {
|
||||
id: string;
|
||||
originalUrl: string;
|
||||
processedUrl: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ImageDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface RemoveBackgroundState {
|
||||
images: ProcessedImage[];
|
||||
activeImageId: string | null;
|
||||
isProcessing: boolean;
|
||||
currentOriginalUrl: string;
|
||||
pendingJobId: string | null;
|
||||
imageDimensions: ImageDimensions | null;
|
||||
pendingExternalUrl: string | null;
|
||||
|
||||
addImage: (image: ProcessedImage) => void;
|
||||
setActiveImage: (id: string | null) => void;
|
||||
getActiveImage: () => ProcessedImage | null;
|
||||
setIsProcessing: (value: boolean) => void;
|
||||
setCurrentOriginalUrl: (url: string) => void;
|
||||
setPendingJobId: (id: string | null) => void;
|
||||
setImageDimensions: (dimensions: ImageDimensions | null) => void;
|
||||
setPendingExternalUrl: (url: string | null) => void;
|
||||
clearAll: () => void;
|
||||
}
|
||||
|
||||
export const useRemoveBackgroundStore = create<RemoveBackgroundState>(
|
||||
(set, get) => ({
|
||||
images: [],
|
||||
activeImageId: null,
|
||||
isProcessing: false,
|
||||
currentOriginalUrl: "",
|
||||
pendingJobId: null,
|
||||
imageDimensions: null,
|
||||
pendingExternalUrl: null,
|
||||
|
||||
addImage: (image) => {
|
||||
set((state) => ({
|
||||
images: [...state.images, image],
|
||||
activeImageId: image.id,
|
||||
}));
|
||||
},
|
||||
|
||||
setActiveImage: (id) => {
|
||||
set({ activeImageId: id });
|
||||
},
|
||||
|
||||
getActiveImage: () => {
|
||||
const state = get();
|
||||
return state.images.find((img) => img.id === state.activeImageId) ?? null;
|
||||
},
|
||||
|
||||
setIsProcessing: (value) => {
|
||||
set({ isProcessing: value });
|
||||
},
|
||||
|
||||
setCurrentOriginalUrl: (url) => {
|
||||
set({ currentOriginalUrl: url });
|
||||
},
|
||||
|
||||
setPendingJobId: (id) => {
|
||||
set({ pendingJobId: id });
|
||||
},
|
||||
|
||||
setImageDimensions: (dimensions) => {
|
||||
set({ imageDimensions: dimensions });
|
||||
},
|
||||
|
||||
setPendingExternalUrl: (url) => {
|
||||
set({ pendingExternalUrl: url });
|
||||
},
|
||||
|
||||
clearAll: () => {
|
||||
set({
|
||||
images: [],
|
||||
activeImageId: null,
|
||||
isProcessing: false,
|
||||
currentOriginalUrl: "",
|
||||
pendingJobId: null,
|
||||
imageDimensions: null,
|
||||
pendingExternalUrl: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export { RemoveBackground } from "./RemoveBackground";
|
||||
export { useRemoveBackgroundStore } from "./RemoveBackgroundStore";
|
||||
|
||||
|
|
@ -10,7 +10,6 @@ export type TabId =
|
|||
| "IMAGE_WATERMARK_REMOVAL"
|
||||
| "IMAGE_TO_3D_OBJECT"
|
||||
| "IMAGE_TO_3D_WORLD"
|
||||
| "REMOVE_BACKGROUND"
|
||||
| "ANGLES"
|
||||
| "STORYBOARD"
|
||||
| "BACKGROUND_CHANGE"
|
||||
|
|
|
|||
|
|
@ -14,12 +14,10 @@ import {
|
|||
faPause,
|
||||
faPencil,
|
||||
faPlay,
|
||||
faTrashCan,
|
||||
faVideo,
|
||||
faWandMagicSparkles,
|
||||
faArrowRotateRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { MediaFileDelete } from "@storyteller/tauri-api";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import { Viewer3D } from "@storyteller/ui-viewer-3d";
|
||||
import { WaveformAudioPlayer } from "@storyteller/ui-audio-player";
|
||||
|
|
@ -41,8 +39,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||
import { faCopy, faLink, faCheck } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
showActionReminder,
|
||||
isActionReminderOpen,
|
||||
} from "@storyteller/ui-action-reminder-modal";
|
||||
import {
|
||||
getCreatorIconPathForModelId,
|
||||
|
|
@ -408,33 +404,6 @@ export function LightboxModal({
|
|||
[emblaMainApi, emblaThumbsApi],
|
||||
);
|
||||
|
||||
const onDeleteClicked = useCallback(
|
||||
(mediaToken: string) => {
|
||||
showActionReminder({
|
||||
reminderType: "default",
|
||||
title: "Delete this media?",
|
||||
message: (
|
||||
<p className="text-sm text-white/70">
|
||||
This will permanently remove the media from your library. This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
),
|
||||
primaryActionText: "Delete",
|
||||
secondaryActionText: "Cancel",
|
||||
primaryActionBtnClassName: "bg-red text-white hover:bg-red/90",
|
||||
onPrimaryAction: async () => {
|
||||
try {
|
||||
await MediaFileDelete(mediaToken);
|
||||
} finally {
|
||||
isActionReminderOpen.value = false;
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
if (!emblaMainApi || !emblaThumbsApi) return;
|
||||
const index = emblaMainApi.selectedScrollSnap();
|
||||
|
|
@ -1256,20 +1225,6 @@ export function LightboxModal({
|
|||
</Button>
|
||||
)}
|
||||
|
||||
{selectedMediaToken && onDeleteClicked && (
|
||||
<Button
|
||||
icon={faTrashCan}
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="destructive"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("delete_media_clicked");
|
||||
await onDeleteClicked(selectedMediaToken);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,18 +23,15 @@
|
|||
{
|
||||
"path": "../../analytics/google-analytics"
|
||||
},
|
||||
{
|
||||
"path": "../audio-player"
|
||||
},
|
||||
{
|
||||
"path": "../viewer-3d"
|
||||
},
|
||||
{
|
||||
"path": "../loading-spinner"
|
||||
},
|
||||
{
|
||||
"path": "../audio-player"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api"
|
||||
},
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -64,18 +64,15 @@
|
|||
{
|
||||
"path": "../../analytics/google-analytics/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../audio-player/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../viewer-3d/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../loading-spinner/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../audio-player/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../tauri-api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,233 +0,0 @@
|
|||
import { useEffect, useState, ReactNode } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faGripVertical,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
SetProviderOrder,
|
||||
Provider,
|
||||
GetProviderOrder,
|
||||
} from "@storyteller/tauri-api";
|
||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
|
||||
interface ProviderItem {
|
||||
id: Provider;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
isUpdating?: boolean;
|
||||
}
|
||||
|
||||
const SortableItem = ({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
isUpdating = false,
|
||||
}: SortableItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`
|
||||
flex items-center justify-between rounded-lg p-3 transition-colors duration-200 border border-ui-controls-border bg-ui-controls/60 text-base-fg
|
||||
${
|
||||
isUpdating
|
||||
? "opacity-60 cursor-not-allowed"
|
||||
: "cursor-move hover:bg-ui-controls/80"
|
||||
}
|
||||
${isDragging ? "opacity-70 shadow-lg" : ""}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-flex items-center justify-center h-4 w-4">
|
||||
{icon}
|
||||
</span>
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faGripVertical}
|
||||
className="text-base-fg/40 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProviderItemMap = {
|
||||
[Provider.ArtCraft]: { id: Provider.ArtCraft, name: "ArtCraft" },
|
||||
[Provider.Fal]: { id: Provider.Fal, name: "Fal" },
|
||||
[Provider.Sora]: { id: Provider.Sora, name: "Sora / ChatGPT" },
|
||||
};
|
||||
|
||||
const PROVIDER_TO_CREATOR: Partial<Record<Provider, ModelCreator>> = {
|
||||
[Provider.ArtCraft]: ModelCreator.ArtCraft,
|
||||
[Provider.Fal]: ModelCreator.Fal,
|
||||
[Provider.Sora]: ModelCreator.OpenAi,
|
||||
};
|
||||
|
||||
const getProviderIcon = (provider: Provider): ReactNode => {
|
||||
const creator = PROVIDER_TO_CREATOR[provider];
|
||||
if (creator) return getCreatorIcon(creator, "h-4 w-4 icon-auto-contrast");
|
||||
return (
|
||||
<img
|
||||
src={
|
||||
IsDesktopApp()
|
||||
? "/resources/images/services/generic.svg"
|
||||
: "/images/services/generic.svg"
|
||||
}
|
||||
alt="generic logo"
|
||||
className="h-4 w-4 icon-auto-contrast"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProviderPrioritySettingsPane = () => {
|
||||
const [items, setItems] = useState<ProviderItem[]>([]);
|
||||
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const providers = await GetProviderOrder();
|
||||
|
||||
let items: ProviderItem[] = [];
|
||||
|
||||
// Add providers from backend (in order)
|
||||
for (let provider of providers.payload.providers) {
|
||||
items.push(ProviderItemMap[provider]);
|
||||
}
|
||||
|
||||
// Add providers not in backend (in order)
|
||||
for (const [key, value] of Object.entries(ProviderItemMap)) {
|
||||
if (!providers.payload.providers.includes(key as Provider)) {
|
||||
items.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
setItems(items);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const updateProviderPriorityOnBackend = async (newOrder: ProviderItem[]) => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
console.log("Updating provider priority on backend:", newOrder);
|
||||
|
||||
let ordering = newOrder.map((item) => item.id);
|
||||
console.log("Provider order:", ordering);
|
||||
|
||||
await SetProviderOrder({ providers: ordering });
|
||||
|
||||
console.log("Provider priority updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to update provider priority:", error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
setItems((prevItems) => {
|
||||
const oldIndex = prevItems.findIndex((item) => item.id === active.id);
|
||||
const newIndex = prevItems.findIndex((item) => item.id === over?.id);
|
||||
|
||||
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
|
||||
|
||||
// Send update to backend
|
||||
updateProviderPriorityOnBackend(newOrder);
|
||||
|
||||
return newOrder;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-base-fg/70 mb-4">
|
||||
Drag and drop to reorder model provider priority. Higher items will be
|
||||
tried first. You can use this to control favorite services and
|
||||
spending.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
name={item.name}
|
||||
icon={getProviderIcon(item.id)}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{isUpdating && (
|
||||
<div className="text-xs rounded-full animate-pulse mt-4 flex items-center gap-2 text-base-fg/70">
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
Updating...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,7 +5,6 @@ import { MiscSettingsPane } from "./panes/MiscSettingsPane";
|
|||
import { AudioSettingsPane } from "./panes/AudioSettingsPane";
|
||||
import { AccountSettingsPane } from "./panes/AccountSettings/AccountSettingsPane";
|
||||
import { AboutSettingsPane } from "./panes/AboutSettingsPane";
|
||||
import { ProviderPrioritySettingsPane } from "./panes/ProviderPrioritySettingsPane";
|
||||
import { ExperimentalSettingsPane } from "./panes/ExperimentalSettingsPane";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { BillingSettingsPane } from "./panes/BillingSettingsPane";
|
||||
|
|
@ -20,7 +19,6 @@ export type SettingsSection =
|
|||
| "accounts"
|
||||
| "alerts"
|
||||
| "about"
|
||||
| "provider_priority"
|
||||
| "billing"
|
||||
| "experimental";
|
||||
|
||||
|
|
@ -85,8 +83,6 @@ export const SettingsContent = ({
|
|||
);
|
||||
case "about":
|
||||
return <AboutSettingsPane />;
|
||||
case "provider_priority":
|
||||
return <ProviderPrioritySettingsPane />;
|
||||
case "billing":
|
||||
return <BillingSettingsPane />;
|
||||
case "experimental":
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../tauri-utils"
|
||||
},
|
||||
{
|
||||
"path": "../../api"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,9 +43,6 @@
|
|||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../tauri-utils/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../api/tsconfig.lib.json"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ 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";
|
||||
export * from "./lib/enqueue/EnqueueImageToVideo.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
|
|
@ -26,14 +25,11 @@ export * from "./lib/fal/FalKlingImageToVideo.js";
|
|||
export * from "./lib/fal/GetFalApiKey.js";
|
||||
export * from "./lib/fal/SetFalApiKey.js";
|
||||
export * from "./lib/grok/GrokGetCredentialInfo.js";
|
||||
export * from "./lib/media_files/MediaFileDeleteCommand.js";
|
||||
export * from "./lib/midjourney/MidjourneyGetCredentialInfo.js";
|
||||
export * from "./lib/openai/GetOpenAIApiKey.js";
|
||||
export * from "./lib/openai/SetOpenAIApiKey.js";
|
||||
export * from "./lib/opener/OpenUrl.js";
|
||||
export * from "./lib/providers/GetProviderOrder.js";
|
||||
export * from "./lib/providers/Provider.js";
|
||||
export * from "./lib/providers/SetProviderOrder.js";
|
||||
export * from "./lib/settings/GetAppPreferences.js";
|
||||
export * from "./lib/settings/UpdateAppPreference.js";
|
||||
export * from "./lib/sora/CheckSoraSession.js";
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
|
||||
export interface EnqueueImageBgRemovalRequest {
|
||||
image_media_token?: string;
|
||||
base64_image?: string;
|
||||
|
||||
// TODO: Actual enum.
|
||||
frontend_caller?: string;
|
||||
|
||||
// We'll use the node id here.
|
||||
frontend_subscriber_id?: string;
|
||||
}
|
||||
|
||||
interface RawEnqueueImageBgRemovalRequest {
|
||||
image_media_token?: string;
|
||||
base64_image?: string;
|
||||
frontend_caller?: string;
|
||||
frontend_subscriber_id?: string;
|
||||
}
|
||||
|
||||
export enum EnqueueImageBgRemovalErrorType {
|
||||
/// Caller didn't specify a model
|
||||
ModelNotSpecified = "model_not_specified",
|
||||
/// Generic server error
|
||||
ServerError = "server_error",
|
||||
/// No Fal API key available
|
||||
NeedsFalApiKey = "needs_fal_api_key",
|
||||
/// Fal had an API error
|
||||
FalError = "fal_error",
|
||||
}
|
||||
|
||||
export interface EnqueueImageBgRemovalError extends CommandResult {
|
||||
error_type: EnqueueImageBgRemovalErrorType;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
export interface EnqueueImageBgRemovalPayload {
|
||||
}
|
||||
|
||||
export interface EnqueueImageBgRemovalSuccess extends CommandResult {
|
||||
payload: EnqueueImageBgRemovalPayload;
|
||||
}
|
||||
|
||||
export type EnqueueImageBgRemovalResult = EnqueueImageBgRemovalSuccess | EnqueueImageBgRemovalError;
|
||||
|
||||
export const EnqueueImageBgRemoval = async (request: EnqueueImageBgRemovalRequest) : Promise<EnqueueImageBgRemovalResult> => {
|
||||
let mutableRequest : RawEnqueueImageBgRemovalRequest = {};
|
||||
|
||||
if (!!request.image_media_token) {
|
||||
mutableRequest.image_media_token = request.image_media_token;
|
||||
}
|
||||
|
||||
if (!!request.base64_image) {
|
||||
mutableRequest.base64_image = request.base64_image;
|
||||
}
|
||||
|
||||
if (!!request.frontend_caller) {
|
||||
mutableRequest.frontend_caller = request.frontend_caller;
|
||||
}
|
||||
|
||||
if (!!request.frontend_subscriber_id) {
|
||||
mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id;
|
||||
}
|
||||
|
||||
const result = await invoke("enqueue_image_bg_removal_command", {
|
||||
request: mutableRequest,
|
||||
});
|
||||
|
||||
return (result as EnqueueImageBgRemovalResult);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
|
||||
export interface MediaFileDeleteError extends CommandResult {
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
export interface MediaFileDeletePayload {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface MediaFileDeleteSuccess extends CommandResult {
|
||||
payload: MediaFileDeletePayload;
|
||||
}
|
||||
|
||||
export type MediaFileDeleteResult = MediaFileDeleteSuccess | MediaFileDeleteError;
|
||||
|
||||
export const MediaFileDelete = async (media_file_token: string) : Promise<MediaFileDeleteResult> => {
|
||||
const result = await invoke("media_file_delete_command", {
|
||||
request: {
|
||||
media_file_token: media_file_token,
|
||||
}
|
||||
});
|
||||
|
||||
return (result as MediaFileDeleteResult);
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
import { Provider } from "./Provider";
|
||||
|
||||
export interface GetProviderOrderSuccess extends CommandResult {
|
||||
payload: GetProviderOrderPayload;
|
||||
}
|
||||
|
||||
export interface GetProviderOrderPayload {
|
||||
providers: Provider[];
|
||||
}
|
||||
|
||||
// Returns the Success and Error variants directly.
|
||||
// Throws on Network/Tauri errors.
|
||||
export const GetProviderOrder = async () : Promise<GetProviderOrderSuccess> => {
|
||||
try {
|
||||
return await invoke("get_provider_order_command") as GetProviderOrderSuccess;
|
||||
} catch (error) {
|
||||
// NB: Endpoint should be infalliable
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../common/CommandStatus";
|
||||
import { Provider } from "./Provider";
|
||||
|
||||
export interface SetProviderOrderRequest {
|
||||
providers: Provider[];
|
||||
}
|
||||
|
||||
export const SetProviderOrder = async (request: SetProviderOrderRequest) : Promise<CommandResult> => {
|
||||
const result = await invoke("set_provider_order_command", {
|
||||
request: {
|
||||
providers: request.providers,
|
||||
}
|
||||
});
|
||||
|
||||
return (result as CommandResult);
|
||||
}
|
||||
Loading…
Reference in New Issue