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
parent
a11f5857c6
commit
b2354645b2
|
@ -276,9 +276,30 @@ async function editPost({ requester_id, post_id, post_content }) {
|
|||
// Save the updated post to the database
|
||||
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);
|
||||
}
|
||||
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
|
||||
async function getBiography({ requester_id, author_id }) {
|
||||
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);
|
||||
}
|
||||
// TODO: Replace
|
||||
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 }) {
|
||||
async function uploadMedia({ parent_id, parent_type, file_buffer, content_type }) {
|
||||
if (!use_s3_storage) return null;
|
||||
const content_name = crypto.randomUUID();
|
||||
let maximum_image_resolution = { width: 1920, height: 1080 };
|
||||
|
||||
// const image_extensions = ["png", "webp", "jpg", "jpeg"];
|
||||
// const video_extensions = ["mp4", "webm", "mkv", "avi"];
|
||||
|
||||
// Images
|
||||
const compressed_image = await sharp(Buffer.from(file_buffer.split(",")[1], "base64"), { animated: true })
|
||||
.resize({ ...maximum_image_resolution, withoutEnlargement: true, fit: "inside" })
|
||||
.webp({ quality: 90, animated: true })
|
||||
.toBuffer();
|
||||
|
||||
let extension;
|
||||
let s3_content_type;
|
||||
|
||||
if (content_type.includes("image/")) {
|
||||
extension = ".webp";
|
||||
s3_content_type = "image/webp";
|
||||
}
|
||||
|
||||
const params = {
|
||||
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,
|
||||
ContentType: "image/webp",
|
||||
ContentType: s3_content_type,
|
||||
};
|
||||
|
||||
const command = new PutObjectCommand(params);
|
||||
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;
|
||||
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 });
|
||||
}
|
||||
// TODO:
|
||||
// Will be done automatically in the background.
|
||||
// Unreferenced images and media will be deleted
|
||||
async function deleteMedia({ parent_id, file_name }) {}
|
||||
|
||||
async function deleteMedia({ parent_id, parent_type, file_name }) {
|
||||
const request_params = {
|
||||
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" } = {}) {
|
||||
if (order == "count") {
|
||||
|
@ -404,32 +448,6 @@ async function getTags({ order = "count" } = {}) {
|
|||
// Will be done automatically in the background
|
||||
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) {
|
||||
// Erase database images from S3 server
|
||||
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) {
|
||||
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 };
|
||||
};
|
||||
|
||||
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 };
|
||||
|
|
|
@ -46,7 +46,7 @@ async function postSetting(request, response) {
|
|||
async function postImage(request, response) {
|
||||
// TODO: Permissions for uploading images
|
||||
// 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) {
|
||||
// TODO: Permissions for deleting image
|
||||
|
@ -54,7 +54,7 @@ async function deleteImage(req, res) {
|
|||
}
|
||||
async function deleteBlog(req, res) {
|
||||
// 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) {
|
||||
// FIXME: validate does not return post id
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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() };
|
||||
}
|
|
@ -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();
|
|
@ -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}`;
|
||||
}
|
|
@ -48,9 +48,12 @@ rich_text_editors.forEach((editor) => {
|
|||
data_blob: new Blob([await files[i].arrayBuffer()]),
|
||||
content_type: files[i].type,
|
||||
};
|
||||
|
||||
let form_data = {
|
||||
buffer: await _readFile(image_object.data_blob),
|
||||
content_type: image_object.content_type,
|
||||
post_id: window.location.href.split("/")[4],
|
||||
parent_type: "posts",
|
||||
};
|
||||
|
||||
const image_uploading_request = await request("/api/web/image", "POST", form_data);
|
||||
|
|
Loading…
Reference in New Issue