instbyte 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/bin/instbyte.js +28 -0
- package/client/assets/favicon-16.png +0 -0
- package/client/assets/favicon.png +0 -0
- package/client/assets/logo.png +0 -0
- package/client/css/app.css +813 -0
- package/client/index.html +81 -0
- package/client/js/app.js +946 -0
- package/package.json +47 -0
- package/server/cleanup.js +27 -0
- package/server/config.js +69 -0
- package/server/db.js +56 -0
- package/server/server.js +668 -0
package/client/js/app.js
ADDED
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
const socket = io();
|
|
2
|
+
|
|
3
|
+
async function applyBranding() {
|
|
4
|
+
try {
|
|
5
|
+
const res = await fetch("/branding");
|
|
6
|
+
const b = await res.json();
|
|
7
|
+
|
|
8
|
+
// Update page title and app name
|
|
9
|
+
document.title = b.appName;
|
|
10
|
+
const nameEl = document.getElementById("appName");
|
|
11
|
+
if (nameEl) nameEl.innerText = b.appName;
|
|
12
|
+
|
|
13
|
+
// Update logo src to dynamic route
|
|
14
|
+
const logoEl = document.getElementById("appLogo");
|
|
15
|
+
if (logoEl) logoEl.src = "/logo-dynamic.png";
|
|
16
|
+
|
|
17
|
+
// Inject CSS variables
|
|
18
|
+
const p = b.palette;
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
root.style.setProperty("--color-primary", p.primary);
|
|
21
|
+
root.style.setProperty("--color-primary-hover", p.primaryHover);
|
|
22
|
+
root.style.setProperty("--color-primary-light", p.primaryLight);
|
|
23
|
+
root.style.setProperty("--color-primary-dark", p.primaryDark);
|
|
24
|
+
root.style.setProperty("--color-on-primary", p.onPrimary);
|
|
25
|
+
root.style.setProperty("--color-secondary", p.secondary);
|
|
26
|
+
root.style.setProperty("--color-secondary-hover", p.secondaryHover);
|
|
27
|
+
root.style.setProperty("--color-secondary-light", p.secondaryLight);
|
|
28
|
+
root.style.setProperty("--color-on-secondary", p.onSecondary);
|
|
29
|
+
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Branding failed — default styles remain, no crash
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function formatSize(bytes) {
|
|
35
|
+
if (!bytes) return "";
|
|
36
|
+
if (bytes >= 1024 ** 3) return (bytes / 1024 ** 3).toFixed(1) + " GB";
|
|
37
|
+
if (bytes >= 1024 ** 2) return (bytes / 1024 ** 2).toFixed(1) + " MB";
|
|
38
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
39
|
+
return bytes + " B";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getSizeTag(bytes) {
|
|
43
|
+
if (!bytes) return "";
|
|
44
|
+
const mb = bytes / (1024 * 1024);
|
|
45
|
+
if (mb > 1024) return "danger-dark";
|
|
46
|
+
if (mb > 500) return "danger-light";
|
|
47
|
+
if (mb > 100) return "warn";
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Configure marked
|
|
52
|
+
marked.setOptions({
|
|
53
|
+
breaks: true,
|
|
54
|
+
gfm: true
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function looksLikeMarkdown(text) {
|
|
58
|
+
return /^#{1,3} |[*_`~]|\[.+\]\(.+\)|^[-*+] |^\d+\. |^```/m.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderText(text) {
|
|
62
|
+
if (!text) return "";
|
|
63
|
+
if (looksLikeMarkdown(text)) {
|
|
64
|
+
const html = marked.parse(text);
|
|
65
|
+
// highlight code blocks after parse
|
|
66
|
+
const wrap = document.createElement("div");
|
|
67
|
+
wrap.innerHTML = html;
|
|
68
|
+
wrap.querySelectorAll("pre code").forEach(el => hljs.highlightElement(el));
|
|
69
|
+
return `<div class="markdown-body">${wrap.innerHTML}</div>`;
|
|
70
|
+
}
|
|
71
|
+
// plain text — just escape and preserve newlines
|
|
72
|
+
return text
|
|
73
|
+
.replace(/&/g, "&")
|
|
74
|
+
.replace(/</g, "<")
|
|
75
|
+
.replace(/>/g, ">")
|
|
76
|
+
.replace(/\n/g, "<br>");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const TEXT_EXTENSIONS = [
|
|
80
|
+
'txt', 'md', 'js', 'ts', 'jsx', 'tsx', 'json', 'json5',
|
|
81
|
+
'css', 'html', 'htm', 'xml', 'svg', 'sh', 'bash', 'zsh',
|
|
82
|
+
'py', 'rb', 'php', 'java', 'c', 'cpp', 'h', 'cs', 'go',
|
|
83
|
+
'rs', 'swift', 'kt', 'yaml', 'yml', 'toml', 'ini', 'env',
|
|
84
|
+
'gitignore', 'dockerfile', 'sql', 'csv', 'log'
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const MAX_TEXT_PREVIEW_BYTES = 50 * 1024; // 50KB — beyond this we warn
|
|
88
|
+
const MAX_TEXT_LINES = 200;
|
|
89
|
+
|
|
90
|
+
function getPreviewType(filename) {
|
|
91
|
+
if (!filename) return "none";
|
|
92
|
+
const ext = filename.split(".").pop().toLowerCase();
|
|
93
|
+
if (/^(jpg|jpeg|png|gif|webp|bmp|svg)$/.test(ext)) return "image";
|
|
94
|
+
if (/^(mp4|webm|ogg|mov)$/.test(ext)) return "video";
|
|
95
|
+
if (/^(mp3|wav|ogg|m4a|flac|aac)$/.test(ext)) return "audio";
|
|
96
|
+
if (ext === "pdf") return "pdf";
|
|
97
|
+
if (TEXT_EXTENSIONS.includes(ext)) return "text";
|
|
98
|
+
return "none";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getLanguage(filename) {
|
|
102
|
+
const ext = filename.split(".").pop().toLowerCase();
|
|
103
|
+
const map = {
|
|
104
|
+
js: "javascript", ts: "typescript", jsx: "javascript",
|
|
105
|
+
tsx: "typescript", py: "python", rb: "ruby", php: "php",
|
|
106
|
+
java: "java", c: "c", cpp: "cpp", h: "c", cs: "csharp",
|
|
107
|
+
go: "go", rs: "rust", swift: "swift", kt: "kotlin",
|
|
108
|
+
html: "html", htm: "html", css: "css", json: "json",
|
|
109
|
+
json5: "json", xml: "xml", svg: "xml", sh: "bash",
|
|
110
|
+
bash: "bash", zsh: "bash", yaml: "yaml", yml: "yaml",
|
|
111
|
+
toml: "toml", sql: "sql", md: "markdown", csv: "plaintext",
|
|
112
|
+
txt: "plaintext", log: "plaintext", env: "plaintext",
|
|
113
|
+
dockerfile: "dockerfile"
|
|
114
|
+
};
|
|
115
|
+
return map[ext] || "plaintext";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let openPreviewId = null;
|
|
119
|
+
|
|
120
|
+
async function togglePreview(id, filename) {
|
|
121
|
+
const panel = document.getElementById("preview-" + id);
|
|
122
|
+
const btn = document.getElementById("prevbtn-" + id);
|
|
123
|
+
if (!panel || !btn) return;
|
|
124
|
+
|
|
125
|
+
const isOpen = panel.classList.contains("open");
|
|
126
|
+
|
|
127
|
+
// Close any other open preview first
|
|
128
|
+
if (openPreviewId && openPreviewId !== id) {
|
|
129
|
+
const otherPanel = document.getElementById("preview-" + openPreviewId);
|
|
130
|
+
const otherBtn = document.getElementById("prevbtn-" + openPreviewId);
|
|
131
|
+
if (otherPanel) otherPanel.classList.remove("open");
|
|
132
|
+
if (otherBtn) otherBtn.classList.remove("preview-active");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (isOpen) {
|
|
136
|
+
panel.classList.remove("open");
|
|
137
|
+
btn.classList.remove("preview-active");
|
|
138
|
+
openPreviewId = null;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Open this one
|
|
143
|
+
panel.classList.add("open");
|
|
144
|
+
btn.classList.add("preview-active");
|
|
145
|
+
openPreviewId = id;
|
|
146
|
+
|
|
147
|
+
// Only build content once
|
|
148
|
+
if (panel.dataset.loaded) return;
|
|
149
|
+
panel.dataset.loaded = "true";
|
|
150
|
+
|
|
151
|
+
const type = getPreviewType(filename);
|
|
152
|
+
const url = `/uploads/${filename}`;
|
|
153
|
+
|
|
154
|
+
if (type === "image") {
|
|
155
|
+
panel.innerHTML = `<img src="${url}" alt="${filename}">`;
|
|
156
|
+
|
|
157
|
+
} else if (type === "video") {
|
|
158
|
+
panel.innerHTML = `
|
|
159
|
+
<video controls preload="metadata">
|
|
160
|
+
<source src="${url}">
|
|
161
|
+
Your browser doesn't support video preview.
|
|
162
|
+
</video>`;
|
|
163
|
+
|
|
164
|
+
} else if (type === "audio") {
|
|
165
|
+
panel.innerHTML = `
|
|
166
|
+
<audio controls preload="metadata">
|
|
167
|
+
<source src="${url}">
|
|
168
|
+
Your browser doesn't support audio preview.
|
|
169
|
+
</audio>`;
|
|
170
|
+
|
|
171
|
+
} else if (type === "pdf") {
|
|
172
|
+
panel.innerHTML = `<embed src="${url}" type="application/pdf">`;
|
|
173
|
+
|
|
174
|
+
} else if (type === "text") {
|
|
175
|
+
panel.innerHTML = `<div class="preview-loading">Loading...</div>`;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(url);
|
|
179
|
+
|
|
180
|
+
if (!res.ok) throw new Error("Failed to load");
|
|
181
|
+
|
|
182
|
+
// Check size via content-length header before reading
|
|
183
|
+
const contentLength = res.headers.get("content-length");
|
|
184
|
+
if (contentLength && parseInt(contentLength) > MAX_TEXT_PREVIEW_BYTES) {
|
|
185
|
+
panel.innerHTML = `
|
|
186
|
+
<div class="preview-error">
|
|
187
|
+
File is too large to preview (${formatSize(parseInt(contentLength))}).
|
|
188
|
+
<a href="${url}" target="_blank">Open in new tab</a>
|
|
189
|
+
</div>`;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const text = await res.text();
|
|
194
|
+
const lines = text.split("\n");
|
|
195
|
+
const truncated = lines.length > MAX_TEXT_LINES;
|
|
196
|
+
const preview = truncated
|
|
197
|
+
? lines.slice(0, MAX_TEXT_LINES).join("\n")
|
|
198
|
+
: text;
|
|
199
|
+
|
|
200
|
+
const lang = getLanguage(filename);
|
|
201
|
+
const code = document.createElement("code");
|
|
202
|
+
code.className = `language-${lang}`;
|
|
203
|
+
code.textContent = preview;
|
|
204
|
+
|
|
205
|
+
const pre = document.createElement("pre");
|
|
206
|
+
pre.appendChild(code);
|
|
207
|
+
hljs.highlightElement(code);
|
|
208
|
+
|
|
209
|
+
panel.innerHTML = "";
|
|
210
|
+
panel.appendChild(pre);
|
|
211
|
+
|
|
212
|
+
if (truncated) {
|
|
213
|
+
const note = document.createElement("div");
|
|
214
|
+
note.className = "preview-truncated";
|
|
215
|
+
note.innerHTML = `Showing first ${MAX_TEXT_LINES} of ${lines.length} lines. <a href="${url}" target="_blank">View full file</a>`;
|
|
216
|
+
panel.appendChild(note);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
} catch (err) {
|
|
220
|
+
panel.innerHTML = `
|
|
221
|
+
<div class="preview-error">
|
|
222
|
+
Could not load preview. <a href="${url}" target="_blank">Open directly</a>
|
|
223
|
+
</div>`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleRowClick(el, type, value) {
|
|
229
|
+
if (type === "text") {
|
|
230
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
231
|
+
el.classList.add("flash");
|
|
232
|
+
setTimeout(() => el.classList.remove("flash"), 800);
|
|
233
|
+
});
|
|
234
|
+
} else {
|
|
235
|
+
const a = document.createElement("a");
|
|
236
|
+
a.href = value;
|
|
237
|
+
a.download = "";
|
|
238
|
+
a.click();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
document.getElementById("items").addEventListener("click", e => {
|
|
243
|
+
const left = e.target.closest(".left");
|
|
244
|
+
if (!left) return;
|
|
245
|
+
const type = left.dataset.type;
|
|
246
|
+
const value = left.dataset.value;
|
|
247
|
+
handleRowClick(left, type, value);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
let openDropdown = null;
|
|
251
|
+
|
|
252
|
+
function toggleMoveDropdown(e, id, currentChannel) {
|
|
253
|
+
e.stopPropagation();
|
|
254
|
+
|
|
255
|
+
// close any open one first
|
|
256
|
+
if (openDropdown && openDropdown !== e.currentTarget.nextElementSibling) {
|
|
257
|
+
openDropdown.classList.remove("open");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const dropdown = e.currentTarget.nextElementSibling;
|
|
261
|
+
const isOpen = dropdown.classList.contains("open");
|
|
262
|
+
|
|
263
|
+
if (isOpen) {
|
|
264
|
+
dropdown.classList.remove("open");
|
|
265
|
+
openDropdown = null;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// build channel list fresh each time (channels may have changed)
|
|
270
|
+
const others = channels.filter(c => c.name !== currentChannel);
|
|
271
|
+
dropdown.innerHTML = `<div class="dropdown-label">Move to</div>`;
|
|
272
|
+
|
|
273
|
+
others.forEach(ch => {
|
|
274
|
+
const btn = document.createElement("button");
|
|
275
|
+
btn.innerText = ch.name;
|
|
276
|
+
btn.onclick = (ev) => {
|
|
277
|
+
ev.stopPropagation();
|
|
278
|
+
moveItem(id, ch.name);
|
|
279
|
+
dropdown.classList.remove("open");
|
|
280
|
+
openDropdown = null;
|
|
281
|
+
};
|
|
282
|
+
dropdown.appendChild(btn);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
dropdown.classList.add("open");
|
|
286
|
+
openDropdown = dropdown;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function moveItem(id, toChannel) {
|
|
290
|
+
await fetch(`/item/${id}/move`, {
|
|
291
|
+
method: "PATCH",
|
|
292
|
+
headers: { "Content-Type": "application/json" },
|
|
293
|
+
body: JSON.stringify({ channel: toChannel })
|
|
294
|
+
});
|
|
295
|
+
// socket will handle the re-render via item-moved event
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
document.addEventListener("click", () => {
|
|
299
|
+
if (openDropdown) {
|
|
300
|
+
openDropdown.classList.remove("open");
|
|
301
|
+
openDropdown = null;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
let channel = null;
|
|
306
|
+
let channels = [];
|
|
307
|
+
|
|
308
|
+
let uploader = localStorage.getItem("name") || "";
|
|
309
|
+
|
|
310
|
+
async function initName() {
|
|
311
|
+
if (!uploader || uploader === "null" || uploader.trim() === "") {
|
|
312
|
+
let suggested = "USER-" + Math.floor(Math.random() * 1000);
|
|
313
|
+
const ua = navigator.userAgent;
|
|
314
|
+
if (/android/i.test(ua)) suggested = "Android-" + Math.floor(Math.random() * 1000);
|
|
315
|
+
else if (/iphone|ipad/i.test(ua)) suggested = "iPhone-" + Math.floor(Math.random() * 1000);
|
|
316
|
+
else if (/mac/i.test(ua)) suggested = "Mac-" + Math.floor(Math.random() * 1000);
|
|
317
|
+
else if (/windows/i.test(ua)) suggested = "Windows-" + Math.floor(Math.random() * 1000);
|
|
318
|
+
else if (/linux/i.test(ua)) suggested = "Linux-" + Math.floor(Math.random() * 1000);
|
|
319
|
+
else suggested = "User-" + Math.floor(Math.random() * 1000);
|
|
320
|
+
uploader = prompt("Your name?", suggested) || suggested;
|
|
321
|
+
localStorage.setItem("name", uploader);
|
|
322
|
+
}
|
|
323
|
+
document.getElementById("who").innerText = "You: " + uploader;
|
|
324
|
+
socket.emit("join", uploader);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function highlight() {
|
|
328
|
+
document.querySelectorAll(".channels button")
|
|
329
|
+
.forEach(b => b.classList.remove("active"));
|
|
330
|
+
const el = document.getElementById("ch-" + channel);
|
|
331
|
+
if (el) el.classList.add("active");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function setChannel(c) {
|
|
335
|
+
channel = c;
|
|
336
|
+
renderChannels();
|
|
337
|
+
highlight();
|
|
338
|
+
load();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function load() {
|
|
342
|
+
const res = await fetch("/items/" + channel);
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
render(data);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function render(data) {
|
|
348
|
+
const el = document.getElementById("items");
|
|
349
|
+
el.innerHTML = "";
|
|
350
|
+
|
|
351
|
+
if (!data.length) {
|
|
352
|
+
el.innerHTML = `<div class="empty-state">Nothing here yet — paste, type, or drop a file to share</div>`;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
data.forEach(i => {
|
|
357
|
+
const div = document.createElement("div");
|
|
358
|
+
div.className = "item";
|
|
359
|
+
|
|
360
|
+
let content = "";
|
|
361
|
+
|
|
362
|
+
if (i.type === "file") {
|
|
363
|
+
const isImg = i.filename.match(/\.(jpg|png|jpeg|gif)$/i);
|
|
364
|
+
const sizeLabel = formatSize(i.size);
|
|
365
|
+
const sizeClass = getSizeTag(i.size);
|
|
366
|
+
const sizeTag = sizeClass
|
|
367
|
+
? `<span class="size-tag ${sizeClass}">${sizeLabel}</span>`
|
|
368
|
+
: sizeLabel
|
|
369
|
+
? `<span style="font-size:11px;color:#9ca3af;margin-left:6px;">${sizeLabel}</span>`
|
|
370
|
+
: "";
|
|
371
|
+
|
|
372
|
+
if (isImg) {
|
|
373
|
+
content = `<img src="/uploads/${i.filename}" style="max-width:200px;border-radius:6px"><br>
|
|
374
|
+
<a href="/uploads/${i.filename}" target="_blank">${i.filename}</a>${sizeTag}`;
|
|
375
|
+
} else {
|
|
376
|
+
content = `<a href="/uploads/${i.filename}" target="_blank">${i.filename}</a>${sizeTag}`;
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
const isLink = i.content && i.content.startsWith("http");
|
|
380
|
+
if (isLink) {
|
|
381
|
+
content = `<a href="${i.content}" target="_blank">${i.content}</a>`;
|
|
382
|
+
} else {
|
|
383
|
+
content = renderText(i.content);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const pinText = i.pinned ? "unpin" : "pin";
|
|
388
|
+
|
|
389
|
+
const isFile = i.type === "file";
|
|
390
|
+
const clickValue = isFile
|
|
391
|
+
? `/uploads/${i.filename}`
|
|
392
|
+
: i.content;
|
|
393
|
+
const tooltip = isFile ? "Click to download" : "Click to copy";
|
|
394
|
+
|
|
395
|
+
div.innerHTML = `
|
|
396
|
+
<div class="item-top">
|
|
397
|
+
<div class="left"
|
|
398
|
+
data-tooltip="${i.type === 'file' ? 'Click to download' : 'Click to copy'}"
|
|
399
|
+
data-type="${i.type === 'file' ? 'file' : 'text'}"
|
|
400
|
+
data-value="${i.type === 'file'
|
|
401
|
+
? `/uploads/${i.filename}`
|
|
402
|
+
: (i.content || '').replace(/"/g, '"')}">
|
|
403
|
+
${content}
|
|
404
|
+
<div class="meta">${i.uploader}</div>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="item-actions">
|
|
407
|
+
${getPreviewType(i.filename) !== "none" && getPreviewType(i.filename) !== "image"
|
|
408
|
+
? `<button class="icon-btn" id="prevbtn-${i.id}"
|
|
409
|
+
onclick="togglePreview(${i.id}, '${i.filename}')"
|
|
410
|
+
title="Preview">👁</button>`
|
|
411
|
+
: ""}
|
|
412
|
+
<button class="icon-btn" onclick="pin(${i.id})" title="${i.pinned ? 'Unpin' : 'Pin'}">
|
|
413
|
+
${i.pinned ? "📍" : "📌"}
|
|
414
|
+
</button>
|
|
415
|
+
<div class="move-wrapper">
|
|
416
|
+
<button class="icon-btn" title="Move to channel"
|
|
417
|
+
onclick="toggleMoveDropdown(event, ${i.id}, '${i.channel}')">⇄</button>
|
|
418
|
+
<div class="move-dropdown"></div>
|
|
419
|
+
</div>
|
|
420
|
+
<button class="icon-btn delete" onclick="del(${i.id}, ${i.pinned})" title="Delete">🗑</button>
|
|
421
|
+
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
<div class="preview-panel" id="preview-${i.id}"></div>
|
|
425
|
+
`;
|
|
426
|
+
el.appendChild(div);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function renderGrouped(data) {
|
|
431
|
+
const el = document.getElementById("items");
|
|
432
|
+
el.innerHTML = "";
|
|
433
|
+
|
|
434
|
+
if (!data.length) {
|
|
435
|
+
el.innerHTML = "<div style='color:#6b7280;margin-top:10px;'>No results</div>";
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// group by channel
|
|
440
|
+
const grouped = {};
|
|
441
|
+
data.forEach(item => {
|
|
442
|
+
if (!grouped[item.channel]) grouped[item.channel] = [];
|
|
443
|
+
grouped[item.channel].push(item);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
Object.keys(grouped).forEach(ch => {
|
|
447
|
+
const section = document.createElement("div");
|
|
448
|
+
section.style.marginTop = "20px";
|
|
449
|
+
|
|
450
|
+
section.innerHTML = `
|
|
451
|
+
<div style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:8px;">
|
|
452
|
+
${ch.toUpperCase()}
|
|
453
|
+
</div>
|
|
454
|
+
`;
|
|
455
|
+
|
|
456
|
+
grouped[ch].forEach(i => {
|
|
457
|
+
const div = document.createElement("div");
|
|
458
|
+
div.className = "item";
|
|
459
|
+
|
|
460
|
+
let content = "";
|
|
461
|
+
|
|
462
|
+
if (i.type === "file") {
|
|
463
|
+
const isImg = i.filename.match(/\.(jpg|png|jpeg|gif)$/i);
|
|
464
|
+
const sizeLabel = formatSize(i.size);
|
|
465
|
+
const sizeClass = getSizeTag(i.size);
|
|
466
|
+
const sizeTag = sizeClass
|
|
467
|
+
? `<span class="size-tag ${sizeClass}">${sizeLabel}</span>`
|
|
468
|
+
: sizeLabel
|
|
469
|
+
? `<span style="font-size:11px;color:#9ca3af;margin-left:6px;">${sizeLabel}</span>`
|
|
470
|
+
: "";
|
|
471
|
+
|
|
472
|
+
if (isImg) {
|
|
473
|
+
content = `<img src="/uploads/${i.filename}" style="max-width:200px;border-radius:6px"><br>
|
|
474
|
+
<a href="/uploads/${i.filename}" target="_blank">${i.filename}</a>${sizeTag}`;
|
|
475
|
+
} else {
|
|
476
|
+
content = `<a href="/uploads/${i.filename}" target="_blank">${i.filename}</a>${sizeTag}`;
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
const isLink = i.content && i.content.startsWith("http");
|
|
480
|
+
if (isLink) {
|
|
481
|
+
content = `<a href="${i.content}" target="_blank">${i.content}</a>`;
|
|
482
|
+
} else {
|
|
483
|
+
content = renderText(i.content);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const isFile = i.type === "file";
|
|
488
|
+
const clickValue = isFile
|
|
489
|
+
? `/uploads/${i.filename}`
|
|
490
|
+
: i.content;
|
|
491
|
+
const tooltip = isFile ? "Click to download" : "Click to copy";
|
|
492
|
+
|
|
493
|
+
div.innerHTML = `
|
|
494
|
+
<div class="item-top">
|
|
495
|
+
<div class="left"
|
|
496
|
+
data-tooltip="${i.type === 'file' ? 'Click to download' : 'Click to copy'}"
|
|
497
|
+
data-type="${i.type === 'file' ? 'file' : 'text'}"
|
|
498
|
+
data-value="${i.type === 'file'
|
|
499
|
+
? `/uploads/${i.filename}`
|
|
500
|
+
: (i.content || '').replace(/"/g, '"')}">
|
|
501
|
+
${content}
|
|
502
|
+
<div class="meta">${i.uploader}</div>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="item-actions">
|
|
505
|
+
${getPreviewType(i.filename) !== "none" && getPreviewType(i.filename) !== "image"
|
|
506
|
+
? `<button class="icon-btn" id="prevbtn-${i.id}"
|
|
507
|
+
onclick="togglePreview(${i.id}, '${i.filename}')"
|
|
508
|
+
title="Preview">👁</button>`
|
|
509
|
+
: ""}
|
|
510
|
+
<button class="icon-btn" onclick="pin(${i.id})" title="${i.pinned ? 'Unpin' : 'Pin'}">
|
|
511
|
+
${i.pinned ? "📍" : "📌"}
|
|
512
|
+
</button>
|
|
513
|
+
<div class="move-wrapper">
|
|
514
|
+
<button class="icon-btn" title="Move to channel"
|
|
515
|
+
onclick="toggleMoveDropdown(event, ${i.id}, '${i.channel}')">⇄</button>
|
|
516
|
+
<div class="move-dropdown"></div>
|
|
517
|
+
</div>
|
|
518
|
+
<button class="icon-btn delete" onclick="del(${i.id}, ${i.pinned})" title="Delete">🗑</button>
|
|
519
|
+
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
<div class="preview-panel" id="preview-${i.id}"></div>
|
|
523
|
+
`;
|
|
524
|
+
|
|
525
|
+
section.appendChild(div);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
el.appendChild(section);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function sendText() {
|
|
533
|
+
const input = document.getElementById("msg");
|
|
534
|
+
const text = input.value.trim();
|
|
535
|
+
if (!text) return;
|
|
536
|
+
|
|
537
|
+
await fetch("/text", {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: { "Content-Type": "application/json" },
|
|
540
|
+
body: JSON.stringify({ content: text, channel, uploader })
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
input.value = "";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function handleEnter(e) {
|
|
547
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
sendText();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function changeName() {
|
|
554
|
+
const n = prompt("Enter name:");
|
|
555
|
+
if (!n) return;
|
|
556
|
+
uploader = n;
|
|
557
|
+
localStorage.setItem("name", n);
|
|
558
|
+
document.getElementById("who").innerText = "You: " + uploader;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let qrLoaded = false;
|
|
562
|
+
|
|
563
|
+
async function toggleQR() {
|
|
564
|
+
const card = document.getElementById("qrCard");
|
|
565
|
+
card.classList.toggle("open");
|
|
566
|
+
|
|
567
|
+
if (card.classList.contains("open") && !qrLoaded) {
|
|
568
|
+
const res = await fetch("/info");
|
|
569
|
+
const { url } = await res.json();
|
|
570
|
+
document.getElementById("qrUrl").innerText = url;
|
|
571
|
+
document.getElementById("qrImg").src =
|
|
572
|
+
`https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(url)}`;
|
|
573
|
+
qrLoaded = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
document.addEventListener("click", e => {
|
|
578
|
+
const widget = document.querySelector(".qr-widget");
|
|
579
|
+
if (!widget.contains(e.target)) {
|
|
580
|
+
document.getElementById("qrCard").classList.remove("open");
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const fileInput = document.getElementById("fileInput");
|
|
585
|
+
|
|
586
|
+
fileInput.onchange = () => {
|
|
587
|
+
const file = fileInput.files[0];
|
|
588
|
+
if (file) uploadFile(file);
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
async function del(id, pinned) {
|
|
592
|
+
if (pinned) {
|
|
593
|
+
const confirmed = confirm("This item is pinned. Are you sure you want to delete it?");
|
|
594
|
+
if (!confirmed) return;
|
|
595
|
+
}
|
|
596
|
+
await fetch("/item/" + id, { method: "DELETE" });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function pin(id) {
|
|
600
|
+
await fetch("/pin/" + id, { method: "POST" });
|
|
601
|
+
load();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
async function logout() {
|
|
606
|
+
await fetch("/logout", { method: "POST" });
|
|
607
|
+
window.location.href = "/login";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
socket.on("new-item", item => {
|
|
611
|
+
if (item.channel === channel) load();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
socket.on("delete-item", id => {
|
|
615
|
+
load();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
socket.on("item-moved", ({ id, channel: toChannel }) => {
|
|
619
|
+
if (toChannel !== channel) {
|
|
620
|
+
// item left this channel — remove it from view
|
|
621
|
+
load();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
socket.on("channel-added", (ch) => {
|
|
626
|
+
if (!channels.find(c => c.name === ch.name)) {
|
|
627
|
+
channels.push(ch);
|
|
628
|
+
renderChannels();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
socket.on("channel-deleted", ({ name }) => {
|
|
633
|
+
channels = channels.filter(c => c.name !== name);
|
|
634
|
+
if (channel === name) {
|
|
635
|
+
channel = channels.length ? channels[0].name : null;
|
|
636
|
+
load();
|
|
637
|
+
}
|
|
638
|
+
renderChannels();
|
|
639
|
+
highlight();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
socket.on("channel-renamed", ({ oldName, newName }) => {
|
|
643
|
+
channels = channels.map(c => c.name === oldName ? { ...c, name: newName } : c);
|
|
644
|
+
if (channel === oldName) channel = newName;
|
|
645
|
+
renderChannels();
|
|
646
|
+
highlight();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
socket.on("channel-pin-update", ({ name, pinned }) => {
|
|
650
|
+
if (pinned) {
|
|
651
|
+
channels = channels.map(c => c.name === name ? { ...c, pinned } : c);
|
|
652
|
+
channels.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
|
|
653
|
+
renderChannels();
|
|
654
|
+
highlight();
|
|
655
|
+
} else {
|
|
656
|
+
loadChannels();
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
document.getElementById("search").addEventListener("input", async e => {
|
|
661
|
+
const q = e.target.value.trim();
|
|
662
|
+
|
|
663
|
+
if (!q) {
|
|
664
|
+
highlight();
|
|
665
|
+
load();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// remove active tab highlight while searching
|
|
670
|
+
document.querySelectorAll(".channels button")
|
|
671
|
+
.forEach(b => b.classList.remove("active"));
|
|
672
|
+
|
|
673
|
+
const res = await fetch(`/search/${q}`);
|
|
674
|
+
const data = await res.json();
|
|
675
|
+
renderGrouped(data);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
document.addEventListener("paste", async e => {
|
|
680
|
+
const active = document.activeElement;
|
|
681
|
+
|
|
682
|
+
// if user is typing in input, don't auto-send
|
|
683
|
+
if (active && active.id === "msg") return;
|
|
684
|
+
|
|
685
|
+
const text = e.clipboardData.getData("text");
|
|
686
|
+
if (!text) return;
|
|
687
|
+
|
|
688
|
+
await fetch("/text", {
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: { "Content-Type": "application/json" },
|
|
691
|
+
body: JSON.stringify({
|
|
692
|
+
content: text,
|
|
693
|
+
channel,
|
|
694
|
+
uploader
|
|
695
|
+
})
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
function uploadFile(file) {
|
|
700
|
+
const status = document.getElementById("uploadStatus");
|
|
701
|
+
const bar = document.getElementById("uploadBar");
|
|
702
|
+
const text = document.getElementById("uploadText");
|
|
703
|
+
|
|
704
|
+
status.style.display = "block";
|
|
705
|
+
bar.style.width = "0%";
|
|
706
|
+
text.innerText = "Uploading: " + file.name;
|
|
707
|
+
|
|
708
|
+
const form = new FormData();
|
|
709
|
+
form.append("file", file);
|
|
710
|
+
form.append("channel", channel);
|
|
711
|
+
form.append("uploader", uploader);
|
|
712
|
+
|
|
713
|
+
const xhr = new XMLHttpRequest();
|
|
714
|
+
xhr.open("POST", "/upload", true);
|
|
715
|
+
|
|
716
|
+
xhr.upload.onprogress = e => {
|
|
717
|
+
if (e.lengthComputable) {
|
|
718
|
+
const percent = Math.round((e.loaded / e.total) * 100);
|
|
719
|
+
bar.style.width = percent + "%";
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
xhr.onload = () => {
|
|
724
|
+
status.style.display = "none";
|
|
725
|
+
if (xhr.status === 413) {
|
|
726
|
+
alert("File too large — 2GB maximum allowed.");
|
|
727
|
+
fileInput.value = "";
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
fileInput.value = "";
|
|
731
|
+
load();
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
xhr.onerror = () => {
|
|
735
|
+
status.style.display = "none";
|
|
736
|
+
alert("Upload failed");
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
xhr.send(form);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
const overlay = document.getElementById("dragOverlay");
|
|
744
|
+
let dragCounter = 0;
|
|
745
|
+
|
|
746
|
+
/* DRAG ENTER */
|
|
747
|
+
document.addEventListener("dragenter", e => {
|
|
748
|
+
e.preventDefault();
|
|
749
|
+
dragCounter++;
|
|
750
|
+
overlay.style.display = "flex";
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
/* DRAG LEAVE */
|
|
754
|
+
document.addEventListener("dragleave", e => {
|
|
755
|
+
dragCounter--;
|
|
756
|
+
if (dragCounter <= 0) {
|
|
757
|
+
overlay.style.display = "none";
|
|
758
|
+
dragCounter = 0;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
/*
|
|
763
|
+
document.getElementById("addChannelBtn").onclick = async () => {
|
|
764
|
+
if (channels.length >= 10) { alert("Maximum 10 channels allowed"); return; }
|
|
765
|
+
const name = prompt("Channel name?");
|
|
766
|
+
if (!name) return;
|
|
767
|
+
|
|
768
|
+
const res = await fetch("/channels", {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: { "Content-Type": "application/json" },
|
|
771
|
+
body: JSON.stringify({ name })
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (!res.ok) { const err = await res.json(); alert(err.error); return; }
|
|
775
|
+
await loadChannels();
|
|
776
|
+
};
|
|
777
|
+
*/
|
|
778
|
+
|
|
779
|
+
function addChannelHandler() {
|
|
780
|
+
return async () => {
|
|
781
|
+
if (channels.length >= 10) { alert("Maximum 10 channels allowed"); return; }
|
|
782
|
+
const name = prompt("Channel name?");
|
|
783
|
+
if (!name) return;
|
|
784
|
+
|
|
785
|
+
const res = await fetch("/channels", {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: { "Content-Type": "application/json" },
|
|
788
|
+
body: JSON.stringify({ name })
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
if (!res.ok) { const err = await res.json(); alert(err.error); return; }
|
|
792
|
+
await loadChannels();
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
document.getElementById("addChannelBtn").onclick = addChannelHandler();
|
|
797
|
+
document.getElementById("addChannelBtnDesktop").onclick = addChannelHandler();
|
|
798
|
+
|
|
799
|
+
async function loadChannels() {
|
|
800
|
+
const res = await fetch("/channels");
|
|
801
|
+
const data = await res.json();
|
|
802
|
+
channels = data; // [{ id, name, pinned }]
|
|
803
|
+
if (!channel && channels.length) channel = channels[0].name;
|
|
804
|
+
renderChannels();
|
|
805
|
+
highlight();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
function renderChannels() {
|
|
810
|
+
const container = document.getElementById("channels");
|
|
811
|
+
container.innerHTML = "";
|
|
812
|
+
|
|
813
|
+
channels.forEach(ch => {
|
|
814
|
+
const btn = document.createElement("button");
|
|
815
|
+
btn.id = "ch-" + ch.name;
|
|
816
|
+
|
|
817
|
+
btn.onclick = (e) => {
|
|
818
|
+
if (e.target.classList.contains("ch-more")) return;
|
|
819
|
+
setChannel(ch.name);
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
btn.appendChild(document.createTextNode(ch.name));
|
|
823
|
+
|
|
824
|
+
if (ch.pinned) {
|
|
825
|
+
const dot = document.createElement("span");
|
|
826
|
+
dot.className = "ch-pin-dot";
|
|
827
|
+
dot.title = "Pinned — protected from deletion";
|
|
828
|
+
btn.appendChild(dot);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const more = document.createElement("span");
|
|
832
|
+
more.className = "ch-more";
|
|
833
|
+
more.innerText = "⋯";
|
|
834
|
+
more.title = "Channel options";
|
|
835
|
+
more.onclick = (e) => {
|
|
836
|
+
e.stopPropagation();
|
|
837
|
+
showChannelMenu(e, ch);
|
|
838
|
+
};
|
|
839
|
+
btn.appendChild(more);
|
|
840
|
+
|
|
841
|
+
btn.addEventListener("contextmenu", (e) => {
|
|
842
|
+
e.preventDefault();
|
|
843
|
+
showChannelMenu(e, ch);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
if (ch.name === channel) btn.classList.add("active");
|
|
847
|
+
container.appendChild(btn);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function showChannelMenu(e, ch) {
|
|
853
|
+
const menu = document.getElementById("channelMenu");
|
|
854
|
+
|
|
855
|
+
menu.innerHTML = `
|
|
856
|
+
<button onclick="toggleChannelPin('${ch.name}')">
|
|
857
|
+
${ch.pinned ? "📍 Unpin channel" : "📌 Pin channel"}
|
|
858
|
+
</button>
|
|
859
|
+
<div class="menu-divider"></div>
|
|
860
|
+
<button onclick="renameChannelPrompt('${ch.name}')">✎ Rename</button>
|
|
861
|
+
<button class="${ch.pinned ? "muted" : "danger"}"
|
|
862
|
+
${ch.pinned ? "" : `onclick="deleteChannel('${ch.name}')"`}>
|
|
863
|
+
🗑 Delete${ch.pinned ? " (pinned)" : ""}
|
|
864
|
+
</button>
|
|
865
|
+
`;
|
|
866
|
+
|
|
867
|
+
menu.style.top = e.clientY + "px";
|
|
868
|
+
menu.style.left = e.clientX + "px";
|
|
869
|
+
menu.classList.add("open");
|
|
870
|
+
|
|
871
|
+
requestAnimationFrame(() => {
|
|
872
|
+
const rect = menu.getBoundingClientRect();
|
|
873
|
+
if (rect.right > window.innerWidth - 8)
|
|
874
|
+
menu.style.left = (e.clientX - rect.width) + "px";
|
|
875
|
+
if (rect.bottom > window.innerHeight - 8)
|
|
876
|
+
menu.style.top = (e.clientY - rect.height) + "px";
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
document.addEventListener("click", () => {
|
|
881
|
+
document.getElementById("channelMenu").classList.remove("open");
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
document.addEventListener("contextmenu", (e) => {
|
|
885
|
+
if (!e.target.closest("#channels"))
|
|
886
|
+
document.getElementById("channelMenu").classList.remove("open");
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
async function toggleChannelPin(name) {
|
|
890
|
+
await fetch(`/channels/${name}/pin`, { method: "POST" });
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function renameChannelPrompt(name) {
|
|
894
|
+
const newName = prompt("Rename channel to:", name);
|
|
895
|
+
if (!newName || newName.trim() === name) return;
|
|
896
|
+
|
|
897
|
+
const res = await fetch("/channels/" + name, {
|
|
898
|
+
method: "PATCH",
|
|
899
|
+
headers: { "Content-Type": "application/json" },
|
|
900
|
+
body: JSON.stringify({ name: newName.trim() })
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
if (!res.ok) { const err = await res.json(); alert(err.error); }
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function deleteChannel(name) {
|
|
907
|
+
const confirmed = confirm(`Delete "${name}"? All items will be permanently removed.`);
|
|
908
|
+
if (!confirmed) return;
|
|
909
|
+
|
|
910
|
+
const res = await fetch("/channels/" + name, { method: "DELETE" });
|
|
911
|
+
if (!res.ok) { const err = await res.json(); alert(err.error); }
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/* DRAG OVER */
|
|
915
|
+
document.addEventListener("dragover", e => {
|
|
916
|
+
e.preventDefault();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
/* DROP ANYWHERE */
|
|
920
|
+
document.addEventListener("drop", async e => {
|
|
921
|
+
e.preventDefault();
|
|
922
|
+
overlay.style.display = "none";
|
|
923
|
+
dragCounter = 0;
|
|
924
|
+
|
|
925
|
+
const file = e.dataTransfer.files[0];
|
|
926
|
+
if (!file) return;
|
|
927
|
+
|
|
928
|
+
const form = new FormData();
|
|
929
|
+
form.append("file", file);
|
|
930
|
+
form.append("channel", channel);
|
|
931
|
+
form.append("uploader", uploader);
|
|
932
|
+
|
|
933
|
+
uploadFile(file);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
(async function init() {
|
|
937
|
+
await applyBranding();
|
|
938
|
+
await initName();
|
|
939
|
+
const infoRes = await fetch("/info");
|
|
940
|
+
const info = await infoRes.json();
|
|
941
|
+
if (!info.hasAuth) {
|
|
942
|
+
document.getElementById("logoutBtn").style.display = "none";
|
|
943
|
+
}
|
|
944
|
+
await loadChannels();
|
|
945
|
+
load();
|
|
946
|
+
})();
|