SPB Git

Repository Public

lib.ts 4.6 KB 69 lines typescript

1// Small helpers shared by the explorer's files: query key, tag colours, downloads, preferences, dropped folders.2import { useCallback, useState, useSyncExternalStore } from 'react';3import { qs } from '../api.ts';4import type { Item } from '../types.ts';56/** Root of every react-query key owned by the explorer: one invalidation refreshes listings, details and tags. */7export const XKEY = 'explorer';89export const TAG_COLORS = ['slate', 'red', 'orange', 'amber', 'green', 'teal', 'blue', 'violet', 'pink'] as const;10export const TAG_STYLE: Record<string, { chip: string; dot: string }> = {11  slate: { chip: 'bg-slate-500/15 text-slate-700 dark:text-slate-300', dot: 'bg-slate-500' },12  red: { chip: 'bg-red-500/15 text-red-700 dark:text-red-300', dot: 'bg-red-500' },13  orange: { chip: 'bg-orange-500/15 text-orange-800 dark:text-orange-300', dot: 'bg-orange-500' },14  amber: { chip: 'bg-amber-500/20 text-amber-800 dark:text-amber-300', dot: 'bg-amber-500' },15  green: { chip: 'bg-green-500/15 text-green-800 dark:text-green-300', dot: 'bg-green-500' },16  teal: { chip: 'bg-teal-500/15 text-teal-800 dark:text-teal-300', dot: 'bg-teal-500' },17  blue: { chip: 'bg-blue-500/15 text-blue-700 dark:text-blue-300', dot: 'bg-blue-500' },18  violet: { chip: 'bg-violet-500/15 text-violet-700 dark:text-violet-300', dot: 'bg-violet-500' },19  pink: { chip: 'bg-pink-500/15 text-pink-700 dark:text-pink-300', dot: 'bg-pink-500' },20};21export const tagStyle = (color: string) => TAG_STYLE[color] ?? TAG_STYLE.slate!;2223export interface Sel { fileIds: string[]; folderIds: string[] }24export const toSel = (items: Item[]): Sel => ({ fileIds: items.filter((i) => i.type === 'file').map((i) => i.id), folderIds: items.filter((i) => i.type === 'folder').map((i) => i.id) });2526/** Start a browser download without leaving the page (the API answers with an attachment). */27export function triggerDownload(url: string): void {28  const a = document.createElement('a');29  a.href = url; a.download = ''; a.rel = 'noopener'; a.style.display = 'none';30  document.body.appendChild(a); a.click(); a.remove();31}32export const zipUrl = (s: Sel) => `/api/v1/zip${qs({ files: s.fileIds.join(','), folders: s.folderIds.join(',') })}`;3334/** A string preference kept in localStorage. */35export function usePref<T extends string>(key: string, initial: T, allowed: readonly T[]): [T, (v: T) => void] {36  const [v, setV] = useState<T>(() => { const s = localStorage.getItem(key) as T | null; return s && allowed.includes(s) ? s : initial; });37  const set = useCallback((n: T) => { localStorage.setItem(key, n); setV(n); }, [key]);38  return [v, set];39}40export function useMedia(query: string): boolean {41  return useSyncExternalStore((cb) => { const m = window.matchMedia(query); m.addEventListener('change', cb); return () => m.removeEventListener('change', cb); }, () => window.matchMedia(query).matches);42}43export const isTouchDevice = () => navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;4445/** A file chosen for upload; `relativePath` ("Dossier/sous/fichier.txt") recreates the folders of a folder upload. */46export interface Picked { file: File; relativePath?: string }47export const pickedFromInput = (list: FileList | null): Picked[] => [...(list ?? [])].map((file) => ({ file, relativePath: file.webkitRelativePath || undefined }));4849const readAll = (dir: FileSystemDirectoryEntry) => new Promise<FileSystemEntry[]>((resolve, reject) => {50  const reader = dir.createReader(), out: FileSystemEntry[] = [];51  const next = () => reader.readEntries((batch) => { if (!batch.length) resolve(out); else { out.push(...batch); next(); } }, reject); // readEntries answers in batches52  next();53});54async function walk(entry: FileSystemEntry, out: Picked[], top: boolean): Promise<void> {55  if (entry.isFile) {56    const file = await new Promise<File>((res, rej) => (entry as FileSystemFileEntry).file(res, rej));57    out.push({ file, relativePath: top ? undefined : entry.fullPath.replace(/^\/+/, '') });58  } else if (entry.isDirectory) for (const child of await readAll(entry as FileSystemDirectoryEntry)) await walk(child, out, false);59}60/** Files of a desktop drop, walking dropped folders. The entries are taken before the first await: the DataTransfer dies with the event. */61export async function filesFromDrop(dt: DataTransfer): Promise<Picked[]> {62  const entries = [...dt.items].filter((i) => i.kind === 'file').map((i) => i.webkitGetAsEntry?.() ?? null);63  const plain = [...dt.files];64  if (!entries.some(Boolean)) return plain.map((file) => ({ file }));65  const out: Picked[] = [];66  for (const e of entries) if (e) await walk(e, out, true).catch(() => {});67  return out;68}69
Files main
Menu

Theme