code wiki / _hdl_build / nx_manga_ui_emit.nx
nx_manga_ui_emit.nx source
↩ module page · 97 lines · 14059 B
1// nx_manga_ui_emit.nx -- EMITS the sovereign "My Manga" SOURCE BROWSER (knowledge/staging/media/manga.html), served
2// at /library/manga. Operator 2026-07-04: "i want the sites to choose to download or translate like i could in media
3// manager" + "i dont see the mangas working". So this page is now SELF-SUFFICIENT: it shows an inline LOGIN if there
4// is no session (so navigating straight to /library/manga works), then a SOURCE PICKER (My Library / MangaDex /
5// nhentai) with on-site SEARCH -> tap a result to download (mangadex: series -> chapter picker; nhentai: gallery ->
6// direct). Downloads run in the BACKGROUND (async) and appear in My Library via polling. Covers proxied via
7// /api/thumb (Cloudflare-beating). ZERO external loads; NO double-quote chars (single-quote HTML/JS) so the emit
8// fail-closed check passes. license_tier: ORIGINAL
9import "nx_syscalls.nx"
10const K_MAGIC_131072: i64 = 131072
11
12func 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 }
13
14func mu_build(dst: *u8) -> i64 {
15 var o: i64 = 0
16 o = ua(dst, o, "<!doctype html>\n" as *u8)
17 o = ua(dst, o, "<html><head><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><base href='/library/'><title>My Manga</title>\n" as *u8)
18 o = ua(dst, o, "<style>\n" as *u8)
19 o = ua(dst, o, "body{font:15px system-ui,-apple-system,Segoe UI,sans-serif;margin:0;background:#0b1220;color:#e6edf6}\n" as *u8)
20 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)
21 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)
22 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)
23 o = ua(dst, o, ".sb{background:#22334f;color:#cdd9ec;font-weight:600;padding:7px 11px}.sb.on{background:#e8a534;color:#0b2545}\n" as *u8)
24 o = ua(dst, o, "#sub{display:flex;flex-wrap:wrap;gap:8px;padding:8px 14px 2px}#sub:empty{display:none}.subb{background:#1a2c4a;color:#cdd9ec;font-weight:600;padding:6px 12px;font-size:.9rem}\n" as *u8)
25 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)
26 o = ua(dst, o, "#msg{padding:6px 14px;color:#9ab;font-size:.85rem;min-height:1.1em}\n" as *u8)
27 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)
28 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:block;background:#0e1a30}.c .t{padding:7px 8px;font-size:.72rem;line-height:1.3;word-break:break-word}\n" as *u8)
29 o = ua(dst, o, ".empty{grid-column:1/-1;padding:18px;color:#9ab}\n" as *u8)
30 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)
31 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)
32 o = ua(dst, o, "</style></head><body>\n" as *u8)
33 // login gate
34 o = ua(dst, o, "<div id=lg><h2>📚 My Manga</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)
35 // app
36 o = ua(dst, o, "<div id=app hidden><div id=hd><b>📚 My Manga</b><button class=sb data-s=lib>My Library</button><button class=sb data-s=mangadex>MangaDex</button><button class=sb data-s=nhentai>nhentai</button><input id=q placeholder='filter your library...'><a class=lnk href='shelf'>📖 Shelf</a><a class=lnk href='.'>← Library</a></div><div id=sub></div><div id=msg></div><div id=g></div></div>\n" as *u8)
37 o = ua(dst, o, "<div id=ov hidden><div id=ovc></div></div>\n" as *u8)
38 o = ua(dst, o, "<script>\n" as *u8)
39 o = ua(dst, o, "function T(){try{return sessionStorage.nx_sess||''}catch(_){return ''}}function H(){return {'X-Nishi-Session':T()}}\n" as *u8)
40 o = ua(dst, o, "var msg=document.getElementById('msg'),g=document.getElementById('g'),SRC='lib',personalCount=-1,libItems=[],sTimer;\n" as *u8)
41 o = ua(dst, o, "function say(s){msg.textContent=s||''}\n" as *u8)
42 o = ua(dst, o, "function card(title,thumb,onclick){var d=document.createElement('div');d.className='c';if(thumb){var im=document.createElement('img');im.className='cov';im.loading='lazy';im.alt='';im.onerror=function(){im.style.display='none'};im.src=thumb;d.appendChild(im)}var t=document.createElement('div');t.className='t';t.textContent=title;d.appendChild(t);d.onclick=onclick;return d}\n" as *u8)
43 // library
44 o = ua(dst, o, "function renderLib(f){g.innerHTML='';var q=(f||'').toLowerCase();var sh=libItems.filter(function(x){return !q||(x.n||'').toLowerCase().indexOf(q)>=0});if(!sh.length){g.innerHTML='<p class=empty>'+(libItems.length?'No match.':'No manga yet - pick MangaDex or nhentai above to find some.')+'</p>';return}sh.forEach(function(x){g.appendChild(card(x.n,'',function(){location.href='zoom?slug='+encodeURIComponent(x.slug)}))})}\n" as *u8)
45 o = ua(dst, o, "function loadLib(cb){fetch('api/personal',{headers:H()}).then(function(r){if(r.status===401||r.status===403){sessionStorage.removeItem('nx_sess');location.reload();return null}return r.json()}).then(function(j){if(!j)return;libItems=j.items||[];personalCount=libItems.length;if(SRC==='lib')renderLib(document.getElementById('q').value.trim());if(cb)cb()}).catch(function(){say('could not load your library')})}\n" as *u8)
46 // shared: render source results (search OR browse) -> cards; tap = chapters (series) or download (gallery)
47 o = ua(dst, o, "function renderResults(j){g.innerHTML='';var rs=(j&&j.results)||[];say(rs.length?'Tap a title to download.':'Nothing here (the source may have blocked us - try again).');rs.forEach(function(x){var oc;if(j.kind==='series'){oc=function(){openSeries(x.id,x.t)}}else{oc=function(){dl(x.url,x.t)}}g.appendChild(card(x.t,x.thumb||'',oc))})}\n" as *u8)
48 o = ua(dst, o, "function srcErr(){sessionStorage.removeItem('nx_sess');location.reload()}\n" as *u8)
49 o = ua(dst, o, "function searchSrc(){var q=document.getElementById('q').value.trim();if(SRC==='lib'){renderLib(q);return}if(!q){loadBrowse('popular');return}g.innerHTML='';say('Searching '+SRC+'...');fetch('api/msearch?src='+SRC+'&q='+encodeURIComponent(q),{headers:H()}).then(function(r){if(r.status===401||r.status===403){srcErr();return null}return r.json()}).then(function(j){if(j)renderResults(j)}).catch(function(){say('search failed - try again')})}\n" as *u8)
50 // browse WITHOUT search (landing feed) + random
51 o = ua(dst, o, "function loadBrowse(mode){if(SRC==='lib')return;g.innerHTML='';say('Loading '+SRC+'...');fetch('api/mbrowse?src='+SRC+'&mode='+mode,{headers:H()}).then(function(r){if(r.status===401||r.status===403){srcErr();return null}return r.json()}).then(function(j){if(j)renderResults(j)}).catch(function(){say('could not load '+SRC)})}\n" as *u8)
52 o = ua(dst, o, "function doRandom(){if(SRC==='lib')return;say('Rolling the dice...');fetch('api/mrandom?src='+SRC,{headers:H()}).then(function(r){if(r.status===401||r.status===403){srcErr();return null}return r.json()}).then(function(j){var rs=(j&&j.results)||[];if(!rs.length){say('no luck - tap Random again');return}var x=rs[0];if(j.kind==='series'){openSeries(x.id,x.t)}else{dl(x.url,x.t)}}).catch(function(){say('random failed')})}\n" as *u8)
53 o = ua(dst, o, "function subBtn(label,fn){var b=document.createElement('button');b.className='subb';b.textContent=label;b.onclick=fn;return b}\n" as *u8)
54 // mangadex chapter picker
55 o = ua(dst, o, "function openSeries(id,title){say('Loading chapters...');fetch('api/mfeed?id='+encodeURIComponent(id),{headers:H()}).then(function(r){return r.json()}).then(function(j){var ch=(j.results||[]);var ov=document.getElementById('ov'),ovc=document.getElementById('ovc');ovc.innerHTML='';var h=document.createElement('div');h.className='ovh';h.textContent=title;ovc.appendChild(h);if(!ch.length){var p=document.createElement('p');p.style.color='#9ab';p.textContent='No English chapters listed.';ovc.appendChild(p)}ch.forEach(function(c){var b=document.createElement('button');b.className='chb';b.textContent='Chapter '+c.ch;b.onclick=function(){closeOv();dl('https://mangadex.org/chapter/'+c.id,title+' Ch.'+c.ch)};ovc.appendChild(b)});var x=document.createElement('button');x.className='ovx';x.textContent='Close';x.onclick=closeOv;ovc.appendChild(x);ov.hidden=0;say('')}).catch(function(){say('could not load chapters')})}\n" as *u8)
56 o = ua(dst, o, "function closeOv(){document.getElementById('ov').hidden=1}\n" as *u8)
57 // download (async) + poll
58 o = ua(dst, o, "function dl(url,title){say('Starting download...');var hh=H();hh['Content-Type']='application/x-www-form-urlencoded';fetch('api/manga/add',{method:'POST',headers:hh,body:'url='+encodeURIComponent(url)}).then(function(r){return r.json()}).then(function(j){if(j&&j.ok){dlProgress(j.slug)}else{say('Could not start: '+((j&&j.err)||'check the link'))}}).catch(function(){say('Could not reach the server')})}\n" as *u8)
59 o = ua(dst, o, "function dlProgress(slug){var tries=0;var iv=setInterval(function(){tries++;fetch('api/dlstatus?slug='+encodeURIComponent(slug),{headers:H()}).then(function(r){return r.status===200?r.json():null}).then(function(j){if(!j)return;if(j.finished||(j.total>0&&j.done>=j.total)){clearInterval(iv);say('Done! It is in My Library.');loadLib()}else if(j.total>0){say('Downloading '+j.done+' / '+j.total+' pages...')}else{say('Preparing download...')}if(tries>=180){clearInterval(iv);loadLib()}}).catch(function(){})},2000)}\n" as *u8)
60 // source tabs
61 o = ua(dst, o, "function setSrc(s){SRC=s;var bs=document.querySelectorAll('.sb');for(var i=0;i<bs.length;i++){bs[i].className='sb'+(bs[i].getAttribute('data-s')===s?' on':'')}var qi=document.getElementById('q');qi.value='';var sub=document.getElementById('sub');sub.innerHTML='';if(s==='lib'){qi.placeholder='filter your library...';loadLib();return}qi.placeholder='search '+s+'...';if(s==='mangadex'){sub.appendChild(subBtn('Popular',function(){loadBrowse('popular')}));sub.appendChild(subBtn('Latest',function(){loadBrowse('latest')}))}else{sub.appendChild(subBtn('Recent',function(){loadBrowse('popular')}))}sub.appendChild(subBtn('Random',doRandom));loadBrowse('popular')}\n" as *u8)
62 o = ua(dst, o, "function showApp(){document.getElementById('lg').hidden=1;document.getElementById('app').hidden=0;loadLib(function(){setSrc('lib')})}\n" as *u8)
63 // login
64 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)
65 o = ua(dst, o, "document.getElementById('p').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('b').click()});\n" as *u8)
66 o = ua(dst, o, "var qi=document.getElementById('q');qi.addEventListener('input',function(){clearTimeout(sTimer);if(SRC==='lib'){renderLib(qi.value.trim());return}sTimer=setTimeout(searchSrc,500)});qi.addEventListener('keydown',function(e){if(e.key==='Enter'){clearTimeout(sTimer);searchSrc()}});\n" as *u8)
67 o = ua(dst, o, "var sbs=document.querySelectorAll('.sb');for(var k=0;k<sbs.length;k++){sbs[k].onclick=function(){setSrc(this.getAttribute('data-s'))}}\n" as *u8)
68 o = ua(dst, o, "if(sessionStorage.nx_sess){showApp()}\n" as *u8)
69 o = ua(dst, o, "</script></body></html>\n" as *u8)
70 return o
71}
72
73func mu_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
74func mu_w(s: *u8) -> i64 { sys_write(1, s, mu_slen(s)); return 0 }
75
76func main() -> i64 {
77 let buf: *u8 = sys_mmap(K_MAGIC_131072)
78 let n: i64 = mu_build(buf)
79 // fail-closed sovereignty at emit: refuse a double-quote leak or an external <script src
80 var bad: i64 = 0
81 var i: i64 = 0
82 while i < n {
83 if buf[i] == (34 as u8) { bad = 1 }
84 if i + 12 <= n {
85 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) {
86 if buf[i+7] == (32 as u8) { if buf[i+8] == (115 as u8) { if buf[i+9] == (114 as u8) { bad = 2 } } }
87 } } } }
88 }
89 i = i + 1
90 }
91 if bad != 0 { mu_w("MANGA-UI REFUSED (sovereignty check failed)\n" as *u8); sys_exit(1); return 1 }
92 let fd: i64 = sys_openat_wr("knowledge/staging/media/manga.html" as *u8, 0x1a4)
93 if fd < 0 { mu_w("MANGA-UI write-fail\n" as *u8); sys_exit(1); return 1 }
94 sys_write(fd, buf, n); sys_close(fd)
95 mu_w("MANGA-UI OK knowledge/staging/media/manga.html\n" as *u8)
96 sys_exit(0); return 0
97}