Files
duplicati_preview/public/modules/common.js
T

171 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { redirectToLogin, redirectToSetup } from './auth-client.js';
export function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
export function formatBytes(bytes) {
if (bytes === null || bytes === undefined || Number.isNaN(Number(bytes))) {
return 'Unknown';
}
const numeric = Number(bytes);
if (numeric === 0) {
return '0 B';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = numeric;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex += 1;
}
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}
export async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
if (response.status === 401 && window.location.pathname !== '/login.html') {
if (payload.error?.code === 'AUTH_SETUP_REQUIRED') {
redirectToSetup();
} else {
redirectToLogin();
}
}
throw new Error(`${payload.error?.code ?? 'REQUEST_FAILED'}: ${payload.error?.message ?? 'Request failed.'}`);
}
return payload;
}
export function requireQueryParam(name) {
const params = new URLSearchParams(window.location.search);
const value = params.get(name)?.trim();
if (!value) {
throw new Error(`MISSING_${name.toUpperCase()}: Query parameter "${name}" is required.`);
}
return value;
}
export function optionalQueryParam(name, fallback = '') {
const params = new URLSearchParams(window.location.search);
return params.get(name)?.trim() || fallback;
}
export function createFeedbackController(element) {
return {
clear() {
element.hidden = true;
element.className = 'feedback';
element.textContent = '';
},
set(kind, message) {
element.hidden = false;
element.className = `feedback feedback-${kind}`;
element.textContent = message;
}
};
}
export function linkToSource(sourceId) {
return `/source.html?sourceId=${encodeURIComponent(sourceId)}`;
}
export function linkToFile(sourceId, fileId) {
return `/file.html?sourceId=${encodeURIComponent(sourceId)}&id=${encodeURIComponent(fileId)}`;
}
export function inferEnhancementLabel(source) {
switch (source?.enhancement?.status) {
case 'ready':
return '已增强';
case 'running':
return '增强中';
case 'queued':
return '排队中';
case 'failed':
return '增强失败';
default:
return '未增强';
}
}
export function inferEnhancementClass(source) {
switch (source?.enhancement?.status) {
case 'ready':
return 'status-ready';
case 'running':
case 'queued':
return 'status-busy';
case 'failed':
return 'status-error';
default:
return 'status-idle';
}
}
export function renderSourceSummaryCard(source, extraActionsHtml = '') {
const title = escapeHtml(source.displayName || source.originalFilename);
const originalFilename = escapeHtml(source.originalFilename);
const matchedHint = source.matchedBackupName ? '任务名来自 Server DB' : '尚未匹配任务名';
const progress =
source.enhancement.totalVolumes > 0
? `${source.enhancement.processedVolumes}/${source.enhancement.totalVolumes}`
: '0/0';
return `
<article class="source-card">
<div class="source-main">
<div>
<p class="source-name">${title}</p>
<p class="source-meta">数据库文件:${originalFilename}</p>
<p class="source-meta">Source ID${escapeHtml(source.id)}</p>
</div>
<span class="status-pill ${inferEnhancementClass(source)}">${inferEnhancementLabel(source)}</span>
</div>
<div class="summary-chip">${escapeHtml(matchedHint)}</div>
<div class="card-grid">
<div class="stat">
<span>上传大小</span>
<strong>${formatBytes(source.fileSize)}</strong>
</div>
<div class="stat">
<span>最新快照</span>
<strong>${escapeHtml(source.latestSnapshot?.timestamp ?? '无')}</strong>
</div>
<div class="stat">
<span>目录浏览</span>
<strong>${source.canBrowse ? '可用' : '不可用'}</strong>
</div>
<div class="stat">
<span>增强进度</span>
<strong>${escapeHtml(progress)}</strong>
</div>
</div>
${extraActionsHtml}
</article>
`;
}
export function assertBrowserFeature(condition, message) {
if (!condition) {
throw new Error(message);
}
}