/** * ───────────────────────────────────────────── * SPB Drive — Personal Cloud Drive * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/preview/router.mjs * Purpose : MIME/extension → preview strategy dispatcher * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { OFFICE_EXTS } from './office.mjs'; import { archiveKind } from './archive.mjs'; import { EXT_LANG } from './code.mjs'; const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'bmp', 'ico']); const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'mpg', 'mpeg', 'wmv', 'flv', 'ts']); const AUDIO_EXTS = new Set(['mp3', 'wav', 'flac', 'ogg', 'oga', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'aif']); const FONT_EXTS = new Set(['ttf', 'otf', 'woff', 'woff2']); const STRUCTURED_EXTS = new Set(['json', 'yaml', 'yml', 'toml', 'xml', 'plist']); const MODEL3D_EXTS = new Set(['stl', 'glb', 'gltf', 'obj']); /** Lowercased extension without the dot ('' if none). */ export function extOf(name) { const base = String(name).toLowerCase(); const special = base.endsWith('.tar.gz') ? 'tar.gz' : null; if (special) return special; const dot = base.lastIndexOf('.'); return dot > 0 ? base.slice(dot + 1) : ''; } /** * Decide how a file previews. * @param {{name: string, mime?: string, size?: number}} node * @returns {{strategy: string, ext: string}} */ export function previewStrategy(node) { const name = node.name ?? ''; const mime = node.mime ?? ''; const ext = extOf(name); if (ext === 'svg' || mime === 'image/svg+xml') return { strategy: 'svg', ext }; if (ext === 'heic' || ext === 'heif') return { strategy: 'heic', ext }; if (IMAGE_EXTS.has(ext) || mime.startsWith('image/')) return { strategy: 'image', ext }; if (VIDEO_EXTS.has(ext) || mime.startsWith('video/')) return { strategy: 'video', ext }; if (AUDIO_EXTS.has(ext) || mime.startsWith('audio/')) return { strategy: 'audio', ext }; if (ext === 'pdf' || mime === 'application/pdf') return { strategy: 'pdf', ext }; if (OFFICE_EXTS.has(ext)) return { strategy: 'office', ext }; if (ext === 'md' || ext === 'markdown') return { strategy: 'markdown', ext }; if (ext === 'csv' || ext === 'tsv') return { strategy: 'csv', ext }; if (ext === 'ipynb') return { strategy: 'notebook', ext }; if (STRUCTURED_EXTS.has(ext)) return { strategy: 'structured', ext }; if (archiveKind(name)) return { strategy: 'archive', ext }; if (FONT_EXTS.has(ext)) return { strategy: 'font', ext }; if (ext === 'eml') return { strategy: 'email', ext }; if (ext === 'epub') return { strategy: 'epub', ext }; if (MODEL3D_EXTS.has(ext)) return { strategy: 'model3d', ext }; if (ext === 'html' || ext === 'htm') return { strategy: 'code', ext }; if (EXT_LANG[ext] || mime.startsWith('text/')) return { strategy: 'code', ext }; return { strategy: 'fallback', ext }; } /** Which thumbnail pipeline (if any) applies to a file. */ export function thumbKind(node) { const { strategy } = previewStrategy(node); if (['image', 'heic'].includes(strategy)) return 'image'; if (strategy === 'video') return 'video'; if (strategy === 'pdf') return 'pdf'; if (strategy === 'office') return 'office'; // via converted PDF when cached return null; } /** Icon family for the crisp per-extension SVG icon set. */ export function iconFamily(node) { if (node.type === 'folder') return 'folder'; const { strategy, ext } = previewStrategy(node); const map = { image: 'image', svg: 'image', heic: 'image', video: 'video', audio: 'audio', pdf: 'pdf', office: /x?ls|ods/.test(ext) ? 'sheet' : /pptx?|odp/.test(ext) ? 'slides' : 'doc', markdown: 'text', csv: 'sheet', notebook: 'code', structured: 'code', archive: 'archive', font: 'font', email: 'mail', epub: 'book', model3d: 'cube', code: 'code', fallback: 'file', }; return map[strategy] ?? 'file'; }