feat(ui): add overall upload progress and improve file icons

- Track per-file loaded bytes and compute an overall upload percentage
- Add overall progress bar/percent styling and resize upload window to fit
- Hide the upload result section until a share URL is available
- Use a specific icon for .exe files and update the default fallback iconfeat(ui): add overall upload progress and improve file icons

- Track per-file loaded bytes and compute an overall upload percentage
- Add overall progress bar/percent styling and resize upload window to fit
- Hide the upload result section until a share URL is available
- Use a specific icon for .exe files and update the default fallback icon
This commit is contained in:
2026-04-27 17:26:57 +03:00
parent 6a0b3dbe2f
commit b69ec8b99f
4 changed files with 102 additions and 4 deletions

View File

@@ -344,6 +344,8 @@ func iconForMimeType(mimeType string, filename string) string {
extension := strings.ToLower(filepath.Ext(filename))
switch {
case extension == ".exe":
return "/static/img/icons/Program Files Icons - PNG/MSONSEXT.DLL_14_6-0.png"
case strings.HasPrefix(mimeType, "image/"):
return "/static/img/sprites/bitmap.png"
case strings.HasPrefix(mimeType, "video/"):
@@ -361,7 +363,7 @@ func iconForMimeType(mimeType string, filename string) string {
case extension == ".html" || extension == ".css" || extension == ".js":
return "/static/img/sprites/frame_web-0.png"
default:
return "/static/img/sprites/freepad.png"
return "/static/img/icons/Windows Icons - PNG/ole2.dll_14_DEFICON.png"
}
}

View File

@@ -1,6 +1,6 @@
.upload-window {
width: 520px;
height: 456px;
height: 486px;
}
.upload-form {
@@ -145,6 +145,10 @@
line-height: 12px;
}
.upload-result.is-hidden {
visibility: hidden;
}
.upload-result-label {
font-weight: bold;
}
@@ -274,6 +278,48 @@
padding: 0 8px 8px;
}
.upload-overall {
display: grid;
grid-template-columns: minmax(0, 1fr) 42px;
align-items: center;
gap: 6px;
height: 28px;
box-sizing: border-box;
padding: 0 8px 8px;
font-size: 12px;
line-height: 12px;
}
.upload-overall-track {
height: 18px;
box-sizing: border-box;
overflow: hidden;
background: #ffffff;
border-top: 2px solid #808080;
border-left: 2px solid #808080;
border-right: 2px solid #ffffff;
border-bottom: 2px solid #ffffff;
}
.upload-overall-bar {
display: block;
width: 0%;
height: 100%;
background-color: #000078;
background-image: repeating-linear-gradient(
to right,
#000078 0,
#000078 10px,
#c0c0c0 10px,
#c0c0c0 12px
);
}
.upload-overall-percent {
min-width: 0;
text-align: right;
}
.upload-statusbar {
grid-template-columns: 1fr 96px;
}

View File

