max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
5 files changed,
+101 insertions,
-58 deletions
| @@ -7554,6 +7554,22 @@ | |||
| 7554 | 7554 | "winnow 1.0.4", | |
| 7555 | 7555 | ] | |
| 7556 | 7556 | ||
| 7557 | + | [[patch.unused]] | |
| 7558 | + | name = "quasi-type" | |
| 7559 | + | version = "0.1.0" | |
| 7560 | + | ||
| 7561 | + | [[patch.unused]] | |
| 7562 | + | name = "kberg" | |
| 7563 | + | version = "0.1.0" | |
| 7564 | + | ||
| 7565 | + | [[patch.unused]] | |
| 7566 | + | name = "ops-status" | |
| 7567 | + | version = "0.1.0" | |
| 7568 | + | ||
| 7569 | + | [[patch.unused]] | |
| 7570 | + | name = "painhours" | |
| 7571 | + | version = "0.1.0" | |
| 7572 | + | ||
| 7557 | 7573 | [[patch.unused]] | |
| 7558 | 7574 | name = "quasi-axum" | |
| 7559 | 7575 | version = "0.62.0" | |
| @@ -7581,19 +7597,3 @@ | |||
| 7581 | 7597 | [[patch.unused]] | |
| 7582 | 7598 | name = "quasi-webview" | |
| 7583 | 7599 | version = "0.62.0" | |
| 7584 | - | ||
| 7585 | - | [[patch.unused]] | |
| 7586 | - | name = "kberg" | |
| 7587 | - | version = "0.1.0" | |
| 7588 | - | ||
| 7589 | - | [[patch.unused]] | |
| 7590 | - | name = "ops-status" | |
| 7591 | - | version = "0.1.0" | |
| 7592 | - | ||
| 7593 | - | [[patch.unused]] | |
| 7594 | - | name = "painhours" | |
| 7595 | - | version = "0.1.0" | |
| 7596 | - | ||
| 7597 | - | [[patch.unused]] | |
| 7598 | - | name = "quasi-type" | |
| 7599 | - | version = "0.1.0" |
| @@ -16,19 +16,21 @@ | |||
| 16 | 16 | //! you. Which of the two is right is a question for the eyeball, and neither is | |
| 17 | 17 | //! sayable today. | |
| 18 | 18 | //! | |
| 19 | - | //! # THE FINDING, fourth consumer: Locate is a host act | |
| 19 | + | //! # Locate asks for a folder, and the gap it was filed under is closed | |
| 20 | 20 | //! | |
| 21 | - | //! `Locate missing files...` opens a native folder picker | |
| 22 | - | //! (`state.dialogs.pick_folder`), and `quasi:vocabulary:host-save-location` was | |
| 23 | - | //! the gap that nothing describes one. Same shape as the export destination, the | |
| 24 | - | //! import source and the theme export before it. It is an ordinary act here and | |
| 25 | - | //! the host does what only a host can, which is the same workaround those three | |
| 26 | - | //! took. | |
| 21 | + | //! `Locate missing files...` opens a native folder picker, and this module's | |
| 22 | + | //! header used to call that the fourth consumer of | |
| 23 | + | //! `quasi:vocabulary:host-save-location` — the gap that nothing describes one. | |
| 24 | + | //! `ec92f9cb` closed it in quasi 0.60 with [`Outcome::Locate`], and the pair | |
| 25 | + | //! below is the conversion: [`locate`](fn@locate) says a folder is wanted and | |
| 26 | + | //! [`located`](fn@located) is called with what came back. | |
| 27 | 27 | //! | |
| 28 | - | //! The gap is closed: `ec92f9cb` shipped `Outcome::Locate` in quasi 0.60, and | |
| 29 | - | //! the export flip took it at the destination picker. This site has not been | |
| 30 | - | //! converted, so the paragraph above describes what the code does rather than | |
| 31 | - | //! what it should do. | |
| 28 | + | //! The act shape rather than the form shape, which is [`advanced`]'s Import | |
| 29 | + | //! Theme: picking the folder *is* the search, so there is nothing to stash and | |
| 30 | + | //! nothing to confirm afterwards. | |
| 31 | + | //! | |
| 32 | + | //! [`Outcome::Locate`]: quasi_router::Outcome::Locate | |
| 33 | + | //! [`advanced`]: super::advanced | |
| 32 | 34 | //! | |
| 33 | 35 | //! # Purge says what it takes, on the control | |
| 34 | 36 | //! | |
| @@ -40,7 +42,8 @@ | |||
| 40 | 42 | ||
| 41 | 43 | use quasi_router::layout::{Notice, Tone}; | |
| 42 | 44 | use quasi_router::{ | |
| 43 | - | Act, Action, Node, Outcome, RegionKind, Request, Response, RouteError, Router, Screen, Slot, | |
| 45 | + | Act, Action, Locating, Node, Outcome, RegionKind, Request, Response, RouteError, Router, | |
| 46 | + | Screen, Slot, | |
| 44 | 47 | }; | |
| 45 | 48 | ||
| 46 | 49 | use super::Panels; | |
| @@ -51,6 +54,9 @@ | |||
| 51 | 54 | /// Where an answered warning goes. | |
| 52 | 55 | const BACK: &str = "/"; | |
| 53 | 56 | ||
| 57 | + | /// The name a picked folder comes back under. | |
| 58 | + | const FOLDER: &str = "folder"; | |
| 59 | + | ||
| 54 | 60 | /// What Purge takes with it, said on the control that does it. | |
| 55 | 61 | const BLAST: &str = | |
| 56 | 62 | "Tags, analysis results, and history for these samples will be permanently deleted. Purge?"; | |
| @@ -61,6 +67,7 @@ | |||
| 61 | 67 | .get("/library/loose-files", screen) | |
| 62 | 68 | .post("/library/loose-files/dismiss", dismiss) | |
| 63 | 69 | .post("/library/loose-files/locate", locate) | |
| 70 | + | .post("/library/loose-files/located", located) | |
| 64 | 71 | .post("/library/loose-files/purge", purge) | |
| 65 | 72 | } | |
| 66 | 73 | ||
| @@ -111,12 +118,27 @@ | |||
| 111 | 118 | ||
| 112 | 119 | /// `POST /library/loose-files/locate` | |
| 113 | 120 | /// | |
| 114 | - | /// The answer leaves before the picker opens, and that is honest rather than | |
| 115 | - | /// hurried: the host's dialog runs on its own and lands in the app whenever the | |
| 116 | - | /// user is done with it, so there is nothing for this screen to wait for. See | |
| 117 | - | /// the module header on why a picker is not described at all. | |
| 118 | - | fn locate(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 119 | - | state.integrity.locate(); | |
| 121 | + | /// Says a folder is wanted and leaves the picker to the host, which is the only | |
| 122 | + | /// half of this a description can hold. The search happens in | |
| 123 | + | /// [`located`](fn@located), on the answer. | |
| 124 | + | fn locate(_state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 125 | + | Ok(Response::locate(Locating::folder( | |
| 126 | + | "Locate missing sample files", | |
| 127 | + | Action::post("/library/loose-files/located"), | |
| 128 | + | FOLDER, | |
| 129 | + | ))) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | /// `POST /library/loose-files/located` | |
| 133 | + | /// | |
| 134 | + | /// A reader who backed out of the picker has answered nothing: `ui::dialog` | |
| 135 | + | /// skips its handler on an empty result, so this is the second guard rather than | |
| 136 | + | /// the only one, and it is here because the address is reachable by typing. | |
| 137 | + | fn located(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 138 | + | let folder = request.payload.get(FOLDER).unwrap_or_default(); | |
| 139 | + | if !folder.is_empty() { | |
| 140 | + | state.integrity.locate(folder); | |
| 141 | + | } | |
| 120 | 142 | Ok(Response::from(Outcome::Goto(Action::get(BACK)))) | |
| 121 | 143 | } | |
| 122 | 144 |
| @@ -941,8 +941,8 @@ | |||
| 941 | 941 | DismissLooseFiles, | |
| 942 | 942 | /// Delete the registry entries whose files are gone. | |
| 943 | 943 | PurgeLooseFiles, | |
| 944 | - | /// Ask the host for a folder to look for the missing files in. | |
| 945 | - | LocateLooseFiles, | |
| 944 | + | /// Look for the missing files in this folder, which the reader picked. | |
| 945 | + | LocateLooseFilesIn(std::path::PathBuf), | |
| 946 | 946 | /// Bring a tombstoned sample back. | |
| 947 | 947 | RestoreSample(String), | |
| 948 | 948 | /// Put this machine's id on the clipboard. | |
| @@ -4304,10 +4304,10 @@ | |||
| 4304 | 4304 | ||
| 4305 | 4305 | /// The vault's own health, as much as the warning needs. | |
| 4306 | 4306 | /// | |
| 4307 | - | /// The eleventh narrow trait. Every write is an intent, and the third of them is | |
| 4308 | - | /// the interesting one: locating the missing files opens a native folder picker, | |
| 4309 | - | /// which is a host act with no described step. See [`integrity`]'s header — it is | |
| 4310 | - | /// the **fourth consumer** of `quasi:vocabulary:host-save-location`. | |
| 4307 | + | /// The eleventh narrow trait. Every write is an intent, and the third of them | |
| 4308 | + | /// carries a folder: the description asks for one with | |
| 4309 | + | /// [`Outcome::Locate`](quasi_router::Outcome::Locate) and this is called with | |
| 4310 | + | /// what came back. See [`integrity`]'s header. | |
| 4311 | 4311 | pub trait Integrity { | |
| 4312 | 4312 | /// How many samples have lost the file they point at. | |
| 4313 | 4313 | fn missing(&self) -> usize; | |
| @@ -4315,8 +4315,8 @@ | |||
| 4315 | 4315 | /// Put the warning away without acting. | |
| 4316 | 4316 | fn dismiss(&self); | |
| 4317 | 4317 | ||
| 4318 | - | /// Go looking for the files. | |
| 4319 | - | fn locate(&self); | |
| 4318 | + | /// Go looking for the files in this folder. | |
| 4319 | + | fn locate(&self, folder: &str); | |
| 4320 | 4320 | ||
| 4321 | 4321 | /// Delete the entries whose files are gone. | |
| 4322 | 4322 | fn purge(&self); | |
| @@ -4339,8 +4339,8 @@ | |||
| 4339 | 4339 | self.push(Intent::DismissLooseFiles); | |
| 4340 | 4340 | } | |
| 4341 | 4341 | ||
| 4342 | - | fn locate(&self) { | |
| 4343 | - | self.push(Intent::LocateLooseFiles); | |
| 4342 | + | fn locate(&self, folder: &str) { | |
| 4343 | + | self.push(Intent::LocateLooseFilesIn(std::path::PathBuf::from(folder))); | |
| 4344 | 4344 | } | |
| 4345 | 4345 | ||
| 4346 | 4346 | fn purge(&self) { |
| @@ -1392,15 +1392,10 @@ | |||
| 1392 | 1392 | Intent::Rescan => state.classifier_review_library(), | |
| 1393 | 1393 | Intent::CloseReview => state.close_review_screen(), | |
| 1394 | 1394 | Intent::BeginExport => state.start_export_flow(None), | |
| 1395 | - | // The host act with no described step. See `integrity`'s header: | |
| 1396 | - | // fourth consumer of `quasi:vocabulary:host-save-location`. | |
| 1397 | - | Intent::LocateLooseFiles => { | |
| 1398 | - | state | |
| 1399 | - | .dialogs | |
| 1400 | - | .pick_folder("Locate missing sample files", |state, folder| { | |
| 1401 | - | state.locate_missing_loose_files(&folder); | |
| 1402 | - | }); | |
| 1403 | - | } | |
| 1395 | + | // The picker is `locate` above rather than anything here: the | |
| 1396 | + | // warning asks with `Outcome::Locate` and this lands with the | |
| 1397 | + | // folder that came back. | |
| 1398 | + | Intent::LocateLooseFilesIn(folder) => state.locate_missing_loose_files(&folder), | |
| 1404 | 1399 | // The Storage section. Every one of these is the shipped section's | |
| 1405 | 1400 | // own write: `VaultAction` is the queue the app layer already drains | |
| 1406 | 1401 | // each frame, so the described side asks for exactly what the egui |
| @@ -5794,7 +5794,7 @@ | |||
| 5794 | 5794 | } | |
| 5795 | 5795 | ||
| 5796 | 5796 | fn dismiss(&self) {} | |
| 5797 | - | fn locate(&self) {} | |
| 5797 | + | fn locate(&self, _folder: &str) {} | |
| 5798 | 5798 | fn purge(&self) {} | |
| 5799 | 5799 | } | |
| 5800 | 5800 | ||
| @@ -6597,8 +6597,8 @@ | |||
| 6597 | 6597 | self.asked.borrow_mut().push("dismiss".to_owned()); | |
| 6598 | 6598 | } | |
| 6599 | 6599 | ||
| 6600 | - | fn locate(&self) { | |
| 6601 | - | self.asked.borrow_mut().push("locate".to_owned()); | |
| 6600 | + | fn locate(&self, folder: &str) { | |
| 6601 | + | self.asked.borrow_mut().push(format!("locate {folder}")); | |
| 6602 | 6602 | } | |
| 6603 | 6603 | ||
| 6604 | 6604 | fn purge(&self) { | |
| @@ -6668,10 +6668,9 @@ | |||
| 6668 | 6668 | } | |
| 6669 | 6669 | ||
| 6670 | 6670 | #[test] | |
| 6671 | - | fn each_of_the_three_answers_does_one_thing_and_leaves() { | |
| 6671 | + | fn each_of_the_two_answers_does_one_thing_and_leaves() { | |
| 6672 | 6672 | for (address, expected) in [ | |
| 6673 | 6673 | ("/library/loose-files/dismiss", "dismiss"), | |
| 6674 | - | ("/library/loose-files/locate", "locate"), | |
| 6675 | 6674 | ("/library/loose-files/purge", "purge"), | |
| 6676 | 6675 | ] { | |
| 6677 | 6676 | let vault = FakeIntegrity::missing(3); | |
| @@ -6684,6 +6683,33 @@ | |||
| 6684 | 6683 | } | |
| 6685 | 6684 | } | |
| 6686 | 6685 | ||
| 6686 | + | #[test] | |
| 6687 | + | fn locating_asks_the_host_for_a_folder_and_searches_it_on_the_answer() { | |
| 6688 | + | // The act shape of `Outcome::Locate`: picking the folder *is* the search, | |
| 6689 | + | // so the call that lands does the searching rather than stashing a path. | |
| 6690 | + | let vault = FakeIntegrity::missing(3); | |
| 6691 | + | let response = checking(&vault, Request::post("/library/loose-files/locate")).unwrap(); | |
| 6692 | + | let Outcome::Locate(asking) = &response.outcome else { | |
| 6693 | + | panic!("expected an ask, got {:?}", response.outcome); | |
| 6694 | + | }; | |
| 6695 | + | assert_eq!(asking.answers.route(), Some("/library/loose-files/located")); | |
| 6696 | + | assert_eq!(asking.sought, quasi_router::Sought::Folder); | |
| 6697 | + | assert!(vault.asked().is_empty(), "the ask went looking"); | |
| 6698 | + | ||
| 6699 | + | let answered = Request::post("/library/loose-files/located") | |
| 6700 | + | .sending(Params::new().with("folder".to_owned(), "/mnt/samples".to_owned())); | |
| 6701 | + | let response = checking(&vault, answered).unwrap(); | |
| 6702 | + | assert_eq!(vault.asked(), ["locate /mnt/samples"]); | |
| 6703 | + | assert!(matches!(response.outcome, Outcome::Goto(_)), "{response:?}"); | |
| 6704 | + | } | |
| 6705 | + | ||
| 6706 | + | #[test] | |
| 6707 | + | fn backing_out_of_the_locate_picker_searches_nothing() { | |
| 6708 | + | let vault = FakeIntegrity::missing(3); | |
| 6709 | + | checking(&vault, Request::post("/library/loose-files/located")).unwrap(); | |
| 6710 | + | assert!(vault.asked().is_empty()); | |
| 6711 | + | } | |
| 6712 | + | ||
| 6687 | 6713 | #[test] | |
| 6688 | 6714 | fn a_healthy_vault_has_no_warning_to_open() { | |
| 6689 | 6715 | assert!(checking(&Sound, Request::get("/library/loose-files")).is_err()); |