feat: download - download dwnload limieten in settings
This commit is contained in:
+103
-18
@@ -85,6 +85,7 @@ let downloadProgressState = {
|
||||
active: false,
|
||||
archiveLabel: "",
|
||||
totalItems: 0,
|
||||
requestKey: null,
|
||||
};
|
||||
let folderUploadPlanState = {
|
||||
targetPane: "left",
|
||||
@@ -103,6 +104,7 @@ let settingsState = {
|
||||
preferredStartupPathRight: null,
|
||||
selectedTheme: "default",
|
||||
selectedColorMode: "dark",
|
||||
zipDownloadLimits: null,
|
||||
};
|
||||
const VALID_THEME_FAMILIES = [
|
||||
"default",
|
||||
@@ -368,6 +370,10 @@ function isZipDownloadSelection(items) {
|
||||
return items.length > 1 || (items.length === 1 && items[0].kind === "directory");
|
||||
}
|
||||
|
||||
function zipDownloadRequestKey(paths) {
|
||||
return paths.join("\n");
|
||||
}
|
||||
|
||||
function selectedItemCountLabel(totalItems) {
|
||||
return `${totalItems} selected item${totalItems === 1 ? "" : "s"}`;
|
||||
}
|
||||
@@ -399,17 +405,19 @@ function updateDownloadModalDisplay(info) {
|
||||
}
|
||||
|
||||
function openZipDownloadModal(selectedItems) {
|
||||
const requestPaths = selectedItems.map((item) => item.path);
|
||||
downloadProgressState.active = true;
|
||||
downloadProgressState.archiveLabel = "ZIP archive";
|
||||
downloadProgressState.totalItems = selectedItems.length;
|
||||
downloadProgressState.requestKey = zipDownloadRequestKey(requestPaths);
|
||||
setDownloadModalVisible(true);
|
||||
updateDownloadModalDisplay({
|
||||
active: true,
|
||||
targetText: "Preparing ZIP download",
|
||||
targetText: "Preparing download...",
|
||||
currentFileText: `Selection: ${selectedItemCountLabel(selectedItems.length)}`,
|
||||
countText: "Step 1/3",
|
||||
statusText: "preparing",
|
||||
percent: 33,
|
||||
countText: "Preparing zip download",
|
||||
statusText: "Preparing download...",
|
||||
percent: 20,
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
if (!downloadProgressState.active) {
|
||||
@@ -417,11 +425,11 @@ function openZipDownloadModal(selectedItems) {
|
||||
}
|
||||
updateDownloadModalDisplay({
|
||||
active: true,
|
||||
targetText: "Preparing ZIP download",
|
||||
targetText: "Preparing download...",
|
||||
currentFileText: `Packaging ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
|
||||
countText: "Step 2/3",
|
||||
statusText: "packaging items",
|
||||
percent: 66,
|
||||
countText: "Zip preflight and packaging",
|
||||
statusText: "Preparing download...",
|
||||
percent: 55,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -431,24 +439,24 @@ function markZipDownloadReady(fileName) {
|
||||
downloadProgressState.archiveLabel = fileName || "ZIP archive";
|
||||
updateDownloadModalDisplay({
|
||||
active: false,
|
||||
targetText: `Ready: ${downloadProgressState.archiveLabel}`,
|
||||
targetText: `Download started: ${downloadProgressState.archiveLabel}`,
|
||||
currentFileText: `Prepared ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
|
||||
countText: "Step 3/3",
|
||||
statusText: "ready",
|
||||
countText: "Browser download started",
|
||||
statusText: "Download started",
|
||||
percent: 100,
|
||||
});
|
||||
window.setTimeout(closeDownloadModal, 240);
|
||||
window.setTimeout(closeDownloadModal, 480);
|
||||
}
|
||||
|
||||
function markZipDownloadFailed(err) {
|
||||
downloadProgressState.active = false;
|
||||
updateDownloadModalDisplay({
|
||||
active: false,
|
||||
targetText: "Preparing ZIP download",
|
||||
targetText: "Preparing download...",
|
||||
currentFileText: `Selection: ${selectedItemCountLabel(downloadProgressState.totalItems)}`,
|
||||
countText: "Step 2/3",
|
||||
statusText: `failed: ${err.message}`,
|
||||
percent: 66,
|
||||
countText: "Zip download failed",
|
||||
statusText: err.message || "Download failed",
|
||||
percent: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -458,6 +466,7 @@ function closeDownloadModal() {
|
||||
}
|
||||
downloadProgressState.archiveLabel = "";
|
||||
downloadProgressState.totalItems = 0;
|
||||
downloadProgressState.requestKey = null;
|
||||
updateDownloadModalDisplay({
|
||||
active: false,
|
||||
targetText: "",
|
||||
@@ -622,12 +631,19 @@ async function startDownloadSelected() {
|
||||
return;
|
||||
}
|
||||
const zipDownload = isZipDownloadSelection(selectedItems);
|
||||
const selectedPaths = selectedItems.map((item) => item.path);
|
||||
const requestKey = zipDownloadRequestKey(selectedPaths);
|
||||
if (zipDownload && downloadProgressState.active && downloadProgressState.requestKey === requestKey) {
|
||||
setStatus("Preparing download...");
|
||||
return;
|
||||
}
|
||||
if (zipDownload) {
|
||||
openZipDownloadModal(selectedItems);
|
||||
setStatus("Preparing download...");
|
||||
}
|
||||
try {
|
||||
const selected = selectedItems[0];
|
||||
const { blob, fileName } = await downloadFileRequest(selectedItems.map((item) => item.path));
|
||||
const { blob, fileName } = await downloadFileRequest(selectedPaths);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
@@ -680,9 +696,11 @@ function settingsElements() {
|
||||
closeButton: document.getElementById("settings-close-btn"),
|
||||
generalTab: document.getElementById("settings-general-tab"),
|
||||
interfaceTab: document.getElementById("settings-interface-tab"),
|
||||
downloadsTab: document.getElementById("settings-downloads-tab"),
|
||||
logsTab: document.getElementById("settings-logs-tab"),
|
||||
generalPanel: document.getElementById("settings-general-panel"),
|
||||
interfacePanel: document.getElementById("settings-interface-panel"),
|
||||
downloadsPanel: document.getElementById("settings-downloads-panel"),
|
||||
showThumbnailsInput: document.getElementById("settings-show-thumbnails"),
|
||||
startupPathLeftInput: document.getElementById("settings-startup-path-left"),
|
||||
startupPathRightInput: document.getElementById("settings-startup-path-right"),
|
||||
@@ -691,6 +709,11 @@ function settingsElements() {
|
||||
selectedThemeInput: document.getElementById("settings-selected-theme"),
|
||||
interfaceError: document.getElementById("settings-interface-error"),
|
||||
interfaceSaveButton: document.getElementById("settings-interface-save-btn"),
|
||||
downloadMaxItems: document.getElementById("settings-download-max-items"),
|
||||
downloadMaxTotalSize: document.getElementById("settings-download-max-total-size"),
|
||||
downloadMaxFileSize: document.getElementById("settings-download-max-file-size"),
|
||||
downloadScanTimeout: document.getElementById("settings-download-scan-timeout"),
|
||||
downloadSymlinkPolicy: document.getElementById("settings-download-symlink-policy"),
|
||||
logsPanel: document.getElementById("settings-logs-panel"),
|
||||
logsList: document.getElementById("settings-logs-list"),
|
||||
logsError: document.getElementById("settings-logs-error"),
|
||||
@@ -1661,6 +1684,7 @@ async function loadSettings() {
|
||||
settingsState.preferredStartupPathRight = data.preferred_startup_path_right || null;
|
||||
settingsState.selectedTheme = VALID_THEME_FAMILIES.includes(data.selected_theme) ? data.selected_theme : "default";
|
||||
settingsState.selectedColorMode = VALID_COLOR_MODES.includes(data.selected_color_mode) ? data.selected_color_mode : "dark";
|
||||
settingsState.zipDownloadLimits = data.zip_download_limits || null;
|
||||
const elements = settingsElements();
|
||||
if (elements.showThumbnailsInput) {
|
||||
elements.showThumbnailsInput.checked = settingsState.showThumbnails;
|
||||
@@ -1674,6 +1698,7 @@ async function loadSettings() {
|
||||
if (elements.selectedThemeInput) {
|
||||
elements.selectedThemeInput.value = settingsState.selectedTheme;
|
||||
}
|
||||
renderDownloadSettings();
|
||||
}
|
||||
|
||||
async function saveSettings(update) {
|
||||
@@ -1683,6 +1708,7 @@ async function saveSettings(update) {
|
||||
settingsState.preferredStartupPathRight = data.preferred_startup_path_right || null;
|
||||
settingsState.selectedTheme = VALID_THEME_FAMILIES.includes(data.selected_theme) ? data.selected_theme : "default";
|
||||
settingsState.selectedColorMode = VALID_COLOR_MODES.includes(data.selected_color_mode) ? data.selected_color_mode : "dark";
|
||||
settingsState.zipDownloadLimits = data.zip_download_limits || null;
|
||||
const elements = settingsElements();
|
||||
if (elements.showThumbnailsInput) {
|
||||
elements.showThumbnailsInput.checked = settingsState.showThumbnails;
|
||||
@@ -1696,6 +1722,7 @@ async function saveSettings(update) {
|
||||
if (elements.selectedThemeInput) {
|
||||
elements.selectedThemeInput.value = settingsState.selectedTheme;
|
||||
}
|
||||
renderDownloadSettings();
|
||||
applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);
|
||||
renderPaneItems("left");
|
||||
renderPaneItems("right");
|
||||
@@ -1728,6 +1755,57 @@ function isEditableSelection(item) {
|
||||
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".py", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
|
||||
}
|
||||
|
||||
function formatBinarySize(bytes) {
|
||||
const value = Number(bytes);
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
return "-";
|
||||
}
|
||||
if (value < 1024) {
|
||||
return `${value} B`;
|
||||
}
|
||||
const units = ["KiB", "MiB", "GiB", "TiB"];
|
||||
let scaled = value;
|
||||
let unitIndex = -1;
|
||||
do {
|
||||
scaled /= 1024;
|
||||
unitIndex += 1;
|
||||
} while (scaled >= 1024 && unitIndex < units.length - 1);
|
||||
const digits = scaled >= 10 || unitIndex === 0 ? 0 : 1;
|
||||
return `${scaled.toFixed(digits)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatSeconds(seconds) {
|
||||
const value = Number(seconds);
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
return "-";
|
||||
}
|
||||
return `${value % 1 === 0 ? value.toFixed(0) : value.toFixed(1)} seconds`;
|
||||
}
|
||||
|
||||
function formatSymlinkPolicy(policy) {
|
||||
return policy === "not_allowed" ? "Rejected / not allowed" : (policy || "-");
|
||||
}
|
||||
|
||||
function renderDownloadSettings() {
|
||||
const elements = settingsElements();
|
||||
const limits = settingsState.zipDownloadLimits || {};
|
||||
if (elements.downloadMaxItems) {
|
||||
elements.downloadMaxItems.textContent = limits.max_items ? `${limits.max_items} items` : "-";
|
||||
}
|
||||
if (elements.downloadMaxTotalSize) {
|
||||
elements.downloadMaxTotalSize.textContent = formatBinarySize(limits.max_total_input_bytes);
|
||||
}
|
||||
if (elements.downloadMaxFileSize) {
|
||||
elements.downloadMaxFileSize.textContent = formatBinarySize(limits.max_individual_file_bytes);
|
||||
}
|
||||
if (elements.downloadScanTimeout) {
|
||||
elements.downloadScanTimeout.textContent = formatSeconds(limits.scan_timeout_seconds);
|
||||
}
|
||||
if (elements.downloadSymlinkPolicy) {
|
||||
elements.downloadSymlinkPolicy.textContent = formatSymlinkPolicy(limits.symlink_policy);
|
||||
}
|
||||
}
|
||||
|
||||
function monacoLanguageForName(name) {
|
||||
const lower = (name || "").toLowerCase();
|
||||
if (lower === "dockerfile" || lower === "containerfile") {
|
||||
@@ -3302,18 +3380,22 @@ async function submitSearch() {
|
||||
|
||||
function setSettingsTab(tab) {
|
||||
const elements = settingsElements();
|
||||
settingsState.activeTab = tab === "logs" ? "logs" : (tab === "interface" ? "interface" : "general");
|
||||
settingsState.activeTab = tab === "logs" ? "logs" : (tab === "downloads" ? "downloads" : (tab === "interface" ? "interface" : "general"));
|
||||
const isGeneral = settingsState.activeTab === "general";
|
||||
const isInterface = settingsState.activeTab === "interface";
|
||||
const isDownloads = settingsState.activeTab === "downloads";
|
||||
const isLogs = settingsState.activeTab === "logs";
|
||||
elements.generalTab.classList.toggle("is-active", isGeneral);
|
||||
elements.generalTab.setAttribute("aria-selected", isGeneral ? "true" : "false");
|
||||
elements.interfaceTab.classList.toggle("is-active", isInterface);
|
||||
elements.interfaceTab.setAttribute("aria-selected", isInterface ? "true" : "false");
|
||||
elements.downloadsTab.classList.toggle("is-active", isDownloads);
|
||||
elements.downloadsTab.setAttribute("aria-selected", isDownloads ? "true" : "false");
|
||||
elements.logsTab.classList.toggle("is-active", isLogs);
|
||||
elements.logsTab.setAttribute("aria-selected", isLogs ? "true" : "false");
|
||||
elements.generalPanel.classList.toggle("hidden", !isGeneral);
|
||||
elements.interfacePanel.classList.toggle("hidden", !isInterface);
|
||||
elements.downloadsPanel.classList.toggle("hidden", !isDownloads);
|
||||
elements.logsPanel.classList.toggle("hidden", !isLogs);
|
||||
}
|
||||
|
||||
@@ -3430,6 +3512,8 @@ async function openSettings(tab = "general") {
|
||||
}
|
||||
(settingsState.activeTab === "logs"
|
||||
? elements.logsTab
|
||||
: settingsState.activeTab === "downloads"
|
||||
? elements.downloadsTab
|
||||
: settingsState.activeTab === "interface"
|
||||
? elements.interfaceTab
|
||||
: elements.generalTab).focus();
|
||||
@@ -4107,6 +4191,7 @@ function setupEvents() {
|
||||
settings.closeButton.onclick = closeSettings;
|
||||
settings.generalTab.onclick = () => setSettingsTab("general");
|
||||
settings.interfaceTab.onclick = () => setSettingsTab("interface");
|
||||
settings.downloadsTab.onclick = () => setSettingsTab("downloads");
|
||||
settings.logsTab.onclick = async () => {
|
||||
setSettingsTab("logs");
|
||||
await loadHistoryForSettings();
|
||||
|
||||
@@ -1029,6 +1029,34 @@ button:disabled {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.settings-readonly-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.settings-readonly-item {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.settings-readonly-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.settings-readonly-value {
|
||||
font-size: 14px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.settings-placeholder-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
<div class="settings-tabs" role="tablist" aria-label="Settings tabs">
|
||||
<button id="settings-general-tab" class="settings-tab is-active" type="button" role="tab" aria-selected="true">General</button>
|
||||
<button id="settings-interface-tab" class="settings-tab" type="button" role="tab" aria-selected="false">Interface</button>
|
||||
<button id="settings-downloads-tab" class="settings-tab" type="button" role="tab" aria-selected="false">Downloads</button>
|
||||
<button id="settings-logs-tab" class="settings-tab" type="button" role="tab" aria-selected="false">Logs</button>
|
||||
</div>
|
||||
<section id="settings-general-panel" class="settings-panel" role="tabpanel" aria-labelledby="settings-general-tab">
|
||||
@@ -198,6 +199,32 @@
|
||||
<button id="settings-interface-save-btn" type="button">Save</button>
|
||||
</div>
|
||||
</section>
|
||||
<section id="settings-downloads-panel" class="settings-panel hidden" role="tabpanel" aria-labelledby="settings-downloads-tab">
|
||||
<div class="settings-placeholder-title">Downloads</div>
|
||||
<div class="popup-meta">ZIP download limits are shown for reference and cannot be changed here.</div>
|
||||
<div class="settings-readonly-list">
|
||||
<div class="settings-readonly-item">
|
||||
<div class="settings-readonly-label">Max items</div>
|
||||
<div id="settings-download-max-items" class="settings-readonly-value"></div>
|
||||
</div>
|
||||
<div class="settings-readonly-item">
|
||||
<div class="settings-readonly-label">Max total input size</div>
|
||||
<div id="settings-download-max-total-size" class="settings-readonly-value"></div>
|
||||
</div>
|
||||
<div class="settings-readonly-item">
|
||||
<div class="settings-readonly-label">Max individual file size</div>
|
||||
<div id="settings-download-max-file-size" class="settings-readonly-value"></div>
|
||||
</div>
|
||||
<div class="settings-readonly-item">
|
||||
<div class="settings-readonly-label">Scan timeout</div>
|
||||
<div id="settings-download-scan-timeout" class="settings-readonly-value"></div>
|
||||
</div>
|
||||
<div class="settings-readonly-item">
|
||||
<div class="settings-readonly-label">Symlink policy</div>
|
||||
<div id="settings-download-symlink-policy" class="settings-readonly-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="settings-logs-panel" class="settings-panel hidden" role="tabpanel" aria-labelledby="settings-logs-tab">
|
||||
<div id="settings-logs-error" class="error"></div>
|
||||
<div id="settings-logs-list" class="settings-log-list"></div>
|
||||
|
||||
Reference in New Issue
Block a user