| 105 |
105 |
|
//! pass verifies the root is clean, so stopping defers the remainder rather than
|
| 106 |
106 |
|
//! abandoning it.
|
| 107 |
107 |
|
|
| 108 |
|
- |
use quasi_router::layout::{Priority, Tone};
|
| 109 |
|
- |
use quasi_router::{
|
| 110 |
|
- |
Act, Action, Figure, Meter, Node, RegionKind, Request, Response, RouteError, Router, Screen,
|
| 111 |
|
- |
Slot, Tag,
|
| 112 |
|
- |
};
|
|
108 |
+ |
use quasi_declare::declare;
|
|
109 |
+ |
use quasi_router::layout::Tone;
|
|
110 |
+ |
use quasi_router::{Request, Response, RouteError, Router};
|
| 113 |
111 |
|
|
| 114 |
|
- |
use super::{Panel, Panels, Playing, Saying};
|
|
112 |
+ |
use super::{Panel, Panels, Saying};
|
| 115 |
113 |
|
|
| 116 |
114 |
|
/// The band under the list.
|
| 117 |
115 |
|
const FOOT: &str = "shell-foot";
|
| 130 |
128 |
|
|
| 131 |
129 |
|
/// `GET /`
|
| 132 |
130 |
|
fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 133 |
|
- |
Ok(screen(state).into())
|
|
131 |
+ |
Ok(showing(state))
|
| 134 |
132 |
|
}
|
| 135 |
133 |
|
|
| 136 |
134 |
|
/// `POST /playback/stop`
|
| 137 |
135 |
|
fn stop(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 138 |
136 |
|
state.shell.stop();
|
| 139 |
|
- |
Ok(screen(state).into())
|
|
137 |
+ |
Ok(showing(state))
|
| 140 |
138 |
|
}
|
| 141 |
139 |
|
|
| 142 |
140 |
|
/// `POST /hint/dismiss`
|
| 143 |
141 |
|
fn dismiss(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 144 |
142 |
|
state.shell.dismiss_hint();
|
| 145 |
|
- |
Ok(screen(state).into())
|
|
143 |
+ |
Ok(showing(state))
|
| 146 |
144 |
|
}
|
| 147 |
145 |
|
|
| 148 |
146 |
|
/// `POST /storage/pause`
|
| 154 |
152 |
|
return Err(RouteError::not_found("no migration is running"));
|
| 155 |
153 |
|
}
|
| 156 |
154 |
|
state.shell.pause_migration();
|
| 157 |
|
- |
Ok(screen(state).into())
|
|
155 |
+ |
Ok(showing(state))
|
| 158 |
156 |
|
}
|
| 159 |
157 |
|
|
| 160 |
|
- |
/// The window: what you can filter by, what is in it, and what it is doing.
|
|
158 |
+ |
/// The window, read and then described.
|
| 161 |
159 |
|
///
|
| 162 |
|
- |
/// `pub(super)` because the sidebar's routes answer it. Every control in
|
| 163 |
|
- |
/// [`library`](super::library) changes what the *list* shows — choosing a vault,
|
| 164 |
|
- |
/// applying a tag filter, opening a collection — so the answer is the window
|
| 165 |
|
- |
/// rather than the corner of it that was pressed.
|
| 166 |
|
- |
pub(super) fn screen(state: &Panels<'_>) -> Screen {
|
| 167 |
|
- |
let screen = Screen::sidebar_content("audiofiles").with(super::toolbar::body(state));
|
| 168 |
|
- |
|
| 169 |
|
- |
// The left column is one of two things and sometimes neither, which is the
|
| 170 |
|
- |
// shipped window's own arrangement rather than a new one: the filter panel
|
| 171 |
|
- |
// takes the sidebar's place while it is open, and the sidebar itself is a
|
| 172 |
|
- |
// toggle. Said here because the toggles are already described -- `Panel`
|
| 173 |
|
- |
// carries both facts and the toolbar draws both chips -- so a host reading
|
| 174 |
|
- |
// `sidebar_visible` a second time to decide the layout would be the second
|
| 175 |
|
- |
// answer this layer exists to remove.
|
| 176 |
|
- |
//
|
| 177 |
|
- |
// The filter panel is a screen of its own (`/filters`) and stays one. It is
|
| 178 |
|
- |
// the host that puts it where the sidebar was, the same way it puts the
|
| 179 |
|
- |
// detail pane on the right: both are regions the window arranges, and
|
| 180 |
|
- |
// neither is a region of this screen.
|
| 181 |
|
- |
let showing = state.bar.showing();
|
| 182 |
|
- |
let screen = if showing.contains(&Panel::Filters) || !showing.contains(&Panel::Sidebar) {
|
| 183 |
|
- |
screen
|
| 184 |
|
- |
} else {
|
| 185 |
|
- |
screen.with(super::library::body(state))
|
| 186 |
|
- |
};
|
| 187 |
|
- |
let screen = screen.with(super::files::body(state));
|
| 188 |
|
- |
|
| 189 |
|
- |
// Above the footer, which is where the shipped strip declares itself, and
|
| 190 |
|
- |
// only while there is something to say. See the header.
|
| 191 |
|
- |
match migrating(state) {
|
| 192 |
|
- |
Some(strip) => screen.with(strip).with(foot(state)),
|
| 193 |
|
- |
None => screen.with(foot(state)),
|
| 194 |
|
- |
}
|
|
160 |
+ |
/// `pub(super)` because the sidebar's and the toolbar's routes answer it. Every
|
|
161 |
+ |
/// control in [`library`](super::library) changes what the *list* shows --
|
|
162 |
+ |
/// choosing a vault, applying a tag filter, opening a collection -- so the
|
|
163 |
+ |
/// answer is the window rather than the corner of it that was pressed.
|
|
164 |
+ |
///
|
|
165 |
+ |
/// A [`Response`] rather than a [`Screen`], which is the rule wave 15 settled
|
|
166 |
+ |
/// on `settings::showing`: a wrapper in front of a declared screen that hands
|
|
167 |
+ |
/// back a screen is a supplier the conversion itself wrote, and the census
|
|
168 |
+ |
/// counts it. Every caller was writing `.into()` on the way out anyway.
|
|
169 |
+ |
pub(super) fn showing(state: &Panels<'_>) -> Response {
|
|
170 |
+ |
Response::from(screen(state, &read(state)))
|
| 195 |
171 |
|
}
|
| 196 |
172 |
|
|
| 197 |
|
- |
/// Blobs being moved into their shards, while any are.
|
| 198 |
|
- |
fn migrating(state: &Panels<'_>) -> Option<Slot> {
|
| 199 |
|
- |
let running = state.shell.migrating()?;
|
| 200 |
|
- |
Some(
|
| 201 |
|
- |
Slot::new(STRIP, RegionKind::Band)
|
| 202 |
|
- |
.with(Node::text("Optimising storage layout"))
|
| 203 |
|
- |
.with(Node::Meter(
|
| 204 |
|
- |
Meter::new(clamp(running.done), clamp(running.total)).label("files"),
|
| 205 |
|
- |
))
|
| 206 |
|
- |
.with(Node::Act(
|
| 207 |
|
- |
Act::new("Pause", Action::post("/storage/pause")).confirm(
|
| 208 |
|
- |
"Stop for now. The remainder resumes the next time this vault opens. Pause?",
|
| 209 |
|
- |
),
|
| 210 |
|
- |
)),
|
| 211 |
|
- |
)
|
|
173 |
+ |
/// What the window and its band say, read off the app.
|
|
174 |
+ |
struct Window {
|
|
175 |
+ |
/// Whether the library sidebar is drawn beside the list.
|
|
176 |
+ |
///
|
|
177 |
+ |
/// The left column is one of two things and sometimes neither, which is the
|
|
178 |
+ |
/// shipped window's own arrangement rather than a new one: the filter panel
|
|
179 |
+ |
/// takes the sidebar's place while it is open, and the sidebar itself is a
|
|
180 |
+ |
/// toggle. Read once here because the toggles are already described --
|
|
181 |
+ |
/// `Panel` carries both facts and the toolbar draws both chips -- so a host
|
|
182 |
+ |
/// reading `sidebar_visible` a second time to decide the layout would be
|
|
183 |
+ |
/// the second answer this layer exists to remove.
|
|
184 |
+ |
///
|
|
185 |
+ |
/// The filter panel is a screen of its own (`/filters`) and stays one. It is
|
|
186 |
+ |
/// the host that puts it where the sidebar was, the same way it puts the
|
|
187 |
+ |
/// detail pane on the right: both are regions the window arranges, and
|
|
188 |
+ |
/// neither is a region of this screen.
|
|
189 |
+ |
sidebar: bool,
|
|
190 |
+ |
/// The blob migration, while one is running.
|
|
191 |
+ |
moving: Option<Moving>,
|
|
192 |
+ |
/// What is playing, while anything is.
|
|
193 |
+ |
playing: Option<Sound>,
|
|
194 |
+ |
/// How many rows are chosen, while more than one is.
|
|
195 |
+ |
chosen: Option<String>,
|
|
196 |
+ |
/// How far analysis has got, while there is anything to have got through.
|
|
197 |
+ |
analysed: Option<Coverage>,
|
|
198 |
+ |
/// The samples that cannot find their files, while any cannot.
|
|
199 |
+ |
lost: Option<Lost>,
|
|
200 |
+ |
/// What the app is saying, while it is saying anything.
|
|
201 |
+ |
said: Option<Said>,
|
|
202 |
+ |
/// Whether the first-launch hint is still up.
|
|
203 |
+ |
///
|
|
204 |
+ |
/// False while the app is saying something, which is the shipped footer's
|
|
205 |
+ |
/// own order: a status line and a hint never share the band.
|
|
206 |
+ |
hinting: bool,
|
|
207 |
+ |
/// What preview plays through, as the line the band draws.
|
|
208 |
+ |
preview: String,
|
|
209 |
+ |
/// Whether that line is a warning, which it is when there is no device.
|
|
210 |
+ |
///
|
|
211 |
+ |
/// Said rather than omitted, because a silent preview with no device is the
|
|
212 |
+ |
/// case this line exists to make diagnosable without opening Settings.
|
|
213 |
+ |
tone: Tone,
|
|
214 |
+ |
/// The focused sample's tags.
|
|
215 |
+ |
tags: Vec<String>,
|
|
216 |
+ |
}
|
|
217 |
+ |
|
|
218 |
+ |
/// Blobs on their way into their shards.
|
|
219 |
+ |
struct Moving {
|
|
220 |
+ |
/// How many have moved, as the meter carries a count.
|
|
221 |
+ |
done: u32,
|
|
222 |
+ |
/// How many there are.
|
|
223 |
+ |
total: u32,
|
|
224 |
+ |
}
|
|
225 |
+ |
|
|
226 |
+ |
/// What is playing, and how far through.
|
|
227 |
+ |
struct Sound {
|
|
228 |
+ |
/// What it is called.
|
|
229 |
+ |
name: String,
|
|
230 |
+ |
/// How far in, in seconds.
|
|
231 |
+ |
position: u32,
|
|
232 |
+ |
/// How long it runs, in seconds.
|
|
233 |
+ |
total: u32,
|
|
234 |
+ |
/// Both of those as the transport writes them.
|
|
235 |
+ |
clock: String,
|
|
236 |
+ |
}
|
|
237 |
+ |
|
|
238 |
+ |
/// How much of what is on screen has been analysed.
|
|
239 |
+ |
struct Coverage {
|
|
240 |
+ |
/// How many of them have been analysed.
|
|
241 |
+ |
analysed: u32,
|
|
242 |
+ |
/// How many are on screen.
|
|
243 |
+ |
samples: u32,
|
|
244 |
+ |
/// What that proportion means: green once it is all of them.
|
|
245 |
+ |
tone: Tone,
|
|
246 |
+ |
/// How many carry no tags, once analysis has produced anything.
|
|
247 |
+ |
///
|
|
248 |
+ |
/// Absent until it has, which is the shipped footer's rule: before the
|
|
249 |
+ |
/// first result every sample is untagged and the count says nothing.
|
|
250 |
+ |
untagged: Option<String>,
|
|
251 |
+ |
}
|
|
252 |
+ |
|
|
253 |
+ |
/// The samples that have lost their files.
|
|
254 |
+ |
struct Lost {
|
|
255 |
+ |
/// The sentence, pluralised for one.
|
|
256 |
+ |
said: String,
|
|
257 |
+ |
}
|
|
258 |
+ |
|
|
259 |
+ |
/// Whatever the app is telling the user.
|
|
260 |
+ |
struct Said {
|
|
261 |
+ |
/// What it says.
|
|
262 |
+ |
text: String,
|
|
263 |
+ |
/// What kind of thing it is.
|
|
264 |
+ |
tone: Tone,
|
|
265 |
+ |
}
|
|
266 |
+ |
|
|
267 |
+ |
/// What the window draws, read off the app.
|
|
268 |
+ |
fn read(state: &Panels<'_>) -> Window {
|
|
269 |
+ |
let showing = state.bar.showing();
|
|
270 |
+ |
let status = state.shell.status();
|
|
271 |
+ |
let seen = state.shell.analysed();
|
|
272 |
+ |
let chosen = state.shell.chosen();
|
|
273 |
+ |
let missing = state.integrity.missing();
|
|
274 |
+ |
let device = state.shell.device();
|
|
275 |
+ |
Window {
|
|
276 |
+ |
sidebar: !showing.contains(&Panel::Filters) && showing.contains(&Panel::Sidebar),
|
|
277 |
+ |
moving: state.shell.migrating().map(|running| Moving {
|
|
278 |
+ |
done: clamp(running.done),
|
|
279 |
+ |
total: clamp(running.total),
|
|
280 |
+ |
}),
|
|
281 |
+ |
playing: state.shell.playing().map(|playing| Sound {
|
|
282 |
+ |
name: playing.name,
|
|
283 |
+ |
position: playing.position,
|
|
284 |
+ |
total: playing.total,
|
|
285 |
+ |
clock: format!("{}/{}", clock(playing.position), clock(playing.total)),
|
|
286 |
+ |
}),
|
|
287 |
+ |
chosen: (chosen > 1).then(|| chosen.to_string()),
|
|
288 |
+ |
analysed: (seen.samples > 0).then(|| Coverage {
|
|
289 |
+ |
analysed: seen.analysed,
|
|
290 |
+ |
samples: seen.samples,
|
|
291 |
+ |
tone: if seen.analysed == seen.samples {
|
|
292 |
+ |
Tone::Success
|
|
293 |
+ |
} else {
|
|
294 |
+ |
Tone::Neutral
|
|
295 |
+ |
},
|
|
296 |
+ |
untagged: (seen.analysed > 0 && seen.untagged > 0).then(|| seen.untagged.to_string()),
|
|
297 |
+ |
}),
|
|
298 |
+ |
lost: (missing > 0).then(|| Lost {
|
|
299 |
+ |
said: format!(
|
|
300 |
+ |
"{missing} sample{} cannot find {} file.",
|
|
301 |
+ |
if missing == 1 { "" } else { "s" },
|
|
302 |
+ |
if missing == 1 { "its" } else { "their" },
|
|
303 |
+ |
),
|
|
304 |
+ |
}),
|
|
305 |
+ |
said: status.map(|(text, saying)| Said {
|
|
306 |
+ |
text,
|
|
307 |
+ |
tone: match saying {
|
|
308 |
+ |
Saying::Failed => Tone::Danger,
|
|
309 |
+ |
Saying::Ordinary => Tone::Neutral,
|
|
310 |
+ |
},
|
|
311 |
+ |
}),
|
|
312 |
+ |
hinting: state.shell.hinting() && state.shell.status().is_none(),
|
|
313 |
+ |
preview: match &device {
|
|
314 |
+ |
Some(device) => format!("Preview: {device}"),
|
|
315 |
+ |
None => "Preview: no device".to_owned(),
|
|
316 |
+ |
},
|
|
317 |
+ |
tone: if device.is_some() {
|
|
318 |
+ |
Tone::Neutral
|
|
319 |
+ |
} else {
|
|
320 |
+ |
Tone::Warning
|
|
321 |
+ |
},
|
|
322 |
+ |
tags: state.shell.tags(),
|
|
323 |
+ |
}
|
| 212 |
324 |
|
}
|
| 213 |
325 |
|
|
| 214 |
326 |
|
/// A count as the meter carries one.
|
| 216 |
328 |
|
u32::try_from(count).unwrap_or(u32::MAX)
|
| 217 |
329 |
|
}
|
| 218 |
330 |
|
|
| 219 |
|
- |
/// The status band.
|
| 220 |
|
- |
fn foot(state: &Panels<'_>) -> Slot {
|
| 221 |
|
- |
let mut band = Slot::new(FOOT, RegionKind::Band);
|
|
331 |
+ |
declare! {
|
|
332 |
+ |
/// The window: what you can filter by, what is in it, and what it is doing.
|
|
333 |
+ |
///
|
|
334 |
+ |
/// Three of its five regions are other modules' and arrive already drawn.
|
|
335 |
+ |
/// `files::body` is the one worth naming: the standalone `/files` window
|
|
336 |
+ |
/// wraps the same shape in a screen of its own, so **two callers, one
|
|
337 |
+ |
/// description**, and the list never had to learn that it might not be
|
|
338 |
+ |
/// alone.
|
|
339 |
+ |
///
|
|
340 |
+ |
/// The strip is placed above the footer, which is where the shipped one
|
|
341 |
+ |
/// declares itself, and only while there is something to say. See the
|
|
342 |
+ |
/// header.
|
|
343 |
+ |
shape screen(state: &Panels<'_>, window: &Window) -> Screen;
|
| 222 |
344 |
|
|
| 223 |
|
- |
if let Some(playing) = state.shell.playing() {
|
| 224 |
|
- |
band = transport(band, &playing);
|
|
345 |
+ |
screen sidebar_content "audiofiles" {
|
|
346 |
+ |
include super::toolbar::body(state);
|
|
347 |
+ |
include super::library::body(state) when window.sidebar;
|
|
348 |
+ |
include super::files::body(state);
|
|
349 |
+ |
|
|
350 |
+ |
for moving in window.moving.iter() {
|
|
351 |
+ |
include migrating(moving);
|
|
352 |
+ |
}
|
|
353 |
+ |
include foot(window);
|
| 225 |
354 |
|
}
|
| 226 |
|
- |
|
| 227 |
|
- |
let chosen = state.shell.chosen();
|
| 228 |
|
- |
if chosen > 1 {
|
| 229 |
|
- |
band = band.with(Node::Figure(Figure::new(chosen.to_string(), "selected")));
|
| 230 |
|
- |
}
|
| 231 |
|
- |
|
| 232 |
|
- |
band = coverage(band, state);
|
| 233 |
|
- |
band = missing(band, state);
|
| 234 |
|
- |
band = saying(band, state);
|
| 235 |
|
- |
|
| 236 |
|
- |
if let Some(device) = state.shell.device() {
|
| 237 |
|
- |
band = band.with(Node::text(format!("Preview: {device}")));
|
| 238 |
|
- |
} else {
|
| 239 |
|
- |
// Said rather than omitted, because a silent preview with no device is
|
| 240 |
|
- |
// the case this line exists to make diagnosable without opening
|
| 241 |
|
- |
// Settings.
|
| 242 |
|
- |
band = band.with(Node::Text {
|
| 243 |
|
- |
text: "Preview: no device".to_owned(),
|
| 244 |
|
- |
tone: Tone::Warning,
|
| 245 |
|
- |
});
|
| 246 |
|
- |
}
|
| 247 |
|
- |
|
| 248 |
|
- |
// The focused sample's tags, inert. The shipped footer renders these as
|
| 249 |
|
- |
// muted text rather than chips on purpose -- "these are inert
|
| 250 |
|
- |
// (informational only), so the affordance contract should not invite a
|
| 251 |
|
- |
// click" -- which is exactly what a badge is and a chip is not.
|
| 252 |
|
- |
//
|
| 253 |
|
- |
// Optional, and they are the only thing in this band that is. A badge here
|
| 254 |
|
- |
// repeats a fact the detail panel states in full, so a window with no room
|
| 255 |
|
- |
// for everything loses the repetition first. Everything else in the footer
|
| 256 |
|
- |
// is a fact stated nowhere else on the screen.
|
| 257 |
|
- |
for tag in state.shell.tags() {
|
| 258 |
|
- |
band = band.with_ranked(Node::Token(Tag::badge(tag)), Priority::Optional);
|
| 259 |
|
- |
}
|
| 260 |
|
- |
|
| 261 |
|
- |
band
|
| 262 |
355 |
|
}
|
| 263 |
356 |
|
|
| 264 |
|
- |
/// What is playing, and how far through.
|
| 265 |
|
- |
fn transport(band: Slot, playing: &Playing) -> Slot {
|
| 266 |
|
- |
band.with(Node::text(format!("Playing: {}", playing.name)))
|
| 267 |
|
- |
.with(Node::Meter(
|
| 268 |
|
- |
Meter::new(playing.position, playing.total).label("seconds"),
|
| 269 |
|
- |
))
|
| 270 |
|
- |
.with(Node::text(format!(
|
| 271 |
|
- |
"{}/{}",
|
| 272 |
|
- |
clock(playing.position),
|
| 273 |
|
- |
clock(playing.total)
|
| 274 |
|
- |
)))
|
| 275 |
|
- |
.with(Node::Act(
|
| 276 |
|
- |
Act::new("Stop", Action::post("/playback/stop")).key("space"),
|
| 277 |
|
- |
))
|
|
357 |
+ |
declare! {
|
|
358 |
+ |
/// Blobs being moved into their shards.
|
|
359 |
+ |
///
|
|
360 |
+ |
/// Pause is honest here in a way a cancel usually is not, and the
|
|
361 |
+ |
/// description says so on the control: the sweep is resumable and records
|
|
362 |
+ |
/// nothing until a pass verifies the root is clean, so stopping defers the
|
|
363 |
+ |
/// remainder rather than abandoning it.
|
|
364 |
+ |
shape migrating(moving: &Moving) -> Slot;
|
|
365 |
+ |
|
|
366 |
+ |
region STRIP as Band {
|
|
367 |
+ |
text "Optimising storage layout";
|
|
368 |
+ |
proportion moving.done moving.total {
|
|
369 |
+ |
label "files";
|
|
370 |
+ |
}
|
|
371 |
+ |
act "Pause" to post "/storage/pause" {
|
|
372 |
+ |
confirm "Stop for now. The remainder resumes the next time this \
|
|
373 |
+ |
vault opens. Pause?";
|
|
374 |
+ |
}
|
|
375 |
+ |
}
|
| 278 |
376 |
|
}
|
| 279 |
377 |
|
|
| 280 |
|
- |
/// How much of what is on screen has been analysed.
|
| 281 |
|
- |
///
|
| 282 |
|
- |
/// A [`Meter`] and a [`Figure`], where the shipped footer has two coloured
|
| 283 |
|
- |
/// strings. The proportion is the meter; the untagged count is a separate fact
|
| 284 |
|
- |
/// about the same set rather than a second proportion of it, which is why it is
|
| 285 |
|
- |
/// a figure and not a second bar.
|
| 286 |
|
- |
fn coverage(band: Slot, state: &Panels<'_>) -> Slot {
|
| 287 |
|
- |
let seen = state.shell.analysed();
|
| 288 |
|
- |
if seen.samples == 0 {
|
| 289 |
|
- |
return band;
|
| 290 |
|
- |
}
|
|
378 |
+ |
declare! {
|
|
379 |
+ |
/// The status band.
|
|
380 |
+ |
shape foot(window: &Window) -> Slot;
|
| 291 |
381 |
|
|
| 292 |
|
- |
let done = seen.analysed == seen.samples;
|
| 293 |
|
- |
let mut band = band.with(Node::Meter(
|
| 294 |
|
- |
Meter::new(seen.analysed, seen.samples)
|
| 295 |
|
- |
.label("analysed")
|
| 296 |
|
- |
.tone(if done { Tone::Success } else { Tone::Neutral }),
|
| 297 |
|
- |
));
|
|
382 |
+ |
region FOOT as Band {
|
|
383 |
+ |
for sound in window.playing.iter() {
|
|
384 |
+ |
extend transport(sound);
|
|
385 |
+ |
}
|
|
386 |
+ |
for chosen in window.chosen.iter() {
|
|
387 |
+ |
tally chosen "selected";
|
|
388 |
+ |
}
|
|
389 |
+ |
for seen in window.analysed.iter() {
|
|
390 |
+ |
extend coverage(seen);
|
|
391 |
+ |
}
|
|
392 |
+ |
for lost in window.lost.iter() {
|
|
393 |
+ |
extend missing(lost);
|
|
394 |
+ |
}
|
|
395 |
+ |
extend saying(window);
|
|
396 |
+ |
toned &window.preview window.tone;
|
| 298 |
397 |
|
|
| 299 |
|
- |
// Suppressed until analysis has produced something, which is the shipped
|
| 300 |
|
- |
// footer's rule: before the first result every sample is untagged and the
|
| 301 |
|
- |
// count says nothing.
|
| 302 |
|
- |
if seen.analysed > 0 && seen.untagged > 0 {
|
| 303 |
|
- |
band = band.with(Node::Figure(Figure::new(
|
| 304 |
|
- |
seen.untagged.to_string(),
|
| 305 |
|
- |
"untagged",
|
| 306 |
|
- |
)));
|
|
398 |
+ |
// The focused sample's tags, inert. The shipped footer renders these as
|
|
399 |
+ |
// muted text rather than chips on purpose -- "these are inert
|
|
400 |
+ |
// (informational only), so the affordance contract should not invite a
|
|
401 |
+ |
// click" -- which is exactly what a badge is and a chip is not.
|
|
402 |
+ |
//
|
|
403 |
+ |
// Optional, and they are the only thing in this band that is. A badge
|
|
404 |
+ |
// here repeats a fact the detail panel states in full, so a window with
|
|
405 |
+ |
// no room for everything loses the repetition first. Everything else in
|
|
406 |
+ |
// the footer is a fact stated nowhere else on the screen.
|
|
407 |
+ |
for tag in window.tags.iter() {
|
|
408 |
+ |
beside Optional badge tag;
|
|
409 |
+ |
}
|
| 307 |
410 |
|
}
|
| 308 |
|
- |
band
|
| 309 |
411 |
|
}
|
| 310 |
412 |
|
|
| 311 |
|
- |
/// The samples that have lost their files, and the way in to what can be done.
|
| 312 |
|
- |
///
|
| 313 |
|
- |
/// **The band says it because no description can raise the overlay that says
|
| 314 |
|
- |
/// it.** The shipped app puts the warning up over whatever the user was doing,
|
| 315 |
|
- |
/// as soon as a vault load finds anything missing, and nothing a route answers
|
| 316 |
|
- |
/// can do that: `Outcome::Over` exists because something was pressed. See
|
| 317 |
|
- |
/// [`integrity`](super::integrity)'s header and the finding it shares with the
|
| 318 |
|
- |
/// import preflight. So the fact is stated where the app states its other facts
|
| 319 |
|
- |
/// and the modal is one act away.
|
| 320 |
|
- |
fn missing(band: Slot, state: &Panels<'_>) -> Slot {
|
| 321 |
|
- |
let missing = state.integrity.missing();
|
| 322 |
|
- |
if missing == 0 {
|
| 323 |
|
- |
return band;
|
|
413 |
+ |
declare! {
|
|
414 |
+ |
/// What is playing, and how far through.
|
|
415 |
+ |
shape transport(sound: &Sound) -> Vec<Node>;
|
|
416 |
+ |
|
|
417 |
+ |
text "Playing: {sound.name}";
|
|
418 |
+ |
proportion sound.position sound.total {
|
|
419 |
+ |
label "seconds";
|
|
420 |
+ |
}
|
|
421 |
+ |
text &sound.clock;
|
|
422 |
+ |
act "Stop" to post "/playback/stop" {
|
|
423 |
+ |
key "space";
|
| 324 |
424 |
|
}
|
| 325 |
|
- |
band.with(Node::banner(
|
| 326 |
|
- |
Tone::Warning,
|
| 327 |
|
- |
format!(
|
| 328 |
|
- |
"{missing} sample{} cannot find {} file.",
|
| 329 |
|
- |
if missing == 1 { "" } else { "s" },
|
| 330 |
|
- |
if missing == 1 { "its" } else { "their" },
|
| 331 |
|
- |
),
|
| 332 |
|
- |
))
|
| 333 |
|
- |
.with(Node::Act(Act::new(
|
| 334 |
|
- |
"What is missing",
|
| 335 |
|
- |
Action::get("/library/loose-files"),
|
| 336 |
|
- |
)))
|
| 337 |
425 |
|
}
|
| 338 |
426 |
|
|
| 339 |
|
- |
/// Whatever the app is telling the user.
|
| 340 |
|
- |
///
|
| 341 |
|
- |
/// Tone rather than a timer. The shipped footer decides the colour from
|
| 342 |
|
- |
/// `is_error_status`, a substring match over the message it is about to draw,
|
| 343 |
|
- |
/// and then decides how long to keep it up from two constants and an elapsed
|
| 344 |
|
- |
/// `Instant`. The first half is a fact the app knows when it writes the message
|
| 345 |
|
- |
/// and is what `Tone` carries; the second half is renderer policy and is gone.
|
| 346 |
|
- |
fn saying(band: Slot, state: &Panels<'_>) -> Slot {
|
| 347 |
|
- |
if let Some((text, saying)) = state.shell.status() {
|
| 348 |
|
- |
return band.with(Node::toast(
|
| 349 |
|
- |
match saying {
|
| 350 |
|
- |
Saying::Failed => Tone::Danger,
|
| 351 |
|
- |
Saying::Ordinary => Tone::Neutral,
|
| 352 |
|
- |
},
|
| 353 |
|
- |
text,
|
| 354 |
|
- |
));
|
|
427 |
+ |
declare! {
|
|
428 |
+ |
/// How much of what is on screen has been analysed.
|
|
429 |
+ |
///
|
|
430 |
+ |
/// A meter and a figure, where the shipped footer has two coloured strings.
|
|
431 |
+ |
/// The proportion is the meter; the untagged count is a separate fact about
|
|
432 |
+ |
/// the same set rather than a second proportion of it, which is why it is a
|
|
433 |
+ |
/// figure and not a second bar.
|
|
434 |
+ |
shape coverage(seen: &Coverage) -> Vec<Node>;
|
|
435 |
+ |
|
|
436 |
+ |
proportion seen.analysed seen.samples {
|
|
437 |
+ |
label "analysed";
|
|
438 |
+ |
tone seen.tone;
|
| 355 |
439 |
|
}
|
| 356 |
|
- |
if state.shell.hinting() {
|
| 357 |
|
- |
return band
|