//! The rest of Settings > Advanced, described: importing a theme, and the //! library mirror. //! //! Export current theme is [`Outcome::File`]'s (`67881a88`). //! //! # Importing a theme needs a path, not bytes //! //! `makeover::import_theme(&path, &custom_dir)` takes a **path**, validates the //! file itself, names it and copies it in. No bytes go anywhere near a route. //! //! [`Outcome::Locate`]'s `Sought::File { accept }` asks for exactly that, and //! `panel::locate` serves every `Sought` shape with the accept-list plumbed //! through, the save shape included. So this is the same `locate`-then-act pair //! [`storage`](super::storage)'s relocation uses. //! //! # A `cfg` is a host fact, so the host answers it //! //! The mirror builds a symlink tree and exists only on unix; the shipped //! section wrapped its whole block in `#[cfg(unix)]`. A description cannot //! carry a `cfg` and should not try: [`Advanced::mirror`] answers `None` where //! there is no mirror to have, and the section is not described there. The //! `cfg` lives in [`FromAdvanced`](super::FromAdvanced), which is the host. //! //! # The error is described, because the checkbox lies without it //! //! `set_mirror_enabled` applies the change in memory whatever happens and //! reports separately that the config write or the teardown failed. So a //! checkbox that took and a checkbox that will not survive a restart look //! identical, which is what the shipped warning banner existed to say and what //! [`Mirror::failed`](super::Mirror::failed) carries here. //! //! [`Advanced::mirror`]: super::Advanced::mirror //! [`Outcome::File`]: quasi_router::Outcome::File //! [`Outcome::Locate`]: quasi_router::Outcome::Locate use quasi_declare::declare; use quasi_router::layout::Tone; use quasi_router::{Accepted, Action, Locating, Request, Response, RouteError, Router, Sought}; use super::Mirror; use super::Panels; /// The name a picked file comes back under. const FILE: &str = "file"; /// The name a picked folder comes back under. const FOLDER: &str = "folder"; /// Register the section's routes. pub fn routes(router: Router>) -> Router> { router .post("/settings/advanced/theme/import", import) .post("/settings/advanced/theme/imported", imported) .post("/settings/advanced/mirror/enabled", enable) .post("/settings/advanced/mirror/folder", folder) .post("/settings/advanced/mirror/path", path) } /// `POST /settings/advanced/theme/import` /// /// The act shape of [`Outcome::Locate`](quasi_router::Outcome::Locate): picking /// the file *is* the import, so the call that lands does the importing. The /// form shape — where the answer is stashed and shown back — is what Storage's /// Choose folder uses, and it is the wrong one here because there is nothing to /// show and nothing to confirm afterwards. fn import(_state: &Panels<'_>, _request: Request) -> Result { Ok(Response::locate(Locating::new( Sought::File { accept: vec![Accepted::suffix(".toml")], }, "Import Theme", Action::post("/settings/advanced/theme/imported"), FILE, ))) } /// `POST /settings/advanced/theme/imported` /// /// A reader who backed out of the picker has answered nothing: `ui::dialog` /// skips its handler on an empty result, so this is the second guard rather /// than the only one, and it is here because the address is reachable by /// typing. fn imported(state: &Panels<'_>, request: Request) -> Result { let file = request.payload.get(FILE).unwrap_or_default(); if !file.is_empty() { state.advanced.import_theme(file); } settled(state) } /// `POST /settings/advanced/mirror/enabled` fn enable(state: &Panels<'_>, request: Request) -> Result { mirrored(state)?; let on = !request .payload .get("enabled") .unwrap_or_default() .is_empty(); state.advanced.enable_mirror(on); settled(state) } /// `POST /settings/advanced/mirror/folder` fn folder(state: &Panels<'_>, _request: Request) -> Result { mirrored(state)?; Ok(Response::locate(Locating::folder( "Choose library mirror location", Action::post("/settings/advanced/mirror/path"), FOLDER, ))) } /// `POST /settings/advanced/mirror/path` fn path(state: &Panels<'_>, request: Request) -> Result { mirrored(state)?; let folder = request.payload.get(FOLDER).unwrap_or_default(); if !folder.is_empty() { state.advanced.set_mirror_path(folder); } settled(state) } /// Refuse where this host has no mirror. /// /// Every mirror address answers `NotFound` on Windows rather than quietly doing /// nothing, which is the same refusal `ConfigKey::from_key` makes for a setting /// that is not one: the address is reachable by typing on every host, and the /// screen that offers it is not drawn on this one. fn mirrored(state: &Panels<'_>) -> Result<(), RouteError> { if state.advanced.mirror().is_none() { return Err(RouteError::not_found("this host has no library mirror")); } Ok(()) } /// The settings window again, which is what every act here answers with. fn settled(state: &Panels<'_>) -> Result { super::settings::showing(state) } declare! { /// The rest of the section, spliced in below Export current theme. /// /// The mirror is a whole host's worth of the section and it is absent on /// hosts that have none, so everything under the heading sits inside a loop /// over the `Option` rather than behind a predicate repeated seven times. pub(super) shape section(mirror: Option<&Mirror>) -> Vec; text "Importing reads a theme .toml and adds it to your custom themes."; act "Import theme..." to post "/settings/advanced/theme/import"; for &mirror in mirror.iter() { let keeping = given mirror.enabled { true -> "on", otherwise -> "", }; section "Library Mirror"; text "Create a symlink tree so DAWs can browse your library as a normal folder."; field Checkbox "enabled" "Enable library mirror" { value keeping; writes Action::post("/settings/advanced/mirror/enabled"); } // Said before it is enabled rather than after, which is the shipped // section's own reasoning: the reader should know where the tree will // be built without hunting for a hidden config. text "Mirror location: {mirror.shown}"; act "Change..." to post "/settings/advanced/mirror/folder"; for failed in mirror.failed.iter() { banner Tone::Warning "Mirror setting not saved: {failed}. It applies to this session only."; } } }