Multi file upload, updated styling.

This commit is contained in:
2025-08-12 15:53:36 +02:00
parent e2772a31ab
commit 406836d1dc
8 changed files with 157 additions and 68 deletions

View File

@@ -19,6 +19,7 @@ func generateID() (string, error) {
} }
func HandleFileUpload(c *gin.Context) { func HandleFileUpload(c *gin.Context) {
// Try to get a single file first (backward compatibility)
file, header, err := c.Request.FormFile("file") file, header, err := c.Request.FormFile("file")
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"})

Binary file not shown.

Binary file not shown.

BIN
dist/fitra-linux-amd64 vendored

Binary file not shown.

BIN
dist/fitra-linux-arm64 vendored

Binary file not shown.

View File

@@ -117,7 +117,6 @@ pre code {
background: #f8fafc; background: #f8fafc;
box-shadow: none; box-shadow: none;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-left: 4px solid #3b82f6;
transition: all 0.15s ease; transition: all 0.15s ease;
position: relative; position: relative;
} }
@@ -160,7 +159,6 @@ pre code {
border-radius: 12px; border-radius: 12px;
box-shadow: none; box-shadow: none;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-left: 4px solid #6366f1;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
@@ -176,7 +174,6 @@ pre code {
border-radius: 12px; border-radius: 12px;
box-shadow: none; box-shadow: none;
border: 1px solid #dcfce7; border: 1px solid #dcfce7;
border-left: 4px solid #22c55e;
} }
.storage-info h3 { .storage-info h3 {
@@ -229,7 +226,6 @@ pre code {
border-radius: 12px; border-radius: 12px;
box-shadow: none; box-shadow: none;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-left: 4px solid #8b5cf6;
} }
/* Remove redundant changelog wrapper styles */ /* Remove redundant changelog wrapper styles */
@@ -330,7 +326,6 @@ pre code {
padding: 14px 18px; padding: 14px 18px;
border-radius: 10px; border-radius: 10px;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-left: 4px solid #3b82f6;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
@@ -364,7 +359,6 @@ pre code {
.donation-item { .donation-item {
background: #ffffff; background: #ffffff;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-left: 4px solid #22c55e;
border-radius: 10px; border-radius: 10px;
padding: 12px; padding: 12px;
margin: 10px 0; margin: 10px 0;
@@ -733,7 +727,8 @@ pre code {
.tab-nav { .tab-nav {
display: flex; display: flex;
gap: 10px; flex-wrap: wrap;
gap: 8px;
background: #ffffff; background: #ffffff;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 12px; border-radius: 12px;
@@ -744,6 +739,8 @@ pre code {
z-index: 5; z-index: 5;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
box-shadow: 0 2px 8px rgba(2, 6, 23, 0.04); box-shadow: 0 2px 8px rgba(2, 6, 23, 0.04);
overflow-x: hidden;
box-sizing: border-box;
} }
.tab-btn { .tab-btn {
@@ -751,12 +748,18 @@ pre code {
background: #f1f5f9; background: #f1f5f9;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
color: #0f172a; color: #0f172a;
padding: 10px 16px; padding: 8px 12px;
border-radius: 10px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 13px;
transition: all 0.15s ease; 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 { .tab-btn:hover {
@@ -781,9 +784,39 @@ pre code {
.tab-panel[hidden] { display: none; } .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) { @media (max-width: 640px) {
.tab-layout { padding: 14px; } .tab-layout { padding: 12px; }
.tab-nav { position: static; }
.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 */ /* Upload UI - aligned with app styles */

View File

@@ -1,6 +1,15 @@
{{define "changelog"}} {{define "changelog"}}
<h2>Changelog</h2> <h2>Changelog</h2>
<div class="changelog-entry">
<div class="version">v1.3.0</div>
<div class="date">2025-08-12</div>
<ul>
<li>Allow to select multiple files in web uploader</li>
</ul>
</div>
<div class="changelog-entry"> <div class="changelog-entry">
<div class="version">v1.2.0</div> <div class="version">v1.2.0</div>
<div class="date">2025-08-09</div> <div class="date">2025-08-09</div>

View File

@@ -1,11 +1,11 @@
{{define "web-upload"}} {{define "web-upload"}}
<h2>📤 Web Upload</h2> <h2>Web upload</h2>
<p>Upload a file directly from your browser. Files are temporary and automatically deleted after 24 hours.</p> <p>Upload files directly from your browser. Files are temporary and automatically deleted after 24 hours.</p>
<div class="upload-card"> <div class="upload-card">
<form id="web-upload-form" class="upload-form" aria-label="Web upload form"> <form id="web-upload-form" class="upload-form" aria-label="Web upload form">
<div class="form-row"> <div class="form-row">
<input type="file" id="file-input" name="file" required aria-label="Choose a file to upload" data-rybbit-event="Select file"> <input type="file" id="file-input" name="file" multiple required aria-label="Choose files to upload" data-rybbit-event="Select files">
</div> </div>
<div class="form-row"> <div class="form-row">
<button type="submit" id="upload-btn" class="btn-primary" data-rybbit-event="Upload">Upload</button> <button type="submit" id="upload-btn" class="btn-primary" data-rybbit-event="Upload">Upload</button>
@@ -15,14 +15,7 @@
<div id="upload-status" class="upload-status" role="status" aria-live="polite" hidden></div> <div id="upload-status" class="upload-status" role="status" aria-live="polite" hidden></div>
<div id="upload-result" class="upload-result" hidden> <div id="upload-result" class="upload-result" hidden>
<p><strong>File uploaded successfully!</strong></p> <div id="upload-results-list"></div>
<p>
<span>Download URL:</span>
<a id="download-link" href="#" target="_blank" rel="noopener noreferrer">open</a>
</p>
<div class="actions">
<button id="copy-link-btn" class="btn-secondary" type="button">Copy link</button>
</div>
</div> </div>
</div> </div>
@@ -32,8 +25,6 @@
const fileInput = document.getElementById('file-input'); const fileInput = document.getElementById('file-input');
const statusEl = document.getElementById('upload-status'); const statusEl = document.getElementById('upload-status');
const resultEl = document.getElementById('upload-result'); 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'); const uploadBtn = document.getElementById('upload-btn');
function showStatus(msg, type) { function showStatus(msg, type) {
@@ -55,19 +46,31 @@
clearStatus(); clearStatus();
resultEl.hidden = true; resultEl.hidden = true;
const file = fileInput.files && fileInput.files[0]; const files = fileInput.files;
if(!file){ if(!files || files.length === 0){
showStatus('Please choose a file first.', 'error'); showStatus('Please choose at least one file.', 'error');
return; return;
} }
setLoading(true);
const resultsList = document.getElementById('upload-results-list');
resultsList.innerHTML = '';
let successCount = 0;
let totalFiles = files.length;
try {
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(); const data = new FormData();
data.append('file', file); data.append('file', file);
setLoading(true);
try { try {
const resp = await fetch('/upload', { method: 'POST', body: data }); const resp = await fetch('/upload', { method: 'POST', body: data });
const contentType = resp.headers.get('content-type') || ''; const contentType = resp.headers.get('content-type') || '';
if(!resp.ok){ if(!resp.ok){
let message = 'Upload failed'; let message = 'Upload failed';
if (contentType.includes('application/json')) { if (contentType.includes('application/json')) {
@@ -76,41 +79,84 @@
} else { } else {
message = await resp.text(); message = await resp.text();
} }
showStatus(message || 'Upload failed', 'error'); addFileResult(file.name, null, message, false);
return; continue;
} }
const json = contentType.includes('application/json') ? await resp.json() : null; const json = contentType.includes('application/json') ? await resp.json() : null;
const url = json && json.url ? json.url : null; const url = json && json.url ? json.url : null;
if(!url){ if(!url){
showStatus('Upload succeeded but no URL was returned by the server.', 'error'); addFileResult(file.name, null, 'Upload succeeded but no URL was returned', false);
return; continue;
} }
linkEl.href = url; addFileResult(file.name, url, 'Success', true);
linkEl.textContent = url; successCount++;
resultEl.hidden = false;
showStatus('Done.', 'success');
} catch (err) { } catch (err) {
showStatus('Network error. Please try again.', 'error'); addFileResult(file.name, null, 'Network error', false);
}
}
resultEl.hidden = false;
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 { } finally {
setLoading(false); setLoading(false);
} }
}); });
copyBtn.addEventListener('click', function(){ function addFileResult(filename, url, message, success) {
const url = linkEl.href; const resultsList = document.getElementById('upload-results-list');
const resultDiv = document.createElement('div');
resultDiv.className = 'file-result ' + (success ? 'success' : 'error');
if (success && url) {
resultDiv.innerHTML = `
<p><strong>${filename}</strong> - ${message}</p>
<p>
<span>Download URL:</span>
<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>
</p>
<div class="actions">
<button class="btn-secondary copy-link-btn" type="button" data-url="${url}">Copy link</button>
</div>
`;
} else {
resultDiv.innerHTML = `
<p><strong>${filename}</strong> - ${message}</p>
`;
}
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; if (!url) return;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
const original = copyBtn.textContent; const original = e.target.textContent;
copyBtn.textContent = 'Copied!'; e.target.textContent = 'Copied!';
copyBtn.classList.add('copied'); e.target.classList.add('copied');
setTimeout(()=>{ copyBtn.textContent = original; copyBtn.classList.remove('copied'); }, 1500); setTimeout(() => {
e.target.textContent = original;
e.target.classList.remove('copied');
}, 1500);
}).catch(() => { }).catch(() => {
const original = copyBtn.textContent; const original = e.target.textContent;
copyBtn.textContent = 'Failed'; e.target.textContent = 'Failed';
setTimeout(()=>{ copyBtn.textContent = original; }, 1500); setTimeout(() => {
e.target.textContent = original;
}, 1500);
}); });
}
}); });
})(); })();
</script> </script>