|
10 | 10 | setupEventListeners(); |
11 | 11 |
|
12 | 12 | function renderContent() { |
13 | | - const mainContent = document.getElementById('main-content'); |
14 | 13 | if (token) { |
15 | | - renderFileList(mainContent); |
| 14 | + renderFileList(); |
16 | 15 | } else { |
17 | | - renderTokenInput(mainContent); |
| 16 | + renderTokenInput(); |
18 | 17 | } |
19 | 18 | } |
20 | 19 |
|
21 | | - function renderTokenInput(container) { |
22 | | - container.innerHTML = ` |
23 | | - <div class="login-container"> |
24 | | - <div class="login-form"> |
25 | | - <div class="form-group"> |
26 | | - <label for="token-input">Enter your access token</label> |
27 | | - <input type="text" id="token-input" autofocus> |
28 | | - <div class="helper-text" id="helper-text"> |
29 | | - Enter the access token displayed on the SmallBASIC [About] screen. |
30 | | - </div> |
31 | | - </div> |
32 | | - <button class="btn" id="submit-btn">Submit</button> |
33 | | - </div> |
34 | | - </div> |
35 | | - `; |
36 | | - |
| 20 | + function renderTokenInput() { |
37 | 21 | const tokenInput = document.getElementById('token-input'); |
38 | 22 | const submitBtn = document.getElementById('submit-btn'); |
39 | 23 |
|
| 24 | + showLoginView(true); |
| 25 | + |
40 | 26 | tokenInput.addEventListener('keypress', (e) => { |
41 | 27 | if (e.key === 'Enter') { |
42 | 28 | handleLogin(); |
|
49 | 35 | } |
50 | 36 |
|
51 | 37 | function renderFileList(container) { |
52 | | - container.innerHTML = ` |
53 | | - <div class="table-container"> |
54 | | - <table> |
55 | | - <thead> |
56 | | - <tr> |
57 | | - <th> |
58 | | - <input type="checkbox" class="checkbox" id="select-all"> |
59 | | - </th> |
60 | | - <th data-field="fileName" class="sortable" style="cursor: pointer;"> |
61 | | - Name <span class="sort-indicator active">↑</span> |
62 | | - </th> |
63 | | - <th data-field="size" class="sortable" style="cursor: pointer;"> |
64 | | - Size <span class="sort-indicator">↕</span> |
65 | | - </th> |
66 | | - <th data-field="date" class="sortable" style="cursor: pointer;"> |
67 | | - Modified <span class="sort-indicator">↕</span> |
68 | | - </th> |
69 | | - </tr> |
70 | | - </thead> |
71 | | - <tbody id="file-table-body"> |
72 | | - <!-- Rows will be inserted here --> |
73 | | - </tbody> |
74 | | - </table> |
75 | | - </div> |
76 | | - `; |
77 | | - |
78 | | - const toolbar = document.getElementById('toolbar'); |
79 | | - toolbar.classList.remove('hidden'); |
80 | | - |
| 38 | + showLoginView(false); |
81 | 39 | renderTableRows(); |
82 | 40 | updateToolbarState(); |
83 | 41 | setupFileListEventListeners(); |
84 | 42 | } |
85 | 43 |
|
86 | 44 | function renderTableRows() { |
87 | 45 | const tbody = document.getElementById('file-table-body'); |
88 | | - if (!tbody) return; |
| 46 | + if (!tbody) { |
| 47 | + console.err("file-table-body not found"); |
| 48 | + return; |
| 49 | + } |
89 | 50 |
|
90 | 51 | // Sort rows |
91 | 52 | const sortedRows = [...rows].sort((a, b) => { |
|
274 | 235 | if (!editingCell) return; |
275 | 236 |
|
276 | 237 | const newValue = input.value.trim(); |
| 238 | + // this overwrites 'input' thus removing from the DOM and GCd |
277 | 239 | editingCell.textContent = save ? newValue : currentValue; |
278 | 240 |
|
279 | 241 | if (save && newValue !== currentValue && newValue) { |
|
327 | 289 | async function handleFileUpload(event) { |
328 | 290 | const files = Array.from(event.target.files); |
329 | 291 | if (files.length === 0) return; |
| 292 | + let result = null; |
330 | 293 |
|
331 | 294 | showSnackbar('Uploading files...', 'info'); |
332 | 295 |
|
333 | 296 | try { |
334 | 297 | for (let i = 0; i < files.length; i++) { |
335 | 298 | const file = files[i]; |
336 | | - const dataUrl = await fileToDataURL(file); |
337 | | - await callApi('/api/upload', `fileName=${encodeURIComponent(file.name)}&data=${dataUrl}`); |
| 299 | + |
| 300 | + // Send raw file bytes |
| 301 | + const response = await fetch('/api/upload', { |
| 302 | + method: 'POST', |
| 303 | + headers: { |
| 304 | + 'Content-Type': 'application/octet-stream', |
| 305 | + 'X-File-Name': encodeURIComponent(file.name), |
| 306 | + 'X-File-Size': file.size |
| 307 | + }, |
| 308 | + body: file |
| 309 | + }); |
| 310 | + |
| 311 | + if (!response.ok) { |
| 312 | + throw new Error('Upload failed'); |
| 313 | + } |
| 314 | + |
| 315 | + result = await response.json() |
| 316 | + if (result.error) { |
| 317 | + throw new Error(result.error); |
| 318 | + } |
338 | 319 |
|
339 | 320 | if (files.length > 1) { |
340 | 321 | showSnackbar(`Uploaded ${i + 1}/${files.length} files`, 'info'); |
341 | 322 | } |
342 | 323 | } |
343 | 324 |
|
344 | | - const data = await callApi('/api/files', ''); |
345 | | - rows = data; |
346 | | - selectedRows.clear(); |
347 | | - renderTableRows(); |
348 | | - updateToolbarState(); |
349 | | - showSnackbar(`Successfully uploaded ${files.length} file(s)`, 'success'); |
| 325 | + // refresh file list |
| 326 | + rows = await callApi('/api/files', ''); |
| 327 | + |
| 328 | + if (result?.success && files.length === 1) { |
| 329 | + showSnackbar(result.success, 'success'); |
| 330 | + } else { |
| 331 | + showSnackbar(`Successfully uploaded ${files.length} file(s)`, 'success'); |
| 332 | + } |
350 | 333 | } catch (error) { |
351 | | - showSnackbar(error, 'error'); |
352 | | - // Refresh to show any partial uploads |
| 334 | + // refresh to show any partial uploads |
353 | 335 | try { |
354 | | - const data = await callApi('/api/files', ''); |
355 | | - rows = data; |
356 | | - renderTableRows(); |
| 336 | + rows = await callApi('/api/files', ''); |
357 | 337 | } catch (e) {} |
| 338 | + showSnackbar(error.message, 'error'); |
358 | 339 | } |
359 | | - |
360 | | - // Reset file input |
361 | 340 | event.target.value = ''; |
| 341 | + |
| 342 | + selectedRows.clear(); |
| 343 | + renderTableRows(); |
| 344 | + updateToolbarState(); |
362 | 345 | } |
363 | 346 |
|
364 | 347 | function handleDownload() { |
|
404 | 387 | `Are you sure you want to permanently delete ${selectedRow.fileName}? You cannot undo ` |
405 | 388 | )) { |
406 | 389 | try { |
407 | | - const data = await callApi('/api/delete', `fileName=${encodeURIComponent(selectedRow.fileName)}`); |
408 | | - rows = data; |
| 390 | + rows = await callApi('/api/delete', `fileName=${encodeURIComponent(selectedRow.fileName)}`); |
409 | 391 | selectedRows.clear(); |
410 | 392 | renderTableRows(); |
411 | 393 | updateToolbarState(); |
|
443 | 425 |
|
444 | 426 | const data = await response.json(); |
445 | 427 | if (data.error) { |
446 | | - console.log(data); |
| 428 | + console.error(data); |
447 | 429 | throw new Error(data.error); |
448 | 430 | } |
| 431 | + |
449 | 432 | return data; |
450 | 433 | } |
451 | 434 |
|
|
520 | 503 | div.textContent = text; |
521 | 504 | return div.innerHTML; |
522 | 505 | } |
| 506 | + |
| 507 | + function showLoginView(show) { |
| 508 | + showView('login', show); |
| 509 | + showView('toolbar', !show); |
| 510 | + showView('content', !show); |
| 511 | + } |
| 512 | + |
| 513 | + function showView(name, show = true) { |
| 514 | + const view = document.getElementById(`view-${name}`); |
| 515 | + if (show) { |
| 516 | + view.classList.remove('hidden'); |
| 517 | + } else { |
| 518 | + view.classList.add('hidden'); |
| 519 | + } |
| 520 | + } |
| 521 | + |
523 | 522 | }()); |
0 commit comments