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 '
正在计算这个 source 的默认 WebDAV 目标...
';
}
return `
任务默认 URL${escapeHtml(webdav.effectiveWebdavBaseUrl ?? webdav.derivedWebdavBaseUrl ?? '未推导到')}
默认 URL 来源${escapeHtml(webdav.effectiveWebdavBaseUrlSource ?? 'none')}
全局默认值${webdav.globalDefaultsConfigured ? '已配置' : '未配置'}
当前 source 覆盖${webdav.sourceOverrideSaved ? '已保存' : '未保存'}
`;
}
function renderEnhanceTab() {
return `
增强会优先使用全局 WebDAV 默认值,并尽量从 Server DB 推导目标 URL。这里只填与全局不同的部分;全部留空并保存会清掉当前 source 覆盖。
${webdavHint()}
`;
}
function renderThumbnailsTab() {
const cache = source?.thumbnailCache ?? { count: 0, totalBytes: 0 };
return `
这里展示当前 source 已缓存到后端的预览图数量。清空后,视频会退回占位图,后续再次成功预览时会自动重建。
预览图数量${escapeHtml(String(cache.count))}
总占用空间${formatBytes(cache.totalBytes)}
`;
}
function renderDangerTab() {
return `
删除会移除当前任务库、本地增强副本和该 source 的预览图缓存,但不会删除 Duplicati-server.sqlite。
`;
}
function renderBody() {
if (!source) {
rootElement.innerHTML = '未找到对应数据源。
';
return;
}
let content = '';
if (activeTab === 'thumbnails') {
content = renderThumbnailsTab();
} else if (activeTab === 'danger') {
content = renderDangerTab();
} else {
content = renderEnhanceTab();
}
rootElement.innerHTML = `
${escapeHtml(source.displayName || source.originalFilename)} · 属性
${TABS.map((tab) => ``).join('')}
${content}
`;
}
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 = `${escapeHtml(error.message)}
`;
}
}
};
}