This commit is contained in:
shuaiplus committed 2025-06-06 21:26:31 +08:00
1 parent feca67635c
commit 1bd4a0ba73
8 files changed
+336 -126

No files matched your search

+32 -12
View File
@@ -454,6 +454,7 @@ body {
top: 50%;
transform: translateY(-50%);
transition: transform 0.2s ease, color 0.2s ease;
z-index: 5;
}
.chat-attach-btn-abs:hover {
@@ -522,30 +523,49 @@ body {
padding: 0.7em 0.7em;
border-radius: 50px;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
margin: 0px 0px 3px 10px;
transition: all 0.2s ease;
margin: 0px 0px 3px 7px;
transition: all 0.3s ease;
z-index: 10;
}
.send-message-btn:hover {
background: #fff;
transition: all 0.6s;
transform: translateY(-2px);
transition: all 0.3s ease;
}
.send-message-btn:active {
transform: scale(0.95);
box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.15);
transition: all 0.1s;
transform: scale(0.95) translateY(0px);
box-shadow: 0 0.3rem 0.8rem rgba(3, 169, 244, 0.2);
transition: all 0.1s ease;
}
.send-message-btn .text {
display: flex;
align-items: center;
justify-content: center;
fill: #fff;
transition: all 0.2s;
transition: all 0.3s ease;
}
.send-message-btn:hover .text {
fill: rgb(3, 169, 244);
transition: all 0.6s;
transform: scale(1.05);
transition: all 0.3s ease;
}
.send-message-btn:active .text {
fill: #fff;
transition: all 0.1s;
transform: scale(0.95);
transition: all 0.1s ease;
}
/* 确保发送按钮的点击区域不被其他元素遮挡 */
.send-message-btn {
position: relative;
z-index: 10;
}
/* 确保发送按钮在所有父容器中都有足够的层级 */
.chat-input-wrapper .send-message-btn {
position: relative;
z-index: 15;
}
/* 防止任何绝对定位的元素遮挡发送按钮 */
.new-message-wrapper {
position: relative;
z-index: 1;
}
+15 -6
View File
@@ -25,7 +25,7 @@ flex-direction: column;
display: flex;
flex-direction: column;
box-shadow: 2px 0 8px #e0e0e0a0;
transition: width 0.3s ease;
transition: opacity 0.3s ease;
}
.sidebar-user {
@@ -58,16 +58,25 @@ flex-direction: column;
display: block;
}
/* New settings sidebar styles */
/* Settings sidebar overlay styles */
.settings-sidebar {
width: 250px;
background: #fff;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
box-shadow: 2px 0 8px #e0e0e0a0;
transition: width 0.3s ease;
position: relative;
box-shadow: 2px 0 10px #e0e0e0a0;
position: absolute;
top: 0;
left: 0;
height: 100%;
z-index: 1000;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.settings-sidebar.open {
transform: translateX(0);
}
.settings-header {
@@ -544,7 +553,7 @@ flex-direction: column;
.chat-area {
flex: 1;
overflow-y: auto;
padding: 32px 24px 24px 24px;
padding: 15px 15px 15px 15px;
display: flex;
flex-direction: column;
background: none;
+60 -20
View File
@@ -7,18 +7,17 @@
<link rel="stylesheet" href="css/body.css">
<link rel="stylesheet" href="css/phone.css">
<link rel="stylesheet" href="css/form-animation.css">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<meta name="description" content="NodeCrypt - 真正的端到端加密聊天系统,无数据库,所有消息本地加密,服务器仅做加密数据中转,支持 Cloudflare Workers、Docker、自托管和本地开发。">
<meta name="keywords" content="端到端加密, 安全, 聊天, WebSocket, Cloudflare Workers, JavaScript, E2EE, 匿名通信, AES, ECDH, RSA, ChaCha20, 安全, 开源, NodeCrypt, shuaiplus">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg"> <meta name="description" content="NodeCrypt - True end-to-end encrypted chat system, no database, all messages encrypted locally, server only relays encrypted data, supports Cloudflare Workers, Docker, self-hosting and local development.">
<meta name="keywords" content="end-to-end encryption, security, chat, WebSocket, Cloudflare Workers, JavaScript, E2EE, anonymous communication, AES, ECDH, RSA, ChaCha20, security, open source, NodeCrypt, shuaiplus">
<meta name="author" content="shuaiplus">
<meta property="og:title" content="NodeCrypt - 端到端加密聊天系统">
<meta property="og:description" content="NodeCrypt 是一个零知识、端到端加密的开源聊天系统,所有加密解密均在客户端本地完成,服务器无法获取明文。支持多平台部署,安全、匿名、无历史消息。">
<meta property="og:title" content="NodeCrypt - End-to-End Encrypted Chat System">
<meta property="og:description" content="NodeCrypt is a zero-knowledge, end-to-end encrypted open source chat system where all encryption and decryption is done locally on the client side, and servers cannot access plaintext. Supports multi-platform deployment, secure, anonymous, no message history.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://github.com/shuaiplus/NodeCrypt">
<meta property="og:image" content="https://crypt.works/client/assets/favicon.svg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="NodeCrypt - 端到端加密聊天系统">
<meta name="twitter:description" content="NodeCrypt 是一个零知识、端到端加密的开源聊天系统,所有加密解密均在客户端本地完成,服务器无法获取明文。">
<meta name="twitter:title" content="NodeCrypt - End-to-End Encrypted Chat System">
<meta name="twitter:description" content="NodeCrypt is a zero-knowledge, end-to-end encrypted open source chat system where all encryption and decryption is done locally on the client side, and servers cannot access plaintext.">
<meta name="twitter:image" content="https://crypt.works/client/assets/favicon.svg">
<meta name="google-site-verification" content="2KGYZcUDyYzGVeYAQ9mMRUGcTiMyZqFF6D7232SyGgw" />
<style>
@@ -55,30 +54,71 @@
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button> <h2 data-i18n="help.usage_guide">使用说明</h2>
</button> <h2 data-i18n="help.usage_guide"></h2>
<div class="help-content">
<div class="help-section">
<h3 data-i18n="help.what_is_nodecrypt">🔐 什么是 NodeCrypt?</h3>
<p data-i18n="help.what_is_nodecrypt_desc">NodeCrypt 是一个开源的端到端加密聊天系统,采用无数据库架构设计。所有消息在您的设备上本地加密,服务器和中间人无法解密您的任何聊天内容。</p>
<h3 data-i18n="help.what_is_nodecrypt"></h3>
<p data-i18n="help.what_is_nodecrypt_desc"></p>
</div>
<div class="help-section">
<h3 data-i18n="help.how_to_start">🚀 如何开始?</h3>
<h3 data-i18n="help.how_to_start"></h3>
<ol>
<li><strong data-i18n="help.step_username">输入用户名</strong>:<span data-i18n="help.step_username_desc">选择一个昵称</span></li>
<li><strong data-i18n="help.step_node_name">输入节点名</strong>:<span data-i18n="help.step_node_name_desc">创建或加入现有房间</span></li>
<li><strong data-i18n="help.step_password">设置节点密码</strong>:<span data-i18n="help.step_password_desc">用于排除巧合性</span></li>
<li><strong data-i18n="help.step_join">点击"加入房间"</strong>:<span data-i18n="help.step_join_desc">开始安全聊天</span></li>
<li>
<strong data-i18n="help.step_username"></strong>:
<span data-i18n="help.step_username_desc"></span>
</li>
<li>
<strong data-i18n="help.step_node_name"></strong>:
<span data-i18n="help.step_node_name_desc"></span>
</li>
<li>
<strong data-i18n="help.step_password"></strong>:
<span data-i18n="help.step_password_desc"></span>
</li>
<li>
<strong data-i18n="help.step_join"></strong>:
<span data-i18n="help.step_join_desc"></span>
</li>
</ol>
<div class="help-important">
<strong data-i18n="help.important_note"></strong>:<span data-i18n="help.room_isolation_note"></span>
</div>
</div>
<div class="help-section">
<h3 data-i18n="help.security_features">🔑 安全特性</h3>
<h3 data-i18n="help.security_features"></h3>
<ul>
<li><strong data-i18n="help.e2e_encryption">端到端加密</strong>:<span data-i18n="help.e2e_encryption_desc">消息仅您和接收者可解密</span></li>
<li><strong data-i18n="help.no_history">无历史记录</strong>:<span data-i18n="help.no_history_desc">新用户无法看到历史消息</span></li>
<li><strong data-i18n="help.password_protection">密码保护</strong>:<span data-i18n="help.password_protection_desc">房间密码参与加密过程</span></li>
<li><strong data-i18n="help.anonymous_communication">匿名通信</strong>:<span data-i18n="help.anonymous_communication_desc">无需注册真实身份</span></li>
<li>
<strong data-i18n="help.e2e_encryption"></strong>:
<span data-i18n="help.e2e_encryption_desc"></span>
</li>
<li>
<strong data-i18n="help.password_enhanced_encryption"></strong>:
<span data-i18n="help.password_enhanced_encryption_desc"></span>
</li>
<li>
<strong data-i18n="help.no_history"></strong>:
<span data-i18n="help.no_history_desc"></span>
</li>
<li>
<strong data-i18n="help.anonymous_communication"></strong>:
<span data-i18n="help.anonymous_communication_desc"></span>
</li>
<li>
<strong data-i18n="help.decentralized"></strong>:
<span data-i18n="help.decentralized_desc"></span>
</li>
</ul>
</div>
<div class="help-section">
<h3 data-i18n="help.usage_tips"></h3>
<ul>
<li><strong data-i18n="help.tip_private_chat"></strong>:<span data-i18n="help.tip_private_chat_desc"></span></li>
<li><strong data-i18n="help.tip_group_chat"></strong>:<span data-i18n="help.tip_group_chat_desc"></span></li>
<li><strong data-i18n="help.tip_security_reminder"></strong>:<span data-i18n="help.tip_security_reminder_desc"></span></li>
<li><strong data-i18n="help.tip_password_strategy"></strong>:<span data-i18n="help.tip_password_strategy_desc"></span></li>
</ul>
</div>
</div>
-13
View File
@@ -353,19 +353,6 @@ export function togglePrivateChat(targetId, targetName) {
updateChatInputStyle()
}
// Set the status bar text
// 设置状态栏文本
export function setStatus(text) {
let statusBar = $id('status-bar');
if (!statusBar) {
statusBar = createElement('div', {
id: 'status-bar',
style: 'color:green;padding:4px 10px;font-size:13px;'
});
document.body.appendChild(statusBar)
}
statusBar.innerText = text
}
// Exit the current room
// 退出当前房间
+77 -21
View File
@@ -232,23 +232,48 @@ export function setupMobileUIHandlers() {
rightbar.classList.remove('mobile-open');
rightbarMask.classList.remove('active')
}
} document.addEventListener('click', function(ev) {
if (!isMobile()) return;
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')) {
if (!settingsSidebar.contains(ev.target)) {
} // 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 (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')
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();
}
}
}
})
@@ -633,7 +658,7 @@ export function initFlipCard() {
// 开始持续旋转
function startContinuousRotation() {
if (isHovering) return; // 避免重复启动
if (isHovering) return; // 已经在旋转中
isHovering = true;
// 立即执行一次旋转
@@ -656,18 +681,49 @@ export function initFlipCard() {
}
}
// 帮助按钮事件
helpBtn.addEventListener('mouseenter', startContinuousRotation);
helpBtn.addEventListener('mouseleave', stopContinuousRotation);
// 检查鼠标是否在任一按钮上
function isMouseOverButton(e) {
const helpBtnRect = helpBtn.getBoundingClientRect();
const backBtnRect = backBtn.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
// 检查是否在帮助按钮上
const onHelpBtn = x >= helpBtnRect.left && x <= helpBtnRect.right &&
y >= helpBtnRect.top && y <= helpBtnRect.bottom;
// 检查是否在返回按钮上
const onBackBtn = x >= backBtnRect.left && x <= backBtnRect.right &&
y >= backBtnRect.top && y <= backBtnRect.bottom;
return onHelpBtn || onBackBtn;
}
// 监听整个卡片的鼠标移动
flipCard.addEventListener('mousemove', (e) => {
if (isMouseOverButton(e)) {
if (!isHovering) {
startContinuousRotation();
}
} else {
if (isHovering) {
stopContinuousRotation();
}
}
});
// 监听鼠标离开卡片
flipCard.addEventListener('mouseleave', () => {
stopContinuousRotation();
});
// 按钮点击事件(保持原有功能)
helpBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
performFlip();
});
// 返回按钮事件(同样的逻辑)
backBtn.addEventListener('mouseenter', startContinuousRotation);
backBtn.addEventListener('mouseleave', stopContinuousRotation);
backBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
+1 -1
View File
@@ -11,7 +11,7 @@ const addEmojiPickerStyles = () => {
if (document.querySelector('#emoji-picker-styles')) return;
const style = document.createElement('style');
style.id = 'emoji-picker-styles';
style.textContent = `emoji-picker{--background:#fff;--border-color:rgba(0,0,0,0.1);--border-radius:10px;--emoji-padding:0.4rem;--category-emoji-size:1.2rem;--font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;position:absolute;bottom:60px;left:22px;z-index:5;box-shadow:0 3px 12px rgba(0,0,0,0.15);width:320px;display:none;opacity:0;transform:translateY(-10px) scale(0.95);transition:opacity 0.3s ease,transform 0.3s ease}emoji-picker.show{opacity:1;transform:translateY(0) scale(1)}`;
style.textContent = `emoji-picker{--background:#fff;--border-color:rgba(0,0,0,0.1);--border-radius:10px;--emoji-padding:0.4rem;--category-emoji-size:1.2rem;--font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;position:absolute;bottom:60px;left:22px;z-index:5;box-shadow:0 3px 12px rgba(0,0,0,0.15);display:none;opacity:0;transform:translateY(-10px) scale(0.95);transition:opacity 0.3s ease,transform 0.3s ease}emoji-picker.show{opacity:1;transform:translateY(0) scale(1)}`;
document.head.appendChild(style)
};
// Setup emoji picker for chat input
+118 -37
View File
@@ -9,6 +9,14 @@ const LANGUAGES = {
name: 'English',
flag: '🇺🇸',
translations: {
// Meta tags for SEO
'meta.description': 'NodeCrypt - True end-to-end encrypted chat system, no database, all messages encrypted locally, server only relays encrypted data, supports Cloudflare Workers, Docker, self-hosting and local development.',
'meta.keywords': 'end-to-end encryption, security, chat, WebSocket, Cloudflare Workers, JavaScript, E2EE, anonymous communication, AES, ECDH, RSA, ChaCha20, security, open source, NodeCrypt, shuaiplus',
'meta.og_title': 'NodeCrypt - End-to-End Encrypted Chat System',
'meta.og_description': 'NodeCrypt is a zero-knowledge, end-to-end encrypted open source chat system where all encryption and decryption is done locally on the client side, and servers cannot access plaintext. Supports multi-platform deployment, secure, anonymous, no message history.',
'meta.twitter_title': 'NodeCrypt - End-to-End Encrypted Chat System',
'meta.twitter_description': 'NodeCrypt is a zero-knowledge, end-to-end encrypted open source chat system where all encryption and decryption is done locally on the client side, and servers cannot access plaintext.',
// Login and main UI
'ui.enter_node': 'Enter a Node',
'ui.username': 'Username',
@@ -88,26 +96,36 @@ const LANGUAGES = {
'help.title': 'User Guide',
'help.back_to_login': 'Back to Login',
'help.usage_guide': 'User Guide',
'help.what_is_nodecrypt': '🔐 What is NodeCrypt?',
'help.what_is_nodecrypt_desc': 'NodeCrypt is an open-source end-to-end encrypted chat system with a database-free architecture. All messages are encrypted locally on your device, and servers and intermediaries cannot decrypt any of your chat content.',
'help.how_to_start': '🚀 How to Start?',
'help.step_username': 'Username',
'help.step_username_desc': 'Choose a nickname',
'help.step_node_name': 'Node Name',
'help.step_node_name_desc': 'Create or join an existing node',
'help.step_password': 'Node Password',
'help.step_password_desc': 'Ensure node independence when room names are the same',
'help.what_is_nodecrypt': '🔐 What is NodeCrypt?', 'help.what_is_nodecrypt_desc': 'NodeCrypt is a true zero-knowledge end-to-end encrypted chat system. With a database-free architecture, all messages are encrypted locally on your device, and the server serves only as an encrypted data relay station, unable to access any of your plaintext content.',
'help.how_to_start': '🚀 Quick Start',
'help.step_username': 'Enter Username',
'help.step_username_desc': 'Choose a display name for the room, can be any name you like',
'help.step_node_name': 'Set Node Name',
'help.step_node_name_desc': 'Unique identifier for the room, equivalent to room number',
'help.step_password': 'Set Node Password',
'help.step_password_desc': 'Used to distinguish different rooms while participating in encryption process to enhance security',
'help.step_join': 'Click "Join Room"',
'help.step_join_desc': 'Start end-to-end encrypted chatting',
'help.security_features': '🔑 Security Features',
'help.e2e_encryption': 'End-to-End Encryption',
'help.e2e_encryption_desc': 'Messages can only be decrypted by you and the recipient',
'help.no_history': 'No History Records',
'help.no_history_desc': 'New users cannot see historical messages',
'help.password_protection': 'Password Protection',
'help.password_protection_desc': 'Room password participates in the encryption process',
'help.anonymous_communication': 'Anonymous Communication',
'help.anonymous_communication_desc': 'No need to register real identity',
'help.step_join_desc': 'System will automatically generate encryption keys and start secure chatting',
'help.security_features': '🔑 Security Features', 'help.e2e_encryption': '🛡️ End-to-End Encryption',
'help.e2e_encryption_desc': 'Uses AES-256 + ECDH encryption algorithm, messages can only be decrypted by you and the recipient',
'help.password_enhanced_encryption': '🔐 Password Enhanced Encryption',
'help.password_enhanced_encryption_desc': 'Node password directly participates in encryption key generation, providing additional security protection layer',
'help.no_history': '🚫 Zero History Records',
'help.no_history_desc': 'All messages exist only in current session, offline users cannot get historical messages',
'help.anonymous_communication': '🎭 Complete Anonymity',
'help.anonymous_communication_desc': 'No account registration required, no personal information collected',
'help.decentralized': '🌐 Decentralized',
'help.decentralized_desc': 'Supports self-hosted deployment, server does not participate in encryption/decryption process', 'help.usage_tips': '💡 Usage Tips',
'help.important_note': '⚠️ Important Note',
'help.room_isolation_note': 'Same node name but different passwords are two completely independent rooms that cannot communicate with each other.',
'help.tip_private_chat': 'Private Chat',
'help.tip_private_chat_desc': 'Use complex node names and passwords, share only with specific people',
'help.tip_group_chat': 'Group Chat',
'help.tip_group_chat_desc': 'Use simple and memorable node names and passwords for easy multi-user joining',
'help.tip_security_reminder': 'Security Reminder',
'help.tip_security_reminder_desc': 'Both node name and password must be exactly the same to enter the same room',
'help.tip_password_strategy': 'Password Strategy',
'help.tip_password_strategy_desc': 'Recommend using strong passwords containing letters, numbers and symbols',
}
},
zh: {
@@ -115,6 +133,14 @@ const LANGUAGES = {
name: '中文',
flag: '🇨🇳',
translations: {
// Meta tags for SEO
'meta.description': 'NodeCrypt - 真正的端到端加密聊天系统,无数据库,所有消息本地加密,服务器仅做加密数据中转,支持 Cloudflare Workers、Docker、自托管和本地开发。',
'meta.keywords': '端到端加密, 安全, 聊天, WebSocket, Cloudflare Workers, JavaScript, E2EE, 匿名通信, AES, ECDH, RSA, ChaCha20, 安全, 开源, NodeCrypt, shuaiplus',
'meta.og_title': 'NodeCrypt - 端到端加密聊天系统',
'meta.og_description': 'NodeCrypt 是一个端到端加密的开源聊天系统,所有加密解密均在客户端本地完成,服务器无法获取明文。支持多平台部署,安全、匿名、无历史消息。',
'meta.twitter_title': 'NodeCrypt - 端到端加密聊天系统',
'meta.twitter_description': 'NodeCrypt 是一个端到端加密的开源聊天系统,所有加密解密均在客户端本地完成,服务器无法获取明文。',
// Login and main UI
'ui.enter_node': '进入新的节点',
'ui.username': '用户名',
@@ -195,26 +221,37 @@ const LANGUAGES = {
'help.title': '使用说明',
'help.back_to_login': '返回登录',
'help.usage_guide': '使用说明',
'help.what_is_nodecrypt': '🔐 什么是 NodeCrypt?',
'help.what_is_nodecrypt_desc': 'NodeCrypt 是一个开源的端到端加密聊天系统,采用无数据库架构设计。所有消息在您的设备上本地加密,服务器和中间人无法解密您的任何聊天内容。',
'help.how_to_start': '🚀 如何开始?',
'help.step_username': '用户名',
'help.step_username_desc': '选择一个昵称',
'help.step_node_name': '节点名称',
'help.step_node_name_desc': '创建或加入现有节点',
'help.step_password': '节点密码',
'help.step_password_desc': '确保房间号相同时节点的独立性',
'help.what_is_nodecrypt': '🔐 什么是 NodeCrypt?', 'help.what_is_nodecrypt_desc': 'NodeCrypt 是一个真正的端到端加密聊天系统。采用无数据库架构,所有消息在您的设备上本地加密,服务器仅作为加密数据的中转站,无法获取您的任何明文内容。',
'help.how_to_start': '🚀 快速开始',
'help.step_username': '输入用户名',
'help.step_username_desc': '选择一个在房间中显示的昵称,可以是任何您喜欢的名称',
'help.step_node_name': '设置节点名',
'help.step_node_name_desc': '房间的唯一标识符,相当于房间号',
'help.step_password': '设置节点密码',
'help.step_password_desc': '用于区分不同房间,同时参与加密过程,提升安全性',
'help.step_join': '点击"加入房间"',
'help.step_join_desc': '开始端到端加密聊天',
'help.step_join_desc': '系统将自动生成加密密钥,开始安全聊天',
'help.security_features': '🔑 安全特性',
'help.e2e_encryption': '端到端加密',
'help.e2e_encryption_desc': '消息仅您和接收者可解密',
'help.no_history': '无历史记录',
'help.no_history_desc': '新用户无法看到历史消息',
'help.password_protection': '密码保护',
'help.password_protection_desc': '房间密码参与加密过程',
'help.anonymous_communication': '匿名通信',
'help.anonymous_communication_desc': '无需注册真实身份',
'help.e2e_encryption': '🛡️ 端到端加密',
'help.e2e_encryption_desc': '使用 AES-256 + ECDH 加密算法,消息仅您和接收者可解密',
'help.password_enhanced_encryption': '🔐 密码增强加密',
'help.password_enhanced_encryption_desc': '节点密码直接参与加密密钥生成,提供额外的安全保护层',
'help.no_history': '🚫 零历史记录',
'help.no_history_desc': '所有消息仅存在于当前会话,离线用户无法获取历史消息',
'help.anonymous_communication': '🎭 完全匿名',
'help.anonymous_communication_desc': '无需注册账户,不收集任何个人信息',
'help.decentralized': '🌐 去中心化',
'help.decentralized_desc': '支持自托管部署,服务器不参与加密解密过程', 'help.usage_tips': '💡 使用技巧',
'help.important_note': '⚠️ 重要提示',
'help.room_isolation_note': '相同节点名但不同密码的是两个完全独立的房间,无法相互通信。',
'help.tip_private_chat': '私人对话',
'help.tip_private_chat_desc': '使用复杂的节点名和密码,只分享给特定人员',
'help.tip_group_chat': '群聊',
'help.tip_group_chat_desc': '使用简单易记的节点名和密码,方便多人加入',
'help.tip_security_reminder': '安全提醒',
'help.tip_security_reminder_desc': '节点名和密码都需要完全一致才能进入同一个房间',
'help.tip_password_strategy': '密码策略',
'help.tip_password_strategy_desc': '建议使用包含字母、数字和符号的强密码',
}
}
};
@@ -396,4 +433,48 @@ export function updateStaticTexts() {
element.title = t(key, element.title || key);
}
});
// Update meta tags
// 更新meta标签
updateMetaTags();
}
// Update meta tags with current language
// 使用当前语言更新meta标签
function updateMetaTags() {
// Update description meta tag
const metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
metaDescription.content = t('meta.description', metaDescription.content);
}
// Update keywords meta tag
const metaKeywords = document.querySelector('meta[name="keywords"]');
if (metaKeywords) {
metaKeywords.content = t('meta.keywords', metaKeywords.content);
}
// Update og:title meta tag
const metaOgTitle = document.querySelector('meta[property="og:title"]');
if (metaOgTitle) {
metaOgTitle.content = t('meta.og_title', metaOgTitle.content);
}
// Update og:description meta tag
const metaOgDescription = document.querySelector('meta[property="og:description"]');
if (metaOgDescription) {
metaOgDescription.content = t('meta.og_description', metaOgDescription.content);
}
// Update twitter:title meta tag
const metaTwitterTitle = document.querySelector('meta[name="twitter:title"]');
if (metaTwitterTitle) {
metaTwitterTitle.content = t('meta.twitter_title', metaTwitterTitle.content);
}
// Update twitter:description meta tag
const metaTwitterDescription = document.querySelector('meta[name="twitter:description"]');
if (metaTwitterDescription) {
metaTwitterDescription.content = t('meta.twitter_description', metaTwitterDescription.content);
}
}
+33 -16
View File
@@ -379,14 +379,18 @@ function openSettingsPanel() {
// 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: hide main sidebar and show settings sidebar
sidebar.style.display = 'none';
} } 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
@@ -397,25 +401,38 @@ function openSettingsPanel() {
// 关闭设置面板
function closeSettingsPanel() {
const settingsSidebar = $id('settings-sidebar');
const sidebar = $id('sidebar');
const sidebarMask = $id('mobile-sidebar-mask');
if (!settingsSidebar || !sidebar) return;
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()) {
// Mobile: hide settings sidebar and mask, show main sidebar if needed
settingsSidebar.classList.remove('mobile-open');
if (sidebarMask) {
sidebarMask.classList.remove('active');
}
// Don't auto-show main sidebar on mobile - let user click menu button
// 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(() => {
settingsSidebar.style.display = 'none';
}, 300); // Wait for animation to complete
if (!settingsSidebar.classList.contains('mobile-open')) { // check if it wasn't reopened
settingsSidebar.style.display = 'none';
}
}, 350); // Slightly longer than CSS transition
} else {
// Desktop: hide settings sidebar and show main sidebar
settingsSidebar.style.display = 'none';
sidebar.style.display = 'flex';
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);
}
}