Add a chunk-based progress tracking
authorAlexander Ebert <ebert@woltlab.com>
Fri, 21 Jun 2024 12:46:27 +0000 (14:46 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 21 Jun 2024 12:46:27 +0000 (14:46 +0200)
There will be no progress bar if there is only a single chunk to be uploaded.

ts/WoltLabSuite/Core/Component/Attachment/Entry.ts
ts/WoltLabSuite/Core/Component/File/Upload.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/Entry.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js
wcfsetup/install/files/style/ui/attachment.scss

index fc0ae3da68a5ddf5742d711ed5d720676db610ad..408b113141b21b73e7a704ed4fbc5295246a2a83 100644 (file)
@@ -188,6 +188,38 @@ function markElementAsErroneous(element: HTMLElement, errorMessage: string): voi
   element.append(errorElement);
 }
 
+function trackUploadProgress(element: HTMLElement, file: WoltlabCoreFileElement): void {
+  const progress = document.createElement("progress");
+  progress.classList.add("attachment__item__progress__bar");
+  progress.max = 100;
+  const readout = document.createElement("span");
+  readout.classList.add("attachment__item__progress__readout");
+
+  file.addEventListener("uploadProgress", (event: CustomEvent<number>) => {
+    progress.value = event.detail;
+    readout.textContent = `${event.detail}%`;
+
+    if (progress.parentNode === null) {
+      element.classList.add("attachment__item--uploading");
+
+      const wrapper = document.createElement("div");
+      wrapper.classList.add("attachment__item__progress");
+      wrapper.append(progress, readout);
+
+      element.append(wrapper);
+    }
+  });
+}
+
+function removeUploadProgress(element: HTMLElement): void {
+  if (!element.classList.contains("attachment__item--uploading")) {
+    return;
+  }
+
+  element.classList.remove("attachment__item--uploading");
+  element.querySelector(".attachment__item__progress")?.remove();
+}
+
 export function createAttachmentFromFile(file: WoltlabCoreFileElement, editor: HTMLElement) {
   const element = document.createElement("li");
   element.classList.add("attachment__item");
@@ -212,7 +244,12 @@ export function createAttachmentFromFile(file: WoltlabCoreFileElement, editor: H
     })
     .catch((reason) => {
       fileInitializationFailed(element, file, reason);
+    })
+    .finally(() => {
+      removeUploadProgress(element);
     });
 
+  trackUploadProgress(element, file);
+
   return element;
 }
index 0f88d2337619569def20140cb45151148007baff..babee01432a589aeab3c6d981950ed2b5c19fbe0 100644 (file)
@@ -65,7 +65,7 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
 
   const chunkSize = Math.ceil(file.size / numberOfChunks);
 
-  // TODO: Can we somehow report any meaningful upload progress?
+  notifyChunkProgress(fileElement, 0, numberOfChunks);
 
   for (let i = 0; i < numberOfChunks; i++) {
     const start = i * chunkSize;
@@ -81,6 +81,8 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
       throw response.error;
     }
 
+    notifyChunkProgress(fileElement, i + 1, numberOfChunks);
+
     await chunkUploadCompleted(fileElement, response.value);
 
     if (response.value.completed) {
@@ -89,6 +91,19 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
   }
 }
 
+function notifyChunkProgress(element: WoltlabCoreFileElement, currentChunk: number, numberOfChunks: number): void {
+  // Suppress the progress bar for uploads that are processed in a single
+  // request, because we cannot track the upload progress within a chunk.
+  if (numberOfChunks === 1) {
+    return;
+  }
+
+  const event = new CustomEvent<number>("uploadProgress", {
+    detail: Math.floor((currentChunk / numberOfChunks) * 100),
+  });
+  element.dispatchEvent(event);
+}
+
 async function chunkUploadCompleted(fileElement: WoltlabCoreFileElement, result: UploadChunkResponse): Promise<void> {
   if (!result.completed) {
     return;
index 67e741e7653965a2fe72d7366e8adf2da7bff735..a158f397958da4d2dcf64110587c227a66f6e7d7 100644 (file)
@@ -141,6 +141,31 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/FileUtil", "WoltLabSui
         errorElement.textContent = errorMessage;
         element.append(errorElement);
     }
+    function trackUploadProgress(element, file) {
+        const progress = document.createElement("progress");
+        progress.classList.add("attachment__item__progress__bar");
+        progress.max = 100;
+        const readout = document.createElement("span");
+        readout.classList.add("attachment__item__progress__readout");
+        file.addEventListener("uploadProgress", (event) => {
+            progress.value = event.detail;
+            readout.textContent = `${event.detail}%`;
+            if (progress.parentNode === null) {
+                element.classList.add("attachment__item--uploading");
+                const wrapper = document.createElement("div");
+                wrapper.classList.add("attachment__item__progress");
+                wrapper.append(progress, readout);
+                element.append(wrapper);
+            }
+        });
+    }
+    function removeUploadProgress(element) {
+        if (!element.classList.contains("attachment__item--uploading")) {
+            return;
+        }
+        element.classList.remove("attachment__item--uploading");
+        element.querySelector(".attachment__item__progress")?.remove();
+    }
     function createAttachmentFromFile(file, editor) {
         const element = document.createElement("li");
         element.classList.add("attachment__item");
@@ -160,7 +185,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/FileUtil", "WoltLabSui
         })
             .catch((reason) => {
             fileInitializationFailed(element, file, reason);
+        })
+            .finally(() => {
+            removeUploadProgress(element);
         });
+        trackUploadProgress(element, file);
         return element;
     }
     exports.createAttachmentFromFile = createAttachmentFromFile;
index 06612c3e339b5180dce289268c60cd687db6da96..c05f517015f957000c78a690d1759887d944ff7f 100644 (file)
@@ -23,7 +23,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
         }
         const { identifier, numberOfChunks } = response.value;
         const chunkSize = Math.ceil(file.size / numberOfChunks);
-        // TODO: Can we somehow report any meaningful upload progress?
+        notifyChunkProgress(fileElement, 0, numberOfChunks);
         for (let i = 0; i < numberOfChunks; i++) {
             const start = i * chunkSize;
             const end = start + chunkSize;
@@ -34,12 +34,24 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
                 fileElement.uploadFailed(response.error);
                 throw response.error;
             }
+            notifyChunkProgress(fileElement, i + 1, numberOfChunks);
             await chunkUploadCompleted(fileElement, response.value);
             if (response.value.completed) {
                 return response.value;
             }
         }
     }
+    function notifyChunkProgress(element, currentChunk, numberOfChunks) {
+        // Suppress the progress bar for uploads that are processed in a single
+        // request, because we cannot track the upload progress within a chunk.
+        if (numberOfChunks === 1) {
+            return;
+        }
+        const event = new CustomEvent("uploadProgress", {
+            detail: Math.floor((currentChunk / numberOfChunks) * 100),
+        });
+        element.dispatchEvent(event);
+    }
     async function chunkUploadCompleted(fileElement, result) {
         if (!result.completed) {
             return;
index 0af744b1902861a3d80473bfa9af2ee1749df21f..fdce55b367f6dbcbed3a13195cf349c401f2cd3a 100644 (file)
@@ -200,6 +200,17 @@ html[data-color-scheme="dark"] {
        justify-content: end;
 }
 
+.attachment__item__progress {
+       align-items: center;
+       column-gap: 10px;
+       display: flex;
+       grid-area: buttons;
+}
+
+.attachment__item__progress__readout {
+       @include wcfFontSmall;
+}
+
 .formAttachmentContent {
        .formAttachmentList {
                display: flex;