Implement drag'n'drop

This commit is contained in:
Reimar 2026-01-08 12:53:23 +01:00
parent 63f5ddd8af
commit 266fe05c09
Signed by: Reimar
GPG Key ID: 93549FA07F0AE268
5 changed files with 104 additions and 42 deletions

View File

@ -0,0 +1,63 @@
export class FileSelector {
dropZones = [];
selectedFile = null;
onFileSelected = null;
constructor(mimeType) {
this.mimeType = mimeType;
window.ondrop = (e) => e.preventDefault();
window.ondragover = (e) => {
if (![...e.dataTransfer.items].some(it => it.kind === "file")) return;
e.preventDefault();
if (!this.dropZones.some(dropZone => dropZone.contains(e.target))) {
e.dataTransfer.dropEffect = "none";
}
};
}
selectFile(file) {
this.selectedFile = file;
if (this.onFileSelected) this.onFileSelected(file);
}
addInput(elem) {
elem.oninput = () => {
setTimeout(() => this.selectFile(elem.files[0]), 200);
};
}
addDropZone(elem) {
this.dropZones.push(elem);
elem.ondrop = (e) => {
e.preventDefault();
const file = [...e.dataTransfer.files].find(file => file.type.startsWith(this.mimeType));
if (file) {
this.selectFile(file);
}
};
elem.ondragover = (e) => {
const items = [...e.dataTransfer.items];
if (!items.some(it => it.kind === "file")) return;
e.preventDefault();
if (items.some(it => it.type.startsWith(this.mimeType))) {
e.dataTransfer.dropEffect = "copy";
elem.classList.add("dropping");
} else {
e.dataTransfer.dropEffect = "copy";
}
};
elem.ondragleave = () => elem.classList.remove("dropping");
}
}

View File

