Public Access
The Storage App browses the SD card: folders first with sizes and dates, three sorts, one item at a time with a clipboard (c, x, v), rename, delete after counting what's inside, new folder, details. A listing holds 256 entries and says when a folder has more. FileOps does the card's work for the App and the console alike, one operation at a time on the storage task in turns of about 150 ms, so Logs and Captures are still written during a long copy. A copy shows progress, can be cancelled (what it wrote is taken back) and compares sizes after. The read-only rules are checked there: the firmware's top-level folders, /gemini/cache, and files being written (a Track, a Capture, an upload, today's IRC Logs). A listing reads the folder straight from FatFs: through the Arduino File, 329 entries took over two seconds. Viewers by type: text read a screen at a time whatever the file's size (logs open at the end), a hex dump, a Capture's packets as the LoRa Scanner lists them, a Track's summary, and an Update File checked as an install would check it, without writing anything. Tab shows any file as hex or text. Settings > Storage is gone: usage, Storage Clean-up and Erase are the App's Maintenance, behind a warning. The Storage Warning points there. The Clock sets the system time whatever its source, so files are dated correctly with a GNSS Fix alone (Q137). Console: cp, mv, mkdir, du, cancel; rm takes folders and follows the rules; ls shows dates; Debug Builds get `sd fill`. 424 host tests. Checked on the device: docs/milestones/F1.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
172 lines
6.4 KiB
C++
172 lines
6.4 KiB
C++
#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);
|
|
}
|
|
|
|
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
|