spb/drive Public
SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.
JavaScript 82.7%
CSS 10.6%
Nunjucks 3.6%
Shell 1.8%
SQL 1.3%
1/**2 * ─────────────────────────────────────────────3 * SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/web/assets/js/share-page.js8 * Purpose : Public share pages — file preview + read-only folder browser9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { h, fmtSize } from './ui.js';14import { fileIcon, folderIcon, UI } from './icons.js';15import { renderPreview, Viewer } from './viewer.js';1617const page = document.querySelector('.share-page');18const token = page.dataset.token;19const allowDownload = page.dataset.allowDownload === '1';2021document.getElementById('shareIcon').innerHTML = page.dataset.folder22 ? folderIcon({})23 : fileIcon('file');2425if (page.dataset.folder) {26 bootFolder();27} else {28 bootFile();29}3031// ── Single file share ────────────────────────────────────────────────32async function bootFile() {33 const stage = document.getElementById('shareStage');34 stage.append(h('div.pv-processing', { style: { color: 'var(--text)' } }, 'Loading preview…'));35 try {36 const res = await fetch(`/s/${token}/preview`, { credentials: 'same-origin' });37 if (!res.ok) throw new Error('preview unavailable');38 const desc = await res.json();39 document.getElementById('shareIcon').innerHTML = fileIcon(iconFor(desc.strategy));40 stage.innerHTML = '';41 await renderPreview(stage, desc, {42 stream: `/s/${token}/stream`,43 dl: allowDownload ? `/s/${token}/dl` : null,44 });45 } catch {46 stage.innerHTML = '';47 stage.append(h('div.fallback-card', {},48 h('div', { html: fileIcon('file') }),49 h('h3', {}, page.dataset.name),50 h('div.fm', {}, fmtSize(page.dataset.size)),51 allowDownload ? h('a.btn.primary', { href: `/s/${token}/dl`, download: '' }, 'Download') : null));52 }53}5455function iconFor(strategy) {56 const map = {57 image: 'image', svg: 'image', heic: 'image', video: 'video', audio: 'audio',58 pdf: 'pdf', office: 'doc', markdown: 'text', csv: 'sheet', notebook: 'code',59 structured: 'code', archive: 'archive', font: 'font', email: 'mail',60 epub: 'book', model3d: 'cube', code: 'code',61 };62 return map[strategy] ?? 'file';63}6465// ── Folder share browser ─────────────────────────────────────────────66async function bootFolder() {67 const rootId = Number(page.dataset.nodeId);68 const browser = document.getElementById('shareBrowser');69 const crumbsEl = document.getElementById('shareCrumbs');7071 const load = async (folderId) => {72 browser.innerHTML = '<div class="grid">' + '<div class="skeleton"></div>'.repeat(6) + '</div>';73 const res = await fetch(`/s/${token}/api/children?id=${folderId}`, { credentials: 'same-origin' });74 if (!res.ok) { browser.innerHTML = '<p class="muted">Could not load folder.</p>'; return; }75 const { children, path } = await res.json();7677 crumbsEl.innerHTML = '';78 path.forEach((part, i) => {79 if (i > 0) crumbsEl.append(h('span.crumb-sep', {}, '›'));80 crumbsEl.append(h(`button.crumb${i === path.length - 1 ? '.current' : ''}`, {81 onclick: () => load(part.id),82 }, part.name || page.dataset.name));83 });8485 browser.innerHTML = '';86 if (!children.length) {87 browser.append(h('p.muted', { style: { textAlign: 'center', padding: '40px' } }, 'This folder is empty.'));88 return;89 }90 const grid = h('div.grid', { style: { '--card': '168px' } });91 const files = children.filter((c) => c.type === 'file');92 for (const child of children) {93 const thumb = child.hasThumb94 ? h('img', { loading: 'lazy', src: `/s/${token}/thumb/${child.id}?size=256`, alt: '' })95 : h('span', { html: child.type === 'folder' ? folderIcon(child) : fileIcon(child.icon), style: { display: 'contents' } });96 const card = h('div.card', { tabindex: 0 },97 h('div.thumb', {}, thumb),98 h('div.meta', {},99 h('span', { html: child.type === 'folder' ? folderIcon(child) : fileIcon(child.icon), style: { display: 'contents' } }),100 h('span.name', { title: child.name }, child.name)));101 card.addEventListener('dblclick', () => open(child, files));102 card.addEventListener('click', () => open(child, files));103 grid.append(card);104 }105 browser.append(grid);106 };107108 const open = (child, files) => {109 if (child.type === 'folder') { load(child.id); return; }110 const index = files.findIndex((f) => f.id === child.id);111 new Viewer(files, Math.max(index, 0), {112 descUrl: (n) => `/s/${token}/preview/${n.id}`,113 streamUrl: (n) => `/s/${token}/stream/${n.id}`,114 dlUrl: allowDownload ? (n) => `/s/${token}/dl/${n.id}` : null,115 actions: allowDownload116 ? [{ title: 'Download', icon: UI.download, onClick: (n) => { location.href = `/s/${token}/dl/${n.id}`; } }]117 : [],118 });119 };120121 load(rootId);122}123