Files
duplicati_preview/public/modules/explorer/propsDialog.js
T
nanxun fcf56bcbd3 feat: rebuild frontend as file-manager style explorer SPA
将原来的三页跳转结构(控制台/工作台/文件页)重构为单页资源管理器:
左侧数据源盘符树、主区列表/网格双视图、右侧内嵌播放器与详情面板,
管理功能收进侧栏对话框。媒体引擎(media-core + media-sw)原样复用,
仅把 file.js 的播放器逻辑抽成参数驱动组件。默认路由指向 explorer。

- 新增 explorer.html/js 与 modules/explorer/ 六个组件模块
- 删除旧的 index/source/file 页面及其控制器
- styles.css 新增 explorer 布局与三档响应式断点
2026-07-04 14:53:07 +08:00

227 lines
9.3 KiB
JavaScript

import { escapeHtml, fetchJson, formatBytes } from '../common.js';
// 源属性对话框:增强配置(覆盖 WebDAV/启动增强)、预览图缓存清理、删除源。
// 移植自 source.js 的 saveEnhancementSecrets / runEnhancement / clearSourceThumbnails / deleteCurrentSource。
// deps: { feedback, onSourceChanged, onSourceDeleted, close }
const TABS = [
{ key: 'enhance', label: '增强配置' },
{ key: 'thumbnails', label: '预览图缓存' },
{ key: 'danger', label: '危险操作' }
];
export function createPropsDialog(rootElement, deps = {}) {
const { feedback } = deps;
let source = null;
let activeTab = 'enhance';
function webdavHint() {
const webdav = source?.webdav;
if (!webdav) {
return '<div class="summary-card">正在计算这个 source 的默认 WebDAV 目标...</div>';
}
return `
<div class="summary-stack">
<div class="summary-row"><span>任务默认 URL</span><strong>${escapeHtml(webdav.effectiveWebdavBaseUrl ?? webdav.derivedWebdavBaseUrl ?? '未推导到')}</strong></div>
<div class="summary-row"><span>默认 URL 来源</span><strong>${escapeHtml(webdav.effectiveWebdavBaseUrlSource ?? 'none')}</strong></div>
<div class="summary-row"><span>全局默认值</span><strong>${webdav.globalDefaultsConfigured ? '已配置' : '未配置'}</strong></div>
<div class="summary-row"><span>当前 source 覆盖</span><strong>${webdav.sourceOverrideSaved ? '已保存' : '未保存'}</strong></div>
</div>
`;
}
function renderEnhanceTab() {
return `
<p class="panel-copy">增强会优先使用全局 WebDAV 默认值,并尽量从 Server DB 推导目标 URL。这里只填与全局不同的部分;全部留空并保存会清掉当前 source 覆盖。</p>
${webdavHint()}
<form id="propsSecretForm" class="secret-form">
<label><span>Source 覆盖:WebDAV Base URL</span><input id="propsUrl" type="url" placeholder="留空则用全局默认 / Server DB 推导"></label>
<label><span>认证方式</span>
<select id="propsAuthMode">
<option value="">自动 / 继承</option>
<option value="basic">Basic</option>
<option value="anonymous">Anonymous</option>
</select>
</label>
<label><span>用户名</span><input id="propsUsername" type="text" placeholder="留空则继承全局默认"></label>
<label><span>密码</span><input id="propsPassword" type="password" placeholder="留空则继承全局默认"></label>
<label><span>备份口令</span><input id="propsPassphrase" type="password" placeholder="留空则继承全局默认"></label>
<div class="form-actions">
<button id="propsSaveSecrets" class="ghost-button" type="submit">保存覆盖</button>
<button id="propsRunEnhance" class="primary-button" type="button">按当前规则启动增强</button>
</div>
</form>
`;
}
function renderThumbnailsTab() {
const cache = source?.thumbnailCache ?? { count: 0, totalBytes: 0 };
return `
<p class="panel-copy">这里展示当前 source 已缓存到后端的预览图数量。清空后,视频会退回占位图,后续再次成功预览时会自动重建。</p>
<div class="summary-stack">
<div class="summary-row"><span>预览图数量</span><strong>${escapeHtml(String(cache.count))}</strong></div>
<div class="summary-row"><span>总占用空间</span><strong>${formatBytes(cache.totalBytes)}</strong></div>
</div>
<div class="form-actions">
<button id="propsClearThumbs" class="ghost-button danger-button" type="button">清空当前 source 预览图</button>
</div>
`;
}
function renderDangerTab() {
return `
<p class="panel-copy">删除会移除当前任务库、本地增强副本和该 source 的预览图缓存,但不会删除 Duplicati-server.sqlite。</p>
<div class="form-actions">
<button id="propsDeleteSource" class="primary-button danger-button" type="button" style="background:var(--danger)">删除这个 source</button>
</div>
`;
}
function renderBody() {
if (!source) {
rootElement.innerHTML = '<div class="empty-state-card">未找到对应数据源。</div>';
return;
}
let content = '';
if (activeTab === 'thumbnails') {
content = renderThumbnailsTab();
} else if (activeTab === 'danger') {
content = renderDangerTab();
} else {
content = renderEnhanceTab();
}
rootElement.innerHTML = `
<div class="explorer-dialog-head">
<h3 class="serif">${escapeHtml(source.displayName || source.originalFilename)} · 属性</h3>
<button class="explorer-icon-btn" type="button" data-props-close aria-label="关闭">✕</button>
</div>
<div class="tabs">
${TABS.map((tab) => `<button type="button" data-props-tab="${tab.key}" class="${tab.key === activeTab ? 'active' : ''}">${escapeHtml(tab.label)}</button>`).join('')}
</div>
<div class="explorer-manage-content">${content}</div>
`;
}
function readSecretForm() {
return {
webdavBaseUrl: rootElement.querySelector('#propsUrl')?.value.trim() ?? '',
authMode: rootElement.querySelector('#propsAuthMode')?.value ?? '',
username: rootElement.querySelector('#propsUsername')?.value.trim() ?? '',
password: rootElement.querySelector('#propsPassword')?.value ?? '',
passphrase: rootElement.querySelector('#propsPassphrase')?.value ?? ''
};
}
async function reloadSource() {
const payload = await fetchJson(`/api/sources/${encodeURIComponent(source.id)}`);
source = payload.source;
deps.onSourceChanged?.(source);
}
async function saveSecrets() {
feedback?.set('info', '正在保存当前 source 的覆盖规则...');
try {
await fetchJson(`/api/sources/${encodeURIComponent(source.id)}/secrets`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(readSecretForm())
});
await reloadSource();
renderBody();
feedback?.set('success', '覆盖规则已保存。留空项会继续继承全局默认或 Server DB 推导结果。');
} catch (error) {
feedback?.set('error', error.message);
}
}
async function runEnhancement() {
feedback?.set('info', '正在提交增强任务...');
try {
await fetchJson(`/api/sources/${encodeURIComponent(source.id)}/enhance`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(readSecretForm())
});
await reloadSource();
renderBody();
feedback?.set('success', '增强任务已开始。当前规则会合并全局默认、Server DB 推导 URL 和单源覆盖。');
} catch (error) {
feedback?.set('error', error.message);
}
}
async function clearThumbnails() {
if (!window.confirm('确定要清空这个 source 的全部预览图吗?\n\n这不会删除任务库、增强索引或 Server DB,只会删除后端缩略图缓存。')) {
return;
}
feedback?.set('info', '正在清空预览图缓存...');
try {
const payload = await fetchJson(`/api/sources/${encodeURIComponent(source.id)}/thumbnails`, { method: 'DELETE' });
await reloadSource();
renderBody();
feedback?.set('success', `预览图已清空,删除了 ${payload.cleared.removedCount} 张,共 ${formatBytes(payload.cleared.removedBytes)}。`);
} catch (error) {
feedback?.set('error', error.message);
}
}
async function deleteSource() {
const label = source.displayName || source.originalFilename || source.id;
if (!window.confirm(`确定要删除这个 source 吗?\n\n${label}\n\n这会删除任务库、本地增强副本和预览图缓存,但不会删除 Duplicati-server.sqlite。`)) {
return;
}
feedback?.set('info', `正在删除 ${label}...`);
try {
await fetchJson(`/api/sources/${encodeURIComponent(source.id)}`, { method: 'DELETE' });
deps.onSourceDeleted?.(source.id);
deps.close?.();
feedback?.set('success', `已删除 ${label}。`);
} catch (error) {
feedback?.set('error', error.message);
}
}
rootElement.addEventListener('click', (event) => {
const tab = event.target.closest('[data-props-tab]');
if (tab instanceof HTMLElement) {
activeTab = tab.dataset.propsTab;
renderBody();
return;
}
if (event.target.closest('[data-props-close]')) {
deps.close?.();
return;
}
if (event.target.closest('#propsRunEnhance')) {
void runEnhancement();
} else if (event.target.closest('#propsClearThumbs')) {
void clearThumbnails();
} else if (event.target.closest('#propsDeleteSource')) {
void deleteSource();
}
});
rootElement.addEventListener('submit', (event) => {
if (event.target.id === 'propsSecretForm') {
event.preventDefault();
void saveSecrets();
}
});
return {
async open(sourceId, tab = 'enhance') {
activeTab = TABS.some((item) => item.key === tab) ? tab : 'enhance';
try {
const payload = await fetchJson(`/api/sources/${encodeURIComponent(sourceId)}`);
source = payload.source;
renderBody();
} catch (error) {
source = null;
rootElement.innerHTML = `<div class="empty-state-card">${escapeHtml(error.message)}</div>`;
}
}
};
}