Media upload pruning.

Uploaded media is now pruned automatically every time a post is updated.
Minor cleanup.
Groundwork for media types other than images.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>
pull/1/head
Armored Dragon 2024-04-25 08:49:49 -05:00
parent a11f5857c6
commit b2354645b2
Signed by: ArmoredDragon
GPG Key ID: C7207ACC3382AD8B
10 changed files with 82 additions and 495 deletions

View File

@ -276,9 +276,30 @@ async function editPost({ requester_id, post_id, post_content }) {
// Save the updated post to the database // Save the updated post to the database
await prisma.post.update({ where: { id: post.id }, data: post_formatted }); await prisma.post.update({ where: { id: post.id }, data: post_formatted });
// Prune the post to save on storage
await pruneMedia({ parent_id: post_id, parent_type: "posts" });
return _r(true); return _r(true);
} }
async function deletePost({ requester_id, post_id }) {} async function deletePost({ requester_id, post_id }) {
let user = await getUser({ user_id: requester_id });
let post = await getPost({ post_id: post_id });
if (!user.success) return { success: false, message: user.message || "User does not exist" };
user = user.data;
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
post = post.data;
let can_delete = post.owner.id === user.id || user.role === "ADMIN";
if (!can_delete) return { success: false, message: "Action not permitted" };
await prisma.post.delete({ where: { id: post.id } });
_deleteS3Directory(post.id, "post");
return { success: true };
}
// User Profiles // User Profiles
async function getBiography({ requester_id, author_id }) { async function getBiography({ requester_id, author_id }) {
if (!author_id) return _r(false, "No Author specified."); if (!author_id) return _r(false, "No Author specified.");
@ -328,58 +349,81 @@ async function updateBiography({ requester_id, author_id, biography_content }) {
return _r(true); return _r(true);
} }
// TODO: Replace async function uploadMedia({ parent_id, parent_type, file_buffer, content_type }) {
async function deleteBlog(blog_id, requester_id) {
const user = await getUser({ user_id: requester_id });
const post = await getPost({ post_id: blog_id });
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
let can_delete = post.data.owner.id === user.data.id || user.data.role === "ADMIN";
if (can_delete) {
await prisma.post.delete({ where: { id: post.data.id } });
_deleteS3Directory(post.data.id, "blog");
return { success: true };
}
return { success: false, message: "Action not permitted" };
}
async function uploadMedia({ parent_id, file_buffer, file_extension }) {
if (!use_s3_storage) return null; if (!use_s3_storage) return null;
const content_name = crypto.randomUUID(); const content_name = crypto.randomUUID();
let maximum_image_resolution = { width: 1920, height: 1080 }; let maximum_image_resolution = { width: 1920, height: 1080 };
// const image_extensions = ["png", "webp", "jpg", "jpeg"];
// const video_extensions = ["mp4", "webm", "mkv", "avi"];
// Images // Images
const compressed_image = await sharp(Buffer.from(file_buffer.split(",")[1], "base64"), { animated: true }) const compressed_image = await sharp(Buffer.from(file_buffer.split(",")[1], "base64"), { animated: true })
.resize({ ...maximum_image_resolution, withoutEnlargement: true, fit: "inside" }) .resize({ ...maximum_image_resolution, withoutEnlargement: true, fit: "inside" })
.webp({ quality: 90, animated: true }) .webp({ quality: 90, animated: true })
.toBuffer(); .toBuffer();
let extension;
let s3_content_type;
if (content_type.includes("image/")) {
extension = ".webp";
s3_content_type = "image/webp";
}
const params = { const params = {
Bucket: process.env.S3_BUCKET_NAME, Bucket: process.env.S3_BUCKET_NAME,
Key: `${process.env.ENVIRONMENT}/posts/${parent_id}/${content_name}.webp`, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${content_name}${extension}`,
Body: compressed_image, Body: compressed_image,
ContentType: "image/webp", ContentType: s3_content_type,
}; };
const command = new PutObjectCommand(params); const command = new PutObjectCommand(params);
await s3.send(command); await s3.send(command);
return content_name; return content_name + extension;
} }
async function getMedia({ parent_id, file_name }) { async function getMedia({ parent_id, parent_type, file_name }) {
if (!use_s3_storage) return null; if (!use_s3_storage) return null;
const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/posts/${parent_id}/${file_name}.webp` }; const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${file_name}` };
return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 }); return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 });
} }
// TODO:
// Will be done automatically in the background. async function deleteMedia({ parent_id, parent_type, file_name }) {
// Unreferenced images and media will be deleted const request_params = {
async function deleteMedia({ parent_id, file_name }) {} Bucket: process.env.S3_BUCKET_NAME,
Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${file_name}`,
};
const command = new DeleteObjectCommand(request_params);
await s3.send(command);
return { success: true };
}
// This cleans up all unused and unreferenced media files.
// NOTE: Only made for posts, as that is all that there is right now
async function pruneMedia({ parent_id, parent_type }) {
let post = await getPost({ post_id: parent_id });
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
post = post.data;
// const total_number_of_media = post.raw_media.length;
for (let media_index = 0; post.raw_media.length > media_index; media_index++) {
if (!post.raw_content.includes(post.raw_media[media_index])) {
// Delete the media off of the S3 server
let delete_request = await deleteMedia({ parent_id: parent_id, parent_type: parent_type, file_name: post.raw_media[media_index] });
if (!delete_request.success) continue;
// Remove from the list in the database
await post.raw_media.splice(media_index, 1);
// Save in the database
await prisma.post.update({ where: { id: parent_id }, data: { media: post.raw_media } });
// Delete was successful, move the index back to account for new array length
media_index--;
}
}
}
async function getTags({ order = "count" } = {}) { async function getTags({ order = "count" } = {}) {
if (order == "count") { if (order == "count") {
@ -404,32 +448,6 @@ async function getTags({ order = "count" } = {}) {
// Will be done automatically in the background // Will be done automatically in the background
async function deleteTag({ tag_id }) {} async function deleteTag({ tag_id }) {}
// async function deleteImage(image, requester_id) {
// const user = await getUser({ id: requester_id });
// const post = await getBlog({ id: image.parent, raw: true });
// // Check if post exists
// if (!post) return { success: false, message: "Post does not exist" };
// // Check for permissions
// if (post.owner.id !== user.data.id || user.data.role !== "ADMIN") return { success: false, message: "User is not permitted" };
// let image_index = post.raw_images.indexOf(image.id);
// post.raw_images.splice(image_index, 1);
// await prisma.post.update({ where: { id: post.id }, data: { images: post.raw_images } });
// const request_params = {
// Bucket: process.env.S3_BUCKET_NAME,
// Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`,
// };
// const command = new DeleteObjectCommand(request_params);
// await s3.send(command);
// return { success: true };
// }
async function _deleteS3Directory(id, type) { async function _deleteS3Directory(id, type) {
// Erase database images from S3 server // Erase database images from S3 server
const folder_params = { Bucket: process.env.S3_BUCKET_NAME, Prefix: `${process.env.ENVIRONMENT}/${type}/${id}` }; const folder_params = { Bucket: process.env.S3_BUCKET_NAME, Prefix: `${process.env.ENVIRONMENT}/${type}/${id}` };
@ -467,7 +485,7 @@ async function _renderPost(post) {
if (post.media) { if (post.media) {
for (i = 0; post.media.length > i; i++) { for (i = 0; post.media.length > i; i++) {
post.media[i] = await getMedia({ parent_id: post.id, file_name: post.media[i] }); post.media[i] = await getMedia({ parent_id: post.id, parent_type: "posts", file_name: post.media[i] });
} }
} }
@ -599,4 +617,4 @@ const _r = (s, m) => {
return { success: s, message: m }; return { success: s, message: m };
}; };
module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, getBiography, updateBiography, uploadMedia, deleteBlog, getTags, postSetting, getSetting }; module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, deletePost, getBiography, updateBiography, uploadMedia, getTags, postSetting, getSetting };

View File

@ -46,7 +46,7 @@ async function postSetting(request, response) {
async function postImage(request, response) { async function postImage(request, response) {
// TODO: Permissions for uploading images // TODO: Permissions for uploading images
// TODO: Verification for image uploading // TODO: Verification for image uploading
return response.json(await core.uploadMedia({ parent_id: request.body.post_id, file_buffer: request.body.buffer })); return response.json(await core.uploadMedia({ parent_id: request.body.post_id, parent_type: request.body.parent_type, file_buffer: request.body.buffer, content_type: request.body.content_type }));
} }
async function deleteImage(req, res) { async function deleteImage(req, res) {
// TODO: Permissions for deleting image // TODO: Permissions for deleting image
@ -54,7 +54,7 @@ async function deleteImage(req, res) {
} }
async function deleteBlog(req, res) { async function deleteBlog(req, res) {
// TODO: Permissions for deleting blog // TODO: Permissions for deleting blog
return res.json(await core.deleteBlog(req.body.id, req.session.user.id)); return res.json(await core.deletePost({ post_id: req.body.id, requester_id: req.session.user.id }));
} }
async function patchBlog(req, res) { async function patchBlog(req, res) {
// FIXME: validate does not return post id // FIXME: validate does not return post id

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="32"
viewBox="0 -960 800 640"
width="40"
version="1.1"
id="svg1"
sodipodi:docname="sidebyside.svg"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="24.291667"
inkscape:cx="20.006861"
inkscape:cy="16.013722"
inkscape:window-width="2560"
inkscape:window-height="1370"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 193,-320 0,-513 l 193,-193 42,42 -121,121 h 316 v 60 H 114 l 121,121 z m 414,-254 -42,-42 121,-121 H 370 v -60 h 316 l -121,-121 42,-42 193,193 z"
id="path1"
style="fill:#f9f9f9" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="40"
viewBox="0 -960 800 800"
width="40"
version="1.1"
id="svg1"
sodipodi:docname="video.svg"
inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="24.291667"
inkscape:cx="20.006861"
inkscape:cy="20.006861"
inkscape:window-width="2560"
inkscape:window-height="1370"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 303,-390 570,-560 303,-730 Z m 97,230 q -82,0 -155,-31.5 -73,-31.5 -127.5,-86 Q 63,-332 31.5,-405 0,-478 0,-560 q 0,-83 31.5,-156 31.5,-73 86,-127 54.5,-54 127.5,-85.5 73,-31.5 155,-31.5 83,0 156,31.5 73,31.5 127,85.5 54,54 85.5,127 31.5,73 31.5,156 0,82 -31.5,155 -31.5,73 -85.5,127.5 -54,54.5 -127,86 -73,31.5 -156,31.5 z m 0,-60 q 142,0 241,-99.5 99,-99.5 99,-240.5 0,-142 -99,-241 -99,-99 -241,-99 -141,0 -240.5,99 -99.5,99 -99.5,241 0,141 99.5,240.5 Q 259,-220 400,-220 Z m 0,-340 z"
id="path1"
style="fill:#f9f9f9" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,72 +0,0 @@
async function toggleState(setting_name, new_value, element_id) {
// Show spinner
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden");
const form = {
setting_name: setting_name,
value: JSON.stringify(new_value),
};
const response = await request("/setting", "POST", form);
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden");
// TODO: On failure, notify the user
// Check response for errors
if (response.body.success) {
// Update visual to reflect current setting
// Class
const add_class = new_value ? "good" : "bad";
const remove_class = new_value ? "bad" : "good";
qs(`#${element_id}`).classList.remove(remove_class);
qs(`#${element_id}`).classList.add(add_class);
// Text
const new_text = new_value ? "Enabled" : "Disabled";
qs(`#${element_id}`).children[0].innerText = new_text;
// Function
const add_function = new_value ? `toggleState('${setting_name}', false, this.id)` : `toggleState('${setting_name}', true, this.id)`;
qs(`#${element_id}`).removeAttribute("onclick");
qs(`#${element_id}`).setAttribute("onclick", add_function);
}
}
async function updateValue(key_pressed, setting_name, new_value, element_id) {
if (key_pressed !== 13) return;
// Show spinner
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden");
const form = {
setting_name: setting_name,
value: new_value,
};
const response = await request("/setting", "POST", form);
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden");
// TODO: On failure, notify the user
// Check response for errors
if (response.body.success) {
}
}
function toggleActiveCategory(category_id) {
// Pages ----------------
// Hide all pages
qsa(".category-page").forEach((page) => {
page.classList.add("hidden");
});
// Show requested page
qs(`#${category_id}`).classList.remove("hidden");
// Navigation bar -------
// Unactive all buttons
qsa(".category-navigation button").forEach((btn) => {
btn.classList.remove("active");
});
// Active current page
qs(`#${category_id}-nav-btn`).classList.add("active");
qs(`#${category_id}-nav-btn`).blur(); // Unfocus the button
}

View File

@ -1,8 +0,0 @@
const delete_post_btn = qs("#delete-post");
if (delete_post_btn) {
delete_post_btn.addEventListener("click", async () => {
let req = await request("/api/web/blog", "DELETE", { id: window.location.href.split("/")[4] });
if (req.body.success) location.reload();
});
}

View File

@ -1,15 +0,0 @@
// Quick document selectors
const qs = (selector) => document.querySelector(selector);
const qsa = (selector) => document.querySelectorAll(selector);
// TODO: Try/Catch for failed requests
async function request(url, method, body) {
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json", // Set the Content-Type header
},
body: JSON.stringify(body),
});
return { status: response.status, body: await response.json() };
}

View File

@ -1,257 +0,0 @@
const blog_id = window.location.href.split("/")[4];
let existing_images = [];
let pending_images = [];
let pending_thumbnail = {};
const thumbnail_area = qs(".e-thumbnail");
const image_area = qs(".e-image-area");
const post_content_area = qs(".e-content textarea");
// Style
function stylizeDropArea(element) {
// Drag over start
element.addEventListener("dragover", (e) => {
e.preventDefault();
e.target.classList.add("drag-over");
});
// Drag over leave
element.addEventListener("dragleave", (e) => {
e.target.classList.remove("drag-over");
});
// Do nothing on drop
element.addEventListener("drop", (e) => {
e.preventDefault();
});
}
// Auto resize on page load
post_content_area.style.height = post_content_area.scrollHeight + "px";
post_content_area.style.minHeight = post_content_area.scrollHeight + "px";
// Auto expand blog area
post_content_area.addEventListener("input", (e) => {
post_content_area.style.height = post_content_area.scrollHeight + "px";
post_content_area.style.minHeight = e.target.scrollHeight + "px";
});
stylizeDropArea(thumbnail_area);
stylizeDropArea(image_area);
// Upload an image to the blog post
image_area.addEventListener("drop", async (e) => {
const files = e.dataTransfer.files;
for (let i = 0; i < files.length; i++) {
// Each dropped image will be stored in this formatted object
const image_object = {
id: crypto.randomUUID(),
data_blob: new Blob([await files[i].arrayBuffer()]),
content_type: files[i].type,
};
// Add the image's data to the list
pending_images.push(image_object);
}
// Update the displayed images
updateImages();
});
// Upload an image to the blog post
thumbnail_area.addEventListener("drop", async (e) => {
const file = e.dataTransfer.files[0];
// The thumbnail will be stored in this formatted object
const image_object = {
id: crypto.randomUUID(),
data_blob: new Blob([await file.arrayBuffer()]),
content_type: file.type,
};
// Add the image's data to the list
pending_thumbnail = image_object;
// Update the visible thumbnail
qs(".e-thumbnail img").src = URL.createObjectURL(image_object.data_blob);
// Update the displayed images
updateImages();
});
// Publish or Update a blog post with the new data
async function publishBlog(unlisted, edit) {
// Format our request we will send to the server
let form_data = {
title: qs("#title").value,
description: qs("#description").value,
content: qs("#content").value,
visibility: unlisted ? "UNLISTED" : "PUBLISHED",
tags: [],
date: qs("#date").value,
time: qs("#time").value,
};
// Get our tags, trim them, then shove them into an array
const tags_value = qs("#tags").value || "";
if (tags_value.length) {
let tags_array = qs("#tags").value.split(",");
tags_array.forEach((tag) => form_data.tags.push(tag.trim()));
}
// If we have a thumbnail, read the thumbnail image and store it
if (pending_thumbnail.data_blob) {
form_data.thumbnail = { ...pending_thumbnail, data_blob: await _readFile(pending_thumbnail.data_blob) };
}
// We have images to upload
if (pending_images.length + existing_images.length > 0) {
// Initialize the image array
form_data.images = [];
// Read the image, convert to base64, update the existing variable with the base64
for (let i = 0; pending_images.length > i; i++) {
form_data.images.push({ ...pending_images[i], data_blob: await _readFile(pending_images[i].data_blob) });
}
}
// We are making edits to a post, not uploading a new one
if (edit) {
form_data.id = blog_id;
}
// Send the request!
const method = edit ? "PATCH" : "post";
const res = await request("/api/web/blog", method, form_data);
if (res.body.success) {
window.location.href = `/blog/${res.body.blog_id || blog_id}`;
}
}
// Send a request to delete an image
async function deleteImage(image_id) {
const res = await request("/api/web/blog/image", "delete", { id: image_id, parent: blog_id, parent_type: "blog" });
if (res.body.success) {
// Remove from existing images (If it exists)
let image = existing_images.find((item) => item.id === image_id);
if (image) existing_images.splice(existing_images.indexOf(image), 1);
image = pending_images.find((item) => item.id === image_id);
if (image) pending_images.splice(pending_images.indexOf(image), 1);
updateImages();
}
}
// We need to read the file contents in order to convert it to base64 to send to the server
function _readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function customDragString() {
const images = qsa(".e-image-area .image img");
images.forEach((image) => {
image.addEventListener("dragstart", (event) => {
event.dataTransfer.setData("text/plain", event.target.getAttribute("data-image_id"));
});
});
}
function updateImages() {
const image_div = (img_id, img_url) => `<div class="image"><img data-image_id="${img_id}" src="${img_url}" /><div><a onclick="deleteImage('${img_id}')">X</a></div></div>`;
// Clear existing listings
qsa(".e-image-area .image").forEach((entry) => entry.remove());
// Clear placeholder text
if (existing_images.length + pending_images.length > 0) if (qs(".e-image-area .placeholder")) qs(".e-image-area .placeholder").remove();
existing_images.forEach((image) => {
image_area.insertAdjacentHTML("beforeend", image_div(image.id, image.url));
});
// Add new entries based on saved list
pending_images.forEach((image) => {
image_area.insertAdjacentHTML("beforeend", image_div(image.id, URL.createObjectURL(image.data_blob)));
});
customDragString();
}
// Text area custom text editor
qs("#insert-sidebyside").addEventListener("click", () => textareaAction("{sidebyside}{/sidebyside}", 12));
qs("#insert-video").addEventListener("click", () => textareaAction("{video:}", 7));
qs("#insert-h1").addEventListener("click", () => textareaAction("# "));
qs("#insert-h2").addEventListener("click", () => textareaAction("## "));
qs("#insert-h3").addEventListener("click", () => textareaAction("### "));
qs("#insert-h4").addEventListener("click", () => textareaAction("#### "));
qs("#insert-underline").addEventListener("click", () => textareaAction("_", undefined, true));
qs("#insert-italics").addEventListener("click", () => textareaAction("*", undefined, true));
qs("#insert-bold").addEventListener("click", () => textareaAction("__", undefined, true));
qs("#insert-strike").addEventListener("click", () => textareaAction("~~", undefined, true));
qs("#insert-sup").addEventListener("click", () => textareaAction("^", undefined, true));
function textareaAction(insert, cursor_position, dual_side) {
// Insert the custom string at the cursor position
const selectionStart = post_content_area.selectionStart;
const selectionEnd = post_content_area.selectionEnd;
const textBefore = post_content_area.value.substring(0, selectionStart);
const textAfter = post_content_area.value.substring(selectionEnd);
const selectedText = post_content_area.value.substring(selectionStart, selectionEnd);
let updatedText;
if (dual_side) {
updatedText = `${textBefore}${insert}${selectedText}${insert}${textAfter}`;
} else {
updatedText = `${textBefore}${insert}${selectedText}${textAfter}`;
}
post_content_area.value = updatedText;
// Set the cursor position after the custom string
qs(".e-content textarea").focus();
const newPosition = selectionStart + (cursor_position || insert.length);
post_content_area.setSelectionRange(newPosition, newPosition);
}
post_content_area.addEventListener("drop", (event) => {
event.preventDefault();
// Get the custom string from the drag data
const customString = `\{image:${event.dataTransfer.getData("text/plain")}\}\n`;
// Insert the custom string at the cursor position
const selectionStart = post_content_area.selectionStart;
const selectionEnd = post_content_area.selectionEnd;
const textBefore = post_content_area.value.substring(0, selectionStart);
const textAfter = post_content_area.value.substring(selectionEnd);
const updatedText = textBefore + customString + textAfter;
post_content_area.value = updatedText;
// Set the cursor position after the custom string
const newPosition = selectionStart + customString.length;
post_content_area.setSelectionRange(newPosition, newPosition);
});
// Load the existing images into our existing_images variable
qsa(".e-image-area img").forEach((image) => {
existing_images.push({ id: image.getAttribute("data-image_id"), url: image.src });
});
updateImages();

View File

@ -1,6 +0,0 @@
qs("#search-btn").addEventListener("click", search);
function search() {
const url_query = `search=${qs("input").value}`;
window.location.href = `${window.location.origin}${window.location.pathname}?${url_query}`;
}

View File

@ -48,9 +48,12 @@ rich_text_editors.forEach((editor) => {
data_blob: new Blob([await files[i].arrayBuffer()]), data_blob: new Blob([await files[i].arrayBuffer()]),
content_type: files[i].type, content_type: files[i].type,
}; };
let form_data = { let form_data = {
buffer: await _readFile(image_object.data_blob), buffer: await _readFile(image_object.data_blob),
content_type: image_object.content_type,
post_id: window.location.href.split("/")[4], post_id: window.location.href.split("/")[4],
parent_type: "posts",
}; };
const image_uploading_request = await request("/api/web/image", "POST", form_data); const image_uploading_request = await request("/api/web/image", "POST", form_data);