//! The loose-files warning, described: what is missing, and the three things //! that can be done about it. //! //! # The second consumer of the unprompted-overlay finding //! //! [`importing`](super::importing)'s header files it and this is the other half: //! nobody asks for this modal either. `check_loose_files_integrity` runs after a //! vault loads, counts the samples whose source file is gone, and the shipped //! overlay appears over whatever the user was doing. //! //! The described side cannot raise itself, so the fact is said where it is //! known — the status band, which already reports what the app is doing — and the //! overlay is one act away from it. See [`shell`](super::shell)'s `foot`. That is //! a real behaviour difference from the shipped app and it is recorded rather //! than smoothed over: a warning you can ignore is not a warning that stopped //! you. Which of the two is right is a question for the eyeball, and neither is //! sayable today. //! //! # THE FINDING, fourth consumer: Locate is a host act //! //! `Locate missing files...` opens a native folder picker //! (`state.dialogs.pick_folder`), and `quasi:vocabulary:host-save-location` is //! the gap that nothing describes one. Same shape as the export destination, the //! import source and the theme export before it. It is an ordinary act here and //! the host does what only a host can, which is the same workaround those three //! took. //! //! # Purge says what it takes, on the control //! //! The shipped modal draws the blast radius as a warning-toned line above the //! button row: tags, analysis and history go. `Act::confirm` carries it now, so //! the sentence is attached to the thing that does it rather than sitting near //! it, which is `524a63fe`'s argument and the third `ConfirmAction`-shaped thing //! this port has replaced with a builder method. use quasi_router::layout::{Notice, Tone}; use quasi_router::{ Act, Action, Node, Outcome, RegionKind, Request, Response, RouteError, Router, Screen, Slot, }; use super::Panels; /// The region the warning answers into. const BODY: &str = "loose-files"; /// Where an answered warning goes. const BACK: &str = "/"; /// What Purge takes with it, said on the control that does it. const BLAST: &str = "Tags, analysis results, and history for these samples will be permanently deleted. Purge?"; /// Register the warning's routes. pub fn routes(router: Router>) -> Router> { router .get("/library/loose-files", screen) .post("/library/loose-files/dismiss", dismiss) .post("/library/loose-files/locate", locate) .post("/library/loose-files/purge", purge) } /// `GET /library/loose-files` fn screen(state: &Panels<'_>, _request: Request) -> Result { let missing = state.integrity.missing(); if missing == 0 { return Err(RouteError::not_found("nothing is missing")); } let body = Slot::new(BODY, RegionKind::Pane) .with(Node::page("Loose-files mode warning")) .with(Node::Notice { kind: Notice::Banner, tone: Tone::Warning, text: format!( "{missing} sample{} in this vault {} missing source {}.", if missing == 1 { "" } else { "s" }, if missing == 1 { "has a" } else { "have" }, if missing == 1 { "file" } else { "files" }, ), }) .with(Node::text( "The original files may have been moved or deleted. These samples cannot be played or exported until the files are restored.", )) .with(Node::Act( Act::new("Locate missing files", Action::post("/library/loose-files/locate")), )) .with(Node::Act( Act::new("Purge", Action::post("/library/loose-files/purge")) .tone(Tone::Danger) .confirm(BLAST), )) .with(Node::Act( Act::new("Cancel", Action::post("/library/loose-files/dismiss")).key("esc"), )); Ok(Response::from(Outcome::Over( Screen::sidebar_content("Loose-files mode warning").with(body), ))) } /// `POST /library/loose-files/dismiss` fn dismiss(state: &Panels<'_>, _request: Request) -> Result { state.integrity.dismiss(); Ok(Response::from(Outcome::Goto(Action::get(BACK)))) } /// `POST /library/loose-files/locate` /// /// The answer leaves before the picker opens, and that is honest rather than /// hurried: the host's dialog runs on its own and lands in the app whenever the /// user is done with it, so there is nothing for this screen to wait for. See /// the module header on why a picker is not described at all. fn locate(state: &Panels<'_>, _request: Request) -> Result { state.integrity.locate(); Ok(Response::from(Outcome::Goto(Action::get(BACK)))) } /// `POST /library/loose-files/purge` fn purge(state: &Panels<'_>, _request: Request) -> Result { let missing = state.integrity.missing(); if missing == 0 { return Err(RouteError::not_found("nothing is missing")); } state.integrity.purge(); Ok(Response::from(Outcome::Goto(Action::get(BACK))) .toast(Tone::Warning, format!("Purging {missing} samples."))) }