// UI logic for NodeCrypt web client
// NodeCrypt 网页客户端的 UI 逻辑
import {
createAvatarSVG
} from './util.avatar.js';
import {
roomsData,
activeRoomIndex,
togglePrivateChat,
exitRoom
} from './room.js';
import {
escapeHTML
} from './util.string.js';
import {
$id
} from './util.dom.js';
import {
closeSettingsPanel
} from './util.settings.js';
import {
t
} from './util.i18n.js';
import {
updateChatInputStyle
} from './chat.js';
// Utility functions for security and error handling
// 安全和错误处理工具函数
// Simple encryption/decryption using base64 and character shifting
// 使用base64和字符偏移的简单加密/解密
function simpleEncrypt(text) {
if (!text) return '';
// Convert to base64 and shift characters
const base64 = btoa(unescape(encodeURIComponent(text)));
return base64.split('').map(char => {
const code = char.charCodeAt(0);
return String.fromCharCode(code + 3);
}).join('');
}
function simpleDecrypt(encrypted) {
if (!encrypted) return '';
try {
// Reverse character shifting and decode base64
const shifted = encrypted.split('').map(char => {
const code = char.charCodeAt(0);
return String.fromCharCode(code - 3);
}).join('');
return decodeURIComponent(escape(atob(shifted)));
} catch (error) {
console.warn('Failed to decrypt data:', error);
return '';
}
}
// Validate room data
// 验证房间数据
function validateRoomData(roomData) {
if (!roomData) {
return { valid: false, error: 'No room data available' };
}
if (!roomData.roomName || roomData.roomName.trim() === '') {
return { valid: false, error: 'Room name is required' };
}
return { valid: true };
}
// Copy text to clipboard with fallback
// 复制文本到剪贴板(含降级处理)
function copyToClipboard(text, successMessage = t('action.copied', 'Copied to clipboard!'), errorPrefix = t('action.copy_failed', 'Copy failed, url:')) {
if (!text) {
window.addSystemMsg && window.addSystemMsg(t('action.nothing_to_copy', 'Nothing to copy'));
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
window.addSystemMsg && window.addSystemMsg(successMessage);
}).catch((error) => {
console.error('Clipboard write failed:', error);
showFallbackCopy(text, errorPrefix);
});
} else {
showFallbackCopy(text, errorPrefix);
}
}
// Show fallback copy method
// 显示降级复制方法
function showFallbackCopy(text, prefix) {
if (typeof prompt === 'function') {
prompt(prefix, text);
} else {
// For environments where prompt is not available
window.addSystemMsg && window.addSystemMsg(t('action.copy_not_supported', 'Copy not supported in this environment'));
}
}
// Execute menu action with error handling
// 执行菜单操作并处理错误
function executeMenuAction(action, closeMenuCallback) {
try {
switch (action) {
case 'share':
handleShareAction();
break;
case 'exit':
handleExitAction();
break;
default:
console.warn('Unknown menu action:', action);
}
} catch (error) {
console.error('Menu action failed:', error);
window.addSystemMsg && window.addSystemMsg(t('action.action_failed', 'Action failed. Please try again.'));
} finally {
closeMenuCallback && closeMenuCallback();
}
}
// Handle share action
// 处理分享操作
function handleShareAction() {
const validation = validateRoomData(roomsData[activeRoomIndex]);
if (!validation.valid) {
window.addSystemMsg && window.addSystemMsg(`${t('action.cannot_share', 'Cannot share:')} ${validation.error}`);
return;
}
const rd = roomsData[activeRoomIndex];
const roomName = rd.roomName.trim();
const password = rd.password || '';
// Encrypt room name and password
const encryptedRoom = simpleEncrypt(roomName);
const encryptedPwd = password ? simpleEncrypt(password) : '';
// Create share URL with encrypted data
let url = `${location.origin}${location.pathname}?r=${encodeURIComponent(encryptedRoom)}`;
if (encryptedPwd) {
url += `&p=${encodeURIComponent(encryptedPwd)}`;
}
copyToClipboard(url, t('action.share_copied', 'Share link copied!'), t('action.copy_url_failed', 'Copy failed, url:'));
}
// Handle exit action
// 处理退出操作
function handleExitAction() {
try {
const result = exitRoom();
if (!result) {
location.reload();
}
} catch (error) {
console.error('Exit room failed:', error);
// Force reload as fallback
location.reload();
}
}
// Render the main header
// 渲染主标题栏
export function renderMainHeader() {
const rd = roomsData[activeRoomIndex];
let roomName = rd ? rd.roomName : 'Room';
let onlineCount = rd && rd.userList ? rd.userList.length : 0;
if (rd && !rd.userList.some(u => u.clientId === rd.myId)) {
onlineCount += 1
}
const safeRoomName = escapeHTML(roomName);
$id("main-header").innerHTML = `
#${safeRoomName}
${onlineCount} ${t('ui.members', 'members')} `;
setupMoreBtnMenu();
setupMobileUIHandlers()
}
// Setup mobile UI event handlers
// 设置移动端 UI 事件处理
export function setupMobileUIHandlers() {
const sidebar = document.getElementById('sidebar');
const rightbar = document.getElementById('rightbar');
const settingsSidebar = document.getElementById('settings-sidebar');
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileInfoBtn = document.getElementById('mobile-info-btn');
const sidebarMask = document.getElementById('mobile-sidebar-mask');
const rightbarMask = document.getElementById('mobile-rightbar-mask');
function isMobile() {
return window.innerWidth <= 768
}
function updateMobileBtnDisplay() {
if (isMobile()) {
if (mobileMenuBtn) mobileMenuBtn.style.display = 'flex';
if (mobileInfoBtn) mobileInfoBtn.style.display = 'flex'
} else {
if (mobileMenuBtn) mobileMenuBtn.style.display = 'none';
if (mobileInfoBtn) mobileInfoBtn.style.display = 'none';
if (sidebar) sidebar.classList.remove('mobile-open');
if (rightbar) rightbar.classList.remove('mobile-open');
if (sidebarMask) sidebarMask.classList.remove('active');
if (rightbarMask) rightbarMask.classList.remove('active')
}
}
updateMobileBtnDisplay();
window.addEventListener('resize', updateMobileBtnDisplay);
if (mobileMenuBtn && sidebar && sidebarMask) {
mobileMenuBtn.onclick = function(e) {
e.stopPropagation();
sidebar.classList.add('mobile-open');
sidebarMask.classList.add('active')
}; sidebarMask.onclick = function() {
// Check if settings sidebar is open
if (settingsSidebar && settingsSidebar.classList.contains('mobile-open')) {
closeSettingsPanel();
} else {
sidebar.classList.remove('mobile-open');
sidebarMask.classList.remove('active');
}
}
}
if (mobileInfoBtn && rightbar && rightbarMask) {
mobileInfoBtn.onclick = function(e) {
e.stopPropagation();
rightbar.classList.add('mobile-open');
rightbarMask.classList.add('active')
};
rightbarMask.onclick = function() {
rightbar.classList.remove('mobile-open');
rightbarMask.classList.remove('active')
}
} // Consolidated click event listener for closing sidebars
document.addEventListener('click', function(ev) {
const settingsBtn = $id('settings-btn');
const isSettingsButtonClick = settingsBtn && settingsBtn.contains(ev.target);
const isSettingsBackButtonClick = $id('settings-back-btn') && $id('settings-back-btn').contains(ev.target);
// Close settings sidebar if open and click is outside (and not on the open button or back button)
if (settingsSidebar && (settingsSidebar.classList.contains('open') || settingsSidebar.classList.contains('mobile-open'))) {
if (!settingsSidebar.contains(ev.target) && !isSettingsButtonClick && !isSettingsBackButtonClick) {
closeSettingsPanel();
}
}
if (isMobile()) {
// Mobile-specific logic
if (sidebar && sidebar.classList.contains('mobile-open')) {
if (!sidebar.contains(ev.target) && ev.target !== mobileMenuBtn) {
sidebar.classList.remove('mobile-open');
if (sidebarMask) sidebarMask.classList.remove('active');
}
}
if (settingsSidebar && settingsSidebar.classList.contains('mobile-open')) {
// 检查点击目标是否为设置按钮本身
const isSettingsButton = settingsBtn && settingsBtn.contains(ev.target);
if (!settingsSidebar.contains(ev.target) && !isSettingsButton) {
closeSettingsPanel();
}
}
if (rightbar && rightbar.classList.contains('mobile-open')) {
if (!rightbar.contains(ev.target) && ev.target !== mobileInfoBtn) {
rightbar.classList.remove('mobile-open');
if (rightbarMask) rightbarMask.classList.remove('active');
}
}
} else {
// Desktop-specific logic
// 如果设置侧边栏打开,并且点击位置在侧边栏外部且不是设置按钮本身
if (settingsSidebar && settingsSidebar.classList.contains('open')) {
const isSettingsButton = settingsBtn && settingsBtn.contains(ev.target);
if (!settingsSidebar.contains(ev.target) && !isSettingsButton) {
closeSettingsPanel();
}
}
}
})
}
// Render the user/member list
// 渲染用户/成员列表
export function renderUserList(updateHeader = false) {
const userListEl = $id('member-list');
if (!userListEl) return;
userListEl.innerHTML = '';
const rd = roomsData[activeRoomIndex];
if (!rd) return;
const me = rd.userList.find(u => u.clientId === rd.myId);
const others = rd.userList.filter(u => u.clientId !== rd.myId);
// 新增:如有其他成员,顶部插入简洁提示
if (others.length > 0) {
const tip = document.createElement('div');
tip.className = 'member-tip member-tip-center';
tip.textContent = t('ui.start_private_chat', '选择用户开始私信');
userListEl.appendChild(tip);
}
if (me) userListEl.appendChild(createUserItem(me, true));
others.forEach(u => userListEl.appendChild(createUserItem(u, false)));
if (updateHeader) {
renderMainHeader()
}
}
// Create a user list item
// 创建一个用户列表项
export function createUserItem(user, isMe) {
const div = document.createElement('div');
const rd = roomsData[activeRoomIndex];
const isPrivateTarget = rd && user.clientId === rd.privateChatTargetId;
div.className = 'member' + (isMe ? ' me' : '') + (isPrivateTarget ? ' private-chat-active' : '');
const rawName = user.userName || user.username || user.name || '';
const safeUserName = escapeHTML(rawName);
div.innerHTML = `${safeUserName}${isMe?t('ui.me', ' (me)'):''}
`;
const avatarEl = div.querySelector('.avatar');
if (avatarEl) {
const svg = createAvatarSVG(rawName);
const cleanSvg = svg.replace(/