UNPKG

@e280/quay

Version:

File-browser and outliner UI for the web

134 lines 4.47 kB
import { Kv, StorageDriver } from "@e280/kv"; import { MediaGroup } from "./group.js"; import { Cellar } from "../../../cellar/cellar.js"; export class MediaLibrary extends MediaGroup { static async open(scope = "default") { const group = new this(); group.cellar = await Cellar.opfs("media"); group.#index = mediaIndex(scope); await group.#load(); return group; } #objectUrls = new Map(); #index; cellar = new Cellar(); constructor() { super(); this.#index = mediaIndex("default"); this.on.upload.sub(({ files, target }) => { return this.#upload(files, target); }); this.on.delete.sub(({ items }) => { const hashes = this.#hashes(items); return this.#delete(hashes); }); } async *records() { for await (const [, record] of this.#index.entries()) yield record; } async #load() { for await (const record of this.records()) { if (record.format === "image" && !this.#objectUrls.has(record.hash)) await this.#loadPreview(record); this.#attachRecord(record, this.config.root); } } async #upload(files, parent) { await Promise.all(files.map(file => this.#storeFile(file, parent))); } async #storeFile(file, parent) { const cask = await this.cellar.save(file); const existing = await this.#index.get(cask.hash); const now = Date.now(); const record = { hash: cask.hash, label: existing?.label ?? file.name, format: mediaFormat(file.type), mime: file.type, size: file.size, createdAt: existing?.createdAt ?? now, updatedAt: now, }; await this.#index.set(record.hash, record); if (record.format === "image") this.#setPreview(record.hash, URL.createObjectURL(file)); this.#attachRecord(record, parent); return record; } #hashes(items) { return items .map(item => item.isKind("file") ? item.specimen.hash : undefined) .filter((hash) => !!hash); } async #delete(hashes) { for (const hash of hashes) { await this.#index.del(hash); await this.cellar.delete(hash); this.#revokePreview(hash); } } findByHash(hash) { for (const [item] of this.config.root.crawl()) { if (item.isKind("file") && item.specimen.hash === hash) return item; } } dispose() { for (const url of this.#objectUrls.values()) URL.revokeObjectURL(url); this.#objectUrls.clear(); } #attachRecord(record, parent = this.config.root) { const existing = this.findByHash(record.hash); if (existing) return existing; const item = this.config.codex.create("file", { hash: record.hash, label: record.label, format: record.format, mime: record.mime, size: record.size, previewUrl: this.#objectUrls.get(record.hash) ?? null, }); parent.attach(item); return item; } async #loadPreview(record) { const cask = await this.cellar.load(record.hash); const blob = new Blob([cask.file], { type: record.mime || "image/*" }); return this.#setPreview(record.hash, URL.createObjectURL(blob)); } #setPreview(hash, url) { this.#revokePreview(hash); this.#objectUrls.set(hash, url); return url; } #revokePreview(hash) { const url = this.#objectUrls.get(hash); if (url) { URL.revokeObjectURL(url); this.#objectUrls.delete(hash); } } } const mediaFormats = new Set(["video", "image", "audio"]); const memoryIndexes = new Map(); function mediaFormat(mime) { const [type] = mime.split("/"); return mediaFormats.has(type) ? type : "other"; } function mediaIndex(scope) { const storage = globalThis.localStorage; if (storage) return new Kv(new StorageDriver(storage)) .scope("quay.media") .scope(scope); const existing = memoryIndexes.get(scope); if (existing) return existing; const index = new Kv(); memoryIndexes.set(scope, index); return index; } //# sourceMappingURL=library.js.map