Files
roro9stack/src/services/web_share_page.h
T
twislaandClaude Opus 5.5 6b02cd3d5f
CI / build (pull_request) Successful in 1m51s
Site / build (pull_request) Successful in 16s
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
2026-10-08 00:11:55 +02:00

70 lines
5.5 KiB
C++

#pragma once
// The page a phone's browser gets (issue #88): one file, no other request but the API's. Kept
// small: it is sent from flash as it is.
namespace roro {
inline constexpr char kWebSharePage[] = R"HTML(<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>roro9stack files</title>
<style>
:root{color-scheme:dark light;--bg:#000;--fg:#fff;--mu:#9ab;--ac:#2cf;--ln:#345;--wa:#fa3}
@media (prefers-color-scheme:light){:root{--bg:#fff;--fg:#012;--mu:#567;--ac:#06a;--ln:#bcd;--wa:#b50}}
*{box-sizing:border-box}body{margin:0;font:16px/1.4 system-ui,sans-serif;background:var(--bg);color:var(--fg)}
main{max-width:720px;margin:0 auto;padding:12px}h1{font-size:18px;margin:4px 0 12px}
button,input,.btn{font:inherit;min-height:44px;padding:0 14px;border:1px solid var(--ln);background:none;color:inherit;border-radius:6px}
button,.btn{cursor:pointer}.main{background:var(--ac);color:#000;border-color:var(--ac)}
#crumbs{margin:8px 0;word-break:break-all}#crumbs a{color:var(--ac);text-decoration:none;padding:6px 2px;display:inline-block}
ul{list-style:none;margin:0;padding:0}li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--ln);min-height:48px}
li a{flex:1;color:inherit;text-decoration:none;padding:10px 0;word-break:break-all}li.d a{color:var(--ac)}
li small{color:var(--mu);white-space:nowrap}li button{min-height:36px;padding:0 10px;color:var(--mu)}
#bar{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}#msg{color:var(--wa);min-height:24px;margin:8px 0}
progress{width:100%}.hide{display:none}#login input{width:9em;font-size:22px;letter-spacing:.15em;text-align:center}
</style></head><body><main>
<h1>roro9stack: the SD card</h1>
<form id="login" class="hide"><p>The code on the device's screen:</p>
<input id="code" inputmode="numeric" autocomplete="off" maxlength="7" autofocus> <button class="main">Open</button></form>
<div id="app" class="hide">
<div id="crumbs"></div>
<div id="bar"><label class="btn main" style="display:inline-flex;align-items:center">Upload files<input id="pick" type="file" multiple class="hide"></label>
<button id="mk">New folder</button></div>
<progress id="prog" class="hide" max="100" value="0"></progress>
<ul id="list"></ul></div>
<p id="msg"></p>
</main><script>
const $=i=>document.getElementById(i),E=encodeURIComponent;let cwd='/';
const say=t=>{$('msg').textContent=t||''};
const join=(d,n)=>d=='/'?'/'+n:d+'/'+n;
const size=s=>s<1024?s+' B':s<1048576?(s/1024).toFixed(1)+' KB':(s/1048576).toFixed(1)+' MB';
function show(app){$('login').classList.toggle('hide',app);$('app').classList.toggle('hide',!app)}
async function call(u,o){const r=await fetch(u,o);if(r.status==401){show(false);throw new Error('The code, please')}
if(!r.ok)throw new Error(await r.text()||('Error '+r.status));return r}
async function login(code){const r=await fetch('/api/login',{method:'POST',body:'code='+E(code)});
if(r.ok){show(true);list('/')}else say(await r.text())}
async function list(p){try{const j=await(await call('/api/list?path='+E(p))).json();cwd=j.path;say(j.more?'Only the first '+j.items.length+' are shown':'');
const c=$('crumbs');c.textContent='';let at='';const parts=['/'].concat(cwd.split('/').filter(x=>x));
parts.forEach((n,i)=>{at=i?join(at||'/',n):'/';const a=document.createElement('a');a.href='#';a.textContent=i?n:'SD card';const to=at;a.onclick=e=>{e.preventDefault();list(to)};
if(i)c.append(' / ');c.append(a)});
const ul=$('list');ul.textContent='';j.items.sort((a,b)=>b.d-a.d||a.n.localeCompare(b.n));
if(!j.items.length){const li=document.createElement('li');li.innerHTML='<small>Nothing here</small>';ul.append(li)}
for(const it of j.items){const li=document.createElement('li'),a=document.createElement('a'),s=document.createElement('small'),b=document.createElement('button'),full=join(cwd,it.n);
a.textContent=it.n+(it.d?'/':'');if(it.d){li.className='d';a.href='#';a.onclick=e=>{e.preventDefault();list(full)}}else{a.href='/dl?path='+E(full);a.download=it.n;s.textContent=size(it.s)}
b.textContent='Delete';b.onclick=async()=>{if(!confirm('Delete '+it.n+'?'))return;try{await call('/api/delete?path='+E(full),{method:'POST'});list(cwd)}catch(e){say(e.message)}};
li.append(a,s,b);ul.append(li)}}catch(e){say(e.message)}}
function put(f,replace){return new Promise((ok,no)=>{const x=new XMLHttpRequest();x.open('PUT','/up?path='+E(join(cwd,f.name))+(replace?'&replace=1':''));
x.upload.onprogress=e=>{if(e.lengthComputable)$('prog').value=100*e.loaded/e.total};
x.onload=()=>x.status==200?ok():x.status==409&&!replace&&confirm(f.name+' is there already. Replace it?')?put(f,1).then(ok,no):x.status==409?ok():no(new Error(x.responseText||'Error '+x.status));
x.onerror=()=>no(new Error('The connection was lost'));x.send(f)})}
$('pick').onchange=async e=>{const fs=[...e.target.files];$('prog').classList.remove('hide');
try{let n=0;for(const f of fs){say('Uploading '+f.name+' ('+(++n)+' of '+fs.length+')');$('prog').value=0;await put(f)}say('')}catch(err){say(err.message)}
$('prog').classList.add('hide');e.target.value='';list(cwd)};
$('mk').onclick=async()=>{const n=prompt('The new folder\'s name');if(!n)return;try{await call('/api/mkdir?path='+E(join(cwd,n)),{method:'POST'});list(cwd)}catch(e){say(e.message)}};
$('login').onsubmit=e=>{e.preventDefault();login($('code').value)};
async function start(){const h=location.hash.slice(1);if(h){history.replaceState(null,'','/');await login(h)}
else{const r=await fetch('/api/list?path=%2F');if(r.ok){show(true);list('/')}else show(false)}}
onhashchange=start;start();
</script></body></html>
)HTML";
} // namespace roro