@@ -5,8 +5,11 @@ const dropzone = document.querySelector(".upload-dropzone");
const uploadForm = document.querySelector(".upload-form");
const uploadStatus = document.querySelector(".upload-statusbar span:first-child");
const boxStatus = document.querySelector(".upload-statusbar span:last-child");
const uploadResult = document.querySelector(".upload-result");
const boxLink = document.querySelector("#upload-box-link");
const shareButton = document.querySelector("#upload-share-button");
const overallProgressBar = document.querySelector(".upload-overall-bar");
const overallProgressPercent = document.querySelector(".upload-overall-percent");
let selectedFiles = [];
let statusTimer = null;
@@ -62,6 +65,10 @@ function setBoxStatus(message) {
function setBoxLink(path) {
shareURL = path ? new URL(path, window.location.origin).toString() : "";
if (uploadResult) {
uploadResult.classList.toggle("is-hidden", !shareURL);
}
if (boxLink) {
boxLink.href = shareURL || "#";
boxLink.textContent = shareURL || "Waiting for upload";
@@ -75,6 +82,25 @@ function setBoxLink(path) {
}
}
function setOverallProgress(percent) {
const clampedPercent = Math.max(0, Math.min(100, percent));
const displayPercent = `${Math.round(clampedPercent)}%`;
if (overallProgressBar) {
overallProgressBar.style.width = displayPercent;
}
if (overallProgressPercent) {
overallProgressPercent.textContent = displayPercent;
}
}
function updateOverallProgress() {
const totalBytes = selectedFiles.reduce((total, selectedFile) => total + selectedFile.file.size, 0);
const loadedBytes = selectedFiles.reduce((total, selectedFile) => total + selectedFile.loaded, 0);
setOverallProgress(totalBytes > 0 ? (loadedBytes / totalBytes) * 100 : 0);
}
function updateFileCount() {
if (fileCount) {
fileCount.textContent = `${selectedFiles.length} ${selectedFiles.length === 1 ? "file" : "files"}`;
@@ -121,6 +147,7 @@ function createFileRow(selectedFile) {
function updateSelectedFiles(files) {
selectedFiles = Array.from(files || []).map((file) => ({
file,
loaded: 0,
row: null,
uploaded: false,
failed: false,
@@ -142,6 +169,7 @@ function updateSelectedFiles(files) {
updateStatus("Ready");
setBoxStatus("WarpBox");
setBoxLink("");
setOverallProgress(0);
return;
}
@@ -154,6 +182,7 @@ function updateSelectedFiles(files) {
updateStatus("Files selected");
setBoxStatus("WarpBox");
setBoxLink("");
setOverallProgress(0);
}
async function createBox() {
@@ -174,6 +203,8 @@ function uploadFile(boxID, selectedFile, onComplete) {
xhr.open("POST", `/box/${boxID}/upload`);
xhr.upload.addEventListener("loadstart", () => {
selectedFile.loaded = 0;
updateOverallProgress();
setRowProgress(selectedFile.row, 2);
});
@@ -182,6 +213,8 @@ function uploadFile(boxID, selectedFile, onComplete) {
return;
}
selectedFile.loaded = Math.min(event.loaded, selectedFile.file.size);
updateOverallProgress();
setRowProgress(selectedFile.row, (event.loaded / event.total) * 100);
});
@@ -194,7 +227,9 @@ function uploadFile(boxID, selectedFile, onComplete) {
}
selectedFile.uploaded = true;
selectedFile.loaded = selectedFile.file.size;
selectedFile.row.classList.add("is-uploaded");
updateOverallProgress();
setRowProgress(selectedFile.row, 100);
onComplete();
resolve();
@@ -257,17 +292,19 @@ if (uploadForm) {
selectedFiles.forEach((selectedFile) => {
selectedFile.uploaded = false;
selectedFile.failed = false;
selectedFile.loaded = 0;
selectedFile.row.classList.remove("is-uploaded", "is-failed");
setRowProgress(selectedFile.row, 0);
});
setBoxLink("");
setOverallProgress(0);
updateStatus(`${statusPrefix()} Uploading.`);
animateUploadStatus(statusPrefix);
try {
const box = await createBox();
setBoxStatus(box.box_url);
setBoxLink(box.box_url);
await Promise.allSettled(selectedFiles.map((selectedFile) => {
return uploadFile(box.box_id, selectedFile, () => {
@@ -278,13 +315,19 @@ if (uploadForm) {
stopStatusAnimation();
const failedCount = selectedFiles.filter((selectedFile) => selectedFile.failed).length;
if (failedCount > 0) {
if (completedCount > 0) {
setBoxLink(box.box_url);
}
updateStatus(`${completedCount}/${totalCount} Uploaded, ${failedCount} failed`);
return;
}
setBoxLink(box.box_url);
setOverallProgress(100);
updateStatus(`${completedCount}/${totalCount} Uploaded`);
} catch (error) {
stopStatusAnimation();
setBoxLink("");
updateStatus("Upload failed");
}
});

View File

@@ -48,7 +48,7 @@
<p class="upload-empty-state">No files selected</p>
</div>
<div class="upload-result" aria-live="polite">
<div class="upload-result is-hidden" aria-live="polite">
<span class="upload-result-label">Folder link</span>
<a id="upload-box-link" class="upload-result-link is-empty" href="#" aria-disabled="true">Waiting for upload</a>
<button id="upload-share-button" class="win98-button upload-share-button" type="button" disabled>Share</button>
@@ -60,6 +60,13 @@
<button class="win98-button" type="submit">Upload</button>
</footer>
<div class="upload-overall" aria-live="polite">
<div class="upload-overall-track" aria-hidden="true">
<span class="upload-overall-bar"></span>
</div>
<span class="upload-overall-percent">0%</span>
</div>
<div class="win98-statusbar upload-statusbar" aria-live="polite">
<span>Ready</span>
<span>WarpBox</span>