瀏覽代碼

Refine file explorer upload panel

Improve the file explorer's upload UI with a compact minimized card that stays above the status bar, adds restore/close controls, and preserves panel state until the queue is cleared. The summary now shows a dedicated overall percentage and uses byte-weighted progress when task sizes are known so the ring matches the transferred totals. Also increases the upload chunk size from 512KB to 1MB.
Toby Chui 4 天之前
父節點
當前提交
d45d453df2

+ 65 - 3
src/web/SystemAO/file_system/file_explorer.css

@@ -1556,10 +1556,15 @@ body.fmResizing *{
     The floating upload / zip task card at the bottom right. Markup shell lives
     in file_explorer.html, rows are drawn by js/explorer/uploadui.js.
 */
+/*
+    Floats above the status bar rather than over it, so the tray buttons (the
+    upload list toggle included) stay clickable. --fm-statusbar-h is measured
+    by applyUploadPanelVisibility(); the fallback is the bar's usual height.
+*/
 #uploadTab{
     position: fixed;
-    right: 16px;
-    bottom: 16px;
+    right: 8px;
+    bottom: calc(var(--fm-statusbar-h, 39px) + 10px);
     width: 360px;
     max-width: calc(100% - 32px);
     background-color: var(--upload_bg);
@@ -1714,6 +1719,14 @@ body.fmResizing *{
     text-align: center;
 }
 
+/* The summary's overall percentage: its own right aligned cell, like the numbers */
+#uploadTab .uploadPct{
+    display: inline-block;
+    min-width: 4ch;
+    margin-left: 8px;
+    text-align: right;
+}
+
 /*
     Always the right hand end of the status line, whatever is to its left: a
     byte counter, the download link of a finished download, or nothing at all.
@@ -1971,15 +1984,64 @@ body.fmResizing *{
     background-color: var(--fs-accent-soft);
 }
 
+/*
+    Minimized card (#uploadTab.compact, see toggleUploadMinimize)
+
+    The same panel shrunk in place to its summary row: ring, running total and
+    the maximize / close buttons, which only exist on this view because the
+    header that normally carries the controls is hidden.
+*/
+#uploadTab .uploadCompactBtn{
+    display: none;
+}
+
+/*
+    Shrink to fit rather than a fixed width: the total's label differs in
+    length between locales, and it must never be cut off mid number.
+*/
+#uploadTab.compact{
+    width: auto;
+    min-width: 240px;
+}
+#uploadTab.compact .uploadHeader,
+#uploadTab.compact .uploadList,
+#uploadTab.compact .uploadActions{
+    display: none;
+}
+#uploadTab.compact .uploadSummary{
+    gap: 8px;
+    padding: 8px 8px 8px 14px;
+    margin-top: 0px;
+    border-top: none;
+    background-color: var(--uploadtab_bg);
+}
+#uploadTab.compact .uploadSummaryIcon svg{
+    width: 22px;
+    height: 22px;
+}
+/* Not allowed to shrink: the card grows with the text instead of clipping it */
+#uploadTab.compact .uploadSummaryText{
+    flex: 0 0 auto;
+    overflow: visible;
+    font-size: 13px;
+    color: var(--uploadtab_txt);
+}
+#uploadTab.compact .uploadCompactBtn{
+    display: flex;
+}
+
 
 @media screen and (max-width: 500px) {
     #uploadTab{
         right: 12px;
         left: 12px;
-        bottom: 12px;
+        bottom: calc(var(--fm-statusbar-h, 37px) + 8px);
         width: auto;
         max-width: none;
     }
+    #uploadTab.compact{
+        min-width: 0px;
+    }
     .uploadList{
         max-height: 40vh;
     }

+ 7 - 0
src/web/SystemAO/file_system/file_explorer.html

@@ -554,6 +554,13 @@
             <div class="uploadSummary">
                 <span class="uploadSummaryIcon" id="uploadSummaryIcon" data-fsicon="taskProgress"></span>
                 <span class="uploadSummaryText" id="uploadSummaryText"></span>
