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-modal"', body)
|
||||||
self.assertIn('id="feedback-message"', body)
|
self.assertIn('id="feedback-message"', body)
|
||||||
self.assertIn('id="feedback-close-btn"', 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"', body)
|
||||||
self.assertIn('id="context-menu-scope"', body)
|
self.assertIn('id="context-menu-scope"', body)
|
||||||
self.assertIn('id="context-menu-target"', 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 feedbackElements()', app_js)
|
||||||
self.assertIn('function openFeedbackModal(message)', app_js)
|
self.assertIn('function openFeedbackModal(message)', app_js)
|
||||||
self.assertIn('function closeFeedbackModal()', 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 contextMenuElements()', app_js)
|
||||||
self.assertIn('function openContextMenu(pane, entry, event)', app_js)
|
self.assertIn('function openContextMenu(pane, entry, event)', app_js)
|
||||||
self.assertIn('function closeContextMenu()', app_js)
|
self.assertIn('function closeContextMenu()', app_js)
|
||||||
self.assertIn('function isOpenableSelection(item)', app_js)
|
self.assertIn('function isOpenableSelection(item)', app_js)
|
||||||
self.assertIn('async function downloadFileRequest(paths)', 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 applyContextMenuSelection()', app_js)
|
||||||
self.assertIn('function startContextMenuOpen()', app_js)
|
self.assertIn('function startContextMenuOpen()', app_js)
|
||||||
self.assertIn('function startContextMenuEdit()', app_js)
|
self.assertIn('function startContextMenuEdit()', app_js)
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ let uploadState = {
|
|||||||
conflictResolver: null,
|
conflictResolver: null,
|
||||||
cancelRequested: false,
|
cancelRequested: false,
|
||||||
};
|
};
|
||||||
|
let downloadProgressState = {
|
||||||
|
active: false,
|
||||||
|
archiveLabel: "",
|
||||||
|
totalItems: 0,
|
||||||
|
};
|
||||||
let folderUploadPlanState = {
|
let folderUploadPlanState = {
|
||||||
targetPane: "left",
|
targetPane: "left",
|
||||||
targetPath: "",
|
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() {
|
function contextMenuElements() {
|
||||||
return {
|
return {
|
||||||
menu: document.getElementById("context-menu"),
|
menu: document.getElementById("context-menu"),
|
||||||
@@ -347,6 +364,111 @@ function isOpenableSelection(item) {
|
|||||||
return isImageSelection(item) || isVideoSelection(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() {
|
function isContextMenuOpen() {
|
||||||
return contextMenuState.open && !contextMenuElements().menu.classList.contains("hidden");
|
return contextMenuState.open && !contextMenuElements().menu.classList.contains("hidden");
|
||||||
}
|
}
|
||||||
@@ -499,6 +621,10 @@ async function startDownloadSelected() {
|
|||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const zipDownload = isZipDownloadSelection(selectedItems);
|
||||||
|
if (zipDownload) {
|
||||||
|
openZipDownloadModal(selectedItems);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const selected = selectedItems[0];
|
const selected = selectedItems[0];
|
||||||
const { blob, fileName } = await downloadFileRequest(selectedItems.map((item) => item.path));
|
const { blob, fileName } = await downloadFileRequest(selectedItems.map((item) => item.path));
|
||||||
@@ -507,13 +633,21 @@ async function startDownloadSelected() {
|
|||||||
anchor.href = url;
|
anchor.href = url;
|
||||||
anchor.download = fileName || selected.name;
|
anchor.download = fileName || selected.name;
|
||||||
document.body.append(anchor);
|
document.body.append(anchor);
|
||||||
|
if (zipDownload) {
|
||||||
|
markZipDownloadReady(anchor.download);
|
||||||
|
}
|
||||||
anchor.click();
|
anchor.click();
|
||||||
anchor.remove();
|
anchor.remove();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
setStatus(`Download started: ${anchor.download}`);
|
setStatus(`Download started: ${anchor.download}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (zipDownload) {
|
||||||
|
markZipDownloadFailed(err);
|
||||||
|
setStatus("Download failed");
|
||||||
|
} else {
|
||||||
setActionError("Download", err);
|
setActionError("Download", err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startContextMenuDownload() {
|
function startContextMenuDownload() {
|
||||||
@@ -3615,6 +3749,13 @@ function handleKeyboardShortcuts(event) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isDownloadModalOpen()) {
|
||||||
|
if (event.key === "Escape" && !downloadProgressState.active) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeDownloadModal();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isInfoOpen()) {
|
if (isInfoOpen()) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
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();
|
const contextMenu = contextMenuElements();
|
||||||
if (contextMenu.renameButton) {
|
if (contextMenu.renameButton) {
|
||||||
contextMenu.renameButton.onclick = startContextMenuRename;
|
contextMenu.renameButton.onclick = startContextMenuRename;
|
||||||
|
|||||||
@@ -772,6 +772,12 @@ button:disabled {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#download-modal .popup-card {
|
||||||
|
max-width: 320px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-modal-progress {
|
.upload-modal-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
@@ -793,6 +799,27 @@ button:disabled {
|
|||||||
color: var(--color-text-muted);
|
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 {
|
.popup-meta {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -118,6 +118,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</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" 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-scope" class="context-menu-scope"></div>
|
||||||
<div id="context-menu-target" class="context-menu-target"></div>
|
<div id="context-menu-target" class="context-menu-target"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user