triflix commited on
Commit
114fd40
·
verified ·
1 Parent(s): df8bae7

Create static/js/app.js

Browse files
Files changed (1) hide show
  1. static/js/app.js +296 -0
static/js/app.js ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (() => {
2
+ const fileInput = document.getElementById("fileInput");
3
+ const uploadCard = document.getElementById("uploadCard");
4
+ const placeholder = document.getElementById("placeholder");
5
+ const preview = document.getElementById("preview");
6
+ const previewThumb = document.getElementById("previewThumb");
7
+ const previewName = document.getElementById("previewName");
8
+ const previewSize = document.getElementById("previewSize");
9
+ const uploadProgress = document.getElementById("uploadProgress");
10
+ const progressText = document.getElementById("progressText");
11
+ const uploadBtn = document.getElementById("uploadBtn");
12
+ const copyBtn = document.getElementById("copyBtn");
13
+ const viewBtn = document.getElementById("viewBtn");
14
+ const dlBtn = document.getElementById("dlBtn");
15
+ const historyList = document.getElementById("historyList");
16
+ const clearHistoryBtn = document.getElementById("clearHistory");
17
+ const apiBtn = document.getElementById("apiBtn");
18
+ const apiModal = document.getElementById("apiModal");
19
+ const closeApi = document.getElementById("closeApi");
20
+ const copyCurl = document.getElementById("copyCurl");
21
+ const curlExample = document.getElementById("curlExample");
22
+
23
+ const MAX_BYTES = window.APP_CONFIG.MAX_BYTES || (2 * 1024 * 1024 * 1024);
24
+ const EXPIRE_SECONDS = window.APP_CONFIG.EXPIRE_SECONDS || (3 * 3600);
25
+
26
+ let currentFile = null;
27
+ let currentSlug = null;
28
+ let currentUrl = null;
29
+
30
+ // helper
31
+ function humanFileSize(bytes) {
32
+ if (bytes === 0) return "0 B";
33
+ const k = 1024;
34
+ const dm = 1;
35
+ const sizes = ["B","KB","MB","GB","TB"];
36
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
37
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
38
+ }
39
+
40
+ // show placeholder click to open file picker
41
+ placeholder.addEventListener("click", () => fileInput.click());
42
+
43
+ fileInput.addEventListener("change", (ev) => {
44
+ const f = ev.target.files[0];
45
+ if (!f) return;
46
+ selectFile(f);
47
+ });
48
+
49
+ function selectFile(file) {
50
+ // client-side checks
51
+ if (file.size > MAX_BYTES) {
52
+ alert("File larger than max allowed (2GB).");
53
+ return;
54
+ }
55
+ const banned = ["bat","exe","cmd","sh","msi","ps1","com","scr"];
56
+ const ext = (file.name.split(".").pop() || "").toLowerCase();
57
+ if (banned.includes(ext)) {
58
+ alert("File type not allowed.");
59
+ return;
60
+ }
61
+ currentFile = file;
62
+ previewName.textContent = file.name;
63
+ previewSize.textContent = humanFileSize(file.size);
64
+ // preview thumb for images
65
+ if (file.type.startsWith("image/")) {
66
+ const url = URL.createObjectURL(file);
67
+ previewThumb.style.backgroundImage = `url(${url})`;
68
+ } else {
69
+ previewThumb.style.backgroundImage = 'none';
70
+ previewThumb.textContent = ext.toUpperCase();
71
+ }
72
+ placeholder.hidden = true;
73
+ preview.hidden = false;
74
+ uploadProgress.value = 0;
75
+ progressText.textContent = "";
76
+ }
77
+
78
+ async function uploadSelectedFile(customSlug = "") {
79
+ if (!currentFile) {
80
+ // trigger file picker
81
+ fileInput.click();
82
+ return;
83
+ }
84
+ const fd = new FormData();
85
+ fd.append("file", currentFile);
86
+ if (customSlug) fd.append("custom_slug", customSlug);
87
+
88
+ const xhr = new XMLHttpRequest();
89
+ xhr.open("POST", "/api/upload", true);
90
+
91
+ xhr.upload.onprogress = (e) => {
92
+ if (e.lengthComputable) {
93
+ const pct = Math.floor((e.loaded / e.total) * 100);
94
+ uploadProgress.value = pct;
95
+ progressText.textContent = `${pct}%`;
96
+ }
97
+ };
98
+
99
+ xhr.onload = function () {
100
+ if (xhr.status >= 200 && xhr.status < 300) {
101
+ const data = JSON.parse(xhr.responseText);
102
+ currentSlug = data.slug;
103
+ currentUrl = data.url;
104
+ // show toast and copy to clipboard
105
+ try {
106
+ navigator.clipboard.writeText(window.location.origin + currentUrl);
107
+ showToast("Link copied to clipboard");
108
+ } catch (e) {}
109
+ // save history
110
+ addHistory({
111
+ slug: data.slug,
112
+ url: data.url,
113
+ filename: data.filename,
114
+ size: data.size,
115
+ created_at: Date.now(),
116
+ expires_at: data.expires_at * 1000
117
+ });
118
+ renderHistory();
119
+ } else {
120
+ const err = tryParseJSON(xhr.responseText);
121
+ alert((err && err.detail) ? err.detail : "Upload failed.");
122
+ }
123
+ progressText.textContent = "";
124
+ uploadProgress.value = 0;
125
+ // keep preview visible and set currentUrl for view/download
126
+ if (currentSlug) {
127
+ currentUrl = `/f/${currentSlug}`;
128
+ }
129
+ };
130
+
131
+ xhr.onerror = function () {
132
+ alert("Upload failed due to network error.");
133
+ };
134
+
135
+ xhr.send(fd);
136
+ }
137
+
138
+ // wire upload button
139
+ uploadBtn.addEventListener("click", async () => {
140
+ // prompt for custom slug on long-press? For simplicity: hold Shift to allow slug
141
+ let custom = "";
142
+ if (window.event && window.event.shiftKey) {
143
+ custom = prompt("Enter custom slug (letters, numbers, -, _ )");
144
+ if (!custom) custom = "";
145
+ }
146
+ await uploadSelectedFile(custom);
147
+ });
148
+
149
+ copyBtn.addEventListener("click", () => {
150
+ if (!currentUrl && !currentSlug) return showToast("No link yet");
151
+ const u = window.location.origin + (currentUrl || `/f/${currentSlug}`);
152
+ navigator.clipboard.writeText(u).then(() => showToast("Copied"));
153
+ });
154
+
155
+ viewBtn.addEventListener("click", () => {
156
+ if (!currentUrl && !currentSlug) return showToast("No file to view");
157
+ const u = (currentUrl || `/f/${currentSlug}`);
158
+ window.open(u, "_blank");
159
+ });
160
+
161
+ dlBtn.addEventListener("click", () => {
162
+ if (!currentUrl && !currentSlug) return showToast("No file to download");
163
+ const u = (currentUrl || `/f/${currentSlug}`) + "?dl=1";
164
+ window.open(u, "_blank");
165
+ });
166
+
167
+ // history in localStorage
168
+ function historyKey(){ return "doto_history_v1" }
169
+ function getHistory(){
170
+ try {
171
+ const raw = localStorage.getItem(historyKey());
172
+ if (!raw) return [];
173
+ const parsed = JSON.parse(raw);
174
+ // filter expired entries client-side
175
+ const now = Date.now();
176
+ return (parsed || []).filter(it => !it.expires_at || it.expires_at > now);
177
+ } catch (e) {
178
+ return [];
179
+ }
180
+ }
181
+ function addHistory(item){
182
+ const arr = getHistory();
183
+ arr.unshift(item);
184
+ // cap to 50
185
+ localStorage.setItem(historyKey(), JSON.stringify(arr.slice(0,50)));
186
+ }
187
+ function clearHistory(){
188
+ localStorage.removeItem(historyKey());
189
+ renderHistory();
190
+ }
191
+
192
+ function renderHistory(){
193
+ const arr = getHistory();
194
+ historyList.innerHTML = "";
195
+ if (arr.length === 0) {
196
+ historyList.innerHTML = "<div class='muted'>No recent uploads</div>";
197
+ return;
198
+ }
199
+ arr.forEach(item => {
200
+ const el = document.createElement("div");
201
+ el.className = "history-item";
202
+ el.innerHTML = `
203
+ <div class="history-thumb" style="${item.filename && item.filename.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? 'background-image:url('+ window.location.origin + '/f/' + item.slug + ')' : ''}"></div>
204
+ <div class="history-meta">
205
+ <div style="font-weight:700">${escapeHtml(item.filename || item.slug)}</div>
206
+ <div style="font-size:12px;color:var(--muted)">${timeAgo(item.created_at)}</div>
207
+ </div>
208
+ <div style="display:flex;flex-direction:column;gap:6px">
209
+ <button class="link-btn" data-slug="${item.slug}" data-action="copy">Copy</button>
210
+ <button class="link-btn" data-slug="${item.slug}" data-action="open">Open</button>
211
+ </div>
212
+ `;
213
+ historyList.appendChild(el);
214
+ });
215
+ }
216
+
217
+ historyList.addEventListener("click", (e) => {
218
+ const btn = e.target.closest("button");
219
+ if (!btn) return;
220
+ const slug = btn.dataset.slug;
221
+ const action = btn.dataset.action;
222
+ if (action === "copy") {
223
+ const u = window.location.origin + `/f/${slug}`;
224
+ navigator.clipboard.writeText(u).then(()=> showToast("Copied"));
225
+ } else if (action === "open") {
226
+ window.open(`/f/${slug}`, "_blank");
227
+ }
228
+ });
229
+
230
+ clearHistoryBtn.addEventListener("click", () => {
231
+ if (confirm("Clear local history?")) clearHistory();
232
+ });
233
+
234
+ // modal api
235
+ apiBtn.addEventListener("click", () => {
236
+ apiModal.hidden = false;
237
+ });
238
+ closeApi.addEventListener("click", () => apiModal.hidden = true);
239
+ copyCurl.addEventListener("click", () => {
240
+ navigator.clipboard.writeText(curlExample.textContent).then(()=> showToast("Copied"));
241
+ });
242
+
243
+ // small helpers
244
+ function showToast(text) {
245
+ // lightweight toast
246
+ const t = document.createElement("div");
247
+ t.textContent = text;
248
+ t.style.position = "fixed";
249
+ t.style.bottom = "86px";
250
+ t.style.left = "50%";
251
+ t.style.transform = "translateX(-50%)";
252
+ t.style.background = "#111";
253
+ t.style.color = "#fff";
254
+ t.style.padding = "10px 14px";
255
+ t.style.borderRadius = "10px";
256
+ t.style.zIndex = 2000;
257
+ document.body.appendChild(t);
258
+ setTimeout(()=> t.remove(), 1800);
259
+ }
260
+
261
+ function timeAgo(ts){
262
+ const s = Math.floor((Date.now() - ts)/1000);
263
+ if (s < 60) return `${s}s ago`;
264
+ if (s < 3600) return `${Math.floor(s/60)}m ago`;
265
+ if (s < 86400) return `${Math.floor(s/3600)}h ago`;
266
+ return `${Math.floor(s/86400)}d ago`;
267
+ }
268
+
269
+ function escapeHtml(s){
270
+ if(!s) return "";
271
+ return s.replace(/[&<>"']/g, (m) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]));
272
+ }
273
+
274
+ function tryParseJSON(text){
275
+ try { return JSON.parse(text); } catch(e){ return null; }
276
+ }
277
+
278
+ // UI init
279
+ renderHistory();
280
+
281
+ // optional: restore last history item to current
282
+ (function restoreLast(){
283
+ const arr = getHistory();
284
+ if (arr.length > 0) {
285
+ currentSlug = arr[0].slug;
286
+ currentUrl = `/f/${currentSlug}`;
287
+ currentFile = null;
288
+ // set preview to first history filename
289
+ previewName.textContent = arr[0].filename;
290
+ previewSize.textContent = humanFileSize(arr[0].size || 0);
291
+ placeholder.hidden = true;
292
+ preview.hidden = false;
293
+ }
294
+ })();
295
+
296
+ })();