mirror of
https://codeberg.org/listyantidewi/your-everyday-tools.git
synced 2026-07-01 23:17:37 +08:00
330 lines
12 KiB
JavaScript
330 lines
12 KiB
JavaScript
/* ── Sidebar ──────────────────────────────────── */
|
|
function toggleCategory(btn) {
|
|
btn.classList.toggle("open");
|
|
const items = btn.nextElementSibling;
|
|
items.classList.toggle("open");
|
|
}
|
|
|
|
function openSidebar() {
|
|
document.getElementById("sidebar").classList.add("open");
|
|
document.getElementById("overlay").classList.add("open");
|
|
}
|
|
|
|
function closeSidebar() {
|
|
document.getElementById("sidebar").classList.remove("open");
|
|
document.getElementById("overlay").classList.remove("open");
|
|
}
|
|
|
|
// Highlight active nav item & auto-open its category
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const path = window.location.pathname;
|
|
document.querySelectorAll(".nav-item").forEach(a => {
|
|
if (a.getAttribute("href") === path) {
|
|
a.classList.add("active");
|
|
const items = a.closest(".nav-items");
|
|
if (items) {
|
|
items.classList.add("open");
|
|
const btn = items.previousElementSibling;
|
|
if (btn) btn.classList.add("open");
|
|
}
|
|
}
|
|
});
|
|
|
|
initUploadZone();
|
|
initToolForm();
|
|
initDependentOptions();
|
|
initCapabilityStatus();
|
|
});
|
|
|
|
async function initCapabilityStatus() {
|
|
const box = document.getElementById("capability-status");
|
|
if (!box) return;
|
|
const endpoint = box.dataset.endpoint;
|
|
if (!endpoint) return;
|
|
try {
|
|
const resp = await fetch("/capabilities");
|
|
if (!resp.ok) return;
|
|
const data = await resp.json();
|
|
const status = data.routes && data.routes[endpoint];
|
|
if (!status) return;
|
|
box.className = "capability-status " + status.quality;
|
|
box.style.display = "block";
|
|
|
|
const missing = (status.missing_engines || [])
|
|
.map(id => data.engines[id]?.label || id)
|
|
.join(", ");
|
|
const engines = (status.required_engines || [])
|
|
.map(id => data.engines[id]?.label || id)
|
|
.join(", ");
|
|
const detail = status.quality === "high"
|
|
? `Using local high-fidelity engine${engines ? ": " + engines : ""}.`
|
|
: status.quality === "basic"
|
|
? `High-fidelity engine missing${missing ? ": " + missing : ""}. ${status.fallback || ""}`
|
|
: `Required local engine missing${missing ? ": " + missing : ""}.`;
|
|
|
|
box.innerHTML = `
|
|
<strong><i class="bi ${status.quality === "high" ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill"}"></i> ${status.status}</strong>
|
|
<span>${status.label}</span>
|
|
<small>${detail}</small>
|
|
`;
|
|
} catch (_) {}
|
|
}
|
|
|
|
|
|
/* ── Upload Zone ──────────────────────────────── */
|
|
let selectedFiles = [];
|
|
|
|
function initUploadZone() {
|
|
const zone = document.getElementById("upload-zone");
|
|
const input = document.getElementById("file-input");
|
|
if (!zone || !input) return;
|
|
|
|
zone.addEventListener("dragover", e => { e.preventDefault(); zone.classList.add("dragover"); });
|
|
zone.addEventListener("dragleave", () => zone.classList.remove("dragover"));
|
|
zone.addEventListener("drop", e => {
|
|
e.preventDefault();
|
|
zone.classList.remove("dragover");
|
|
addFiles(e.dataTransfer.files);
|
|
});
|
|
|
|
input.addEventListener("change", () => {
|
|
addFiles(input.files);
|
|
input.value = "";
|
|
});
|
|
}
|
|
|
|
function addFiles(fileList) {
|
|
const input = document.getElementById("file-input");
|
|
const isMultiple = input && input.hasAttribute("multiple");
|
|
|
|
if (isMultiple) {
|
|
selectedFiles.push(...Array.from(fileList));
|
|
} else {
|
|
selectedFiles = [fileList[0]];
|
|
}
|
|
renderFileList();
|
|
}
|
|
|
|
function removeFile(idx) {
|
|
selectedFiles.splice(idx, 1);
|
|
renderFileList();
|
|
}
|
|
|
|
function renderFileList() {
|
|
const list = document.getElementById("file-list");
|
|
const prompt = document.getElementById("upload-prompt");
|
|
if (!list) return;
|
|
|
|
if (selectedFiles.length === 0) {
|
|
list.innerHTML = "";
|
|
if (prompt) prompt.style.display = "";
|
|
return;
|
|
}
|
|
if (prompt) prompt.style.display = "none";
|
|
|
|
list.innerHTML = selectedFiles.map((f, i) => `
|
|
<div class="file-item">
|
|
<span><i class="bi bi-file-earmark"></i> ${f.name}
|
|
<small>(${formatSize(f.size)})</small></span>
|
|
<button type="button" class="remove-file" onclick="removeFile(${i})">×</button>
|
|
</div>
|
|
`).join("");
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes < 1024) return bytes + " B";
|
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
|
|
return (bytes / 1048576).toFixed(1) + " MB";
|
|
}
|
|
|
|
|
|
/* ── Form Submission ──────────────────────────── */
|
|
function initToolForm() {
|
|
const form = document.getElementById("tool-form");
|
|
if (!form) return;
|
|
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const endpoint = form.dataset.endpoint;
|
|
if (!endpoint) return;
|
|
|
|
const btnText = document.querySelector(".btn-text");
|
|
const btnLoad = document.querySelector(".btn-loading");
|
|
const submitBtn = document.getElementById("submit-btn");
|
|
const resultArea = document.getElementById("result-area");
|
|
|
|
// Validate: either files or text input required
|
|
const textInput = form.querySelector("textarea[name='text']");
|
|
if (!textInput && selectedFiles.length === 0) {
|
|
showError("Please select a file first.");
|
|
return;
|
|
}
|
|
if (textInput && !textInput.value.trim()) {
|
|
showError("Please enter some text.");
|
|
return;
|
|
}
|
|
|
|
// Show loading
|
|
if (btnText) btnText.style.display = "none";
|
|
if (btnLoad) btnLoad.style.display = "inline-flex";
|
|
submitBtn.disabled = true;
|
|
resultArea.style.display = "none";
|
|
|
|
const formData = new FormData(form);
|
|
|
|
// Remove the empty file input and add our tracked files
|
|
formData.delete("files");
|
|
selectedFiles.forEach(f => formData.append("files", f));
|
|
|
|
try {
|
|
const resp = await fetch(endpoint, { method: "POST", body: formData });
|
|
|
|
if (!resp.ok) {
|
|
let msg = "Processing failed.";
|
|
try {
|
|
const json = await resp.json();
|
|
msg = json.error || msg;
|
|
} catch (_) {}
|
|
showError(msg);
|
|
return;
|
|
}
|
|
|
|
const ct = resp.headers.get("Content-Type") || "";
|
|
|
|
if (ct.includes("application/json")) {
|
|
const json = await resp.json();
|
|
if (json.error) {
|
|
showError(json.error);
|
|
} else if (json.text !== undefined) {
|
|
showTextResult(json.text);
|
|
} else if (json.data !== undefined) {
|
|
showTextResult(typeof json.data === "string" ? json.data : JSON.stringify(json.data, null, 2));
|
|
}
|
|
} else {
|
|
// Binary file download
|
|
const blob = await resp.blob();
|
|
const cd = resp.headers.get("Content-Disposition") || "";
|
|
let filename = "download";
|
|
const match = cd.match(/filename="?([^";\n]+)"?/);
|
|
if (match) filename = match[1];
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// If image, show preview
|
|
const meta = {
|
|
engine: resp.headers.get("X-Conversion-Engine") || "",
|
|
quality: resp.headers.get("X-Conversion-Quality") || "",
|
|
warnings: resp.headers.get("X-Fidelity-Warnings") || ""
|
|
};
|
|
|
|
if (ct.startsWith("image/")) {
|
|
showFileResult(url, filename, true, meta);
|
|
} else {
|
|
showFileResult(url, filename, false, meta);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
showError("Network error: " + err.message);
|
|
} finally {
|
|
if (btnText) btnText.style.display = "";
|
|
if (btnLoad) btnLoad.style.display = "none";
|
|
submitBtn.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function showError(msg) {
|
|
const area = document.getElementById("result-area");
|
|
area.style.display = "block";
|
|
document.getElementById("result-success").style.display = "none";
|
|
document.getElementById("result-text")?.style.setProperty("display", "none");
|
|
const errEl = document.getElementById("result-error");
|
|
errEl.style.display = "flex";
|
|
document.getElementById("error-message").textContent = msg;
|
|
}
|
|
|
|
function showFileResult(url, filename, isImage, meta = {}) {
|
|
const area = document.getElementById("result-area");
|
|
area.style.display = "block";
|
|
document.getElementById("result-error").style.display = "none";
|
|
document.getElementById("result-text")?.style.setProperty("display", "none");
|
|
|
|
const success = document.getElementById("result-success");
|
|
success.style.display = "flex";
|
|
document.getElementById("result-message").textContent = "File ready!";
|
|
|
|
const btn = document.getElementById("download-btn");
|
|
btn.href = url;
|
|
btn.download = filename;
|
|
btn.textContent = "";
|
|
btn.innerHTML = '<i class="bi bi-download"></i> Download ' + filename;
|
|
|
|
const preview = document.getElementById("result-preview");
|
|
if (isImage) {
|
|
preview.style.display = "block";
|
|
preview.innerHTML = `<img src="${url}" alt="Preview">`;
|
|
} else {
|
|
preview.style.display = "none";
|
|
}
|
|
|
|
const oldMeta = success.querySelector(".result-meta");
|
|
if (oldMeta) oldMeta.remove();
|
|
if (meta.engine || meta.quality || meta.warnings) {
|
|
const div = document.createElement("div");
|
|
div.className = "result-meta";
|
|
const parts = [];
|
|
if (meta.engine) parts.push(`<span>Engine: ${escapeHtml(meta.engine)}</span>`);
|
|
if (meta.quality) parts.push(`<span>Quality: ${escapeHtml(meta.quality)}</span>`);
|
|
if (meta.warnings) parts.push(`<span>Warnings: ${escapeHtml(meta.warnings)}</span>`);
|
|
div.innerHTML = parts.join("");
|
|
success.appendChild(div);
|
|
}
|
|
}
|
|
|
|
function showTextResult(text) {
|
|
const area = document.getElementById("result-area");
|
|
area.style.display = "block";
|
|
document.getElementById("result-error").style.display = "none";
|
|
document.getElementById("result-success").style.display = "none";
|
|
|
|
const textBox = document.getElementById("result-text");
|
|
if (textBox) {
|
|
textBox.style.display = "block";
|
|
document.getElementById("result-text-content").textContent = text;
|
|
}
|
|
}
|
|
|
|
function copyResult() {
|
|
const text = document.getElementById("result-text-content")?.textContent;
|
|
if (text) navigator.clipboard.writeText(text);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
return String(text).replace(/[&<>"']/g, c => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'"
|
|
}[c]));
|
|
}
|
|
|
|
|
|
/* ── Dependent Options ────────────────────────── */
|
|
function initDependentOptions() {
|
|
document.querySelectorAll("[data-depends-on]").forEach(el => {
|
|
const parentName = el.dataset.dependsOn;
|
|
const requiredVal = el.dataset.dependsValue;
|
|
const parentInput = document.querySelector(`[name="${parentName}"]`);
|
|
if (!parentInput) return;
|
|
|
|
const check = () => {
|
|
// Support comma-separated values
|
|
const vals = requiredVal.split(",");
|
|
el.style.display = vals.includes(parentInput.value) ? "" : "none";
|
|
};
|
|
parentInput.addEventListener("change", check);
|
|
check();
|
|
});
|
|
}
|