@e280/quay
Version:
File-browser and outliner UI for the web
134 lines • 4.47 kB
JavaScript
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