531 lines
15 KiB
JavaScript
531 lines
15 KiB
JavaScript
// Import DOM utility functions
|
|
// 导入 DOM 工具函数
|
|
import {
|
|
$,
|
|
$$,
|
|
$id,
|
|
createElement,
|
|
on,
|
|
off,
|
|
addClass,
|
|
removeClass
|
|
} from './util.dom.js';
|
|
|
|
// Import theme utilities
|
|
// 导入主题工具函数
|
|
import { THEMES, getCurrentTheme, applyTheme } from './util.theme.js';
|
|
|
|
// Import i18n utilities
|
|
// 导入国际化工具函数
|
|
import { t, setLanguage, getCurrentLanguage, initI18n } from './util.i18n.js';
|
|
// Default settings
|
|
// 默认设置
|
|
const DEFAULT_SETTINGS = {
|
|
notify: false,
|
|
sound: false,
|
|
theme: 'theme1'
|
|
// 注意:我们不设置默认语言,让系统自动检测浏览器语言
|
|
// Note: We don't set a default language, let the system auto-detect browser language
|
|
};
|
|
|
|
// Load settings from localStorage
|
|
// 从 localStorage 加载设置
|
|
function loadSettings() {
|
|
let s = localStorage.getItem('settings');
|
|
try {
|
|
s = s ? JSON.parse(s) : {}
|
|
} catch {
|
|
s = {}
|
|
}
|
|
return {
|
|
...DEFAULT_SETTINGS,
|
|
...s
|
|
}
|
|
}
|
|
|
|
// Save settings to localStorage
|
|
// 保存设置到 localStorage
|
|
function saveSettings(settings) {
|
|
const {
|
|
notify,
|
|
sound,
|
|
theme,
|
|
language
|
|
} = settings;
|
|
localStorage.setItem('settings', JSON.stringify({
|
|
notify,
|
|
sound,
|
|
theme,
|
|
language
|
|
}))
|
|
}
|
|
|
|
// Apply settings to the document
|
|
// 应用设置到文档
|
|
function applySettings(settings) {
|
|
// Initialize i18n with current language setting
|
|
// 根据当前语言设置初始化国际化
|
|
initI18n(settings);
|
|
}
|
|
|
|
// Ask for browser notification permission
|
|
// 请求浏览器通知权限
|
|
function askNotificationPermission(callback) {
|
|
if (Notification.requestPermission.length === 0) {
|
|
Notification.requestPermission().then(callback)
|
|
} else {
|
|
Notification.requestPermission(callback)
|
|
}
|
|
}
|
|
|
|
// Setup the settings panel UI
|
|
// 设置设置面板 UI
|
|
function setupSettingsPanel() {
|
|
const settingsSidebar = $id('settings-sidebar');
|
|
const settingsContent = $id('settings-content');
|
|
const settingsTitle = $id('settings-title');
|
|
if (!settingsSidebar || !settingsContent) return;
|
|
|
|
const settings = loadSettings();
|
|
|
|
// Update settings title
|
|
// 更新设置标题
|
|
if (settingsTitle) {
|
|
settingsTitle.textContent = t('settings.title', 'Settings');
|
|
}// Create settings content HTML
|
|
settingsContent.innerHTML = `
|
|
<div class="settings-section">
|
|
<div class="settings-section-title">${t('settings.notification', 'Notification Settings')}</div>
|
|
<div class="settings-item">
|
|
<div class="settings-item-label">
|
|
<div>${t('settings.desktop_notifications', 'Desktop Notifications')}</div>
|
|
</div>
|
|
<label class="switch">
|
|
<input type="checkbox" id="settings-notify" ${settings.notify ? 'checked' : ''}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="settings-item">
|
|
<div class="settings-item-label">
|
|
<div>${t('settings.sound_notifications', 'Sound Notifications')}</div>
|
|
</div>
|
|
<label class="switch">
|
|
<input type="checkbox" id="settings-sound" ${settings.sound ? 'checked' : ''}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="settings-section">
|
|
<div class="settings-section-title">${t('settings.language', 'Language Settings')}</div>
|
|
<div class="settings-item">
|
|
<div class="settings-item-label">
|
|
<div>${t('settings.language_switch', 'Language')}</div>
|
|
</div>
|
|
<div class="language-selector">
|
|
<select id="settings-language" class="language-select">
|
|
<option value="en" ${settings.language === 'en' ? 'selected' : ''}>🇺🇸 English</option>
|
|
<option value="zh" ${settings.language === 'zh' ? 'selected' : ''}>🇨🇳 中文</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<div class="settings-section-title">${t('settings.theme', 'Theme Settings')}</div>
|
|
<div class="theme-selector" id="theme-selector">
|
|
${THEMES.map(theme => `
|
|
<div class="theme-item ${settings.theme === theme.id ? 'active' : ''}" data-theme-id="${theme.id}" style="background: ${theme.background}; background-size: cover; background-position: center;">
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`; const notifyCheckbox = $('#settings-notify', settingsContent);
|
|
const soundCheckbox = $('#settings-sound', settingsContent);
|
|
const languageSelect = $('#settings-language', settingsContent);
|
|
|
|
// Language select event handler
|
|
// 语言选择事件处理
|
|
on(languageSelect, 'change', e => {
|
|
const newLanguage = e.target.value;
|
|
settings.language = newLanguage;
|
|
|
|
// Set language immediately
|
|
// 立即设置语言
|
|
setLanguage(newLanguage);
|
|
|
|
// Save settings
|
|
// 保存设置
|
|
saveSettings(settings);
|
|
applySettings(settings);
|
|
|
|
// Refresh the settings panel to show updated translations
|
|
// 刷新设置面板以显示更新的翻译
|
|
setTimeout(() => {
|
|
setupSettingsPanel();
|
|
}, 100);
|
|
});
|
|
|
|
on(notifyCheckbox, 'change', e => {
|
|
const checked = e.target.checked;
|
|
if (checked) {
|
|
if (!('Notification' in window)) {
|
|
alert('Notifications are not supported by your browser.');
|
|
e.target.checked = false;
|
|
return
|
|
}
|
|
askNotificationPermission(permission => {
|
|
if (permission === 'granted') {
|
|
settings.notify = true;
|
|
settings.sound = false;
|
|
if (soundCheckbox) soundCheckbox.checked = false;
|
|
saveSettings(settings);
|
|
applySettings(settings); // 防止重复通知,添加一个标志位
|
|
if (!settingsSidebar._notificationShown) {
|
|
new Notification('Notifications enabled', {
|
|
body: 'You will receive alerts here.'
|
|
});
|
|
settingsSidebar._notificationShown = true; // 设置标志位
|
|
}
|
|
} else {
|
|
settings.notify = false;
|
|
e.target.checked = false;
|
|
saveSettings(settings);
|
|
applySettings(settings);
|
|
alert('Please allow notifications in your browser settings.')
|
|
}
|
|
})
|
|
} else {
|
|
settings.notify = false;
|
|
saveSettings(settings);
|
|
applySettings(settings); // 重置标志位
|
|
if (settingsSidebar._notificationShown) {
|
|
settingsSidebar._notificationShown = false;
|
|
}
|
|
}
|
|
}); on(soundCheckbox, 'change', e => {
|
|
settings.sound = e.target.checked;
|
|
if (settings.sound) {
|
|
settings.notify = false;
|
|
if (notifyCheckbox) notifyCheckbox.checked = false;
|
|
}
|
|
saveSettings(settings);
|
|
applySettings(settings)
|
|
});
|
|
// Theme selection event handlers
|
|
// 主题选择事件处理
|
|
const themeSelector = $('#theme-selector', settingsContent);
|
|
if (themeSelector) {
|
|
// Custom scrolling functionality
|
|
// 自定义滚动功能
|
|
let isDragging = false;
|
|
let startX = 0;
|
|
let scrollLeft = 0;
|
|
|
|
// Mouse wheel scrolling (vertical -> horizontal)
|
|
// 鼠标滚轮滚动(垂直转水平)
|
|
on(themeSelector, 'wheel', e => {
|
|
e.preventDefault();
|
|
const scrollAmount = e.deltaY * 0.5; // Adjust scroll sensitivity
|
|
themeSelector.scrollLeft += scrollAmount;
|
|
});
|
|
// Mouse drag scrolling
|
|
// 鼠标拖拽滚动
|
|
let dragStartTime = 0;
|
|
let hasDragged = false;
|
|
|
|
on(themeSelector, 'mousedown', e => {
|
|
isDragging = true;
|
|
hasDragged = false;
|
|
dragStartTime = Date.now();
|
|
startX = e.pageX - themeSelector.offsetLeft;
|
|
scrollLeft = themeSelector.scrollLeft;
|
|
themeSelector.classList.add('dragging');
|
|
e.preventDefault(); // Prevent text selection
|
|
});
|
|
on(document, 'mousemove', e => {
|
|
if (!isDragging) return;
|
|
e.preventDefault();
|
|
const x = e.pageX - themeSelector.offsetLeft;
|
|
const walk = (x - startX) * 2; // Scroll speed multiplier
|
|
const moved = Math.abs(walk);
|
|
|
|
// If moved more than 5px, consider it a drag
|
|
if (moved > 5) {
|
|
hasDragged = true;
|
|
}
|
|
|
|
themeSelector.scrollLeft = scrollLeft - walk;
|
|
});
|
|
|
|
on(document, 'mouseup', () => {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
themeSelector.classList.remove('dragging');
|
|
}
|
|
});
|
|
// Touch support for mobile
|
|
// 移动端触摸支持
|
|
let touchStartX = 0;
|
|
let touchScrollLeft = 0;
|
|
let touchStartTime = 0;
|
|
let touchHasMoved = false;
|
|
|
|
on(themeSelector, 'touchstart', e => {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchScrollLeft = themeSelector.scrollLeft;
|
|
touchStartTime = Date.now();
|
|
touchHasMoved = false;
|
|
});
|
|
|
|
on(themeSelector, 'touchmove', e => {
|
|
e.preventDefault();
|
|
const touchX = e.touches[0].clientX;
|
|
const walk = (touchStartX - touchX) * 1.5; // Touch scroll sensitivity
|
|
|
|
// If moved more than 10px, consider it a swipe
|
|
if (Math.abs(walk) > 10) {
|
|
touchHasMoved = true;
|
|
}
|
|
|
|
themeSelector.scrollLeft = touchScrollLeft + walk;
|
|
});
|
|
|
|
// Handle touch end for theme selection
|
|
// 处理触摸结束的主题选择
|
|
on(themeSelector, 'touchend', e => {
|
|
// If user swiped, don't trigger theme selection
|
|
// 如果用户滑动过,不触发主题选择
|
|
if (touchHasMoved) {
|
|
touchHasMoved = false;
|
|
return;
|
|
}
|
|
|
|
// Check if it was a quick tap
|
|
// 检查是否是快速点击
|
|
const tapDuration = Date.now() - touchStartTime;
|
|
if (tapDuration > 300) {
|
|
return;
|
|
}
|
|
|
|
const themeItem = e.target.closest('.theme-item');
|
|
if (themeItem) {
|
|
const themeId = themeItem.dataset.themeId;
|
|
if (themeId && themeId !== settings.theme) {
|
|
// Update active state
|
|
$$('.theme-item', themeSelector).forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
themeItem.classList.add('active');
|
|
|
|
// Apply theme and save settings
|
|
settings.theme = themeId;
|
|
applyTheme(themeId);
|
|
saveSettings(settings);
|
|
}
|
|
}
|
|
});
|
|
// Theme selection click handler
|
|
// 主题选择点击处理器
|
|
on(themeSelector, 'click', e => {
|
|
// If user just dragged, don't trigger theme selection
|
|
// 如果用户刚刚拖拽过,不触发主题选择
|
|
if (hasDragged) {
|
|
hasDragged = false;
|
|
return;
|
|
}
|
|
|
|
// Also check if it was a quick click (less than 200ms and minimal movement)
|
|
// 同时检查是否是快速点击(少于200ms且移动很少)
|
|
const clickDuration = Date.now() - dragStartTime;
|
|
if (clickDuration > 200) {
|
|
return;
|
|
}
|
|
|
|
const themeItem = e.target.closest('.theme-item');
|
|
if (themeItem) {
|
|
const themeId = themeItem.dataset.themeId;
|
|
if (themeId && themeId !== settings.theme) {
|
|
// Update active state
|
|
$$('.theme-item', themeSelector).forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
themeItem.classList.add('active');
|
|
|
|
// Apply theme and save settings
|
|
settings.theme = themeId;
|
|
applyTheme(themeId);
|
|
saveSettings(settings);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check if device is mobile
|
|
function isMobile() {
|
|
return window.innerWidth <= 768;
|
|
}
|
|
|
|
// Open the settings panel
|
|
// 打开设置面板
|
|
function openSettingsPanel() {
|
|
const settingsSidebar = $id('settings-sidebar');
|
|
const sidebar = $id('sidebar');
|
|
const sidebarMask = $id('mobile-sidebar-mask');
|
|
|
|
if (!settingsSidebar || !sidebar) return;
|
|
|
|
if (isMobile()) {
|
|
// Mobile: hide main sidebar and show settings sidebar with mask
|
|
sidebar.classList.remove('mobile-open');
|
|
settingsSidebar.style.display = 'flex';
|
|
// Force reflow then add animation class
|
|
settingsSidebar.offsetHeight;
|
|
settingsSidebar.classList.add('mobile-open');
|
|
if (sidebarMask) {
|
|
sidebarMask.classList.add('active');
|
|
} } else {
|
|
// Desktop: show settings sidebar as overlay with slide animation
|
|
settingsSidebar.style.display = 'flex';
|
|
// Force reflow then slide in
|
|
settingsSidebar.offsetHeight;
|
|
settingsSidebar.classList.add('open');
|
|
// Keep main sidebar visible - settings sidebar is an overlay
|
|
}
|
|
|
|
// Setup settings content
|
|
setupSettingsPanel();
|
|
}
|
|
|
|
// Close the settings panel
|
|
// 关闭设置面板
|
|
function closeSettingsPanel() {
|
|
const settingsSidebar = $id('settings-sidebar');
|
|
const sidebarMask = $id('mobile-sidebar-mask'); // mobile-sidebar-mask is used for settings on mobile
|
|
|
|
if (!settingsSidebar) return;
|
|
|
|
const animationEnded = () => {
|
|
settingsSidebar.style.display = 'none';
|
|
settingsSidebar.removeEventListener('transitionend', animationEnded);
|
|
};
|
|
|
|
if (isMobile()) {
|
|
settingsSidebar.classList.remove('mobile-open');
|
|
if (sidebarMask) {
|
|
sidebarMask.classList.remove('active');
|
|
}
|
|
// Listen for transition end to set display none
|
|
settingsSidebar.addEventListener('transitionend', animationEnded);
|
|
// Fallback if transitionend doesn't fire (e.g., if no transition is defined or display:none is set too early by other means)
|
|
setTimeout(() => {
|
|
if (!settingsSidebar.classList.contains('mobile-open')) { // check if it wasn't reopened
|
|
settingsSidebar.style.display = 'none';
|
|
}
|
|
}, 350); // Slightly longer than CSS transition
|
|
} else {
|
|
settingsSidebar.classList.remove('open');
|
|
// Listen for transition end to set display none
|
|
settingsSidebar.addEventListener('transitionend', animationEnded);
|
|
// Fallback
|
|
setTimeout(() => {
|
|
if (!settingsSidebar.classList.contains('open')) { // check if it wasn't reopened
|
|
settingsSidebar.style.display = 'none';
|
|
}
|
|
}, 350);
|
|
}
|
|
}
|
|
|
|
// Initialize settings on page load
|
|
// 页面加载时初始化设置
|
|
function initSettings() {
|
|
const settings = loadSettings();
|
|
applySettings(settings);
|
|
|
|
// Apply theme from settings
|
|
// 从设置中应用主题
|
|
if (settings.theme) {
|
|
applyTheme(settings.theme);
|
|
}
|
|
|
|
// Listen for language change events to update UI
|
|
// 监听语言变更事件以更新UI
|
|
window.addEventListener('languageChange', () => {
|
|
// Update settings title if settings panel is open
|
|
// 如果设置面板已打开,更新设置标题
|
|
const settingsTitle = $id('settings-title');
|
|
if (settingsTitle) {
|
|
settingsTitle.textContent = t('settings.title', 'Settings');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Maximum notification text length
|
|
// 通知文本最大长度
|
|
const MAX_NOTIFY_TEXT_LEN = 100;
|
|
|
|
// Truncate text for notifications
|
|
// 截断通知文本
|
|
function truncateText(text) {
|
|
return text.length > MAX_NOTIFY_TEXT_LEN ? text.slice(0, MAX_NOTIFY_TEXT_LEN) + '...' : text
|
|
}
|
|
|
|
// Play sound notification
|
|
// 播放声音通知
|
|
function playSoundNotification() {
|
|
try {
|
|
const ctx = new(window.AudioContext || window.webkitAudioContext)();
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
osc.frequency.value = 1000;
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start();
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.5);
|
|
setTimeout(() => {
|
|
osc.stop();
|
|
ctx.close()
|
|
}, 600)
|
|
} catch (e) {
|
|
console.error('Sound notification failed', e)
|
|
}
|
|
}
|
|
|
|
// Show desktop notification
|
|
// 显示桌面通知
|
|
function showDesktopNotification(roomName, text, msgType, sender) {
|
|
if (!('Notification' in window) || Notification.permission !== 'granted') return;
|
|
let body;
|
|
const senderPrefix = sender ? `${sender}:` : ''; if (msgType === 'image' || msgType === 'private image') {
|
|
body = `${senderPrefix}${t('notification.image', '[image]')}`;
|
|
if (msgType === 'private image') {
|
|
body = `${t('notification.private', '(Private)')}${body}`
|
|
}
|
|
} else if (msgType === 'text' || msgType === 'private text') {
|
|
body = `${senderPrefix}${truncateText(text)}`;
|
|
if (msgType === 'private text') {
|
|
body = `${t('notification.private', '(Private)')}${body}`
|
|
}
|
|
} else {
|
|
body = truncateText(text)
|
|
}
|
|
new Notification(`#${roomName}`, {
|
|
body
|
|
})
|
|
}
|
|
|
|
// Notify message entry point
|
|
// 通知消息主入口
|
|
export function notifyMessage(roomName, msgType, text, sender) {
|
|
const settings = loadSettings();
|
|
if (settings.notify) {
|
|
showDesktopNotification(roomName, text, msgType, sender)
|
|
} else if (settings.sound) {
|
|
playSoundNotification()
|
|
}
|
|
}
|
|
export {
|
|
openSettingsPanel,
|
|
closeSettingsPanel,
|
|
initSettings
|
|
}; |