+                <!-- Only shown on the minimized card, where the header is hidden -->
+                <div class="uploadHeaderBtn uploadCompactBtn" onclick="restoreUploadPanel();" title="Maximize">
+                    <span data-fsicon="maximize"></span>
+                </div>
+                <div class="uploadHeaderBtn uploadCompactBtn" onclick="closeUploadTab();" title="Close">
+                    <span data-fsicon="close"></span>
+                </div>
             </div>
             <div class="uploadActions">
                 <div class="uploadActionBtn" onclick="clearCompletedUploads();">

+ 3 - 1
src/web/SystemAO/file_system/js/explorer/state.js

@@ -95,7 +95,7 @@ let uploadPendingList = []; //Upload pending queue for mass upoad
 let uploadRetryMap = new Map(); //taskUUID -> {file, targetDir} for WebSocket upload retry
 let lowMemoryMode = true;   //Upload with low memory mode channel
 let largeFileCutoffSize = 8192 * 1024 * 1024; //Any file larger than this size is consider "large file", default to 8GB
-let uploadFileChunkSize = 1024 * 512; //512KB, 4MB not working quite well on slow network
+let uploadFileChunkSize = 1024 * 1024 * 1; //1MB, 4MB not working quite well on slow network
 let postUploadModeCutoff = 25 * 1048576; //25MB, files smaller than this will upload using POST Mode
 const CHUNK_TIMEOUT_MS = 30000; //30s timeout waiting for server "next" ack before retrying a chunk
 
@@ -127,6 +127,8 @@ let uploadTaskInfo = new Map();
 let uploadTransferMap = new Map();
 //Panel collapsed to the #fmUploadListBtn in the status bar; a new task expands it
 let uploadPanelCollapsed = false;
+//Panel minimized to the compact card (ring + total only) in the same corner
+let uploadPanelCompact = false;
 const MAX_FINISHED_UPLOAD_ROWS = 60; //Trim completed rows past this to keep mass uploads responsive
 
 /*

+ 1 - 1
src/web/SystemAO/file_system/js/explorer/upload.js

@@ -48,7 +48,7 @@ function initUploadMode(){
 
 function uploadFile(file, uuid=undefined, targetDir=undefined) {
     if (file.size > postUploadModeCutoff && lowMemoryMode){
-            /*
+        /*
             Low Memory Upload Mode
         */
         var filename = encodeURIComponent(file.name);

+ 64 - 9
src/web/SystemAO/file_system/js/explorer/uploadui.js

@@ -467,20 +467,33 @@ function updateUploadSummary(){
         taskCount++;
     });
 
-    //Every task counts the same towards the ring. Weighting them by size would
-    //need a size for each, which is exactly what a zip task cannot give.
-    let overall = taskCount > 0 ? percentSum / taskCount : 0;
+    /*
+        With every size known the overall figure is bytes moved over bytes in
+        total, so the ring and the percentage agree with the byte counter next
+        to them. A zip task has no size to weigh, so once one is in the list
+        every task counts the same instead.
+    */
+    let bytesKnown = total > 0 && !hasUnknown;
+    let overall = 0;
+    if (bytesKnown){
+        overall = Math.min(100, loaded / total * 100);
+    }else if (taskCount > 0){
+        overall = percentSum / taskCount;
+    }
     setUploadSummaryRing(overall);
 
+    //Floored, so 100% only ever shows once everything is actually through
+    let pctHTML = '<span class="uploadPct">' + Math.floor(overall) + '%</span>';
+
     //The %s slots get the same boxed number markup as the rows. The literal
     //text around them comes from the locale file and never changes width.
     let html = "";
