max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
5 files changed,
+96 insertions,
-45 deletions
| @@ -7554,6 +7554,18 @@ | |||
| 7554 | 7554 | "winnow 1.0.4", | |
| 7555 | 7555 | ] | |
| 7556 | 7556 | ||
| 7557 | + | [[patch.unused]] | |
| 7558 | + | name = "kberg" | |
| 7559 | + | version = "0.1.0" | |
| 7560 | + | ||
| 7561 | + | [[patch.unused]] | |
| 7562 | + | name = "ops-status" | |
| 7563 | + | version = "0.1.0" | |
| 7564 | + | ||
| 7565 | + | [[patch.unused]] | |
| 7566 | + | name = "painhours" | |
| 7567 | + | version = "0.1.0" | |
| 7568 | + | ||
| 7557 | 7569 | [[patch.unused]] | |
| 7558 | 7570 | name = "quasi-axum" | |
| 7559 | 7571 | version = "0.62.0" | |
| @@ -7582,18 +7594,6 @@ | |||
| 7582 | 7594 | name = "quasi-webview" | |
| 7583 | 7595 | version = "0.62.0" | |
| 7584 | 7596 | ||
| 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 | 7597 | [[patch.unused]] | |
| 7598 | 7598 | name = "quasi-type" | |
| 7599 | 7599 | version = "0.1.0" |
| @@ -81,18 +81,23 @@ | |||
| 81 | 81 | //! | invalid-tag warning per folder | yes | `tags::validate_tag` over what was typed, and pure | | |
| 82 | 82 | //! | BPM range and top keys over the batch | yes | arithmetic over the results the screen already carries | | |
| 83 | 83 | //! | **rate and ETA** | no | a rolling wall-clock buffer, which is a fact about this machine | | |
| 84 | - | //! | **the four native pickers** | no | `quasi:vocabulary:host-save-location`, consumers five to eight | | |
| 84 | + | //! | **`Import files...`**, and two doors not converted yet | no | see below | | |
| 85 | 85 | //! | **↑/↓ walking the review list** | no | a key bound to "the next row of a list", which `Chrome` cannot say | | |
| 86 | 86 | //! | |
| 87 | 87 | //! The pickers are the sharper half. `Import folder...`, `Quick import | |
| 88 | 88 | //! folder...`, `Import files...` and `Change...` each open a native dialog and | |
| 89 | - | //! then act, which is the gap `integrity` filed as its fourth consumer. **Four | |
| 90 | - | //! more arrive here at once**, and with them a second half of the same gap that | |
| 91 | - | //! nothing had hit yet: a route that hands off to the host has nothing true to | |
| 92 | - | //! answer. [`Outcome`](quasi_router::Outcome) is `Screen`, `Fragment`, `Goto` or | |
| 93 | - | //! `Over`, and none of them is "nothing here changed". They answer | |
| 94 | - | //! `Goto(/import)` — right by the time the picker returns, and a frame early, | |
| 95 | - | //! which is the standing cost `Runtime::reload` corrects. | |
| 89 | + | //! then act, which this header filed as consumers five to eight of | |
| 90 | + | //! `quasi:vocabulary:host-save-location`. That gap closed in quasi 0.60 | |
| 91 | + | //! (`ec92f9cb`) and `Import folder...` is converted: [`open_folder`](fn@open_folder) | |
| 92 | + | //! says a folder is wanted and [`folder_chosen`](fn@folder_chosen) sets the | |
| 93 | + | //! wizard up on the answer. | |
| 94 | + | //! | |
| 95 | + | //! The other three still hand off, and a route that hands off to the host has | |
| 96 | + | //! nothing true to answer. [`Outcome`](quasi_router::Outcome) is `Screen`, | |
| 97 | + | //! `Fragment`, `Goto` or `Over`, and none of them is "nothing here changed". | |
| 98 | + | //! They answer `Goto(/import)`, right by the time the picker returns and a | |
| 99 | + | //! frame early when it is pressed, which is the standing cost | |
| 100 | + | //! `Runtime::reload` corrects. | |
| 96 | 101 | //! | |
| 97 | 102 | //! # The preflight | |
| 98 | 103 | //! | |
| @@ -144,8 +149,8 @@ | |||
| 144 | 149 | ||
| 145 | 150 | use quasi_router::layout::{FieldKind, Readiness, Selector, Tone}; | |
| 146 | 151 | use quasi_router::{ | |
| 147 | - | Act, Action, Choice, Field, Figure, Meter, Node, Outcome, Prose, RegionKind, Request, Response, | |
| 148 | - | RouteError, Router, Row, Screen, Slot, | |
| 152 | + | Act, Action, Choice, Field, Figure, Locating, Meter, Node, Outcome, Prose, RegionKind, Request, | |
| 153 | + | Response, RouteError, Router, Row, Screen, Slot, | |
| 149 | 154 | }; | |
| 150 | 155 | ||
| 151 | 156 | use super::{ | |
| @@ -178,6 +183,9 @@ | |||
| 178 | 183 | /// The name the "don't ask again" answer is submitted under. | |
| 179 | 184 | const AGAIN: &str = "again"; | |
| 180 | 185 | ||
| 186 | + | /// The name a picked folder comes back under. | |
| 187 | + | const FOLDER: &str = "folder"; | |
| 188 | + | ||
| 181 | 189 | /// The name a folder's tags are submitted under. | |
| 182 | 190 | const TAGS: &str = "tags"; | |
| 183 | 191 | ||
| @@ -213,6 +221,7 @@ | |||
| 213 | 221 | .get("/import", index) | |
| 214 | 222 | .get("/import/open", open) | |
| 215 | 223 | .post("/import/open/folder", open_folder) | |
| 224 | + | .post("/import/open/folder/chosen", folder_chosen) | |
| 216 | 225 | .post("/import/open/quick", open_quickly) | |
| 217 | 226 | .post("/import/open/files", open_files) | |
| 218 | 227 | .post("/import/source", change_source) | |
| @@ -366,8 +375,27 @@ | |||
| 366 | 375 | } | |
| 367 | 376 | ||
| 368 | 377 | /// `POST /import/open/folder` | |
| 369 | - | fn open_folder(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 370 | - | state.importing.open_folder(); | |
| 378 | + | /// | |
| 379 | + | /// Says a folder is wanted and leaves the picker to the host. Setting the | |
| 380 | + | /// wizard up is [`folder_chosen`](fn@folder_chosen)'s, on the answer. | |
| 381 | + | fn open_folder(_state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> { | |
| 382 | + | Ok(Response::locate(Locating::folder( | |
| 383 | + | "Import folder", | |
| 384 | + | Action::post("/import/open/folder/chosen"), | |
| 385 | + | FOLDER, | |
| 386 | + | ))) | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | /// `POST /import/open/folder/chosen` | |
| 390 | + | /// | |
| 391 | + | /// A reader who backed out of the picker has answered nothing: `ui::dialog` | |
| 392 | + | /// skips its handler on an empty result, so this is the second guard rather than | |
| 393 | + | /// the only one, and it is here because the address is reachable by typing. | |
| 394 | + | fn folder_chosen(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> { | |
| 395 | + | let folder = request.payload.get(FOLDER).unwrap_or_default(); | |
| 396 | + | if !folder.is_empty() { | |
| 397 | + | state.importing.open_folder(folder); | |
| 398 | + | } | |
| 371 | 399 | Ok(handed_off()) | |
| 372 | 400 | } | |
| 373 | 401 |
| @@ -840,8 +840,8 @@ | |||
| 840 | 840 | AcceptImport { again: bool }, | |
| 841 | 841 | /// Drop the import that is waiting. | |
| 842 | 842 | CancelImport, | |
| 843 | - | /// Ask the host for a folder, then set the wizard up on it. | |
| 844 | - | OpenImportFolder, | |
| 843 | + | /// Set the wizard up on this folder, which the reader picked. | |
| 844 | + | ImportFolder(std::path::PathBuf), | |
| 845 | 845 | /// Ask the host for a folder, then index it with no questions asked. | |
| 846 | 846 | OpenQuickImport, | |
| 847 | 847 | /// Ask the host for files, then merge them into the vault that is open. | |
| @@ -3794,12 +3794,13 @@ | |||
| 3794 | 3794 | /// Orphaned samples being swept up, if any are. See [`Sweep`]. | |
| 3795 | 3795 | fn sweeping(&self) -> Option<Sweep>; | |
| 3796 | 3796 | ||
| 3797 | - | // The doors. Each opens a native picker and then acts, which is a host act | |
| 3798 | - | // with no described step: see this module's `integrity` header, and | |
| 3799 | - | // `importing`'s for why three more consumers of it arrive at once. | |
| 3797 | + | // The doors. Three of them are answered pickers: the route asks with | |
| 3798 | + | // `Outcome::Locate` and the call lands with what the reader picked. The | |
| 3799 | + | // fourth, `open_files`, opens its own, for the reason `importing`'s header | |
| 3800 | + | // gives. | |
| 3800 | 3801 | ||
| 3801 | - | /// Pick a folder, then set the wizard up on it. | |
| 3802 | - | fn open_folder(&self); | |
| 3802 | + | /// Set the wizard up on this folder. | |
| 3803 | + | fn open_folder(&self, folder: &str); | |
| 3803 | 3804 | ||
| 3804 | 3805 | /// Pick a folder, then index it with no questions asked. | |
| 3805 | 3806 | fn open_quickly(&self); | |
| @@ -4092,8 +4093,8 @@ | |||
| 4092 | 4093 | } | |
| 4093 | 4094 | } | |
| 4094 | 4095 | ||
| 4095 | - | fn open_folder(&self) { | |
| 4096 | - | self.push(Intent::OpenImportFolder); | |
| 4096 | + | fn open_folder(&self, folder: &str) { | |
| 4097 | + | self.push(Intent::ImportFolder(std::path::PathBuf::from(folder))); | |
| 4097 | 4098 | } | |
| 4098 | 4099 | ||
| 4099 | 4100 | fn open_quickly(&self) { |
| @@ -1044,14 +1044,11 @@ | |||
| 1044 | 1044 | // is the app's own screen state, so the whole capability writes | |
| 1045 | 1045 | // through intents -- see `Importing`'s header. | |
| 1046 | 1046 | // | |
| 1047 | - | // The four doors are host acts with no described step, which is | |
| 1048 | - | // `quasi:vocabulary:host-save-location`'s fifth to eighth consumers | |
| 1049 | - | // and the same shape `LocateLooseFiles` takes below. | |
| 1050 | - | Intent::OpenImportFolder => { | |
| 1051 | - | state | |
| 1052 | - | .dialogs | |
| 1053 | - | .pick_folder("Import folder", BrowserState::show_import_options); | |
| 1054 | - | } | |
| 1047 | + | // Three of the four doors are answered pickers: the route asks | |
| 1048 | + | // with `Outcome::Locate` and the intent lands with what came back. | |
| 1049 | + | // `OpenImportFiles` opens its own, for the reason `importing`'s | |
| 1050 | + | // header gives. | |
| 1051 | + | Intent::ImportFolder(folder) => state.show_import_options(folder), | |
| 1055 | 1052 | Intent::OpenQuickImport => { | |
| 1056 | 1053 | state | |
| 1057 | 1054 | .dialogs |
| @@ -5755,7 +5755,7 @@ | |||
| 5755 | 5755 | ||
| 5756 | 5756 | fn accept(&self, _again: bool) {} | |
| 5757 | 5757 | fn cancel(&self) {} | |
| 5758 | - | fn open_folder(&self) {} | |
| 5758 | + | fn open_folder(&self, _folder: &str) {} | |
| 5759 | 5759 | fn open_quickly(&self) {} | |
| 5760 | 5760 | fn open_files(&self) {} | |
| 5761 | 5761 | fn change_source(&self) {} | |
| @@ -6412,8 +6412,8 @@ | |||
| 6412 | 6412 | self.say("cancel"); | |
| 6413 | 6413 | } | |
| 6414 | 6414 | ||
| 6415 | - | fn open_folder(&self) { | |
| 6416 | - | self.say("open:folder"); | |
| 6415 | + | fn open_folder(&self, folder: &str) { | |
| 6416 | + | self.say(format!("open:folder {folder}")); | |
| 6417 | 6417 | } | |
| 6418 | 6418 | ||
| 6419 | 6419 | fn open_quickly(&self) { | |
| @@ -8159,7 +8159,6 @@ | |||
| 8159 | 8159 | // No outcome means "nothing here changed", so they answer the flow, which | |
| 8160 | 8160 | // is right by the time the picker returns. See the module header. | |
| 8161 | 8161 | for (address, expected) in [ | |
| 8162 | - | ("/import/open/folder", "open:folder"), | |
| 8163 | 8162 | ("/import/open/quick", "open:quick"), | |
| 8164 | 8163 | ("/import/open/files", "open:files"), | |
| 8165 | 8164 | ] { | |
| @@ -8174,6 +8173,32 @@ | |||
| 8174 | 8173 | } | |
| 8175 | 8174 | } | |
| 8176 | 8175 | ||
| 8176 | + | #[test] | |
| 8177 | + | fn the_wizard_door_asks_the_host_for_a_folder_and_opens_on_the_answer() { | |
| 8178 | + | // The act shape of `Outcome::Locate`: picking the folder *is* opening the | |
| 8179 | + | // wizard, so the call that lands sets it up rather than stashing a path. | |
| 8180 | + | let import = FakeImport::default(); | |
| 8181 | + | let response = importing(&import, Request::post("/import/open/folder")).unwrap(); | |
| 8182 | + | let Outcome::Locate(asking) = &response.outcome else { | |
| 8183 | + | panic!("expected an ask, got {:?}", response.outcome); | |
| 8184 | + | }; | |
| 8185 | + | assert_eq!(asking.answers.route(), Some("/import/open/folder/chosen")); | |
| 8186 | + | assert_eq!(asking.sought, quasi_router::Sought::Folder); | |
| 8187 | + | assert!(import.answered().is_empty(), "the ask opened something"); | |
| 8188 | + | ||
| 8189 | + | let answered = Request::post("/import/open/folder/chosen") | |
| 8190 | + | .sending(Params::new().with("folder".to_owned(), "/mnt/kit".to_owned())); | |
| 8191 | + | importing(&import, answered).unwrap(); | |
| 8192 | + | assert_eq!(import.answered(), ["open:folder /mnt/kit"]); | |
| 8193 | + | } | |
| 8194 | + | ||
| 8195 | + | #[test] | |
| 8196 | + | fn backing_out_of_the_wizard_picker_opens_nothing() { | |
| 8197 | + | let import = FakeImport::default(); | |
| 8198 | + | importing(&import, Request::post("/import/open/folder/chosen")).unwrap(); | |
| 8199 | + | assert!(import.answered().is_empty()); | |
| 8200 | + | } | |
| 8201 | + | ||
| 8177 | 8202 | #[test] | |
| 8178 | 8203 | fn the_source_can_only_be_changed_while_there_is_one_being_configured() { | |
| 8179 | 8204 | let configuring = FakeImport::at(configuring(Strategy::Flat, "", &[])); |