Skip to main content

max / makenotwork

9.2 KB · 248 lines History Blame Raw
1 //! upload-screen key-input handler (split out of tui/input.rs).
2
3 use super::{
4 App, AppEvent, DataPayload, EditField, KeyCode, KeyEvent, MnwApiClient, Path, Screen,
5 load_staged_files, mpsc, publish_file,
6 };
7
8 /// Save the current value of an upload edit field.
9 fn save_upload_field(app: &mut App, field: EditField, idx: usize) {
10 match field {
11 EditField::Title => {
12 if !app.edit_buffer.is_empty() {
13 app.file_metadata[idx].title = Some(app.edit_buffer.clone());
14 }
15 }
16 EditField::Project => {
17 if let Ok(n) = app.edit_buffer.parse::<usize>()
18 && n > 0
19 && n <= app.projects.len()
20 {
21 let pidx = n - 1;
22 app.file_metadata[idx].project_idx = Some(pidx);
23 app.file_metadata[idx].project_name = Some(app.projects[pidx].title.clone());
24 }
25 }
26 EditField::Price => {
27 let cents = crate::tui::parse_price(&app.edit_buffer);
28 app.file_metadata[idx].price_cents = cents;
29 }
30 }
31 }
32 /// Generate the status prompt for an upload edit field.
33 fn upload_field_prompt(field: EditField, app: &App) -> String {
34 match field {
35 EditField::Title => {
36 "Title: _ (Enter to save, Tab for next field, Esc to cancel)".to_string()
37 }
38 EditField::Project => {
39 let project_list = app
40 .projects
41 .iter()
42 .enumerate()
43 .map(|(i, p)| format!("{}:{}", i + 1, p.title))
44 .collect::<Vec<_>>()
45 .join(" ");
46 format!("Project #: _ | {project_list}")
47 }
48 EditField::Price => "Price ($): _ (0 or empty for free)".to_string(),
49 }
50 }
51 pub(crate) async fn handle_upload_input(
52 key: KeyEvent,
53 app: &mut App,
54 screen: &mut Screen,
55 api: &MnwApiClient,
56 tx: &mpsc::Sender<AppEvent>,
57 staging_dir: &Path,
58 ) {
59 // Handle editing mode
60 if let Some(field) = app.editing_field {
61 match key.code {
62 KeyCode::Esc => {
63 app.editing_field = None;
64 app.edit_buffer.clear();
65 app.upload_status = None;
66 }
67 KeyCode::Tab => {
68 // Save current field and advance to next
69 let idx = app.selected_index;
70 if idx < app.file_metadata.len() {
71 save_upload_field(app, field, idx);
72 }
73 let next = match field {
74 EditField::Title => EditField::Project,
75 EditField::Project => EditField::Price,
76 EditField::Price => EditField::Title,
77 };
78 app.editing_field = Some(next);
79 app.edit_buffer.clear();
80 app.upload_status = Some(upload_field_prompt(next, app));
81 return;
82 }
83 KeyCode::Enter => {
84 // Save current field and exit edit mode
85 let idx = app.selected_index;
86 if idx < app.file_metadata.len() {
87 save_upload_field(app, field, idx);
88 }
89 app.editing_field = None;
90 app.edit_buffer.clear();
91 app.upload_status = None;
92 }
93 KeyCode::Backspace => {
94 app.edit_buffer.pop();
95 if let Some(field) = app.editing_field {
96 app.upload_status =
97 Some(crate::tui::format_edit_prompt(field, &app.edit_buffer));
98 }
99 }
100 KeyCode::Char(c) => {
101 app.edit_buffer.push(c);
102 if let Some(field) = app.editing_field {
103 app.upload_status =
104 Some(crate::tui::format_edit_prompt(field, &app.edit_buffer));
105 }
106 }
107 _ => {}
108 }
109 return;
110 }
111
112 // Cancel pending delete confirmation on non-d keys
113 if !matches!(key.code, KeyCode::Char('d' | 'D'))
114 && app
115 .upload_status
116 .as_ref()
117 .is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again"))
118 {
119 app.upload_status = None;
120 }
121
122 // Normal mode
123 match key.code {
124 KeyCode::Char('j') | KeyCode::Down => app.move_down(screen),
125 KeyCode::Char('k') | KeyCode::Up => app.move_up(screen),
126 KeyCode::Esc => {
127 *screen = Screen::Home;
128 app.staged_files.clear();
129 app.file_metadata.clear();
130 app.upload_status = None;
131 app.selected_index = 0;
132 }
133 KeyCode::Char('e' | 'E') if !app.staged_files.is_empty() => {
134 let idx = app.selected_index;
135 let current_title = app.file_metadata.get(idx).and_then(|m| m.title.clone());
136 app.editing_field = Some(EditField::Title);
137 app.edit_buffer = current_title.unwrap_or_default();
138 app.upload_status =
139 Some("Title: _ (Enter to save, Tab for next field, Esc to cancel)".to_string());
140 }
141 KeyCode::Char('p' | 'P') if !app.staged_files.is_empty() && !app.publishing => {
142 let idx = app.selected_index;
143 let file = &app.staged_files[idx];
144 let meta = app.file_metadata.get(idx).cloned().unwrap_or_default();
145
146 // Validate
147 let Some(classification) = file.classification else {
148 app.upload_status = Some(
149 "Unsupported file type. Supported: mp3, wav, flac, ogg, m4a, aac, zip, dmg, exe, appimage, deb, clap, vst3".to_string()
150 );
151 return;
152 };
153 let Some(project_idx) = meta.project_idx else {
154 app.upload_status = Some("Set project first (press [e] to edit)".to_string());
155 return;
156 };
157 let Some(project) = app.projects.get(project_idx) else {
158 app.upload_status = Some("Invalid project".to_string());
159 return;
160 };
161
162 let title = meta
163 .title
164 .unwrap_or_else(|| crate::staging::derive_title(&file.filename));
165 let project_id = project.id.clone();
166 let user_id = app.user.user_id.clone();
167 let filename = file.filename.clone();
168 let file_path = staging_dir.join(&filename);
169 let price_cents = meta.price_cents;
170 let item_type = classification.item_type.to_string();
171 let file_type = classification.file_type.to_string();
172 let content_type = classification.content_type.to_string();
173 let api = api.clone();
174 let tx = tx.clone();
175
176 app.publishing = true;
177 app.upload_status = Some(format!("Publishing {filename}..."));
178
179 tokio::spawn(async move {
180 let result = publish_file(
181 &api,
182 &user_id,
183 &project_id,
184 &title,
185 &item_type,
186 &file_type,
187 &filename,
188 &content_type,
189 price_cents,
190 &file_path,
191 )
192 .await;
193
194 let (success, error) = match result {
195 Ok(()) => (true, None),
196 Err(e) => (false, Some(e.to_string())),
197 };
198
199 let _ = tx
200 .send(AppEvent::DataLoaded(DataPayload::PublishResult {
201 filename,
202 success,
203 error,
204 }))
205 .await;
206 });
207 }
208 KeyCode::Char('d' | 'D') if !app.staged_files.is_empty() => {
209 let idx = app.selected_index;
210 let filename = &app.staged_files[idx].filename;
211 if app
212 .upload_status
213 .as_ref()
214 .is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again"))
215 {
216 // Confirmed — execute delete
217 let file_path = staging_dir.join(filename);
218 let staging_dir = staging_dir.to_path_buf();
219 let api = api.clone();
220 let user_id = app.user.user_id.clone();
221 let tx = tx.clone();
222
223 app.upload_status = Some(format!("Deleting {filename}..."));
224 tokio::spawn(async move {
225 if let Err(e) = tokio::fs::remove_file(&file_path).await {
226 tracing::warn!(error = %e, "failed to delete staged file");
227 }
228 load_staged_files(&staging_dir, &api, &user_id, &tx).await;
229 });
230 } else {
231 // First press — ask for confirmation
232 app.upload_status = Some(format!("Delete '{filename}'? Press d again"));
233 }
234 }
235 KeyCode::Char('r' | 'R') => {
236 app.loading = true;
237 let staging_dir = staging_dir.to_path_buf();
238 let api = api.clone();
239 let user_id = app.user.user_id.clone();
240 let tx = tx.clone();
241 tokio::spawn(async move {
242 load_staged_files(&staging_dir, &api, &user_id, &tx).await;
243 });
244 }
245 _ => {}
246 }
247 }
248