//! Async data loading functions and the publish flow. use tokio::sync::mpsc; use crate::api::{CreatorStats, MnwApiClient}; use crate::staging; use super::{AppEvent, DataPayload}; pub(super) async fn load_home_data( api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender, ) { let projects = api.get_projects(user_id).await.unwrap_or_else(|e| { tracing::warn!(error = %e, "failed to load projects"); Vec::new() }); let stats = api .get_stats(user_id, "30d") .await .unwrap_or_else(|e| { tracing::warn!(error = %e, "failed to load stats"); CreatorStats { current_revenue_cents: 0, previous_revenue_cents: 0, current_sales: 0, previous_sales: 0, current_followers: 0, previous_followers: 0, total_projects: 0, total_items: 0, } }); let _ = tx .send(AppEvent::DataLoaded(DataPayload::Home { projects, stats })) .await; } pub(super) async fn load_project_items( api: &MnwApiClient, project_id: &str, user_id: &str, tx: &mpsc::Sender, ) { let items = api .get_project_items(project_id, user_id) .await .unwrap_or_else(|e| { tracing::warn!(error = %e, %project_id, "failed to load project items"); Vec::new() }); let _ = tx .send(AppEvent::DataLoaded(DataPayload::ProjectItems { items })) .await; } pub(super) async fn load_staged_files( staging_dir: &std::path::Path, api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender, ) { let files = staging::list_staged_files(staging_dir).await; let storage = match api.get_storage_info(user_id).await { Ok(s) => Some(s), Err(e) => { tracing::warn!(error = %e, "failed to load storage info"); None } }; let _ = tx .send(AppEvent::DataLoaded(DataPayload::StagedFiles { files, storage, })) .await; } pub(super) async fn load_item_detail( api: &MnwApiClient, user_id: &str, item_id: &str, tx: &mpsc::Sender, ) { let detail = api.get_item_detail(user_id, item_id).await; let versions = api.get_item_versions(user_id, item_id).await; let tags = api.list_item_tags(user_id, item_id).await.unwrap_or_default(); match detail { Ok(detail) => { let versions = versions.unwrap_or_default(); let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemDetail { detail, versions, })) .await; let _ = tx.send(AppEvent::DataLoaded(DataPayload::ItemTags { tags })).await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } } pub(super) async fn load_collections( api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender, ) { match api.list_collections(user_id).await { Ok(collections) => { let _ = tx.send(AppEvent::DataLoaded(DataPayload::CollectionsList { collections })).await; } Err(e) => { let _ = tx.send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string() })).await; } } } pub(super) async fn load_tiers( api: &MnwApiClient, user_id: &str, project_id: &str, tx: &mpsc::Sender, ) { match api.list_tiers(user_id, project_id).await { Ok(tiers) => { let _ = tx.send(AppEvent::DataLoaded(DataPayload::TiersList { tiers })).await; } Err(e) => { let _ = tx.send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string() })).await; } } } pub(super) async fn search_tags( api: &MnwApiClient, query: &str, tx: &mpsc::Sender, ) { match api.search_tags(query).await { Ok(results) => { let _ = tx.send(AppEvent::DataLoaded(DataPayload::TagSearchResults { results })).await; } Err(_) => { let _ = tx.send(AppEvent::DataLoaded(DataPayload::TagSearchResults { results: vec![] })).await; } } } pub(super) async fn load_blog_posts( api: &MnwApiClient, user_id: &str, project_id: &str, tx: &mpsc::Sender, ) { let posts = api .list_blog_posts(user_id, project_id) .await .unwrap_or_else(|e| { tracing::warn!(error = %e, %project_id, "failed to load blog posts"); Vec::new() }); let _ = tx .send(AppEvent::DataLoaded(DataPayload::BlogPosts { posts })) .await; } pub(super) async fn load_promo_codes( api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender, ) { let codes = api .list_promo_codes(user_id) .await .unwrap_or_else(|e| { tracing::warn!(error = %e, "failed to load promo codes"); Vec::new() }); let _ = tx .send(AppEvent::DataLoaded(DataPayload::PromoCodes { codes })) .await; } pub(super) async fn load_license_keys( api: &MnwApiClient, user_id: &str, item_id: &str, tx: &mpsc::Sender, ) { let keys = api .list_license_keys(user_id, item_id) .await .unwrap_or_else(|e| { tracing::warn!(error = %e, %item_id, "failed to load license keys"); Vec::new() }); let _ = tx .send(AppEvent::DataLoaded(DataPayload::LicenseKeys { keys })) .await; } pub(super) async fn load_analytics( api: &MnwApiClient, user_id: &str, range: &str, tx: &mpsc::Sender, ) { match api.get_analytics(user_id, range).await { Ok(data) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::Analytics { data })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } } pub(super) async fn load_transactions( api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender, ) { let txs = api.get_transactions(user_id).await.unwrap_or_else(|e| { tracing::warn!(error = %e, "failed to load transactions"); Vec::new() }); let _ = tx .send(AppEvent::DataLoaded(DataPayload::Transactions { txs })) .await; } pub(super) async fn load_settings( api: &MnwApiClient, user_id: &str, tx: &mpsc::Sender, ) { let keys = api.list_ssh_keys(user_id).await.unwrap_or_else(|e| { tracing::warn!(error = %e, "failed to load SSH keys"); Vec::new() }); let storage = match api.get_storage_info(user_id).await { Ok(s) => Some(s), Err(e) => { tracing::warn!(error = %e, "failed to load storage info for settings"); None } }; let _ = tx .send(AppEvent::DataLoaded(DataPayload::Settings { keys, storage })) .await; } /// Full publish flow: create item -> presign -> upload to S3 -> confirm -> delete staging file. #[allow(clippy::too_many_arguments)] pub(super) async fn publish_file( api: &MnwApiClient, user_id: &str, project_id: &str, title: &str, item_type: &str, file_type: &str, filename: &str, content_type: &str, price_cents: i32, file_path: &std::path::Path, ) -> anyhow::Result<()> { // Step 1: Create item let item = api .create_item(user_id, project_id, title, item_type, price_cents) .await?; // Step 2: Get presigned URL let presign = api .presign_upload(user_id, &item.item_id, file_type, filename, content_type) .await?; // Step 3: Upload to S3 api.upload_to_s3( &presign.upload_url, file_path, content_type, presign.cache_control.as_deref(), ) .await?; // Step 4: Confirm upload api.confirm_upload(user_id, &item.item_id, file_type, &presign.s3_key) .await?; // Step 5: Delete staging file if let Err(e) = tokio::fs::remove_file(file_path).await { tracing::warn!(error = %e, path = %file_path.display(), "failed to delete staging file after publish"); } Ok(()) } /// Bulk publish items. pub(super) async fn bulk_publish( api: &MnwApiClient, user_id: &str, item_ids: Vec, tx: &mpsc::Sender, ) { let total = item_ids.len(); let mut ok = 0; for id in &item_ids { if api.publish_item(user_id, id).await.is_ok() { ok += 1; } } let msg = format!("Published {}/{} items", ok, total); let _ = tx.send(AppEvent::DataLoaded(DataPayload::BulkActionComplete { message: msg })).await; } /// Bulk unpublish items. pub(super) async fn bulk_unpublish( api: &MnwApiClient, user_id: &str, item_ids: Vec, tx: &mpsc::Sender, ) { let total = item_ids.len(); let mut ok = 0; for id in &item_ids { if api.unpublish_item(user_id, id).await.is_ok() { ok += 1; } } let msg = format!("Unpublished {}/{} items", ok, total); let _ = tx.send(AppEvent::DataLoaded(DataPayload::BulkActionComplete { message: msg })).await; } /// Bulk delete items. pub(super) async fn bulk_delete( api: &MnwApiClient, user_id: &str, item_ids: Vec, tx: &mpsc::Sender, ) { let total = item_ids.len(); let mut ok = 0; for id in &item_ids { if api.delete_item(user_id, id).await.is_ok() { ok += 1; } } let msg = format!("Deleted {}/{} items", ok, total); let _ = tx.send(AppEvent::DataLoaded(DataPayload::BulkActionComplete { message: msg })).await; }