@ -1,21 +1,33 @@
import { Notifier } from "./Notifier.js"; import { Notifier } from "./Notifier.js";
import { ProgressBar } from "./ProgressBar.js"; import { ProgressBar } from "./ProgressBar.js";
import { VideoCompressor } from "./VideoCompressor.js"; import { VideoCompressor } from "./VideoCompressor.js";
import { FileSelector } from "./FileSelector.js";
const notifier = new Notifier(); const notifier = new Notifier();
const progressBar = new ProgressBar(); const progressBar = new ProgressBar();
const videoCompressor = new VideoCompressor(progressBar); const videoCompressor = new VideoCompressor(progressBar);
const fileSelector = new FileSelector("video/");
// Reset file input cache after reload fileSelector.addInput(document.getElementById("file-input"));
document.getElementById("file-input").value = ""; fileSelector.addDropZone(document.getElementById("file-drop-area"));
fileSelector.onFileSelected = file => {
document.getElementById("uploaded-video").src = URL.createObjectURL(file);
document.getElementById("uploaded-video").load();
hideElements("#file-drop-area", "#file-input-spacing");
showElements("#uploaded-video", "#change-file");
};
async function compress(filesize, filesizeUnit) { async function compress(filesize, filesizeUnit) {
document.getElementById("uploaded-video").pause(); document.getElementById("uploaded-video").pause();
const file = document.getElementById("file-input").files[0]; const sectionChangePromise = showSection("loading");
if (!file) { if (!fileSelector.selectedFile) {
await sectionChangePromise; // Avoid changing sections at the same time
alert("Please select a file"); alert("Please select a file");
return; return;
} }
@ -27,12 +39,11 @@ async function compress(filesize, filesizeUnit) {
case "mb": targetFilesize = filesize * 1000000; break; case "mb": targetFilesize = filesize * 1000000; break;
} }
showSection("loading");
let result; let result;
try { try {
result = await videoCompressor.compress(file, targetFilesize, videoLength); result = await videoCompressor.compress(fileSelector.selectedFile, targetFilesize, videoLength);
} catch (e) { } catch (e) {
await sectionChangePromise; // Avoid changing sections at the same time
alert(e.message); alert(e.message);
showSection("file-picker"); showSection("file-picker");
@ -80,45 +91,16 @@ document.getElementById("compress").onclick = async () => {
await compress(filesize, filesizeUnit); await compress(filesize, filesizeUnit);
} }
for (const preset of document.getElementsByClassName("preset")) {
preset.onclick = () => compress(parseInt(preset.dataset.size), preset.dataset.unit);
}
document.getElementById("cancel").onclick = () => { document.getElementById("cancel").onclick = () => {
videoCompressor.stop(); videoCompressor.stop();
showSection("file-picker"); showSection("file-picker");
}; };
function openFileSelector() {
document.getElementById("file-input").click();
}
document.getElementById("file-drop-area").onclick = openFileSelector; document.getElementById("change-file").onclick = () => document.getElementById("file-input").click();
document.getElementById("change-file").onclick = openFileSelector;
for (const tabbable of document.querySelectorAll("[tabindex='0']")) { document.getElementById("back").onclick = () => showSection("file-picker");
tabbable.onkeydown = event => {
if (['Enter', 'Space'].includes(event.code))
tabbable.click();
}
}
document.getElementById("file-input").oninput = () => {
setTimeout(() => {
const file = document.getElementById("file-input").files[0];
document.getElementById("uploaded-video").src = URL.createObjectURL(file);
document.getElementById("uploaded-video").load();
hideElements("#file-drop-area", "#file-input-spacing");
showElements("#uploaded-video", "#change-file");
}, 200);
};
document.getElementById("back").onclick = () => {
showSection("file-picker");
};
window.onbeforeunload = event => { window.onbeforeunload = event => {
if (videoCompressor.inProgress) event.preventDefault(); if (videoCompressor.inProgress) event.preventDefault();
@ -130,3 +112,14 @@ function bytesToSizeString(bytes) {
if (bytes >= 1000) return (bytes / 1000).toFixed(1) + " KB"; if (bytes >= 1000) return (bytes / 1000).toFixed(1) + " KB";
return bytes + " B"; return bytes + " B";
} }
for (const tabbable of document.querySelectorAll("[tabindex='0']")) {
tabbable.onkeydown = event => {
if (['Enter', 'Space'].includes(event.code))
tabbable.click();
}
}
for (const preset of document.getElementsByClassName("preset")) {
preset.onclick = () => compress(parseInt(preset.dataset.size), preset.dataset.unit);
}

View File

@ -6,6 +6,8 @@ function showSection(section) {
} }
showElements(`#${section}-section`); showElements(`#${section}-section`);
return new Promise(resolve => setTimeout(resolve, TRANSITION_TIME * 2));
} }
function hideElements() { function hideElements() {

View File

@ -29,6 +29,10 @@
outline: none; outline: none;
} }
#file-drop-area.dropping {
border-color: black;
}
#uploaded-video { #uploaded-video {
display: none; display: none;
opacity: 0; opacity: 0;

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Easily compress video files to a specific file size for uploading to various communication platforms that restrict upload size"> <meta name="description" content="Easily compress video files to a specific file size for uploading to various communication platforms that restrict upload size">
<title>Video Compressor</title> <title>Compact.Video - Compress videos online</title>
<script defer src="/assets/scripts/ffmpeg/package/dist/umd/ffmpeg.js"></script> <script defer src="/assets/scripts/ffmpeg/package/dist/umd/ffmpeg.js"></script>
<script defer src="/assets/scripts/ui.js"></script> <script defer src="/assets/scripts/ui.js"></script>
<script defer type="module" src="/assets/scripts/main.js"></script> <script defer type="module" src="/assets/scripts/main.js"></script>
@ -19,10 +19,10 @@
<p>Compress video files to a specific file size, so you can upload them to your favorite social media and messaging apps!</p> <p>Compress video files to a specific file size, so you can upload them to your favorite social media and messaging apps!</p>
<section id="file-picker-section"> <section id="file-picker-section">
<div id="file-drop-area" tabindex="0"> <label id="file-drop-area" tabindex="0">
<span>Drag and drop a file here!</span> <span>Drag and drop a file here!</span>
</div> <input id="file-input" type="file" accept="video/*" tabindex="-1">
<input id="file-input" type="file" accept="video/*" tabindex="-1"> </label>
<div id="file-input-spacing" style="margin-top: -1rem"> <div id="file-input-spacing" style="margin-top: -1rem">
<button class="simple">.</button> <button class="simple">.</button>
</div> </div>