Skip to main content

max / audiofiles

Declare the trash section The third and last of the settings sections that took the body and handed it back. Same answer: a Vec<Node> the screen extends. The read is where the work is, as it was in every wave-14 file. `Bin` holds the retention window and its agreeing noun, and `Gone` holds one tombstone in the words the row uses: the size and age already joined, and the head of the hash as an Option rather than as a length test the description would have to repeat. No production earned. The empty case and the list are a dispatch, the badge is a loop over an Option, and the purge confirmation interpolates the retention window it is asking the reader to skip.
Author: Max Johnson <me@maxj.phd> · 2026-09-04 20:40 UTC
Signed with PGP, not checked
Commit: 4a86101fcf30fc19494105543a82277e92cb0bed
Parent: 1b8680b
2 files changed, +74 insertions, -45 deletions
@@ -194,7 +194,7 @@
194 194 .with(Node::Field(Box::new(row_height(state)?)));
195 195
196 196 body = super::storage::section(body, state);
197 - body = super::trash::section(body, state);
197 + body = body.extend(super::trash::section(&super::trash::read(state)));
198 198 body = body.extend(super::licence::section(&super::licence::read(state)));
199 199
200 200 // The classifier's door. Max ruled it a window of its own; what stays here
@@ -45,8 +45,8 @@
45 45 //! [`Act::confirm`]: quasi_router::Act::confirm
46 46 //! [`Trash::retain_days`]: super::Trash::retain_days
47 47
48 - use quasi_router::layout::Tone;
49 - use quasi_router::{Act, Action, Node, Request, Response, RouteError, Router, Row, Slot, Tag};
48 + use quasi_declare::declare;
49 + use quasi_router::{Request, Response, RouteError, Router, Tag};
50 50
51 51 use super::Panels;
52 52
@@ -88,53 +88,82 @@
88 88 Ok(super::settings::screen(state)?.into())
89 89 }
90 90
91 - /// The whole section, added to the settings body.
92 - pub(super) fn section(body: Slot, state: &Panels<'_>) -> Slot {
91 + /// What the section draws, read off the app once.
92 + pub(super) struct Bin {
93 + /// How long a deleted sample is kept, in days.
94 + days: i64,
95 + /// "day" or "days", to agree with `days` in the sentence above the list.
96 + unit: &'static str,
97 + /// What is in there now, most recently deleted first.
98 + gone: Vec<Gone>,
99 + }
100 +
101 + /// One tombstoned sample, in the words the row uses.
102 + struct Gone {
103 + /// Its content address, which is what both acts carry.
104 + hash: String,
105 + /// The name a reader sees.
106 + filename: String,
107 + /// Size and age, on the line under the name.
108 + meta: String,
109 + /// The head of the hash, when the hash has a head worth showing.
110 + short: Option<String>,
111 + }
112 +
113 + /// What the section draws, read off the app.
114 + pub(super) fn read(state: &Panels<'_>) -> Bin {
93 115 let days = state.trash.retain_days();
94 - let body = body
95 - .with(Node::section("Trash"))
96 - .with(Node::text(format!(
97 - "Deleted samples are kept here for {days} {}, then removed permanently. Restoring brings a sample back on all your devices.",
98 - if days == 1 { "day" } else { "days" },
99 - )));
100 -
101 - let deleted = state.trash.deleted();
102 - if deleted.is_empty() {
103 - return body.with(Node::empty("Trash is empty."));
116 + Bin {
117 + days,
118 + unit: if days == 1 { "day" } else { "days" },
119 + gone: state
120 + .trash
121 + .deleted()
122 + .iter()
123 + .map(|entry| Gone {
124 + hash: entry.hash.clone(),
125 + filename: entry.filename(),
126 + meta: format!(
127 + "{} \u{b7} {}",
128 + crate::ui::widgets::format_bytes(entry.size_bytes),
129 + deleted_age(entry.age_secs),
130 + ),
131 + short: (entry.hash.len() >= SHOWN).then(|| entry.hash[..SHOWN].to_owned()),
132 + })
133 + .collect(),
104 134 }
135 + }
105 136
106 - let rows = deleted
107 - .iter()
108 - .map(|entry| {
109 - let mut row = Row::new(entry.filename()).meta(format!(
110 - "{} \u{b7} {}",
111 - crate::ui::widgets::format_bytes(entry.size_bytes),
112 - deleted_age(entry.age_secs),
113 - ));
137 + declare! {
138 + /// The whole section, spliced into the settings body.
139 + pub(super) shape section(trash: &Bin) -> Vec<Node>;
114 140
115 - if entry.hash.len() >= SHOWN {
116 - row = row.token(Tag::badge(&entry.hash[..SHOWN]));
141 + section "Trash";
142 + text "Deleted samples are kept here for {trash.days} {trash.unit}, then removed \
143 + permanently. Restoring brings a sample back on all your devices.";
144 +
145 + given trash.gone.is_empty() {
146 + true -> empty "Trash is empty.";
147 + otherwise -> list {
148 + for entry in trash.gone.iter() {
149 + row &entry.filename {
150 + meta &entry.meta;
151 +
152 + for short in entry.short.iter() {
153 + token Tag::badge(short);
154 + }
155 +
156 + act "Restore" to post "/settings/trash/{entry.hash}/restore";
157 +
158 + act "Delete permanently" to post "/settings/trash/{entry.hash}/purge" {
159 + tone Danger;
160 + confirm "Remove \"{entry.filename}\" now, skipping the \
161 + {trash.days}-day window? This cannot be undone.";
162 + }
163 + }
117 164 }
118 -
119 - row.act(Act::new(
120 - "Restore",
121 - Action::post(format!("/settings/trash/{}/restore", entry.hash)),
122 - ))
123 - .act(
124 - Act::new(
125 - "Delete permanently",
126 - Action::post(format!("/settings/trash/{}/purge", entry.hash)),
127 - )
128 - .tone(Tone::Danger)
129 - .confirm(format!(
130 - "Remove \"{}\" now, skipping the {days}-day window? This cannot be undone.",
131 - entry.filename(),
132 - )),
133 - )
134 - })
135 - .collect();
136 -
137 - body.with(Node::List { rows, more: None })
165 + }
166 + }
138 167 }
139 168
140 169 /// How long ago a sample was deleted.