max / audiofiles
2 files changed,
+273 insertions,
-200 deletions
| @@ -258,7 +258,7 @@ | |||
| 258 | 258 | pub(super) fn showing(state: &Panels<'_>) -> Result<Response, RouteError> { | |
| 259 | 259 | Ok(Response::from(screen( | |
| 260 | 260 | &read(state)?, | |
| 261 | - | super::storage::section(state), | |
| 261 | + | super::storage::section(&super::storage::read(state)), | |
| 262 | 262 | super::trash::section(&super::trash::read(state)), | |
| 263 | 263 | super::licence::section(&super::licence::read(state)), | |
| 264 | 264 | super::advanced::section(state.advanced.mirror().as_ref()), |
| @@ -78,11 +78,9 @@ | |||
| 78 | 78 | //! [`Outcome::Locate`]: quasi_router::Outcome::Locate | |
| 79 | 79 | //! [`Row::activate`]: quasi_router::Row::activate | |
| 80 | 80 | ||
| 81 | - | use quasi_router::layout::{FieldKind, Tone}; | |
| 82 | - | use quasi_router::{ | |
| 83 | - | Act, Action, Choice, Field, Locating, Node, Outcome, Request, Response, RouteError, Router, | |
| 84 | - | Row, Tag, | |
| 85 | - | }; | |
| 81 | + | use quasi_declare::declare; | |
| 82 | + | use quasi_router::layout::Tone; | |
| 83 | + | use quasi_router::{Action, Choice, Locating, Outcome, Request, Response, RouteError, Router, Tag}; | |
| 86 | 84 | ||
| 87 | 85 | use super::Panels; | |
| 88 | 86 | ||
| @@ -301,125 +299,62 @@ | |||
| 301 | 299 | .map_err(|_| RouteError::not_found("that is not a row")) | |
| 302 | 300 | } | |
| 303 | 301 | ||
| 304 | - | /// The whole section, spliced into the settings body. | |
| 305 | - | /// | |
| 306 | - | /// Nodes rather than a `Slot` handed in and handed back, which is the shape a | |
| 307 | - | /// declaration is refused and the shape the settings screen cannot splice now | |
| 308 | - | /// that it is one. The three sections beside this one made the same move. | |
| 309 | - | pub(super) fn section(state: &Panels<'_>) -> Vec<Node> { | |
| 310 | - | let mut body = vec![ | |
| 311 | - | Node::section("Storage"), | |
| 312 | - | Node::text( | |
| 313 | - | "Each library is an independent sample collection with its own database and files. A library can contain multiple vaults (top-level browse buckets).", | |
| 314 | - | ), | |
| 315 | - | libraries(state), | |
| 316 | - | ]; | |
| 317 | - | ||
| 318 | - | if let Some(at) = state.storage.renaming() { | |
| 319 | - | body.push(rename_form(at, state)); | |
| 320 | - | } | |
| 321 | - | ||
| 322 | - | body.extend(maintenance(state)); | |
| 323 | - | ||
| 324 | - | if state.storage.loose_files() { | |
| 325 | - | body.push(Node::Text { | |
| 326 | - | text: "This library uses loose-files mode. Samples are referenced in place, not duplicated.".to_owned(), | |
| 327 | - | tone: Tone::Warning, | |
| 328 | - | }); | |
| 329 | - | } | |
| 330 | - | ||
| 331 | - | body.extend(add_library(state)); | |
| 332 | - | body | |
| 302 | + | /// What the Storage section draws, read off the app. | |
| 303 | + | pub(super) struct Storage { | |
| 304 | + | /// The libraries, one row each. | |
| 305 | + | libraries: Vec<Library>, | |
| 306 | + | /// The inline rename, against whichever row is being renamed. | |
| 307 | + | renaming: Option<Renaming>, | |
| 308 | + | /// Scan, and the three maintenance passes over the open library. | |
| 309 | + | maintenance: Maintenance, | |
| 310 | + | /// Whether the open library references samples in place. | |
| 311 | + | loose: bool, | |
| 312 | + | /// The Add Library form. | |
| 313 | + | draft: Draft, | |
| 333 | 314 | } | |
| 334 | 315 | ||
| 335 | - | /// The libraries, one row each. | |
| 336 | - | fn libraries(state: &Panels<'_>) -> Node { | |
| 337 | - | let all = state.storage.libraries(); | |
| 338 | - | let scan = state.storage.scan(); | |
| 339 | - | let interrupting = state.storage.interrupting(); | |
| 340 | - | let mut rows = Vec::with_capacity(all.len()); | |
| 316 | + | /// One library, as the row it is. | |
| 317 | + | struct Library { | |
| 318 | + | /// What it is called. | |
| 319 | + | name: String, | |
| 320 | + | /// Its directory, with the home prefix collapsed the way the reader saw it. | |
| 321 | + | shown: String, | |
| 322 | + | /// What the row is badged with, where it is badged with anything. | |
| 323 | + | badge: Option<&'static str>, | |
| 324 | + | /// The one extra line the row carries. | |
| 325 | + | /// | |
| 326 | + | /// At most one, and structurally rather than by luck: an offline row says | |
| 327 | + | /// its last-known path and the open row says what the scan counted, and no | |
| 328 | + | /// row is both. The offline row's path was a hover in the shipped panel; a | |
| 329 | + | /// hover is a host's, so it is said outright and a terminal or a screen | |
| 330 | + | /// reader gets to keep it. | |
| 331 | + | meta: Option<String>, | |
| 332 | + | /// Where it sits, which is what every one of its addresses is built from. | |
| 333 | + | at: usize, | |
| 334 | + | /// Switching to it, where it can be switched to. | |
| 335 | + | open: Option<Open>, | |
| 336 | + | /// Whether it can be pointed at a new directory. | |
| 337 | + | locatable: bool, | |
| 338 | + | /// Whether it can be forgotten. | |
| 339 | + | forgettable: bool, | |
| 340 | + | } | |
| 341 | 341 | ||
| 342 | - | for (at, entry) in all.iter().enumerate() { | |
| 343 | - | // The offline row's badge carried the last-known path on hover in the | |
| 344 | - | // shipped panel. A hover is a host's; the path is said outright here, | |
| 345 | - | // which is what a terminal or a screen reader gets to keep. | |
| 346 | - | let mut row = Row::new(&entry.name).secondary(entry.shown.clone()); | |
| 347 | - | if entry.active { | |
| 348 | - | row = row.token(Tag::badge("active")); | |
| 349 | - | } else if !entry.reachable { | |
| 350 | - | row = row | |
| 351 | - | .token(Tag::badge("offline")) | |
| 352 | - | .meta(format!("Last known path: {}", entry.path)); | |
| 353 | - | } | |
| 354 | - | ||
| 355 | - | // Only the open library has been counted: the scan reads the database | |
| 356 | - | // that is open, and the shipped rows were path-only for the rest. | |
| 357 | - | if entry.active | |
| 358 | - | && let Some(scan) = scan | |
| 359 | - | { | |
| 360 | - | row = row.meta(format!( | |
| 361 | - | "{} samples \u{b7} {}", | |
| 362 | - | scan.samples, | |
| 363 | - | bytes(scan.total_bytes), | |
| 364 | - | )); | |
| 365 | - | } | |
| 366 | - | ||
| 367 | - | let switchable = !entry.active && entry.reachable; | |
| 368 | - | if switchable { | |
| 369 | - | let mut act = Act::new("Open", Action::post(format!("/settings/storage/open/{at}"))); | |
| 370 | - | if interrupting { | |
| 371 | - | act = act.confirm(INTERRUPT); | |
| 372 | - | } else { | |
| 373 | - | // See the module header: the click is only offered where it | |
| 374 | - | // cannot skip an asking. | |
| 375 | - | row = row.activate(Action::post(format!("/settings/storage/open/{at}"))); | |
| 376 | - | } | |
| 377 | - | row = row.act(act); | |
| 378 | - | } | |
| 379 | - | ||
| 380 | - | row = row.act(Act::new( | |
| 381 | - | "Rename", | |
| 382 | - | Action::post(format!("/settings/storage/rename/{at}")), | |
| 383 | - | )); | |
| 384 | - | ||
| 385 | - | if !entry.active && !entry.reachable { | |
| 386 | - | row = row.act(Act::new( | |
| 387 | - | "Locate", | |
| 388 | - | Action::post(format!("/settings/storage/locate/{at}")), | |
| 389 | - | )); | |
| 390 | - | } | |
| 391 | - | ||
| 392 | - | if !entry.active { | |
| 393 | - | row = row.act( | |
| 394 | - | Act::new( | |
| 395 | - | "Remove", | |
| 396 | - | Action::post(format!("/settings/storage/forget/{at}")), | |
| 397 | - | ) | |
| 398 | - | .tone(Tone::Danger) | |
| 399 | - | .confirm(FORGET), | |
| 400 | - | ); | |
| 401 | - | } | |
| 402 | - | ||
| 403 | - | rows.push(row); | |
| 404 | - | } | |
| 405 | - | ||
| 406 | - | Node::List { rows, more: None } | |
| 342 | + | /// Switching to a library that is not the open one. | |
| 343 | + | struct Open { | |
| 344 | + | /// Whether the switch would interrupt a running job, which is what makes it | |
| 345 | + | /// ask first. | |
| 346 | + | /// | |
| 347 | + | /// The press is offered on the row itself only where it cannot skip an | |
| 348 | + | /// asking. See the module header. | |
| 349 | + | interrupting: bool, | |
| 407 | 350 | } | |
| 408 | 351 | ||
| 409 | 352 | /// The inline rename, against whichever row is being renamed. | |
| 410 | - | fn rename_form(at: usize, state: &Panels<'_>) -> Node { | |
| 411 | - | let current = state | |
| 412 | - | .storage | |
| 413 | - | .libraries() | |
| 414 | - | .get(at) | |
| 415 | - | .map(|entry| entry.name.clone()) | |
| 416 | - | .unwrap_or_default(); | |
| 417 | - | ||
| 418 | - | Node::Form { | |
| 419 | - | fields: vec![Field::new(FieldKind::Text, NAME, "New name").value(current)], | |
| 420 | - | submit: "Save".to_owned(), | |
| 421 | - | action: Action::post(format!("/settings/storage/rename/{at}/save")), | |
| 422 | - | } | |
| 353 | + | struct Renaming { | |
| 354 | + | /// Where that row sits. | |
| 355 | + | at: usize, | |
| 356 | + | /// What it is called now. | |
| 357 | + | current: String, | |
| 423 | 358 | } | |
| 424 | 359 | ||
| 425 | 360 | /// Scan, and the three maintenance passes over the open library. | |
| @@ -428,145 +363,294 @@ | |||
| 428 | 363 | /// swapping its label, which is the shipped busy state minus the spinner: a | |
| 429 | 364 | /// spinner is a renderer's way of drawing "working", and every host has one or | |
| 430 | 365 | /// has something better. | |
| 431 | - | fn maintenance(state: &Panels<'_>) -> Vec<Node> { | |
| 432 | - | let scanning = state.storage.scanning(); | |
| 433 | - | let mut scan_act = Act::new( | |
| 434 | - | if scanning { "Scanning..." } else { "Scan" }, | |
| 435 | - | Action::post("/settings/storage/scan"), | |
| 436 | - | ); | |
| 437 | - | if scanning { | |
| 438 | - | scan_act = scan_act.disabled(); | |
| 439 | - | } | |
| 440 | - | let mut body = vec![Node::Act(scan_act)]; | |
| 366 | + | struct Maintenance { | |
| 367 | + | /// What the scan control reads. | |
| 368 | + | scan: &'static str, | |
| 369 | + | /// Whether a scan is running. | |
| 370 | + | scanning: bool, | |
| 371 | + | /// What the last scan counted, where one has run. | |
| 372 | + | counted: Option<Counted>, | |
| 373 | + | /// What the backfill control reads. | |
| 374 | + | backfill: &'static str, | |
| 375 | + | /// Whether the feature backfill is running. | |
| 376 | + | backfilling: bool, | |
| 377 | + | /// Whether the maintenance worker is busy, which is what the integrity check | |
| 378 | + | /// shares its flag with. | |
| 379 | + | busy: bool, | |
| 380 | + | } | |
| 441 | 381 | ||
| 442 | - | if let Some(scan) = state.storage.scan() { | |
| 443 | - | body.push(Node::text(format!( | |
| 444 | - | "{} samples, {} total, {} database", | |
| 445 | - | scan.samples, | |
| 446 | - | bytes(scan.total_bytes), | |
| 447 | - | bytes(scan.db_bytes), | |
| 448 | - | ))); | |
| 449 | - | let (age, stale) = scan_age(scan.age_secs); | |
| 450 | - | body.push(Node::Text { | |
| 451 | - | text: age, | |
| 452 | - | tone: if stale { Tone::Warning } else { Tone::Neutral }, | |
| 453 | - | }); | |
| 454 | - | } | |
| 455 | - | ||
| 456 | - | body.push(Node::text( | |
| 457 | - | "Free disk by deleting samples no longer referenced anywhere in the library. Local-only: other synced devices keep their own copies.", | |
| 458 | - | )); | |
| 459 | - | body.push(Node::Act(Act::new( | |
| 460 | - | "Cleanup orphans", | |
| 461 | - | Action::post("/settings/storage/orphans"), | |
| 462 | - | ))); | |
| 463 | - | ||
| 464 | - | let backfilling = state.storage.backfilling(); | |
| 465 | - | let mut backfill = Act::new( | |
| 466 | - | if backfilling { | |
| 467 | - | "Backfilling audio features..." | |
| 468 | - | } else { | |
| 469 | - | "Backfill audio features" | |
| 470 | - | }, | |
| 471 | - | Action::post("/settings/storage/backfill"), | |
| 472 | - | ); | |
| 473 | - | if backfilling { | |
| 474 | - | backfill = backfill.disabled(); | |
| 475 | - | } | |
| 476 | - | body.push(Node::text( | |
| 477 | - | "Compute the audio feature data used by tag suggestions for samples that don't have it yet. Runs in the background: keep working; it yields to any analysis you start and resumes later.", | |
| 478 | - | )); | |
| 479 | - | body.push(Node::Act(backfill)); | |
| 480 | - | ||
| 481 | - | let mut integrity = Act::new( | |
| 482 | - | "Verify library integrity", | |
| 483 | - | Action::post("/settings/storage/verify"), | |
| 484 | - | ); | |
| 485 | - | if state.storage.busy() { | |
| 486 | - | integrity = integrity.disabled(); | |
| 487 | - | } | |
| 488 | - | body.push(Node::text( | |
| 489 | - | "Re-hash every stored sample and confirm its bytes still match its content address. Catches silent on-disk corruption. Runs in the background: the result appears in the status line.", | |
| 490 | - | )); | |
| 491 | - | body.push(Node::Act(integrity)); | |
| 492 | - | body | |
| 382 | + | /// What the last scan counted, and how fresh the numbers are. | |
| 383 | + | struct Counted { | |
| 384 | + | /// The three totals. | |
| 385 | + | totals: String, | |
| 386 | + | /// How long ago it ran. | |
| 387 | + | age: String, | |
| 388 | + | /// Whether that is long enough to say so. | |
| 389 | + | tone: Tone, | |
| 493 | 390 | } | |
| 494 | 391 | ||
| 495 | 392 | /// The Add Library form: a name, a folder, and how samples are stored. | |
| 496 | - | fn add_library(state: &Panels<'_>) -> Vec<Node> { | |
| 497 | - | let draft = state.storage.draft(); | |
| 393 | + | struct Draft { | |
| 394 | + | /// What the new library is to be called. | |
| 395 | + | name: String, | |
| 396 | + | /// The complaint, where a folder is chosen and the name is empty. | |
| 397 | + | /// | |
| 398 | + | /// The error the shipped form left unexplained: Create New disabled itself | |
| 399 | + | /// and said nothing about why. | |
| 400 | + | error: Option<&'static str>, | |
| 401 | + | /// The folder the host picked, where one was picked. | |
| 402 | + | folder: Option<String>, | |
| 403 | + | /// Which storage style is chosen. | |
| 404 | + | style: &'static str, | |
| 405 | + | /// Whether that style is the one that breaks if the originals move. | |
| 406 | + | loose: bool, | |
| 407 | + | /// Whether the form has enough to act on. | |
| 408 | + | ready: bool, | |
| 409 | + | /// Whether it has anything in it to discard. | |
| 410 | + | started: bool, | |
| 411 | + | } | |
| 498 | 412 | ||
| 499 | - | // The error the shipped form left unexplained: a folder chosen and no name, | |
| 500 | - | // where Create New disabled itself and said nothing about why. | |
| 501 | - | let mut name = Field::new(FieldKind::Text, "create_name", "Name") | |
| 502 | - | .value(draft.name.clone()) | |
| 503 | - | .writes(Action::post("/settings/storage/draft/name")); | |
| 504 | - | name.required = true; | |
| 505 | - | if draft.folder.is_some() && draft.name.trim().is_empty() { | |
| 506 | - | name = name.error("A library needs a name."); | |
| 413 | + | /// What the section draws, read off the app. | |
| 414 | + | pub(super) fn read(state: &Panels<'_>) -> Storage { | |
| 415 | + | let all = state.storage.libraries(); | |
| 416 | + | let scan = state.storage.scan(); | |
| 417 | + | let interrupting = state.storage.interrupting(); | |
| 418 | + | let draft = state.storage.draft(); | |
| 419 | + | let scanning = state.storage.scanning(); | |
| 420 | + | let backfilling = state.storage.backfilling(); | |
| 421 | + | Storage { | |
| 422 | + | libraries: all | |
| 423 | + | .iter() | |
| 424 | + | .enumerate() | |
| 425 | + | .map(|(at, entry)| Library { | |
| 426 | + | name: entry.name.clone(), | |
| 427 | + | shown: entry.shown.clone(), | |
| 428 | + | badge: if entry.active { | |
| 429 | + | Some("active") | |
| 430 | + | } else if entry.reachable { | |
| 431 | + | None | |
| 432 | + | } else { | |
| 433 | + | Some("offline") | |
| 434 | + | }, | |
| 435 | + | // Only the open library has been counted: the scan reads the | |
| 436 | + | // database that is open, and the shipped rows were path-only | |
| 437 | + | // for the rest. | |
| 438 | + | meta: match (entry.active, entry.reachable, scan) { | |
| 439 | + | (true, _, Some(scan)) => Some(format!( | |
| 440 | + | "{} samples \u{b7} {}", | |
| 441 | + | scan.samples, | |
| 442 | + | bytes(scan.total_bytes), | |
| 443 | + | )), | |
| 444 | + | (false, false, _) => Some(format!("Last known path: {}", entry.path)), | |
| 445 | + | _ => None, | |
| 446 | + | }, | |
| 447 | + | at, | |
| 448 | + | open: (!entry.active && entry.reachable).then_some(Open { interrupting }), | |
| 449 | + | locatable: !entry.active && !entry.reachable, | |
| 450 | + | forgettable: !entry.active, | |
| 451 | + | }) | |
| 452 | + | .collect(), | |
| 453 | + | renaming: state.storage.renaming().map(|at| Renaming { | |
| 454 | + | at, | |
| 455 | + | current: all | |
| 456 | + | .get(at) | |
| 457 | + | .map(|entry| entry.name.clone()) | |
| 458 | + | .unwrap_or_default(), | |
| 459 | + | }), | |
| 460 | + | maintenance: Maintenance { | |
| 461 | + | scan: if scanning { "Scanning..." } else { "Scan" }, | |
| 462 | + | scanning, | |
| 463 | + | counted: scan.map(|scan| { | |
| 464 | + | let (age, stale) = scan_age(scan.age_secs); | |
| 465 | + | Counted { | |
| 466 | + | totals: format!( | |
| 467 | + | "{} samples, {} total, {} database", | |
| 468 | + | scan.samples, | |
| 469 | + | bytes(scan.total_bytes), | |
| 470 | + | bytes(scan.db_bytes), | |
| 471 | + | ), | |
| 472 | + | age, | |
| 473 | + | tone: if stale { Tone::Warning } else { Tone::Neutral }, | |
| 474 | + | } | |
| 475 | + | }), | |
| 476 | + | backfill: if backfilling { | |
| 477 | + | "Backfilling audio features..." | |
| 478 | + | } else { | |
| 479 | + | "Backfill audio features" | |
| 480 | + | }, | |
| 481 | + | backfilling, | |
| 482 | + | busy: state.storage.busy(), | |
| 483 | + | }, | |
| 484 | + | loose: state.storage.loose_files(), | |
| 485 | + | draft: Draft { | |
| 486 | + | error: (draft.folder.is_some() && draft.name.trim().is_empty()) | |
| 487 | + | .then_some("A library needs a name."), | |
| 488 | + | style: if draft.reference_in_place { | |
| 489 | + | "reference" | |
| 490 | + | } else { | |
| 491 | + | "copy" | |
| 492 | + | }, | |
| 493 | + | loose: draft.reference_in_place, | |
| 494 | + | ready: draft.ready(), | |
| 495 | + | started: draft.started(), | |
| 496 | + | name: draft.name, | |
| 497 | + | folder: draft.folder, | |
| 498 | + | }, | |
| 499 | + | } | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | declare! { | |
| 503 | + | /// The whole section, spliced into the settings body. | |
| 504 | + | /// | |
| 505 | + | /// Nodes rather than a `Slot` handed in and handed back, which is the shape | |
| 506 | + | /// a declaration is refused and the shape the settings screen cannot splice | |
| 507 | + | /// now that it is one. The three sections beside this one made the same | |
| 508 | + | /// move. | |
| 509 | + | pub(super) shape section(storage: &Storage) -> Vec<Node>; | |
| 510 | + | ||
| 511 | + | section "Storage"; | |
| 512 | + | text "Each library is an independent sample collection with its own \ | |
| 513 | + | database and files. A library can contain multiple vaults (top-level \ | |
| 514 | + | browse buckets)."; | |
| 515 | + | include libraries(storage); | |
| 516 | + | ||
| 517 | + | for renaming in storage.renaming.iter() { | |
| 518 | + | include rename_form(renaming); | |
| 507 | 519 | } | |
| 508 | 520 | ||
| 509 | - | let mut body = vec![ | |
| 510 | - | Node::section("Add Library"), | |
| 511 | - | Node::Field(Box::new(name)), | |
| 512 | - | Node::Act(Act::new( | |
| 513 | - | "Choose folder...", | |
| 514 | - | Action::post("/settings/storage/folder"), | |
| 515 | - | )), | |
| 516 | - | ]; | |
| 521 | + | extend maintenance(&storage.maintenance); | |
| 517 | 522 | ||
| 518 | - | if let Some(folder) = &draft.folder { | |
| 519 | - | body.push(Node::text(folder.clone())); | |
| 523 | + | toned "This library uses loose-files mode. Samples are referenced in place, \ | |
| 524 | + | not duplicated." Tone::Warning when storage.loose; | |
| 525 | + | ||
| 526 | + | extend add_library(&storage.draft); | |
| 527 | + | } | |
| 528 | + | ||
| 529 | + | declare! { | |
| 530 | + | /// The libraries, one row each. | |
| 531 | + | shape libraries(storage: &Storage) -> Node; | |
| 532 | + | ||
| 533 | + | list { | |
| 534 | + | for library in storage.libraries.iter() { | |
| 535 | + | row &library.name { | |
| 536 | + | secondary &library.shown; | |
| 537 | + | for &badge in library.badge.iter() { | |
| 538 | + | token Tag::badge(badge); | |
| 539 | + | } | |
| 540 | + | for line in library.meta.iter() { | |
| 541 | + | meta line; | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | for open in library.open.iter() { | |
| 545 | + | // See the module header: the click is only offered where it | |
| 546 | + | // cannot skip an asking. | |
| 547 | + | activate to post "/settings/storage/open/{library.at}" | |
| 548 | + | unless open.interrupting; | |
| 549 | + | act "Open" to post "/settings/storage/open/{library.at}" { | |
| 550 | + | confirm INTERRUPT when open.interrupting; | |
| 551 | + | } | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | act "Rename" to post "/settings/storage/rename/{library.at}"; | |
| 555 | + | act "Locate" to post "/settings/storage/locate/{library.at}" | |
| 556 | + | when library.locatable; | |
| 557 | + | act "Remove" to post "/settings/storage/forget/{library.at}" | |
| 558 | + | when library.forgettable { | |
| 559 | + | tone Danger; | |
| 560 | + | confirm FORGET; | |
| 561 | + | } | |
| 562 | + | } | |
| 563 | + | } | |
| 564 | + | } | |
| 565 | + | } | |
| 566 | + | ||
| 567 | + | declare! { | |
| 568 | + | /// The inline rename, against whichever row is being renamed. | |
| 569 | + | shape rename_form(renaming: &Renaming) -> Node; | |
| 570 | + | ||
| 571 | + | form post "/settings/storage/rename/{renaming.at}/save" { | |
| 572 | + | submit "Save"; | |
| 573 | + | field Text NAME "New name" { | |
| 574 | + | value &renaming.current; | |
| 575 | + | } | |
| 576 | + | } | |
| 577 | + | } | |
| 578 | + | ||
| 579 | + | declare! { | |
| 580 | + | /// Scan, and the three maintenance passes over the open library. See | |
| 581 | + | /// [`Maintenance`]. | |
| 582 | + | shape maintenance(maintenance: &Maintenance) -> Vec<Node>; | |
| 583 | + | ||
| 584 | + | act maintenance.scan to post "/settings/storage/scan" { | |
| 585 | + | disabled when maintenance.scanning; | |
| 586 | + | } | |
| 587 | + | for counted in maintenance.counted.iter() { | |
| 588 | + | text &counted.totals; | |
| 589 | + | toned &counted.age counted.tone; | |
| 590 | + | } | |
| 591 | + | ||
| 592 | + | text "Free disk by deleting samples no longer referenced anywhere in the \ |
Lines truncated