Skip to content

Commit

Permalink
[externalUpload] better dashboard w previews, ability to insert alrea…
Browse files Browse the repository at this point in the history
…dy uploaded images
  • Loading branch information
xirreal committed Feb 3, 2025
1 parent a5001c2 commit 9d54c16
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 93 deletions.
1 change: 1 addition & 0 deletions plugins/externalUpload/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function onLoad() {
plugin.store.secretAccessKey ??= "";
plugin.store.bucket ??= "";
plugin.store.publicUrl ??= "";
plugin.store.previews ??= "";

updateConfig();

Expand Down
112 changes: 77 additions & 35 deletions plugins/externalUpload/modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const {
Button,
ButtonColors,
ButtonSizes,
LinkButton,
focusring,
},
solid: { createSignal, createEffect, Show, For },
util: { log, getFiber },
Expand All @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;

Expand All @@ -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) => {
Expand Down Expand Up @@ -233,38 +238,75 @@ export function UploadModal(closeModal) {
<Show when={dashOpen()}>
<ModalBody>
<p>Total bucket usage: {formatFileSize(dashboardFiles().reduce((acc, file) => acc + file.Size, 0))}</p>
<table class={styles.dashboardTable}>
<thead>
<tr>
<th>File Name</th>
<th>Size</th>
<th>Uploaded</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<For each={dashboardFiles()}>
{(file) => (
<tr>
<td>
<LinkButton href={getUrl(file, store.publicUrl)}>{file.Key}</LinkButton>
</td>
<td>{formatFileSize(file.Size)}</td>
<td>{formatDate(file.LastModified)}</td>
<td>
<Button
size={ButtonSizes.SMALL}
color={ButtonColors.RED}
onClick={() => handleDeleteFile(file)}
<div class={styles.previewArea}>
<For each={dashboardFiles()}>
{(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 (
<div
class={styles.dashboardItem}
use:focusring
onclick={() => {
const fiber = getFiber(document.querySelector('[class*="slateContainer"]'));
const editor = fiber.child.pendingProps.editor;

const url = getUrl(file, store.publicUrl);
editor.insertText(url + " ");
}}
>
{preview() && isImage && (
<img src={preview()} alt={file.Key} class={styles.previewImage} />
)}
{preview() && isVideo && (
<img src={preview()} alt={file.Key} class={styles.previewVideo} />
)}
{(!preview() || (!isImage && !isVideo)) && <div class={styles.previewIcon}>📄</div>}
<div class={styles.previewItemInfo}>
<p>{file.Key}</p>
<p>{formatFileSize(file.Size)}</p>
<p>{formatDate(file.LastModified)}</p>
</div>
<button class={styles.removeButton} onClick={() => handleDeleteFile(file)}>
<svg
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
Delete
</Button>
</td>
</tr>
)}
</For>
</tbody>
</table>
<path
fill="currentColor"
d="M14.25 1c.41 0 .75.34.75.75V3h5.25c.41 0 .75.34.75.75v.5c0 .41-.34.75-.75.75H3.75A.75.75 0 0 1 3 4.25v-.5c0-.41.34-.75.75-.75H9V1.75c0-.41.34-.75.75-.75h4.5Z"
class=""
></path>
<path
fill="currentColor"
fill-rule="evenodd"
d="M5.06 7a1 1 0 0 0-1 1.06l.76 12.13a3 3 0 0 0 3 2.81h8.36a3 3 0 0 0 3-2.81l.75-12.13a1 1 0 0 0-1-1.06H5.07ZM11 12a1 1 0 1 0-2 0v6a1 1 0 1 0 2 0v-6Zm3-1a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-6a1 1 0 0 1 1-1Z"
clip-rule="evenodd"
class=""
></path>
</svg>
</button>
</div>
);
}}
</For>
</div>
</ModalBody>
</Show>
<ModalFooter>
Expand Down
64 changes: 17 additions & 47 deletions plugins/externalUpload/modal.jsx.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,23 +44,30 @@
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
width: 110%;
}

.previewItem {
.previewItem,
.dashboardItem {
position: relative;
width: 180px;
height: 180px;
display: flex;
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%;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
26 changes: 15 additions & 11 deletions plugins/externalUpload/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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() {
Expand Down

0 comments on commit 9d54c16

Please sign in to comment.