feat: download - fase 03

This commit is contained in:
kodi
2026-03-14 13:10:52 +01:00
parent dab87878cc
commit 7e7c2f3958
5 changed files with 218 additions and 1 deletions
@@ -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)
+152
View File
@@ -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,13 +633,21 @@ 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) {
if (zipDownload) {
markZipDownloadFailed(err);
setStatus("Download failed");
} else {
setActionError("Download", err);
}
}
}
function startContextMenuDownload() {
@@ -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;
+27
View File
@@ -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;
+16
View File
@@ -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>