Public Access
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
178 lines
6.5 KiB
C++
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
|