SPB Git

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%
5.3 KB · 123 lines javascript
Raw Blame History
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