Files
roro9stack/src/apps/maintenance_page.cpp
T
twislaandClaude Opus 5.5 34e6714785
CI / build (pull_request) Successful in 7m14s
Site / build (pull_request) Successful in 8s
Keys: one table file for the device's help panel and the website's key tables (#72)
Every screen's keys are constant tables in lib/core/src/app_keys.h (52 of
them, each under an `// id: Title` comment). An App's help() picks the table
of the state it is in. site/tools/gen_dev_docs.py reads the same file and
writes site/data/keys.toml; the `keys` shortcode shows a screen's tables on
its guide page, and /guide/keys/ shows all of them.

The Site job fails when the data file is out of date or a page asks for a
table that doesn't exist, and now also runs when app_keys.h changes. A key
added to an App shows up on the website without anyone editing a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-07 01:42:48 +02:00

178 lines
6.5 KiB
C++

#include "app_keys.h"
#include "maintenance_page.h"
#include "ui/widgets.h"
namespace roro {
namespace {
constexpr int kAges = sizeof(kCleanupAgeLabels) / sizeof(kCleanupAgeLabels[0]);
constexpr int kCategories = sizeof(kCleanupCategories) / sizeof(kCleanupCategories[0]);
} // namespace
void MaintenancePage::enter() {
view_ = View::Main;
dialog_.reset();
main_.setCount(kRows);
main_.select(kCleanUp);
}
void MaintenancePage::warn(const char* text) {
bus_.publish(Event::withText(EventType::Notification, text, static_cast<int32_t>(NotificationLevel::Warning)));
}
uint64_t MaintenancePage::categoryBytes(int category) const {
uint64_t sum = 0;
if (category < static_cast<int>(listing_.size()))
for (auto& f : listing_[category]) sum += f.bytes;
return sum;
}
CleanupPlan MaintenancePage::planFor(int age) const {
int today = clock_.today();
auto cutoff = static_cast<CleanupAge>(age);
// Without a clock only "Everything" can be judged.
if (today < 0 && cutoff != CleanupAge::Everything) return {};
return CleanupPlan::make(listing_[categories_.selected()], cutoff, today);
}
void MaintenancePage::help(std::vector<KeyHelp>& out) const {
if (dialog_) return keys::add(out, keys::kDialog);
keys::add(out, keys::kMaintenance);
}
bool MaintenancePage::onKey(const KeyEvent& e) {
if (dialog_) {
dialog_->onKey(e);
int result = dialog_->result();
if (result == 1) {
if (view_ == View::Main) {
storage_.requestFormat();
} else {
storage_.requestDelete(pending_.paths);
enter();
}
}
if (result != DialogModel::kPending) dialog_.reset();
return true;
}
switch (view_) {
case View::Main:
switch (e.key) {
case Key::Up: main_.up(); break;
case Key::Down: main_.down(); break;
case Key::Back: return false;
case Key::Select:
if (!storage_.state().present) {
warn("No SD card");
} else if (main_.selected() == kCleanUp) {
storage_.requestListing();
haveListing_ = false;
categories_.setCount(kCategories);
view_ = View::Categories;
} else if (main_.selected() == kErase) {
dialog_.reset(new DialogModel({"Cancel", "Erase"}));
}
break;
default: break;
}
return true;
case View::Categories:
switch (e.key) {
case Key::Up: categories_.up(); break;
case Key::Down: categories_.down(); break;
case Key::Back: view_ = View::Main; break;
case Key::Select:
if (!haveListing_) break;
if (listing_[categories_.selected()].empty()) {
warn("Nothing to clean up here");
break;
}
ages_.setCount(kAges);
view_ = View::Ages;
break;
default: break;
}
return true;
case View::Ages:
switch (e.key) {
case Key::Up: ages_.up(); break;
case Key::Down: ages_.down(); break;
case Key::Back: view_ = View::Categories; break;
case Key::Select:
pending_ = planFor(ages_.selected());
if (pending_.paths.empty()) warn(clock_.today() < 0 ? "Clock not set: only Everything works"
: "No files that old");
else dialog_.reset(new DialogModel({"Cancel", "Delete"}));
break;
default: break;
}
return true;
}
return true;
}
void MaintenancePage::draw(Canvas& c) {
const auto& area = theme::kContent;
if (!haveListing_ && storage_.listingReady()) {
listing_ = storage_.listing();
haveListing_ = true;
}
switch (view_) {
case View::Main: {
auto s = storage_.state();
widgets::list(
c, main_, area,
[](int i) -> std::string {
const char* labels[] = {"SD card", "Logs", "Captures", "Clean up", "Erase SD card"};
return labels[i];
},
[&](int i) -> std::string {
if (storage_.formatting() && i == kCard) return "erasing...";
if (!s.present) return i == kCard ? "none" : "";
switch (i) {
case kCard: return formatBytes(s.totalBytes) + ", " + std::to_string(s.usedPercent) + "% used";
case kLogs: return s.logsAllowed ? "recording" : "paused (over 90%)";
case kCaptures: return s.capturesAllowed ? "allowed" : "stopped (full)";
default: return ">";
}
});
if (dialog_)
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
*dialog_);
break;
}
case View::Categories:
if (!haveListing_) {
widgets::textLines(c, {"Listing files..."}, 0, area);
break;
}
widgets::list(
c, categories_, area, [](int i) { return std::string(kCleanupCategories[i].label); },
[this](int i) {
return std::to_string(listing_[i].size()) + " files, " + formatBytes(categoryBytes(i));
});
break;
case View::Ages:
widgets::list(
c, ages_, area, [](int i) { return std::string(kCleanupAgeLabels[i]); },
[this](int i) {
auto plan = planFor(i);
return plan.paths.empty() ? std::string("-") : formatBytes(plan.bytes);
});
if (dialog_) {
std::string message = "Delete " + std::to_string(pending_.paths.size()) + " files from " +
kCleanupCategories[categories_.selected()].label + ", freeing " +
formatBytes(pending_.bytes) + "?";
widgets::dialog(c, "Clean up", message, *dialog_);
}
break;
}
}
} // namespace roro