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