Files
2025-12-01 22:07:55 +08:00

954 lines
36 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// 创建样式
const style = document.createElement("style");
style.textContent = `
.ky-file-browser {
display: flex;
flex-direction: column;
height: 100%;
color: var(--fg-color);
font-family: sans-serif;
}
.ky-browser-header {
padding: 10px;
background: var(--bg-color);
border-bottom: 1px solid var(--border-color);
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.ky-filter-container {
display: flex;
align-items: center;
gap: 5px;
}
.ky-filter-label {
font-size: 12px;
color: var(--fg-color);
}
.ky-filter-select {
background: #333333;
color: var(--input-text);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
min-width: 120px;
}
.ky-current-path {
flex-grow: 1;
background: var(--input-bg);
color: var(--input-text);
padding: 5px;
border-radius: 4px;
border: 1px solid var(--border-color);
}
.ky-file-list {
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 2px;
}
.ky-file-item {
padding: 6px 10px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
}
.ky-file-item:hover {
background: var(--tr-even-bg-color);
}
.ky-file-item.selected {
background: var(--p-600, #3b82f6);
color: #ffffff;
border-left: 3px solid var(--p-800, #1e40af);
}
.ky-file-item.selected:hover {
background: var(--p-600, #3b82f6);
}
.ky-item-icon {
margin-right: 10px;
width: 20px;
text-align: center;
display: inline-block;
}
.ky-browser-footer {
padding: 10px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 10px;
}
.ky-btn {
padding: 5px 15px;
cursor: pointer;
background: var(--comfy-input-bg);
border: 1px solid var(--border-color);
color: var(--fg-color);
border-radius: 4px;
}
.ky-btn:hover {
background: var(--comfy-menu-bg);
}
.ky-btn.primary {
background: var(--p-700);
color: white;
}
.ky-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ky-browser-body {
display: flex;
flex: 1;
min-height: 0;
border-top: 1px solid var(--border-color);
}
.ky-file-list {
flex: 0 0 35%;
border-right: 1px solid var(--border-color);
}
.ky-preview {
flex: 0 0 65%;
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
}
.ky-preview-title {
font-size: 12px;
color: var(--fg-color);
}
.ky-preview-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
background: var(--tr-even-bg-color);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.ky-preview-content img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.ky-header-meta {
font-size: 12px;
color: var(--fg-color);
opacity: 0.8;
white-space: nowrap;
}
`;
document.head.appendChild(style);
// 根据文件扩展名获取图标
function getFileIcon(fileName) {
if (!fileName || typeof fileName !== 'string') return "📄";
const extension = fileName.split('.').pop().toLowerCase();
// 图像文件
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif'];
if (imageExtensions.includes(extension)) return "🖼️";
// 视频文件
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', 'm4v', '3gp', 'ogv'];
if (videoExtensions.includes(extension)) return "🎬";
// 音频文件
const audioExtensions = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a', 'opus'];
if (audioExtensions.includes(extension)) return "🎵";
// 文档文件
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp'];
if (documentExtensions.includes(extension)) return "📋";
// 代码文件
const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'scss', 'less', 'json', 'xml', 'py', 'java', 'cpp', 'c', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'scala', 'r', 'sql', 'sh', 'bat', 'ps1'];
if (codeExtensions.includes(extension)) return "💻";
// 压缩文件
const archiveExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lzma'];
if (archiveExtensions.includes(extension)) return "📦";
// 可执行文件
const executableExtensions = ['exe', 'msi', 'app', 'deb', 'rpm', 'dmg', 'pkg'];
if (executableExtensions.includes(extension)) return "⚙️";
// 文本文件
const textExtensions = ['txt', 'md', 'rtf', 'log', 'ini', 'cfg', 'conf', 'yaml', 'yml', 'toml'];
if (textExtensions.includes(extension)) return "📝";
// 电子表格文件
const spreadsheetExtensions = ['csv', 'tsv'];
if (spreadsheetExtensions.includes(extension)) return "📊";
// 字体文件
const fontExtensions = ['ttf', 'otf', 'woff', 'woff2', 'eot'];
if (fontExtensions.includes(extension)) return "🔤";
// 3D模型文件
const modelExtensions = ['obj', 'fbx', 'dae', '3ds', 'blend', 'max', 'ma'];
if (modelExtensions.includes(extension)) return "🎮";
// 默认文件图标
return "📄";
}
// 根据文件类型和过滤条件判断是否应该显示该文件
function shouldShowFile(file, filterType) {
// 文件夹和驱动器始终显示,不受过滤条件影响
if (file.type === "dir" || file.type === "drive") {
return true;
}
// 如果是"folder"过滤条件,只显示文件夹
if (filterType === "folder") {
return false;
}
// 如果是"all"过滤条件,显示所有文件
if (filterType === "all") {
return true;
}
// 获取文件扩展名
if (!file.name || typeof file.name !== 'string') return false;
const extension = file.name.split('.').pop().toLowerCase();
// 根据过滤类型判断
switch (filterType) {
case "image":
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif'].includes(extension);
case "video":
return ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', 'm4v', '3gp', 'ogv'].includes(extension);
case "audio":
return ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a', 'opus'].includes(extension);
case "document":
return ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp'].includes(extension);
case "code":
return ['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'scss', 'less', 'json', 'xml', 'py', 'java', 'cpp', 'c', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt', 'scala', 'r', 'sql', 'sh', 'bat', 'ps1'].includes(extension);
case "archive":
return ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lzma'].includes(extension);
case "text":
return ['txt', 'md', 'rtf', 'log', 'ini', 'cfg', 'conf', 'yaml', 'yml', 'toml', 'csv', 'tsv'].includes(extension);
default:
return true;
}
}
app.registerExtension({
name: "KY.PathSelector",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "KY_GetFromPath") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const dirWidget = this.widgets.find((w) => w.name === "path");
this.addWidget("button", "📁 Open File Browser", null, async (widget, graphCanvas, node, pos, event) => {
const entered = (dirWidget?.value || "").trim().replace(/"/g, "");
if (!entered) {
showFileBrowser("output", (selectedPath) => { dirWidget.value = selectedPath; }, null, dirWidget);
return;
}
try {
const resp = await api.fetchApi("/ky_utils/check_path", { method: "POST", body: JSON.stringify({ path: entered }) });
const data = await resp.json();
if (data.type === "file") {
const parentDir = entered.substring(0, entered.lastIndexOf('\\')) || entered.substring(0, entered.lastIndexOf('/')) || entered;
showFileBrowser(parentDir, (selectedPath) => { dirWidget.value = selectedPath; }, entered, dirWidget);
} else if (data.type === "directory") {
showFileBrowser(entered, (selectedPath) => { dirWidget.value = selectedPath; }, null, dirWidget);
} else {
showFileBrowser("output", (selectedPath) => { dirWidget.value = selectedPath; }, null, dirWidget);
}
} catch (e) {
showFileBrowser(entered || "output", (selectedPath) => { dirWidget.value = selectedPath; }, null, dirWidget);
}
});
const originalCallback = dirWidget.callback;
dirWidget.callback = function(value, ...args) {
if (originalCallback) {
originalCallback.call(this, value, ...args);
}
};
return r;
};
}
},
});
// 处理路径输入的函数
async function handlePathInput(path, dirWidget) {
try {
// 规范化路径
const normalizedPath = path.trim().replace(/"/g, '');
// 检查路径是否存在
const response = await api.fetchApi("/ky_utils/check_path", {
method: "POST",
body: JSON.stringify({ path: normalizedPath }),
});
const data = await response.json();
if (data.error) {
console.error("Path check error:", data.error);
return;
}
// 如果是文件,打开文件浏览器并预览文件
if (data.type === "file") {
// 获取文件的父目录
const parentDir = normalizedPath.substring(0, normalizedPath.lastIndexOf('\\')) ||
normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) ||
normalizedPath;
// 打开文件浏览器,显示父目录内容,并预览该文件
showFileBrowser(parentDir, (selectedPath) => {
// 更新路径输入框的值
if (dirWidget) {
dirWidget.value = selectedPath;
// 不触发widget的回调,避免重新打开文件浏览器
// 用户已经通过文件浏览器选择了路径,不需要再次处理
}
}, normalizedPath, dirWidget); // 传递文件路径用于预览和dirWidget
}
// 如果是目录,打开文件浏览器并显示目录内容
else if (data.type === "directory") {
showFileBrowser(normalizedPath, (selectedPath) => {
// 更新路径输入框的值
if (dirWidget) {
dirWidget.value = selectedPath;
// 不触发widget的回调,避免重新打开文件浏览器
// 用户已经通过文件浏览器选择了路径,不需要再次处理
}
}, null, dirWidget); // 传递dirWidget
}
} catch (error) {
console.error("Error handling path input:", error);
}
}
// 全局变量,跟踪当前打开的对话框
let currentDialog = null;
let suppressPathHandling = false;
function showFileBrowser(initialPath, onSelect, filePathToPreview = null, dirWidget = null) {
// 如果已有对话框打开,先关闭它
if (currentDialog && document.body.contains(currentDialog)) {
document.body.removeChild(currentDialog);
currentDialog = null;
}
const dialog = document.createElement("div");
dialog.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8); z-index: 10000;
display: flex; justify-content: center; align-items: center;
`;
// 保存当前对话框引用
currentDialog = dialog;
const content = document.createElement("div");
content.style.cssText = `
width: 100vw; height: 100vh;
background: var(--comfy-menu-bg);
border-radius: 8px; border: 1px solid var(--border-color);
display: flex; flex-direction: column; overflow: hidden;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
`;
dialog.appendChild(content);
content.innerHTML = `
<div class="ky-file-browser">
<div class="ky-browser-header">
<button class="ky-btn" id="ky-up-btn">⬆ Up</button>
<div class="ky-filter-container">
<span class="ky-filter-label">Filter:</span>
<select class="ky-filter-select" id="ky-filter-select">
<option value="all">📄 All Files</option>
<option value="image">🖼️ Images</option>
<option value="video">🎬 Videos</option>
<option value="audio">🎵 Audio</option>
<option value="document">📋 Documents</option>
<option value="code">💻 Code</option>
<option value="archive">📦 Archives</option>
<option value="text">📝 Text</option>
<option value="folder">📁 Folders Only</option>
</select>
<span class="ky-filter-label">Sort:</span>
<select class="ky-filter-select" id="ky-sort-select">
<option value="name">Name</option>
<option value="modified">Modified</option>
<option value="created">Created</option>
</select>
<select class="ky-filter-select" id="ky-order-select">
<option value="asc">Asc</option>
<option value="desc">Desc</option>
</select>
</div>
<input type="text" class="ky-current-path" id="ky-path-input" readonly />
<span class="ky-header-meta" id="ky-header-meta"></span>
<a class="ky-btn" id="ky-download-btn" style="display:none">⬇ Save</a>
</div>
<div class="ky-browser-body">
<div class="ky-file-list" id="ky-file-list"></div>
<div class="ky-preview" id="ky-preview">
<div class="ky-preview-title">Preview</div>
<div class="ky-preview-content" id="ky-preview-content"></div>
</div>
</div>
<div class="ky-browser-footer">
<button class="ky-btn" id="ky-cancel-btn">❌ Cancel</button>
<button class="ky-btn primary" id="ky-select-btn">✅ Select</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const pathInput = content.querySelector("#ky-path-input");
const fileListEl = content.querySelector("#ky-file-list");
const upBtn = content.querySelector("#ky-up-btn");
const cancelBtn = content.querySelector("#ky-cancel-btn");
const selectBtn = content.querySelector("#ky-select-btn");
const filterSelect = content.querySelector("#ky-filter-select");
const sortSelect = content.querySelector("#ky-sort-select");
const orderSelect = content.querySelector("#ky-order-select");
const previewEl = content.querySelector("#ky-preview");
const previewContentEl = content.querySelector("#ky-preview-content");
const headerMetaEl = content.querySelector("#ky-header-meta");
const downloadBtn = content.querySelector("#ky-download-btn");
let currentPath = initialPath || "";
let parentPath = ""; // 由后端 API 提供
let selectedItemPath = null;
let currentFilter = "all"; // 当前过滤类型
let currentSortKey = "name";
let currentSortOrder = "asc";
let allFiles = []; // 存储所有文件,用于过滤
let initialFilePath = filePathToPreview; // 存储初始文件路径,用于预览
let renderedFiles = [];
let previewCache = new Map();
let currentPreviewToken = 0;
function finalizeSelection(finalPath) {
if (finalPath === "My Computer") {
return;
}
if (dirWidget) {
suppressPathHandling = true;
dirWidget.value = finalPath;
}
closeDialog();
setTimeout(() => { suppressPathHandling = false; }, 0);
}
async function fetchPath(path) {
try {
const response = await api.fetchApi("/ky_utils/browse", {
method: "POST",
body: JSON.stringify({ path: path }),
});
const data = await response.json();
if (data.error) {
alert("Error: " + data.error);
return;
}
render(data);
// 如果有初始文件路径,在渲染完成后预览该文件
if (initialFilePath) {
// 查找文件列表中的文件
const fileItem = allFiles.find(file => file.path === initialFilePath);
if (fileItem) {
// 选中该文件并预览
selectFileAndPreview(fileItem);
}
initialFilePath = null; // 清除初始文件路径
}
} catch (e) {
console.error(e);
alert("Failed to browse path.");
}
}
// 选中文件并预览
function selectFileAndPreview(file) {
const fileItems = document.querySelectorAll(".ky-file-item");
for (const item of fileItems) {
if (item.dataset && item.dataset.path === file.path) {
document.querySelectorAll(".ky-file-item").forEach(i => i.classList.remove("selected"));
item.classList.add("selected");
selectedItemPath = file.path;
pathInput.value = file.path;
initialFilePath = null;
updatePreview(file);
if (item.scrollIntoView) item.scrollIntoView({ block: "nearest" });
prefetchNeighbors();
break;
}
}
}
function findFirstFileIndex() {
const i = renderedFiles.findIndex(f => f.type === "file");
return i === -1 ? 0 : i;
}
function findLastFileIndex() {
for (let i = renderedFiles.length - 1; i >= 0; i--) {
if (renderedFiles[i].type === "file") return i;
}
return renderedFiles.length - 1;
}
function findNextFileIndex(start, step) {
let i = start + step;
if (i < 0) i = 0;
if (i > renderedFiles.length - 1) i = renderedFiles.length - 1;
const dir = step >= 0 ? 1 : -1;
while (i >= 0 && i < renderedFiles.length) {
if (renderedFiles[i].type === "file") return i;
i += dir;
}
return start;
}
function moveSelection(delta) {
if (!renderedFiles || renderedFiles.length === 0) return;
let idx = renderedFiles.findIndex(f => f.path === selectedItemPath);
if (idx === -1) idx = findFirstFileIndex();
const nextIdx = findNextFileIndex(idx, delta);
selectFileAndPreview(renderedFiles[nextIdx]);
}
function getPageStep() {
const firstItem = fileListEl.querySelector(".ky-file-item");
const itemHeight = firstItem ? firstItem.offsetHeight : 24;
const page = Math.floor(fileListEl.clientHeight / (itemHeight || 1));
return page > 0 ? page : 10;
}
function prefetchNeighbors() {
if (!renderedFiles || renderedFiles.length === 0 || !selectedItemPath) return;
const idx = renderedFiles.findIndex(f => f.path === selectedItemPath);
const neighbors = [idx - 1, idx + 1];
for (const i of neighbors) {
if (i >= 0 && i < renderedFiles.length) {
const f = renderedFiles[i];
if (f.type === "file") ensureCached(f);
}
}
}
async function ensureCached(file) {
if (!file || file.type !== "file") return;
if (previewCache.has(file.path)) return;
try {
const response = await api.fetchApi("/ky_utils/file_preview", {
method: "POST",
body: JSON.stringify({ path: file.path })
});
const info = await response.json();
if (info && !info.error) {
if (info.type === "image" && info.preview_url) {
const img = new Image();
const entry = { info, element: img };
previewCache.set(file.path, entry);
img.onload = () => {};
img.src = info.preview_url;
} else if (info.type === "text") {
previewCache.set(file.path, { info });
} else if ((info.type === "video" || info.type === "audio") && info.preview_url) {
previewCache.set(file.path, { info });
} else {
previewCache.set(file.path, { info });
}
}
} catch (e) {}
}
async function updatePreview(file) {
const previewContentEl = document.querySelector("#ky-preview-content");
if (!previewContentEl) return;
if (!file || file.type !== "file") {
if (headerMetaEl) headerMetaEl.textContent = "";
if (downloadBtn) {
downloadBtn.style.display = "none";
downloadBtn.removeAttribute("href");
downloadBtn.removeAttribute("download");
}
return;
}
const token = ++currentPreviewToken;
let cached = previewCache.get(file.path);
if (!cached) {
try {
const response = await api.fetchApi("/ky_utils/file_preview", {
method: "POST",
body: JSON.stringify({ path: file.path })
});
const info = await response.json();
if (info.error) {
if (token !== currentPreviewToken) return;
if (headerMetaEl) headerMetaEl.textContent = "";
return;
}
if (info.type === "image" && info.preview_url) {
const img = new Image();
cached = { info, element: img };
previewCache.set(file.path, cached);
img.onload = () => {
if (token !== currentPreviewToken) return;
clearPreview();
const clone = img.cloneNode();
previewContentEl.appendChild(clone);
};
img.src = info.preview_url;
} else {
cached = { info };
previewCache.set(file.path, cached);
}
} catch (e) {
if (token !== currentPreviewToken) return;
previewContentEl.textContent = "Preview failed";
return;
}
}
const info = cached.info;
const mb = 1024*1024;
//如果字节数小于1024则不加单位,如果大于1024则用KB, 如果大于1024*1024则用MB
const sizeStr = typeof info?.size === "number" ? `${(info.size>mb?info.size/mb:info.size>1024?info.size/1024:info.size).toFixed(2)} ${info.size>mb?"MB":"KB"}` : "";
if (headerMetaEl) headerMetaEl.textContent = `${file.name}${sizeStr ? ` • ${sizeStr}` : ""}`;
if (downloadBtn) {
if (info?.preview_url) {
downloadBtn.style.display = "";
downloadBtn.setAttribute("href", info.preview_url);
downloadBtn.setAttribute("download", file.name);
} else {
downloadBtn.style.display = "none";
downloadBtn.removeAttribute("href");
downloadBtn.removeAttribute("download");
}
}
if (info?.type === "image" && cached.element) {
if (cached.element.complete) {
clearPreview();
const clone = cached.element.cloneNode();
previewContentEl.appendChild(clone);
}
} else if (info?.type === "text" && info.snippet) {
clearPreview();
const pre = document.createElement("pre");
pre.style.whiteSpace = "pre-wrap";
pre.style.wordBreak = "break-word";
pre.textContent = info.snippet;
previewContentEl.appendChild(pre);
} else if (info?.type === "video" && info.preview_url) {
clearPreview();
const video = document.createElement("video");
video.controls = true;
video.style.width = "100%";
video.style.height = "100%";
video.src = info.preview_url;
previewContentEl.appendChild(video);
} else if (info?.type === "audio" && info.preview_url) {
clearPreview();
const audio = document.createElement("audio");
audio.controls = true;
audio.style.width = "100%";
audio.src = info.preview_url;
previewContentEl.appendChild(audio);
} else {
clearPreview();
previewContentEl.textContent = "No preview available";
}
ensureCached(file);
prefetchNeighbors();
}
function render(data) {
// 更新状态
currentPath = data.path;
parentPath = data.parent_path; // 可能是路径,也可能是 "ROOT_DRIVES" 或空字符串
// 更新 UI
pathInput.value = currentPath;
fileListEl.innerHTML = "";
selectedItemPath = null;
clearPreview();
// Up 按钮状态:如果没有父级(且不是特殊的 ROOT_DRIVES 模式),则禁用
upBtn.disabled = !parentPath;
// 存储所有文件
allFiles = data.files || [];
// 应用过滤
applyFilter();
}
function applyFilter() {
fileListEl.innerHTML = "";
selectedItemPath = null;
clearPreview();
// 根据当前过滤条件筛选文件
const filteredFiles = allFiles.filter(file => shouldShowFile(file, currentFilter));
// 排序
const orderFactor = currentSortOrder === "desc" ? -1 : 1;
filteredFiles.sort((a, b) => {
if (a && a.name === "..") return -1;
if (b && b.name === "..") return 1;
const aDir = a && (a.type === "dir" || a.type === "drive");
const bDir = b && (b.type === "dir" || b.type === "drive");
if (aDir && !bDir) return -1;
if (!aDir && bDir) return 1;
if (currentSortKey === "name") {
const an = (a.name || "").toLowerCase();
const bn = (b.name || "").toLowerCase();
return an.localeCompare(bn) * orderFactor;
}
const av = typeof a[currentSortKey] === "number" ? a[currentSortKey] : 0;
const bv = typeof b[currentSortKey] === "number" ? b[currentSortKey] : 0;
if (av === bv) {
const an = (a.name || "").toLowerCase();
const bn = (b.name || "").toLowerCase();
return an.localeCompare(bn) * orderFactor;
}
return (av - bv) * orderFactor;
});
renderedFiles = filteredFiles;
filteredFiles.forEach((file, i) => {
const el = document.createElement("div");
el.className = "ky-file-item";
// 根据类型显示不同图标
let icon = "📄";
if (file.type === "dir") icon = "📁";
else if (file.type === "drive") icon = "💾"; // 硬盘图标
else if (file.type === "file") icon = getFileIcon(file.name); // 根据文件扩展名获取图标
el.innerHTML = `<span class="ky-item-icon">${icon}</span> ${file.name}`;
el.dataset.path = file.path;
el.dataset.index = String(i);
el.onclick = () => {
// ".." 保持为进入上级
if (file.name === "..") {
fetchPath(file.path);
return;
}
// 单击:文件与文件夹都可选中
document.querySelectorAll(".ky-file-item").forEach(i => i.classList.remove("selected"));
el.classList.add("selected");
selectedItemPath = file.path;
pathInput.value = file.path;
initialFilePath = null;
if (file.type === "file") {
updatePreview(file);
} else {
clearPreview();
}
};
el.ondblclick = () => {
if (file.type === "file") {
finalizeSelection(file.path);
} else if (file.type === "dir" || file.type === "drive" || file.name === "..") {
fetchPath(file.path);
}
};
fileListEl.appendChild(el);
});
}
// 事件绑定
upBtn.onclick = () => {
if (parentPath) {
fetchPath(parentPath);
}
};
cancelBtn.onclick = () => {
closeDialog();
};
selectBtn.onclick = () => {
const finalPath = initialFilePath || selectedItemPath || currentPath;
if (finalPath === "My Computer") {
alert("Please select a valid drive or folder.");
return;
}
finalizeSelection(finalPath);
};
// 关闭对话框的函数
function closeDialog() {
if (currentDialog && document.body.contains(currentDialog)) {
document.body.removeChild(currentDialog);
currentDialog = null;
}
}
// 过滤下拉框事件处理
filterSelect.onchange = () => {
currentFilter = filterSelect.value;
applyFilter();
};
sortSelect.onchange = () => {
currentSortKey = sortSelect.value;
applyFilter();
};
orderSelect.onchange = () => {
currentSortOrder = orderSelect.value;
applyFilter();
};
async function deleteSelected() {
try {
if (!currentDialog || !document.body.contains(currentDialog)) return;
if (!selectedItemPath) return;
const target = renderedFiles.find(f => f.path === selectedItemPath);
if (!target) return;
if (target.name === "..") return;
if (target.type === "drive") return;
if (target.type === "dir") {
const ok = confirm(`Delete folder "${target.name}"? This cannot be undone.`);
if (!ok) return;
}
const resp = await api.fetchApi("/ky_utils/delete", {
method: "DELETE",
body: JSON.stringify({ path: target.path })
});
const data = await resp.json();
if (data && data.ok) {
const startIdx = renderedFiles.findIndex(f => f.path === selectedItemPath);
const wasFile = target.type === "file";
allFiles = allFiles.filter(f => f.path !== target.path);
selectedItemPath = null;
applyFilter();
if (wasFile && renderedFiles && renderedFiles.length > 0) {
let anchor = startIdx;
if (anchor < 0) anchor = findFirstFileIndex();
if (anchor > renderedFiles.length - 1) anchor = renderedFiles.length - 1;
const candidate = renderedFiles[anchor];
if (candidate && candidate.type === "file") {
selectFileAndPreview(candidate);
} else {
let nextIdx = findNextFileIndex(anchor, 1);
if (renderedFiles[nextIdx] && renderedFiles[nextIdx].type === "file") {
selectFileAndPreview(renderedFiles[nextIdx]);
} else {
nextIdx = findNextFileIndex(anchor, -1);
if (renderedFiles[nextIdx] && renderedFiles[nextIdx].type === "file") {
selectFileAndPreview(renderedFiles[nextIdx]);
} else {
clearPreview();
if (headerMetaEl) headerMetaEl.textContent = "";
}
}
}
}
} else {
alert(`Delete failed: ${data?.error || "Unknown error"}`);
}
} catch (err) {
alert(`Delete failed: ${err?.message || err}`);
}
}
// 添加键盘事件监听器,只在对话框打开时有效
const keyHandler = (e) => {
// 确保事件只在对话框打开时处理
if (!currentDialog || !document.body.contains(currentDialog)) {
return;
}
if (e.key === "Escape") {
// Esc键等同于点击取消按钮
e.preventDefault();
e.stopPropagation();
cancelBtn.onclick();
} else if (e.key === "Enter") {
// Enter键等同于点击选择按钮
e.preventDefault();
e.stopPropagation();
selectBtn.onclick();
} else if (e.key === "ArrowDown") {
e.preventDefault();
e.stopPropagation();
moveSelection(1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
e.stopPropagation();
moveSelection(-1);
} else if (e.key === "PageDown") {
e.preventDefault();
e.stopPropagation();
moveSelection(getPageStep());
} else if (e.key === "PageUp") {
e.preventDefault();
e.stopPropagation();
moveSelection(-getPageStep());
} else if (e.key === "Home") {
e.preventDefault();
e.stopPropagation();
const idx = findFirstFileIndex();
selectFileAndPreview(renderedFiles[idx]);
} else if (e.key === "End") {
e.preventDefault();
e.stopPropagation();
const idx = findLastFileIndex();
selectFileAndPreview(renderedFiles[idx]);
} else if (e.key === "Delete") {
e.preventDefault();
e.stopPropagation();
deleteSelected();
}
};
// 添加键盘事件监听器到对话框元素,而不是document
dialog.addEventListener("keydown", keyHandler);
// 确保对话框可以获得焦点
dialog.tabIndex = -1;
dialog.focus();
// 初始化加载
fetchPath(currentPath);
}
function clearPreview() {
const previewContentEl = document.querySelector("#ky-preview-content");
if (previewContentEl) previewContentEl.innerHTML = "";
}
//
//
//