diff --git a/controllers/upload.go b/controllers/upload.go index 408e8c4..809dc10 100644 --- a/controllers/upload.go +++ b/controllers/upload.go @@ -19,6 +19,7 @@ func generateID() (string, error) { } func HandleFileUpload(c *gin.Context) { + // Try to get a single file first (backward compatibility) file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) diff --git a/dist/fitra-darwin-amd64 b/dist/fitra-darwin-amd64 index 2404b7f..27bfc45 100755 Binary files a/dist/fitra-darwin-amd64 and b/dist/fitra-darwin-amd64 differ diff --git a/dist/fitra-darwin-arm64 b/dist/fitra-darwin-arm64 index 81dd61b..8e55af3 100755 Binary files a/dist/fitra-darwin-arm64 and b/dist/fitra-darwin-arm64 differ diff --git a/dist/fitra-linux-amd64 b/dist/fitra-linux-amd64 index 95b397b..e02c0d5 100755 Binary files a/dist/fitra-linux-amd64 and b/dist/fitra-linux-amd64 differ diff --git a/dist/fitra-linux-arm64 b/dist/fitra-linux-arm64 index c719e6b..6ab6c70 100755 Binary files a/dist/fitra-linux-arm64 and b/dist/fitra-linux-arm64 differ diff --git a/static/main.css b/static/main.css index b307b22..597d8e3 100644 --- a/static/main.css +++ b/static/main.css @@ -117,7 +117,6 @@ pre code { background: #f8fafc; box-shadow: none; border: 1px solid #e2e8f0; - border-left: 4px solid #3b82f6; transition: all 0.15s ease; position: relative; } @@ -160,7 +159,6 @@ pre code { border-radius: 12px; box-shadow: none; border: 1px solid #e2e8f0; - border-left: 4px solid #6366f1; transition: all 0.15s ease; } @@ -176,7 +174,6 @@ pre code { border-radius: 12px; box-shadow: none; border: 1px solid #dcfce7; - border-left: 4px solid #22c55e; } .storage-info h3 { @@ -229,7 +226,6 @@ pre code { border-radius: 12px; box-shadow: none; border: 1px solid #e2e8f0; - border-left: 4px solid #8b5cf6; } /* Remove redundant changelog wrapper styles */ @@ -330,7 +326,6 @@ pre code { padding: 14px 18px; border-radius: 10px; border: 1px solid #e2e8f0; - border-left: 4px solid #3b82f6; transition: all 0.15s ease; } @@ -364,7 +359,6 @@ pre code { .donation-item { background: #ffffff; border: 1px solid #e2e8f0; - border-left: 4px solid #22c55e; border-radius: 10px; padding: 12px; margin: 10px 0; @@ -733,7 +727,8 @@ pre code { .tab-nav { display: flex; - gap: 10px; + flex-wrap: wrap; + gap: 8px; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; @@ -744,6 +739,8 @@ pre code { z-index: 5; backdrop-filter: blur(8px); box-shadow: 0 2px 8px rgba(2, 6, 23, 0.04); + overflow-x: hidden; + box-sizing: border-box; } .tab-btn { @@ -751,12 +748,18 @@ pre code { background: #f1f5f9; border: 1px solid #e2e8f0; color: #0f172a; - padding: 10px 16px; - border-radius: 10px; + padding: 8px 12px; + border-radius: 8px; cursor: pointer; font-weight: 600; - font-size: 14px; + font-size: 13px; transition: all 0.15s ease; + flex: 1; + min-width: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + box-sizing: border-box; } .tab-btn:hover { @@ -781,9 +784,39 @@ pre code { .tab-panel[hidden] { display: none; } +@media (max-width: 768px) { + .tab-nav { + flex-direction: column; + gap: 6px; + padding: 8px; + margin: 8px 0 16px 0; + position: static; + } + + .tab-btn { + padding: 10px 12px; + font-size: 14px; + text-align: center; + flex: none; + width: 100%; + white-space: normal; + } +} + @media (max-width: 640px) { - .tab-layout { padding: 14px; } - .tab-nav { position: static; } + .tab-layout { padding: 12px; } + + .tab-nav { + padding: 6px; + margin: 6px 0 12px 0; + border-radius: 8px; + } + + .tab-btn { + padding: 8px 10px; + font-size: 13px; + border-radius: 6px; + } } /* Upload UI - aligned with app styles */ diff --git a/views/partials/changelog.html b/views/partials/changelog.html index df9f2fb..f137d07 100644 --- a/views/partials/changelog.html +++ b/views/partials/changelog.html @@ -1,6 +1,15 @@ {{define "changelog"}}

Changelog

+
+
v1.3.0
+
2025-08-12
+ +
+ +
v1.2.0
2025-08-09
diff --git a/views/partials/web-upload.html b/views/partials/web-upload.html index 286696c..a8a2b5b 100644 --- a/views/partials/web-upload.html +++ b/views/partials/web-upload.html @@ -1,11 +1,11 @@ {{define "web-upload"}} -

📤 Web Upload

-

Upload a file directly from your browser. Files are temporary and automatically deleted after 24 hours.

+

Web upload

+

Upload files directly from your browser. Files are temporary and automatically deleted after 24 hours.

- +
@@ -15,14 +15,7 @@
@@ -32,8 +25,6 @@ const fileInput = document.getElementById('file-input'); const statusEl = document.getElementById('upload-status'); const resultEl = document.getElementById('upload-result'); - const linkEl = document.getElementById('download-link'); - const copyBtn = document.getElementById('copy-link-btn'); const uploadBtn = document.getElementById('upload-btn'); function showStatus(msg, type) { @@ -55,62 +46,117 @@ clearStatus(); resultEl.hidden = true; - const file = fileInput.files && fileInput.files[0]; - if(!file){ - showStatus('Please choose a file first.', 'error'); + const files = fileInput.files; + if(!files || files.length === 0){ + showStatus('Please choose at least one file.', 'error'); return; } - const data = new FormData(); - data.append('file', file); - setLoading(true); + const resultsList = document.getElementById('upload-results-list'); + resultsList.innerHTML = ''; + + let successCount = 0; + let totalFiles = files.length; + try { - const resp = await fetch('/upload', { method: 'POST', body: data }); - const contentType = resp.headers.get('content-type') || ''; - if(!resp.ok){ - let message = 'Upload failed'; - if (contentType.includes('application/json')) { - const errJson = await resp.json().catch(()=>({})); - if (errJson && errJson.error) message = errJson.error; - } else { - message = await resp.text(); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + showStatus(`Uploading file ${i + 1} of ${totalFiles}: ${file.name}`, 'info'); + + const data = new FormData(); + data.append('file', file); + + try { + const resp = await fetch('/upload', { method: 'POST', body: data }); + const contentType = resp.headers.get('content-type') || ''; + + if(!resp.ok){ + let message = 'Upload failed'; + if (contentType.includes('application/json')) { + const errJson = await resp.json().catch(()=>({})); + if (errJson && errJson.error) message = errJson.error; + } else { + message = await resp.text(); + } + addFileResult(file.name, null, message, false); + continue; + } + + const json = contentType.includes('application/json') ? await resp.json() : null; + const url = json && json.url ? json.url : null; + + if(!url){ + addFileResult(file.name, null, 'Upload succeeded but no URL was returned', false); + continue; + } + + addFileResult(file.name, url, 'Success', true); + successCount++; + } catch (err) { + addFileResult(file.name, null, 'Network error', false); } - showStatus(message || 'Upload failed', 'error'); - return; } - - const json = contentType.includes('application/json') ? await resp.json() : null; - const url = json && json.url ? json.url : null; - if(!url){ - showStatus('Upload succeeded but no URL was returned by the server.', 'error'); - return; - } - - linkEl.href = url; - linkEl.textContent = url; + resultEl.hidden = false; - showStatus('Done.', 'success'); - } catch (err) { - showStatus('Network error. Please try again.', 'error'); + if (successCount === totalFiles) { + showStatus(`All ${totalFiles} files uploaded successfully!`, 'success'); + } else if (successCount > 0) { + showStatus(`${successCount} of ${totalFiles} files uploaded successfully.`, 'warning'); + } else { + showStatus('All uploads failed.', 'error'); + } } finally { setLoading(false); } }); - copyBtn.addEventListener('click', function(){ - const url = linkEl.href; - if(!url) return; - navigator.clipboard.writeText(url).then(()=>{ - const original = copyBtn.textContent; - copyBtn.textContent = 'Copied!'; - copyBtn.classList.add('copied'); - setTimeout(()=>{ copyBtn.textContent = original; copyBtn.classList.remove('copied'); }, 1500); - }).catch(()=>{ - const original = copyBtn.textContent; - copyBtn.textContent = 'Failed'; - setTimeout(()=>{ copyBtn.textContent = original; }, 1500); - }); + function addFileResult(filename, url, message, success) { + const resultsList = document.getElementById('upload-results-list'); + const resultDiv = document.createElement('div'); + resultDiv.className = 'file-result ' + (success ? 'success' : 'error'); + + if (success && url) { + resultDiv.innerHTML = ` +

${filename} - ${message}

+

+ Download URL: + ${url} +

+
+ +
+ `; + } else { + resultDiv.innerHTML = ` +

${filename} - ${message}

+ `; + } + + resultsList.appendChild(resultDiv); + } + + document.addEventListener('click', function(e) { + if (e.target.classList.contains('copy-link-btn')) { + const url = e.target.getAttribute('data-url'); + if (!url) return; + + navigator.clipboard.writeText(url).then(() => { + const original = e.target.textContent; + e.target.textContent = 'Copied!'; + e.target.classList.add('copied'); + setTimeout(() => { + e.target.textContent = original; + e.target.classList.remove('copied'); + }, 1500); + }).catch(() => { + const original = e.target.textContent; + e.target.textContent = 'Failed'; + setTimeout(() => { + e.target.textContent = original; + }, 1500); + }); + } }); })();