fileport-gateway 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui.js +4 -4
- package/package.json +1 -1
package/dist/ui.js
CHANGED
|
@@ -3,15 +3,15 @@ export function previewHtml(share) {
|
|
|
3
3
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Fileport</title>
|
|
4
4
|
<style>:root{font:15px system-ui,sans-serif;color:#202124;background:#f6f7f9}*{box-sizing:border-box}body{margin:0}header{height:52px;display:flex;align-items:center;gap:12px;padding:0 18px;background:#fff;border-bottom:1px solid #dfe1e5}header strong{font-size:17px}.badge{font-size:12px;padding:3px 7px;border-radius:12px;background:#e8f0fe;color:#174ea6}main{max-width:1200px;margin:18px auto;padding:0 16px}.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:12px}.path{font-family:ui-monospace,monospace;overflow-wrap:anywhere}.status{margin-left:auto;color:#5f6368;font-size:13px}.panel{background:#fff;border:1px solid #dfe1e5;border-radius:10px;overflow:hidden;min-height:160px}.entry{display:flex;gap:10px;padding:10px 14px;border-bottom:1px solid #eee;color:#174ea6;text-decoration:none}.entry:hover{background:#f8faff}.content{padding:18px;overflow:auto}pre{margin:0;white-space:pre-wrap;word-break:break-word;font:13px/1.5 ui-monospace,SFMono-Regular,monospace}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px;text-align:left}img{max-width:100%;height:auto}.error{color:#b3261e;padding:18px}button{border:1px solid #ccd;background:#fff;border-radius:6px;padding:6px 10px;cursor:pointer}button:disabled{opacity:.5}.save{background:#174ea6;color:#fff}textarea{width:100%;height:70vh;border:0;padding:16px;resize:vertical;font:13px/1.5 ui-monospace,SFMono-Regular,monospace}.markdown-body{font-size:16px;line-height:1.65;color:#1f2328}.markdown-body h1,.markdown-body h2,.markdown-body h3{line-height:1.25;margin:1.4em 0 .6em}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #d8dee4}.markdown-body p{margin:.8em 0}.markdown-body ul,.markdown-body ol{padding-left:2em}.markdown-body li+li{margin-top:.25em}.markdown-body blockquote{margin:1em 0;padding:0 1em;color:#59636e;border-left:.25em solid #d0d7de}.markdown-body code{padding:.15em .35em;border-radius:4px;background:#eff1f3;font:85% ui-monospace,SFMono-Regular,monospace}.markdown-body pre{padding:16px;overflow:auto;background:#f6f8fa;border-radius:6px;white-space:pre}.markdown-body pre code{padding:0;background:transparent;font-size:100%}.markdown-body table{display:block;overflow:auto;margin:1em 0;border-spacing:0}.markdown-body th{font-weight:600;background:#f6f8fa}.markdown-body th,.markdown-body td{padding:6px 13px;border:1px solid #d0d7de}.markdown-body tr:nth-child(2n){background:#f6f8fa}.markdown-body a{color:#0969da;text-decoration:none}.markdown-body a:hover{text-decoration:underline}.markdown-body hr{height:.25em;border:0;background:#d0d7de}.markdown-body input[type=checkbox]{margin-right:.5em}</style></head><body>
|
|
5
5
|
<header><strong>Fileport</strong><span class="badge">${share.permission === "rw" ? "RW · saves modify the authoritative file" : "RO · read-only capability"}</span></header>
|
|
6
|
-
<main><div class="toolbar"><button id="up">Up</button><button id="edit" hidden>Edit</button><button id="save" class="save" hidden>Save</button><span class="path" id="path"></span><span class="status" id="status"></span></div><section class="panel" id="view"><div class="content">Loading…</div></section></main>
|
|
6
|
+
<main><div class="toolbar"><button id="up">Up</button><button id="edit" hidden>Edit</button><button id="save" class="save" hidden>Save</button><button id="download" hidden>Download</button><span class="path" id="path"></span><span class="status" id="status"></span></div><section class="panel" id="view"><div class="content">Loading…</div></section></main>
|
|
7
7
|
<script>const CONFIG=${config};
|
|
8
|
-
const view=document.getElementById('view'),pathLabel=document.getElementById('path'),up=document.getElementById('up'),edit=document.getElementById('edit'),save=document.getElementById('save'),status=document.getElementById('status');let active={path:'',text:'',version:'',editable:false,editing:false,type:''};
|
|
8
|
+
const view=document.getElementById('view'),pathLabel=document.getElementById('path'),up=document.getElementById('up'),edit=document.getElementById('edit'),save=document.getElementById('save'),downloadBtn=document.getElementById('download'),status=document.getElementById('status');let active={path:'',text:'',version:'',editable:false,editing:false,type:''};
|
|
9
9
|
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
10
10
|
const enc=p=>p.split('/').filter(Boolean).map(encodeURIComponent).join('/');const fsUrl=p=>'/s/'+CONFIG.shareId+'/fs/'+enc(p);const apiUrl=(kind,p)=>'/s/'+CONFIG.shareId+'/api/'+kind+'/'+enc(p);const current=()=>new URL(location.href).searchParams.get('path')??CONFIG.initialPath;
|
|
11
11
|
function navigate(p){const u=new URL(location.href);p?u.searchParams.set('path',p):u.searchParams.delete('path');history.pushState({},'',u);render()}
|
|
12
12
|
function csv(text){const rows=text.trim().split(/\\r?\\n/).map(r=>r.split(','));return '<table>'+rows.map((r,i)=>'<tr>'+r.map(c=>'<'+(i?'td':'th')+'>'+esc(c)+'</'+(i?'td':'th')+'>').join('')+'</tr>').join('')+'</table>'}
|
|
13
|
-
async function sourceView(p,text,type){const ext=p.toLowerCase().split('.').pop();if(type.includes('text/html')||ext==='html'){const url=URL.createObjectURL(new Blob([text],{type:'text/html'}));view.innerHTML='<iframe sandbox style="width:100%;height:75vh;border:0" src="'+url+'"></iframe>';return}let formatted=text;if(ext==='json'){try{formatted=JSON.stringify(JSON.parse(text),null,2)}catch{}}if(ext==='md'||ext==='markdown'){const response=await fetch(apiUrl('render/markdown',p),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({source:text})});const data=await response.json();if(!response.ok)throw new Error(data.error||'Markdown render failed');view.innerHTML='<article class="content markdown-body">'+data.html+'</article>';return}if(ext==='csv'||ext==='tsv'){view.innerHTML='<div class="content">'+(ext==='csv'?csv(formatted):csv(formatted.replaceAll('\\t',',')))+'</div>';return}view.innerHTML='<div class="content"><pre>'+esc(formatted)+'</pre></div>'}
|
|
14
|
-
async function showFile(p){const response=await fetch(fsUrl(p));if(!response.ok)throw new Error('HTTP '+response.status);const type=response.headers.get('content-type')||'';if(type.startsWith('image/')){const url=URL.createObjectURL(await response.blob());view.innerHTML='<div class="content"><img alt="preview" src="'+url+'"></div>';return}let text=await response.text(),version='',editable=false;if(CONFIG.permission==='rw'){const editResponse=await fetch(apiUrl('edit',p));if(editResponse.ok){const snapshot=await editResponse.json();text=snapshot.content;version=snapshot.version;editable=snapshot.editable===true}}active={path:p,text,version,editable,editing:false,type};await sourceView(p,text,type);edit.hidden=!active.editable;edit.onclick=()=>{active.editing=true;view.innerHTML='<textarea id="editor"></textarea>';document.getElementById('editor').value=active.text;edit.hidden=true;save.hidden=false;status.textContent='Unsaved changes'};save.onclick=async()=>{if(!confirm('Save changes to the authoritative file?'))return;save.disabled=true;const content=document.getElementById('editor').value,response=await fetch(apiUrl('save',active.path),{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({content,expectedVersion:active.version})}),data=await response.json();save.disabled=false;if(!response.ok){status.textContent='Save failed: '+data.error;return}active.text=content;active.version=data.version;active.editing=false;status.textContent='Saved';edit.hidden=false;save.hidden=true;await sourceView(active.path,active.text,active.type)}}
|
|
13
|
+
async function sourceView(p,text,type){const ext=p.toLowerCase().split('.').pop();if(type.includes('text/html')||ext==='html'){const url=URL.createObjectURL(new Blob([text],{type:'text/html'}));view.innerHTML='<iframe sandbox="allow-scripts" style="width:100%;height:75vh;border:0" src="'+url+'"></iframe>';return}let formatted=text;if(ext==='json'){try{formatted=JSON.stringify(JSON.parse(text),null,2)}catch{}}if(ext==='md'||ext==='markdown'){const response=await fetch(apiUrl('render/markdown',p),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({source:text})});const data=await response.json();if(!response.ok)throw new Error(data.error||'Markdown render failed');view.innerHTML='<article class="content markdown-body">'+data.html+'</article>';return}if(ext==='csv'||ext==='tsv'){view.innerHTML='<div class="content">'+(ext==='csv'?csv(formatted):csv(formatted.replaceAll('\\t',',')))+'</div>';return}view.innerHTML='<div class="content"><pre>'+esc(formatted)+'</pre></div>'}
|
|
14
|
+
async function showFile(p){const response=await fetch(fsUrl(p));if(!response.ok)throw new Error('HTTP '+response.status);const type=response.headers.get('content-type')||'';if(type.startsWith('image/')){const url=URL.createObjectURL(await response.blob());view.innerHTML='<div class="content"><img alt="preview" src="'+url+'"></div>';return}let text=await response.text(),version='',editable=false;if(CONFIG.permission==='rw'){const editResponse=await fetch(apiUrl('edit',p));if(editResponse.ok){const snapshot=await editResponse.json();text=snapshot.content;version=snapshot.version;editable=snapshot.editable===true}}active={path:p,text,version,editable,editing:false,type};await sourceView(p,text,type);downloadBtn.hidden=false;downloadBtn.onclick=()=>{const a=document.createElement('a');a.href=fsUrl(p);a.download=p.split('/').pop()||'download';a.click()};edit.hidden=!active.editable;edit.onclick=()=>{active.editing=true;view.innerHTML='<textarea id="editor"></textarea>';document.getElementById('editor').value=active.text;edit.hidden=true;save.hidden=false;downloadBtn.hidden=true;status.textContent='Unsaved changes'};save.onclick=async()=>{if(!confirm('Save changes to the authoritative file?'))return;save.disabled=true;const content=document.getElementById('editor').value,response=await fetch(apiUrl('save',active.path),{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({content,expectedVersion:active.version})}),data=await response.json();save.disabled=false;if(!response.ok){status.textContent='Save failed: '+data.error;return}active.text=content;active.version=data.version;active.editing=false;status.textContent='Saved';edit.hidden=false;save.hidden=true;await sourceView(active.path,active.text,active.type)}}
|
|
15
15
|
async function showDirectory(p){const response=await fetch((p?fsUrl(p)+'/':fsUrl(''))+'?json');if(!response.ok)throw new Error('HTTP '+response.status);const data=await response.json(),entries=data.paths||[];view.innerHTML=entries.length?entries.map(item=>{const name=String(item.name),leaf=name.includes('/')?name.slice(name.lastIndexOf('/')+1):name,next=[p,leaf].filter(Boolean).join('/'),icon=item.path_type==='Dir'?'📁':'📄';return '<a class="entry" href="?path='+encodeURIComponent(next)+'" data-path="'+esc(next)+'"><span>'+icon+'</span><span>'+esc(leaf)+'</span></a>'}).join(''):'<div class="content">Empty directory</div>';view.querySelectorAll('[data-path]').forEach(a=>a.addEventListener('click',e=>{e.preventDefault();navigate(a.dataset.path)}))}
|
|
16
16
|
async function render(){const p=current();edit.hidden=true;save.hidden=true;status.textContent='';pathLabel.textContent='/'+p;up.disabled=!p||CONFIG.kind==='file';up.onclick=()=>navigate(p.split('/').slice(0,-1).join('/'));view.innerHTML='<div class="content">Loading…</div>';try{if(CONFIG.kind==='file')await showFile(CONFIG.initialPath);else{const probe=await fetch(fsUrl(p)+(p?'/':'')+'?json');if(probe.ok){const data=await probe.clone().json().catch(()=>null);if(data?.kind==='Index'){await showDirectory(p);return}}await showFile(p)}}catch(e){view.innerHTML='<div class="error">'+esc(e.message||e)+'</div>'}}
|
|
17
17
|
addEventListener('popstate',render);render();</script></body></html>`;
|