code wiki / _hdl_build / nx_manga_shelf_emit.nx
nx_manga_shelf_emit.nx source
↩ module page · 80 lines · 10156 B
1// nx_manga_shelf_emit.nx -- EMITS the LOCAL Comics Shelf browse page (knowledge/staging/media/shelf.html),
2// served at /library/shelf. The operator's "browse before download" LOCAL half: a SERIES grid (covers via the
3// EXISTING thumbnailed /api/cover) over /api/shelf's series->volumes catalog -> tap a series -> volume list
4// -> tap a volume -> the EXISTING /api/open extract-on-demand chain lands in the guided zoom reader
5// (/library/zoom). Inline login (same no-cookie session as the rest of /library). ZERO external loads; NO
6// double-quote chars (single-quote HTML/JS) so the fail-closed emit check passes. license_tier: ORIGINAL
7import "nx_syscalls.nx"
8const K_MAGIC_131072: i64 = 131072
9
10func ua(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o + i] = s[i]; i = i + 1 } return o + i }
11
12func sh_build(dst: *u8) -> i64 {
13 var o: i64 = 0
14 o = ua(dst, o, "<!doctype html>\n" as *u8)
15 o = ua(dst, o, "<html><head><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><base href='/library/'><title>Comics Shelf</title>\n" as *u8)
16 o = ua(dst, o, "<style>\n" as *u8)
17 o = ua(dst, o, "body{font:15px system-ui,-apple-system,Segoe UI,sans-serif;margin:0;background:#0b1220;color:#e6edf6}\n" as *u8)
18 o = ua(dst, o, "#lg{max-width:330px;margin:12vh auto;padding:0 20px}#lg input{width:100%;padding:10px;margin:.4rem 0;box-sizing:border-box;border:1px solid #28406a;border-radius:7px;background:#0e1a30;color:#e6edf6}\n" as *u8)
19 o = ua(dst, o, "button{padding:9px 13px;border:0;border-radius:8px;background:#e8a534;color:#0b2545;font-weight:700;cursor:pointer}.e{color:#ff8a8a;min-height:1.2em}\n" as *u8)
20 o = ua(dst, o, "#hd{background:#0b2545;padding:10px 12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;position:sticky;top:0;z-index:5}#hd b{font-size:1.02rem;white-space:nowrap}\n" as *u8)
21 o = ua(dst, o, "#q{flex:1 1 150px;min-width:0;padding:9px 12px;border-radius:8px;border:1px solid #28406a;background:#0e1a30;color:#e6edf6;font-size:1rem}a.lnk{color:#8ac;text-decoration:none;font-size:.9rem;white-space:nowrap}\n" as *u8)
22 o = ua(dst, o, "#msg{padding:6px 14px;color:#9ab;font-size:.85rem;min-height:1.1em}\n" as *u8)
23 o = ua(dst, o, "#g{display:grid;grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:12px;padding:0 14px 24px}\n" as *u8)
24 o = ua(dst, o, ".c{background:#13233f;border-radius:10px;overflow:hidden;cursor:pointer}.c img.cov{width:100%;aspect-ratio:2/3;object-fit:cover;display:none;background:#0e1a30}.c .t{padding:7px 8px 2px;font-size:.74rem;line-height:1.3;word-break:break-word;font-weight:600}.c .a{padding:0 8px 8px;font-size:.66rem;color:#9ab;word-break:break-word}\n" as *u8)
25 o = ua(dst, o, ".empty{grid-column:1/-1;padding:18px;color:#9ab}\n" as *u8)
26 o = ua(dst, o, "#ov{position:fixed;inset:0;background:rgba(4,8,16,.86);z-index:20;overflow:auto}#ovc{max-width:520px;margin:5vh auto;background:#0e1a30;border-radius:12px;padding:14px}.ovh{font-weight:700;margin-bottom:8px}.chb{display:block;width:100%;text-align:left;margin:5px 0;background:#22334f;color:#e6edf6;font-weight:600}.ovx{margin-top:10px;background:#33507f;color:#e6edf6}\n" as *u8)
27 o = ua(dst, o, "@media(max-width:480px){#g{grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:9px}#hd b{width:100%}}\n" as *u8)
28 o = ua(dst, o, "</style></head><body>\n" as *u8)
29 o = ua(dst, o, "<div id=lg><h2>📚 Comics Shelf</h2><p style='color:#9ab;font-size:.9rem'>Sign in with your Nishi Library handle.</p><input id=h placeholder=handle autocomplete=username autofocus><input id=p type=password placeholder=passphrase autocomplete=current-password><button id=b>Sign in</button><p id=le class=e></p></div>\n" as *u8)
30 o = ua(dst, o, "<div id=app hidden><div id=hd><b>📚 Comics Shelf</b><input id=q placeholder='filter series...'><a class=lnk href='manga'>My Manga</a><a class=lnk href='.'>← Library</a></div><div id=msg></div><div id=g></div></div>\n" as *u8)
31 o = ua(dst, o, "<div id=ov hidden><div id=ovc></div></div>\n" as *u8)
32 o = ua(dst, o, "<script>\n" as *u8)
33 o = ua(dst, o, "function T(){try{return sessionStorage.nx_sess||''}catch(_){return ''}}function H(){return {'X-Nishi-Session':T()}}\n" as *u8)
34 o = ua(dst, o, "var msg=document.getElementById('msg'),g=document.getElementById('g'),SER=[];\n" as *u8)
35 o = ua(dst, o, "function say(s){msg.textContent=s||''}\n" as *u8)
36 o = ua(dst, o, "var IO=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){IO.unobserve(e.target);cov(e.target)}})},{rootMargin:'400px'});\n" as *u8)
37 o = ua(dst, o, "function cov(d){if(!d.dataset.p)return;fetch('api/cover?p='+encodeURIComponent(d.dataset.p),{headers:H()}).then(function(r){if(!r.ok)throw 0;return r.blob()}).then(function(b){var im=d.firstChild;im.src=URL.createObjectURL(b);im.style.display='block'}).catch(function(){})}\n" as *u8)
38 o = ua(dst, o, "function card(s){var d=document.createElement('div');d.className='c';d.dataset.p=((s.vols||[])[0]||{}).p||'';var im=document.createElement('img');im.className='cov';im.alt='';d.appendChild(im);var t=document.createElement('div');t.className='t';t.textContent=s.t;d.appendChild(t);var a=document.createElement('div');a.className='a';a.textContent=(s.a||'')+(s.n>1?(' \\u00b7 '+s.n+' vols'):'');d.appendChild(a);d.onclick=function(){openSeries(s)};IO.observe(d);return d}\n" as *u8)
39 o = ua(dst, o, "function render(f){g.innerHTML='';var q=(f||'').toLowerCase();var sh=SER.filter(function(x){return !q||(x.t||'').toLowerCase().indexOf(q)>=0||(x.a||'').toLowerCase().indexOf(q)>=0});if(!sh.length){g.innerHTML='<p class=empty>'+(SER.length?'No match.':'Nothing on the shelf yet - comics and manga land here as the library grows.')+'</p>';return}sh.forEach(function(x){g.appendChild(card(x))})}\n" as *u8)
40 o = ua(dst, o, "function load(){say('Loading shelf...');fetch('api/shelf',{headers:H()}).then(function(r){if(r.status===401||r.status===403){sessionStorage.removeItem('nx_sess');location.reload();return null}if(!r.ok)throw 0;return r.json()}).then(function(j){if(!j)return;SER=j.series||[];say('');render(document.getElementById('q').value.trim())}).catch(function(){say('could not load the shelf - tap here to retry');msg.onclick=function(){msg.onclick=null;load()}})}\n" as *u8)
41 o = ua(dst, o, "function openb(p){say('Preparing volume... first open extracts pages, give it a moment');fetch('api/open?p='+encodeURIComponent(p),{cache:'no-store',headers:H()}).then(function(r){var u=r.url||'';if(u.indexOf('/zoom')>=0||u.indexOf('/cbx')>=0||u.indexOf('/reader')>=0){location.href=u;return null}if(u.indexOf('/file')>=0){say('');alert('This volume cannot open in the reader (DRM or unsupported format).');return null}return r.text()}).then(function(t){if(t){document.open();document.write(t);document.close()}}).catch(function(){say('could not open - tap the volume again')})}\n" as *u8)
42 o = ua(dst, o, "function openSeries(s){var vs=s.vols||[];if(vs.length===1){openb(vs[0].p);return}var ovc=document.getElementById('ovc');ovc.innerHTML='';var h=document.createElement('div');h.className='ovh';h.textContent=s.t+' \\u2014 '+s.n+' volumes';ovc.appendChild(h);vs.forEach(function(v){var b=document.createElement('button');b.className='chb';b.textContent=(v.v>0?('Vol. '+v.v+' \\u00b7 '):'')+v.t+(v.x?' \\u2713':'');b.onclick=function(){closeOv();openb(v.p)};ovc.appendChild(b)});var x=document.createElement('button');x.className='ovx';x.textContent='Close';x.onclick=closeOv;ovc.appendChild(x);document.getElementById('ov').hidden=0}\n" as *u8)
43 o = ua(dst, o, "function closeOv(){document.getElementById('ov').hidden=1}\n" as *u8)
44 o = ua(dst, o, "document.getElementById('ov').onclick=function(e){if(e.target===this)closeOv()};\n" as *u8)
45 o = ua(dst, o, "function showApp(){document.getElementById('lg').hidden=1;document.getElementById('app').hidden=0;load()}\n" as *u8)
46 o = ua(dst, o, "document.getElementById('b').onclick=function(){document.getElementById('le').textContent='';var b='handle='+encodeURIComponent(document.getElementById('h').value)+'&passphrase='+encodeURIComponent(document.getElementById('p').value);fetch('api/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b}).then(function(r){if(!r.ok)throw 0;return r.json()}).then(function(j){sessionStorage.nx_sess=j.token;showApp()}).catch(function(){document.getElementById('le').textContent='Wrong handle or passphrase.'})};\n" as *u8)
47 o = ua(dst, o, "document.getElementById('p').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('b').click()});\n" as *u8)
48 o = ua(dst, o, "document.getElementById('q').addEventListener('input',function(){render(this.value.trim())});\n" as *u8)
49 o = ua(dst, o, "if(sessionStorage.nx_sess){showApp()}\n" as *u8)
50 o = ua(dst, o, "</script></body></html>\n" as *u8)
51 return o
52}
53
54func sh_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
55func sh_w(s: *u8) -> i64 { sys_write(1, s, sh_slen(s)); return 0 }
56
57func main() -> i64 {
58 let buf: *u8 = sys_mmap(K_MAGIC_131072)
59 let n: i64 = sh_build(buf)
60 // fail-closed sovereignty at emit: refuse a double-quote leak or an external <script src
61 var bad: i64 = 0
62 var i: i64 = 0
63 while i < n {
64 if buf[i] == (34 as u8) { bad = 1 }
65 if i + 12 <= n {
66 if buf[i] == (60 as u8) { if buf[i+1] == (115 as u8) { if buf[i+2] == (99 as u8) { if buf[i+3] == (114 as u8) {
67 if buf[i+7] == (32 as u8) { if buf[i+8] == (115 as u8) { if buf[i+9] == (114 as u8) { bad = 2 } } }
68 } } } }
69 }
70 i = i + 1
71 }
72 if bad != 0 { sh_w("SHELF-UI REFUSED (sovereignty check failed)\n" as *u8); sys_exit(1); return 1 }
73 let fd: i64 = sys_openat_wr("knowledge/staging/media/shelf.html" as *u8, 0x1a4)
74 if fd < 0 { sh_w("SHELF-UI write-fail\n" as *u8); sys_exit(1); return 1 }
75 sys_write(fd, buf, n)
76 sys_close(fd)
77 sh_w("SHELF-UI OK knowledge/staging/media/shelf.html\n" as *u8)
78 sys_exit(0)
79 return 0
80}