Skip to main content

max / makenotwork

18.6 KB · 443 lines History Blame Raw
1 //! item-screen key-input handler (split out of tui/input.rs).
2
3 use super::{
4 App, AppEvent, ConfirmAction, DataPayload, KeyCode, KeyEvent, MnwApiClient, Screen,
5 load_item_detail, load_license_keys, mpsc, search_tags,
6 };
7
8 pub(crate) async fn handle_item_input(
9 key: KeyEvent,
10 app: &mut App,
11 screen: &mut Screen,
12 api: &MnwApiClient,
13 tx: &mpsc::Sender<AppEvent>,
14 ) {
15 use crate::tui::item::ItemEditField;
16
17 // Handle tag search mode
18 if app.tag_searching {
19 match key.code {
20 KeyCode::Esc => {
21 app.tag_searching = false;
22 app.edit_buffer.clear();
23 app.tag_search_results.clear();
24 app.item_status = None;
25 }
26 KeyCode::Enter => {
27 // Add the first search result as a tag
28 if let (Some(tag), Some(detail)) =
29 (app.tag_search_results.first(), &app.item_detail)
30 {
31 let tag_id = tag.id.clone();
32 let item_id = detail.id.clone();
33 let user_id = app.user.user_id.clone();
34 let api = api.clone();
35 let tx = tx.clone();
36 let tag_name = tag.name.clone();
37 tokio::spawn(async move {
38 match api.add_item_tag(&user_id, &item_id, &tag_id).await {
39 Ok(()) => {
40 // Reload tags
41 let tags = api
42 .list_item_tags(&user_id, &item_id)
43 .await
44 .unwrap_or_default();
45 let _ = tx
46 .send(AppEvent::DataLoaded(DataPayload::ItemTags { tags }))
47 .await;
48 let _ = tx
49 .send(AppEvent::DataLoaded(DataPayload::ItemActionError {
50 error: format!("Added tag: {tag_name}"), // Reuse error channel for status
51 }))
52 .await;
53 }
54 Err(e) => {
55 let _ = tx
56 .send(AppEvent::DataLoaded(DataPayload::ItemActionError {
57 error: e.to_string(),
58 }))
59 .await;
60 }
61 }
62 });
63 app.tag_searching = false;
64 app.edit_buffer.clear();
65 app.tag_search_results.clear();
66 app.item_status = None;
67 }
68 }
69 KeyCode::Backspace => {
70 app.edit_buffer.pop();
71 if app.edit_buffer.len() >= 2 {
72 let api = api.clone();
73 let query = app.edit_buffer.clone();
74 let tx = tx.clone();
75 tokio::spawn(async move {
76 search_tags(&api, &query, &tx).await;
77 });
78 } else {
79 app.tag_search_results.clear();
80 }
81 }
82 KeyCode::Char(c) => {
83 app.edit_buffer.push(c);
84 if app.edit_buffer.len() >= 2 {
85 let api = api.clone();
86 let query = app.edit_buffer.clone();
87 let tx = tx.clone();
88 tokio::spawn(async move {
89 search_tags(&api, &query, &tx).await;
90 });
91 }
92 let results_preview: String = app
93 .tag_search_results
94 .iter()
95 .take(3)
96 .map(|t| t.name.as_str())
97 .collect::<Vec<_>>()
98 .join(", ");
99 app.item_status = Some(format!(
100 "Tag: {}_ {}",
101 app.edit_buffer,
102 if results_preview.is_empty() {
103 String::new()
104 } else {
105 format!("[{results_preview}]")
106 },
107 ));
108 }
109 _ => {}
110 }
111 return;
112 }
113
114 // Cancel pending confirmation on any key other than the confirmation key
115 if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('d' | 'D')) {
116 app.confirm_action = None;
117 app.item_status = None;
118 }
119
120 // Handle editing mode
121 if let Some(field) = app.item_editing {
122 match key.code {
123 KeyCode::Esc => {
124 app.item_editing = None;
125 app.edit_buffer.clear();
126 app.item_status = None;
127 }
128 KeyCode::Enter => {
129 if let Some(ref detail) = app.item_detail {
130 let item_id = detail.id.clone();
131 let user_id = app.user.user_id.clone();
132 let api = api.clone();
133 let tx = tx.clone();
134 let buffer = app.edit_buffer.clone();
135
136 match field {
137 ItemEditField::Title => {
138 if buffer.is_empty() {
139 app.item_editing = None;
140 app.edit_buffer.clear();
141 } else {
142 let title = buffer;
143 tokio::spawn(async move {
144 match api
145 .update_item(
146 &user_id,
147 &item_id,
148 Some(&title),
149 None,
150 None,
151 None,
152 )
153 .await
154 {
155 Ok(d) => {
156 let _ = tx
157 .send(AppEvent::DataLoaded(
158 DataPayload::ItemUpdated { detail: d },
159 ))
160 .await;
161 }
162 Err(e) => {
163 let _ = tx
164 .send(AppEvent::DataLoaded(
165 DataPayload::ItemActionError {
166 error: e.to_string(),
167 },
168 ))
169 .await;
170 }
171 }
172 });
173 }
174 }
175 ItemEditField::Description => {
176 let desc = if buffer.is_empty() {
177 None
178 } else {
179 Some(buffer.as_str().to_string())
180 };
181 tokio::spawn(async move {
182 match api
183 .update_item(
184 &user_id,
185 &item_id,
186 None,
187 desc.as_deref(),
188 None,
189 None,
190 )
191 .await
192 {
193 Ok(d) => {
194 let _ = tx
195 .send(AppEvent::DataLoaded(DataPayload::ItemUpdated {
196 detail: d,
197 }))
198 .await;
199 }
200 Err(e) => {
201 let _ = tx
202 .send(AppEvent::DataLoaded(
203 DataPayload::ItemActionError {
204 error: e.to_string(),
205 },
206 ))
207 .await;
208 }
209 }
210 });
211 }
212 ItemEditField::Price => {
213 let cents = crate::tui::parse_price(&buffer);
214 tokio::spawn(async move {
215 match api
216 .update_item(&user_id, &item_id, None, None, Some(cents), None)
217 .await
218 {
219 Ok(d) => {
220 let _ = tx
221 .send(AppEvent::DataLoaded(DataPayload::ItemUpdated {
222 detail: d,
223 }))
224 .await;
225 }
226 Err(e) => {
227 let _ = tx
228 .send(AppEvent::DataLoaded(
229 DataPayload::ItemActionError {
230 error: e.to_string(),
231 },
232 ))
233 .await;
234 }
235 }
236 });
237 }
238 }
239 }
240 if app.item_editing.is_some() {
241 // Only clear if not already cleared by the empty-buffer path
242 app.item_editing = None;
243 app.edit_buffer.clear();
244 }
245 return;
246 }
247 KeyCode::Backspace => {
248 app.edit_buffer.pop();
249 }
250 KeyCode::Char(c) => {
251 app.edit_buffer.push(c);
252 }
253 _ => {}
254 }
255 return;
256 }
257
258 // Normal mode
259 match key.code {
260 KeyCode::Char('j') | KeyCode::Down => app.move_down(screen),
261 KeyCode::Char('k') | KeyCode::Up => app.move_up(screen),
262 KeyCode::Esc | KeyCode::Char('q' | 'Q') => {
263 // Go back to project view
264 if let Screen::Item(project_idx, _) = screen {
265 let pidx = *project_idx;
266 *screen = Screen::Project(pidx);
267 app.item_detail = None;
268 app.item_versions.clear();
269 app.item_status = None;
270 app.item_editing = None;
271 app.selected_index = 0;
272 }
273 }
274 KeyCode::Char('e' | 'E') => {
275 // Start editing — cycle through Title → Description → Price
276 app.item_editing = Some(ItemEditField::Title);
277 app.edit_buffer.clear();
278 app.item_status = Some("Editing title (Enter to save, Esc to cancel)".to_string());
279 }
280 KeyCode::Tab => {
281 // Cycle edit fields when in edit mode (already started with [e])
282 if let Some(field) = app.item_editing {
283 let next = match field {
284 ItemEditField::Title => ItemEditField::Description,
285 ItemEditField::Description => ItemEditField::Price,
286 ItemEditField::Price => ItemEditField::Title,
287 };
288 app.item_editing = Some(next);
289 app.edit_buffer.clear();
290 let label = match next {
291 ItemEditField::Title => "title",
292 ItemEditField::Description => "description",
293 ItemEditField::Price => "price",
294 };
295 app.item_status = Some(format!("Editing {label} (Enter to save, Esc to cancel)"));
296 }
297 }
298 KeyCode::Char('p' | 'P') => {
299 if let Some(ref detail) = app.item_detail
300 && !detail.is_public
301 {
302 let item_id = detail.id.clone();
303 let user_id = app.user.user_id.clone();
304 let api = api.clone();
305 let tx = tx.clone();
306 app.item_status = Some("Publishing...".to_string());
307 tokio::spawn(async move {
308 match api.publish_item(&user_id, &item_id).await {
309 Ok(d) => {
310 let _ = tx
311 .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d }))
312 .await;
313 }
314 Err(e) => {
315 let _ = tx
316 .send(AppEvent::DataLoaded(DataPayload::ItemActionError {
317 error: e.to_string(),
318 }))
319 .await;
320 }
321 }
322 });
323 }
324 }
325 KeyCode::Char('u' | 'U') => {
326 if let Some(ref detail) = app.item_detail
327 && detail.is_public
328 {
329 let item_id = detail.id.clone();
330 let user_id = app.user.user_id.clone();
331 let api = api.clone();
332 let tx = tx.clone();
333 app.item_status = Some("Unpublishing...".to_string());
334 tokio::spawn(async move {
335 match api.unpublish_item(&user_id, &item_id).await {
336 Ok(d) => {
337 let _ = tx
338 .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d }))
339 .await;
340 }
341 Err(e) => {
342 let _ = tx
343 .send(AppEvent::DataLoaded(DataPayload::ItemActionError {
344 error: e.to_string(),
345 }))
346 .await;
347 }
348 }
349 });
350 }
351 }
352 KeyCode::Char('d' | 'D') => {
353 if let Some(ref detail) = app.item_detail {
354 if matches!(app.confirm_action, Some(ConfirmAction::DeleteItem)) {
355 // Confirmed — execute delete
356 app.confirm_action = None;
357 let item_id = detail.id.clone();
358 let item_title = detail.title.clone();
359 let user_id = app.user.user_id.clone();
360 tracing::info!(
361 user_id = %user_id,
362 item_id = %item_id,
363 item_title = %item_title,
364 "delete item confirmed"
365 );
366 let api = api.clone();
367 let tx = tx.clone();
368 app.item_status = Some("Deleting...".to_string());
369 tokio::spawn(async move {
370 match api.delete_item(&user_id, &item_id).await {
371 Ok(()) => {
372 let _ = tx
373 .send(AppEvent::DataLoaded(DataPayload::ItemDeleted))
374 .await;
375 }
376 Err(e) => {
377 let _ = tx
378 .send(AppEvent::DataLoaded(DataPayload::ItemActionError {
379 error: e.to_string(),
380 }))
381 .await;
382 }
383 }
384 });
385 } else {
386 // First press — ask for confirmation
387 app.confirm_action = Some(ConfirmAction::DeleteItem);
388 app.item_status = Some(format!(
389 "Delete '{}'? Press d again to confirm",
390 detail.title
391 ));
392 }
393 }
394 }
395 KeyCode::Char('t' | 'T') if app.item_detail.is_some() => {
396 app.tag_searching = true;
397 app.edit_buffer.clear();
398 app.tag_search_results.clear();
399 app.item_status =
400 Some("Tag: type to search, Enter to add first result, Esc to cancel".to_string());
401 }
402 KeyCode::Char('l' | 'L') => {
403 // Open license keys screen
404 if let Screen::Item(project_idx, item_id) = &*screen {
405 let pidx = *project_idx;
406 let iid = item_id.clone();
407 let item_title = app
408 .item_detail
409 .as_ref()
410 .map(|d| d.title.clone())
411 .unwrap_or_default();
412
413 *screen = Screen::Keys(pidx, iid.clone());
414 app.license_keys.clear();
415 app.keys_item_title = Some(item_title);
416 app.keys_status = None;
417 app.selected_index = 0;
418 app.loading = true;
419
420 let api = api.clone();
421 let user_id = app.user.user_id.clone();
422 let tx = tx.clone();
423 tokio::spawn(async move {
424 load_license_keys(&api, &user_id, &iid, &tx).await;
425 });
426 }
427 }
428 KeyCode::Char('r' | 'R') => {
429 if let Screen::Item(_, item_id) = &*screen {
430 app.loading = true;
431 let item_id = item_id.clone();
432 let api = api.clone();
433 let user_id = app.user.user_id.clone();
434 let tx = tx.clone();
435 tokio::spawn(async move {
436 load_item_detail(&api, &user_id, &item_id, &tx).await;
437 });
438 }
439 }
440 _ => {}
441 }
442 }
443