quackscape/assets/js/editor.js
Kumi 7d78c5d4a0
feat: Introduce file upload functionality
This commit overhauls the user area with the addition of comprehensive
file upload capabilities for images and videos. Notably, it integrates
front-end enhancements for drag-and-drop uploads in the user area and
introduces a secure back-end handling process. The back-end adjustments
include a new media upload view and adjustments to image and video
models to support large file handling through settings for maximum image
pixel limits. Additionally, the refactor standardizes CSRF token
retrieval across JavaScript modules, improving security and code
maintainability.

- Front-end additions include detailed user feedback during the file
upload process, such as progress bars and success/error indicators.
- Back-end improvements ensure large image files are processed
efficiently, mitigating potential memory issues by configuring a maximum
image pixel threshold.
- Consolidating the CSRF token retrieval into the `api.js` module
centralizes security mechanisms, reducing redundancy and enhancing the
codebase's clarity.

Overall, these changes enrich the platform's media management
capabilities, bolster security practices, and improve user experience
through intuitive interface updates and robust back-end processing.
2024-03-16 21:30:12 +01:00

246 lines
7.8 KiB
JavaScript

import { getScene, getSceneElement, getCategory, getCookie } from "./api";
import { populateDestinationDropdown } from "./editor/teleport";
import "../css/editor.css";
let clickTimestamp = 0;
// Find parent quackscape-scene for ID
function findParentScene(element) {
var parent = element.parentElement;
while (parent.tagName != "QUACKSCAPE-SCENE") {
parent = parent.parentElement;
}
return parent;
}
// Distinguishing clicks from drags based on duration.
// TODO: Find a better way to distinguish these.
function addEventListeners(element) {
element.addEventListener("mousedown", function (event) {
clickTimestamp = event.timeStamp;
});
element.addEventListener("mouseup", function (event) {
if (event.timeStamp - clickTimestamp > 200) {
// Ignoring this, we only handle regular short clicks.
// TODO: Find a way to handle drags of elements
return;
} else {
handleClick(event);
}
// Right-clicks are definitely intentional.
element.addEventListener("contextmenu", function (event) {
handleClick(event);
});
});
}
// Open a modal for creating a new Element
function startCreateElement(event) {
var propertiesTitle = document.getElementById("propertiesTitle");
modalLabel.textContent = "Create Element";
var propertiesContent = document.getElementById("propertiesContent");
var thetaStart = cartesianToTheta(
event.detail.intersection.point.x,
event.detail.intersection.point.z
);
modalContent.innerHTML = `<b>Creating element at:</b><br/>
X: ${event.detail.intersection.point.x}<br/>
Y: ${event.detail.intersection.point.y}<br/>
Z: ${event.detail.intersection.point.z}<br/>
Calculated Theta: ${thetaStart}<br/>
Parent Element Type: ${event.srcElement.tagName}<br/>
<hr/>
<form id="newElementForm">
<label for="resourcetype" class="form-label">Element Type</label>
<select class="form-control" aria-label="Element Type" id="resourcetype">
<option selected>Select Element Type</option>
<option value="MarkerElement">Marker</option>
<option value="ImageElement">Image</option>
<option value="ImageElement">Teleport</option>
</select>
</form>
`;
}
function startModifyElement(event) {
var propertiesTitle = document.getElementById("propertiesTitle");
propertiesTitle.textContent = "Modify Element";
// Get element from API
var scene = findParentScene(event.target);
var element_data_request = getSceneElement(
scene.getAttribute("id"),
event.target.getAttribute("id")
);
element_data_request.then((element_data) => {
var propertiesContent = document.getElementById("propertiesContent");
propertiesContent.innerHTML = `<b>Modifying element:</b><br/>
Element Type: ${event.srcElement.tagName}<br/>
Element ID: ${event.target.getAttribute("id")}<br/>
Element data: ${JSON.stringify(element_data.obj)}<br/>
<hr/>
<form id="modifyElementForm">
<input type="hidden" id="csrfmiddlewaretoken" value="${getCookie(
"csrftoken"
)}">
<input type="hidden" id="id" value="${event.target.getAttribute("id")}">
<div class="dropdown mb-2">
<label for="destinationDropdownSearch" class="form-label">Teleport Destination</label>
<input class="form-control" autocomplete="off" id="destinationDropdownSearch" type="text" placeholder="Search...">
<input type="hidden" id="destination">
<div class="dropdown-menu" id="destinationDropdownMenu">
<!-- Dropdown items will be populated here by JavaScript -->
</div>
</div>
<div class="mb-2">
<label for="destination_x" class="form-label">Destination X/Y/Z Rotation</label>
<div class="row g-2">
<div class="col">
<input type="number" class="form-control" id="destination_x" name="input1" min="0" max="360" placeholder="X">
</div>
<div class="col">
<input type="number" class="form-control" id="destination_y" name="input2" min="0" max="360" placeholder="Y">
</div>
<div class="col">
<input type="number" class="form-control" id="destination_z" name="input3" min="0" max="360" placeholder="Z">
</div>
</div>
</div>
</div>
<div class="mb-2">
<label for="elementUpload" class="form-label">Marker image</label>
<input id="elementUpload" type="file" class="form-control">
</div>
</form>
`;
var scene_data_request = getScene(scene.getAttribute("id"));
scene_data_request.then((scene_data) => {
var category_data_request = getCategory(scene_data.obj.category);
category_data_request.then((category_data) => {
populateDestinationDropdown(
element_data.obj.destination,
category_data
);
});
});
document
.getElementById("elementUpload")
.addEventListener("change", function () {
if (this.files && this.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
// Set the image to the file contents
if (event.target.getAttribute("data-original-src") == undefined) {
event.target.setAttribute(
"data-original-src",
event.target.getAttribute("src")
);
}
event.target.setAttribute("src", e.target.result);
};
reader.readAsDataURL(this.files[0]);
}
});
if (element_data.obj.destination_x != -1) {
document.getElementById("destination_x").value =
element_data.obj.destination_x;
}
if (element_data.obj.destination_y != -1) {
document.getElementById("destination_y").value =
element_data.obj.destination_y;
}
if (element_data.obj.destination_z != -1) {
document.getElementById("destination_z").value =
element_data.obj.destination_z;
}
document.getElementById("resetButton").style = "display: inline;";
document.getElementById("buttons").style = "display: block;";
});
}
function cartesianToTheta(x, z) {
// Calculate the angle in radians
let angleRadians = Math.atan2(z, x);
// Convert to degrees
let angleDegrees = angleRadians * (180 / Math.PI);
// A-Frame's thetaStart is measured from the positive X-axis ("right" direction)
// and goes counter-clockwise, so this should directly give us the thetaStart value.
let thetaStart = 90 - angleDegrees;
// Since atan2 returns values from -180 to 180, let's normalize this to 0 - 360
thetaStart = thetaStart < 0 ? thetaStart + 360 : thetaStart;
return thetaStart;
}
function latLonToXYZ(lat, lon, radius = 5) {
// Convert lat/lon to X/Y/Z coordinates on the sphere
const phi = (90 - lat) * (Math.PI / 180);
const theta = (lon + 180) * (Math.PI / 180);
const x = -(radius * Math.sin(phi) * Math.cos(theta));
const y = radius * Math.cos(phi);
const z = radius * Math.sin(phi) * Math.sin(theta);
return { x, y, z };
}
function handleClick(event) {
// If clicked on the sky, start creating a new element
if (event.target.tagName == "A-SKY") {
startCreateElement(event);
} else {
// Else we are modifying an existing element
startModifyElement(event);
}
}
document.addEventListener("loadedQuackscapeScene", function (event) {
// Get the scene
var scene = document.querySelector("a-scene");
// Get all children
var children = scene.children;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.tagName.startsWith("A-")) {
// Remove original onclick events
if (child.hasAttribute("onclick")) {
child.removeAttribute("onclick");
}
// Add new event listeners
addEventListeners(child);
// Add click-drag component to all a-entity elements
child.setAttribute("click-drag", "");
}
}
});