Public Access
Storage: share the card with a phone's browser (#88)
`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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user