// Toast Notification
const Toast = {
show(message, type = 'success') {
const container = document.getElementById('toast-container') || this.createContainer();
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.style.cssText = `
background: var(--bg-surface); color: var(--text-primary);
padding: 12px 24px; border-radius: 8px; margin-top: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 14px;
display: flex; align-items: center; gap: 8px;
transform: translateY(-20px); opacity: 0; transition: all 0.3s;
border-left: 4px solid ${type === 'error' ? 'var(--danger-color)' : 'var(--success-color)'};
`;
// Icon
const icon = type === 'error'
? ''
: '';
toast.innerHTML = `${icon}${message}`;
container.appendChild(toast);
// Animate in
requestAnimationFrame(() => {
toast.style.transform = 'translateY(0)';
toast.style.opacity = '1';
});
// Auto remove
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(-20px)';
setTimeout(() => toast.remove(), 300);
}, 3000);
},
createContainer() {
const div = document.createElement('div');
div.id = 'toast-container';
div.style.cssText = `
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
z-index: 2100; display: flex; flex-direction: column; align-items: center;
`;
document.body.appendChild(div);
return div;
}
};
// Modal System
const Modal = {
init() {
if (document.getElementById('app-modal-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'app-modal-overlay';
overlay.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); z-index: 2000;
display: none; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.2s;
`;
const card = document.createElement('div');
card.id = 'app-modal-card';
card.style.cssText = `
background: var(--bg-surface); width: 90%; max-width: 320px;
border-radius: var(--radius-lg); padding: 24px;
box-shadow: var(--shadow-md); transform: scale(0.95);
transition: transform 0.2s;
`;
card.innerHTML = `
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
this.overlay = overlay;
this.card = card;
this.titleEl = card.querySelector('#modal-title');
this.msgEl = card.querySelector('#modal-msg');
this.cancelBtn = card.querySelector('#modal-cancel');
this.confirmBtn = card.querySelector('#modal-confirm');
},
confirm(title, message) {
this.init();
return new Promise((resolve) => {
this.titleEl.textContent = title;
this.msgEl.textContent = message;
this.overlay.style.display = 'flex';
// Force reflow
this.overlay.offsetHeight;
this.overlay.style.opacity = '1';
this.card.style.transform = 'scale(1)';
const close = (result) => {
this.overlay.style.opacity = '0';
this.card.style.transform = 'scale(0.95)';
setTimeout(() => {
this.overlay.style.display = 'none';
resolve(result);
}, 200);
};
this.confirmBtn.onclick = () => close(true);
this.cancelBtn.onclick = () => close(false);
this.overlay.onclick = (e) => {
if (e.target === this.overlay) close(false);
};
});
}
};
// 通用工具
const Utils = {
async copy(text) {
// 如果 text 是相对路径,自动转换为完整 URL
if (text.startsWith('/')) {
text = window.location.origin + text;
}
const successToast = () => Toast.show('已复制到剪贴板');
const failToast = () => Toast.show('复制失败,请手动复制', 'error');
// 优先使用 navigator.clipboard
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
successToast();
return true;
} catch (err) {
console.warn('Clipboard API failed, trying fallback...', err);
}
}
// Fallback: document.execCommand('copy')
try {
const textarea = document.createElement('textarea');
textarea.value = text;
// 必须可见才能被 select,使用 fixed 移出可视区但保持渲染
textarea.style.position = 'fixed';
textarea.style.left = '0';
textarea.style.top = '0';
textarea.style.opacity = '0.01';
textarea.style.pointerEvents = 'none';
textarea.setAttribute('readonly', ''); // 防止软键盘弹出
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textarea);
if (successful) {
successToast();
return true;
}
} catch (fallbackErr) {
console.error('Fallback copy failed:', fallbackErr);
}
// Final fallback: Modal with text
try {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5); z-index: 9999;
display: flex; align-items: center; justify-content: center;
`;
const content = document.createElement('div');
content.style.cssText = `
background: var(--bg-surface); padding: 24px; border-radius: 12px;
width: 90%; max-width: 320px; text-align: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
content.innerHTML = `
复制失败,请长按手动复制:
`;
modal.appendChild(content);
document.body.appendChild(modal);
const closeBtn = content.querySelector('button');
const close = () => modal.remove();
closeBtn.onclick = close;
modal.onclick = (e) => { if(e.target === modal) close(); };
} catch (e) {}
return false;
},
setLoading(btn, isLoading) {
if (!btn) return;
if (isLoading) {
btn.dataset.originalText = btn.innerHTML;
btn.innerHTML = ` 处理中...`;
btn.classList.add('loading');
btn.disabled = true;
} else {
btn.innerHTML = btn.dataset.originalText || btn.innerHTML;
btn.classList.remove('loading');
btn.disabled = false;
}
}
};
// 复制文件链接的辅助函数
window.copyLink = (shortId, fileId, filename) => {
// 回滚:只生成 /d/{id},不带文件名/slug
const id = (shortId && shortId !== 'None' && shortId !== '') ? shortId : fileId;
const path = `/d/${id}`;
Utils.copy(path);
};
// 认证系统
const Auth = {
async logout() {
const confirmed = await Modal.confirm('退出登录', '确定要退出当前账号吗?');
if (!confirmed) return;
try {
const res = await fetch('/api/auth/logout', {
method: 'POST',
// 确保携带凭证(Cookies)
credentials: 'include'
});
if (res.ok) {
// 清理可能存在的本地状态
// localStorage.removeItem('some_key');
// 强制跳转到登录页,并替换历史记录,防止后退
window.location.replace('/login');
} else {
Toast.show('退出失败,请刷新重试', 'error');
}
} catch (e) {
console.error(e);
Toast.show('网络错误', 'error');
}
}
};
// Theme System
const Theme = {
init() {
const pref = localStorage.getItem('tgstate_theme_pref') || 'auto';
this.apply(pref);
},
apply(mode) {
let theme = mode;
if (mode === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', theme);
// Update Toggle Button Text/Icon if needed (optional)
const label = document.querySelector('.theme-label');
if (label) {
label.textContent = mode === 'auto' ? '跟随系统' : (mode === 'dark' ? '深色模式' : '浅色模式');
}
},
cycle() {
const current = localStorage.getItem('tgstate_theme_pref') || 'auto';
const next = current === 'auto' ? 'light' : (current === 'light' ? 'dark' : 'auto');
localStorage.setItem('tgstate_theme_pref', next);
this.apply(next);
const modeNames = { 'auto': '跟随系统', 'light': '浅色模式', 'dark': '深色模式' };
Toast.show(`已切换到${modeNames[next]}`);
}
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
Theme.init();
// 绑定主题切换按钮点击事件
document.querySelectorAll('.theme-toggle-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
Theme.cycle();
});
});
// 侧边栏/移动端菜单切换
const toggleBtn = document.querySelector('.menu-toggle');
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (toggleBtn && sidebar) {
toggleBtn.addEventListener('click', () => {
sidebar.classList.toggle('active');
if (overlay) overlay.classList.toggle('active');
});
}
if (overlay) {
overlay.addEventListener('click', () => {
sidebar.classList.remove('active');
overlay.classList.remove('active');
});
}
});
// Expose to window for inline onclick handlers
window.Theme = Theme;
window.Auth = Auth;
window.Utils = Utils;
window.Modal = Modal;
window.Toast = Toast;