UNPKG

alinea

Version:

[![npm](https://img.shields.io/npm/v/alinea.svg)](https://npmjs.org/package/alinea) [![install size](https://packagephobia.com/badge?p=alinea)](https://packagephobia.com/result?p=alinea)

826 lines (823 loc) 28.7 kB
import { rgbaToThumbHash, thumbHashToAverageRGBA } from "../../chunks/chunk-57Y4IQL4.js"; import { I, m } from "../../chunks/chunk-57QP2MGK.js"; import { useAtom, useSetAtom } from "../../chunks/chunk-WF77DMLN.js"; import { atom } from "../../chunks/chunk-OBOPLPUQ.js"; import { pLimit } from "../../chunks/chunk-QQTYTFWR.js"; import { __commonJS, __toESM } from "../../chunks/chunk-U5RRZUYZ.js"; // node_modules/smartcrop/smartcrop.js var require_smartcrop = __commonJS({ "node_modules/smartcrop/smartcrop.js"(exports, module) { (function() { "use strict"; var smartcrop2 = {}; function NoPromises() { throw new Error("No native promises and smartcrop.Promise not set."); } smartcrop2.Promise = typeof Promise !== "undefined" ? Promise : NoPromises; smartcrop2.DEFAULTS = { width: 0, height: 0, aspect: 0, cropWidth: 0, cropHeight: 0, detailWeight: 0.2, skinColor: [0.78, 0.57, 0.44], skinBias: 0.01, skinBrightnessMin: 0.2, skinBrightnessMax: 1, skinThreshold: 0.8, skinWeight: 1.8, saturationBrightnessMin: 0.05, saturationBrightnessMax: 0.9, saturationThreshold: 0.4, saturationBias: 0.2, saturationWeight: 0.1, // Step * minscale rounded down to the next power of two should be good scoreDownSample: 8, step: 8, scaleStep: 0.1, minScale: 1, maxScale: 1, edgeRadius: 0.4, edgeWeight: -20, outsideImportance: -0.5, boostWeight: 100, ruleOfThirds: true, prescale: true, imageOperations: null, canvasFactory: defaultCanvasFactory, // Factory: defaultFactories, debug: false }; smartcrop2.crop = function(inputImage, options_, callback) { var options = extend({}, smartcrop2.DEFAULTS, options_); if (options.aspect) { options.width = options.aspect; options.height = 1; } if (options.imageOperations === null) { options.imageOperations = canvasImageOperations(options.canvasFactory); } var iop = options.imageOperations; var scale = 1; var prescale = 1; return iop.open(inputImage, options.input).then(function(image) { if (options.width && options.height) { scale = min( image.width / options.width, image.height / options.height ); options.cropWidth = ~~(options.width * scale); options.cropHeight = ~~(options.height * scale); options.minScale = min( options.maxScale, max(1 / scale, options.minScale) ); if (options.prescale !== false) { prescale = min(max(256 / image.width, 256 / image.height), 1); if (prescale < 1) { image = iop.resample( image, image.width * prescale, image.height * prescale ); options.cropWidth = ~~(options.cropWidth * prescale); options.cropHeight = ~~(options.cropHeight * prescale); if (options.boost) { options.boost = options.boost.map(function(boost) { return { x: ~~(boost.x * prescale), y: ~~(boost.y * prescale), width: ~~(boost.width * prescale), height: ~~(boost.height * prescale), weight: boost.weight }; }); } } else { prescale = 1; } } } return image; }).then(function(image) { return iop.getData(image).then(function(data) { var result = analyse(options, data); var crops = result.crops || [result.topCrop]; for (var i = 0, iLen = crops.length; i < iLen; i++) { var crop = crops[i]; crop.x = ~~(crop.x / prescale); crop.y = ~~(crop.y / prescale); crop.width = ~~(crop.width / prescale); crop.height = ~~(crop.height / prescale); } if (callback) callback(result); return result; }); }); }; smartcrop2.isAvailable = function(options) { if (!smartcrop2.Promise) return false; var canvasFactory = options ? options.canvasFactory : defaultCanvasFactory; if (canvasFactory === defaultCanvasFactory) { var c = document.createElement("canvas"); if (!c.getContext("2d")) { return false; } } return true; }; function edgeDetect(i, o) { var id = i.data; var od = o.data; var w = i.width; var h = i.height; for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var p = (y * w + x) * 4; var lightness; if (x === 0 || x >= w - 1 || y === 0 || y >= h - 1) { lightness = sample(id, p); } else { lightness = sample(id, p) * 4 - sample(id, p - w * 4) - sample(id, p - 4) - sample(id, p + 4) - sample(id, p + w * 4); } od[p + 1] = lightness; } } } function skinDetect(options, i, o) { var id = i.data; var od = o.data; var w = i.width; var h = i.height; for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var p = (y * w + x) * 4; var lightness = cie(id[p], id[p + 1], id[p + 2]) / 255; var skin = skinColor(options, id[p], id[p + 1], id[p + 2]); var isSkinColor = skin > options.skinThreshold; var isSkinBrightness = lightness >= options.skinBrightnessMin && lightness <= options.skinBrightnessMax; if (isSkinColor && isSkinBrightness) { od[p] = (skin - options.skinThreshold) * (255 / (1 - options.skinThreshold)); } else { od[p] = 0; } } } } function saturationDetect(options, i, o) { var id = i.data; var od = o.data; var w = i.width; var h = i.height; for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var p = (y * w + x) * 4; var lightness = cie(id[p], id[p + 1], id[p + 2]) / 255; var sat = saturation(id[p], id[p + 1], id[p + 2]); var acceptableSaturation = sat > options.saturationThreshold; var acceptableLightness = lightness >= options.saturationBrightnessMin && lightness <= options.saturationBrightnessMax; if (acceptableLightness && acceptableSaturation) { od[p + 2] = (sat - options.saturationThreshold) * (255 / (1 - options.saturationThreshold)); } else { od[p + 2] = 0; } } } } function applyBoosts(options, output) { if (!options.boost) return; var od = output.data; for (var i = 0; i < output.width; i += 4) { od[i + 3] = 0; } for (i = 0; i < options.boost.length; i++) { applyBoost(options.boost[i], options, output); } } function applyBoost(boost, options, output) { var od = output.data; var w = output.width; var x0 = ~~boost.x; var x1 = ~~(boost.x + boost.width); var y0 = ~~boost.y; var y1 = ~~(boost.y + boost.height); var weight = boost.weight * 255; for (var y = y0; y < y1; y++) { for (var x = x0; x < x1; x++) { var i = (y * w + x) * 4; od[i + 3] += weight; } } } function generateCrops(options, width, height) { var results = []; var minDimension = min(width, height); var cropWidth = options.cropWidth || minDimension; var cropHeight = options.cropHeight || minDimension; for (var scale = options.maxScale; scale >= options.minScale; scale -= options.scaleStep) { for (var y = 0; y + cropHeight * scale <= height; y += options.step) { for (var x = 0; x + cropWidth * scale <= width; x += options.step) { results.push({ x, y, width: cropWidth * scale, height: cropHeight * scale }); } } } return results; } function score(options, output, crop) { var result = { detail: 0, saturation: 0, skin: 0, boost: 0, total: 0 }; var od = output.data; var downSample2 = options.scoreDownSample; var invDownSample = 1 / downSample2; var outputHeightDownSample = output.height * downSample2; var outputWidthDownSample = output.width * downSample2; var outputWidth = output.width; for (var y = 0; y < outputHeightDownSample; y += downSample2) { for (var x = 0; x < outputWidthDownSample; x += downSample2) { var p = (~~(y * invDownSample) * outputWidth + ~~(x * invDownSample)) * 4; var i = importance(options, crop, x, y); var detail = od[p + 1] / 255; result.skin += od[p] / 255 * (detail + options.skinBias) * i; result.detail += detail * i; result.saturation += od[p + 2] / 255 * (detail + options.saturationBias) * i; result.boost += od[p + 3] / 255 * i; } } result.total = (result.detail * options.detailWeight + result.skin * options.skinWeight + result.saturation * options.saturationWeight + result.boost * options.boostWeight) / (crop.width * crop.height); return result; } function importance(options, crop, x, y) { if (crop.x > x || x >= crop.x + crop.width || crop.y > y || y >= crop.y + crop.height) { return options.outsideImportance; } x = (x - crop.x) / crop.width; y = (y - crop.y) / crop.height; var px = abs(0.5 - x) * 2; var py = abs(0.5 - y) * 2; var dx = Math.max(px - 1 + options.edgeRadius, 0); var dy = Math.max(py - 1 + options.edgeRadius, 0); var d = (dx * dx + dy * dy) * options.edgeWeight; var s = 1.41 - sqrt(px * px + py * py); if (options.ruleOfThirds) { s += Math.max(0, s + d + 0.5) * 1.2 * (thirds(px) + thirds(py)); } return s + d; } smartcrop2.importance = importance; function skinColor(options, r, g, b) { var mag = sqrt(r * r + g * g + b * b); var rd = r / mag - options.skinColor[0]; var gd = g / mag - options.skinColor[1]; var bd = b / mag - options.skinColor[2]; var d = sqrt(rd * rd + gd * gd + bd * bd); return 1 - d; } function analyse(options, input) { var result = {}; var output = new ImgData(input.width, input.height); edgeDetect(input, output); skinDetect(options, input, output); saturationDetect(options, input, output); applyBoosts(options, output); var scoreOutput = downSample(output, options.scoreDownSample); var topScore = -Infinity; var topCrop = null; var crops = generateCrops(options, input.width, input.height); for (var i = 0, iLen = crops.length; i < iLen; i++) { var crop = crops[i]; crop.score = score(options, scoreOutput, crop); if (crop.score.total > topScore) { topCrop = crop; topScore = crop.score.total; } } result.topCrop = topCrop; if (options.debug && topCrop) { result.crops = crops; result.debugOutput = output; result.debugOptions = options; result.debugTopCrop = extend({}, result.topCrop); } return result; } function ImgData(width, height, data) { this.width = width; this.height = height; if (data) { this.data = new Uint8ClampedArray(data); } else { this.data = new Uint8ClampedArray(width * height * 4); } } smartcrop2.ImgData = ImgData; function downSample(input, factor) { var idata = input.data; var iwidth = input.width; var width = Math.floor(input.width / factor); var height = Math.floor(input.height / factor); var output = new ImgData(width, height); var data = output.data; var ifactor2 = 1 / (factor * factor); for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var i = (y * width + x) * 4; var r = 0; var g = 0; var b = 0; var a = 0; var mr = 0; var mg = 0; for (var v = 0; v < factor; v++) { for (var u = 0; u < factor; u++) { var j = ((y * factor + v) * iwidth + (x * factor + u)) * 4; r += idata[j]; g += idata[j + 1]; b += idata[j + 2]; a += idata[j + 3]; mr = Math.max(mr, idata[j]); mg = Math.max(mg, idata[j + 1]); } } data[i] = r * ifactor2 * 0.5 + mr * 0.5; data[i + 1] = g * ifactor2 * 0.7 + mg * 0.3; data[i + 2] = b * ifactor2; data[i + 3] = a * ifactor2; } } return output; } smartcrop2._downSample = downSample; function defaultCanvasFactory(w, h) { var c = document.createElement("canvas"); c.width = w; c.height = h; return c; } function canvasImageOperations(canvasFactory) { return { // Takes imageInput as argument // returns an object which has at least // {width: n, height: n} open: function(image) { var w = image.naturalWidth || image.width; var h = image.naturalHeight || image.height; var c = canvasFactory(w, h); var ctx = c.getContext("2d"); if (image.naturalWidth && (image.naturalWidth != image.width || image.naturalHeight != image.height)) { c.width = image.naturalWidth; c.height = image.naturalHeight; } else { c.width = image.width; c.height = image.height; } ctx.drawImage(image, 0, 0); return smartcrop2.Promise.resolve(c); }, // Takes an image (as returned by open), and changes it's size by resampling resample: function(image, width, height) { return Promise.resolve(image).then(function(image2) { var c = canvasFactory(~~width, ~~height); var ctx = c.getContext("2d"); ctx.drawImage( image2, 0, 0, image2.width, image2.height, 0, 0, c.width, c.height ); return smartcrop2.Promise.resolve(c); }); }, getData: function(image) { return Promise.resolve(image).then(function(c) { var ctx = c.getContext("2d"); var id = ctx.getImageData(0, 0, c.width, c.height); return new ImgData(c.width, c.height, id.data); }); } }; } smartcrop2._canvasImageOperations = canvasImageOperations; var min = Math.min; var max = Math.max; var abs = Math.abs; var sqrt = Math.sqrt; function extend(o) { for (var i = 1, iLen = arguments.length; i < iLen; i++) { var arg = arguments[i]; if (arg) { for (var name in arg) { o[name] = arg[name]; } } } return o; } function thirds(x) { x = ((x - 1 / 3 + 1) % 2 * 0.5 - 0.5) * 16; return Math.max(1 - x * x, 0); } function cie(r, g, b) { return 0.5126 * b + 0.7152 * g + 0.0722 * r; } function sample(id, p) { return cie(id[p], id[p + 1], id[p + 2]); } function saturation(r, g, b) { var maximum = max(r / 255, g / 255, b / 255); var minimum = min(r / 255, g / 255, b / 255); if (maximum === minimum) { return 0; } var l = (maximum + minimum) / 2; var d = maximum - minimum; return l > 0.5 ? d / (2 - maximum - minimum) : d / (maximum + minimum); } if (typeof define !== "undefined" && define.amd) define(function() { return smartcrop2; }); if (typeof exports !== "undefined") exports.smartcrop = smartcrop2; else if (typeof navigator !== "undefined") window.SmartCrop = window.smartcrop = smartcrop2; if (typeof module !== "undefined") { module.exports = smartcrop2; } })(); } }); // src/dashboard/hook/UseUploads.ts import { Media } from "alinea/backend/Media"; import { createFileHash } from "alinea/backend/util/ContentHash"; import { Entry, EntryPhase, HttpError, Workspace } from "alinea/core"; import { entryFileName, entryFilepath } from "alinea/core/EntryFilenames"; import { createId } from "alinea/core/Id"; import { MutationType } from "alinea/core/Mutation"; import { base64 } from "alinea/core/util/Encoding"; import { createEntryRow } from "alinea/core/util/EntryRows"; import { generateKeyBetween } from "alinea/core/util/FractionalIndexing"; import { basename, dirname, extname, join, normalize } from "alinea/core/util/Paths"; var import_smartcrop = __toESM(require_smartcrop(), 1); import { useEffect } from "react"; import { useMutate } from "../atoms/DbAtoms.js"; import { errorAtom } from "../atoms/ErrorAtoms.js"; import { withResolvers } from "../util/WithResolvers.js"; import { useConfig } from "./UseConfig.js"; import { useGraph } from "./UseGraph.js"; import { useSession } from "./UseSession.js"; var UploadStatus = /* @__PURE__ */ ((UploadStatus2) => { UploadStatus2[UploadStatus2["Queued"] = 0] = "Queued"; UploadStatus2[UploadStatus2["CreatingPreview"] = 1] = "CreatingPreview"; UploadStatus2[UploadStatus2["Uploading"] = 2] = "Uploading"; UploadStatus2[UploadStatus2["Uploaded"] = 3] = "Uploaded"; UploadStatus2[UploadStatus2["Done"] = 4] = "Done"; return UploadStatus2; })(UploadStatus || {}); var defaultTasker = pLimit(Infinity); var cpuTasker = pLimit(1); var networkTasker = pLimit(8); var batchTasker = pLimit(1); var tasker = { [0 /* Queued */]: defaultTasker, [1 /* CreatingPreview */]: cpuTasker, [2 /* Uploading */]: networkTasker, [3 /* Uploaded */]: defaultTasker, [4 /* Done */]: defaultTasker }; async function process(upload, publishUpload, client) { switch (upload.status) { case 0 /* Queued */: const isImage = Media.isImage(upload.file.name); const next = isImage ? 1 /* CreatingPreview */ : 2 /* Uploading */; return { ...upload, status: next }; case 1 /* CreatingPreview */: { const url = URL.createObjectURL(upload.file); const image = await new Promise((resolve, reject) => { const image2 = new Image(); image2.onload = () => resolve(image2); image2.onerror = (err) => reject(err); image2.src = url; }).finally(() => URL.revokeObjectURL(url)); const size = Math.max(image.width, image.height); const thumbW = Math.round(100 * image.width / size); const thumbH = Math.round(100 * image.height / size); const thumbCanvas = document.createElement("canvas"); const thumbContext = thumbCanvas.getContext("2d"); thumbCanvas.width = thumbW; thumbCanvas.height = thumbH; thumbContext.drawImage(image, 0, 0, thumbW, thumbH); const pixels = thumbContext.getImageData(0, 0, thumbW, thumbH); const thumbHash = rgbaToThumbHash(thumbW, thumbH, pixels.data); const { r, g, b, a } = thumbHashToAverageRGBA(thumbHash); const averageColor = I(m(r * 255, g * 255, b * 255, a)); const previewW = Math.min( Math.round(160 * image.width / size), image.width ); const previewH = Math.min( Math.round(160 * image.height / size), image.height ); const previewCanvas = document.createElement("canvas"); const previewContext = previewCanvas.getContext("2d"); previewContext.imageSmoothingEnabled = true; previewContext.imageSmoothingQuality = "high"; previewCanvas.width = previewW; previewCanvas.height = previewH; previewContext.drawImage(image, 0, 0, previewW, previewH); const preview = previewCanvas.toDataURL("image/webp"); const crop = await import_smartcrop.default.crop(image, { width: 100, height: 100 }); const focus = { x: (crop.topCrop.x + crop.topCrop.width / 2) / image.width, y: (crop.topCrop.y + crop.topCrop.height / 2) / image.height }; return { ...upload, preview, averageColor, focus, thumbHash: base64.stringify(thumbHash), width: image.naturalWidth, height: image.naturalHeight, status: 2 /* Uploading */ }; } case 2 /* Uploading */: { const fileName = upload.file.name; const file = join(upload.to.directory, fileName); const info = await client.prepareUpload(file); await fetch(info.upload.url, { method: info.upload.method ?? "POST", body: upload.file }).then(async (result) => { if (!result.ok) throw new HttpError( result.status, `Could not reach server for upload` ); }); return { ...upload, info, status: 3 /* Uploaded */ }; } case 3 /* Uploaded */: { const { replace } = upload; const info = upload.info; const entry = await publishUpload(upload); return { ...upload, result: entry, status: 4 /* Done */ }; } case 4 /* Done */: throw new Error("Should not end up here"); } } function createBatch(mutate) { let trigger = withResolvers(); let nextRun = void 0; const batch = []; async function run() { const todo = batch.splice(0, batch.length); try { await batchTasker(() => mutate(...todo)); trigger.resolve(void 0); } catch (error) { trigger.reject(error); } finally { trigger = withResolvers(); } } return (...mutations) => { batch.push(...mutations); clearTimeout(nextRun); nextRun = setTimeout(run, 200); return trigger.promise; }; } var uploadsAtom = atom([]); function useUploads(onSelect) { const config = useConfig(); const graph = useGraph(); const { cnx: client } = useSession(); const mutate = useMutate(); const setErrorAtom = useSetAtom(errorAtom); const [uploads, setUploads] = useAtom(uploadsAtom); const batch = createBatch(mutate); useEffect(() => { return () => setUploads([]); }, []); async function batchMutations(...mutations) { await batch(...mutations); } async function createEntry(upload2) { const entryId = upload2.info?.entryId ?? createId(); const { parentId } = upload2.to; const buffer = await upload2.file.arrayBuffer(); const parent = await graph.preferPublished.maybeGet( Entry({ entryId: parentId }).select({ level: Entry.level, entryId: Entry.entryId, url: Entry.url, path: Entry.path, parentPaths({ parents }) { return parents().select(Entry.path); } }) ); const prev = await graph.preferPublished.maybeGet(Entry({ parent: parentId })); const extension = extname(upload2.file.name.toLowerCase()); const path = basename(upload2.file.name.toLowerCase(), extension); const entryLocation = { workspace: upload2.to.workspace, root: upload2.to.root, locale: null, path, phase: EntryPhase.Published }; const filePath = entryFilepath( config, entryLocation, parent ? parent.parentPaths.concat(parent.path) : [] ); const parentDir = dirname(filePath); const { location } = upload2.info; const workspace = Workspace.data(config.workspaces[upload2.to.workspace]); const prefix = workspace.mediaDir && normalize(workspace.mediaDir); const fileLocation = prefix && location.startsWith(prefix) ? location.slice(prefix.length) : location; const hash = await createFileHash(new Uint8Array(buffer)); const entry = await createEntryRow(config, { ...entryLocation, parent: parent?.entryId ?? null, entryId, type: "MediaFile", url: (parent ? parent.url : "") + "/" + path, title: basename(path, extension), seeded: false, modifiedAt: Date.now(), searchableText: "", index: generateKeyBetween(null, prev?.index ?? null), i18nId: entryId, level: parent ? parent.level + 1 : 0, parentDir, filePath, childrenDir: filePath.slice(0, -".json".length), active: true, main: true, data: { title: basename(path, extension), location: fileLocation, extension, size: buffer.byteLength, hash, width: upload2.width, height: upload2.height, averageColor: upload2.averageColor, focus: upload2.focus, thumbHash: upload2.thumbHash, preview: upload2.preview } }); const file = entryFileName( config, entry, parent ? parent.parentPaths.concat(parent.path) : [] ); return { file, entry }; } async function uploadFile(upload2) { function update(upload3) { setUploads((current) => { const result = current.slice(); const index = current.findIndex((u) => u.id === upload3.id); if (index === -1) return result; result[index] = upload3; return result; }); } while (true) { const next = await tasker[upload2.status]( () => process(upload2, publishUpload, client) ).catch((error) => { return { ...upload2, error, status: 4 /* Done */ }; }); update(next); if (next.status === 4 /* Done */) { if (next.error) { setErrorAtom(next.error.message, next.error); } const result = next.result; if (!result) break; onSelect?.(result); break; } else { upload2 = next; } } } async function publishUpload(upload2) { const { replace } = upload2; const info = upload2.info; const { file, entry } = await createEntry(upload2); if (!replace) { await batchMutations( { type: MutationType.Create, entryId: entry.entryId, file, entry }, { type: MutationType.Upload, entryId: entry.entryId, url: info.previewUrl, file: info.location } ); return entry; } const newEntry = await createEntryRow(config, { ...replace.entry, data: { ...entry.data, title: replace.entry.title } }); const mediaFile = replace.entry.data; await batchMutations( { type: MutationType.Edit, entryId: replace.entry.entryId, file: replace.entryFile, entry: newEntry }, { type: MutationType.Upload, entryId: replace.entry.entryId, url: info.previewUrl, file: info.location }, { type: MutationType.FileRemove, entryId: replace.entry.entryId, file: replace.entryFile, workspace: replace.entry.workspace, location: Media.ORIGINAL_LOCATION in mediaFile ? mediaFile[Media.ORIGINAL_LOCATION] : mediaFile.location, replace: true } ); return newEntry; } async function upload(files, to, replace) { const uploads2 = Array.from(files).map((file) => { return { id: createId(), file, to, replace, status: 0 /* Queued */ }; }); setUploads((current) => [...uploads2, ...current]); return Promise.all(uploads2.map(uploadFile)); } return { upload, uploads }; } export { UploadStatus, useUploads };