diff options
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 80 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/i18n.js | 2 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 9 |
3 files changed, 69 insertions, 22 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 3087e0e..cff001b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -823,6 +823,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { const [previewEntry, setPreviewEntry] = useState(null); const [tab, setTab] = useState('files'); const [uploading, setUploading] = useState(false); + const [ulState, setUlState] = useState(null); const [menuOpen, setMenuOpen] = useState(null); const [isNodeAdmin, setIsNodeAdmin] = useState(false); const [needsCode, setNeedsCode] = useState(false); @@ -1020,13 +1021,20 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { if (!transport || !transport.connected) return; setUploading(true); setError(''); + setUlState({ name: file.name, sent: 0, total: file.size, indexing: false }); try { const totalChunks = Math.ceil(file.size / UPLOAD_CHUNK_SIZE); for (let i = 0; i < totalChunks; i++) { const slice = file.slice(i * UPLOAD_CHUNK_SIZE, (i + 1) * UPLOAD_CHUNK_SIZE); const buf = new Uint8Array(await slice.arrayBuffer()); await transport.uploadChunk(file.name, i, totalChunks, buf); + // Bytes actually acknowledged by the node, not bytes read locally. + setUlState(prev => prev && { ...prev, sent: Math.min(file.size, + (i + 1) * UPLOAD_CHUNK_SIZE) }); } + // The node re-indexes on a filesystem event; there is nothing to poll, so + // say what is happening instead of showing a finished bar and no file. + setUlState(prev => prev && { ...prev, indexing: true }); await new Promise(r => setTimeout(r, 2500)); const indexMsg = await transport.fetchIndex(); if (indexMsg.entries) setEntries(indexMsg.entries); @@ -1034,6 +1042,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { setError(err.message); } finally { setUploading(false); + setUlState(null); } }, []); @@ -1155,6 +1164,21 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { </div> </form> `} + ${ulState && html` + <div class="dl-bar"> + <span class="spinner"></span> + <span class="dl-name">${ulState.name}</span> + <div class="dl-progress"> + <div class="dl-fill" style=${`width:${ulState.total + ? Math.round(ulState.sent / ulState.total * 100) : 0}%`}></div> + </div> + <span class="dl-pct"> + ${ulState.indexing + ? t('group.upload_indexing') + : `${ulState.total ? Math.round(ulState.sent / ulState.total * 100) : 0}%`} + </span> + </div> + `} ${dlState && html` <div class="dl-bar"> <span class="dl-name">${dlState.name}</span> @@ -1361,7 +1385,13 @@ function FilePreview({ entry, transportRef, gekRef, onClose }) { ); if (cancelled) return; - if (entry.name.match(IMAGE_EXTS)) { + if (/\.pdf$/i.test(entry.name)) { + // Decrypted here and shown from a blob: URL — the bytes never leave + // the page, and the browser's own viewer renders them. + const blob = new Blob(chunks, { type: 'application/pdf' }); + blobUrlRef.current = URL.createObjectURL(blob); + setContent({ type: 'pdf' }); + } else if (entry.name.match(IMAGE_EXTS)) { const ext = entry.name.split('.').pop().toLowerCase(); const mime = ext === 'svg' ? 'image/svg+xml' : ext === 'png' ? 'image/png' @@ -1416,6 +1446,12 @@ function FilePreview({ entry, transportRef, gekRef, onClose }) { </div> </div> `} + ${phase === 'ready' && content?.type === 'pdf' && html` + <object data=${blobUrlRef.current} type="application/pdf" + class="preview-pdf" aria-label=${entry.name}> + <p class="page-message">${t('preview.pdf_fallback')}</p> + </object> + `} ${phase === 'ready' && content?.type === 'image' && html` <div class="preview-image-wrap"> <img class="preview-image" src=${blobUrlRef.current} alt=${entry.name} /> @@ -1542,27 +1578,6 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, return html` <div class="members-panel"> - <table class="admin-table"> - <thead> - <tr> - <th>${t('admin.col_username')}</th> - <th>${t('members.group_role')}</th> - </tr> - </thead> - <tbody> - ${members.map(m => html` - <tr key=${m.user_id}> - <td>${m.username}</td> - <td> - ${m.user_id === adminId - ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>` - : html`<span class="badge">${t('members.member')}</span>` - } - </td> - </tr> - `)} - </tbody> - </table> ${isAdmin && html` <form class="invite-form" onSubmit=${doInvite}> <h4>${t('members.invite_title')}</h4> @@ -1585,6 +1600,27 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, </div> </form> `} + <table class="admin-table"> + <thead> + <tr> + <th>${t('admin.col_username')}</th> + <th>${t('members.group_role')}</th> + </tr> + </thead> + <tbody> + ${members.map(m => html` + <tr key=${m.user_id}> + <td>${m.username}</td> + <td> + ${m.user_id === adminId + ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>` + : html`<span class="badge">${t('members.member')}</span>` + } + </td> + </tr> + `)} + </tbody> + </table> ${isNodeAdmin && html` <form class="invite-form" onSubmit=${doPair}> <h4>${t('members.pair_title')}</h4> diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index f0c5cef..e3e0def 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -116,6 +116,8 @@ const en = { 'video.loading': 'Loading {name}...', 'video.buffering': 'Buffering...', 'video.close': 'Close (Esc)', + 'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.', + 'group.upload_indexing': 'indexing…', 'video.err_transport': 'Transport not connected', 'video.err_mse': 'Codec not supported for streaming: {codec}', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 81c4130..361ff7e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -1411,3 +1411,12 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } -webkit-box-orient: vertical; overflow: hidden; } + +/* PDF preview: fills the overlay, scrolls in its own viewer. */ +.preview-pdf { + width: min(1000px, 92vw); + height: 82vh; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-surface); +} |