feat: download - fase 03
This commit is contained in:
Binary file not shown.
@@ -68,6 +68,13 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('id="feedback-modal"', body)
|
||||
self.assertIn('id="feedback-message"', body)
|
||||
self.assertIn('id="feedback-close-btn"', body)
|
||||
self.assertIn('id="download-modal"', body)
|
||||
self.assertIn('id="download-modal-target"', body)
|
||||
self.assertIn('id="download-modal-current-file"', body)
|
||||
self.assertIn('id="download-modal-progress-bar"', body)
|
||||
self.assertIn('id="download-modal-count"', body)
|
||||
self.assertIn('id="download-modal-status"', body)
|
||||
self.assertIn('id="download-modal-close-btn"', body)
|
||||
self.assertIn('id="context-menu"', body)
|
||||
self.assertIn('id="context-menu-scope"', body)
|
||||
self.assertIn('id="context-menu-target"', body)
|
||||
@@ -211,11 +218,26 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('function feedbackElements()', app_js)
|
||||
self.assertIn('function openFeedbackModal(message)', app_js)
|
||||
self.assertIn('function closeFeedbackModal()', app_js)
|
||||
self.assertIn('function downloadModalElements()', app_js)
|
||||
self.assertIn('function isZipDownloadSelection(items)', app_js)
|
||||
self.assertIn('function openZipDownloadModal(selectedItems)', app_js)
|
||||
self.assertIn('function markZipDownloadReady(fileName)', app_js)
|
||||
self.assertIn('function markZipDownloadFailed(err)', app_js)
|
||||
self.assertIn('function closeDownloadModal()', app_js)
|
||||
self.assertIn('function contextMenuElements()', app_js)
|
||||
self.assertIn('function openContextMenu(pane, entry, event)', app_js)
|
||||
self.assertIn('function closeContextMenu()', app_js)
|
||||
self.assertIn('function isOpenableSelection(item)', app_js)
|
||||
self.assertIn('async function downloadFileRequest(paths)', app_js)
|
||||
self.assertIn('const zipDownload = isZipDownloadSelection(selectedItems);', app_js)
|
||||
self.assertIn('openZipDownloadModal(selectedItems);', app_js)
|
||||
self.assertIn('statusText: "preparing"', app_js)
|
||||
self.assertIn('statusText: "packaging items"', app_js)
|
||||
self.assertIn('statusText: "ready"', app_js)
|
||||
self.assertIn('statusText: `failed: ${err.message}`', app_js)
|
||||
self.assertIn('countText: "Step 1/3"', app_js)
|
||||
self.assertIn('countText: "Step 2/3"', app_js)
|
||||
self.assertIn('countText: "Step 3/3"', app_js)
|
||||
self.assertIn('function applyContextMenuSelection()', app_js)
|
||||
self.assertIn('function startContextMenuOpen()', app_js)
|
||||
self.assertIn('function startContextMenuEdit()', app_js)
|
||||
|
||||
+153
-1
@@ -81,6 +81,11 @@ let uploadState = {
|
||||
conflictResolver: null,
|
||||
cancelRequested: false,
|
||||
};
|
||||
let downloadProgressState = {
|
||||
active: false,
|
||||
archiveLabel: "",
|
||||
totalItems: 0,
|
||||
};
|
||||
let folderUploadPlanState = {
|
||||
targetPane: "left",
|
||||
targetPath: "",
|
||||
@@ -321,6 +326,18 @@ function feedbackElements() {
|
||||
};
|
||||
}
|
||||
|
||||
function downloadModalElements() {
|
||||
return {
|
||||
overlay: document.getElementById("download-modal"),
|
||||
target: document.getElementById("download-modal-target"),
|
||||
currentFile: document.getElementById("download-modal-current-file"),
|
||||
count: document.getElementById("download-modal-count"),
|
||||
progressBar: document.getElementById("download-modal-progress-bar"),
|
||||
status: document.getElementById("download-modal-status"),
|
||||
closeButton: document.getElementById("download-modal-close-btn"),
|
||||
};
|
||||
}
|
||||
|
||||
function contextMenuElements() {
|
||||
return {
|
||||
menu: document.getElementById("context-menu"),
|
||||
@@ -347,6 +364,111 @@ function isOpenableSelection(item) {
|
||||
return isImageSelection(item) || isVideoSelection(item);
|
||||
}
|
||||
|
||||
function isZipDownloadSelection(items) {
|
||||
return items.length > 1 || (items.length === 1 && items[0].kind === "directory");
|
||||
}
|
||||
|
||||
function selectedItemCountLabel(totalItems) {
|
||||
return `${totalItems} selected item${totalItems === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
function isDownloadModalOpen() {
|
||||
return !downloadModalElements().overlay.classList.contains("hidden");
|
||||
}
|
||||
|
||||
function setDownloadModalVisible(visible) {
|
||||
const elements = downloadModalElements();
|
||||
if (!elements.overlay) {
|
||||
return;
|
||||
}
|
||||
elements.overlay.classList.toggle("hidden", !visible);
|
||||
}
|
||||
|
||||
function updateDownloadModalDisplay(info) {
|
||||
const elements = downloadModalElements();
|
||||
if (!elements.overlay) {
|
||||
return;
|
||||
}
|
||||
elements.target.textContent = info.targetText || "";
|
||||
elements.currentFile.textContent = info.currentFileText || "";
|
||||
elements.count.textContent = info.countText || "";
|
||||
elements.status.textContent = info.statusText || "";
|
||||
elements.progressBar.style.width = `${Math.max(0, Math.min(100, info.percent || 0))}%`;
|
||||
elements.closeButton.disabled = !!info.active;
|
||||
elements.closeButton.classList.toggle("hidden", !!info.active);
|
||||
}
|
||||
|
||||
function openZipDownloadModal(selectedItems) {
|
||||
downloadProgressState.active = true;
|
||||
downloadProgressState.archiveLabel = "ZIP archive";
|
||||
downloadProgressState.totalItems = selectedItems.length;
|
||||
setDownloadModalVisible(true);
|
||||
updateDownloadModalDisplay({
|
||||
active: true,
|
||||
targetText: "Preparing ZIP download",
|
||||
currentFileText: `Selection: ${selectedItemCountLabel(selectedItems.length)}`,
|
||||
countText: "Step 1/3",
|
||||
statusText: "preparing",
|
||||
percent: 33,
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
if (!downloadProgressState.active) {
|
||||
return;
|
||||
}
|
||||
updateDownloadModalDisplay({
|
||||
active: true,
|
||||
targetText: "Preparing ZIP download",
|
||||
currentFileText: `Packaging ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
|
||||
countText: "Step 2/3",
|
||||
statusText: "packaging items",
|
||||
percent: 66,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function markZipDownloadReady(fileName) {
|
||||
downloadProgressState.active = false;
|
||||
downloadProgressState.archiveLabel = fileName || "ZIP archive";
|
||||
updateDownloadModalDisplay({
|
||||
active: false,
|
||||
targetText: `Ready: ${downloadProgressState.archiveLabel}`,
|
||||
currentFileText: `Prepared ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
|
||||
countText: "Step 3/3",
|
||||
statusText: "ready",
|
||||
percent: 100,
|
||||
});
|
||||
window.setTimeout(closeDownloadModal, 240);
|
||||
}
|
||||
|
||||
function markZipDownloadFailed(err) {
|
||||
downloadProgressState.active = false;
|
||||
updateDownloadModalDisplay({
|
||||
active: false,
|
||||
targetText: "Preparing ZIP download",
|
||||
currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
|
||||
countText: "Step 2/3",
|
||||
statusText: `failed: ${err.message}`,
|
||||
percent: 66,
|
||||
});
|
||||
}
|
||||
|
||||
function closeDownloadModal() {
|
||||
if (downloadProgressState.active) {
|
||||
return;
|
||||
}
|
||||
downloadProgressState.archiveLabel = "";
|
||||
downloadProgressState.totalItems = 0;
|
||||
updateDownloadModalDisplay({
|
||||
active: false,
|
||||
targetText: "",
|
||||
currentFileText: "",
|
||||
countText: "",
|
||||
statusText: "",
|
||||
percent: 0,
|
||||
});
|
||||
setDownloadModalVisible(false);
|
||||
}
|
||||
|
||||
function isContextMenuOpen() {
|
||||
return contextMenuState.open && !contextMenuElements().menu.classList.contains("hidden");
|
||||
}
|
||||
@@ -499,6 +621,10 @@ async function startDownloadSelected() {
|
||||
if (selectedItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
const zipDownload = isZipDownloadSelection(selectedItems);
|
||||
if (zipDownload) {
|
||||
openZipDownloadModal(selectedItems);
|
||||
}
|
||||
try {
|
||||
const selected = selectedItems[0];
|
||||
const { blob, fileName } = await downloadFileRequest(selectedItems.map((item) => item.path));
|
||||
@@ -507,12 +633,20 @@ async function startDownloadSelected() {
|
||||
anchor.href = url;
|
||||
anchor.download = fileName || selected.name;
|
||||
document.body.append(anchor);
|
||||
if (zipDownload) {
|
||||
markZipDownloadReady(anchor.download);
|
||||
}
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setStatus(`Download started: ${anchor.download}`);
|
||||
} catch (err) {
|
||||
setActionError("Download", err);
|
||||
if (zipDownload) {
|
||||
markZipDownloadFailed(err);
|
||||
setStatus("Download failed");
|
||||
} else {
|
||||
setActionError("Download", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3615,6 +3749,13 @@ function handleKeyboardShortcuts(event) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isDownloadModalOpen()) {
|
||||
if (event.key === "Escape" && !downloadProgressState.active) {
|
||||
event.preventDefault();
|
||||
closeDownloadModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isInfoOpen()) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
@@ -3882,6 +4023,17 @@ function setupEvents() {
|
||||
}
|
||||
};
|
||||
}
|
||||
const downloadModal = downloadModalElements();
|
||||
if (downloadModal.closeButton) {
|
||||
downloadModal.closeButton.onclick = closeDownloadModal;
|
||||
}
|
||||
if (downloadModal.overlay) {
|
||||
downloadModal.overlay.onclick = (event) => {
|
||||
if (event.target === downloadModal.overlay) {
|
||||
closeDownloadModal();
|
||||
}
|
||||
};
|
||||
}
|
||||
const contextMenu = contextMenuElements();
|
||||
if (contextMenu.renameButton) {
|
||||
contextMenu.renameButton.onclick = startContextMenuRename;
|
||||
|
||||
@@ -772,6 +772,12 @@ button:disabled {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#download-modal .popup-card {
|
||||
max-width: 320px;
|
||||
padding: 12px 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.upload-modal-progress {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
@@ -793,6 +799,27 @@ button:disabled {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.download-modal-progress {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-border);
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.download-modal-progress-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--color-accent);
|
||||
transition: width 150ms ease;
|
||||
}
|
||||
|
||||
.download-modal-count {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.popup-meta {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -118,6 +118,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="download-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="download-modal-title">
|
||||
<div class="popup-card download-modal-card">
|
||||
<h3 id="download-modal-title">Preparing download</h3>
|
||||
<div id="download-modal-target" class="popup-meta"></div>
|
||||
<div id="download-modal-current-file" class="popup-meta"></div>
|
||||
<div class="download-modal-progress">
|
||||
<div id="download-modal-progress-bar" class="download-modal-progress-bar"></div>
|
||||
</div>
|
||||
<div id="download-modal-count" class="download-modal-count"></div>
|
||||
<div id="download-modal-status" class="popup-meta"></div>
|
||||
<div class="popup-actions">
|
||||
<button id="download-modal-close-btn" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="context-menu" class="context-menu hidden" role="menu" aria-label="Item context menu">
|
||||
<div id="context-menu-scope" class="context-menu-scope"></div>
|
||||
<div id="context-menu-target" class="context-menu-target"></div>
|
||||
|
||||
Reference in New Issue
Block a user