-    if (total > 0 && !hasUnknown){
+    if (bytesKnown){
         html = applocale.getString("upload/total", "Total: %s / %s")
                 .replace("%s", formatUploadBytesHTML(loaded))
-                .replace("%s", formatUploadBytesHTML(total));
+                .replace("%s", formatUploadBytesHTML(total)) + pctHTML;
     }else if (loaded > 0){
-        html = formatUploadBytesHTML(loaded);
+        html = formatUploadBytesHTML(loaded) + pctHTML;
     }else if (taskCount > 0){
         //Nothing to count in bytes - a zip task only ever reports a percentage
         html = Math.round(overall) + "%";
@@ -559,14 +572,22 @@ function applyUploadPanelVisibility(){
     });
 
     if (taskCount == 0){
-        //Nothing left to show: the panel and its button both go away
+        //Nothing left to show: the panel and its button both go away, and the
+        //next batch opens as the full list again
         $("#uploadTab").hide();
         $("#fmUploadListBtn").hide();
         uploadPanelCollapsed = false;
+        uploadPanelCompact = false;
+        $("#uploadTab").removeClass("compact");
         return;
     }
 
     let expanded = !uploadPanelCollapsed;
+    //The panel floats just above the status bar, whose height varies with the
+    //layout (the narrow screen bar is shorter), so it is measured, not assumed
+    let statusBarHeight = $("#fmStatusBar").is(":visible") ? $("#fmStatusBar").outerHeight() : 0;
+    document.getElementById("uploadTab").style.setProperty("--fm-statusbar-h", statusBarHeight + "px");
+    $("#uploadTab").toggleClass("compact", uploadPanelCompact);
     $("#uploadTab").toggle(expanded);
     $("#fmUploadListBtn").css("display", "");
     $("#fmUploadListBtn").toggleClass("active", expanded);
@@ -777,12 +798,28 @@ function collapseUploadPanelForDialog(){
     applyUploadPanelVisibility();
 }
 
-//Collapse the panel down to #fmUploadListBtn. The tasks keep running.
+/*
+    Minimize: shrink the panel in place to a compact card that keeps only the
+    progress ring and the running total, with maximize and close beside them.
+    The tasks keep running, and the choice sticks until the list empties, so
+    reopening from the status bar or a new upload brings back the same view.
+*/
 function toggleUploadMinimize(){
-    uploadPanelCollapsed = true;
+    uploadPanelCompact = true;
+    uploadPanelCollapsed = false;
     applyUploadPanelVisibility();
 }
 
+//Maximize: back from the compact card to the full task list
+function restoreUploadPanel(){
+    uploadPanelCompact = false;
+    uploadPanelCollapsed = false;
+    applyUploadPanelVisibility();
+    //The list was hidden, so the in-flight row may be out of view
+    uploadListUserScrolledAt = 0;
+    scrollActiveUploadIntoView();
+}
+
 //The status bar button: expands the parked panel, or collapses it again
 function toggleUploadPanel(){
     uploadPanelCollapsed = !uploadPanelCollapsed;
@@ -794,7 +831,24 @@ function toggleUploadPanel(){
     }
 }
 
+/*
+    Close. With every task completed there is nothing left to come back to, so
+    the list is cleared and the panel goes away with its status bar button.
+    Otherwise it hides to the status bar, where the transfers keep running. A
+    failed task counts as unfinished: clearing it would throw away its Retry.
+*/
 function closeUploadTab(){
+    let allCompleted = uploadTaskInfo.size > 0;
+    uploadTaskInfo.forEach(function(info){
+        if (info.state != "done"){
+            allCompleted = false;
+        }
+    });
+
+    if (allCompleted){
+        clearCompletedUploads();
+        return;
+    }
     uploadPanelCollapsed = true;
     applyUploadPanelVisibility();
 }
@@ -816,6 +870,7 @@ function removeThisTask(object, taskUUID){
 window.removeThisTask = removeThisTask;
 window.retryUploadFile = retryUploadFile;
 window.toggleUploadMinimize = toggleUploadMinimize;
+window.restoreUploadPanel = restoreUploadPanel;   // the compact card's maximize button
 window.toggleUploadPanel = toggleUploadPanel;   // the status bar button
 window.collapseUploadPanelForDialog = collapseUploadPanelForDialog;
 window.closeUploadTab = closeUploadTab;

+ 1 - 0
src/web/SystemAO/file_system/shared/fsicons.js

@@ -78,6 +78,7 @@
 
         /* Transfer panel */
         minus:       S + '<path d="M5 12h14"/></svg>',
+        maximize:    S + '<rect x="5" y="5" width="14" height="14" rx="2"/></svg>',
         chevronDown: S + '<path d="M6 9.5l6 6 6-6"/></svg>',
         chevronUp:   S + '<path d="M6 14.5l6-6 6 6"/></svg>',
         cloudUpload: S + '<path d="M6.5 19a4.5 4.5 0 0 1-.6-8.96A6 6 0 0 1 17.7 9.2 3.9 3.9 0 0 1 17.5 19z"/><path d="M12 18v-7M9.2 13.3L12 10.5l2.8 2.8"/></svg>',