Skip to main content

max / audiofiles

6.7 KB · 178 lines History Blame Raw
1 //! The rest of Settings > Advanced, described: importing a theme, and the
2 //! library mirror.
3 //!
4 //! Export current theme is [`Outcome::File`]'s (`67881a88`).
5 //!
6 //! # Importing a theme needs a path, not bytes
7 //!
8 //! `makeover::import_theme(&path, &custom_dir)` takes a **path**, validates the
9 //! file itself, names it and copies it in. No bytes go anywhere near a route.
10 //!
11 //! [`Outcome::Locate`]'s `Sought::File { accept }` asks for exactly that, and
12 //! `panel::locate` serves every `Sought` shape with the accept-list plumbed
13 //! through, the save shape included. So this is the same `locate`-then-act pair
14 //! [`storage`](super::storage)'s relocation uses.
15 //!
16 //! # A `cfg` is a host fact, so the host answers it
17 //!
18 //! The mirror builds a symlink tree and exists only on unix; the shipped
19 //! section wrapped its whole block in `#[cfg(unix)]`. A description cannot
20 //! carry a `cfg` and should not try: [`Advanced::mirror`] answers `None` where
21 //! there is no mirror to have, and the section is not described there. The
22 //! `cfg` lives in [`FromAdvanced`](super::FromAdvanced), which is the host.
23 //!
24 //! # The error is described, because the checkbox lies without it
25 //!
26 //! `set_mirror_enabled` applies the change in memory whatever happens and
27 //! reports separately that the config write or the teardown failed. So a
28 //! checkbox that took and a checkbox that will not survive a restart look
29 //! identical, which is what the shipped warning banner existed to say and what
30 //! [`Mirror::failed`](super::Mirror::failed) carries here.
31 //!
32 //! [`Advanced::mirror`]: super::Advanced::mirror
33 //! [`Outcome::File`]: quasi_router::Outcome::File
34 //! [`Outcome::Locate`]: quasi_router::Outcome::Locate
35
36 use quasi_declare::declare;
37 use quasi_router::layout::Tone;
38 use quasi_router::{Accepted, Action, Locating, Request, Response, RouteError, Router, Sought};
39
40 use super::Mirror;
41
42 use super::Panels;
43
44 /// The name a picked file comes back under.
45 const FILE: &str = "file";
46
47 /// The name a picked folder comes back under.
48 const FOLDER: &str = "folder";
49
50 /// Register the section's routes.
51 pub fn routes(router: Router<Panels<'_>>) -> Router<Panels<'_>> {
52 router
53 .post("/settings/advanced/theme/import", import)
54 .post("/settings/advanced/theme/imported", imported)
55 .post("/settings/advanced/mirror/enabled", enable)
56 .post("/settings/advanced/mirror/folder", folder)
57 .post("/settings/advanced/mirror/path", path)
58 }
59
60 /// `POST /settings/advanced/theme/import`
61 ///
62 /// The act shape of [`Outcome::Locate`](quasi_router::Outcome::Locate): picking
63 /// the file *is* the import, so the call that lands does the importing. The
64 /// form shape — where the answer is stashed and shown back — is what Storage's
65 /// Choose folder uses, and it is the wrong one here because there is nothing to
66 /// show and nothing to confirm afterwards.
67 fn import(_state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
68 Ok(Response::locate(Locating::new(
69 Sought::File {
70 accept: vec![Accepted::suffix(".toml")],
71 },
72 "Import Theme",
73 Action::post("/settings/advanced/theme/imported"),
74 FILE,
75 )))
76 }
77
78 /// `POST /settings/advanced/theme/imported`
79 ///
80 /// A reader who backed out of the picker has answered nothing: `ui::dialog`
81 /// skips its handler on an empty result, so this is the second guard rather
82 /// than the only one, and it is here because the address is reachable by
83 /// typing.
84 fn imported(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
85 let file = request.payload.get(FILE).unwrap_or_default();
86 if !file.is_empty() {
87 state.advanced.import_theme(file);
88 }
89 settled(state)
90 }
91
92 /// `POST /settings/advanced/mirror/enabled`
93 fn enable(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
94 mirrored(state)?;
95 let on = !request
96 .payload
97 .get("enabled")
98 .unwrap_or_default()
99 .is_empty();
100 state.advanced.enable_mirror(on);
101 settled(state)
102 }
103
104 /// `POST /settings/advanced/mirror/folder`
105 fn folder(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
106 mirrored(state)?;
107 Ok(Response::locate(Locating::folder(
108 "Choose library mirror location",
109 Action::post("/settings/advanced/mirror/path"),
110 FOLDER,
111 )))
112 }
113
114 /// `POST /settings/advanced/mirror/path`
115 fn path(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
116 mirrored(state)?;
117 let folder = request.payload.get(FOLDER).unwrap_or_default();
118 if !folder.is_empty() {
119 state.advanced.set_mirror_path(folder);
120 }
121 settled(state)
122 }
123
124 /// Refuse where this host has no mirror.
125 ///
126 /// Every mirror address answers `NotFound` on Windows rather than quietly doing
127 /// nothing, which is the same refusal `ConfigKey::from_key` makes for a setting
128 /// that is not one: the address is reachable by typing on every host, and the
129 /// screen that offers it is not drawn on this one.
130 fn mirrored(state: &Panels<'_>) -> Result<(), RouteError> {
131 if state.advanced.mirror().is_none() {
132 return Err(RouteError::not_found("this host has no library mirror"));
133 }
134 Ok(())
135 }
136
137 /// The settings window again, which is what every act here answers with.
138 fn settled(state: &Panels<'_>) -> Result<Response, RouteError> {
139 super::settings::showing(state)
140 }
141
142 declare! {
143 /// The rest of the section, spliced in below Export current theme.
144 ///
145 /// The mirror is a whole host's worth of the section and it is absent on
146 /// hosts that have none, so everything under the heading sits inside a loop
147 /// over the `Option` rather than behind a predicate repeated seven times.
148 pub(super) shape section(mirror: Option<&Mirror>) -> Vec<Node>;
149
150 text "Importing reads a theme .toml and adds it to your custom themes.";
151 act "Import theme..." to post "/settings/advanced/theme/import";
152
153 for &mirror in mirror.iter() {
154 let keeping = given mirror.enabled {
155 true -> "on",
156 otherwise -> "",
157 };
158
159 section "Library Mirror";
160 text "Create a symlink tree so DAWs can browse your library as a normal folder.";
161
162 field Checkbox "enabled" "Enable library mirror" {
163 value keeping;
164 writes Action::post("/settings/advanced/mirror/enabled");
165 }
166
167 // Said before it is enabled rather than after, which is the shipped
168 // section's own reasoning: the reader should know where the tree will
169 // be built without hunting for a hidden config.
170 text "Mirror location: {mirror.shown}";
171 act "Change..." to post "/settings/advanced/mirror/folder";
172
173 for failed in mirror.failed.iter() {
174 banner Tone::Warning "Mirror setting not saved: {failed}. It applies to this session only.";
175 }
176 }
177 }
178