| 50 |
50 |
|
//! that belongs to the description rather than to a renderer: one description
|
| 51 |
51 |
|
//! has to become a context menu, an action sheet and a key-driven menu, and no
|
| 52 |
52 |
|
//! single renderer can be where that is said. It is right, and the file list is
|
| 53 |
|
- |
//! a [`Node::Table`], whose [`Cells`] had no `menu`.
|
|
53 |
+ |
//! a [`Node::Table`], whose
|
|
54 |
+ |
//! [`Cells`](quasi_router::Cells) had no `menu`.
|
| 54 |
55 |
|
//!
|
| 55 |
|
- |
//! **[`Cells::menu`] exists as of quasi-router 0.20.0, and this screen describes
|
|
56 |
+ |
//! **[`Cells::menu`](quasi_router::Cells::menu) exists as of quasi-router 0.20.0, and this screen describes
|
| 56 |
57 |
|
//! its row menu through it.** See `menu` below. A member is added when a consumer
|
| 57 |
58 |
|
//! asks for it, not because its sibling has one.
|
| 58 |
59 |
|
//!
|
| 89 |
90 |
|
//! on one. Picking eleven kicks out of four hundred by hand is close to what this
|
| 90 |
91 |
|
//! app is for, so the selection has a described shape.
|
| 91 |
92 |
|
//!
|
| 92 |
|
- |
//! It is two members. [`Cells::chosen`] is the
|
|
93 |
+ |
//! It is two members. `Cells::chosen` is the
|
| 93 |
94 |
|
//! third state beside `current` and `selected`, three-valued for the reason the
|
| 94 |
95 |
|
//! tick is: a ctrl-click lands on an **unchosen** row, so a renderer has to know
|
| 95 |
96 |
|
//! that row can join a set before anything is in one.
|
| 101 |
102 |
|
//! from the start: windowing rows the app already holds is a performance
|
| 102 |
103 |
|
//! technique, not a described fact.
|
| 103 |
104 |
|
|
| 104 |
|
- |
use quasi_router::layout::{Priority, Sort, Tone, Width};
|
|
105 |
+ |
use quasi_declare::declare;
|
|
106 |
+ |
use quasi_router::layout::{Sort, Tone};
|
| 105 |
107 |
|
use quasi_router::{
|
| 106 |
|
- |
Act, Action, Cell, Cells, Choice, Choosing, Column, Field, Node, RegionKind, Request, Response,
|
| 107 |
|
- |
RouteError, Router, Screen, Slot, Table, Tag,
|
|
108 |
+ |
Act, Action, Choice, Choosing, Node, RegionKind, Request, Response, RouteError, Router, Screen,
|
|
109 |
+ |
Slot,
|
| 108 |
110 |
|
};
|
| 109 |
111 |
|
|
| 110 |
|
- |
use super::{Collection, Panels, Sample};
|
|
112 |
+ |
use super::{Panels, Sample};
|
| 111 |
113 |
|
|
| 112 |
114 |
|
/// The region the screen answers into.
|
| 113 |
115 |
|
const BODY: &str = "files-body";
|
| 173 |
175 |
|
|
| 174 |
176 |
|
/// `GET /files`
|
| 175 |
177 |
|
fn index(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 176 |
|
- |
Ok(screen(state).into())
|
|
178 |
+ |
Ok(showing(state))
|
| 177 |
179 |
|
}
|
| 178 |
180 |
|
|
| 179 |
181 |
|
/// `POST /files/{id}/open`
|
| 190 |
192 |
|
state
|
| 191 |
193 |
|
.files
|
| 192 |
194 |
|
.choose(id, Choosing::read(request.payload.get(Node::CHOOSING)));
|
| 193 |
|
- |
Ok(screen(state).into())
|
|
195 |
+ |
Ok(showing(state))
|
| 194 |
196 |
|
}
|
| 195 |
197 |
|
|
| 196 |
198 |
|
/// `POST /files/{id}/play`
|
| 197 |
199 |
|
fn play(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 198 |
200 |
|
let id = id_of(&request)?;
|
| 199 |
201 |
|
state.files.play(id);
|
| 200 |
|
- |
Ok(screen(state).into())
|
|
202 |
+ |
Ok(showing(state))
|
| 201 |
203 |
|
}
|
| 202 |
204 |
|
|
| 203 |
205 |
|
/// `POST /files/{id}/enter`
|
| 207 |
209 |
|
fn enter(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 208 |
210 |
|
let id = id_of(&request)?;
|
| 209 |
211 |
|
state.files.enter(id);
|
| 210 |
|
- |
Ok(screen(state).into())
|
|
212 |
+ |
Ok(showing(state))
|
| 211 |
213 |
|
}
|
| 212 |
214 |
|
|
| 213 |
215 |
|
/// `POST /files/{id}/path/copy`
|
| 220 |
222 |
|
let id = id_of(&request)?;
|
| 221 |
223 |
|
state.files.open(id);
|
| 222 |
224 |
|
state.detail.copy_path();
|
| 223 |
|
- |
Ok(Response::from(screen(state)).toast(Tone::Success, "Path copied."))
|
|
225 |
+ |
Ok(showing(state).toast(Tone::Success, "Path copied."))
|
| 224 |
226 |
|
}
|
| 225 |
227 |
|
|
| 226 |
228 |
|
/// `POST /files/{id}/reveal`
|
| 227 |
229 |
|
fn reveal(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 228 |
230 |
|
let id = id_of(&request)?;
|
| 229 |
231 |
|
state.files.reveal(id);
|
| 230 |
|
- |
Ok(screen(state).into())
|
|
232 |
+ |
Ok(showing(state))
|
| 231 |
233 |
|
}
|
| 232 |
234 |
|
|
| 233 |
235 |
|
/// `POST /files/{id}/similar`
|
| 235 |
237 |
|
let id = id_of(&request)?;
|
| 236 |
238 |
|
state.files.open(id);
|
| 237 |
239 |
|
state.detail.find_similar();
|
| 238 |
|
- |
Ok(screen(state).into())
|
|
240 |
+ |
Ok(showing(state))
|
| 239 |
241 |
|
}
|
| 240 |
242 |
|
|
| 241 |
243 |
|
/// `POST /files/{id}/basket`
|
| 246 |
248 |
|
fn basket(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 247 |
249 |
|
let id = id_of(&request)?;
|
| 248 |
250 |
|
state.files.toggle_basket(id);
|
| 249 |
|
- |
Ok(screen(state).into())
|
|
251 |
+ |
Ok(showing(state))
|
| 250 |
252 |
|
}
|
| 251 |
253 |
|
|
| 252 |
254 |
|
/// `POST /files/{id}/duplicates`
|
| 254 |
256 |
|
let id = id_of(&request)?;
|
| 255 |
257 |
|
state.files.open(id);
|
| 256 |
258 |
|
state.detail.find_duplicates();
|
| 257 |
|
- |
Ok(screen(state).into())
|
|
259 |
+ |
Ok(showing(state))
|
| 258 |
260 |
|
}
|
| 259 |
261 |
|
|
| 260 |
262 |
|
/// `POST /files/{id}/edit`
|
| 262 |
264 |
|
let id = id_of(&request)?;
|
| 263 |
265 |
|
state.files.open(id);
|
| 264 |
266 |
|
state.detail.edit();
|
| 265 |
|
- |
Ok(screen(state).into())
|
|
267 |
+ |
Ok(showing(state))
|
| 266 |
268 |
|
}
|
| 267 |
269 |
|
|
| 268 |
270 |
|
/// `POST /files/{id}/instrument`
|
| 269 |
271 |
|
fn instrument(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 270 |
272 |
|
let id = id_of(&request)?;
|
| 271 |
273 |
|
state.files.as_instrument(id);
|
| 272 |
|
- |
Ok(screen(state).into())
|
|
274 |
+ |
Ok(showing(state))
|
| 273 |
275 |
|
}
|
| 274 |
276 |
|
|
| 275 |
277 |
|
/// `POST /files/{id}/export`
|
| 291 |
293 |
|
fn reanalyze(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 292 |
294 |
|
let id = id_of(&request)?;
|
| 293 |
295 |
|
state.files.reanalyze(id);
|
| 294 |
|
- |
Ok(screen(state).into())
|
|
296 |
+ |
Ok(showing(state))
|
| 295 |
297 |
|
}
|
| 296 |
298 |
|
|
| 297 |
299 |
|
/// `POST /files/{id}/delete`
|
| 303 |
305 |
|
fn delete(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 304 |
306 |
|
let id = id_of(&request)?;
|
| 305 |
307 |
|
state.files.delete(id);
|
| 306 |
|
- |
Ok(screen(state).into())
|
|
308 |
+ |
Ok(showing(state))
|
| 307 |
309 |
|
}
|
| 308 |
310 |
|
|
| 309 |
311 |
|
/// `POST /files/{id}/download`
|
| 310 |
312 |
|
fn download(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 311 |
313 |
|
let id = id_of(&request)?;
|
| 312 |
314 |
|
state.files.download(id);
|
| 313 |
|
- |
Ok(screen(state).into())
|
|
315 |
+ |
Ok(showing(state))
|
| 314 |
316 |
|
}
|
| 315 |
317 |
|
|
| 316 |
318 |
|
/// `POST /files/{id}/collection/remove`
|
| 317 |
319 |
|
fn remove_from_collection(state: &Panels<'_>, request: Request) -> Result<Response, RouteError> {
|
| 318 |
320 |
|
let id = id_of(&request)?;
|
| 319 |
321 |
|
state.files.remove_from_collection(id);
|
| 320 |
|
- |
Ok(screen(state).into())
|
|
322 |
+ |
Ok(showing(state))
|
| 321 |
323 |
|
}
|
| 322 |
324 |
|
|
| 323 |
325 |
|
/// `POST /files/{id}/collection/add`
|
| 341 |
343 |
|
.find(|collection| collection.id == chosen)
|
| 342 |
344 |
|
.ok_or_else(|| RouteError::not_found("no such collection"))?;
|
| 343 |
345 |
|
state.files.add_to_collection(id, named.id);
|
| 344 |
|
- |
Ok(Response::from(screen(state)).toast(Tone::Success, format!("Added to {}.", named.name)))
|
|
346 |
+ |
Ok(showing(state).toast(Tone::Success, format!("Added to {}.", named.name)))
|
| 345 |
347 |
|
}
|
| 346 |
348 |
|
|
| 347 |
349 |
|
// --- the selection, which is the whole set rather than a row ---
|
| 349 |
351 |
|
/// `POST /files/chosen/invert`
|
| 350 |
352 |
|
fn invert_chosen(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 351 |
353 |
|
state.files.invert_chosen();
|
| 352 |
|
- |
Ok(screen(state).into())
|
|
354 |
+ |
Ok(showing(state))
|
| 353 |
355 |
|
}
|
| 354 |
356 |
|
|
| 355 |
357 |
|
/// `POST /files/chosen/clear`
|
| 356 |
358 |
|
fn deselect(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 357 |
359 |
|
state.files.deselect();
|
| 358 |
|
- |
Ok(screen(state).into())
|
|
360 |
+ |
Ok(showing(state))
|
| 359 |
361 |
|
}
|
| 360 |
362 |
|
|
| 361 |
363 |
|
/// `POST /files/chosen/export`
|
| 373 |
375 |
|
/// `POST /files/chosen/reanalyze`
|
| 374 |
376 |
|
fn reanalyze_chosen(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 375 |
377 |
|
state.files.reanalyze_chosen();
|
| 376 |
|
- |
Ok(screen(state).into())
|
|
378 |
+ |
Ok(showing(state))
|
| 377 |
379 |
|
}
|
| 378 |
380 |
|
|
| 379 |
381 |
|
/// `POST /files/chosen/path/copy`
|
| 385 |
387 |
|
/// route does not hold.
|
| 386 |
388 |
|
fn copy_chosen_paths(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 387 |
389 |
|
state.files.copy_chosen_paths();
|
| 388 |
|
- |
Ok(screen(state).into())
|
|
390 |
+ |
Ok(showing(state))
|
| 389 |
391 |
|
}
|
| 390 |
392 |
|
|
| 391 |
393 |
|
/// `POST /files/chosen/tags/copy`
|
| 392 |
394 |
|
fn copy_tags_from_focus(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 393 |
395 |
|
state.files.copy_tags_from_focus();
|
| 394 |
|
- |
Ok(screen(state).into())
|
|
396 |
+ |
Ok(showing(state))
|
| 395 |
397 |
|
}
|
| 396 |
398 |
|
|
| 397 |
399 |
|
/// `POST /files/chosen/delete`
|
| 401 |
403 |
|
/// reader needs when the subject is a set.
|
| 402 |
404 |
|
fn delete_chosen(state: &Panels<'_>, _request: Request) -> Result<Response, RouteError> {
|
| 403 |
405 |
|
state.files.delete_chosen();
|
| 404 |
|
- |
Ok(screen(state).into())
|
|
406 |
+ |
Ok(showing(state))
|
| 405 |
407 |
|
}
|
| 406 |
408 |
|
|
| 407 |
409 |
|
/// `POST /files/chosen/collection/add`
|
| 418 |
420 |
|
.find(|collection| collection.id == chosen)
|
| 419 |
421 |
|
.ok_or_else(|| RouteError::not_found("no such collection"))?;
|
| 420 |
422 |
|
state.files.add_chosen_to_collection(named.id);
|
| 421 |
|
- |
Ok(Response::from(screen(state)).toast(Tone::Success, format!("Added to {}.", named.name)))
|
|
423 |
+ |
Ok(showing(state).toast(Tone::Success, format!("Added to {}.", named.name)))
|
| 422 |
424 |
|
}
|
| 423 |
425 |
|
|
| 424 |
426 |
|
/// `POST /files/chosen/collection/remove`
|
| 427 |
429 |
|
_request: Request,
|
| 428 |
430 |
|
) -> Result<Response, RouteError> {
|
| 429 |
431 |
|
state.files.remove_chosen_from_collection();
|
| 430 |
|
- |
Ok(screen(state).into())
|
|
432 |
+ |
Ok(showing(state))
|
| 431 |
433 |
|
}
|
| 432 |
434 |
|
|
| 433 |
435 |
|
/// `GET /files/menu/background`
|
| 511 |
513 |
|
return Err(RouteError::not_found("no such sort"));
|
| 512 |
514 |
|
}
|
| 513 |
515 |
|
state.files.sort_by(column);
|
| 514 |
|
- |
Ok(screen(state).into())
|
|
516 |
+ |
Ok(showing(state))
|
| 515 |
517 |
|
}
|
| 516 |
518 |
|
|
| 517 |
519 |
|
/// The row a request names.
|
| 532 |
534 |
|
matches!(column, NAME | DUR | BPM | KEY)
|
| 533 |
535 |
|
}
|
| 534 |
536 |
|
|
| 535 |
|
- |
/// The whole screen.
|
| 536 |
|
- |
fn screen(state: &Panels<'_>) -> Screen {
|
| 537 |
|
- |
Screen::sidebar_content("Samples").with(body(state))
|
|
537 |
+ |
/// The screen, read and then described.
|
|
538 |
+ |
pub(super) fn showing(state: &Panels<'_>) -> Response {
|
|
539 |
+ |
Response::from(screen(&read(state)))
|
| 538 |
540 |
|
}
|
| 539 |
541 |
|
|
| 540 |
|
- |
/// The list, as a region something else can hold.
|
|
542 |
+ |
/// What the list draws, read off the app.
|
| 541 |
543 |
|
///
|
| 542 |
|
- |
/// Public because [`shell`](super::shell) puts it inside the main screen rather
|
| 543 |
|
- |
/// than beside it: the file list is the app's central pane, so a described app
|
| 544 |
|
- |
/// composes this region while the standalone `/files` window answers it alone.
|
| 545 |
|
- |
/// Two callers, one description, which is what a region is for.
|
| 546 |
|
- |
pub fn body(state: &Panels<'_>) -> Slot {
|
| 547 |
|
- |
let shown = state.files.columns();
|
| 548 |
|
- |
let samples = state.files.samples();
|
| 549 |
|
- |
let current = state.files.current();
|
| 550 |
|
- |
// The collections, which decide two menu entries: which one to take a sample
|
| 551 |
|
- |
// out of, and the list to offer for putting one in. Read once for the table
|
| 552 |
|
- |
// rather than per row -- it is a fact about the screen, and every row would
|
| 553 |
|
- |
// otherwise ask the library the same question.
|
| 554 |
|
- |
let collections = state.library.collections();
|
| 555 |
|
- |
let listing = Listing {
|
| 556 |
|
- |
shown,
|
| 557 |
|
- |
current,
|
| 558 |
|
- |
collection: collections.iter().any(|collection| collection.active),
|
| 559 |
|
- |
collections: &collections,
|
| 560 |
|
- |
basket_showing: state.bar.basket_showing(),
|
| 561 |
|
- |
// What the selection menu hangs on. Counted here for the same reason
|
| 562 |
|
- |
// the collections are read here: it is a fact about the screen, and a
|
| 563 |
|
- |
// row asking it would ask it once per row.
|
| 564 |
|
- |
chosen: samples.iter().filter(|sample| sample.selected).count(),
|
| 565 |
|
- |
focus: current.and_then(|id| {
|
| 566 |
|
- |
samples
|
| 567 |
|
- |
.iter()
|
| 568 |
|
- |
.find(|sample| sample.id == id)
|
| 569 |
|
- |
.map(|sample| sample.name.clone())
|
| 570 |
|
- |
}),
|
| 571 |
|
- |
};
|
| 572 |
|
- |
|
| 573 |
|
- |
if samples.is_empty() {
|
| 574 |
|
- |
// The sentence and the way out are both on the node rather than on the
|
| 575 |
|
- |
// region: `703f4cd2` settled that a region with a heading and no rows
|
| 576 |
|
- |
// still has content, so emptiness belongs to the thing that is empty.
|
| 577 |
|
- |
Slot::new(BODY, RegionKind::Pane).with(
|
| 578 |
|
- |
Node::empty("Nothing here yet.")
|
| 579 |
|
- |
.offering(Act::new("Import samples", Action::get("/import/open"))),
|
| 580 |
|
- |
)
|
| 581 |
|
- |
} else {
|
| 582 |
|
- |
// No `more`: everything the app has loaded and filtered is here.
|
| 583 |
|
- |
// Windowing rows it already holds is a renderer's job, which is the
|
| 584 |
|
- |
// note in this module's header.
|
| 585 |
|
- |
Slot::new(BODY, RegionKind::Pane).with(Node::from(
|
| 586 |
|
- |
Table::new(columns(state, shown))
|
| 587 |
|
- |
.rows(samples.iter().map(|sample| row(sample, &listing))),
|
| 588 |
|
- |
))
|
| 589 |
|
- |
}
|
| 590 |
|
- |
}
|
| 591 |
|
- |
|
| 592 |
|
- |
/// The columns, in the order the shipped list puts them.
|
| 593 |
|
- |
///
|
| 594 |
|
- |
/// Nearly a copy of `ui::file_list::describe`, and that is the point: it already
|
| 595 |
|
- |
/// built `makeover_layout::Column`s. What is added is the one thing that file
|
| 596 |
|
- |
/// could not say — the address a heading calls — which is
|
| 597 |
|
- |
/// [`Column::reorder`] and is quasi's rather than the vocabulary's.
|
| 598 |
|
- |
fn columns(state: &Panels<'_>, shown: super::ColumnsShown) -> Vec<Column> {
|
| 599 |
|
- |
let (by, ascending) = state.files.sort();
|
| 600 |
|
- |
let sorted_by = |name: &str| {
|
| 601 |
|
- |
(name == by).then_some(if ascending {
|
| 602 |
|
- |
Sort::Ascending
|
| 603 |
|
- |
} else {
|
| 604 |
|
- |
Sort::Descending
|
| 605 |
|
- |
})
|
| 606 |
|
- |
};
|
| 607 |
|
- |
let data = |name: &'static str, priority: Priority| {
|
| 608 |
|
- |
let mut column = Column::new(name)
|
| 609 |
|
- |
.width(if name == NAME {
|
| 610 |
|
- |
Width::Fill
|
| 611 |
|
- |
} else {
|
| 612 |
|
- |
Width::Fixed
|
| 613 |
|
- |
})
|
| 614 |
|
- |
.priority(priority);
|
| 615 |
|
- |
column.sorted = sorted_by(name);
|
| 616 |
|
- |
if sortable(name) {
|
| 617 |
|
- |
column = column.reorder(Action::post(format!("/files/sort/{name}")));
|
| 618 |
|
- |
}
|
| 619 |
|
- |
column
|
| 620 |
|
- |
};
|
| 621 |
|
- |
|
| 622 |
|
- |
let mut columns = vec![data(NAME, Priority::Essential)];
|
| 623 |
|
- |
// Only while a basket answer is on screen, because it is the only time the
|
| 624 |
|
- |
// question it answers ("which of them is this near?") can be asked. Second
|
| 625 |
|
- |
// rather than last: it is why the row is in the list at all, and a reason
|
| 626 |
|
- |
// shown after the tempo and the key reads as a detail about the row instead.
|
| 627 |
|
- |
if state.bar.basket_showing() {
|
| 628 |
|
- |
columns.push(
|
| 629 |
|
- |
Column::new(NEAR)
|
| 630 |
|
- |
.width(Width::Fixed)
|
| 631 |
|
- |
.priority(Priority::Secondary),
|
| 632 |
|
- |
);
|
| 633 |
|
- |
}
|
| 634 |
|
- |
if shown.duration {
|
| 635 |
|
- |
columns.push(data(DUR, Priority::Secondary));
|
| 636 |
|
- |
}
|
| 637 |
|
- |
if shown.bpm {
|
| 638 |
|
- |
columns.push(data(BPM, Priority::Secondary));
|
| 639 |
|
- |
}
|
| 640 |
|
- |
if shown.key {
|
| 641 |
|
- |
columns.push(data(KEY, Priority::Secondary));
|
| 642 |
|
- |
}
|
| 643 |
|
- |
if shown.peak_db {
|
| 644 |
|
- |
columns.push(data(PEAK, Priority::Optional));
|
| 645 |
|
- |
}
|
| 646 |
|
- |
if shown.tags {
|
| 647 |
|
- |
columns.push(data(TAGS, Priority::Optional));
|
| 648 |
|
- |
}
|
| 649 |
|
- |
// The play control is essential, because a list of samples you cannot hear
|
| 650 |
|
- |
// is a list of filenames.
|
| 651 |
|
- |
columns.push(data(PLAY, Priority::Essential));
|
| 652 |
|
- |
columns
|
| 653 |
|
- |
}
|
| 654 |
|
- |
|
| 655 |
|
- |
/// What every row of one drawing of the list is built against.
|
| 656 |
|
- |
///
|
| 657 |
|
- |
/// Read once in [`body`] and handed down, rather than asked per row: each of
|
| 658 |
|
- |
/// these is a fact about the *screen* -- which columns are on, what is chosen,
|
| 659 |
|
- |
/// which collection is open -- and a row that read them itself would ask the
|
| 660 |
|
- |
/// library the same question five hundred times.
|
| 661 |
|
- |
struct Listing<'a> {
|
|
544 |
+ |
/// Every fact about the *screen* is read once here and handed down, rather than
|
|
545 |
+ |
/// asked per row: which columns are on, what is chosen, which collection is
|
|
546 |
+ |
/// open. A row that read them itself would ask the library the same question
|
|
547 |
+ |
/// five hundred times.
|
|
548 |
+ |
pub(super) struct Listing {
|
| 662 |
549 |
|
/// Which columns the reader has switched on.
|
| 663 |
550 |
|
shown: super::ColumnsShown,
|
| 664 |
|
- |
/// The row the app is pointing at.
|
| 665 |
|
- |
current: Option<i64>,
|
| 666 |
|
- |
/// Whether a collection is the thing being viewed.
|
| 667 |
|
- |
collection: bool,
|
| 668 |
|
- |
/// The collections a sample can be put into.
|
| 669 |
|
- |
collections: &'a [Collection],
|
| 670 |
551 |
|
/// Whether a basket answer is on screen, which adds the anchors column.
|
| 671 |
|
- |
basket_showing: bool,
|
| 672 |
|
- |
/// How many rows the reader has chosen.
|
| 673 |
|
- |
chosen: usize,
|
| 674 |
|
- |
/// What the row in focus is called, for the entry that spreads its tags.
|
| 675 |
|
- |
focus: Option<String>,
|
|
552 |
+ |
basket: bool,
|
|
553 |
+ |
/// Which way each sortable column is ordered, where it is the one in force.
|
|
554 |
+ |
sorted: Sorted,
|
|
555 |
+ |
/// The rows.
|
|
556 |
+ |
rows: Vec<Listed>,
|
|
557 |
+ |
/// Whether there are none.
|
|
558 |
+ |
bare: bool,
|
|
559 |
+ |
/// What the collection and copy-tags entries need.
|
|
560 |
+ |
shared: Shared,
|
|
561 |
+ |
}
|
|
562 |
+ |
|
|
563 |
+ |
/// Which way each sortable column is ordered.
|
|
564 |
+ |
///
|
|
565 |
+ |
/// One field per sortable column rather than the name of the column in force
|
|
566 |
+ |
/// beside its direction, because the description asks each column its own
|
|
567 |
+ |
/// question and a name it would have to compare against is not an answer.
|
|
568 |
+ |
struct Sorted {
|
|
569 |
+ |
name: Option<Sort>,
|
|
570 |
+ |
duration: Option<Sort>,
|
|
571 |
+ |
bpm: Option<Sort>,
|
|
572 |
+ |
key: Option<Sort>,
|
| 676 |
573 |
|
}
|
| 677 |
574 |
|
|
| 678 |
575 |
|
/// One sample as a row.
|
| 679 |
576 |
|
///
|
| 680 |
577 |
|
/// Every cell names its column, and six of the eight are conditional: the
|
| 681 |
578 |
|
/// anchors cell appears with a basket answer and five more follow the reader's
|
| 682 |
|
- |
/// switched-on columns. Built by position, this function and [`columns`] were
|
| 683 |
|
- |
/// two lists of conditionals that had to agree in the same order, in two places,
|
| 684 |
|
- |
/// with nothing checking that they did. Naming them makes [`columns`] the only
|
| 685 |
|
- |
/// place the shape is decided, and a cell whose column is off is dropped rather
|
| 686 |
|
- |
/// than shifting everything after it one column left.
|
| 687 |
|
- |
fn row(sample: &Sample, listing: &Listing<'_>) -> Cells {
|
| 688 |
|
- |
let shown = listing.shown;
|
| 689 |
|
- |
let basket_showing = listing.basket_showing;
|
| 690 |
|
- |
let mut values = Cells::default().at(NAME, Cell::new(&sample.name));
|
| 691 |
|
- |
if basket_showing {
|
| 692 |
|
- |
// A badge per anchor, in basket order, the way the tags cell holds a run
|
| 693 |
|
- |
// of tags: each anchor keeps its own edges rather than becoming a comma
|
| 694 |
|
- |
// in a sentence. Three badges beside one badge is the whole point --
|
| 695 |
|
- |
// "near all of these" and "near one of these" are different answers and
|
| 696 |
|
- |
// must not look alike.
|
| 697 |
|
- |
let mut cell = match sample.matched.first() {
|
| 698 |
|
- |
Some(first) => Cell::tag(Tag::badge(first.clone())),
|
| 699 |
|
- |
None => Cell::new(""),
|
| 700 |
|
- |
};
|
| 701 |
|
- |
for anchor in sample.matched.iter().skip(1) {
|
| 702 |
|
- |
cell = cell.token(Tag::badge(anchor.clone()));
|
|
579 |
+ |
/// switched-on columns. Built by position, this and the column list were two
|
|
580 |
+ |
/// lists of conditionals that had to agree in the same order, in two places,
|
|
581 |
+ |
/// with nothing checking that they did. Naming them makes the column list the
|
|
582 |
+ |
/// only place the shape is decided, and a cell whose column is off is dropped
|
|
583 |
+ |
/// rather than shifting everything after it one column left.
|
|
584 |
+ |
struct Listed {
|
|
585 |
+ |
/// The row's own id, which is what its addresses are built from.
|
|
586 |
+ |
id: String,
|
|
587 |
+ |
/// What it is called.
|
|
588 |
+ |
name: String,
|
|
589 |
+ |
/// The anchors it answered to, while a basket answer is on screen.
|
|
590 |
+ |
near: Option<Vec<String>>,
|
|
591 |
+ |
/// How long it runs, where the column is on.
|
|
592 |
+ |
duration: Option<String>,
|
|
593 |
+ |
/// Beats per minute, where the column is on.
|
|
594 |
+ |
bpm: Option<String>,
|
|
595 |
+ |
/// The musical key, where the column is on.
|
|
596 |
+ |
key: Option<String>,
|
|
597 |
+ |
/// Peak level, where the column is on.
|
|
598 |
+ |
peak: Option<String>,
|
|
599 |
+ |
/// Whatever it is tagged with, where the column is on.
|
|
600 |
+ |
tags: Option<Vec<String>>,
|
|
601 |
+ |
/// Whether there is anything to play.
|
|
602 |
+ |
///
|
|
603 |
+ |
/// A folder has nothing. The cell is said anyway rather than left out: the
|
|
604 |
+ |
/// Play column is always on, and empty rather than absent is the same answer
|
|
605 |
+ |
/// the analysis cells already give for a folder.
|
|
606 |
+ |
playable: bool,
|
|
607 |
+ |
/// Whether the reader has chosen this row.
|
|
608 |
+ |
chosen: bool,
|
|
609 |
+ |
/// Whether the app is pointing at it.
|
|
610 |
+ |
current: bool,
|
|
611 |
+ |
/// Which menu a press on it answers with.
|
|
612 |
+ |
///
|
|
613 |
+ |
/// Which menu a row offers is which subject the press is about. A row inside
|
|
614 |
+ |
/// a multi-selection is not being asked about itself, and the shipped list
|
|
615 |
+ |
/// said so at the same fork.
|
|
616 |
+ |
menu: Menu,
|
|
617 |
+ |
/// Whether the bytes are only in the cloud.
|
|
618 |
+ |
cloud_only: bool,
|
|
619 |
+ |
/// What the basket entry reads, which says which way it goes.
|
|
620 |
+ |
basket: &'static str,
|
|
621 |
+ |
/// What reveal is called on this host.
|
|
622 |
+ |
reveal: &'static str,
|
|
623 |
+ |
/// What deleting it asks before it runs.
|
|
624 |
+ |
confirm: String,
|
|
625 |
+ |
}
|
|
626 |
+ |
|
|
627 |
+ |
/// Which menu a row answers a press with.
|
|
628 |
+ |
enum Menu {
|
|
629 |
+ |
/// The row's own, for a folder.
|
|
630 |
+ |
Folder,
|
|
631 |
+ |
/// The row's own, for a sample.
|
|
632 |
+ |
Sample,
|
|
633 |
+ |
/// What the chosen set offers, for a row inside a multi-selection.
|
|
634 |
+ |
Chosen,
|
|
635 |
+ |
}
|
|
636 |
+ |
|
|
637 |
+ |
/// What the collection and copy-tags entries need.
|
|
638 |
+ |
struct Shared {
|
|
639 |
+ |
/// The collections a sample can be put into.
|
|
640 |
+ |
collections: Vec<Choice>,
|
|
641 |
+ |
/// Whether there are any.
|