/** * ───────────────────────────────────────────── * SPB Drive — Personal Cloud Drive * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/web/assets/js/share-page.js * Purpose : Public share pages — file preview + read-only folder browser * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { h, fmtSize } from './ui.js'; import { fileIcon, folderIcon, UI } from './icons.js'; import { renderPreview, Viewer } from './viewer.js'; const page = document.querySelector('.share-page'); const token = page.dataset.token; const allowDownload = page.dataset.allowDownload === '1'; document.getElementById('shareIcon').innerHTML = page.dataset.folder ? folderIcon({}) : fileIcon('file'); if (page.dataset.folder) { bootFolder(); } else { bootFile(); } // ── Single file share ──────────────────────────────────────────────── async function bootFile() { const stage = document.getElementById('shareStage'); stage.append(h('div.pv-processing', { style: { color: 'var(--text)' } }, 'Loading preview…')); try { const res = await fetch(`/s/${token}/preview`, { credentials: 'same-origin' }); if (!res.ok) throw new Error('preview unavailable'); const desc = await res.json(); document.getElementById('shareIcon').innerHTML = fileIcon(iconFor(desc.strategy)); stage.innerHTML = ''; await renderPreview(stage, desc, { stream: `/s/${token}/stream`, dl: allowDownload ? `/s/${token}/dl` : null, }); } catch { stage.innerHTML = ''; stage.append(h('div.fallback-card', {}, h('div', { html: fileIcon('file') }), h('h3', {}, page.dataset.name), h('div.fm', {}, fmtSize(page.dataset.size)), allowDownload ? h('a.btn.primary', { href: `/s/${token}/dl`, download: '' }, 'Download') : null)); } } function iconFor(strategy) { const map = { image: 'image', svg: 'image', heic: 'image', video: 'video', audio: 'audio', pdf: 'pdf', office: 'doc', markdown: 'text', csv: 'sheet', notebook: 'code', structured: 'code', archive: 'archive', font: 'font', email: 'mail', epub: 'book', model3d: 'cube', code: 'code', }; return map[strategy] ?? 'file'; } // ── Folder share browser ───────────────────────────────────────────── async function bootFolder() { const rootId = Number(page.dataset.nodeId); const browser = document.getElementById('shareBrowser'); const crumbsEl = document.getElementById('shareCrumbs'); const load = async (folderId) => { browser.innerHTML = '
' + '
'.repeat(6) + '
'; const res = await fetch(`/s/${token}/api/children?id=${folderId}`, { credentials: 'same-origin' }); if (!res.ok) { browser.innerHTML = '

Could not load folder.

'; return; } const { children, path } = await res.json(); crumbsEl.innerHTML = ''; path.forEach((part, i) => { if (i > 0) crumbsEl.append(h('span.crumb-sep', {}, '›')); crumbsEl.append(h(`button.crumb${i === path.length - 1 ? '.current' : ''}`, { onclick: () => load(part.id), }, part.name || page.dataset.name)); }); browser.innerHTML = ''; if (!children.length) { browser.append(h('p.muted', { style: { textAlign: 'center', padding: '40px' } }, 'This folder is empty.')); return; } const grid = h('div.grid', { style: { '--card': '168px' } }); const files = children.filter((c) => c.type === 'file'); for (const child of children) { const thumb = child.hasThumb ? h('img', { loading: 'lazy', src: `/s/${token}/thumb/${child.id}?size=256`, alt: '' }) : h('span', { html: child.type === 'folder' ? folderIcon(child) : fileIcon(child.icon), style: { display: 'contents' } }); const card = h('div.card', { tabindex: 0 }, h('div.thumb', {}, thumb), h('div.meta', {}, h('span', { html: child.type === 'folder' ? folderIcon(child) : fileIcon(child.icon), style: { display: 'contents' } }), h('span.name', { title: child.name }, child.name))); card.addEventListener('dblclick', () => open(child, files)); card.addEventListener('click', () => open(child, files)); grid.append(card); } browser.append(grid); }; const open = (child, files) => { if (child.type === 'folder') { load(child.id); return; } const index = files.findIndex((f) => f.id === child.id); new Viewer(files, Math.max(index, 0), { descUrl: (n) => `/s/${token}/preview/${n.id}`, streamUrl: (n) => `/s/${token}/stream/${n.id}`, dlUrl: allowDownload ? (n) => `/s/${token}/dl/${n.id}` : null, actions: allowDownload ? [{ title: 'Download', icon: UI.download, onClick: (n) => { location.href = `/s/${token}/dl/${n.id}`; } }] : [], }); }; load(rootId); }