diff --git a/plugins/externalUpload/index.jsx b/plugins/externalUpload/index.jsx index 0cc36b9..56829e8 100644 --- a/plugins/externalUpload/index.jsx +++ b/plugins/externalUpload/index.jsx @@ -57,6 +57,7 @@ export function onLoad() { plugin.store.secretAccessKey ??= ""; plugin.store.bucket ??= ""; plugin.store.publicUrl ??= ""; + plugin.store.previews ??= ""; updateConfig(); diff --git a/plugins/externalUpload/modal.jsx b/plugins/externalUpload/modal.jsx index 1259fb2..671cfe6 100644 --- a/plugins/externalUpload/modal.jsx +++ b/plugins/externalUpload/modal.jsx @@ -12,7 +12,7 @@ const { Button, ButtonColors, ButtonSizes, - LinkButton, + focusring, }, solid: { createSignal, createEffect, Show, For }, util: { log, getFiber }, @@ -27,6 +27,7 @@ export function UploadModal(closeModal) { const [uploadProgress, setUploadProgress] = createSignal(0); const [dashOpen, setDashOpen] = createSignal(false); const [dashboardFiles, setDashboardFiles] = createSignal([]); + const [fetchingFiles, setFetchingFiles] = createSignal(false); let fileInputRef; @@ -73,15 +74,15 @@ export function UploadModal(closeModal) { setIsUploading(true); setUploadProgress(0); - const uploadedFiles = await uploadFiles(files(), (progress) => { + const { uploadedFiles, previewsToSave } = await uploadFiles(files(), previews(), (progress) => { setUploadProgress(progress * 100); }); - const uploadedUrls = uploadedFiles + const uploadedUrls = (await uploadedFiles) .filter((result) => result.status === "fulfilled") .map((result) => result.value); - const failedUploads = uploadedFiles + const failedUploads = (await uploadedFiles) .filter((result) => result.status === "rejected") .map((result) => result.reason); @@ -109,6 +110,8 @@ export function UploadModal(closeModal) { content: "All files uploaded successfully", }); + store.previews = { ...store.previews, ...previewsToSave }; + const fiber = getFiber(document.querySelector('[class*="slateContainer"]')); const editor = fiber.child.pendingProps.editor; @@ -127,8 +130,10 @@ export function UploadModal(closeModal) { }; const fetchDashboardFiles = async () => { + setFetchingFiles(true); const files = await getAllFiles(); setDashboardFiles(files); + setFetchingFiles(false); }; const handleDeleteFile = async (file) => { @@ -233,38 +238,75 @@ export function UploadModal(closeModal) { Total bucket usage: {formatFileSize(dashboardFiles().reduce((acc, file) => acc + file.Size, 0))} - - - - File Name - Size - Uploaded - Actions - - - - - {(file) => ( - - - {file.Key} - - {formatFileSize(file.Size)} - {formatDate(file.LastModified)} - - handleDeleteFile(file)} + + + {(file) => { + const extension = file.Key.split(".").pop(); + const isImage = ["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension); + const isVideo = ["mp4", "webm"].includes(extension); + + const [preview, setPreview] = createSignal(store.previews[file.Key]); + + if (!preview() && (isImage || isVideo)) { + getFilePreview(file, isImage, isVideo, store.publicUrl).then((url) => { + store.previews = { ...store.previews, [file.Key]: url }; + setPreview(url); + }); + } + + return ( + { + const fiber = getFiber(document.querySelector('[class*="slateContainer"]')); + const editor = fiber.child.pendingProps.editor; + + const url = getUrl(file, store.publicUrl); + editor.insertText(url + " "); + }} + > + {preview() && isImage && ( + + )} + {preview() && isVideo && ( + + )} + {(!preview() || (!isImage && !isVideo)) && 📄} + + {file.Key} + {formatFileSize(file.Size)} + {formatDate(file.LastModified)} + + handleDeleteFile(file)}> + - Delete - - - - )} - - - + + + + + + ); + }} + + diff --git a/plugins/externalUpload/modal.jsx.scss b/plugins/externalUpload/modal.jsx.scss index c8ea428..fa5cd5b 100644 --- a/plugins/externalUpload/modal.jsx.scss +++ b/plugins/externalUpload/modal.jsx.scss @@ -8,12 +8,17 @@ } .uploadArea:hover, -.uploadArea.dragOver, -.previewItem:hover { +.uploadArea.dragOver { border-color: white; background-color: var(--brand-15a); } +.previewItem:hover, +.dashboardItem:hover { + border-color: var(--background-secondary); + background-color: var(--brand-15a); +} + .uploadArea.uploading { pointer-events: none; opacity: 0.7; @@ -39,9 +44,11 @@ flex-wrap: wrap; gap: 10px; margin-top: 20px; + width: 110%; } -.previewItem { +.previewItem, +.dashboardItem { position: relative; width: 180px; height: 180px; @@ -49,13 +56,18 @@ flex-direction: column; align-items: center; justify-content: space-between; - border: 1px solid var(--interactive-normal); + border: 1px solid var(--background-tertiary); + background-color: var(--background-secondary); border-radius: 8px; padding: 8px; overflow: hidden; transition: all 0.3s ease; } +.dashboardItem { + cursor: pointer; +} + .previewImage, .previewVideo { max-width: 100%; @@ -88,7 +100,7 @@ padding: 4px 5px 1px 5px; font-size: 12px; color: var(--status-danger); - background-color: color-mix(in srgb, var(--background-tertiary) 90%, transparent); + background-color: color-mix(in srgb, var(--background-tertiary) 80%, transparent); backdrop-filter: blur(4px); border-radius: 8px; cursor: pointer; @@ -137,49 +149,7 @@ margin-right: auto; } -.dashboardTable { - width: 100%; - border-collapse: collapse; - background-color: var(--background-primary); - border-radius: 8px; - overflow: hidden; - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); -} -.dashboardTable thead { - background-color: var(--background-tertiary); - color: var(--text-normal); -} - -.dashboardTable th { - padding: 12px 16px; - text-align: left; - font-weight: 600; - font-size: 14px; - text-transform: uppercase; -} - -.dashboardTable tbody tr { - transition: background-color 0.3s ease; -} - -.dashboardTable tbody tr:nth-child(even) { - background-color: var(--background-secondary); -} - -.dashboardTable tbody tr:hover { - background-color: var(--brand-15a); - color: var(--text-bright); -} - -.dashboardTable td { - padding: 12px 16px; - font-size: 14px; -} - -.dashboardTable td:last-child { - text-align: center; -} [class^="buttons"]>[aria-haspopup="dialog"] { display: none; diff --git a/plugins/externalUpload/utils.js b/plugins/externalUpload/utils.js index d632727..3ac4146 100644 --- a/plugins/externalUpload/utils.js +++ b/plugins/externalUpload/utils.js @@ -2,26 +2,27 @@ import { S3Client, ListObjectsV2Command, DeleteObjectCommand } from "@aws-sdk/cl import { Upload } from "@aws-sdk/lib-storage"; import xxhash from "xxhash-wasm"; -export function getFilePreview(file) { - if (file.type.startsWith("image/")) { - return URL.createObjectURL(file); - } else if (file.type.startsWith("video/")) { +export async function getFilePreview(file, isImage, isVideo, publicUrl) { + if (isImage || file?.type?.startsWith("image/")) { + return URL.createObjectURL(file.Key ? await fetch(getUrl(file, publicUrl)).then((body) => body.blob()) : file); + } else if (isVideo || file?.type?.startsWith("video/")) { return new Promise((resolve) => { let video = document.createElement("video"); video.preload = "metadata"; + video.crossOrigin = "anonymous"; video.onloadedmetadata = function () { video.currentTime = 1; video.onseeked = function () { const canvas = document.createElement("canvas"); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; + canvas.width = video.videoWidth / 2; + canvas.height = video.videoHeight / 2; canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(video.src); video = null; - resolve(canvas.toDataURL()); + resolve(canvas.toDataURL("image/webp")); }; }; - video.src = URL.createObjectURL(file); + video.src = file.Key ? getUrl(file, publicUrl) : URL.createObjectURL(file); }); } else { return null; @@ -90,11 +91,12 @@ function getTotalUploadedSize(uploadedSizes) { return Object.values(uploadedSizes).reduce((acc, size) => acc + size, 0); } -export async function uploadFiles(files, onProgress) { +export async function uploadFiles(files, _previews, onProgress) { const totalSize = files.reduce((acc, file) => acc + file.size, 0); let uploadedSizes = {}; + const previews = {}; - const uploadPromises = files.map(async (file) => { + const uploadPromises = files.map(async (file, index) => { const name = await getUUID(file); const upload = new Upload({ client: s3Client, @@ -112,10 +114,12 @@ export async function uploadFiles(files, onProgress) { onProgress(getTotalUploadedSize(uploadedSizes) / totalSize); }); + previews[name] = _previews[index]; + return uploadPromise; }); - return Promise.allSettled(uploadPromises); + return { uploadedFiles: Promise.allSettled(uploadPromises), previewsToSave: previews }; } export async function getAllFiles() {
Total bucket usage: {formatFileSize(dashboardFiles().reduce((acc, file) => acc + file.Size, 0))}
{file.Key}
{formatFileSize(file.Size)}
{formatDate(file.LastModified)}