1use crate::*;
2use alux_ext::ext;
3use rsynko_media::portable_user_file_name;
4use std::path::{Path, PathBuf};
5
6#[ext(name = CollectionNavigationExt)]
8pub impl<This> This
9where
10 This: NavigationStateAlg + QueueCatalogAlg + QueueRemoveAlg,
11 This::Id: Copy + Eq,
12{
13 fn select_relative(&mut self, forward: bool) {
15 let ids = self.queue_ids().collect::<Vec<_>>();
16 if ids.is_empty() {
17 self.set_selected_queue_id(None);
18 return;
19 }
20 let current = self
21 .selected_queue_id()
22 .and_then(|selected| ids.iter().position(|id| *id == selected))
23 .unwrap_or(0);
24 let next = if forward {
25 (current + 1) % ids.len()
26 } else if current == 0 {
27 ids.len() - 1
28 } else {
29 current - 1
30 };
31 self.set_selected_queue_id(Some(ids[next]));
32 if matches!(self.page(), ManagerPage::Details(_)) {
33 self.set_page(ManagerPage::Details(ids[next]));
34 }
35 }
36
37 fn remove_selected(&mut self) {
39 let ids = self.queue_ids().collect::<Vec<_>>();
40 let Some(selected) = self.selected_queue_id() else {
41 return;
42 };
43 let index = ids.iter().position(|id| *id == selected).unwrap_or(0);
44 self.remove_queue_entry(selected);
45 let remaining = self.queue_ids().collect::<Vec<_>>();
46 let next = remaining
47 .get(index.min(remaining.len().saturating_sub(1)))
48 .copied();
49 self.set_selected_queue_id(next);
50 if matches!(self.page(), ManagerPage::Details(id) | ManagerPage::Formats(id) | ManagerPage::Output(id) | ManagerPage::Log(id) | ManagerPage::Command(id) if id == selected)
51 {
52 self.set_page(ManagerPage::Collection);
53 }
54 }
55}
56
57#[ext(name = DetailNavigationExt)]
59pub impl<This> This
60where
61 This: NavigationStateAlg + QueueCatalogAlg + DetailSelectionAlg,
62 This::Entry: QueueEntryAlg,
63 This::Id: Copy + Eq,
64{
65 fn select_detail(&mut self, forward: bool) {
67 let ManagerPage::Details(id) = self.page() else {
68 return;
69 };
70 let Some(entry) = self.queue_entry(id) else {
71 return;
72 };
73 let controls = entry.detail_controls();
74 if controls.is_empty() {
75 return;
76 }
77 let next = match self
78 .selected_detail_control()
79 .and_then(|selected| controls.iter().position(|control| *control == selected))
80 {
81 None if forward => Some(controls[0]),
82 None => controls.last().copied(),
83 Some(current) if forward && current + 1 == controls.len() => None,
84 Some(current) if forward => Some(controls[current + 1]),
85 Some(0) => None,
86 Some(current) => Some(controls[current - 1]),
87 };
88 self.set_selected_detail_control(next);
89 }
90
91 fn normalize_detail_control(&mut self, id: This::Id) {
93 if self.page() != ManagerPage::Details(id) {
94 return;
95 }
96 let Some(entry) = self.queue_entry(id) else {
97 return;
98 };
99 let controls = entry.detail_controls();
100 if self
101 .selected_detail_control()
102 .is_some_and(|selected| !controls.contains(&selected))
103 {
104 self.set_selected_detail_control(controls.first().copied());
105 }
106 }
107}
108
109#[ext(name = OutputEditingExt)]
111pub impl<This> This
112where
113 This: NavigationStateAlg
114 + QueueCatalogAlg
115 + OutputDraftAlg
116 + InputDraftAlg
117 + QueueOutputAlg
118 + QueueSourceAlg
119 + ManagerStatusAlg,
120 This::Entry: QueueEntryAlg,
121 This::Id: Copy,
122{
123 fn open_input(&mut self) {
128 let Some(id) = self.selected_queue_id() else {
129 return;
130 };
131 let Some(entry) = self.queue_entry(id) else {
132 return;
133 };
134 if entry.is_editable() && entry.output_naming() == OutputNaming::Stated {
135 self.set_input_draft(entry.source().to_owned());
136 self.set_page(ManagerPage::Input(id));
137 self.set_manager_message(None);
138 }
139 }
140
141 fn submit_input(&mut self) {
143 let ManagerPage::Input(id) = self.page() else {
144 return;
145 };
146 if self.input_draft().trim().is_empty() {
147 self.set_manager_message(Some("enter an input".to_owned()));
148 return;
149 }
150 self.set_queue_source(id, self.input_draft().trim().to_owned());
151 self.set_page(ManagerPage::Details(id));
152 self.set_manager_message(None);
153 }
154
155 fn open_output(&mut self) {
157 let Some(id) = self.selected_queue_id() else {
158 return;
159 };
160 let Some(entry) = self.queue_entry(id) else {
161 return;
162 };
163 if entry.is_editable() {
164 self.set_output_draft(
165 entry
166 .output()
167 .map_or_else(String::new, |path| path.display().to_string()),
168 );
169 self.set_page(ManagerPage::Output(id));
170 self.set_manager_message(None);
171 }
172 }
173
174 fn submit_output(&mut self) {
176 let ManagerPage::Output(id) = self.page() else {
177 return;
178 };
179 let Some(entry) = self.queue_entry(id) else {
180 return;
181 };
182 let naming = entry.output_naming();
183 let extension = entry
184 .output()
185 .and_then(Path::extension)
186 .and_then(std::ffi::OsStr::to_str);
187 if self.output_draft().trim().is_empty() {
188 self.set_manager_message(Some("enter a name".to_owned()));
189 return;
190 }
191 let output = match naming {
193 OutputNaming::Portable => portable_user_file_name(self.output_draft(), extension),
194 OutputNaming::Stated => PathBuf::from(self.output_draft().trim()),
195 };
196 self.set_queue_output(id, output);
197 self.set_page(ManagerPage::Details(id));
198 self.set_manager_message(None);
199 }
200}
201
202#[ext(name = ChoiceEditingExt)]
204pub impl<This> This
205where
206 This: NavigationStateAlg
207 + QueueCatalogAlg
208 + FormatCatalogStateAlg
209 + QueueDryRunAlg
210 + ManagerStatusAlg,
211 This::Entry: QueueEntryAlg,
212 This::Id: Copy,
213{
214 fn open_report(&mut self) {
216 if let Some(id) = self.selected_queue_id()
217 && self.queue_entry(id).is_some()
218 {
219 self.set_page(ManagerPage::Report(id));
220 self.set_manager_message(None);
221 }
222 }
223
224 fn open_log(&mut self) {
226 if let Some(id) = self.selected_queue_id()
227 && self.queue_entry(id).is_some()
228 {
229 self.set_page(ManagerPage::Log(id));
230 self.set_manager_message(None);
231 }
232 }
233
234 fn open_command(&mut self) {
236 if let Some(id) = self.selected_queue_id()
237 && self.queue_entry(id).is_some()
238 {
239 self.set_page(ManagerPage::Command(id));
240 self.set_manager_message(None);
241 }
242 }
243
244 fn toggle_dry_run(&mut self) {
248 if let Some(id) = self.selected_queue_id()
249 && let Some(dry_run) = self.queue_entry(id).and_then(QueueEntryAlg::dry_run)
250 {
251 self.set_queue_dry_run(id, !dry_run);
252 self.set_manager_message(None);
253 }
254 }
255
256 fn open_formats(&mut self) {
258 let Some(id) = self.selected_queue_id() else {
259 return;
260 };
261 if self.queue_entry(id).is_some_and(QueueEntryAlg::is_editable) {
262 self.request_format_catalog(id);
263 self.set_page(ManagerPage::Formats(id));
264 self.set_manager_message(None);
265 } else {
266 self.set_manager_message(Some(
267 "started downloads are immutable; duplicate to change options".to_owned(),
268 ));
269 }
270 }
271}
272
273#[ext(name = DraftSubmissionExt)]
275pub impl<This> This
276where
277 This: NavigationStateAlg
278 + QueueCatalogAlg
279 + QueueAppendAlg
280 + DraftStateAlg
281 + ManagerStatusAlg
282 + SourceRequestAlg
283 + SubmissionAlg
284 + MediaOptionsAlg
285 + OutputChoiceAlg,
286 This::Id: Copy,
287{
288 fn add_sources(&mut self, requests: Vec<This::Source>) {
290 let added = self.append_sources(requests);
291 if let Some(added) = added.first().copied() {
292 self.set_selected_queue_id(Some(added));
293 }
294 self.set_manager_message(None);
295 }
296
297 fn submit_draft(&mut self) {
299 let requests = self
300 .draft()
301 .lines()
302 .map(str::trim)
303 .filter(|source| !source.is_empty())
304 .map(|line| self.submitted(line))
305 .collect::<Vec<_>>();
306 if requests.is_empty() {
307 self.set_manager_message(Some("enter at least one source".to_owned()));
308 } else {
309 self.add_sources(requests);
310 self.set_draft(String::new());
311 self.set_page(ManagerPage::Collection);
312 }
313 }
314}
315
316#[ext(name = QueueLifecycleExt)]
318pub impl<This> This
319where
320 This: NavigationStateAlg
321 + QueueCatalogAlg
322 + QueuePauseAlg
323 + QueueDuplicateAlg
324 + TransferStateAlg
325 + DetailSelectionAlg
326 + ManagerStatusAlg,
327 This::Entry: QueueEntryAlg,
328 This::Id: Copy + Eq,
329{
330 fn apply_selected_space(&mut self) {
334 if !matches!(
335 self.page(),
336 ManagerPage::Collection | ManagerPage::Details(_)
337 ) {
338 return;
339 }
340 let Some(id) = self.selected_queue_id() else {
341 return;
342 };
343 match self.queue_entry(id).and_then(QueueEntryAlg::space_action) {
344 Some(SpaceAction::Start | SpaceAction::Rehearse) => self.set_waiting(id),
345 Some(SpaceAction::Pause | SpaceAction::Resume) => self.toggle_queue_pause(id),
346 None => {}
347 }
348 self.normalize_detail_control(id);
349 }
350
351 fn duplicate_selected(&mut self) {
353 if let Some(id) = self.selected_queue_id()
354 && let Some(duplicate) = self.duplicate_queue_entry(id)
355 {
356 self.set_selected_queue_id(Some(duplicate));
357 self.set_page(ManagerPage::Details(duplicate));
358 self.set_selected_detail_control(None);
359 self.set_manager_message(Some("source duplicated as an editable download".to_owned()));
360 }
361 }
362
363 fn restart_selected(&mut self) {
365 if let Some(id) = self.selected_queue_id() {
366 self.restart_waiting(id);
367 self.normalize_detail_control(id);
368 self.set_manager_message(Some("failed download queued again".to_owned()));
369 }
370 }
371}
372
373#[ext(name = ManagerIntentExt)]
375pub impl<This> This
376where
377 This: NavigationStateAlg
378 + QueueCatalogAlg
379 + QueueAppendAlg
380 + QueueRemoveAlg
381 + QueuePauseAlg
382 + QueueDuplicateAlg
383 + QueueFormatEditAlg
384 + FormatCatalogStateAlg
385 + QueueDryRunAlg
386 + RehearsalStateAlg
387 + TransferStateAlg
388 + DetailSelectionAlg
389 + DraftStateAlg
390 + InputDraftAlg
391 + OutputDraftAlg
392 + QueueOutputAlg
393 + QueueSourceAlg
394 + SourceMetadataAlg
395 + TextEditorStateAlg
396 + ManagerStatusAlg
397 + SafeExitAlg
398 + SourceRequestAlg
399 + SubmissionAlg
400 + MediaOptionsAlg
401 + OutputChoiceAlg,
402 This::Entry: QueueEntryAlg + RequestOptionsAlg,
403 This::Id: Copy + Eq,
404{
405 fn apply_manager_event(
408 &mut self,
409 event: ManagerIntentOp<This::Id, This::Source, This::Format, This::Change>,
410 ) {
411 event.interpret(&mut IntentDispatch(self));
412 }
413}
414
415struct IntentDispatch<'a, This>(&'a mut This);
419
420impl<This, Id, Source, Format, Change> ManagerIntentInterpreter for IntentDispatch<'_, This>
421where
422 This: ManagerSorts<Id = Id, Source = Source, Format = Format, Change = Change>
423 + NavigationStateAlg
424 + QueueCatalogAlg
425 + QueueAppendAlg
426 + QueueRemoveAlg
427 + QueuePauseAlg
428 + QueueDuplicateAlg
429 + QueueFormatEditAlg
430 + FormatCatalogStateAlg
431 + QueueDryRunAlg
432 + RehearsalStateAlg
433 + TransferStateAlg
434 + DetailSelectionAlg
435 + DraftStateAlg
436 + InputDraftAlg
437 + OutputDraftAlg
438 + QueueOutputAlg
439 + QueueSourceAlg
440 + SourceMetadataAlg
441 + TextEditorStateAlg
442 + ManagerStatusAlg
443 + SafeExitAlg
444 + SourceRequestAlg
445 + SubmissionAlg
446 + MediaOptionsAlg
447 + OutputChoiceAlg,
448 This::Entry: QueueEntryAlg,
449 Id: Copy + Eq,
450{
451 type Id = Id;
452 type Source = Source;
453 type Format = Format;
454 type Change = Change;
455
456 fn add_sources(&mut self, requests: Vec<Self::Source>) {
457 self.0.add_sources(requests);
458 }
459
460 fn set_draft(&mut self, draft: String) {
461 self.0.set_draft(draft);
462 }
463
464 fn insert_text(&mut self, text: String) {
465 self.0.insert_text(&text);
466 }
467
468 fn set_output_draft(&mut self, draft: String) {
469 self.0.set_output_draft(draft);
470 }
471
472 fn set_input_draft(&mut self, draft: String) {
473 self.0.set_input_draft(draft);
474 }
475
476 fn open_input(&mut self) {
477 self.0.open_input();
478 }
479
480 fn submit_input(&mut self) {
481 self.0.submit_input();
482 }
483
484 fn source_metadata(&mut self, id: Self::Id, media_id: String, title: Option<String>) {
485 self.0.apply_source_metadata(id, media_id, title);
486 }
487
488 fn format_catalog(&mut self, id: Self::Id, event: FormatDiscoveryOp<Self::Format>) {
489 self.0.apply_format_catalog_event(id, event);
490 }
491
492 fn rehearsal(&mut self, id: Self::Id, event: RehearsalObservationOp<Self::Change>) {
493 self.0.apply_rehearsal_event(id, event);
494 self.0.normalize_detail_control(id);
495 }
496
497 fn open_report(&mut self) {
498 self.0.open_report();
499 }
500
501 fn toggle_dry_run(&mut self) {
502 self.0.toggle_dry_run();
503 }
504
505 fn transfer(&mut self, id: Self::Id, event: TransferObservationOp) {
506 if matches!(event, TransferObservationOp::Started {}) {
507 self.0.set_manager_message(None);
508 }
509 self.0.apply_transfer_event(id, event);
510 self.0.normalize_detail_control(id);
511 }
512
513 fn open_add_sources(&mut self) {
514 self.0.set_draft(String::new());
515 self.0.set_page(ManagerPage::AddSources);
516 }
517
518 fn open_selected(&mut self) {
519 if let Some(id) = self.0.selected_queue_id() {
520 self.0.set_page(ManagerPage::Details(id));
521 self.0.set_selected_detail_control(None);
522 }
523 }
524
525 fn activate_detail(&mut self) {
526 match self.0.selected_detail_control() {
527 Some(DetailControl::Input) => self.0.open_input(),
528 Some(DetailControl::Output) => self.0.open_output(),
529 Some(DetailControl::Format) => self.0.open_formats(),
530 Some(DetailControl::Restart) => self.0.restart_selected(),
531 Some(DetailControl::Duplicate) => self.0.duplicate_selected(),
532 Some(DetailControl::Log) => self.0.open_log(),
533 Some(DetailControl::Command) => self.0.open_command(),
534 Some(DetailControl::Report) => self.0.open_report(),
535 Some(DetailControl::DryRun) => self.0.toggle_dry_run(),
536 Some(DetailControl::Delete) => self.0.remove_selected(),
537 None => self.0.set_page(ManagerPage::Collection),
538 }
539 }
540
541 fn back(&mut self) {
542 match self.0.page() {
543 ManagerPage::Formats(id)
544 | ManagerPage::Output(id)
545 | ManagerPage::Input(id)
546 | ManagerPage::Log(id)
547 | ManagerPage::Report(id)
548 | ManagerPage::Command(id) => {
549 self.0.set_page(ManagerPage::Details(id));
550 }
551 ManagerPage::Collection | ManagerPage::AddSources | ManagerPage::Details(_) => {
552 self.0.set_page(ManagerPage::Collection);
553 }
554 }
555 }
556
557 fn safe_exit_requested(&mut self) {
558 self.0.request_safe_exit();
559 self.0
560 .set_manager_message(Some("cancelling active downloads".to_owned()));
561 }
562
563 fn select_previous_format(&mut self) {
564 if let ManagerPage::Formats(id) = self.0.page() {
565 self.0.cycle_queue_format(id, false);
566 }
567 }
568
569 fn select_next_format(&mut self) {
570 if let ManagerPage::Formats(id) = self.0.page() {
571 self.0.cycle_queue_format(id, true);
572 }
573 }
574
575 fn submit_draft(&mut self) {
576 self.0.submit_draft();
577 }
578
579 fn delete_before_cursor(&mut self) {
580 self.0.delete_before_cursor();
581 }
582
583 fn delete_at_cursor(&mut self) {
584 self.0.delete_at_cursor();
585 }
586
587 fn move_cursor_left(&mut self) {
588 self.0.move_cursor_left();
589 }
590
591 fn move_cursor_right(&mut self) {
592 self.0.move_cursor_right();
593 }
594
595 fn move_cursor_home(&mut self) {
596 self.0.move_cursor_home();
597 }
598
599 fn move_cursor_end(&mut self) {
600 self.0.move_cursor_end();
601 }
602
603 fn select_previous(&mut self) {
604 self.0.select_relative(false);
605 }
606
607 fn select_next(&mut self) {
608 self.0.select_relative(true);
609 }
610
611 fn select_previous_detail(&mut self) {
612 self.0.select_detail(false);
613 }
614
615 fn select_next_detail(&mut self) {
616 self.0.select_detail(true);
617 }
618
619 fn open_formats(&mut self) {
620 self.0.open_formats();
621 }
622
623 fn open_output(&mut self) {
624 self.0.open_output();
625 }
626
627 fn submit_output(&mut self) {
628 self.0.submit_output();
629 }
630
631 fn apply_selected_space(&mut self) {
632 self.0.apply_selected_space();
633 }
634
635 fn duplicate_selected(&mut self) {
636 self.0.duplicate_selected();
637 }
638
639 fn remove_selected(&mut self) {
640 self.0.remove_selected();
641 }
642
643 fn restart_selected(&mut self) {
644 self.0.restart_selected();
645 }
646}