commit
This commit is contained in:
226
static/js/app.js
Normal file
226
static/js/app.js
Normal file
@@ -0,0 +1,226 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const dropZone = document.getElementById("drop-zone");
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const modeBtns = document.querySelectorAll(".mode-btn");
|
||||
const progress = document.getElementById("progress");
|
||||
const progressBar = progress.querySelector(".progress-bar");
|
||||
const progressText = document.getElementById("progress-text");
|
||||
const result = document.getElementById("result");
|
||||
|
||||
let currentMode = "basic";
|
||||
|
||||
modeBtns.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
modeBtns.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
currentMode = btn.dataset.mode || "basic";
|
||||
});
|
||||
});
|
||||
|
||||
["dragenter", "dragover"].forEach((e) =>
|
||||
dropZone.addEventListener(e, (ev) => {
|
||||
ev.preventDefault();
|
||||
dropZone.classList.add("dragover");
|
||||
})
|
||||
);
|
||||
|
||||
["dragleave", "drop"].forEach((e) =>
|
||||
dropZone.addEventListener(e, (ev) => {
|
||||
ev.preventDefault();
|
||||
dropZone.classList.remove("dragover");
|
||||
})
|
||||
);
|
||||
|
||||
dropZone.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
processFiles();
|
||||
});
|
||||
|
||||
fileInput.addEventListener("change", processFiles);
|
||||
|
||||
dropZone.querySelector("label")?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
function processFiles() {
|
||||
const files = fileInput.files;
|
||||
if (!files || !files.length) return;
|
||||
|
||||
const endpoint =
|
||||
currentMode === "advanced" ? "/api/upload-advanced" : "/api/upload";
|
||||
|
||||
const formData = new FormData();
|
||||
Array.from(files).forEach((file) => formData.append("files", file));
|
||||
|
||||
showProgress(currentMode.toUpperCase(), files.length);
|
||||
|
||||
fetch(endpoint, { method: "POST", body: formData })
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
hideProgress();
|
||||
if (data && data.success) showResult(data);
|
||||
else showError(data?.error || "Unknown error");
|
||||
})
|
||||
.catch((e) => {
|
||||
hideProgress();
|
||||
showError(e?.message || "Request failed");
|
||||
});
|
||||
}
|
||||
|
||||
function showProgress(mode, count) {
|
||||
progress.classList.remove("d-none");
|
||||
progressText.classList.remove("d-none");
|
||||
progressText.textContent = `${mode} - ${count} files`;
|
||||
progressBar.style.width = "45%";
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
progress.classList.add("d-none");
|
||||
progressText.classList.add("d-none");
|
||||
}
|
||||
|
||||
async function showResult(data) {
|
||||
const sizeKB = ((data.size || 0) / 1024).toFixed(1);
|
||||
|
||||
const rawHtml =
|
||||
typeof data.aio_html === "string" ? data.aio_html : String(data.aio_html ?? "");
|
||||
|
||||
const formattedHtml = await formatHtml(rawHtml);
|
||||
|
||||
result.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<i class="fa-solid fa-circle-check mt-1 flex-shrink-0"></i>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mb-1">${escapeHtml(data.message ?? "Done")}</h5>
|
||||
<div class="mb-2">
|
||||
<small><code>${escapeHtml(data.original ?? "")}</code> → <strong>${sizeKB}KB</strong></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group-vertical btn-group-sm">
|
||||
<button class="btn btn-success btn-copy-sm" type="button">
|
||||
<i class="fa-solid fa-copy me-1"></i>Copy
|
||||
</button>
|
||||
<a
|
||||
href="data:text/html;charset=utf-8,${encodeURIComponent(rawHtml)}"
|
||||
download="aio.html"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
>
|
||||
<i class="fa-solid fa-download me-1"></i>Save
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card result-card">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="fa-regular fa-eye me-2"></i>Preview</h6>
|
||||
</div>
|
||||
<iframe id="previewFrame" class="preview-iframe" style="height:70vh; min-height:520px;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card result-card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="fa-solid fa-code me-2"></i>Code</h6>
|
||||
<button class="btn btn-sm btn-success btn-copy-all" type="button">
|
||||
<i class="fa-solid fa-copy me-1"></i>Copy All
|
||||
</button>
|
||||
</div>
|
||||
<pre class="code-preview mb-0" style="max-height:70vh; min-height:520px; overflow:auto;">
|
||||
<code class="language-markup">${escapeHtml(formattedHtml)}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<button class="btn btn-outline-secondary px-4" type="button" id="btn-new">
|
||||
<i class="fa-solid fa-arrow-rotate-right me-2"></i>New
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
result.classList.remove("d-none");
|
||||
|
||||
const frame = result.querySelector("#previewFrame");
|
||||
if (frame) frame.srcdoc = rawHtml;
|
||||
|
||||
if (window.Prism) window.Prism.highlightAllUnder(result);
|
||||
|
||||
result.querySelectorAll(".btn-copy-sm, .btn-copy-all").forEach((btn) => {
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.addEventListener("click", () => {
|
||||
navigator.clipboard
|
||||
.writeText(rawHtml)
|
||||
.then(() => {
|
||||
btn.innerHTML = '<i class="fa-solid fa-check me-1"></i>Copied';
|
||||
setTimeout(() => (btn.innerHTML = originalHtml), 1200);
|
||||
})
|
||||
.catch(() => fallbackCopy(rawHtml, btn, originalHtml));
|
||||
});
|
||||
});
|
||||
|
||||
result.querySelector("#btn-new")?.addEventListener("click", () => {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
result.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
|
||||
function fallbackCopy(text, btn, originalHtml) {
|
||||
try {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
btn.innerHTML = '<i class="fa-solid fa-check me-1"></i>Copied';
|
||||
setTimeout(() => (btn.innerHTML = originalHtml), 1200);
|
||||
} catch {
|
||||
showError("Clipboard blocked by browser");
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
result.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fa-solid fa-circle-xmark me-2"></i>${escapeHtml(msg)}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button class="btn btn-outline-primary" type="button" id="btn-try">Try Again</button>
|
||||
</div>
|
||||
`;
|
||||
result.classList.remove("d-none");
|
||||
result.querySelector("#btn-try")?.addEventListener("click", () => location.reload());
|
||||
}
|
||||
});
|
||||
|
||||
async function formatHtml(html) {
|
||||
html = typeof html === "string" ? html : String(html ?? "");
|
||||
try {
|
||||
if (window.prettier && window.prettierPlugins?.html) {
|
||||
return await window.prettier.format(html, {
|
||||
parser: "html",
|
||||
plugins: [window.prettierPlugins.html],
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
text = typeof text === "string" ? text : String(text ?? "");
|
||||
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
Reference in New Issue
Block a user