document.addEventListener('DOMContentLoaded', () => {
// --- Global Variables ---
const uploadArea = document.getElementById('upload-zone');
const fileInput = document.getElementById('file-picker');
const progressArea = document.getElementById('prog-zone');
const doneArea = document.getElementById('done-zone');
const searchInput = document.getElementById('file-search');
// --- Copy Link Delegation ---
document.addEventListener('click', (e) => {
const btn = e.target.closest('.copy-link-btn');
if (!btn) return;
// Prevent default if it's a link (though it's a button)
e.preventDefault();
e.stopPropagation();
const item = btn.closest('.file-item, .image-card');
if (!item) return; // Should exist
// 如果按钮上有 onclick 属性(旧代码或特殊情况),优先执行 onclick,这里不处理
if (btn.hasAttribute('onclick')) return;
const shortId = item.dataset.shortId;
const fileId = item.dataset.fileId;
const filename = item.dataset.filename;
// 核心策略:优先从 DOM 中获取真实可用的绝对 URL
let url = '';
// 1. 尝试获取下载按钮的链接 (文件列表模式)
// 查找 href 以 /d/ 开头的 a 标签
const downloadLink = item.querySelector('a[href^="/d/"]');
if (downloadLink && downloadLink.href) {
url = downloadLink.href;
}
// 2. 尝试获取图片的 src (图床模式)
if (!url) {
const img = item.querySelector('img[src^="/d/"]');
if (img && img.src) {
url = img.src;
}
}
// 3. Fallback: 使用 dataset 中的 fileUrl (如果存在且非空且不是 undefined 字符串)
if (!url) {
const dsUrl = item.dataset.fileUrl;
if (dsUrl && dsUrl !== 'undefined') {
url = dsUrl;
// 确保是绝对路径
if (url.startsWith('/')) {
url = window.location.origin + url;
}
}
}
// 4. Final Fallback: 构造 /d/{id}
if (!url || url.includes('undefined')) {
const id = (shortId && shortId !== 'None' && shortId !== '') ? shortId : fileId;
url = window.location.origin + `/d/${id}`;
}
// 安全检查:如果最终结果包含 undefined,强制重构
if (url.includes('undefined')) {
// 最后的兜底,哪怕 fileId 也是 undefined (极低概率),也比 http://...undefined 好
console.warn('Constructed URL contained undefined, falling back to raw fileId');
url = window.location.origin + '/d/' + (fileId || 'error');
}
if (window.copyLink) {
Utils.copy(url);
} else {
Utils.copy(url);
}
});
// --- Search Functionality ---
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
// Select both file list items and image grid cards
const items = document.querySelectorAll('.file-item, .image-card');
items.forEach(item => {
const name = (item.dataset.filename || '').toLowerCase();
if (name.includes(term)) {
item.style.display = ''; // Reset to default (grid or flex)
} else {
item.style.display = 'none';
}
});
});
}
// --- Upload Logic ---
if (uploadArea && fileInput) {
// Prevent double dialog by stopping propagation from input
fileInput.addEventListener('click', (e) => e.stopPropagation());
uploadArea.addEventListener('click', (e) => {
// Only trigger if not clicking the input itself (though propagation stop handles it, this is extra safety)
if (e.target !== fileInput) {
fileInput.click();
}
});
uploadArea.addEventListener('dragover', (event) => {
event.preventDefault();
uploadArea.style.borderColor = 'var(--primary-color)';
uploadArea.style.backgroundColor = 'var(--bg-surface-hover)';
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.style.borderColor = '';
uploadArea.style.backgroundColor = '';
});
uploadArea.addEventListener('drop', (event) => {
event.preventDefault();
uploadArea.style.borderColor = '';
uploadArea.style.backgroundColor = '';
const files = event.dataTransfer.files;
if (files.length > 0) {
handleFiles(files);
}
});
fileInput.addEventListener('change', ({ target }) => {
if (target.files.length > 0) {
handleFiles(target.files);
}
});
}
// Queue system for uploads
const uploadQueue = [];
let isUploading = false;
function handleFiles(files) {
if (progressArea) progressArea.innerHTML = '';
for (const file of files) {
uploadQueue.push(file);
}
processQueue();
}
function processQueue() {
if (isUploading || uploadQueue.length === 0) return;
isUploading = true;
const file = uploadQueue.shift();
uploadFile(file).then(() => {
isUploading = false;
processQueue();
});
}
function uploadFile(file) {
return new Promise((resolve) => {
const formData = new FormData();
formData.append('file', file, file.name);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
const fileId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
// Initial Progress UI
// 使用新版 UI 风格
const progressHTML = `
`;
if (progressArea) progressArea.insertAdjacentHTML('beforeend', progressHTML);
const progressEl = document.querySelector(`#progress-${fileId} .progress-bar`);
const percentEl = document.querySelector(`#progress-${fileId} .percent`);
xhr.upload.onprogress = ({ loaded, total }) => {
const percent = Math.floor((loaded / total) * 100);
if (progressEl) progressEl.style.width = `${percent}%`;
if (percentEl) percentEl.textContent = `${percent}%`;
};
xhr.onload = () => {
const progressRow = document.getElementById(`progress-${fileId}`);
if (progressRow) progressRow.remove();
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
const fileUrl = response.url;
// Success Toast
if (window.Toast) Toast.show(`${file.name} 上传成功`);
// Add to done area
const successHTML = `
`;
if (doneArea) doneArea.insertAdjacentHTML('afterbegin', successHTML);
} else {
let errorMsg = "上传失败";
try {
const parsed = JSON.parse(xhr.responseText);
const detail = parsed && parsed.detail;
if (typeof detail === 'string') {
errorMsg = detail;
} else if (detail && typeof detail === 'object') {
errorMsg = detail.message || errorMsg;
} else if (parsed && parsed.message) {
errorMsg = parsed.message;
}
} catch (e) {}
if (window.Toast) Toast.show(errorMsg, 'error');
}
resolve();
};
xhr.onerror = () => {
const progressRow = document.getElementById(`progress-${fileId}`);
if (progressRow) progressRow.remove();
if (window.Toast) Toast.show('网络错误', 'error');
resolve();
};
xhr.send(formData);
});
}
// --- Batch Actions ---
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const copyLinksBtn = document.getElementById('copy-links-btn');
const selectionCounter = document.getElementById('selection-counter');
const batchActionsBar = document.getElementById('batch-actions-bar');
const formatOptions = document.querySelectorAll('.format-option');
function updateBatchControls() {
const checkboxes = document.querySelectorAll('.file-checkbox');
const checked = document.querySelectorAll('.file-checkbox:checked');
const count = checked.length;
if (selectionCounter) selectionCounter.textContent = count > 0 ? `${count} 项已选` : '0 项已选';
if (batchActionsBar) {
if (count > 0) {
batchActionsBar.classList.remove('hidden');
} else {
batchActionsBar.classList.add('hidden');
}
}
if (selectAllCheckbox) selectAllCheckbox.checked = (count > 0 && count === checkboxes.length);
}
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
document.querySelectorAll('.file-checkbox').forEach(cb => {
cb.checked = e.target.checked;
});
updateBatchControls();
});
}
// Delegation for dynamic checkboxes
document.addEventListener('change', (e) => {
if (e.target.classList.contains('file-checkbox')) {
updateBatchControls();
}
});
// Format selection (Image Hosting)
if (formatOptions) {
formatOptions.forEach(opt => {
opt.addEventListener('click', () => {
formatOptions.forEach(o => o.classList.remove('active'));
opt.classList.add('active');
});
});
}
// Batch Copy
if (copyLinksBtn) {
copyLinksBtn.addEventListener('click', () => {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 0) return;
const activeFormatBtn = document.querySelector('.format-option.active');
const format = activeFormatBtn ? activeFormatBtn.dataset.format : 'url';
const links = Array.from(checked).map(cb => {
const item = cb.closest('.file-item, .image-card');
let url = '';
// 1. 尝试获取下载按钮
const downloadLink = item.querySelector('a[href^="/d/"]');
if (downloadLink && downloadLink.href) {
url = downloadLink.href;
}
// 2. 尝试获取图片 src
if (!url) {
const img = item.querySelector('img[src^="/d/"]');
if (img && img.src) {
url = img.src;
}
}
// 3. Fallback: Dataset
if (!url) {
const dsUrl = item.dataset.fileUrl;
if (dsUrl && dsUrl !== 'undefined') {
url = dsUrl;
if (url.startsWith('/')) {
url = window.location.origin + url;
}
}
}
// 4. Final Fallback
if (!url || url.includes('undefined')) {
const shortId = item.dataset.shortId;
const fileId = item.dataset.fileId;
const id = (shortId && shortId !== 'None' && shortId !== '') ? shortId : fileId;
url = window.location.origin + `/d/${id}`;
}
const name = item.dataset.filename;
if (format === 'markdown') return ``;
if (format === 'html') return `
`;
return url;
});
Utils.copy(links.join('\n'));
});
}
// Batch Delete
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', async () => {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 0) return;
const confirmed = await Modal.confirm('批量删除', `确定要删除选中的 ${checked.length} 个文件吗?`);
if (!confirmed) return;
const fileIds = Array.from(checked).map(cb => cb.dataset.fileId);
fetch('/api/batch_delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: fileIds })
})
.then(res => res.json())
.then(data => {
if (data.deleted) {
data.deleted.forEach(item => {
const id = item.details?.file_id || item;
removeFileElement(id);
});
if (window.Toast) Toast.show(`已删除 ${data.deleted.length} 个文件`);
}
updateBatchControls();
});
});
}
// --- SSE & Realtime Updates ---
const fileListContainer = document.getElementById('file-list-disk');
if (fileListContainer) {
let eventSource = null;
const connectSSE = () => {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/api/file-updates');
eventSource.onmessage = (event) => {
const msg = JSON.parse(event.data);
const action = msg && msg.action ? msg.action : 'add';
if (action === 'delete') {
removeFileElement(msg.file_id);
updateBatchControls();
return;
}
addNewFileElement(msg);
};
eventSource.onerror = () => {
try { eventSource.close(); } catch (_) {}
setTimeout(connectSSE, 5000);
};
};
connectSSE();
}
function formatDateValue(value) {
if (!value) return '';
const d = new Date(value);
if (!isNaN(d.getTime())) return d.toISOString().split('T')[0];
const s = String(value);
return s.split(' ')[0].split('T')[0];
}
function addNewFileElement(file) {
const isGridView = document.querySelector('.image-grid') !== null;
const container = document.getElementById('file-list-disk');
// Remove empty state if exists
const emptyState = container.querySelector('div[style*="text-align: center"]');
if (emptyState) emptyState.remove();
const formattedSize = (file.filesize / (1024 * 1024)).toFixed(2) + " MB";
const formattedDate = formatDateValue(file.upload_date);
const safeId = file.file_id.replace(':', '-');
// URL construction: Always use /d/{file_id} (short_id preferred)
// 回滚:只使用 /d/{id} 格式,不再拼接文件名或 slug
let fileUrl = `/d/${file.short_id || file.file_id}`;
let html = '';
if (isGridView) {
html = `
${file.filename}
${formattedSize}
`;
} else {
html = `
|
|
${formattedSize} |
${formattedDate} |
|
`;
}
container.insertAdjacentHTML('afterbegin', html);
}
// --- Global Helpers ---
window.deleteFile = async (fileId) => {
const confirmed = await Modal.confirm('删除文件', '确定要删除此文件吗?');
if (!confirmed) return;
fetch(`/api/files/${fileId}`, { method: 'DELETE' })
.then(async (res) => {
let data = null;
try { data = await res.json(); } catch (e) {}
return { ok: res.ok, data };
})
.then(({ ok, data }) => {
if (ok && data && data.status === 'ok') {
removeFileElement(fileId);
if (window.Toast) Toast.show('文件已删除');
updateBatchControls();
} else {
const msg = data?.detail?.message || data?.message || '删除失败';
if (window.Toast) Toast.show(msg, 'error');
}
});
};
function removeFileElement(fileId) {
const el = document.getElementById(`file-item-${fileId.replace(':', '-')}`);
if (el) el.remove();
// Check if empty
const container = document.getElementById('file-list-disk');
if (container && container.children.length === 0) {
// Re-render empty state logic if needed, or let user refresh
// Simple text fallback
const isGridView = document.querySelector('.image-grid') !== null;
if (isGridView) {
container.innerHTML = `
`;
} else {
container.innerHTML = `
|
暂无文件
|
`;
}
}
}
});