|
|
(() => { |
|
|
const fileInput = document.getElementById("fileInput"); |
|
|
const uploadCard = document.getElementById("uploadCard"); |
|
|
const placeholder = document.getElementById("placeholder"); |
|
|
const preview = document.getElementById("preview"); |
|
|
const previewThumb = document.getElementById("previewThumb"); |
|
|
const previewName = document.getElementById("previewName"); |
|
|
const previewSize = document.getElementById("previewSize"); |
|
|
const uploadProgress = document.getElementById("uploadProgress"); |
|
|
const progressText = document.getElementById("progressText"); |
|
|
const uploadBtn = document.getElementById("uploadBtn"); |
|
|
const copyBtn = document.getElementById("copyBtn"); |
|
|
const viewBtn = document.getElementById("viewBtn"); |
|
|
const dlBtn = document.getElementById("dlBtn"); |
|
|
const historyList = document.getElementById("historyList"); |
|
|
const clearHistoryBtn = document.getElementById("clearHistory"); |
|
|
const apiBtn = document.getElementById("apiBtn"); |
|
|
|
|
|
const apiModal = document.getElementById("apiModal"); |
|
|
const closeApi = document.getElementById("closeApi"); |
|
|
const copyCurl = document.getElementById("copyCurl"); |
|
|
const curlExample = document.getElementById("curlExample"); |
|
|
|
|
|
const MAX_BYTES = window.APP_CONFIG.MAX_BYTES || (2 * 1024 * 1024 * 1024); |
|
|
const EXPIRE_SECONDS = window.APP_CONFIG.EXPIRE_SECONDS || (3 * 3600); |
|
|
|
|
|
let currentFile = null; |
|
|
let currentSlug = null; |
|
|
let currentUrl = null; |
|
|
|
|
|
|
|
|
function humanFileSize(bytes) { |
|
|
if (bytes === 0) return "0 B"; |
|
|
const k = 1024; |
|
|
const dm = 1; |
|
|
const sizes = ["B","KB","MB","GB","TB"]; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; |
|
|
} |
|
|
|
|
|
|
|
|
if (placeholder && fileInput) { |
|
|
placeholder.addEventListener("click", () => fileInput.click()); |
|
|
} |
|
|
|
|
|
if (fileInput) { |
|
|
fileInput.addEventListener("change", (ev) => { |
|
|
const f = ev.target.files[0]; |
|
|
if (!f) return; |
|
|
selectFile(f); |
|
|
}); |
|
|
} |
|
|
|
|
|
function selectFile(file) { |
|
|
|
|
|
if (file.size > MAX_BYTES) { |
|
|
alert("File larger than max allowed (2GB)."); |
|
|
return; |
|
|
} |
|
|
const banned = ["bat","exe","cmd","sh","msi","ps1","com","scr"]; |
|
|
const ext = (file.name.split(".").pop() || "").toLowerCase(); |
|
|
if (banned.includes(ext)) { |
|
|
alert("File type not allowed."); |
|
|
return; |
|
|
} |
|
|
currentFile = file; |
|
|
previewName.textContent = file.name; |
|
|
previewSize.textContent = humanFileSize(file.size); |
|
|
|
|
|
if (file.type && file.type.startsWith("image/")) { |
|
|
const url = URL.createObjectURL(file); |
|
|
previewThumb.style.backgroundImage = `url(${url})`; |
|
|
previewThumb.textContent = ""; |
|
|
} else { |
|
|
previewThumb.style.backgroundImage = 'none'; |
|
|
previewThumb.textContent = ext ? ext.toUpperCase() : ""; |
|
|
} |
|
|
if (placeholder) placeholder.hidden = true; |
|
|
if (preview) preview.hidden = false; |
|
|
if (uploadProgress) uploadProgress.value = 0; |
|
|
if (progressText) progressText.textContent = ""; |
|
|
} |
|
|
|
|
|
async function uploadSelectedFile(customSlug = "") { |
|
|
if (!currentFile) { |
|
|
|
|
|
if (fileInput) fileInput.click(); |
|
|
return; |
|
|
} |
|
|
const fd = new FormData(); |
|
|
fd.append("file", currentFile); |
|
|
if (customSlug) fd.append("custom_slug", customSlug); |
|
|
|
|
|
const xhr = new XMLHttpRequest(); |
|
|
xhr.open("POST", "/api/upload", true); |
|
|
|
|
|
xhr.upload.onprogress = (e) => { |
|
|
if (e.lengthComputable) { |
|
|
const pct = Math.floor((e.loaded / e.total) * 100); |
|
|
if (uploadProgress) uploadProgress.value = pct; |
|
|
if (progressText) progressText.textContent = `${pct}%`; |
|
|
} |
|
|
}; |
|
|
|
|
|
xhr.onload = function () { |
|
|
if (xhr.status >= 200 && xhr.status < 300) { |
|
|
const data = JSON.parse(xhr.responseText); |
|
|
currentSlug = data.slug; |
|
|
currentUrl = data.url; |
|
|
|
|
|
try { |
|
|
navigator.clipboard.writeText(window.location.origin + currentUrl); |
|
|
showToast("Link copied to clipboard"); |
|
|
} catch (e) {} |
|
|
|
|
|
addHistory({ |
|
|
slug: data.slug, |
|
|
url: data.url, |
|
|
filename: data.filename, |
|
|
size: data.size, |
|
|
created_at: Date.now(), |
|
|
expires_at: data.expires_at * 1000 |
|
|
}); |
|
|
renderHistory(); |
|
|
} else { |
|
|
const err = tryParseJSON(xhr.responseText); |
|
|
alert((err && err.detail) ? err.detail : "Upload failed."); |
|
|
} |
|
|
if (progressText) progressText.textContent = ""; |
|
|
if (uploadProgress) uploadProgress.value = 0; |
|
|
|
|
|
if (currentSlug) { |
|
|
currentUrl = `/f/${currentSlug}`; |
|
|
} |
|
|
}; |
|
|
|
|
|
xhr.onerror = function () { |
|
|
alert("Upload failed due to network error."); |
|
|
}; |
|
|
|
|
|
xhr.send(fd); |
|
|
} |
|
|
|
|
|
|
|
|
if (uploadBtn) { |
|
|
uploadBtn.addEventListener("click", async (ev) => { |
|
|
|
|
|
const isShift = ev.shiftKey; |
|
|
let custom = ""; |
|
|
if (isShift) { |
|
|
custom = prompt("Enter custom slug (letters, numbers, -, _ )"); |
|
|
if (!custom) custom = ""; |
|
|
} |
|
|
await uploadSelectedFile(custom); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (copyBtn) { |
|
|
copyBtn.addEventListener("click", () => { |
|
|
if (!currentUrl && !currentSlug) return showToast("No link yet"); |
|
|
const u = window.location.origin + (currentUrl || `/f/${currentSlug}`); |
|
|
navigator.clipboard.writeText(u).then(() => showToast("Copied")); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (viewBtn) { |
|
|
viewBtn.addEventListener("click", () => { |
|
|
if (!currentUrl && !currentSlug) return showToast("No file to view"); |
|
|
const u = (currentUrl || `/f/${currentSlug}`); |
|
|
window.open(u, "_blank"); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (dlBtn) { |
|
|
dlBtn.addEventListener("click", () => { |
|
|
if (!currentUrl && !currentSlug) return showToast("No file to download"); |
|
|
const u = (currentUrl || `/f/${currentSlug}`) + "?dl=1"; |
|
|
window.open(u, "_blank"); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function historyKey(){ return "doto_history_v1" } |
|
|
function getHistory(){ |
|
|
try { |
|
|
const raw = localStorage.getItem(historyKey()); |
|
|
if (!raw) return []; |
|
|
const parsed = JSON.parse(raw); |
|
|
|
|
|
const now = Date.now(); |
|
|
return (parsed || []).filter(it => !it.expires_at || it.expires_at > now); |
|
|
} catch (e) { |
|
|
return []; |
|
|
} |
|
|
} |
|
|
function addHistory(item){ |
|
|
const arr = getHistory(); |
|
|
arr.unshift(item); |
|
|
|
|
|
localStorage.setItem(historyKey(), JSON.stringify(arr.slice(0,50))); |
|
|
} |
|
|
function clearHistory(){ |
|
|
localStorage.removeItem(historyKey()); |
|
|
renderHistory(); |
|
|
} |
|
|
|
|
|
function renderHistory(){ |
|
|
const arr = getHistory(); |
|
|
if (!historyList) return; |
|
|
historyList.innerHTML = ""; |
|
|
if (arr.length === 0) { |
|
|
historyList.innerHTML = "<div class='muted'>No recent uploads</div>"; |
|
|
return; |
|
|
} |
|
|
arr.forEach(item => { |
|
|
const el = document.createElement("div"); |
|
|
el.className = "history-item"; |
|
|
|
|
|
const thumbStyle = (item.filename && item.filename.match(/\.(jpg|jpeg|png|gif|webp)$/i)) |
|
|
? `background-image:url(${window.location.origin}/f/${item.slug})` : ""; |
|
|
el.innerHTML = ` |
|
|
<div class="history-thumb" style="${thumbStyle}"></div> |
|
|
<div class="history-meta"> |
|
|
<div style="font-weight:700">${escapeHtml(item.filename || item.slug)}</div> |
|
|
<div style="font-size:12px;color:var(--muted)">${timeAgo(item.created_at)}</div> |
|
|
</div> |
|
|
<div style="display:flex;flex-direction:column;gap:6px"> |
|
|
<button class="link-btn" data-slug="${item.slug}" data-action="copy">Copy</button> |
|
|
<button class="link-btn" data-slug="${item.slug}" data-action="open">Open</button> |
|
|
</div> |
|
|
`; |
|
|
historyList.appendChild(el); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (historyList) { |
|
|
historyList.addEventListener("click", (e) => { |
|
|
const btn = e.target.closest("button"); |
|
|
if (!btn) return; |
|
|
const slug = btn.dataset.slug; |
|
|
const action = btn.dataset.action; |
|
|
if (action === "copy") { |
|
|
const u = window.location.origin + `/f/${slug}`; |
|
|
navigator.clipboard.writeText(u).then(()=> showToast("Copied")); |
|
|
} else if (action === "open") { |
|
|
window.open(`/f/${slug}`, "_blank"); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
if (clearHistoryBtn) { |
|
|
clearHistoryBtn.addEventListener("click", () => { |
|
|
if (confirm("Clear local history?")) clearHistory(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (apiBtn && apiModal) { |
|
|
apiBtn.addEventListener("click", () => { |
|
|
apiModal.hidden = false; |
|
|
|
|
|
const focusable = apiModal.querySelector("button, [tabindex]:not([tabindex='-1'])"); |
|
|
if (focusable) focusable.focus(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (closeApi && apiModal) { |
|
|
closeApi.addEventListener("click", () => { |
|
|
apiModal.hidden = true; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (apiModal) { |
|
|
apiModal.addEventListener("click", (e) => { |
|
|
|
|
|
if (e.target === apiModal) { |
|
|
apiModal.hidden = true; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener("keydown", (e) => { |
|
|
if (e.key === "Escape" && apiModal && !apiModal.hidden) { |
|
|
apiModal.hidden = true; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (copyCurl && curlExample) { |
|
|
copyCurl.addEventListener("click", () => { |
|
|
navigator.clipboard.writeText(curlExample.textContent).then(()=> showToast("Copied")); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function showToast(text) { |
|
|
const t = document.createElement("div"); |
|
|
t.textContent = text; |
|
|
t.style.position = "fixed"; |
|
|
t.style.bottom = "86px"; |
|
|
t.style.left = "50%"; |
|
|
t.style.transform = "translateX(-50%)"; |
|
|
t.style.background = "#111"; |
|
|
t.style.color = "#fff"; |
|
|
t.style.padding = "10px 14px"; |
|
|
t.style.borderRadius = "10px"; |
|
|
t.style.zIndex = 2000; |
|
|
document.body.appendChild(t); |
|
|
setTimeout(()=> t.remove(), 1800); |
|
|
} |
|
|
|
|
|
function timeAgo(ts){ |
|
|
const s = Math.floor((Date.now() - ts)/1000); |
|
|
if (s < 60) return `${s}s ago`; |
|
|
if (s < 3600) return `${Math.floor(s/60)}m ago`; |
|
|
if (s < 86400) return `${Math.floor(s/3600)}h ago`; |
|
|
return `${Math.floor(s/86400)}d ago`; |
|
|
} |
|
|
|
|
|
function escapeHtml(s){ |
|
|
if(!s) return ""; |
|
|
return s.replace(/[&<>"']/g, (m) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); |
|
|
} |
|
|
|
|
|
function tryParseJSON(text){ |
|
|
try { return JSON.parse(text); } catch(e){ return null; } |
|
|
} |
|
|
|
|
|
|
|
|
renderHistory(); |
|
|
|
|
|
|
|
|
(function restoreLast(){ |
|
|
const arr = getHistory(); |
|
|
if (arr.length > 0) { |
|
|
currentSlug = arr[0].slug; |
|
|
currentUrl = `/f/${currentSlug}`; |
|
|
currentFile = null; |
|
|
previewName.textContent = arr[0].filename; |
|
|
previewSize.textContent = humanFileSize(arr[0].size || 0); |
|
|
if (placeholder) placeholder.hidden = true; |
|
|
if (preview) preview.hidden = false; |
|
|
} |
|
|
})(); |
|
|
|
|
|
})(); |
|
|
|