143 lines
6.6 KiB
JavaScript
143 lines
6.6 KiB
JavaScript
/**
|
||
* @file Verwaltet Bildauswahl, Uploads und Vorschauen sowie die Anzeige gespeicherter Anhänge.
|
||
*/
|
||
export function appendAttachments(container, attachments = []) {
|
||
if (!attachments.length) return;
|
||
const gallery = document.createElement("div");
|
||
gallery.className = "message-images";
|
||
for (const item of attachments) {
|
||
if (!/^[a-f0-9-]{36}$/.test(item.id)) continue;
|
||
const link = document.createElement("a");
|
||
link.href = `/attachments/${item.id}`;
|
||
link.target = "_blank";
|
||
link.rel = "noopener";
|
||
const image = document.createElement("img");
|
||
image.src = link.href;
|
||
image.alt = item.originalName || "Attached image";
|
||
image.loading = "lazy";
|
||
link.append(image);
|
||
gallery.append(link);
|
||
}
|
||
container.append(gallery);
|
||
}
|
||
|
||
export function createAttachmentComposer({onChange, onError}) {
|
||
const button = document.getElementById("attachImage");
|
||
const picker = document.getElementById("imagePicker");
|
||
const preview = document.getElementById("uploadPreviews");
|
||
const composer = document.querySelector(".input-container");
|
||
let items = [];
|
||
const drafts = new Map();
|
||
let busy = false;
|
||
let limits = {maxSizeMb: 10, maxFiles: 5};
|
||
fetch("/uploads/config").then(r => r.ok ? r.json() : limits).then(value => { limits = value; }).catch(() => {});
|
||
const render = () => {
|
||
preview.replaceChildren();
|
||
preview.hidden = !items.length;
|
||
for (const item of items) {
|
||
const card = document.createElement("div");
|
||
card.className = "upload-preview";
|
||
const image = document.createElement("img");
|
||
image.src = item.preview;
|
||
image.alt = item.file.name;
|
||
const label = document.createElement("span");
|
||
label.textContent = `${item.file.name} — ${item.error || (item.metadata ? "Ready" : "Uploading…")}`;
|
||
card.append(image, label);
|
||
if (item.error) {
|
||
const retry = document.createElement("button");
|
||
retry.type = "button"; retry.textContent = "Retry"; retry.disabled = busy;
|
||
retry.onclick = () => { void upload(item); };
|
||
card.append(retry);
|
||
}
|
||
const remove = document.createElement("button");
|
||
remove.type = "button"; remove.textContent = "×"; remove.disabled = busy;
|
||
remove.setAttribute("aria-label", `Remove ${item.file.name}`);
|
||
remove.onclick = () => {
|
||
items = items.filter(v => v !== item);
|
||
item.controller?.abort(); URL.revokeObjectURL(item.preview);
|
||
if (item.metadata) void fetch(`/attachments/${item.metadata.id}`, {method: "DELETE"});
|
||
render();
|
||
};
|
||
card.append(remove); preview.append(card);
|
||
}
|
||
onChange();
|
||
};
|
||
const upload = async item => {
|
||
if (item.uploading || item.metadata || !items.includes(item)) return;
|
||
item.uploading = true;
|
||
item.error = null;
|
||
item.controller = new AbortController();
|
||
render();
|
||
try {
|
||
const form = new FormData(); form.append("image", item.file);
|
||
const response = await fetch("/uploads", {method: "POST", body: form, signal: item.controller.signal});
|
||
const result = await response.json();
|
||
if (!response.ok) throw new Error(result.error || "Upload failed");
|
||
item.metadata = result;
|
||
if (![items, ...drafts.values()].some(draft => draft.includes(item))) void fetch(`/attachments/${result.id}`, {method: "DELETE"});
|
||
} catch (error) {
|
||
if (error.name !== "AbortError") item.error = error.message || "Upload failed";
|
||
}
|
||
item.uploading = false;
|
||
item.controller = null;
|
||
if (items.includes(item)) render();
|
||
};
|
||
const add = files => {
|
||
if (busy) return;
|
||
for (const file of files) {
|
||
if (items.length >= limits.maxFiles) { onError(`Attach at most ${limits.maxFiles} images`); break; }
|
||
if (!["image/png", "image/jpeg", "image/webp"].includes(file.type)) { onError("Use PNG, JPEG or WebP images"); continue; }
|
||
if (!file.size || file.size > limits.maxSizeMb * 1024 * 1024) { onError(`Images must be non-empty and at most ${limits.maxSizeMb} MB`); continue; }
|
||
const item = {file, preview: URL.createObjectURL(file)};
|
||
items.push(item); void upload(item);
|
||
}
|
||
};
|
||
button.onclick = () => picker.click();
|
||
picker.onchange = () => { add(picker.files); picker.value = ""; };
|
||
composer.addEventListener("dragover", event => {
|
||
if (event.dataTransfer.types.includes("Files")) { event.preventDefault(); composer.classList.add("image-drag-target"); }
|
||
});
|
||
composer.addEventListener("dragleave", () => composer.classList.remove("image-drag-target"));
|
||
composer.addEventListener("drop", event => {
|
||
event.preventDefault(); composer.classList.remove("image-drag-target"); add(event.dataTransfer.files);
|
||
});
|
||
composer.addEventListener("paste", event => {
|
||
const images = [...(event.clipboardData?.files || [])].filter(file => file.type.startsWith("image/"));
|
||
if (images.length) { if (!event.clipboardData.getData("text/plain")) event.preventDefault(); add(images); }
|
||
});
|
||
return {
|
||
get attachments() { return items.filter(v => v.metadata).map(v => v.metadata); },
|
||
get pending() { return items.some(v => !v.metadata); },
|
||
setBusy(value) { busy = value; button.disabled = value; picker.disabled = value; render(); },
|
||
clear() { items.forEach(v => URL.revokeObjectURL(v.preview)); items = []; render(); },
|
||
discard() {
|
||
for (const item of items) {
|
||
item.controller?.abort();
|
||
URL.revokeObjectURL(item.preview);
|
||
if (item.metadata) void fetch(`/attachments/${item.metadata.id}`, {method: "DELETE"});
|
||
}
|
||
items = [];
|
||
render();
|
||
},
|
||
saveDraft(chatId) {
|
||
if (chatId != null) drafts.set(chatId, items);
|
||
items = [];
|
||
render();
|
||
},
|
||
restoreDraft(chatId) {
|
||
items = drafts.get(chatId) || [];
|
||
drafts.delete(chatId);
|
||
render();
|
||
},
|
||
discardDraft(chatId) {
|
||
const saved = drafts.get(chatId) || [];
|
||
drafts.delete(chatId);
|
||
for (const item of saved) {
|
||
item.controller?.abort();
|
||
URL.revokeObjectURL(item.preview);
|
||
if (item.metadata) void fetch(`/attachments/${item.metadata.id}`, {method: "DELETE"});
|
||
}
|
||
}
|
||
};
|
||
}
|