//! upload-screen key-input handler (split out of tui/input.rs). use super::{ App, AppEvent, DataPayload, EditField, KeyCode, KeyEvent, MnwApiClient, Path, Screen, load_staged_files, mpsc, publish_file, }; /// Save the current value of an upload edit field. fn save_upload_field(app: &mut App, field: EditField, idx: usize) { match field { EditField::Title => { if !app.edit_buffer.is_empty() { app.file_metadata[idx].title = Some(app.edit_buffer.clone()); } } EditField::Project => { if let Ok(n) = app.edit_buffer.parse::() && n > 0 && n <= app.projects.len() { let pidx = n - 1; app.file_metadata[idx].project_idx = Some(pidx); app.file_metadata[idx].project_name = Some(app.projects[pidx].title.clone()); } } EditField::Price => { let cents = crate::tui::parse_price(&app.edit_buffer); app.file_metadata[idx].price_cents = cents; } } } /// Generate the status prompt for an upload edit field. fn upload_field_prompt(field: EditField, app: &App) -> String { match field { EditField::Title => { "Title: _ (Enter to save, Tab for next field, Esc to cancel)".to_string() } EditField::Project => { let project_list = app .projects .iter() .enumerate() .map(|(i, p)| format!("{}:{}", i + 1, p.title)) .collect::>() .join(" "); format!("Project #: _ | {project_list}") } EditField::Price => "Price ($): _ (0 or empty for free)".to_string(), } } pub(crate) async fn handle_upload_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, staging_dir: &Path, ) { // Handle editing mode if let Some(field) = app.editing_field { match key.code { KeyCode::Esc => { app.editing_field = None; app.edit_buffer.clear(); app.upload_status = None; } KeyCode::Tab => { // Save current field and advance to next let idx = app.selected_index; if idx < app.file_metadata.len() { save_upload_field(app, field, idx); } let next = match field { EditField::Title => EditField::Project, EditField::Project => EditField::Price, EditField::Price => EditField::Title, }; app.editing_field = Some(next); app.edit_buffer.clear(); app.upload_status = Some(upload_field_prompt(next, app)); return; } KeyCode::Enter => { // Save current field and exit edit mode let idx = app.selected_index; if idx < app.file_metadata.len() { save_upload_field(app, field, idx); } app.editing_field = None; app.edit_buffer.clear(); app.upload_status = None; } KeyCode::Backspace => { app.edit_buffer.pop(); if let Some(field) = app.editing_field { app.upload_status = Some(crate::tui::format_edit_prompt(field, &app.edit_buffer)); } } KeyCode::Char(c) => { app.edit_buffer.push(c); if let Some(field) = app.editing_field { app.upload_status = Some(crate::tui::format_edit_prompt(field, &app.edit_buffer)); } } _ => {} } return; } // Cancel pending delete confirmation on non-d keys if !matches!(key.code, KeyCode::Char('d' | 'D')) && app .upload_status .as_ref() .is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again")) { app.upload_status = None; } // Normal mode match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { *screen = Screen::Home; app.staged_files.clear(); app.file_metadata.clear(); app.upload_status = None; app.selected_index = 0; } KeyCode::Char('e' | 'E') if !app.staged_files.is_empty() => { let idx = app.selected_index; let current_title = app.file_metadata.get(idx).and_then(|m| m.title.clone()); app.editing_field = Some(EditField::Title); app.edit_buffer = current_title.unwrap_or_default(); app.upload_status = Some("Title: _ (Enter to save, Tab for next field, Esc to cancel)".to_string()); } KeyCode::Char('p' | 'P') if !app.staged_files.is_empty() && !app.publishing => { let idx = app.selected_index; let file = &app.staged_files[idx]; let meta = app.file_metadata.get(idx).cloned().unwrap_or_default(); // Validate let Some(classification) = file.classification else { app.upload_status = Some( "Unsupported file type. Supported: mp3, wav, flac, ogg, m4a, aac, zip, dmg, exe, appimage, deb, clap, vst3".to_string() ); return; }; let Some(project_idx) = meta.project_idx else { app.upload_status = Some("Set project first (press [e] to edit)".to_string()); return; }; let Some(project) = app.projects.get(project_idx) else { app.upload_status = Some("Invalid project".to_string()); return; }; let title = meta .title .unwrap_or_else(|| crate::staging::derive_title(&file.filename)); let project_id = project.id.clone(); let user_id = app.user.user_id.clone(); let filename = file.filename.clone(); let file_path = staging_dir.join(&filename); let price_cents = meta.price_cents; let item_type = classification.item_type.to_string(); let file_type = classification.file_type.to_string(); let content_type = classification.content_type.to_string(); let api = api.clone(); let tx = tx.clone(); app.publishing = true; app.upload_status = Some(format!("Publishing {filename}...")); tokio::spawn(async move { let result = publish_file( &api, &user_id, &project_id, &title, &item_type, &file_type, &filename, &content_type, price_cents, &file_path, ) .await; let (success, error) = match result { Ok(()) => (true, None), Err(e) => (false, Some(e.to_string())), }; let _ = tx .send(AppEvent::DataLoaded(DataPayload::PublishResult { filename, success, error, })) .await; }); } KeyCode::Char('d' | 'D') if !app.staged_files.is_empty() => { let idx = app.selected_index; let filename = &app.staged_files[idx].filename; if app .upload_status .as_ref() .is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again")) { // Confirmed — execute delete let file_path = staging_dir.join(filename); let staging_dir = staging_dir.to_path_buf(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.upload_status = Some(format!("Deleting {filename}...")); tokio::spawn(async move { if let Err(e) = tokio::fs::remove_file(&file_path).await { tracing::warn!(error = %e, "failed to delete staged file"); } load_staged_files(&staging_dir, &api, &user_id, &tx).await; }); } else { // First press — ask for confirmation app.upload_status = Some(format!("Delete '{filename}'? Press d again")); } } KeyCode::Char('r' | 'R') => { app.loading = true; let staging_dir = staging_dir.to_path_buf(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_staged_files(&staging_dir, &api, &user_id, &tx).await; }); } _ => {} } }