diff --git a/main_pyqt5.py b/main_pyqt5.py index afb9bd2..a775f55 100644 --- a/main_pyqt5.py +++ b/main_pyqt5.py @@ -2,9 +2,62 @@ # -*- coding: utf-8 -*- """ -云笔记应用程序 -支持本地和云端文件管理,支持文本文件(.txt)和加密笔记(.bjb) -完整修复版 - 支持端到端加密,云端下载的单独密钥文件需用户提供.key文件 +云笔记应用程序 - 安全加密笔记系统 +==================================== + +作者: dvs (dvsxt) +版本: 5.0 +日期: 2026-06-10 + +版本 5.0 核心改进: +============================================================================ +1. 现代化GUI设计 + - 全新的暗色/亮色主题支持 + - 圆角卡片设计 + - 自适应布局,支持窗口缩放 + - 流畅的动画效果 + +2. 美观的界面元素 + - 渐变按钮 + - 半透明效果 + - 阴影和光效 + - 平滑过渡动画 + +3. 完全自适应尺寸 + - 移除所有固定尺寸 + - 使用布局管理器自动调整 + - 支持不同分辨率屏幕 + +4. 加密配置自动提取与复用(解决保存时重复弹窗问题) + - 打开加密文件时,根据用户输入的密码自动检测强度模式 + - 密码长度≥16且包含大小写字母+数字+特殊字符 → 高强度模式 + - 否则自动判定为低强度模式 + - 保存时直接使用检测到的配置,无需重新配置或重新输入密码 + +5. 智能密码强度检测(仅提示,不阻止) + - 实时显示密码强度等级(极弱/弱/中等/强/极强) + - 使用弱密码时弹出警告,但用户可选择继续使用 + - 尊重用户选择,不强求使用高强度密码 + +6. 云端 API 支持 + - 默认使用官方测试服务器 https://noteapi.dvssvc.site/api + - 可自行部署服务端,源码在 https://gitcode.com/dvsxt/cloudnote + +7. 双路径密钥存储 + - 密钥优先存储在用户目录 ~/.cloudnote/keys/ + - 同时备份到程序目录 .keys/ 防止丢失 + +依赖安装: + pip install PyQt5 requests cryptography + +快捷键: + Ctrl+N 新建文本文件 + Ctrl+Shift+N 新建加密笔记 + Ctrl+O 打开文件 + Ctrl+Shift+O 从云端打开 + Ctrl+S 保存 + Ctrl+Shift+S 另存为加密笔记 + Ctrl+E 加密当前文件 """ import sys @@ -19,45 +72,697 @@ from PyQt5.QtCore import * from PyQt5.QtGui import * import requests -# 导入加密模块 +import warnings +warnings.filterwarnings('ignore', category=DeprecationWarning) + from DVT_RFSA import * -# ==================== 调试日志配置 ==================== -DEBUG = True # True=记录日志, False=不记录日志 +# ============================================================================ +# 调试配置 +# ============================================================================ -def debug_log(msg, level="INFO"): - """调试日志函数""" +DEBUG = False + +def debug_log(msg: str, level: str = "INFO") -> None: if DEBUG: timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] print(f"[{timestamp}] [{level}] {msg}") sys.stdout.flush() -def debug_error(msg): - """错误日志(带堆栈)""" +def debug_error(msg: str) -> None: if DEBUG: debug_log(msg, "ERROR") traceback.print_exc() -# ==================== 配置 ==================== +# ============================================================================ +# 全局样式表 - 现代化设计 +# ============================================================================ + +DARK_STYLE = """ +/* 全局样式 */ +QWidget { + background-color: #1e1e2f; + color: #cdd6f4; + font-family: 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', sans-serif; + font-size: 13px; +} + +/* 主窗口 */ +QMainWindow { + background-color: #1e1e2f; +} + +/* 菜单栏 */ +QMenuBar { + background-color: #181825; + border-bottom: 1px solid #313244; + padding: 4px 0px; +} + +QMenuBar::item { + background-color: transparent; + padding: 8px 12px; + border-radius: 6px; + margin: 0px 2px; +} + +QMenuBar::item:selected { + background-color: #313244; +} + +/* 菜单 */ +QMenu { + background-color: #181825; + border: 1px solid #313244; + border-radius: 8px; + padding: 8px; +} + +QMenu::item { + padding: 8px 32px 8px 24px; + border-radius: 6px; + margin: 2px; +} + +QMenu::item:selected { + background-color: #89b4fa; + color: #1e1e2f; +} + +QMenu::separator { + height: 1px; + background-color: #313244; + margin: 4px 8px; +} + +/* 标签页 */ +QTabWidget::pane { + border: none; + background-color: #1e1e2f; + border-radius: 12px; +} + +QTabWidget::tab-bar { + alignment: left; +} + +QTabBar::tab { + background-color: #181825; + border: none; + border-radius: 8px 8px 0px 0px; + padding: 10px 20px; + margin-right: 4px; + color: #a6adc8; +} + +QTabBar::tab:selected { + background-color: #313244; + color: #89b4fa; +} + +QTabBar::tab:hover:!selected { + background-color: #282838; +} + +QTabBar::close-button { + image: url(none); + subcontrol-position: right; + margin-left: 8px; +} + +QTabBar::close-button:hover { + background-color: #f38ba8; + border-radius: 4px; +} + +/* 文本编辑区 */ +QTextEdit { + background-color: #181825; + border: 1px solid #313244; + border-radius: 12px; + padding: 12px; + selection-background-color: #89b4fa; + selection-color: #1e1e2f; +} + +QTextEdit:focus { + border: 1px solid #89b4fa; +} + +/* 按钮 */ +QPushButton { + background-color: #313244; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-weight: 500; +} + +QPushButton:hover { + background-color: #45475a; +} + +QPushButton:pressed { + background-color: #89b4fa; + color: #1e1e2f; +} + +QPushButton:disabled { + background-color: #282838; + color: #6c7086; +} + +/* 主要按钮 */ +QPushButton[primary="true"] { + background-color: #89b4fa; + color: #1e1e2f; +} + +QPushButton[primary="true"]:hover { + background-color: #b4befe; +} + +/* 危险按钮 */ +QPushButton[danger="true"] { + background-color: #f38ba8; + color: #1e1e2f; +} + +QPushButton[danger="true"]:hover { + background-color: #eba0ac; +} + +/* 输入框 */ +QLineEdit { + background-color: #181825; + border: 1px solid #313244; + border-radius: 8px; + padding: 10px 12px; +} + +QLineEdit:focus { + border: 1px solid #89b4fa; +} + +/* 组合框 */ +QComboBox { + background-color: #181825; + border: 1px solid #313244; + border-radius: 8px; + padding: 8px 12px; +} + +QComboBox::drop-down { + border: none; +} + +QComboBox::down-arrow { + image: none; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #cdd6f4; + margin-right: 8px; +} + +QComboBox QAbstractItemView { + background-color: #181825; + border: 1px solid #313244; + border-radius: 8px; + selection-background-color: #313244; +} + +/* 复选框 */ +QCheckBox { + spacing: 8px; +} + +QCheckBox::indicator { + width: 18px; + height: 18px; + border-radius: 4px; + background-color: #181825; + border: 1px solid #313244; +} + +QCheckBox::indicator:checked { + background-color: #89b4fa; + border: 1px solid #89b4fa; +} + +/* 单选按钮 */ +QRadioButton { + spacing: 8px; +} + +QRadioButton::indicator { + width: 18px; + height: 18px; + border-radius: 9px; + background-color: #181825; + border: 1px solid #313244; +} + +QRadioButton::indicator:checked { + background-color: #89b4fa; + border: 1px solid #89b4fa; +} + +/* 分组框 */ +QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + font-weight: 500; +} + +QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; +} + +/* 滚动条 */ +QScrollBar:vertical { + background-color: #181825; + width: 10px; + border-radius: 5px; +} + +QScrollBar::handle:vertical { + background-color: #45475a; + border-radius: 5px; + min-height: 30px; +} + +QScrollBar::handle:vertical:hover { + background-color: #6c7086; +} + +QScrollBar:horizontal { + background-color: #181825; + height: 10px; + border-radius: 5px; +} + +QScrollBar::handle:horizontal { + background-color: #45475a; + border-radius: 5px; + min-width: 30px; +} + +/* 状态栏 */ +QStatusBar { + background-color: #181825; + border-top: 1px solid #313244; + padding: 4px 8px; +} + +/* 树形控件 */ +QTreeWidget { + background-color: #181825; + border: 1px solid #313244; + border-radius: 12px; + outline: none; +} + +QTreeWidget::item { + padding: 8px; + border-radius: 6px; +} + +QTreeWidget::item:hover { + background-color: #282838; +} + +QTreeWidget::item:selected { + background-color: #313244; +} + +/* 工具栏 */ +QToolBar { + background-color: #181825; + border: none; + border-radius: 12px; + spacing: 4px; + padding: 4px; +} + +QToolBar QToolButton { + background-color: transparent; + border-radius: 6px; + padding: 6px 12px; +} + +QToolBar QToolButton:hover { + background-color: #313244; +} + +/* 标签 */ +QLabel { + color: #cdd6f4; +} + +/* 进度条 */ +QProgressBar { + background-color: #181825; + border: 1px solid #313244; + border-radius: 6px; + text-align: center; +} + +QProgressBar::chunk { + background-color: #89b4fa; + border-radius: 5px; +} + +/* 消息框 */ +QMessageBox { + background-color: #1e1e2f; +} + +QMessageBox QPushButton { + min-width: 80px; +} + +/* 输入对话框 */ +QInputDialog { + background-color: #1e1e2f; +} +""" + +LIGHT_STYLE = """ +/* 全局样式 - 亮色主题 */ +QWidget { + background-color: #ffffff; + color: #1e1e2f; + font-family: 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', sans-serif; + font-size: 13px; +} + +QMainWindow { + background-color: #f5f7fa; +} + +QMenuBar { + background-color: #ffffff; + border-bottom: 1px solid #e1e4e8; + padding: 4px 0px; +} + +QMenuBar::item { + background-color: transparent; + padding: 8px 12px; + border-radius: 6px; + margin: 0px 2px; +} + +QMenuBar::item:selected { + background-color: #e1e4e8; +} + +QMenu { + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 8px; + padding: 8px; +} + +QMenu::item { + padding: 8px 32px 8px 24px; + border-radius: 6px; + margin: 2px; +} + +QMenu::item:selected { + background-color: #89b4fa; + color: #1e1e2f; +} + +QTabWidget::pane { + border: none; + background-color: #f5f7fa; + border-radius: 12px; +} + +QTabBar::tab { + background-color: #e1e4e8; + border: none; + border-radius: 8px 8px 0px 0px; + padding: 10px 20px; + margin-right: 4px; + color: #4a5568; +} + +QTabBar::tab:selected { + background-color: #ffffff; + color: #89b4fa; +} + +QTabBar::tab:hover:!selected { + background-color: #cbd5e0; +} + +QTextEdit { + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 12px; + padding: 12px; + selection-background-color: #89b4fa; + selection-color: #ffffff; +} + +QTextEdit:focus { + border: 1px solid #89b4fa; +} + +QPushButton { + background-color: #e1e4e8; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-weight: 500; +} + +QPushButton:hover { + background-color: #cbd5e0; +} + +QPushButton:pressed { + background-color: #89b4fa; + color: #ffffff; +} + +QPushButton[primary="true"] { + background-color: #89b4fa; + color: #ffffff; +} + +QPushButton[primary="true"]:hover { + background-color: #b4befe; +} + +QPushButton[danger="true"] { + background-color: #f38ba8; + color: #ffffff; +} + +QLineEdit { + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 8px; + padding: 10px 12px; +} + +QLineEdit:focus { + border: 1px solid #89b4fa; +} + +QComboBox { + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 8px; + padding: 8px 12px; +} + +QGroupBox { + border: 1px solid #e1e4e8; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; +} + +QCheckBox::indicator { + width: 18px; + height: 18px; + border-radius: 4px; + background-color: #ffffff; + border: 1px solid #e1e4e8; +} + +QCheckBox::indicator:checked { + background-color: #89b4fa; + border: 1px solid #89b4fa; +} + +QRadioButton::indicator { + width: 18px; + height: 18px; + border-radius: 9px; + background-color: #ffffff; + border: 1px solid #e1e4e8; +} + +QRadioButton::indicator:checked { + background-color: #89b4fa; + border: 1px solid #89b4fa; +} + +QStatusBar { + background-color: #ffffff; + border-top: 1px solid #e1e4e8; +} + +QTreeWidget { + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 12px; +} + +QTreeWidget::item:hover { + background-color: #f0f2f5; +} + +QTreeWidget::item:selected { + background-color: #e1e4e8; +} + +QToolBar { + background-color: #ffffff; + border: 1px solid #e1e4e8; + border-radius: 12px; +} + +QScrollBar:vertical { + background-color: #f0f2f5; + width: 10px; + border-radius: 5px; +} + +QScrollBar::handle:vertical { + background-color: #cbd5e0; + border-radius: 5px; +} +""" + +# ============================================================================ +# 智能密码强度检测(仅提示,不阻止) +# ============================================================================ + +def check_password_strength(password: str) -> dict: + if not password: + return { + 'level': '未输入', + 'color': '#999999', + 'message': '请输入密码', + 'score': 0, + 'is_strong': False + } + + length = len(password) + has_upper = any(c.isupper() for c in password) + has_lower = any(c.islower() for c in password) + has_digit = any(c.isdigit() for c in password) + has_special = any(not c.isalnum() for c in password) + + is_strong = ( + length >= 16 and + has_upper and has_lower and + has_digit and has_special + ) + + score = 0 + if length >= 6: + score += 1 + if length >= 10: + score += 1 + if length >= 14: + score += 1 + if has_upper and has_lower: + score += 1 + if has_digit: + score += 1 + if has_special: + score += 1 + + common = ['123456', 'password', '12345678', 'qwerty', '12345', '123456789', + '111111', '123123', 'abc123', 'password1', '111', '222', '333'] + if password.lower() in common: + return { + 'level': '极弱', + 'color': '#d73a49', + 'message': '这是常见弱密码,极易被破解!', + 'score': 0, + 'is_strong': False + } + + if score <= 2: + return { + 'level': '极弱', + 'color': '#d73a49', + 'message': '密码强度极弱,容易被破解', + 'score': score, + 'is_strong': False + } + elif score <= 3: + return { + 'level': '弱', + 'color': '#e36209', + 'message': '密码强度较弱,建议加强', + 'score': score, + 'is_strong': False + } + elif score <= 4: + return { + 'level': '中等', + 'color': '#fb8532', + 'message': '密码强度中等,可继续使用', + 'score': score, + 'is_strong': False + } + elif score <= 5: + return { + 'level': '强', + 'color': '#28a745', + 'message': '密码强度良好', + 'score': score, + 'is_strong': False + } + else: + return { + 'level': '极强', + 'color': '#28a745', + 'message': '密码强度极高,非常安全', + 'score': score, + 'is_strong': True + } + +# ============================================================================ +# 应用程序配置 +# ============================================================================ + API_BASE_URL = "https://noteapi.dvssvc.site/api" APP_NAME = "CloudNote" -def get_app_dir(): - """获取程序所在目录""" +def get_app_dir() -> str: if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) - else: - return os.path.dirname(os.path.abspath(__file__)) + return os.path.dirname(os.path.abspath(__file__)) APP_DIR = get_app_dir() USER_KEY_DIR = os.path.expanduser(f"~/.{APP_NAME.lower()}/keys") APP_KEY_DIR = os.path.join(APP_DIR, ".keys") -if DEBUG: - debug_log(f"程序目录: {APP_DIR}") - debug_log(f"用户密钥目录: {USER_KEY_DIR}") - debug_log(f"程序密钥目录: {APP_KEY_DIR}") - KEY_FILES = { 'rsa_private': "global_rsa_private.pem", 'rsa_public': "global_rsa_public.pem", @@ -65,8 +770,11 @@ KEY_FILES = { 'x25519_public': "global_x25519_public.bin" } -def get_key_path(key_name, prefer_user=True): - """获取密钥文件路径,优先使用用户目录""" +# ============================================================================ +# 密钥路径管理 +# ============================================================================ + +def get_key_path(key_name: str, prefer_user: bool = True) -> tuple: if prefer_user: search_dirs = [USER_KEY_DIR, APP_KEY_DIR] dir_names = ['user', 'app'] @@ -81,12 +789,9 @@ def get_key_path(key_name, prefer_user=True): debug_log(f"找到密钥 {key_name} 在 {dir_type} 目录: {file_path}") return file_path, dir_type - if DEBUG: - debug_log(f"密钥 {key_name} 未找到,将使用用户目录路径") return os.path.join(USER_KEY_DIR, key_name), 'user' -def ensure_directory(dir_path): - """确保目录存在""" +def ensure_directory(dir_path: str) -> bool: try: os.makedirs(dir_path, exist_ok=True) if DEBUG: @@ -96,11 +801,13 @@ def ensure_directory(dir_path): debug_error(f"创建目录失败 {dir_path}: {e}") return False -# ==================== 全局密钥管理(双路径) ==================== -def ensure_global_keys(): - """确保全局密钥对存在,支持双路径存储""" +# ============================================================================ +# 全局密钥管理 +# ============================================================================ + +def ensure_global_keys() -> None: if DEBUG: - debug_log("开始检查/生成全局密钥(双路径模式)...") + debug_log("开始检查/生成全局密钥...") ensure_directory(USER_KEY_DIR) ensure_directory(APP_KEY_DIR) @@ -120,11 +827,11 @@ def ensure_global_keys(): app_exists = os.path.exists(app_priv) and os.path.exists(app_pub) if DEBUG: - debug_log(f"{key_type.upper()}密钥对 - 用户目录存在: {user_exists}, 程序目录存在: {app_exists}") + debug_log(f"{key_type.upper()} - 用户目录: {user_exists}, 程序目录: {app_exists}") if not user_exists: if DEBUG: - debug_log(f"开始生成{key_type.upper()}密钥对到用户目录...") + debug_log(f"生成{key_type.upper()}密钥对...") try: if key_size: priv, pub = gen_func(key_size) @@ -141,11 +848,9 @@ def ensure_global_keys(): f.write(priv) with open(app_pub, 'wb') as f: f.write(pub) - if DEBUG: - debug_log(f"{key_type.upper()}密钥备份到程序目录成功") except Exception as e: if DEBUG: - debug_log(f"备份到程序目录失败: {e}") + debug_log(f"备份失败: {e}") except Exception as e: debug_error(f"{key_type.upper()}密钥生成失败: {e}") raise @@ -160,17 +865,14 @@ def ensure_global_keys(): f.write(priv) with open(app_pub, 'wb') as f: f.write(pub) - if DEBUG: - debug_log(f"{key_type.upper()}密钥备份到程序目录成功") except Exception as e: if DEBUG: debug_log(f"备份失败: {e}") if DEBUG: - debug_log("全局密钥检查/生成完成") + debug_log("全局密钥检查完成") -def get_global_rsa_private(): - """获取全局RSA私钥""" +def get_global_rsa_private() -> bytes: key_path, _ = get_key_path(KEY_FILES['rsa_private']) if not os.path.exists(key_path): ensure_global_keys() @@ -178,8 +880,7 @@ def get_global_rsa_private(): with open(key_path, 'rb') as f: return f.read() -def get_global_rsa_public(): - """获取全局RSA公钥""" +def get_global_rsa_public() -> bytes: key_path, _ = get_key_path(KEY_FILES['rsa_public']) if not os.path.exists(key_path): ensure_global_keys() @@ -187,8 +888,7 @@ def get_global_rsa_public(): with open(key_path, 'rb') as f: return f.read() -def get_global_x25519_private(): - """获取全局X25519私钥""" +def get_global_x25519_private() -> bytes: key_path, _ = get_key_path(KEY_FILES['x25519_private']) if not os.path.exists(key_path): ensure_global_keys() @@ -196,8 +896,7 @@ def get_global_x25519_private(): with open(key_path, 'rb') as f: return f.read() -def get_global_x25519_public(): - """获取全局X25519公钥""" +def get_global_x25519_public() -> bytes: key_path, _ = get_key_path(KEY_FILES['x25519_public']) if not os.path.exists(key_path): ensure_global_keys() @@ -205,27 +904,14 @@ def get_global_x25519_public(): with open(key_path, 'rb') as f: return f.read() -# ==================== 加密笔记处理 ==================== +# ============================================================================ +# 加密笔记处理器 +# ============================================================================ + class EncryptedNoteHandler: - """加密笔记处理器 - 核心加密解密逻辑""" @staticmethod - def check_file_has_password(file_path): - """检查加密文件是否有密码保护""" - try: - with open(file_path, 'rb') as f: - data = f.read() - clean_data = remove_security_info(data) - json_str = clean_data.decode('utf-8') - info = json.loads(json_str) - return info.get('has_password', False), info.get('mode', 'aes_rsa'), info.get('single_key', False) - except Exception as e: - debug_error(f"检查文件密码状态失败: {e}") - return False, 'unknown', False - - @staticmethod - def get_file_metadata(file_path): - """获取加密文件的元数据(不进行解密)""" + def get_file_metadata(file_path: str) -> dict: try: with open(file_path, 'rb') as f: data = f.read() @@ -244,20 +930,71 @@ class EncryptedNoteHandler: return None @staticmethod - def encrypt_content(content: str, algorithm: str, key_mode: str, password: str = None, - single_private=None, single_public=None, strict_password=True): + def extract_encrypt_config(file_path: str, password: str = None, single_private: bytes = None) -> dict: """ - 加密内容 - content: 要加密的文本内容 - algorithm: 'rsa' 或 'x25519' - key_mode: 'global' 或 'single' - password: 用户密码(可选) - single_private/single_public: 单独密钥对 - strict_password: 是否使用高强度密码要求 + 提取加密配置,根据用户输入的密码自动判断强度模式 + 密码长度≥16且包含大小写字母+数字+特殊字符 → 高强度模式,否则低强度模式 """ + try: + metadata = EncryptedNoteHandler.get_file_metadata(file_path) + if metadata is None: + return None + + mode = metadata['mode'] + has_password = metadata['has_password'] + single_key = metadata['single_key'] + + if 'x25519' in mode: + algorithm = 'x25519' + else: + algorithm = 'rsa' + + key_mode = 'single' if single_key else 'global' + + # 根据密码自动判断强度模式 + strict_password = True + if password: + is_strong = ( + len(password) >= 16 and + any(c.isupper() for c in password) and + any(c.islower() for c in password) and + any(c.isdigit() for c in password) and + any(not c.isalnum() for c in password) + ) + strict_password = is_strong + if DEBUG: + debug_log(f"密码强度检测: {'高强度' if is_strong else '低强度'} (长度={len(password)})") + + single_public = None + if single_key and single_private is not None: + pub_key_path = file_path + '.pub' + if os.path.exists(pub_key_path): + try: + with open(pub_key_path, 'rb') as f: + single_public = f.read() + except Exception: + pass + + return { + 'algorithm': algorithm, + 'key_mode': key_mode, + 'use_password': has_password, + 'custom_password': password, + 'single_private': single_private, + 'single_public': single_public, + 'strict_password': strict_password + } + except Exception as e: + debug_error(f"提取加密配置失败: {e}") + return None + + @staticmethod + def encrypt_content(content: str, algorithm: str, key_mode: str, + password: str = None, single_private: bytes = None, + single_public: bytes = None, strict_password: bool = True) -> tuple: if DEBUG: - debug_log(f"开始加密 - 算法: {algorithm}, 密钥模式: {key_mode}, 密码保护: {password is not None}, 严格模式: {strict_password}") - debug_log(f"内容长度: {len(content)} 字符") + debug_log(f"开始加密 - 算法: {algorithm}, 密钥模式: {key_mode}, " + f"密码保护: {password is not None}, 强度模式: {'高强度' if strict_password else '低强度'}") data = content.encode('utf-8') private_key = None @@ -268,11 +1005,15 @@ class EncryptedNoteHandler: if key_mode == 'global': pub_pem = get_global_rsa_public() if password: - encrypted = encrypt_text_aes_rsa_with_password(data, pub_pem, password, add_header=True, strict_password=strict_password) + encrypted = encrypt_text_aes_rsa_with_password( + data, pub_pem, password, add_header=True, + strict_password=strict_password + ) else: - encrypted = encrypt_text_aes_rsa(data, pub_pem, add_header=True, strict_password=strict_password) - private_key = None - public_key = None + encrypted = encrypt_text_aes_rsa( + data, pub_pem, add_header=True, + strict_password=strict_password + ) else: if single_private is None or single_public is None: private_key, public_key = generate_rsa_key(4096) @@ -281,25 +1022,38 @@ class EncryptedNoteHandler: public_key = single_public if password: - encrypted = encrypt_text_aes_rsa_with_password(data, public_key, password, add_header=True, strict_password=strict_password) + encrypted = encrypt_text_aes_rsa_with_password( + data, public_key, password, add_header=True, + strict_password=strict_password + ) else: - encrypted = encrypt_text_aes_rsa(data, public_key, add_header=True, strict_password=strict_password) + encrypted = encrypt_text_aes_rsa( + data, public_key, add_header=True, + strict_password=strict_password + ) - # 添加single_key标记 json_str = remove_security_info(encrypted).decode('utf-8') info = json.loads(json_str) info['single_key'] = True - encrypted = add_security_info(json.dumps(info).encode('utf-8'), 'aes_rsa', bool(password)) + encrypted = add_security_info( + json.dumps(info).encode('utf-8'), + 'aes_rsa', bool(password) + ) return encrypted, private_key, public_key - else: # x25519 + + else: if key_mode == 'global': pub_raw = get_global_x25519_public() if password: - encrypted = encrypt_text_aes_x25519(data, pub_raw, password, add_header=True, strict_password=strict_password) + encrypted = encrypt_text_aes_x25519( + data, pub_raw, password, add_header=True, + strict_password=strict_password + ) else: - encrypted = encrypt_text_aes_x25519(data, pub_raw, add_header=True, strict_password=strict_password) - private_key = None - public_key = None + encrypted = encrypt_text_aes_x25519( + data, pub_raw, add_header=True, + strict_password=strict_password + ) else: if single_private is None or single_public is None: private_key, public_key = generate_x25519_keys() @@ -308,35 +1062,36 @@ class EncryptedNoteHandler: public_key = single_public if password: - encrypted = encrypt_text_aes_x25519(data, public_key, password, add_header=True, strict_password=strict_password) + encrypted = encrypt_text_aes_x25519( + data, public_key, password, add_header=True, + strict_password=strict_password + ) else: - encrypted = encrypt_text_aes_x25519(data, public_key, add_header=True, strict_password=strict_password) + encrypted = encrypt_text_aes_x25519( + data, public_key, add_header=True, + strict_password=strict_password + ) - # 添加single_key标记 json_str = remove_security_info(encrypted).decode('utf-8') info = json.loads(json_str) info['single_key'] = True - encrypted = add_security_info(json.dumps(info).encode('utf-8'), 'aes_x25519', bool(password)) + encrypted = add_security_info( + json.dumps(info).encode('utf-8'), + 'aes_x25519', bool(password) + ) return encrypted, private_key, public_key return encrypted, private_key, public_key + except Exception as e: debug_error(f"加密过程异常: {e}") raise Exception(f"加密失败: {str(e)}") @staticmethod - def decrypt_file(file_path, password: str = None, single_private: bytes = None): - """ - 解密文件 - file_path: 文件路径 - password: 用户密码(可选) - single_private: 单独私钥(可选) - """ + def decrypt_file(file_path: str, password: str = None, + single_private: bytes = None) -> bytes: if DEBUG: debug_log(f"开始解密文件: {file_path}") - debug_log(f"密码提供: {password is not None}, 单独私钥提供: {single_private is not None}") - if password: - debug_log(f"密码长度: {len(password)}") try: with open(file_path, 'rb') as f: @@ -349,146 +1104,114 @@ class EncryptedNoteHandler: has_password = info.get('has_password', False) single_key = info.get('single_key', False) - if DEBUG: - debug_log(f"加密信息 - 模式: {mode}, 有密码: {has_password}, 单文件密钥: {single_key}") - if mode in ['aes_rsa', 'aes_rsa_password_protected']: if single_key: if single_private is None: - raise ValueError("单独密钥_需要选择密钥文件") + raise ValueError("单独密钥模式需要提供私钥文件") priv = single_private else: priv = get_global_rsa_private() if has_password: if password is None: - raise ValueError("密码保护_需要输入密码") - decrypted = decrypt_text_aes_rsa_with_password(clean_data, priv, password, has_header=False) + raise ValueError("此文件受密码保护,需要输入密码") + decrypted = decrypt_text_aes_rsa_with_password( + clean_data, priv, password, has_header=False + ) else: - decrypted = decrypt_text_aes_rsa(clean_data, priv, has_header=False) + decrypted = decrypt_text_aes_rsa( + clean_data, priv, has_header=False + ) + elif mode in ['aes_x25519', 'aes_x25519_password_protected']: if single_key: if single_private is None: - raise ValueError("单独密钥_需要选择密钥文件") + raise ValueError("单独密钥模式需要提供私钥文件") priv = single_private else: priv = get_global_x25519_private() if has_password: if password is None: - raise ValueError("密码保护_需要输入密码") - decrypted = decrypt_text_aes_x25519(clean_data, priv, password, has_header=False) + raise ValueError("此文件受密码保护,需要输入密码") + decrypted = decrypt_text_aes_x25519( + clean_data, priv, password, has_header=False + ) else: - decrypted = decrypt_text_aes_x25519(clean_data, priv, has_header=False) + decrypted = decrypt_text_aes_x25519( + clean_data, priv, has_header=False + ) else: raise ValueError(f"未知加密模式: {mode}") - # 解密结果可能是 bytes 或 str,统一转为 bytes if isinstance(decrypted, str): decrypted = decrypted.encode('utf-8') - if DEBUG: - debug_log(f"解密成功,解密后数据大小: {len(decrypted)}字节") return decrypted + except Exception as e: debug_error(f"解密过程异常: {e}") raise -# ==================== 通用解密函数(修复版:支持单独密钥文件选择) ==================== -def decrypt_bjb_file(file_path, parent_window, is_cloud_file=False, cloud_filename=None): - """ - 解密.bjb文件的通用函数 - file_path: 文件路径 - parent_window: 父窗口(用于弹窗) - is_cloud_file: 是否为云端下载的文件 - cloud_filename: 云端原始文件名(用于提示) - 返回: (content, success) - """ +# ============================================================================ +# 通用解密函数 +# ============================================================================ + +def decrypt_bjb_file(file_path: str, parent_window: QWidget, + is_cloud_file: bool = False, + cloud_filename: str = None) -> tuple: if DEBUG: - debug_log(f"通用解密函数: {file_path}, 云端文件: {is_cloud_file}") + debug_log(f"通用解密函数: {file_path}") display_name = cloud_filename if cloud_filename else os.path.basename(file_path) try: - # 首先获取文件元数据,判断是否需要单独密钥 metadata = EncryptedNoteHandler.get_file_metadata(file_path) if metadata is None: - QMessageBox.critical(parent_window, "文件错误", "无法读取文件元数据,文件可能已损坏") - return None, False + QMessageBox.critical(parent_window, "文件错误", "无法读取文件元数据") + return None, False, None has_password = metadata['has_password'] single_key = metadata['single_key'] mode = metadata['mode'] - if DEBUG: - debug_log(f"文件元数据 - has_password: {has_password}, single_key: {single_key}, mode: {mode}") - single_private = None - # ========== 处理单独密钥模式 ========== if single_key: - if DEBUG: - debug_log("检测到单独密钥模式,需要查找或选择.key文件") - - # 首先尝试在相同目录下查找对应的.key文件 key_file_path = file_path + '.key' if os.path.exists(key_file_path): - if DEBUG: - debug_log(f"找到本地密钥文件: {key_file_path}") try: with open(key_file_path, 'rb') as f: single_private = f.read() - if DEBUG: - debug_log(f"密钥文件读取成功,长度: {len(single_private)}字节") except Exception as e: debug_error(f"读取密钥文件失败: {e}") QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}") - return None, False + return None, False, None else: - # 没有找到,弹出文件选择对话框 - if DEBUG: - debug_log("未找到本地密钥文件,弹出选择对话框") - - # 构建提示信息 - msg = f"文件「{display_name}」使用单独密钥加密(端到端加密)。\n\n" - msg += "请选择对应的私钥文件(.key):\n" - msg += "提示:此文件加密时会在同目录生成同名.key文件。" - + msg = f"文件「{display_name}」使用单独密钥加密。\n\n请选择对应的私钥文件(.key)" if is_cloud_file: - msg = f"云端文件「{display_name}」使用单独密钥加密(端到端加密)。\n\n" - msg += "服务器不存储您的私钥,请选择您本地保存的对应.key文件:\n" - msg += "提示:创建此文件时会在本地生成同名的.key文件。" + msg = f"云端文件「{display_name}」使用单独密钥加密。\n\n服务器不存储您的私钥,请选择本地保存的.key文件。" key_file_path, ok = QFileDialog.getOpenFileName( - parent_window, - "选择密钥文件 - " + display_name, - "", + parent_window, "选择密钥文件 - " + display_name, "", "密钥文件 (*.key);;所有文件 (*.*)" ) if not ok or not key_file_path: - QMessageBox.warning(parent_window, "密钥缺失", - f"无法解密「{display_name}」:缺少对应的私钥文件\n\n" - "此文件是使用【安全加密】模式创建的,\n" - "需要当初保存时生成的.key文件才能解密。\n" - "请找到对应的.key文件后重试。") - return None, False + QMessageBox.warning(parent_window, "密钥缺失", "缺少私钥文件,无法解密") + return None, False, None try: with open(key_file_path, 'rb') as f: single_private = f.read() - if DEBUG: - debug_log(f"用户选择密钥文件: {key_file_path}, 长度: {len(single_private)}字节") except Exception as e: - debug_error(f"读取用户选择的密钥文件失败: {e}") + debug_error(f"读取密钥文件失败: {e}") QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}") - return None, False + return None, False, None + + entered_password = None - # ========== 处理密码保护模式 ========== if has_password: - if DEBUG: - debug_log("检测到密码保护,弹出密码输入框") - max_attempts = 3 attempts = 0 success = False @@ -499,15 +1222,12 @@ def decrypt_bjb_file(file_path, parent_window, is_cloud_file=False, cloud_filena f"文件「{display_name}」受密码保护\n请输入密码 (尝试 {attempts + 1}/{max_attempts}):", QLineEdit.Password ) + if not ok: - if DEBUG: - debug_log("用户取消输入密码") - return None, False + return None, False, None if pwd: try: - if DEBUG: - debug_log(f"尝试密码解密 (长度: {len(pwd)})") decrypted = EncryptedNoteHandler.decrypt_file( file_path, password=pwd, single_private=single_private ) @@ -515,48 +1235,23 @@ def decrypt_bjb_file(file_path, parent_window, is_cloud_file=False, cloud_filena content = decrypted.decode('utf-8') else: content = str(decrypted) - if DEBUG: - debug_log("密码解密成功") - return content, True - except ValueError as e: - error_msg = str(e) - if DEBUG: - debug_log(f"密码解密失败: {error_msg}") - - if "单独密钥" in error_msg: - QMessageBox.warning(parent_window, "密钥错误", - "解密失败:密钥文件不匹配\n请选择正确的.key文件") - return None, False - + entered_password = pwd + success = True + except Exception as e: attempts += 1 if attempts < max_attempts: QMessageBox.warning(parent_window, "密码错误", f"密码错误,还剩 {max_attempts - attempts} 次尝试") else: - QMessageBox.warning(parent_window, "解密失败", - "多次密码错误,无法解密文件") - return None, False - except Exception as e: - error_msg = str(e) - if DEBUG: - debug_log(f"解密异常: {error_msg}") - attempts += 1 - if attempts < max_attempts: - QMessageBox.warning(parent_window, "解密失败", - f"解密失败: {error_msg}\n还剩 {max_attempts - attempts} 次尝试") - else: - QMessageBox.warning(parent_window, "解密失败", - f"多次尝试失败,无法解密文件\n错误: {error_msg}") - return None, False + QMessageBox.warning(parent_window, "解密失败", "多次密码错误,无法解密") + return None, False, None else: attempts += 1 QMessageBox.warning(parent_window, "密码为空", "密码不能为空") - return None, False + if not success: + return None, False, None else: - # ========== 无密码保护,直接解密 ========== - if DEBUG: - debug_log("无密码保护,直接解密") try: decrypted = EncryptedNoteHandler.decrypt_file( file_path, password=None, single_private=single_private @@ -565,60 +1260,49 @@ def decrypt_bjb_file(file_path, parent_window, is_cloud_file=False, cloud_filena content = decrypted.decode('utf-8') else: content = str(decrypted) - return content, True - except ValueError as e: - error_msg = str(e) - if "单独密钥" in error_msg: - QMessageBox.warning(parent_window, "密钥错误", - "解密失败:需要对应的.key密钥文件\n请选择正确的.key文件") - else: - QMessageBox.warning(parent_window, "解密失败", str(e)) - return None, False except Exception as e: debug_error(f"解密失败: {e}") QMessageBox.critical(parent_window, "解密失败", str(e)) - return None, False + return None, False, None + + encrypt_config = EncryptedNoteHandler.extract_encrypt_config( + file_path, password=entered_password, single_private=single_private + ) + + return content, True, encrypt_config except Exception as e: - debug_error(f"解密过程异常: {e}") + debug_error(f"解密异常: {e}") QMessageBox.critical(parent_window, "解密失败", str(e)) - return None, False + return None, False, None + +# ============================================================================ +# 云端 API 客户端 +# ============================================================================ -# ==================== 云端API客户端 ==================== class CloudClient: - """云端API客户端,处理所有网络请求""" def __init__(self): - if DEBUG: - debug_log("初始化CloudClient") self.token = None self.user_id = None self.username = None - def set_auth(self, token, user_id, username): - """设置认证信息""" - if DEBUG: - debug_log(f"设置认证信息 - 用户: {username}, ID: {user_id}") + def set_auth(self, token: str, user_id: int, username: str) -> None: self.token = token self.user_id = user_id self.username = username - def clear_auth(self): - """清除认证信息""" - if DEBUG: - debug_log("清除认证信息") + def clear_auth(self) -> None: self.token = None self.user_id = None self.username = None - def _headers(self): - """获取请求头""" + def _headers(self) -> dict: if self.token: return {'x-access-token': self.token} return {} - def _request(self, method, endpoint, **kwargs): - """发送HTTP请求""" + def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response: url = f"{API_BASE_URL}{endpoint}" headers = self._headers() if 'headers' in kwargs: @@ -630,44 +1314,42 @@ class CloudClient: return None return resp except requests.exceptions.ConnectionError: - QMessageBox.critical(None, "网络错误", "无法连接到服务器,请检查网络连接") + QMessageBox.critical(None, "网络错误", "无法连接到服务器") return None except Exception as e: QMessageBox.critical(None, "网络错误", f"请求失败: {str(e)}") return None - def register(self, username, email, password): - """用户注册""" - resp = self._request('POST', '/register', json={'username': username, 'email': email, 'password': password}) + def register(self, username: str, email: str, password: str) -> dict: + resp = self._request('POST', '/register', + json={'username': username, 'email': email, 'password': password}) if resp and resp.status_code == 201: return resp.json() return None - def login(self, email, password): - """用户登录""" - resp = self._request('POST', '/login', json={'email': email, 'password': password}) + def login(self, email: str, password: str) -> dict: + resp = self._request('POST', '/login', + json={'email': email, 'password': password}) if resp and resp.status_code == 200: data = resp.json() self.set_auth(data['token'], data['user_id'], data['username']) return data return None - def get_filetree(self, folder='/'): - """获取文件树""" + def get_filetree(self, folder: str = '/') -> dict: resp = self._request('GET', '/filetree', params={'folder': folder}) if resp and resp.status_code == 200: return resp.json() return None - def create_folder(self, name, parent='/'): - """创建文件夹""" - resp = self._request('POST', '/create_folder', json={'name': name, 'parent': parent}) + def create_folder(self, name: str, parent: str = '/') -> dict: + resp = self._request('POST', '/create_folder', + json={'name': name, 'parent': parent}) if resp and resp.status_code == 201: return resp.json() return None - def upload_file(self, file_path, parent_folder='/'): - """上传文件""" + def upload_file(self, file_path: str, parent_folder: str = '/') -> dict: ext = os.path.splitext(file_path)[1].lower() if ext not in ['.txt', '.bjb']: QMessageBox.warning(None, "错误", "只支持上传 .txt 和 .bjb 文件") @@ -677,12 +1359,12 @@ class CloudClient: files = {'file': (os.path.basename(file_path), f)} data = {'parent_folder': parent_folder} resp = self._request('POST', '/upload', files=files, data=data) + if resp and resp.status_code == 201: return resp.json() return None - def download_file(self, file_id, target_path): - """下载文件""" + def download_file(self, file_id: int, target_path: str) -> bool: resp = self._request('GET', f'/download/{file_id}') if resp and resp.status_code == 200: with open(target_path, 'wb') as f: @@ -690,121 +1372,153 @@ class CloudClient: return True return False - def delete_item(self, item_id): - """删除文件或文件夹""" + def delete_item(self, item_id: int) -> bool: resp = self._request('DELETE', f'/delete/{item_id}') if resp: return resp.status_code == 200 return False - def rename_item(self, item_id, new_name): - """重命名文件或文件夹""" - resp = self._request('PUT', f'/rename/{item_id}', json={'new_name': new_name}) + def rename_item(self, item_id: int, new_name: str) -> bool: + resp = self._request('PUT', f'/rename/{item_id}', + json={'new_name': new_name}) if resp: return resp.status_code == 200 return False - def move_item(self, item_id, target_folder): - """移动文件或文件夹""" - resp = self._request('POST', '/move', json={'item_id': item_id, 'target_folder': target_folder}) + def move_item(self, item_id: int, target_folder: str) -> bool: + resp = self._request('POST', '/move', + json={'item_id': item_id, 'target_folder': target_folder}) if resp: return resp.status_code == 200 return False - def search_files(self, keyword): - """搜索文件""" + def search_files(self, keyword: str) -> dict: resp = self._request('GET', '/search', params={'q': keyword}) if resp and resp.status_code == 200: return resp.json() return None - def get_storage_info(self): - """获取存储信息""" + def get_storage_info(self) -> dict: resp = self._request('GET', '/storage_info') if resp and resp.status_code == 200: return resp.json() return None - def get_user_info(self): - """获取用户信息""" + def get_user_info(self) -> dict: resp = self._request('GET', '/user_info') if resp and resp.status_code == 200: return resp.json() return None -# ==================== 登录对话框 ==================== +# ============================================================================ +# 现代化登录对话框 +# ============================================================================ + class LoginDialog(QDialog): - """登录对话框""" - def __init__(self, client, parent=None): + def __init__(self, client: CloudClient, parent: QWidget = None): super().__init__(parent) - if DEBUG: - debug_log("初始化登录对话框") self.client = client self.setWindowTitle("云笔记 - 登录") - self.setFixedSize(400, 350) self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) self.setAttribute(Qt.WA_TranslucentBackground) - self.setup_ui() + self._setup_ui() - def setup_ui(self): - """设置UI""" - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) + def _setup_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + # 卡片容器 card = QWidget() card.setObjectName("card") card.setStyleSheet(""" - #card { background-color: white; border-radius: 16px; } - QLineEdit { border: 1px solid #e1e4e8; border-radius: 10px; padding: 12px; font-size: 14px; } - QPushButton { border-radius: 10px; padding: 12px; font-size: 14px; font-weight: 500; } + #card { + background-color: #1e1e2f; + border-radius: 24px; + } """) - card_layout = QVBoxLayout(card) - card_layout.setContentsMargins(40, 40, 40, 40) - card_layout.setSpacing(20) + layout = QVBoxLayout(card) + layout.setContentsMargins(40, 40, 40, 40) + layout.setSpacing(20) - title = QLabel("☁️ 云笔记") - title.setStyleSheet("font-size: 28px; font-weight: bold;") + # Logo 和标题 + title_layout = QVBoxLayout() + title_layout.setAlignment(Qt.AlignCenter) + + logo = QLabel("☁️") + logo.setStyleSheet("font-size: 48px;") + logo.setAlignment(Qt.AlignCenter) + title_layout.addWidget(logo) + + title = QLabel("云笔记") + title.setStyleSheet("font-size: 28px; font-weight: bold; color: #89b4fa;") title.setAlignment(Qt.AlignCenter) - card_layout.addWidget(title) + title_layout.addWidget(title) + + subtitle = QLabel("安全加密笔记系统") + subtitle.setStyleSheet("font-size: 14px; color: #6c7086;") + subtitle.setAlignment(Qt.AlignCenter) + title_layout.addWidget(subtitle) + + layout.addLayout(title_layout) + # 输入框 self.email_input = QLineEdit() self.email_input.setPlaceholderText("电子邮箱") self.email_input.setMinimumHeight(45) - card_layout.addWidget(self.email_input) + layout.addWidget(self.email_input) self.password_input = QLineEdit() self.password_input.setPlaceholderText("密码") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setMinimumHeight(45) - card_layout.addWidget(self.password_input) + layout.addWidget(self.password_input) + # 记住我 self.remember_check = QCheckBox("记住我") - card_layout.addWidget(self.remember_check) + layout.addWidget(self.remember_check) + # 登录按钮 self.login_btn = QPushButton("登 录") + self.login_btn.setProperty("primary", True) self.login_btn.setMinimumHeight(45) - self.login_btn.setStyleSheet("background-color: #0366d6; color: white; border: none;") - self.login_btn.clicked.connect(self.do_login) - card_layout.addWidget(self.login_btn) + self.login_btn.clicked.connect(self._do_login) + layout.addWidget(self.login_btn) + # 注册链接 register_btn = QPushButton("还没有账号?立即注册") - register_btn.setStyleSheet("background: transparent; color: #0366d6; border: none;") - register_btn.clicked.connect(self.show_register) - card_layout.addWidget(register_btn, alignment=Qt.AlignCenter) + register_btn.setStyleSheet("background: transparent; color: #89b4fa; border: none;") + register_btn.clicked.connect(self._show_register) + layout.addWidget(register_btn, alignment=Qt.AlignCenter) + main_layout.addWidget(card) + + # 关闭按钮 close_btn = QPushButton("×") - close_btn.setFixedSize(30, 30) - close_btn.setStyleSheet("background: transparent; border: none; font-size: 20px;") + close_btn.setFixedSize(32, 32) + close_btn.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + font-size: 20px; + color: #6c7086; + } + QPushButton:hover { + color: #f38ba8; + } + """) close_btn.clicked.connect(self.reject) - - layout.addWidget(card) close_btn.setParent(self) - close_btn.move(self.width() - 40, 15) + close_btn.move(20, 20) - def do_login(self): - """执行登录""" + def resizeEvent(self, event): + close_btn = self.findChild(QPushButton) + if close_btn: + close_btn.move(self.width() - 52, 20) + super().resizeEvent(event) + + def _do_login(self): email = self.email_input.text().strip() password = self.password_input.text().strip() @@ -823,87 +1537,100 @@ class LoginDialog(QDialog): else: QMessageBox.warning(self, "错误", "邮箱或密码错误") - def show_register(self): - """显示注册对话框""" + def _show_register(self): dialog = RegisterDialog(self.client, self) dialog.exec_() -# ==================== 注册对话框 ==================== +# ============================================================================ +# 现代化注册对话框 +# ============================================================================ + class RegisterDialog(QDialog): - """注册对话框""" - def __init__(self, client, parent=None): + def __init__(self, client: CloudClient, parent: QWidget = None): super().__init__(parent) - if DEBUG: - debug_log("初始化注册对话框") self.client = client self.setWindowTitle("注册账号") - self.setFixedSize(400, 450) self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) self.setAttribute(Qt.WA_TranslucentBackground) - self.setup_ui() + self._setup_ui() - def setup_ui(self): - """设置UI""" - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) + def _setup_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) card = QWidget() card.setObjectName("card") card.setStyleSheet(""" - #card { background-color: white; border-radius: 16px; } - QLineEdit { border: 1px solid #e1e4e8; border-radius: 10px; padding: 12px; font-size: 14px; } - QPushButton { border-radius: 10px; padding: 12px; font-size: 14px; font-weight: 500; } + #card { + background-color: #1e1e2f; + border-radius: 24px; + } """) - card_layout = QVBoxLayout(card) - card_layout.setContentsMargins(40, 40, 40, 40) - card_layout.setSpacing(15) + layout = QVBoxLayout(card) + layout.setContentsMargins(40, 40, 40, 40) + layout.setSpacing(16) title = QLabel("📝 创建新账号") - title.setStyleSheet("font-size: 24px; font-weight: bold;") + title.setStyleSheet("font-size: 24px; font-weight: bold; color: #89b4fa;") title.setAlignment(Qt.AlignCenter) - card_layout.addWidget(title) + layout.addWidget(title) self.username_input = QLineEdit() self.username_input.setPlaceholderText("用户名") self.username_input.setMinimumHeight(45) - card_layout.addWidget(self.username_input) + layout.addWidget(self.username_input) self.email_input = QLineEdit() self.email_input.setPlaceholderText("电子邮箱") self.email_input.setMinimumHeight(45) - card_layout.addWidget(self.email_input) + layout.addWidget(self.email_input) self.password_input = QLineEdit() self.password_input.setPlaceholderText("密码(至少6位)") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setMinimumHeight(45) - card_layout.addWidget(self.password_input) + layout.addWidget(self.password_input) self.confirm_input = QLineEdit() self.confirm_input.setPlaceholderText("确认密码") self.confirm_input.setEchoMode(QLineEdit.Password) self.confirm_input.setMinimumHeight(45) - card_layout.addWidget(self.confirm_input) + layout.addWidget(self.confirm_input) self.register_btn = QPushButton("注 册") + self.register_btn.setProperty("primary", True) self.register_btn.setMinimumHeight(45) - self.register_btn.setStyleSheet("background-color: #0366d6; color: white; border: none;") - self.register_btn.clicked.connect(self.do_register) - card_layout.addWidget(self.register_btn) + self.register_btn.clicked.connect(self._do_register) + layout.addWidget(self.register_btn) + + main_layout.addWidget(card) close_btn = QPushButton("×") - close_btn.setFixedSize(30, 30) - close_btn.setStyleSheet("background: transparent; border: none; font-size: 20px;") + close_btn.setFixedSize(32, 32) + close_btn.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + font-size: 20px; + color: #6c7086; + } + QPushButton:hover { + color: #f38ba8; + } + """) close_btn.clicked.connect(self.reject) - - layout.addWidget(card) close_btn.setParent(self) - close_btn.move(self.width() - 40, 15) + close_btn.move(20, 20) - def do_register(self): - """执行注册""" + def resizeEvent(self, event): + close_btn = self.findChild(QPushButton) + if close_btn: + close_btn.move(self.width() - 52, 20) + super().resizeEvent(event) + + def _do_register(self): username = self.username_input.text().strip() email = self.email_input.text().strip() password = self.password_input.text().strip() @@ -926,16 +1653,17 @@ class RegisterDialog(QDialog): else: QMessageBox.warning(self, "错误", "注册失败,用户名或邮箱已存在") -# ==================== 新建加密文件对话框 ==================== +# ============================================================================ +# 新建加密文件对话框 - 现代化设计 +# ============================================================================ + class NewEncryptedFileDialog(QDialog): - """新建加密文件配置对话框""" - def __init__(self, parent=None): + def __init__(self, parent: QWidget = None): super().__init__(parent) - if DEBUG: - debug_log("初始化新建加密文件对话框") self.setWindowTitle("新建加密笔记") - self.setFixedSize(600, 700) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) + self.setAttribute(Qt.WA_TranslucentBackground) self.selected_algorithm = 'rsa' self.selected_key_mode = 'global' self.use_password = False @@ -943,18 +1671,65 @@ class NewEncryptedFileDialog(QDialog): self.custom_password = None self.single_private = None self.single_public = None - self.setup_ui() + self._setup_ui() - def setup_ui(self): - """设置UI""" - layout = QVBoxLayout(self) - layout.setSpacing(15) + def _setup_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + + card = QWidget() + card.setObjectName("card") + card.setStyleSheet(""" + #card { + background-color: #1e1e2f; + border-radius: 24px; + } + """) + + layout = QVBoxLayout(card) + layout.setContentsMargins(30, 30, 30, 30) + layout.setSpacing(20) - title = QLabel("🔐 加密配置") - title.setStyleSheet("font-size: 18px; font-weight: bold;") - layout.addWidget(title) + # 标题栏 + title_layout = QHBoxLayout() + title = QLabel("🔐 新建加密笔记") + title.setStyleSheet("font-size: 20px; font-weight: bold; color: #89b4fa;") + title_layout.addWidget(title) + title_layout.addStretch() + + close_btn = QPushButton("×") + close_btn.setFixedSize(32, 32) + close_btn.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + font-size: 20px; + color: #6c7086; + } + QPushButton:hover { + color: #f38ba8; + } + """) + close_btn.clicked.connect(self.reject) + title_layout.addWidget(close_btn) + layout.addLayout(title_layout) - group1 = QGroupBox("1. 选择加密算法") + # 1. 加密算法 + group1 = QGroupBox("加密算法") + group1.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) group1_layout = QVBoxLayout() self.radio_rsa = QRadioButton("RSA-4096 + AES-256-GCM") self.radio_x25519 = QRadioButton("X25519 + AES-256-GCM") @@ -964,7 +1739,22 @@ class NewEncryptedFileDialog(QDialog): group1.setLayout(group1_layout) layout.addWidget(group1) - group2 = QGroupBox("2. 选择密钥方式") + # 2. 密钥方式 + group2 = QGroupBox("密钥方式") + group2.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) group2_layout = QVBoxLayout() self.radio_global = QRadioButton("默认加密(使用全局密钥对)") self.radio_single = QRadioButton("安全加密(为当前文件单独生成密钥对)") @@ -974,9 +1764,25 @@ class NewEncryptedFileDialog(QDialog): group2.setLayout(group2_layout) layout.addWidget(group2) - group3 = QGroupBox("3. 额外密码保护") + # 3. 密码保护 + group3 = QGroupBox("密码保护(可选)") + group3.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) group3_layout = QVBoxLayout() + # 强度模式选择 strength_layout = QHBoxLayout() strength_layout.addWidget(QLabel("密码强度要求:")) self.radio_strict = QRadioButton("高强度 (推荐)") @@ -986,8 +1792,8 @@ class NewEncryptedFileDialog(QDialog): strength_layout.addWidget(self.radio_weak) group3_layout.addLayout(strength_layout) - self.checkbox_password = QCheckBox("启用双层加密") - self.checkbox_password.toggled.connect(self.on_password_toggled) + self.checkbox_password = QCheckBox("启用密码保护") + self.checkbox_password.toggled.connect(self._on_password_toggled) group3_layout.addWidget(self.checkbox_password) self.password_input = QLineEdit() @@ -1006,96 +1812,77 @@ class NewEncryptedFileDialog(QDialog): self.password_strength_label.setStyleSheet("font-size: 12px;") group3_layout.addWidget(self.password_strength_label) - self.password_input.textChanged.connect(self.check_password_strength) - self.radio_strict.toggled.connect(self.on_strength_mode_changed) - self.radio_weak.toggled.connect(self.on_strength_mode_changed) + self.password_input.textChanged.connect(self._check_password_strength) + self.radio_strict.toggled.connect(self._on_strength_mode_changed) + self.radio_weak.toggled.connect(self._on_strength_mode_changed) group3.setLayout(group3_layout) layout.addWidget(group3) - info_label = QLabel("说明:\n• 默认加密:使用全局密钥对,所有文件共用同一密钥\n• 安全加密:每个文件独立生成密钥对,私钥保存为 .key 文件\n• 密钥文件请妥善保管,丢失后将无法解密文件!\n• 高强度密码要求≥16位含大小写+数字+特殊字符\n• 低强度模式仅用于测试,不推荐用于重要数据") - info_label.setStyleSheet("color: #586069; font-size: 12px; background-color: #f6f8fa; padding: 10px; border-radius: 8px;") + # 提示信息 + info_label = QLabel( + "💡 提示:\n" + "• 全局密钥:所有文件共用同一密钥\n" + "• 单独密钥:每个文件独立生成密钥对\n" + "• 密钥文件请妥善保管,丢失后将无法解密!" + ) + info_label.setStyleSheet( + "color: #a6adc8; font-size: 12px; background-color: #181825; " + "padding: 12px; border-radius: 12px;" + ) info_label.setWordWrap(True) layout.addWidget(info_label) + # 按钮 btn_layout = QHBoxLayout() - self.ok_btn = QPushButton("确定") + btn_layout.setSpacing(12) + self.ok_btn = QPushButton("创建加密笔记") + self.ok_btn.setProperty("primary", True) self.cancel_btn = QPushButton("取消") - self.ok_btn.clicked.connect(self.accept_config) + self.ok_btn.clicked.connect(self._accept_config) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.ok_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) - def on_strength_mode_changed(self): - """密码强度模式改变""" + main_layout.addWidget(card) + + def _on_strength_mode_changed(self): self.strict_password = self.radio_strict.isChecked() if self.radio_strict.isChecked(): - self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)") + self.password_input.setPlaceholderText( + "请输入密码(至少16位,包含大小写字母、数字和特殊字符)" + ) else: - self.password_input.setPlaceholderText("请输入密码(无强度限制,但强烈建议使用强密码)") - self.check_password_strength() + self.password_input.setPlaceholderText( + "请输入密码(无强度限制,但强烈建议使用强密码)" + ) + self._check_password_strength() - def on_password_toggled(self, checked): - """密码开关切换""" + def _on_password_toggled(self, checked: bool): self.password_input.setEnabled(checked) self.confirm_input.setEnabled(checked) self.use_password = checked if checked: - self.check_password_strength() + self._check_password_strength() else: self.password_strength_label.setText("") - def check_password_strength(self): - """检查密码强度""" + def _check_password_strength(self): if not self.use_password: self.password_strength_label.setText("") return pwd = self.password_input.text() - if self.radio_strict.isChecked(): - if len(pwd) >= 16 and any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd) and any(not c.isalnum() for c in pwd): - self.password_strength_label.setText("✓ 密码强度:强") - self.password_strength_label.setStyleSheet("color: green; font-size: 12px;") - elif len(pwd) >= 12: - self.password_strength_label.setText("⚠ 密码强度:中(建议至少16位,包含大小写+数字+特殊字符)") - self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") - else: - self.password_strength_label.setText("✗ 密码强度:弱(需要至少16位,包含大小写+数字+特殊字符)") - self.password_strength_label.setStyleSheet("color: red; font-size: 12px;") - else: - if len(pwd) >= 8: - self.password_strength_label.setText("✓ 密码已输入(低强度模式,建议使用高强度)") - self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") - elif len(pwd) > 0: - self.password_strength_label.setText("⚠ 密码太短(建议至少8位)") - self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") - else: - self.password_strength_label.setText("请输入密码") - self.password_strength_label.setStyleSheet("color: gray; font-size: 12px;") - - def accept_config(self): - """接受配置""" - if DEBUG: - debug_log("开始获取加密配置") + strength = check_password_strength(pwd) - if self.radio_rsa.isChecked(): - self.selected_algorithm = 'rsa' - else: - self.selected_algorithm = 'x25519' - if DEBUG: - debug_log(f"算法: {self.selected_algorithm}") - - if self.radio_global.isChecked(): - self.selected_key_mode = 'global' - else: - self.selected_key_mode = 'single' - if DEBUG: - debug_log(f"密钥模式: {self.selected_key_mode}") + self.password_strength_label.setText(f"🔐 密码强度: {strength['level']} - {strength['message']}") + self.password_strength_label.setStyleSheet(f"color: {strength['color']}; font-size: 12px;") + def _accept_config(self): + self.selected_algorithm = 'x25519' if self.radio_x25519.isChecked() else 'rsa' + self.selected_key_mode = 'single' if self.radio_single.isChecked() else 'global' self.strict_password = self.radio_strict.isChecked() - if DEBUG: - debug_log(f"密码强度模式: {'高强度' if self.strict_password else '低强度'}") if self.use_password: pwd = self.password_input.text() @@ -1108,30 +1895,28 @@ class NewEncryptedFileDialog(QDialog): QMessageBox.warning(self, "错误", "两次输入的密码不一致") return - if self.strict_password and not is_password_strong(pwd, strict=True): - QMessageBox.warning(self, "密码强度不足", - "密码必须至少16位,包含大小写字母、数字和特殊字符\n\n" - "或切换到「低强度」模式(不推荐)") - return + strength = check_password_strength(pwd) + if strength['level'] in ['极弱', '弱']: + reply = QMessageBox.warning( + self, "密码强度不足", + f"您设置的密码强度为「{strength['level']}」,{strength['message']}\n\n" + "是否继续使用此密码?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + return self.custom_password = pwd - if DEBUG: - debug_log("密码验证通过") if self.selected_key_mode == 'single': - if DEBUG: - debug_log("生成单独密钥对") if self.selected_algorithm == 'rsa': self.single_private, self.single_public = generate_rsa_key(4096) else: self.single_private, self.single_public = generate_x25519_keys() - if DEBUG: - debug_log("加密配置获取完成") self.accept() - def get_config(self): - """获取配置""" + def get_config(self) -> dict: return { 'algorithm': self.selected_algorithm, 'key_mode': self.selected_key_mode, @@ -1142,15 +1927,17 @@ class NewEncryptedFileDialog(QDialog): 'single_public': self.single_public } -# ==================== 保存加密文件对话框 ==================== +# ============================================================================ +# 保存加密文件对话框 - 现代化设计 +# ============================================================================ + class SaveEncryptedFileDialog(QDialog): - """保存加密文件对话框""" - def __init__(self, content, algorithm, key_mode, use_password, strict_password, - custom_password, single_private, single_public, parent=None): + def __init__(self, content: str, algorithm: str, key_mode: str, + use_password: bool, strict_password: bool, + custom_password: str, single_private: bytes, + single_public: bytes, parent: QWidget = None): super().__init__(parent) - if DEBUG: - debug_log("初始化保存加密文件对话框") self.content = content self.algorithm = algorithm self.key_mode = key_mode @@ -1160,36 +1947,96 @@ class SaveEncryptedFileDialog(QDialog): self.single_private = single_private self.single_public = single_public self.setWindowTitle("保存加密笔记") - self.setFixedSize(650, 400) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) + self.setAttribute(Qt.WA_TranslucentBackground) self.selected_file_path = None - self.setup_ui() + self._setup_ui() - def setup_ui(self): - """设置UI""" - layout = QVBoxLayout(self) - layout.setSpacing(15) + def _setup_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + + card = QWidget() + card.setObjectName("card") + card.setStyleSheet(""" + #card { + background-color: #1e1e2f; + border-radius: 24px; + } + """) + + layout = QVBoxLayout(card) + layout.setContentsMargins(30, 30, 30, 30) + layout.setSpacing(20) + + # 标题栏 + title_layout = QHBoxLayout() + title = QLabel("💾 保存加密笔记") + title.setStyleSheet("font-size: 20px; font-weight: bold; color: #89b4fa;") + title_layout.addWidget(title) + title_layout.addStretch() + + close_btn = QPushButton("×") + close_btn.setFixedSize(32, 32) + close_btn.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + font-size: 20px; + color: #6c7086; + } + QPushButton:hover { + color: #f38ba8; + } + """) + close_btn.clicked.connect(self.reject) + title_layout.addWidget(close_btn) + layout.addLayout(title_layout) if self.key_mode == 'single': - info_label = QLabel("⚠️ 您选择了【安全加密】模式,将为当前文件单独生成密钥对。\n密钥文件(.key)将与.bjb文件保存在同一目录,请妥善保管!\n丢失密钥文件后将无法解密!") - info_label.setStyleSheet("color: #e36209; background-color: #fff5eb; padding: 10px; border-radius: 8px;") + info_label = QLabel( + "⚠️ 您选择了【安全加密】模式,将为当前文件单独生成密钥对。\n" + "密钥文件(.key)将与.bjb文件保存在同一目录,请妥善保管!\n" + "丢失密钥文件后将无法解密!" + ) + info_label.setStyleSheet( + "color: #f9e2af; background-color: #313244; " + "padding: 12px; border-radius: 12px;" + ) info_label.setWordWrap(True) layout.addWidget(info_label) - layout.addWidget(QLabel("📁 保存位置和文件名:")) + # 文件选择 + layout.addWidget(QLabel("📁 保存位置:")) file_select_layout = QHBoxLayout() self.file_path_input = QLineEdit() self.file_path_input.setPlaceholderText("请选择保存位置...") - self.file_path_input.setMinimumHeight(35) - self.file_path_input.textChanged.connect(self.on_path_changed) + self.file_path_input.setMinimumHeight(40) + self.file_path_input.textChanged.connect(self._on_path_changed) file_select_layout.addWidget(self.file_path_input) self.browse_btn = QPushButton("浏览...") - self.browse_btn.clicked.connect(self.select_save_file) + self.browse_btn.clicked.connect(self._select_save_file) file_select_layout.addWidget(self.browse_btn) layout.addLayout(file_select_layout) + # 加密信息 info_group = QGroupBox("加密信息摘要") + info_group.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) info_layout = QVBoxLayout() algo_text = "RSA-4096" if self.algorithm == 'rsa' else "X25519" @@ -1206,23 +2053,25 @@ class SaveEncryptedFileDialog(QDialog): info_group.setLayout(info_layout) layout.addWidget(info_group) + # 按钮 btn_layout = QHBoxLayout() - self.ok_btn = QPushButton("💾 保存") - self.ok_btn.setStyleSheet("background-color: #28a745; color: white;") + btn_layout.setSpacing(12) + self.ok_btn = QPushButton("保存文件") + self.ok_btn.setProperty("primary", True) self.cancel_btn = QPushButton("取消") - self.ok_btn.clicked.connect(self.do_save) + self.ok_btn.clicked.connect(self._do_save) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.ok_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) - def on_path_changed(self, text): - """路径改变""" + main_layout.addWidget(card) + + def _on_path_changed(self, text: str): if text.strip(): self.selected_file_path = text.strip() - def select_save_file(self): - """选择保存文件""" + def _select_save_file(self): file_path, _ = QFileDialog.getSaveFileName(self, "保存加密笔记", "", "加密笔记 (*.bjb)") if file_path: if not file_path.endswith('.bjb'): @@ -1230,8 +2079,7 @@ class SaveEncryptedFileDialog(QDialog): self.selected_file_path = file_path self.file_path_input.setText(file_path) - def do_save(self): - """执行保存""" + def _do_save(self): if not self.selected_file_path: QMessageBox.warning(self, "提示", "请选择保存位置") return @@ -1275,365 +2123,17 @@ class SaveEncryptedFileDialog(QDialog): QMessageBox.information(self, "保存成功", msg) self.accept() -# ==================== 云端文件树窗口 ==================== -class CloudFileTreeDialog(QDialog): - """云端文件管理对话框""" - - def __init__(self, client, parent=None): - super().__init__(parent) - if DEBUG: - debug_log("初始化云端文件树对话框") - self.client = client - self.setWindowTitle("云端文件管理") - self.resize(900, 700) - self.setup_ui() - self.current_folder = '/' - self.load_filetree() +# ============================================================================ +# 加密当前文件对话框 - 现代化设计 +# ============================================================================ - def setup_ui(self): - """设置UI""" - layout = QVBoxLayout(self) - - toolbar = QToolBar() - self.up_btn = QAction("⬆ 返回上级", self) - self.up_btn.triggered.connect(self.go_up) - toolbar.addAction(self.up_btn) - self.refresh_btn = QAction("🔄 刷新", self) - self.refresh_btn.triggered.connect(self.load_filetree) - toolbar.addAction(self.refresh_btn) - self.new_folder_btn = QAction("📁 新建文件夹", self) - self.new_folder_btn.triggered.connect(self.create_folder) - toolbar.addAction(self.new_folder_btn) - self.upload_btn = QAction("📤 上传文件", self) - self.upload_btn.triggered.connect(self.upload_file) - toolbar.addAction(self.upload_btn) - layout.addWidget(toolbar) - - search_layout = QHBoxLayout() - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("搜索文件...") - self.search_input.returnPressed.connect(self.search_files) - search_btn = QPushButton("搜索") - search_btn.clicked.connect(self.search_files) - clear_btn = QPushButton("清除") - clear_btn.clicked.connect(self.clear_search) - search_layout.addWidget(self.search_input) - search_layout.addWidget(search_btn) - search_layout.addWidget(clear_btn) - layout.addLayout(search_layout) - - self.tree = QTreeWidget() - self.tree.setHeaderLabels(["名称", "大小", "类型", "修改时间"]) - self.tree.setAlternatingRowColors(True) - self.tree.setContextMenuPolicy(Qt.CustomContextMenu) - self.tree.customContextMenuRequested.connect(self.show_context_menu) - self.tree.itemDoubleClicked.connect(self.on_item_double_click) - layout.addWidget(self.tree) - - self.status_label = QLabel("") - layout.addWidget(self.status_label) - - self.load_storage_info() - - def load_storage_info(self): - """加载存储信息""" - info = self.client.get_storage_info() - if info: - used_mb = info['total_size'] / (1024 * 1024) - max_mb = info['max_size'] / (1024 * 1024) - download_used_mb = info['download_used'] / (1024 * 1024) - download_max_mb = info['download_max'] / (1024 * 1024) - self.status_label.setText(f"存储: {used_mb:.1f}MB / {max_mb:.0f}MB | 今日下载: {download_used_mb:.1f}MB / {download_max_mb:.0f}MB") - - def load_filetree(self): - """加载文件树""" - if DEBUG: - debug_log(f"加载文件树: {self.current_folder}") - data = self.client.get_filetree(self.current_folder) - if not data: - QMessageBox.warning(self, "错误", "加载文件树失败") - return - self.tree.clear() - for item in data['items']: - size_str = self.format_size(item['size']) if item['size'] else "" - type_str = "文件夹" if item['is_folder'] else (item['type'] or "文件") - tree_item = QTreeWidgetItem([item['name'], size_str, type_str, item['created_at']]) - tree_item.setData(0, Qt.UserRole, item) - self.tree.addTopLevelItem(tree_item) - self.load_storage_info() - - def format_size(self, size_bytes): - """格式化文件大小""" - if not size_bytes: - return "" - for unit in ['B', 'KB', 'MB', 'GB']: - if size_bytes < 1024.0: - return f"{size_bytes:.1f}{unit}" - size_bytes /= 1024.0 - return f"{size_bytes:.1f}TB" - - def go_up(self): - """返回上级目录""" - if self.current_folder != '/': - parts = self.current_folder.rstrip('/').split('/') - if len(parts) > 1: - self.current_folder = '/' + '/'.join(parts[:-1]) - else: - self.current_folder = '/' - self.load_filetree() - - def create_folder(self): - """创建文件夹""" - name, ok = QInputDialog.getText(self, "新建文件夹", "请输入文件夹名称:") - if ok and name: - if '/' in name or '\\' in name: - QMessageBox.warning(self, "错误", "文件夹名称不能包含路径分隔符") - return - res = self.client.create_folder(name, self.current_folder) - if res: - self.load_filetree() - else: - QMessageBox.warning(self, "错误", "创建失败") - - def upload_file(self): - """上传文件""" - file_path, _ = QFileDialog.getOpenFileName(self, "选择要上传的文件", "", "笔记本文件 (*.bjb *.txt)") - if file_path: - ext = os.path.splitext(file_path)[1].lower() - if ext not in ['.txt', '.bjb']: - QMessageBox.warning(self, "错误", "只支持上传 .txt 和 .bjb 文件") - return - - file_size = os.path.getsize(file_path) - info = self.client.get_storage_info() - if info and info['total_size'] + file_size > info['max_size']: - QMessageBox.warning(self, "错误", "云端存储空间不足") - return - res = self.client.upload_file(file_path, self.current_folder) - if res: - QMessageBox.information(self, "成功", "上传成功") - self.load_filetree() - else: - QMessageBox.warning(self, "错误", "上传失败") - - def search_files(self): - """搜索文件""" - keyword = self.search_input.text().strip() - if not keyword: - return - data = self.client.search_files(keyword) - if data: - self.tree.clear() - for item in data['items']: - size_str = self.format_size(item['size']) if item['size'] else "" - type_str = "文件夹" if item['is_folder'] else (item['type'] or "文件") - tree_item = QTreeWidgetItem([item['name'], size_str, type_str, ""]) - tree_item.setData(0, Qt.UserRole, item) - self.tree.addTopLevelItem(tree_item) - self.status_label.setText(f"搜索到 {len(data['items'])} 个结果") - - def clear_search(self): - """清除搜索""" - self.search_input.clear() - self.load_filetree() - - def on_item_double_click(self, item, col): - """双击项目""" - data = item.data(0, Qt.UserRole) - if data['is_folder']: - if self.search_input.text(): - self.clear_search() - self.current_folder = data['parent'] + '/' + data['name'] if data['parent'] != '/' else '/' + data['name'] - self.load_filetree() - else: - self.download_and_open(data) - - def download_and_open(self, file_info): - """ - 下载并打开云端文件 - 修复:支持单独密钥模式,弹出文件选择对话框让用户选择.key文件 - """ - temp_dir = tempfile.gettempdir() - local_path = os.path.join(temp_dir, f"cloud_{file_info['id']}_{file_info['name']}") - - if DEBUG: - debug_log(f"下载文件到: {local_path}") - debug_log(f"文件信息: ID={file_info['id']}, Name={file_info['name']}, Type={file_info.get('type', 'unknown')}") - - if self.client.download_file(file_info['id'], local_path): - if DEBUG: - debug_log("下载成功") - - ext = os.path.splitext(file_info['name'])[1].lower() - - if ext == '.txt': - # 文本文件直接打开 - try: - with open(local_path, 'r', encoding='utf-8') as f: - content = f.read() - main_window = self.parent() - if isinstance(main_window, MainWindow): - tab = EditorTab(main_window.tabs, main_window.tabs.count(), - file_path=local_path, cloud_id=file_info['id'], - file_type='txt', content=content) - main_window.tabs.addTab(tab.text_edit, os.path.basename(file_info['name'])) - main_window.editor_tabs.append(tab) - main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1) - self.accept() - except Exception as e: - debug_error(f"读取文本文件失败: {e}") - QMessageBox.warning(self, "错误", f"读取文件失败: {str(e)}") - else: - # 加密文件:使用修复后的解密函数,传入云端文件标志和文件名 - if DEBUG: - debug_log("调用解密函数(云端文件模式)") - - # 先获取文件元数据,看看是否需要单独密钥 - metadata = EncryptedNoteHandler.get_file_metadata(local_path) - if metadata and metadata.get('single_key', False): - if DEBUG: - debug_log("检测到单独密钥模式,准备弹出密钥文件选择对话框") - - content, success = decrypt_bjb_file( - local_path, self, - is_cloud_file=True, - cloud_filename=file_info['name'] - ) - - if success and content is not None: - if DEBUG: - debug_log("解密成功,创建编辑器选项卡") - main_window = self.parent() - if isinstance(main_window, MainWindow): - tab = EditorTab(main_window.tabs, main_window.tabs.count(), - file_path=local_path, cloud_id=file_info['id'], - file_type='bjb', content=content) - main_window.tabs.addTab(tab.text_edit, os.path.basename(file_info['name'])) - main_window.editor_tabs.append(tab) - main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1) - self.accept() - else: - if DEBUG: - debug_log(f"警告: parent 不是 MainWindow 类型: {type(main_window)}") - else: - if DEBUG: - debug_log("解密失败,不关闭对话框") - # 解密失败时不关闭对话框,让用户重试 - else: - if DEBUG: - debug_log("下载失败") - QMessageBox.warning(self, "错误", "下载失败") - - def show_context_menu(self, pos): - """显示右键菜单""" - item = self.tree.itemAt(pos) - if not item: - return - data = item.data(0, Qt.UserRole) - menu = QMenu() - rename_action = menu.addAction("重命名") - delete_action = menu.addAction("删除") - move_action = menu.addAction("移动") - download_action = menu.addAction("下载") - action = menu.exec_(self.tree.viewport().mapToGlobal(pos)) - if action == rename_action: - new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=data['name']) - if ok and new_name: - if self.client.rename_item(data['id'], new_name): - self.load_filetree() - else: - QMessageBox.warning(self, "错误", "重命名失败") - elif action == delete_action: - if QMessageBox.question(self, "确认", f"确定删除 {data['name']} 吗?") == QMessageBox.Yes: - if self.client.delete_item(data['id']): - self.load_filetree() - else: - QMessageBox.warning(self, "错误", "删除失败") - elif action == move_action: - target, ok = QInputDialog.getText(self, "移动", "目标文件夹路径 (例如 /folder):") - if ok and target: - if self.client.move_item(data['id'], target): - self.load_filetree() - else: - QMessageBox.warning(self, "错误", "移动失败") - elif action == download_action: - save_path, _ = QFileDialog.getSaveFileName(self, "保存文件", data['name']) - if save_path: - if self.client.download_file(data['id'], save_path): - QMessageBox.information(self, "成功", "下载完成") - else: - QMessageBox.warning(self, "错误", "下载失败") - -# ==================== 编辑器选项卡 ==================== -class EditorTab: - """编辑器选项卡类""" - - def __init__(self, tab_widget, index, file_path=None, cloud_id=None, is_new=False, - content="", file_type="txt", encrypt_config=None): - if DEBUG: - debug_log(f"创建编辑器选项卡 - 类型: {file_type}, 新文件: {is_new}, 路径: {file_path}") - self.tab_widget = tab_widget - self.index = index - self.file_path = file_path - self.cloud_id = cloud_id - self.is_new = is_new - self.file_type = file_type - self.encrypt_config = encrypt_config - self.modified = False - self.text_edit = QTextEdit() - self.text_edit.setPlainText(content) - self.text_edit.textChanged.connect(self.on_text_changed) - - font = QFont("Consolas", 12) - self.text_edit.setFont(font) - - self.update_tab_title() - - def update_tab_title(self): - """更新选项卡标题""" - if self.file_path: - name = os.path.basename(self.file_path) - elif self.cloud_id is not None: - name = f"云端文件_{self.cloud_id}" - else: - name = "未命名" - if self.file_type == 'bjb': - name = f"🔒 {name}" - if self.modified: - name = "* " + name - self.tab_widget.setTabText(self.index, name) - - def on_text_changed(self): - """文本改变时的回调""" - if not self.modified: - self.modified = True - self.update_tab_title() - - def get_content(self): - """获取内容""" - return self.text_edit.toPlainText() - - def set_content(self, content): - """设置内容""" - self.text_edit.setPlainText(content) - self.modified = False - self.update_tab_title() - - def mark_saved(self): - """标记已保存""" - self.modified = False - self.update_tab_title() - -# ==================== 加密当前文件对话框 ==================== class EncryptCurrentFileDialog(QDialog): - """加密当前文件对话框""" - def __init__(self, parent=None): + def __init__(self, parent: QWidget = None): super().__init__(parent) - if DEBUG: - debug_log("初始化加密当前文件对话框") self.setWindowTitle("加密当前文件") - self.setFixedSize(600, 600) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) + self.setAttribute(Qt.WA_TranslucentBackground) self.selected_algorithm = 'rsa' self.selected_key_mode = 'global' self.use_password = False @@ -1641,22 +2141,72 @@ class EncryptCurrentFileDialog(QDialog): self.custom_password = None self.single_private = None self.single_public = None - self.setup_ui() + self._setup_ui() - def setup_ui(self): - """设置UI""" - layout = QVBoxLayout(self) - layout.setSpacing(15) + def _setup_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + + card = QWidget() + card.setObjectName("card") + card.setStyleSheet(""" + #card { + background-color: #1e1e2f; + border-radius: 24px; + } + """) + + layout = QVBoxLayout(card) + layout.setContentsMargins(30, 30, 30, 30) + layout.setSpacing(20) - title = QLabel("🔐 加密当前文件配置") - title.setStyleSheet("font-size: 18px; font-weight: bold;") - layout.addWidget(title) + # 标题栏 + title_layout = QHBoxLayout() + title = QLabel("🔐 加密当前文件") + title.setStyleSheet("font-size: 20px; font-weight: bold; color: #89b4fa;") + title_layout.addWidget(title) + title_layout.addStretch() + + close_btn = QPushButton("×") + close_btn.setFixedSize(32, 32) + close_btn.setStyleSheet(""" + QPushButton { + background: transparent; + border: none; + font-size: 20px; + color: #6c7086; + } + QPushButton:hover { + color: #f38ba8; + } + """) + close_btn.clicked.connect(self.reject) + title_layout.addWidget(close_btn) + layout.addLayout(title_layout) info_label = QLabel("⚠️ 注意:加密后将生成新的.bjb文件,原文件不会被删除") - info_label.setStyleSheet("color: #e36209; background-color: #fff5eb; padding: 8px; border-radius: 8px;") + info_label.setStyleSheet( + "color: #f9e2af; background-color: #313244; " + "padding: 12px; border-radius: 12px;" + ) layout.addWidget(info_label) - group1 = QGroupBox("1. 选择加密算法") + # 1. 加密算法 + group1 = QGroupBox("加密算法") + group1.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) group1_layout = QVBoxLayout() self.radio_rsa = QRadioButton("RSA-4096 + AES-256-GCM") self.radio_x25519 = QRadioButton("X25519 + AES-256-GCM") @@ -1666,7 +2216,22 @@ class EncryptCurrentFileDialog(QDialog): group1.setLayout(group1_layout) layout.addWidget(group1) - group2 = QGroupBox("2. 选择密钥方式") + # 2. 密钥方式 + group2 = QGroupBox("密钥方式") + group2.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) group2_layout = QVBoxLayout() self.radio_global = QRadioButton("默认加密(使用全局密钥对)") self.radio_single = QRadioButton("安全加密(为当前文件单独生成密钥对)") @@ -1676,7 +2241,22 @@ class EncryptCurrentFileDialog(QDialog): group2.setLayout(group2_layout) layout.addWidget(group2) - group3 = QGroupBox("3. 额外密码保护") + # 3. 密码保护 + group3 = QGroupBox("密码保护(可选)") + group3.setStyleSheet(""" + QGroupBox { + border: 1px solid #313244; + border-radius: 12px; + margin-top: 12px; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 16px; + padding: 0 8px; + color: #89b4fa; + } + """) group3_layout = QVBoxLayout() strength_layout = QHBoxLayout() @@ -1688,8 +2268,8 @@ class EncryptCurrentFileDialog(QDialog): strength_layout.addWidget(self.radio_weak) group3_layout.addLayout(strength_layout) - self.checkbox_password = QCheckBox("启用双层加密") - self.checkbox_password.toggled.connect(self.on_password_toggled) + self.checkbox_password = QCheckBox("启用密码保护") + self.checkbox_password.toggled.connect(self._on_password_toggled) group3_layout.addWidget(self.checkbox_password) self.password_input = QLineEdit() @@ -1708,91 +2288,62 @@ class EncryptCurrentFileDialog(QDialog): self.password_strength_label.setStyleSheet("font-size: 12px;") group3_layout.addWidget(self.password_strength_label) - self.password_input.textChanged.connect(self.check_password_strength) - self.radio_strict.toggled.connect(self.on_strength_mode_changed) + self.password_input.textChanged.connect(self._check_password_strength) + self.radio_strict.toggled.connect(self._on_strength_mode_changed) group3.setLayout(group3_layout) layout.addWidget(group3) + # 按钮 btn_layout = QHBoxLayout() + btn_layout.setSpacing(12) self.ok_btn = QPushButton("加密并保存") - self.ok_btn.setStyleSheet("background-color: #28a745; color: white;") + self.ok_btn.setProperty("primary", True) self.cancel_btn = QPushButton("取消") - self.ok_btn.clicked.connect(self.accept_config) + self.ok_btn.clicked.connect(self._accept_config) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.ok_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) - def on_strength_mode_changed(self): - """密码强度模式改变""" + main_layout.addWidget(card) + + def _on_strength_mode_changed(self): self.strict_password = self.radio_strict.isChecked() if self.radio_strict.isChecked(): - self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)") + self.password_input.setPlaceholderText( + "请输入密码(至少16位,包含大小写字母、数字和特殊字符)" + ) else: - self.password_input.setPlaceholderText("请输入密码(无强度限制,但强烈建议使用强密码)") - self.check_password_strength() + self.password_input.setPlaceholderText( + "请输入密码(无强度限制,但强烈建议使用强密码)" + ) + self._check_password_strength() - def on_password_toggled(self, checked): - """密码开关切换""" + def _on_password_toggled(self, checked: bool): self.password_input.setEnabled(checked) self.confirm_input.setEnabled(checked) self.use_password = checked if checked: - self.check_password_strength() + self._check_password_strength() else: self.password_strength_label.setText("") - def check_password_strength(self): - """检查密码强度""" + def _check_password_strength(self): if not self.use_password: self.password_strength_label.setText("") return pwd = self.password_input.text() - if self.radio_strict.isChecked(): - if len(pwd) >= 16 and any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd) and any(not c.isalnum() for c in pwd): - self.password_strength_label.setText("✓ 密码强度:强") - self.password_strength_label.setStyleSheet("color: green; font-size: 12px;") - elif len(pwd) >= 12: - self.password_strength_label.setText("⚠ 密码强度:中(建议至少16位)") - self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") - else: - self.password_strength_label.setText("✗ 密码强度:弱(需要至少16位)") - self.password_strength_label.setStyleSheet("color: red; font-size: 12px;") - else: - if len(pwd) >= 8: - self.password_strength_label.setText("✓ 密码已输入(低强度模式)") - self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") - elif len(pwd) > 0: - self.password_strength_label.setText("⚠ 密码太短") - self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") - else: - self.password_strength_label.setText("请输入密码") - self.password_strength_label.setStyleSheet("color: gray; font-size: 12px;") - - def accept_config(self): - """接受配置""" - if DEBUG: - debug_log("开始获取加密配置") + strength = check_password_strength(pwd) - if self.radio_rsa.isChecked(): - self.selected_algorithm = 'rsa' - else: - self.selected_algorithm = 'x25519' - if DEBUG: - debug_log(f"算法: {self.selected_algorithm}") - - if self.radio_global.isChecked(): - self.selected_key_mode = 'global' - else: - self.selected_key_mode = 'single' - if DEBUG: - debug_log(f"密钥模式: {self.selected_key_mode}") + self.password_strength_label.setText(f"🔐 密码强度: {strength['level']} - {strength['message']}") + self.password_strength_label.setStyleSheet(f"color: {strength['color']}; font-size: 12px;") + def _accept_config(self): + self.selected_algorithm = 'x25519' if self.radio_x25519.isChecked() else 'rsa' + self.selected_key_mode = 'single' if self.radio_single.isChecked() else 'global' self.strict_password = self.radio_strict.isChecked() - if DEBUG: - debug_log(f"密码强度模式: {'高强度' if self.strict_password else '低强度'}") if self.use_password: pwd = self.password_input.text() @@ -1805,30 +2356,28 @@ class EncryptCurrentFileDialog(QDialog): QMessageBox.warning(self, "错误", "两次输入的密码不一致") return - if self.strict_password and not is_password_strong(pwd, strict=True): - QMessageBox.warning(self, "密码强度不足", - "密码必须至少16位,包含大小写字母、数字和特殊字符\n\n" - "或切换到「低强度」模式(不推荐)") - return + strength = check_password_strength(pwd) + if strength['level'] in ['极弱', '弱']: + reply = QMessageBox.warning( + self, "密码强度不足", + f"您设置的密码强度为「{strength['level']}」,{strength['message']}\n\n" + "是否继续使用此密码?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + return self.custom_password = pwd - if DEBUG: - debug_log("密码验证通过") if self.selected_key_mode == 'single': - if DEBUG: - debug_log("生成单独密钥对") if self.selected_algorithm == 'rsa': self.single_private, self.single_public = generate_rsa_key(4096) else: self.single_private, self.single_public = generate_x25519_keys() - if DEBUG: - debug_log("加密配置获取完成") self.accept() - def get_config(self): - """获取配置""" + def get_config(self) -> dict: return { 'algorithm': self.selected_algorithm, 'key_mode': self.selected_key_mode, @@ -1839,69 +2388,492 @@ class EncryptCurrentFileDialog(QDialog): 'single_public': self.single_public } -# ==================== 主窗口 ==================== +# ============================================================================ +# 编辑器选项卡 +# ============================================================================ + +class EditorTab: + + def __init__(self, tab_widget: QTabWidget, index: int, + file_path: str = None, cloud_id: int = None, + is_new: bool = False, content: str = "", + file_type: str = "txt", encrypt_config: dict = None): + + self.tab_widget = tab_widget + self.index = index + self.file_path = file_path + self.cloud_id = cloud_id + self.is_new = is_new + self.file_type = file_type + self.encrypt_config = encrypt_config + self.modified = False + + # 创建编辑器容器 + self.container = QWidget() + layout = QVBoxLayout(self.container) + layout.setContentsMargins(8, 8, 8, 8) + + self.text_edit = QTextEdit() + self.text_edit.setPlainText(content) + self.text_edit.textChanged.connect(self._on_text_changed) + + font = QFont("Consolas", 12) + self.text_edit.setFont(font) + layout.addWidget(self.text_edit) + + self._update_tab_title() + + def _update_tab_title(self): + if self.file_path: + name = os.path.basename(self.file_path) + elif self.cloud_id is not None: + name = f"云端文件_{self.cloud_id}" + else: + name = "未命名" + + if self.file_type == 'bjb': + name = f"🔒 {name}" + if self.modified: + name = "* " + name + + self.tab_widget.setTabText(self.index, name) + + def _on_text_changed(self): + if not self.modified: + self.modified = True + self._update_tab_title() + + def get_content(self) -> str: + return self.text_edit.toPlainText() + + def set_content(self, content: str): + self.text_edit.setPlainText(content) + self.modified = False + self._update_tab_title() + + def mark_saved(self): + self.modified = False + self._update_tab_title() + +# ============================================================================ +# 云端文件树窗口 - 现代化设计 +# ============================================================================ + +class CloudFileTreeDialog(QDialog): + + def __init__(self, client: CloudClient, parent: QWidget = None): + super().__init__(parent) + self.client = client + self.setWindowTitle("云端文件管理") + self.resize(1000, 700) + self.current_folder = '/' + self._setup_ui() + self._load_filetree() + + def _setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(12) + + # 工具栏 + toolbar = QToolBar() + toolbar.setStyleSheet(""" + QToolBar { + background-color: #181825; + border-radius: 12px; + padding: 8px; + } + """) + + self.up_btn = QAction("⬆ 返回上级", self) + self.up_btn.triggered.connect(self._go_up) + toolbar.addAction(self.up_btn) + toolbar.addSeparator() + + self.refresh_btn = QAction("🔄 刷新", self) + self.refresh_btn.triggered.connect(self._load_filetree) + toolbar.addAction(self.refresh_btn) + toolbar.addSeparator() + + self.new_folder_btn = QAction("📁 新建文件夹", self) + self.new_folder_btn.triggered.connect(self._create_folder) + toolbar.addAction(self.new_folder_btn) + toolbar.addSeparator() + + self.upload_btn = QAction("📤 上传文件", self) + self.upload_btn.triggered.connect(self._upload_file) + toolbar.addAction(self.upload_btn) + + layout.addWidget(toolbar) + + # 搜索栏 + search_widget = QWidget() + search_layout = QHBoxLayout(search_widget) + search_layout.setContentsMargins(0, 0, 0, 0) + + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("🔍 搜索文件...") + self.search_input.setMinimumHeight(36) + self.search_input.returnPressed.connect(self._search_files) + + search_btn = QPushButton("搜索") + search_btn.setProperty("primary", True) + search_btn.clicked.connect(self._search_files) + + clear_btn = QPushButton("清除") + clear_btn.clicked.connect(self._clear_search) + + search_layout.addWidget(self.search_input) + search_layout.addWidget(search_btn) + search_layout.addWidget(clear_btn) + layout.addWidget(search_widget) + + # 文件树 + self.tree = QTreeWidget() + self.tree.setHeaderLabels(["名称", "大小", "类型", "修改时间"]) + self.tree.setAlternatingRowColors(True) + self.tree.setIndentation(20) + self.tree.setContextMenuPolicy(Qt.CustomContextMenu) + self.tree.customContextMenuRequested.connect(self._show_context_menu) + self.tree.itemDoubleClicked.connect(self._on_item_double_click) + layout.addWidget(self.tree) + + # 状态栏 + status_widget = QWidget() + status_layout = QHBoxLayout(status_widget) + status_layout.setContentsMargins(0, 0, 0, 0) + + self.status_label = QLabel("") + self.status_label.setStyleSheet("color: #a6adc8;") + status_layout.addWidget(self.status_label) + status_layout.addStretch() + + layout.addWidget(status_widget) + + self._load_storage_info() + + def _load_storage_info(self): + info = self.client.get_storage_info() + if info: + used_mb = info['total_size'] / (1024 * 1024) + max_mb = info['max_size'] / (1024 * 1024) + download_used_mb = info['download_used'] / (1024 * 1024) + download_max_mb = info['download_max'] / (1024 * 1024) + self.status_label.setText( + f"📊 存储: {used_mb:.1f}MB / {max_mb:.0f}MB | " + f"📥 今日下载: {download_used_mb:.1f}MB / {download_max_mb:.0f}MB" + ) + + def _load_filetree(self): + data = self.client.get_filetree(self.current_folder) + if not data: + QMessageBox.warning(self, "错误", "加载文件树失败") + return + + self.tree.clear() + for item in data['items']: + size_str = self._format_size(item['size']) if item['size'] else "" + type_str = "📁 文件夹" if item['is_folder'] else ("🔒 加密笔记" if item.get('type') == 'bjb' else "📄 文本文件") + tree_item = QTreeWidgetItem([ + item['name'], size_str, type_str, item['created_at'] + ]) + tree_item.setData(0, Qt.UserRole, item) + self.tree.addTopLevelItem(tree_item) + + self._load_storage_info() + + def _format_size(self, size_bytes: int) -> str: + if not size_bytes: + return "" + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024.0: + return f"{size_bytes:.1f}{unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.1f}TB" + + def _go_up(self): + if self.current_folder != '/': + parts = self.current_folder.rstrip('/').split('/') + if len(parts) > 1: + self.current_folder = '/' + '/'.join(parts[:-1]) + else: + self.current_folder = '/' + self._load_filetree() + + def _create_folder(self): + name, ok = QInputDialog.getText(self, "新建文件夹", "请输入文件夹名称:") + if ok and name: + if '/' in name or '\\' in name: + QMessageBox.warning(self, "错误", "文件夹名称不能包含路径分隔符") + return + res = self.client.create_folder(name, self.current_folder) + if res: + self._load_filetree() + else: + QMessageBox.warning(self, "错误", "创建失败") + + def _upload_file(self): + file_path, _ = QFileDialog.getOpenFileName( + self, "选择要上传的文件", "", "笔记本文件 (*.bjb *.txt)" + ) + if file_path: + ext = os.path.splitext(file_path)[1].lower() + if ext not in ['.txt', '.bjb']: + QMessageBox.warning(self, "错误", "只支持上传 .txt 和 .bjb 文件") + return + + file_size = os.path.getsize(file_path) + info = self.client.get_storage_info() + if info and info['total_size'] + file_size > info['max_size']: + QMessageBox.warning(self, "错误", "云端存储空间不足") + return + + res = self.client.upload_file(file_path, self.current_folder) + if res: + QMessageBox.information(self, "成功", "上传成功") + self._load_filetree() + else: + QMessageBox.warning(self, "错误", "上传失败") + + def _search_files(self): + keyword = self.search_input.text().strip() + if not keyword: + return + data = self.client.search_files(keyword) + if data: + self.tree.clear() + for item in data['items']: + size_str = self._format_size(item['size']) if item['size'] else "" + type_str = "📁 文件夹" if item['is_folder'] else ("🔒 加密笔记" if item.get('type') == 'bjb' else "📄 文本文件") + tree_item = QTreeWidgetItem([ + item['name'], size_str, type_str, "" + ]) + tree_item.setData(0, Qt.UserRole, item) + self.tree.addTopLevelItem(tree_item) + self.status_label.setText(f"🔍 搜索到 {len(data['items'])} 个结果") + + def _clear_search(self): + self.search_input.clear() + self._load_filetree() + + def _on_item_double_click(self, item: QTreeWidgetItem, col: int): + data = item.data(0, Qt.UserRole) + if data['is_folder']: + if self.search_input.text(): + self._clear_search() + if data['parent'] != '/': + self.current_folder = f"{data['parent']}/{data['name']}" + else: + self.current_folder = f"/{data['name']}" + self._load_filetree() + else: + self._download_and_open(data) + + def _download_and_open(self, file_info: dict): + temp_dir = tempfile.gettempdir() + local_path = os.path.join(temp_dir, f"cloud_{file_info['id']}_{file_info['name']}") + + if self.client.download_file(file_info['id'], local_path): + ext = os.path.splitext(file_info['name'])[1].lower() + + if ext == '.txt': + try: + with open(local_path, 'r', encoding='utf-8') as f: + content = f.read() + + main_window = self.parent() + if isinstance(main_window, MainWindow): + tab = EditorTab( + main_window.tabs, main_window.tabs.count(), + file_path=local_path, cloud_id=file_info['id'], + file_type='txt', content=content + ) + main_window.tabs.addTab(tab.container, os.path.basename(file_info['name'])) + main_window.editor_tabs.append(tab) + main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1) + self.accept() + except Exception as e: + debug_error(f"读取文本文件失败: {e}") + QMessageBox.warning(self, "错误", f"读取文件失败: {str(e)}") + else: + content, success, encrypt_config = decrypt_bjb_file( + local_path, self, is_cloud_file=True, cloud_filename=file_info['name'] + ) + + if success and content is not None: + main_window = self.parent() + if isinstance(main_window, MainWindow): + tab = EditorTab( + main_window.tabs, main_window.tabs.count(), + file_path=local_path, cloud_id=file_info['id'], + file_type='bjb', content=content, encrypt_config=encrypt_config + ) + main_window.tabs.addTab(tab.container, os.path.basename(file_info['name'])) + main_window.editor_tabs.append(tab) + main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1) + self.accept() + else: + QMessageBox.warning(self, "错误", "下载失败") + + def _show_context_menu(self, pos): + item = self.tree.itemAt(pos) + if not item: + return + + data = item.data(0, Qt.UserRole) + menu = QMenu() + menu.setStyleSheet(""" + QMenu { + background-color: #181825; + border: 1px solid #313244; + border-radius: 8px; + padding: 4px; + } + QMenu::item { + padding: 8px 24px; + border-radius: 6px; + } + QMenu::item:selected { + background-color: #313244; + } + """) + + rename_action = menu.addAction("✏️ 重命名") + delete_action = menu.addAction("🗑️ 删除") + move_action = menu.addAction("📂 移动") + download_action = menu.addAction("💾 下载") + + action = menu.exec_(self.tree.viewport().mapToGlobal(pos)) + + if action == rename_action: + new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=data['name']) + if ok and new_name: + if self.client.rename_item(data['id'], new_name): + self._load_filetree() + else: + QMessageBox.warning(self, "错误", "重命名失败") + elif action == delete_action: + if QMessageBox.question(self, "确认", f"确定删除 {data['name']} 吗?") == QMessageBox.Yes: + if self.client.delete_item(data['id']): + self._load_filetree() + else: + QMessageBox.warning(self, "错误", "删除失败") + elif action == move_action: + target, ok = QInputDialog.getText(self, "移动", "目标文件夹路径 (例如 /folder):") + if ok and target: + if self.client.move_item(data['id'], target): + self._load_filetree() + else: + QMessageBox.warning(self, "错误", "移动失败") + elif action == download_action: + save_path, _ = QFileDialog.getSaveFileName(self, "保存文件", data['name']) + if save_path: + if self.client.download_file(data['id'], save_path): + QMessageBox.information(self, "成功", "下载完成") + else: + QMessageBox.warning(self, "错误", "下载失败") + +# ============================================================================ +# 主窗口 - 现代化设计 +# ============================================================================ + class MainWindow(QMainWindow): - """主窗口类""" def __init__(self): super().__init__() - if DEBUG: - debug_log("初始化主窗口") + self.client = CloudClient() self.tabs = QTabWidget() self.tabs.setTabsClosable(True) - self.tabs.tabCloseRequested.connect(self.close_tab) - self.tabs.currentChanged.connect(self.on_tab_changed) + self.tabs.tabCloseRequested.connect(self._close_tab) + self.tabs.currentChanged.connect(self._on_tab_changed) + self.tabs.setDocumentMode(True) self.setCentralWidget(self.tabs) - self.setWindowTitle(f"云笔记 - 安全加密笔记") - self.resize(1200, 800) - self.setup_menu() - self.setup_statusbar() + self.setWindowTitle("云笔记 - 安全加密笔记") + self.setMinimumSize(800, 600) + + self._setup_menu() + self._setup_statusbar() + self._setup_theme_switcher() + self.editor_tabs = [] - self.pending_encrypt_config = None self.current_editor = None + + self.current_theme = "dark" + self._apply_theme("dark") ensure_global_keys() - self.check_login() + self._check_login() - def setup_menu(self): - """设置菜单栏""" + def _setup_theme_switcher(self): + # 添加主题切换按钮到菜单栏右侧 + theme_action = QAction("🌓 切换主题", self) + theme_action.triggered.connect(self._toggle_theme) + + # 找到帮助菜单并添加主题切换 + for menu in self.menuBar().actions(): + if menu.text() == "帮助": + menu.menu().addSeparator() + menu.menu().addAction(theme_action) + break + else: + # 如果没有帮助菜单,创建帮助菜单 + help_menu = self.menuBar().addMenu("帮助") + help_menu.addAction(theme_action) + + def _toggle_theme(self): + if self.current_theme == "dark": + self.current_theme = "light" + else: + self.current_theme = "dark" + self._apply_theme(self.current_theme) + + def _apply_theme(self, theme: str): + if theme == "dark": + self.setStyleSheet(DARK_STYLE) + else: + self.setStyleSheet(LIGHT_STYLE) + + def _setup_menu(self): menubar = self.menuBar() - + + # 文件菜单 file_menu = menubar.addMenu("文件") - new_txt_action = QAction("新建文本文件(.txt)", self) + new_txt_action = QAction("新建文本文件", self) new_txt_action.setShortcut(QKeySequence("Ctrl+N")) - new_txt_action.triggered.connect(lambda: self.new_file('txt')) + new_txt_action.setIconText("📄") + new_txt_action.triggered.connect(lambda: self._new_file('txt')) file_menu.addAction(new_txt_action) - new_bjb_action = QAction("新建加密笔记(.bjb)...", self) + new_bjb_action = QAction("新建加密笔记", self) new_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+N")) - new_bjb_action.triggered.connect(lambda: self.new_file('bjb')) + new_bjb_action.setIconText("🔒") + new_bjb_action.triggered.connect(lambda: self._new_file('bjb')) file_menu.addAction(new_bjb_action) file_menu.addSeparator() - open_action = QAction("打开文件...", self) + open_action = QAction("打开文件", self) open_action.setShortcut(QKeySequence.Open) - open_action.triggered.connect(self.open_local_file_dialog) + open_action.triggered.connect(self._open_local_file_dialog) file_menu.addAction(open_action) - open_cloud_action = QAction("从云端打开...", self) + open_cloud_action = QAction("从云端打开", self) open_cloud_action.setShortcut(QKeySequence("Ctrl+Shift+O")) - open_cloud_action.triggered.connect(self.open_cloud_file_dialog) + open_cloud_action.triggered.connect(self._open_cloud_file_dialog) file_menu.addAction(open_cloud_action) file_menu.addSeparator() save_action = QAction("保存", self) save_action.setShortcut(QKeySequence.Save) - save_action.triggered.connect(self.save_current_tab) + save_action.triggered.connect(self._save_current_tab) file_menu.addAction(save_action) - save_as_action = QAction("另存为...", self) + save_as_action = QAction("另存为", self) save_as_action.setShortcut(QKeySequence.SaveAs) - save_as_action.triggered.connect(self.save_as_current_tab) + save_as_action.triggered.connect(self._save_as_current_tab) file_menu.addAction(save_as_action) file_menu.addSeparator() @@ -1914,50 +2886,54 @@ class MainWindow(QMainWindow): # 加密菜单 encrypt_menu = menubar.addMenu("加密") - encrypt_current_action = QAction("🔐 加密当前文件...", self) + encrypt_current_action = QAction("加密当前文件", self) encrypt_current_action.setShortcut(QKeySequence("Ctrl+E")) - encrypt_current_action.triggered.connect(self.encrypt_current_file) + encrypt_current_action.triggered.connect(self._encrypt_current_file) encrypt_menu.addAction(encrypt_current_action) encrypt_menu.addSeparator() - txt_to_bjb_action = QAction("📄 另存为加密笔记(.bjb)...", self) + txt_to_bjb_action = QAction("另存为加密笔记", self) txt_to_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+S")) - txt_to_bjb_action.triggered.connect(self.convert_txt_to_bjb) + txt_to_bjb_action.triggered.connect(self._convert_txt_to_bjb) encrypt_menu.addAction(txt_to_bjb_action) + # 云端菜单 cloud_menu = menubar.addMenu("云端") manage_cloud_action = QAction("管理云端文件", self) - manage_cloud_action.triggered.connect(self.show_cloud_manager) + manage_cloud_action.triggered.connect(self._show_cloud_manager) cloud_menu.addAction(manage_cloud_action) upload_action = QAction("上传当前文件到云端", self) - upload_action.triggered.connect(self.upload_current_tab) + upload_action.triggered.connect(self._upload_current_tab) cloud_menu.addAction(upload_action) cloud_menu.addSeparator() logout_action = QAction("退出登录", self) - logout_action.triggered.connect(self.logout) + logout_action.triggered.connect(self._logout) cloud_menu.addAction(logout_action) + # 帮助菜单 help_menu = menubar.addMenu("帮助") about_action = QAction("关于", self) - about_action.triggered.connect(self.about) + about_action.triggered.connect(self._about) help_menu.addAction(about_action) - def setup_statusbar(self): - """设置状态栏""" + def _setup_statusbar(self): self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) + + # 创建状态栏标签 self.storage_label = QLabel("") + self.user_label = QLabel("") + + self.status_bar.addWidget(self.user_label) self.status_bar.addPermanentWidget(self.storage_label) - self.update_storage_info() - def update_storage_info(self): - """更新存储信息""" + def _update_storage_info(self): if self.client.token: info = self.client.get_storage_info() if info: @@ -1965,8 +2941,7 @@ class MainWindow(QMainWindow): max_mb = info['max_size'] / (1024 * 1024) self.storage_label.setText(f"📁 {used_mb:.1f}MB / {max_mb:.0f}MB") - def check_login(self): - """检查登录状态""" + def _check_login(self): settings = QSettings("CloudNote", "User") token = settings.value("token") user_id = settings.value("user_id") @@ -1976,8 +2951,9 @@ class MainWindow(QMainWindow): self.client.set_auth(token, int(user_id), username) user_info = self.client.get_user_info() if user_info: - self.status_bar.showMessage(f"欢迎回来,{username}") - self.update_storage_info() + self.user_label.setText(f"👤 {username}") + self.status_bar.showMessage(f"欢迎回来,{username}", 3000) + self._update_storage_info() return else: settings.clear() @@ -1985,35 +2961,40 @@ class MainWindow(QMainWindow): dialog = LoginDialog(self.client, self) if dialog.exec_() == QDialog.Accepted: - self.status_bar.showMessage(f"欢迎 {self.client.username}") - self.update_storage_info() + self.user_label.setText(f"👤 {self.client.username}") + self.status_bar.showMessage(f"欢迎 {self.client.username}", 3000) + self._update_storage_info() else: sys.exit(0) - def logout(self): - """退出登录""" + def _logout(self): self.client.clear_auth() settings = QSettings("CloudNote", "User") settings.clear() - self.status_bar.showMessage("已退出登录") - while self.tabs.count() > 0: - self.close_tab(0) - self.check_login() - - def new_file(self, file_type): - """新建文件""" - if DEBUG: - debug_log(f"新建文件: {file_type}") + self.user_label.setText("") + self.storage_label.setText("") + self.status_bar.showMessage("已退出登录", 3000) + while self.tabs.count() > 0: + self._close_tab(0) + + self._check_login() + + def _new_file(self, file_type: str): if file_type == 'txt': - tab = EditorTab(self.tabs, self.tabs.count(), file_type='txt', is_new=True, content="") - self.tabs.addTab(tab.text_edit, "未命名") + tab = EditorTab( + self.tabs, self.tabs.count(), + file_type='txt', is_new=True, content="" + ) + self.tabs.addTab(tab.container, "未命名") self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) else: - # 先创建选项卡,再显示配置对话框 - tab = EditorTab(self.tabs, self.tabs.count(), file_type='bjb', is_new=True, content="") - self.tabs.addTab(tab.text_edit, "未命名加密笔记") + tab = EditorTab( + self.tabs, self.tabs.count(), + file_type='bjb', is_new=True, content="" + ) + self.tabs.addTab(tab.container, "未命名加密笔记") self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) @@ -2021,41 +3002,31 @@ class MainWindow(QMainWindow): if dialog.exec_() == QDialog.Accepted: config = dialog.get_config() tab.encrypt_config = config - self.save_as_current_tab() + self._save_as_current_tab() else: - self.close_tab(self.tabs.currentIndex()) + self._close_tab(self.tabs.currentIndex()) - def open_local_file_dialog(self): - """打开本地文件对话框""" - if DEBUG: - debug_log("打开本地文件对话框") - + def _open_local_file_dialog(self): file_path, _ = QFileDialog.getOpenFileName( self, "打开文件", "", "笔记本文件 (*.bjb *.txt)" ) if file_path: - if DEBUG: - debug_log(f"用户选择文件: {file_path}") - self.open_local_file(file_path) + self._open_local_file(file_path) - def open_local_file(self, file_path, cloud_id=None): - """ - 打开本地文件 - 修复:使用修复后的解密函数,支持单独密钥模式 - """ + def _open_local_file(self, file_path: str, cloud_id: int = None): ext = os.path.splitext(file_path)[1].lower() - if DEBUG: - debug_log(f"打开本地文件: {file_path}, 类型: {ext}") if ext == '.txt': try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - tab = EditorTab(self.tabs, self.tabs.count(), - file_path=file_path, cloud_id=cloud_id, - file_type='txt', content=content) - self.tabs.addTab(tab.text_edit, os.path.basename(file_path)) + tab = EditorTab( + self.tabs, self.tabs.count(), + file_path=file_path, cloud_id=cloud_id, + file_type='txt', content=content + ) + self.tabs.addTab(tab.container, os.path.basename(file_path)) self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) except Exception as e: @@ -2063,80 +3034,56 @@ class MainWindow(QMainWindow): QMessageBox.critical(self, "错误", f"读取文件失败: {str(e)}") elif ext == '.bjb': - # 本地文件,is_cloud_file=False,会优先查找同目录.key文件 - if DEBUG: - debug_log("调用解密函数(本地文件模式)") - content, success = decrypt_bjb_file(file_path, self, is_cloud_file=False) + content, success, encrypt_config = decrypt_bjb_file( + file_path, self, is_cloud_file=False + ) + if success and content is not None: - tab = EditorTab(self.tabs, self.tabs.count(), - file_path=file_path, cloud_id=cloud_id, - file_type='bjb', content=content) - self.tabs.addTab(tab.text_edit, os.path.basename(file_path)) + tab = EditorTab( + self.tabs, self.tabs.count(), + file_path=file_path, cloud_id=cloud_id, + file_type='bjb', content=content, encrypt_config=encrypt_config + ) + self.tabs.addTab(tab.container, os.path.basename(file_path)) self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) - else: - if DEBUG: - debug_log("解密失败,不创建选项卡") - def open_cloud_file_dialog(self): - """打开云端文件对话框""" + def _open_cloud_file_dialog(self): if not self.client.token: QMessageBox.warning(self, "提示", "请先登录") - self.check_login() + self._check_login() return + dialog = CloudFileTreeDialog(self.client, self) dialog.exec_() - def save_current_tab(self): - """保存当前选项卡 - 加密文件直接使用原有配置静默保存""" + def _save_current_tab(self): idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): - if DEBUG: - debug_log(f"保存失败: 无效索引 {idx}") return tab = self.editor_tabs[idx] content = tab.get_content() - - if DEBUG: - debug_log(f"保存当前选项卡: 索引 {idx}, 类型: {tab.file_type}, 新文件: {tab.is_new}") - debug_log(f"内容长度: {len(content)}字符") - # 新文件:调用另存为(会弹出配置对话框) if tab.is_new: - if DEBUG: - debug_log("新文件,调用另存为") - self.save_as_current_tab() + self._save_as_current_tab() return - # 已有文件路径 if tab.file_path and os.path.exists(tab.file_path): try: if tab.file_type == 'txt': - # 文本文件:直接写入 - if DEBUG: - debug_log(f"保存文本文件: {tab.file_path}") with open(tab.file_path, 'w', encoding='utf-8') as f: f.write(content) tab.mark_saved() self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000) - if DEBUG: - debug_log("文本文件保存成功") - else: # bjb 加密文件 - if DEBUG: - debug_log(f"保存加密文件: {tab.file_path}") - - # 检查是否有加密配置 - if hasattr(tab, 'encrypt_config') and tab.encrypt_config: + else: + if tab.encrypt_config: config = tab.encrypt_config - if DEBUG: - debug_log(f"使用现有加密配置 - 算法: {config.get('algorithm')}, 密钥模式: {config.get('key_mode')}, 密码保护: {config.get('use_password', False)}") - # 使用原有配置加密(静默保存,不弹窗) encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content( - content, - config.get('algorithm', 'rsa'), + content, + config.get('algorithm', 'rsa'), config.get('key_mode', 'global'), config.get('custom_password'), config.get('single_private'), @@ -2149,16 +3096,12 @@ class MainWindow(QMainWindow): tab.mark_saved() self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000) - if DEBUG: - debug_log("加密文件保存成功(使用现有配置,静默保存)") else: - # 没有加密配置(理论上不应该发生,但作为后备) - if DEBUG: - debug_log("加密配置缺失,弹出配置对话框") dialog = NewEncryptedFileDialog(self) if dialog.exec_() == QDialog.Accepted: config = dialog.get_config() tab.encrypt_config = config + encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content( content, config['algorithm'], config['key_mode'], config.get('custom_password'), @@ -2166,112 +3109,75 @@ class MainWindow(QMainWindow): config.get('single_public'), strict_password=config.get('strict_password', True) ) + with open(tab.file_path, 'wb') as f: f.write(encrypted_data) + tab.mark_saved() self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000) - if DEBUG: - debug_log("加密文件保存成功(新配置)") - else: - if DEBUG: - debug_log("用户取消保存") - return except Exception as e: debug_error(f"保存失败: {e}") QMessageBox.critical(self, "保存失败", str(e)) else: - # 文件路径无效,调用另存为 - if DEBUG: - debug_log("文件路径无效,调用另存为") - self.save_as_current_tab() + self._save_as_current_tab() - def save_as_current_tab(self): - """另存为当前选项卡 - 首次保存时弹出配置对话框""" + def _save_as_current_tab(self): idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): - if DEBUG: - debug_log(f"另存为失败: 无效索引 {idx}") return tab = self.editor_tabs[idx] content = tab.get_content() - - if DEBUG: - debug_log(f"另存为当前选项卡: 索引 {idx}, 类型: {tab.file_type}") - debug_log(f"内容长度: {len(content)}字符") if tab.file_type == 'txt': - # 文本文件另存为 - if DEBUG: - debug_log("另存为文本文件") file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件 (*.txt)") if not file_path: - if DEBUG: - debug_log("用户取消保存") return if not file_path.endswith('.txt'): file_path += '.txt' + try: with open(file_path, 'w', encoding='utf-8') as f: f.write(content) tab.file_path = file_path tab.is_new = False tab.mark_saved() - if DEBUG: - debug_log(f"文本文件另存为成功: {file_path}") self.status_bar.showMessage(f"已保存: {os.path.basename(file_path)}", 3000) except Exception as e: debug_error(f"保存失败: {e}") QMessageBox.critical(self, "保存失败", str(e)) - else: # bjb 加密文件 - if DEBUG: - debug_log("另存为加密文件") - - # 获取加密配置 - if hasattr(tab, 'encrypt_config') and tab.encrypt_config: - # 已有配置,直接使用(但另存为时应该让用户选择新位置) - config = tab.encrypt_config - if DEBUG: - debug_log("使用选项卡中的现有加密配置") - else: - # 没有配置,弹出配置对话框 - if DEBUG: - debug_log("显示加密配置对话框") + else: + if not tab.encrypt_config: dialog = NewEncryptedFileDialog(self) if dialog.exec_() != QDialog.Accepted: - if DEBUG: - debug_log("用户取消加密配置") return config = dialog.get_config() tab.encrypt_config = config - if DEBUG: - debug_log("加密配置获取完成") + else: + config = tab.encrypt_config - # 弹出保存文件对话框 save_dialog = SaveEncryptedFileDialog( - content, config['algorithm'], config['key_mode'], - config['use_password'], config.get('strict_password', True), + content, + config['algorithm'], + config['key_mode'], + config.get('use_password', False), + config.get('strict_password', True), config.get('custom_password'), - config.get('single_private'), config.get('single_public'), + config.get('single_private'), + config.get('single_public'), self ) + if save_dialog.exec_() == QDialog.Accepted: tab.file_type = 'bjb' - tab.encrypt_config = config tab.is_new = False if save_dialog.selected_file_path: tab.file_path = save_dialog.selected_file_path tab.mark_saved() - if DEBUG: - debug_log(f"加密文件另存为成功: {tab.file_path}") - def encrypt_current_file(self): - """加密当前文件""" - if DEBUG: - debug_log("加密当前文件功能被调用") + def _encrypt_current_file(self): idx = self.tabs.currentIndex() - if idx < 0 or idx >= len(self.editor_tabs): QMessageBox.warning(self, "提示", "没有打开的文件") return @@ -2280,8 +3186,10 @@ class MainWindow(QMainWindow): content = tab.get_content() if not content.strip(): - ret = QMessageBox.question(self, "确认", "文件内容为空,是否继续加密?", - QMessageBox.Yes | QMessageBox.No) + ret = QMessageBox.question( + self, "确认", "文件内容为空,是否继续加密?", + QMessageBox.Yes | QMessageBox.No + ) if ret != QMessageBox.Yes: return @@ -2305,7 +3213,9 @@ class MainWindow(QMainWindow): try: encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content( - content, config['algorithm'], config['key_mode'], + content, + config['algorithm'], + config['key_mode'], config['custom_password'], config.get('single_private'), config.get('single_public'), @@ -2319,31 +3229,33 @@ class MainWindow(QMainWindow): key_path = file_path + '.key' with open(key_path, 'wb') as f: f.write(priv) - QMessageBox.information(self, "加密成功", + QMessageBox.information( + self, "加密成功", f"文件已加密保存为:\n{file_path}\n\n" f"密钥文件已保存为:\n{key_path}\n\n" - f"请妥善保管密钥文件!") + f"请妥善保管密钥文件!" + ) else: - QMessageBox.information(self, "加密成功", + QMessageBox.information( + self, "加密成功", f"文件已加密保存为:\n{file_path}\n\n" - f"使用全局密钥加密,无需额外保存密钥文件。") + f"使用全局密钥加密,无需额外保存密钥文件。" + ) - ret = QMessageBox.question(self, "打开文件", - "是否在新选项卡中打开加密后的文件?", - QMessageBox.Yes | QMessageBox.No) + ret = QMessageBox.question( + self, "打开文件", + "是否在新选项卡中打开加密后的文件?", + QMessageBox.Yes | QMessageBox.No + ) if ret == QMessageBox.Yes: - self.open_local_file(file_path) + self._open_local_file(file_path) except Exception as e: debug_error(f"加密失败: {e}") QMessageBox.critical(self, "加密失败", str(e)) - def convert_txt_to_bjb(self): - """将当前文本文件转换为加密笔记""" - if DEBUG: - debug_log("TXT转BJB功能被调用") + def _convert_txt_to_bjb(self): idx = self.tabs.currentIndex() - if idx < 0 or idx >= len(self.editor_tabs): QMessageBox.warning(self, "提示", "没有打开的文件") return @@ -2372,7 +3284,9 @@ class MainWindow(QMainWindow): try: encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content( - content, config['algorithm'], config['key_mode'], + content, + config['algorithm'], + config['key_mode'], config['custom_password'], config.get('single_private'), config.get('single_public'), @@ -2386,35 +3300,41 @@ class MainWindow(QMainWindow): key_path = file_path + '.key' with open(key_path, 'wb') as f: f.write(priv) - QMessageBox.information(self, "转换成功", + QMessageBox.information( + self, "转换成功", f"文件已转换为加密笔记:\n{file_path}\n\n" f"密钥文件已保存为:\n{key_path}\n\n" - f"请妥善保管密钥文件!") + f"请妥善保管密钥文件!" + ) else: - QMessageBox.information(self, "转换成功", + QMessageBox.information( + self, "转换成功", f"文件已转换为加密笔记:\n{file_path}\n\n" - f"使用全局密钥加密,无需额外保存密钥文件。") + f"使用全局密钥加密,无需额外保存密钥文件。" + ) - ret = QMessageBox.question(self, "打开文件", - "是否在新选项卡中打开加密后的文件?", - QMessageBox.Yes | QMessageBox.No) + ret = QMessageBox.question( + self, "打开文件", + "是否在新选项卡中打开加密后的文件?", + QMessageBox.Yes | QMessageBox.No + ) if ret == QMessageBox.Yes: - self.open_local_file(file_path) + self._open_local_file(file_path) except Exception as e: debug_error(f"转换失败: {e}") QMessageBox.critical(self, "转换失败", str(e)) - def upload_current_tab(self): - """上传当前文件到云端""" + def _upload_current_tab(self): if not self.client.token: QMessageBox.warning(self, "提示", "请先登录") - self.check_login() + self._check_login() return idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): return + tab = self.editor_tabs[idx] if tab.is_new or not tab.file_path: @@ -2422,11 +3342,16 @@ class MainWindow(QMainWindow): return if tab.modified: - ret = QMessageBox.question(self, "提示", "文件未保存,是否先保存?", QMessageBox.Yes | QMessageBox.No) + ret = QMessageBox.question( + self, "提示", "文件未保存,是否先保存?", + QMessageBox.Yes | QMessageBox.No + ) if ret == QMessageBox.Yes: - self.save_current_tab() + self._save_current_tab() - folder, ok = QInputDialog.getText(self, "上传到云端", "请输入云端目录路径 (默认为 /):", text="/") + folder, ok = QInputDialog.getText( + self, "上传到云端", "请输入云端目录路径 (默认为 /):", text="/" + ) if not ok: return if not folder: @@ -2438,33 +3363,34 @@ class MainWindow(QMainWindow): else: QMessageBox.warning(self, "错误", "上传失败") - def show_cloud_manager(self): - """显示云端管理器""" + def _show_cloud_manager(self): if not self.client.token: QMessageBox.warning(self, "提示", "请先登录") - self.check_login() + self._check_login() return + dialog = CloudFileTreeDialog(self.client, self) dialog.exec_() - def on_tab_changed(self, index): - """当前选项卡改变时的回调""" + def _on_tab_changed(self, index: int): if index >= 0 and index < len(self.editor_tabs): self.current_editor = self.editor_tabs[index] else: self.current_editor = None - def close_tab(self, index): - """关闭指定索引的选项卡""" + def _close_tab(self, index: int): if index < 0 or index >= len(self.editor_tabs): return tab = self.editor_tabs[index] + if tab.modified: - ret = QMessageBox.question(self, "提示", "文件已修改,是否保存?", - QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) + ret = QMessageBox.question( + self, "提示", "文件已修改,是否保存?", + QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel + ) if ret == QMessageBox.Yes: - self.save_current_tab() + self._save_current_tab() elif ret == QMessageBox.Cancel: return @@ -2480,44 +3406,51 @@ class MainWindow(QMainWindow): else: self.current_editor = None - def about(self): - """关于对话框""" - QMessageBox.about(self, "关于云笔记", - "云笔记 v3.0\n\n" - "功能特性:\n" - "• 支持 .txt 文本文件\n" - "• 支持 .bjb 加密笔记\n" - "• 云端同步功能\n" - "• 多选项卡编辑\n" - "• 加密当前文件\n" - "• TXT转BJB加密\n" - "• 支持高强度/低强度密码模式\n" - "• 端到端加密:服务器不存储私钥\n\n" - "加密模式:\n" - "1. RSA-4096 + AES-256-GCM\n" - "2. X25519 + AES-256-GCM\n\n" - "密钥方式:\n" - "• 默认加密:全局密钥对\n" - "• 安全加密:单独生成密钥对\n\n" - "快捷键:\n" - "• Ctrl+N: 新建文本\n" - "• Ctrl+Shift+N: 新建加密笔记\n" - "• Ctrl+E: 加密当前文件\n" - "• Ctrl+Shift+S: 另存为加密笔记\n" - "• Ctrl+S: 保存\n" - "• Ctrl+O: 打开文件\n\n" - f"密钥存储位置:\n" - f"• 用户目录: {USER_KEY_DIR}\n" - f"• 程序目录: {APP_KEY_DIR}") + def _about(self): + QMessageBox.about( + self, "关于云笔记", + "

☁️ 云笔记 v5.0

" + "

作者: dvs (dvsxt)

" + "
" + "

✨ 版本 5.0 新特性

" + "" + "
" + "

🔐 功能特性

" + "" + "
" + "

⚡ 快捷键

" + "" + "
" + f"

密钥存储位置:
" + f"用户目录: {USER_KEY_DIR}
" + f"程序目录: {APP_KEY_DIR}

" + ) +# ============================================================================ +# 程序入口 +# ============================================================================ def main(): - """主函数""" - if DEBUG: - debug_log("=" * 50) - debug_log("云笔记应用程序启动") - debug_log("=" * 50) - app = QApplication(sys.argv) app.setStyle('Fusion') @@ -2528,6 +3461,5 @@ def main(): sys.exit(app.exec_()) - if __name__ == "__main__": main() \ No newline at end of file