Storage: share the card with a phone's browser (#88)
CI / build (pull_request) Successful in 1m51s
Site / build (pull_request) Successful in 16s

`w` in the Storage App starts a small HTTP server and shows its address,
as a QR code and in letters, with a six-digit code. A browser on the same
network that has typed the code can list, download, upload, make folders
and delete, under the Storage App's rules. The server runs only while that
screen is open. Nothing is encrypted, and the screen says so.

Uploads are streamed to the card under a temporary name and renamed when
whole. Every access to the card is handed to the storage task, 8 KB at a
time, from the server's own task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-08 00:11:55 +02:00
co-authored by Claude Opus 5.5
parent c35bc47693
commit 6b02cd3d5f
16 changed files with 934 additions and 5 deletions
+56
View File
@@ -48,6 +48,7 @@ void StorageApp::onEnter() {
}
void StorageApp::onExit() {
share_.stop(); // sharing lasts as long as its screen
if (wait_ != Wait::None && wait_ != Wait::Work) {
ops_.cancel();
wait_ = Wait::None;
@@ -97,6 +98,13 @@ bool StorageApp::work(const std::string& why, const std::string& selectAfter) {
void StorageApp::update(uint32_t nowMs) {
bool present = storage_.state().present;
if (view_ == View::Share) {
if (!present || !share_.running()) {
share_.stop();
view_ = View::Browse;
}
if (nowMs - lastDrawMs_ >= 500) requestRedraw(); // what the browser is doing
}
if (present != cardPresent_) { // taken out, put in, or erased: back to the top
cardPresent_ = present;
cwd_ = want_ = "/";
@@ -281,6 +289,7 @@ void StorageApp::help(std::vector<KeyHelp>& out) const {
case View::Editor: return noteEditor_.help(out);
case View::Viewer: return viewer_.help(out);
case View::Details: return keys::add(out, keys::kStorageDetails);
case View::Share: return keys::add(out, keys::kStorageShare);
default: break;
}
if (dialog_) return keys::add(out, keys::kDialog);
@@ -295,6 +304,7 @@ const char* StorageApp::helpTitle() const {
case View::Editor: return "Storage: the editor";
case View::Viewer: return "Storage: a file";
case View::Details: return "Storage: details";
case View::Share: return "Storage: sharing";
case View::Name: return "Storage: a name";
default: return nullptr;
}
@@ -303,6 +313,14 @@ const char* StorageApp::helpTitle() const {
bool StorageApp::onKey(const KeyEvent& e) {
requestRedraw();
if (view_ == View::NoMemory) return false;
if (view_ == View::Share) {
if (e.key == Key::Back) {
share_.stop();
view_ = View::Browse;
open(cwd_); // what was uploaded or deleted meanwhile
}
return true;
}
if (view_ == View::Maintenance) {
if (!maintenance_.onKey(e)) {
view_ = View::Browse;
@@ -413,6 +431,12 @@ bool StorageApp::onBrowseKey(const KeyEvent& e) {
}
uint32_t ch = e.key == Key::Char ? (e.ch >= 'A' && e.ch <= 'Z' ? e.ch + 32 : e.ch) : 0;
if (ch == 'm') askMaintenance();
if (ch == 'w') {
std::string why = share_.start();
if (why.empty()) view_ = View::Share;
else say(why);
return true;
}
if (!cardPresent_ || !loaded_) return true;
Item item = selected();
@@ -507,6 +531,7 @@ void StorageApp::draw(Canvas& c) {
c.drawString("in Gemini) and open Storage again.", 4, area.y + 22 + theme::kLineHeight);
return;
case View::Maintenance: maintenance_.draw(c); return;
case View::Share: drawShare(c); return;
case View::Viewer: viewer_.draw(c); return;
case View::Editor: noteEditor_.draw(c); return;
case View::Details: {
@@ -539,6 +564,37 @@ void StorageApp::draw(Canvas& c) {
}
}
// The address as a QR code and in letters, the code to type, and what the browser has done.
void StorageApp::drawShare(Canvas& c) {
const auto& area = theme::kContent;
std::string url = share_.url(), code = share_.code();
const int qr = 104;
c.fillRect(2, area.y + 3, qr + 6, qr + 6, 0xFFFF); // a QR code wants a quiet border
c.qrcode((url + "#" + code).c_str(), 5, area.y + 6, qr);
int x = qr + 14, y = area.y + 4;
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.drawString("In a browser, on", x, y);
c.drawString("this network:", x, y + 10);
c.setTextColor(theme::kText);
c.drawString(url.size() > 7 ? url.substr(7).c_str() : url.c_str(), x, y + 24); // without http://
c.setTextColor(theme::kMuted);
c.drawString("Code", x, y + 40);
c.setFont(&fonts::bold);
c.setTextColor(theme::kAccent);
std::string grouped = code.size() == 6 ? code.substr(0, 3) + " " + code.substr(3) : code;
c.drawString(grouped.c_str(), x, y + 50);
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
std::string moved = formatBytes(share_.bytesIn()) + " in, " + formatBytes(share_.bytesOut()) + " out";
c.drawString(share_.requests() ? moved.c_str() : "Nothing asked yet", x, y + 72);
c.drawString("Not encrypted.", x, y + 84);
c.drawString("` stops sharing", x, y + 96);
c.setTextColor(theme::kText);
std::string last = share_.last();
if (!last.empty()) c.drawString(last.c_str(), 4, area.y + area.h - 9);
}
void StorageApp::drawBrowse(Canvas& c) {
const auto& area = theme::kContent;
StorageState card = storage_.state();
+6 -3
View File
@@ -16,6 +16,7 @@
#include "services/clock_service.h"
#include "services/file_ops.h"
#include "services/storage_service.h"
#include "services/web_share.h"
#include "ui/theme.h"
namespace roro {
@@ -26,8 +27,8 @@ namespace roro {
// usage, Storage Clean-up and erasing the card, behind a warning (Q128).
class StorageApp : public App {
public:
StorageApp(FileOps& ops, StorageService& storage, ClockService& clock, UpdateService& update, PowerService& power, EventBus& bus)
: ops_(ops), storage_(storage), bus_(bus), maintenance_(storage, clock, bus), viewer_(storage, update), noteEditor_(storage, clock, power) {}
StorageApp(FileOps& ops, StorageService& storage, ClockService& clock, UpdateService& update, PowerService& power, EventBus& bus, WebShare& share)
: ops_(ops), storage_(storage), bus_(bus), share_(share), maintenance_(storage, clock, bus), viewer_(storage, update), noteEditor_(storage, clock, power) {}
void onEnter() override;
void onExit() override;
bool onKey(const KeyEvent& e) override;
@@ -40,7 +41,7 @@ class StorageApp : public App {
const char* helpTitle() const override;
private:
enum class View { Browse, Details, Name, Viewer, Editor, Maintenance, NoMemory };
enum class View { Browse, Details, Name, Viewer, Editor, Maintenance, NoMemory, Share };
enum class Ask { None, Delete, Replace, Maintenance };
enum class Wait { None, List, CountToDelete, CountForDetails, Work }; // what the running operation is for
static constexpr int kRows = 8;
@@ -66,10 +67,12 @@ class StorageApp : public App {
void say(const std::string& text);
void drawBrowse(Canvas& c);
void drawProgress(Canvas& c);
void drawShare(Canvas& c); // `w`: the card in a browser on the same network (issue #88)
FileOps& ops_;
StorageService& storage_;
EventBus& bus_;
WebShare& share_;
MaintenancePage maintenance_;
FileViewer viewer_;
NoteEditor noteEditor_; // `e` in the text viewer (Q146)