Add toast notification functionality and enhance user feedback in modal interactions

This commit is contained in:
Will Miao
2025-01-31 09:37:04 +08:00
parent 2dff60d367
commit 94283d9930
2 changed files with 150 additions and 25 deletions

View File

@@ -608,4 +608,70 @@ body.modal-open {
.close:hover {
opacity: 1;
}
/* Toast Notifications */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--lora-surface);
color: var(--text-color);
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: calc(var(--z-overlay) + 10); /* 确保 toast 显示在模态窗口之上 */
opacity: 0;
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
text-align: center;
max-width: 90%;
backdrop-filter: blur(8px);
border: 1px solid var(--lora-border);
}
/* 当模态窗口打开时的 toast 样式 */
body.modal-open .toast {
bottom: 50% !important; /* 强制覆盖默认位置 */
transform: translate(-50%, 50%) !important; /* 强制覆盖默认变换 */
background: var(--lora-accent);
color: white;
z-index: 9999; /* 确保显示在最上层 */
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* 确保在模态窗口打开时,不同类型的 toast 依然可辨识 */
body.modal-open .toast-success {
background: oklch(65% 0.2 142); /* 绿色 */
}
body.modal-open .toast-error {
background: oklch(65% 0.2 29); /* 红色 */
}
body.modal-open .toast-info {
background: oklch(65% 0.2 256); /* 蓝色 */
}
.toast-success {
border-left: 4px solid #4caf50;
}
.toast-error {
border-left: 4px solid #f44336;
}
.toast-info {
border-left: 4px solid #2196f3;
}
/* Ensure toasts are visible in both themes */
[data-theme="dark"] .toast {
background: var(--lora-surface);
color: var(--lora-text);
}

View File

@@ -130,6 +130,32 @@ class ModalManager {
if (e.target === this.modal) this.close();
}
}
// 添加 toast 通知功能
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
// 移除任何现有的 toast
document.querySelectorAll('.toast').forEach(t => t.remove());
document.body.appendChild(toast);
// 如果模态窗口打开,调整 toast 位置
if (document.body.classList.contains('modal-open')) {
toast.style.transform = 'translate(-50%, 50%)'; // 在屏幕中间显示
}
// 触发动画
requestAnimationFrame(() => {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 2000);
});
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
@@ -351,8 +377,32 @@ document.querySelectorAll('.lora-card').forEach(card => {
});
});
// 更新卡片复制操作
document.querySelectorAll('.lora-card').forEach(card => {
const copyBtn = card.querySelector('.fa-copy');
if (copyBtn) {
copyBtn.onclick = (event) => {
event.stopPropagation();
navigator.clipboard.writeText(card.dataset.file_name)
.then(() => showToast('Model name copied to clipboard', 'success'))
.catch(() => showToast('Failed to copy model name', 'error'));
};
}
// 为没有元数据的卡片添加点击反馈
card.addEventListener('click', () => {
const meta = JSON.parse(card.dataset.meta || '{}');
if (Object.keys(meta).length === 0) {
showToast('This model is not available on Civitai. No additional information to display.', 'info');
}
});
});
function showModal(lora) {
const modal = document.getElementById('loraModal');
const escapedWords = lora.trainedWords?.length ?
lora.trainedWords.join(', ').toUpperCase().replace(/'/g, '\\\'') : '';
modal.innerHTML = `
<div class="modal-content">
<h2>${lora.model.name}</h2>
@@ -362,9 +412,9 @@ function showModal(lora) {
<div class="description">About this version: ${lora.description ? lora.description : 'N/A'}</div>
<div class="trigger-words">
<strong>Trigger Words:</strong>
<span class="word-list">${lora.trainedWords?.length ? lora.trainedWords.join(', ').toUpperCase() : 'N/A'}</span>
${lora.trainedWords?.length ? `
<button class="copy-btn" onclick="navigator.clipboard.writeText('${lora.trainedWords.join(', ').toUpperCase()}')">
<span class="word-list">${escapedWords || 'N/A'}</span>
${escapedWords ? `
<button class="copy-btn" onclick="copyTriggerWords(\`${escapedWords}\`)">
<svg width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
@@ -377,24 +427,47 @@ function showModal(lora) {
<button class="close" onclick="closeModal()">&times;</button>
</div>
`;
modal.style.display = 'block';
document.body.classList.add('modal-open');
// 添加点击事件监听器
modal.onclick = function (event) {
// 如果点击的是模态窗口的背景(不是内容区域),则关闭模态窗口
if (event.target === modal) {
closeModal();
}
};
}
function closeModal() {
const modal = document.getElementById('loraModal');
modal.style.display = 'none';
document.body.classList.remove('modal-open');
// 移除点击事件监听器
modal.onclick = null;
function copyTriggerWords(words) {
if (!words) return;
navigator.clipboard.writeText(words)
.then(() => {
const toast = document.createElement('div');
toast.className = 'toast toast-success';
toast.textContent = 'Trigger words copied to clipboard';
document.body.appendChild(toast);
// Force recalculation of toast position for modal context
toast.style.position = 'fixed';
toast.style.zIndex = '9999'; // 确保显示在最上层
toast.style.bottom = '50%';
toast.style.transform = 'translate(-50%, 50%)';
requestAnimationFrame(() => {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 2000);
});
})
.catch(() => {
const toast = document.createElement('div');
toast.className = 'toast toast-error';
toast.textContent = 'Failed to copy trigger words';
// ... 相同的 toast 显示逻辑
});
}
// WebSocket handling for progress updates
@@ -512,8 +585,6 @@ function initTheme() {
// 键盘导航
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
if (e.key === 'ArrowLeft') prevImage();
if (e.key === 'ArrowRight') nextImage();
});
// 图片预加载
@@ -569,18 +640,6 @@ async function fetchCivitai() {
file_path: filePath
})
});
// if(!response.ok) {
// const errorText = await response.text();
// throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
// }
// // Optional: Update the card with new metadata
// const result = await response.json();
// if (result.success && result.metadata) {
// card.dataset.meta = JSON.stringify(result.metadata);
// // Update card display if needed
// }
}
// Completion handling