Files
dvsxt 470502dbb2 update: 更新文件 main_pyqt5.py
v4.0.0新版本

Signed-off-by: dvsxt <me@dvsyun.top>
2026-06-10 21:32:31 +08:00

3465 lines
120 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
云笔记应用程序 - 安全加密笔记系统
====================================
作者: 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
import os
import json
import tempfile
import traceback
from datetime import datetime
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import requests
import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)
from DVT_RFSA import *
# ============================================================================
# 调试配置
# ============================================================================
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: 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() -> str:
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
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")
KEY_FILES = {
'rsa_private': "global_rsa_private.pem",
'rsa_public': "global_rsa_public.pem",
'x25519_private': "global_x25519_private.bin",
'x25519_public': "global_x25519_public.bin"
}
# ============================================================================
# 密钥路径管理
# ============================================================================
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']
else:
search_dirs = [APP_KEY_DIR, USER_KEY_DIR]
dir_names = ['app', 'user']
for dir_path, dir_type in zip(search_dirs, dir_names):
file_path = os.path.join(dir_path, key_name)
if os.path.exists(file_path):
if DEBUG:
debug_log(f"找到密钥 {key_name} 在 {dir_type} 目录: {file_path}")
return file_path, dir_type
return os.path.join(USER_KEY_DIR, key_name), 'user'
def ensure_directory(dir_path: str) -> bool:
try:
os.makedirs(dir_path, exist_ok=True)
if DEBUG:
debug_log(f"目录已确保: {dir_path}")
return True
except Exception as e:
debug_error(f"创建目录失败 {dir_path}: {e}")
return False
# ============================================================================
# 全局密钥管理
# ============================================================================
def ensure_global_keys() -> None:
if DEBUG:
debug_log("开始检查/生成全局密钥...")
ensure_directory(USER_KEY_DIR)
ensure_directory(APP_KEY_DIR)
key_pairs = [
('rsa', KEY_FILES['rsa_private'], KEY_FILES['rsa_public'], generate_rsa_key, 4096),
('x25519', KEY_FILES['x25519_private'], KEY_FILES['x25519_public'], generate_x25519_keys, None)
]
for key_type, priv_file, pub_file, gen_func, key_size in key_pairs:
user_priv = os.path.join(USER_KEY_DIR, priv_file)
user_pub = os.path.join(USER_KEY_DIR, pub_file)
user_exists = os.path.exists(user_priv) and os.path.exists(user_pub)
app_priv = os.path.join(APP_KEY_DIR, priv_file)
app_pub = os.path.join(APP_KEY_DIR, pub_file)
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}")
if not user_exists:
if DEBUG:
debug_log(f"生成{key_type.upper()}密钥对...")
try:
if key_size:
priv, pub = gen_func(key_size)
else:
priv, pub = gen_func()
with open(user_priv, 'wb') as f:
f.write(priv)
with open(user_pub, 'wb') as f:
f.write(pub)
try:
with open(app_priv, 'wb') as f:
f.write(priv)
with open(app_pub, 'wb') as f:
f.write(pub)
except Exception as e:
if DEBUG:
debug_log(f"备份失败: {e}")
except Exception as e:
debug_error(f"{key_type.upper()}密钥生成失败: {e}")
raise
elif user_exists and not app_exists:
try:
with open(user_priv, 'rb') as f:
priv = f.read()
with open(user_pub, 'rb') as f:
pub = f.read()
with open(app_priv, 'wb') as f:
f.write(priv)
with open(app_pub, 'wb') as f:
f.write(pub)
except Exception as e:
if DEBUG:
debug_log(f"备份失败: {e}")
if DEBUG:
debug_log("全局密钥检查完成")
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()
key_path, _ = get_key_path(KEY_FILES['rsa_private'])
with open(key_path, 'rb') as f:
return f.read()
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()
key_path, _ = get_key_path(KEY_FILES['rsa_public'])
with open(key_path, 'rb') as f:
return f.read()
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()
key_path, _ = get_key_path(KEY_FILES['x25519_private'])
with open(key_path, 'rb') as f:
return f.read()
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()
key_path, _ = get_key_path(KEY_FILES['x25519_public'])
with open(key_path, 'rb') as f:
return f.read()
# ============================================================================
# 加密笔记处理器
# ============================================================================
class EncryptedNoteHandler:
@staticmethod
def get_file_metadata(file_path: str) -> dict:
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 {
'has_password': info.get('has_password', False),
'mode': info.get('mode', 'aes_rsa'),
'single_key': info.get('single_key', False),
'algorithm': info.get('algorithm', 'unknown'),
'timestamp': info.get('timestamp', 'unknown')
}
except Exception as e:
debug_error(f"获取文件元数据失败: {e}")
return None
@staticmethod
def extract_encrypt_config(file_path: str, password: str = None, single_private: bytes = None) -> dict:
"""
提取加密配置,根据用户输入的密码自动判断强度模式
密码长度≥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}, "
f"密码保护: {password is not None}, 强度模式: {'高强度' if strict_password else '低强度'}")
data = content.encode('utf-8')
private_key = None
public_key = None
try:
if algorithm == 'rsa':
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
)
else:
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)
else:
private_key = single_private
public_key = single_public
if 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
)
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)
)
return encrypted, private_key, public_key
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
)
else:
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()
else:
private_key = single_private
public_key = single_public
if 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
)
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)
)
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: str, password: str = None,
single_private: bytes = None) -> bytes:
if DEBUG:
debug_log(f"开始解密文件: {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)
mode = info.get('mode', 'aes_rsa')
has_password = info.get('has_password', False)
single_key = info.get('single_key', False)
if mode in ['aes_rsa', 'aes_rsa_password_protected']:
if single_key:
if single_private is None:
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
)
else:
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("单独密钥模式需要提供私钥文件")
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
)
else:
decrypted = decrypt_text_aes_x25519(
clean_data, priv, has_header=False
)
else:
raise ValueError(f"未知加密模式: {mode}")
if isinstance(decrypted, str):
decrypted = decrypted.encode('utf-8')
return decrypted
except Exception as e:
debug_error(f"解密过程异常: {e}")
raise
# ============================================================================
# 通用解密函数
# ============================================================================
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}")
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, None
has_password = metadata['has_password']
single_key = metadata['single_key']
mode = metadata['mode']
single_private = None
if single_key:
key_file_path = file_path + '.key'
if os.path.exists(key_file_path):
try:
with open(key_file_path, 'rb') as f:
single_private = f.read()
except Exception as e:
debug_error(f"读取密钥文件失败: {e}")
QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}")
return None, False, None
else:
msg = f"文件「{display_name}」使用单独密钥加密。\n\n请选择对应的私钥文件(.key)"
if is_cloud_file:
msg = f"云端文件「{display_name}」使用单独密钥加密。\n\n服务器不存储您的私钥,请选择本地保存的.key文件。"
key_file_path, ok = QFileDialog.getOpenFileName(
parent_window, "选择密钥文件 - " + display_name, "",
"密钥文件 (*.key);;所有文件 (*.*)"
)
if not ok or not key_file_path:
QMessageBox.warning(parent_window, "密钥缺失", "缺少私钥文件,无法解密")
return None, False, None
try:
with open(key_file_path, 'rb') as f:
single_private = f.read()
except Exception as e:
debug_error(f"读取密钥文件失败: {e}")
QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}")
return None, False, None
entered_password = None
if has_password:
max_attempts = 3
attempts = 0
success = False
while attempts < max_attempts and not success:
pwd, ok = QInputDialog.getText(
parent_window, "输入密码 - " + display_name,
f"文件「{display_name}」受密码保护\n请输入密码 (尝试 {attempts + 1}/{max_attempts}):",
QLineEdit.Password
)
if not ok:
return None, False, None
if pwd:
try:
decrypted = EncryptedNoteHandler.decrypt_file(
file_path, password=pwd, single_private=single_private
)
if isinstance(decrypted, bytes):
content = decrypted.decode('utf-8')
else:
content = str(decrypted)
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, None
else:
attempts += 1
QMessageBox.warning(parent_window, "密码为空", "密码不能为空")
if not success:
return None, False, None
else:
try:
decrypted = EncryptedNoteHandler.decrypt_file(
file_path, password=None, single_private=single_private
)
if isinstance(decrypted, bytes):
content = decrypted.decode('utf-8')
else:
content = str(decrypted)
except Exception as e:
debug_error(f"解密失败: {e}")
QMessageBox.critical(parent_window, "解密失败", str(e))
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}")
QMessageBox.critical(parent_window, "解密失败", str(e))
return None, False, None
# ============================================================================
# 云端 API 客户端
# ============================================================================
class CloudClient:
def __init__(self):
self.token = None
self.user_id = None
self.username = None
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) -> None:
self.token = None
self.user_id = None
self.username = None
def _headers(self) -> dict:
if self.token:
return {'x-access-token': self.token}
return {}
def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
url = f"{API_BASE_URL}{endpoint}"
headers = self._headers()
if 'headers' in kwargs:
headers.update(kwargs.pop('headers'))
try:
resp = requests.request(method, url, headers=headers, timeout=30, **kwargs)
if resp.status_code == 401:
return None
return resp
except requests.exceptions.ConnectionError:
QMessageBox.critical(None, "网络错误", "无法连接到服务器")
return None
except Exception as e:
QMessageBox.critical(None, "网络错误", f"请求失败: {str(e)}")
return None
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: 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: 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: 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: str, parent_folder: str = '/') -> dict:
ext = os.path.splitext(file_path)[1].lower()
if ext not in ['.txt', '.bjb']:
QMessageBox.warning(None, "错误", "只支持上传 .txt 和 .bjb 文件")
return None
with open(file_path, 'rb') as f:
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: 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:
f.write(resp.content)
return True
return False
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: 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: 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: 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) -> dict:
resp = self._request('GET', '/storage_info')
if resp and resp.status_code == 200:
return resp.json()
return None
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: CloudClient, parent: QWidget = None):
super().__init__(parent)
self.client = client
self.setWindowTitle("云笔记 - 登录")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self._setup_ui()
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(40, 40, 40, 40)
layout.setSpacing(20)
# 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)
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)
layout.addWidget(self.email_input)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("密码")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setMinimumHeight(45)
layout.addWidget(self.password_input)
# 记住我
self.remember_check = QCheckBox("记住我")
layout.addWidget(self.remember_check)
# 登录按钮
self.login_btn = QPushButton("登 录")
self.login_btn.setProperty("primary", True)
self.login_btn.setMinimumHeight(45)
self.login_btn.clicked.connect(self._do_login)
layout.addWidget(self.login_btn)
# 注册链接
register_btn = QPushButton("还没有账号?立即注册")
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(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)
close_btn.setParent(self)
close_btn.move(20, 20)
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()
if not email or not password:
QMessageBox.warning(self, "提示", "请填写邮箱和密码")
return
data = self.client.login(email, password)
if data:
if self.remember_check.isChecked():
settings = QSettings("CloudNote", "User")
settings.setValue("token", self.client.token)
settings.setValue("user_id", self.client.user_id)
settings.setValue("username", self.client.username)
self.accept()
else:
QMessageBox.warning(self, "错误", "邮箱或密码错误")
def _show_register(self):
dialog = RegisterDialog(self.client, self)
dialog.exec_()
# ============================================================================
# 现代化注册对话框
# ============================================================================
class RegisterDialog(QDialog):
def __init__(self, client: CloudClient, parent: QWidget = None):
super().__init__(parent)
self.client = client
self.setWindowTitle("注册账号")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self._setup_ui()
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(40, 40, 40, 40)
layout.setSpacing(16)
title = QLabel("📝 创建新账号")
title.setStyleSheet("font-size: 24px; font-weight: bold; color: #89b4fa;")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
self.username_input = QLineEdit()
self.username_input.setPlaceholderText("用户名")
self.username_input.setMinimumHeight(45)
layout.addWidget(self.username_input)
self.email_input = QLineEdit()
self.email_input.setPlaceholderText("电子邮箱")
self.email_input.setMinimumHeight(45)
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)
layout.addWidget(self.password_input)
self.confirm_input = QLineEdit()
self.confirm_input.setPlaceholderText("确认密码")
self.confirm_input.setEchoMode(QLineEdit.Password)
self.confirm_input.setMinimumHeight(45)
layout.addWidget(self.confirm_input)
self.register_btn = QPushButton("注 册")
self.register_btn.setProperty("primary", True)
self.register_btn.setMinimumHeight(45)
self.register_btn.clicked.connect(self._do_register)
layout.addWidget(self.register_btn)
main_layout.addWidget(card)
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)
close_btn.setParent(self)
close_btn.move(20, 20)
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()
confirm = self.confirm_input.text().strip()
if not all([username, email, password]):
QMessageBox.warning(self, "提示", "请填写所有字段")
return
if password != confirm:
QMessageBox.warning(self, "错误", "两次密码不一致")
return
if len(password) < 6:
QMessageBox.warning(self, "错误", "密码至少6位")
return
res = self.client.register(username, email, password)
if res:
QMessageBox.information(self, "成功", "注册成功,请登录")
self.accept()
else:
QMessageBox.warning(self, "错误", "注册失败,用户名或邮箱已存在")
# ============================================================================
# 新建加密文件对话框 - 现代化设计
# ============================================================================
class NewEncryptedFileDialog(QDialog):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.setWindowTitle("新建加密笔记")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self.selected_algorithm = 'rsa'
self.selected_key_mode = 'global'
self.use_password = False
self.strict_password = True
self.custom_password = None
self.single_private = None
self.single_public = None
self._setup_ui()
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)
# 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")
self.radio_rsa.setChecked(True)
group1_layout.addWidget(self.radio_rsa)
group1_layout.addWidget(self.radio_x25519)
group1.setLayout(group1_layout)
layout.addWidget(group1)
# 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("安全加密(为当前文件单独生成密钥对)")
self.radio_global.setChecked(True)
group2_layout.addWidget(self.radio_global)
group2_layout.addWidget(self.radio_single)
group2.setLayout(group2_layout)
layout.addWidget(group2)
# 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("高强度 (推荐)")
self.radio_weak = QRadioButton("低强度 (不推荐)")
self.radio_strict.setChecked(True)
strength_layout.addWidget(self.radio_strict)
strength_layout.addWidget(self.radio_weak)
group3_layout.addLayout(strength_layout)
self.checkbox_password = QCheckBox("启用密码保护")
self.checkbox_password.toggled.connect(self._on_password_toggled)
group3_layout.addWidget(self.checkbox_password)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setEnabled(False)
group3_layout.addWidget(self.password_input)
self.confirm_input = QLineEdit()
self.confirm_input.setPlaceholderText("确认密码")
self.confirm_input.setEchoMode(QLineEdit.Password)
self.confirm_input.setEnabled(False)
group3_layout.addWidget(self.confirm_input)
self.password_strength_label = QLabel("")
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)
group3.setLayout(group3_layout)
layout.addWidget(group3)
# 提示信息
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()
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.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
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位,包含大小写字母、数字和特殊字符)"
)
else:
self.password_input.setPlaceholderText(
"请输入密码(无强度限制,但强烈建议使用强密码)"
)
self._check_password_strength()
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()
else:
self.password_strength_label.setText("")
def _check_password_strength(self):
if not self.use_password:
self.password_strength_label.setText("")
return
pwd = self.password_input.text()
strength = check_password_strength(pwd)
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 self.use_password:
pwd = self.password_input.text()
confirm = self.confirm_input.text()
if not pwd or not confirm:
QMessageBox.warning(self, "提示", "请输入密码")
return
if pwd != confirm:
QMessageBox.warning(self, "错误", "两次输入的密码不一致")
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 self.selected_key_mode == 'single':
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()
self.accept()
def get_config(self) -> dict:
return {
'algorithm': self.selected_algorithm,
'key_mode': self.selected_key_mode,
'use_password': self.use_password,
'strict_password': self.strict_password,
'custom_password': self.custom_password,
'single_private': self.single_private,
'single_public': self.single_public
}
# ============================================================================
# 保存加密文件对话框 - 现代化设计
# ============================================================================
class SaveEncryptedFileDialog(QDialog):
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)
self.content = content
self.algorithm = algorithm
self.key_mode = key_mode
self.use_password = use_password
self.strict_password = strict_password
self.custom_password = custom_password
self.single_private = single_private
self.single_public = single_public
self.setWindowTitle("保存加密笔记")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self.selected_file_path = None
self._setup_ui()
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: #f9e2af; background-color: #313244; "
"padding: 12px; border-radius: 12px;"
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
# 文件选择
layout.addWidget(QLabel("📁 保存位置:"))
file_select_layout = QHBoxLayout()
self.file_path_input = QLineEdit()
self.file_path_input.setPlaceholderText("请选择保存位置...")
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)
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"
key_text = "全局密钥" if self.key_mode == 'global' else "单独密钥(安全模式)"
pwd_text = "已启用" if self.use_password else "未启用"
strength_text = "高强度" if self.strict_password else "低强度"
info_layout.addWidget(QLabel(f"🔐 加密算法: {algo_text} + AES-256-GCM"))
info_layout.addWidget(QLabel(f"🔑 密钥方式: {key_text}"))
info_layout.addWidget(QLabel(f"🔒 密码保护: {pwd_text}"))
if self.use_password:
info_layout.addWidget(QLabel(f"🔐 密码强度: {strength_text}"))
info_group.setLayout(info_layout)
layout.addWidget(info_group)
# 按钮
btn_layout = QHBoxLayout()
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.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
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):
file_path, _ = QFileDialog.getSaveFileName(self, "保存加密笔记", "", "加密笔记 (*.bjb)")
if file_path:
if not file_path.endswith('.bjb'):
file_path += '.bjb'
self.selected_file_path = file_path
self.file_path_input.setText(file_path)
def _do_save(self):
if not self.selected_file_path:
QMessageBox.warning(self, "提示", "请选择保存位置")
return
if not self.selected_file_path.endswith('.bjb'):
self.selected_file_path += '.bjb'
try:
encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content(
self.content, self.algorithm, self.key_mode,
self.custom_password, self.single_private, self.single_public,
strict_password=self.strict_password
)
except Exception as e:
QMessageBox.critical(self, "加密失败", str(e))
return
try:
with open(self.selected_file_path, 'wb') as f:
f.write(encrypted_data)
except Exception as e:
QMessageBox.critical(self, "保存失败", f"无法保存.bjb文件: {str(e)}")
return
key_path = None
if self.key_mode == 'single' and priv:
key_path = self.selected_file_path + '.key'
try:
with open(key_path, 'wb') as f:
f.write(priv)
except Exception as e:
QMessageBox.critical(self, "保存失败", f"无法保存.key文件: {str(e)}")
return
msg = f"✅ 文件已保存!\n\n📄 .bjb文件:{self.selected_file_path}"
if key_path:
msg += f"\n🔑 .key文件:{key_path}\n\n⚠️ 请妥善保管密钥文件!"
else:
msg += "\n\n💡 提示:使用全局密钥,无需额外保存密钥文件。"
QMessageBox.information(self, "保存成功", msg)
self.accept()
# ============================================================================
# 加密当前文件对话框 - 现代化设计
# ============================================================================
class EncryptCurrentFileDialog(QDialog):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.setWindowTitle("加密当前文件")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.setAttribute(Qt.WA_TranslucentBackground)
self.selected_algorithm = 'rsa'
self.selected_key_mode = 'global'
self.use_password = False
self.strict_password = True
self.custom_password = None
self.single_private = None
self.single_public = None
self._setup_ui()
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)
info_label = QLabel("⚠️ 注意:加密后将生成新的.bjb文件,原文件不会被删除")
info_label.setStyleSheet(
"color: #f9e2af; background-color: #313244; "
"padding: 12px; border-radius: 12px;"
)
layout.addWidget(info_label)
# 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")
self.radio_rsa.setChecked(True)
group1_layout.addWidget(self.radio_rsa)
group1_layout.addWidget(self.radio_x25519)
group1.setLayout(group1_layout)
layout.addWidget(group1)
# 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("安全加密(为当前文件单独生成密钥对)")
self.radio_global.setChecked(True)
group2_layout.addWidget(self.radio_global)
group2_layout.addWidget(self.radio_single)
group2.setLayout(group2_layout)
layout.addWidget(group2)
# 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("高强度 (推荐)")
self.radio_weak = QRadioButton("低强度 (不推荐)")
self.radio_strict.setChecked(True)
strength_layout.addWidget(self.radio_strict)
strength_layout.addWidget(self.radio_weak)
group3_layout.addLayout(strength_layout)
self.checkbox_password = QCheckBox("启用密码保护")
self.checkbox_password.toggled.connect(self._on_password_toggled)
group3_layout.addWidget(self.checkbox_password)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)")
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setEnabled(False)
group3_layout.addWidget(self.password_input)
self.confirm_input = QLineEdit()
self.confirm_input.setPlaceholderText("确认密码")
self.confirm_input.setEchoMode(QLineEdit.Password)
self.confirm_input.setEnabled(False)
group3_layout.addWidget(self.confirm_input)
self.password_strength_label = QLabel("")
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)
group3.setLayout(group3_layout)
layout.addWidget(group3)
# 按钮
btn_layout = QHBoxLayout()
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.cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
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位,包含大小写字母、数字和特殊字符)"
)
else:
self.password_input.setPlaceholderText(
"请输入密码(无强度限制,但强烈建议使用强密码)"
)
self._check_password_strength()
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()
else:
self.password_strength_label.setText("")
def _check_password_strength(self):
if not self.use_password:
self.password_strength_label.setText("")
return
pwd = self.password_input.text()
strength = check_password_strength(pwd)
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 self.use_password:
pwd = self.password_input.text()
confirm = self.confirm_input.text()
if not pwd or not confirm:
QMessageBox.warning(self, "提示", "请输入密码")
return
if pwd != confirm:
QMessageBox.warning(self, "错误", "两次输入的密码不一致")
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 self.selected_key_mode == 'single':
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()
self.accept()
def get_config(self) -> dict:
return {
'algorithm': self.selected_algorithm,
'key_mode': self.selected_key_mode,
'use_password': self.use_password,
'strict_password': self.strict_password,
'custom_password': self.custom_password,
'single_private': self.single_private,
'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__()
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.setDocumentMode(True)
self.setCentralWidget(self.tabs)
self.setWindowTitle("云笔记 - 安全加密笔记")
self.setMinimumSize(800, 600)
self._setup_menu()
self._setup_statusbar()
self._setup_theme_switcher()
self.editor_tabs = []
self.current_editor = None
self.current_theme = "dark"
self._apply_theme("dark")
ensure_global_keys()
self._check_login()
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("新建文本文件", self)
new_txt_action.setShortcut(QKeySequence("Ctrl+N"))
new_txt_action.setIconText("📄")
new_txt_action.triggered.connect(lambda: self._new_file('txt'))
file_menu.addAction(new_txt_action)
new_bjb_action = QAction("新建加密笔记", self)
new_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+N"))
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.setShortcut(QKeySequence.Open)
open_action.triggered.connect(self._open_local_file_dialog)
file_menu.addAction(open_action)
open_cloud_action = QAction("从云端打开", self)
open_cloud_action.setShortcut(QKeySequence("Ctrl+Shift+O"))
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)
file_menu.addAction(save_action)
save_as_action = QAction("另存为", self)
save_as_action.setShortcut(QKeySequence.SaveAs)
save_as_action.triggered.connect(self._save_as_current_tab)
file_menu.addAction(save_as_action)
file_menu.addSeparator()
exit_action = QAction("退出", self)
exit_action.setShortcut(QKeySequence.Quit)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 加密菜单
encrypt_menu = menubar.addMenu("加密")
encrypt_current_action = QAction("加密当前文件", self)
encrypt_current_action.setShortcut(QKeySequence("Ctrl+E"))
encrypt_current_action.triggered.connect(self._encrypt_current_file)
encrypt_menu.addAction(encrypt_current_action)
encrypt_menu.addSeparator()
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)
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)
cloud_menu.addAction(manage_cloud_action)
upload_action = QAction("上传当前文件到云端", self)
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)
cloud_menu.addAction(logout_action)
# 帮助菜单
help_menu = menubar.addMenu("帮助")
about_action = QAction("关于", self)
about_action.triggered.connect(self._about)
help_menu.addAction(about_action)
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)
def _update_storage_info(self):
if self.client.token:
info = self.client.get_storage_info()
if info:
used_mb = info['total_size'] / (1024 * 1024)
max_mb = info['max_size'] / (1024 * 1024)
self.storage_label.setText(f"📁 {used_mb:.1f}MB / {max_mb:.0f}MB")
def _check_login(self):
settings = QSettings("CloudNote", "User")
token = settings.value("token")
user_id = settings.value("user_id")
username = settings.value("username")
if token and user_id and username:
self.client.set_auth(token, int(user_id), username)
user_info = self.client.get_user_info()
if user_info:
self.user_label.setText(f"👤 {username}")
self.status_bar.showMessage(f"欢迎回来,{username}", 3000)
self._update_storage_info()
return
else:
settings.clear()
self.client.clear_auth()
dialog = LoginDialog(self.client, self)
if dialog.exec_() == QDialog.Accepted:
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):
self.client.clear_auth()
settings = QSettings("CloudNote", "User")
settings.clear()
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.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.container, "未命名加密笔记")
self.editor_tabs.append(tab)
self.tabs.setCurrentIndex(self.tabs.count() - 1)
dialog = NewEncryptedFileDialog(self)
if dialog.exec_() == QDialog.Accepted:
config = dialog.get_config()
tab.encrypt_config = config
self._save_as_current_tab()
else:
self._close_tab(self.tabs.currentIndex())
def _open_local_file_dialog(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "打开文件", "", "笔记本文件 (*.bjb *.txt)"
)
if file_path:
self._open_local_file(file_path)
def _open_local_file(self, file_path: str, cloud_id: int = None):
ext = os.path.splitext(file_path)[1].lower()
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.container, os.path.basename(file_path))
self.editor_tabs.append(tab)
self.tabs.setCurrentIndex(self.tabs.count() - 1)
except Exception as e:
debug_error(f"读取文件失败: {e}")
QMessageBox.critical(self, "错误", f"读取文件失败: {str(e)}")
elif ext == '.bjb':
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, 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)
def _open_cloud_file_dialog(self):
if not self.client.token:
QMessageBox.warning(self, "提示", "请先登录")
self._check_login()
return
dialog = CloudFileTreeDialog(self.client, self)
dialog.exec_()
def _save_current_tab(self):
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if tab.is_new:
self._save_as_current_tab()
return
if tab.file_path and os.path.exists(tab.file_path):
try:
if tab.file_type == 'txt':
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)
else:
if tab.encrypt_config:
config = tab.encrypt_config
encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content(
content,
config.get('algorithm', 'rsa'),
config.get('key_mode', 'global'),
config.get('custom_password'),
config.get('single_private'),
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)
else:
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'),
config.get('single_private'),
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)
except Exception as e:
debug_error(f"保存失败: {e}")
QMessageBox.critical(self, "保存失败", str(e))
else:
self._save_as_current_tab()
def _save_as_current_tab(self):
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if tab.file_type == 'txt':
file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件 (*.txt)")
if not file_path:
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()
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:
if not tab.encrypt_config:
dialog = NewEncryptedFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
return
config = dialog.get_config()
tab.encrypt_config = config
else:
config = tab.encrypt_config
save_dialog = SaveEncryptedFileDialog(
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'),
self
)
if save_dialog.exec_() == QDialog.Accepted:
tab.file_type = 'bjb'
tab.is_new = False
if save_dialog.selected_file_path:
tab.file_path = save_dialog.selected_file_path
tab.mark_saved()
def _encrypt_current_file(self):
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
QMessageBox.warning(self, "提示", "没有打开的文件")
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if not content.strip():
ret = QMessageBox.question(
self, "确认", "文件内容为空,是否继续加密?",
QMessageBox.Yes | QMessageBox.No
)
if ret != QMessageBox.Yes:
return
dialog = EncryptCurrentFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
return
config = dialog.get_config()
default_name = "encrypted_note.bjb"
if tab.file_path:
base_name = os.path.splitext(os.path.basename(tab.file_path))[0]
default_name = f"{base_name}_encrypted.bjb"
file_path, _ = QFileDialog.getSaveFileName(self, "保存加密文件", default_name, "加密笔记 (*.bjb)")
if not file_path:
return
if not file_path.endswith('.bjb'):
file_path += '.bjb'
try:
encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content(
content,
config['algorithm'],
config['key_mode'],
config['custom_password'],
config.get('single_private'),
config.get('single_public'),
strict_password=config.get('strict_password', True)
)
with open(file_path, 'wb') as f:
f.write(encrypted_data)
if config['key_mode'] == 'single' and priv:
key_path = file_path + '.key'
with open(key_path, 'wb') as f:
f.write(priv)
QMessageBox.information(
self, "加密成功",
f"文件已加密保存为:\n{file_path}\n\n"
f"密钥文件已保存为:\n{key_path}\n\n"
f"请妥善保管密钥文件!"
)
else:
QMessageBox.information(
self, "加密成功",
f"文件已加密保存为:\n{file_path}\n\n"
f"使用全局密钥加密,无需额外保存密钥文件。"
)
ret = QMessageBox.question(
self, "打开文件",
"是否在新选项卡中打开加密后的文件?",
QMessageBox.Yes | QMessageBox.No
)
if ret == QMessageBox.Yes:
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):
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
QMessageBox.warning(self, "提示", "没有打开的文件")
return
tab = self.editor_tabs[idx]
if tab.file_type == 'bjb':
QMessageBox.information(self, "提示", "当前文件已经是加密笔记格式")
return
content = tab.get_content()
dialog = EncryptCurrentFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
return
config = dialog.get_config()
default_name = os.path.splitext(os.path.basename(tab.file_path))[0] + ".bjb" if tab.file_path else "converted.bjb"
file_path, _ = QFileDialog.getSaveFileName(self, "保存为加密笔记", default_name, "加密笔记 (*.bjb)")
if not file_path:
return
if not file_path.endswith('.bjb'):
file_path += '.bjb'
try:
encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content(
content,
config['algorithm'],
config['key_mode'],
config['custom_password'],
config.get('single_private'),
config.get('single_public'),
strict_password=config.get('strict_password', True)
)
with open(file_path, 'wb') as f:
f.write(encrypted_data)
if config['key_mode'] == 'single' and priv:
key_path = file_path + '.key'
with open(key_path, 'wb') as f:
f.write(priv)
QMessageBox.information(
self, "转换成功",
f"文件已转换为加密笔记:\n{file_path}\n\n"
f"密钥文件已保存为:\n{key_path}\n\n"
f"请妥善保管密钥文件!"
)
else:
QMessageBox.information(
self, "转换成功",
f"文件已转换为加密笔记:\n{file_path}\n\n"
f"使用全局密钥加密,无需额外保存密钥文件。"
)
ret = QMessageBox.question(
self, "打开文件",
"是否在新选项卡中打开加密后的文件?",
QMessageBox.Yes | QMessageBox.No
)
if ret == QMessageBox.Yes:
self._open_local_file(file_path)
except Exception as e:
debug_error(f"转换失败: {e}")
QMessageBox.critical(self, "转换失败", str(e))
def _upload_current_tab(self):
if not self.client.token:
QMessageBox.warning(self, "提示", "请先登录")
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:
QMessageBox.warning(self, "提示", "请先保存文件到本地")
return
if tab.modified:
ret = QMessageBox.question(
self, "提示", "文件未保存,是否先保存?",
QMessageBox.Yes | QMessageBox.No
)
if ret == QMessageBox.Yes:
self._save_current_tab()
folder, ok = QInputDialog.getText(
self, "上传到云端", "请输入云端目录路径 (默认为 /):", text="/"
)
if not ok:
return
if not folder:
folder = "/"
res = self.client.upload_file(tab.file_path, folder)
if res:
QMessageBox.information(self, "成功", "文件已上传到云端")
else:
QMessageBox.warning(self, "错误", "上传失败")
def _show_cloud_manager(self):
if not self.client.token:
QMessageBox.warning(self, "提示", "请先登录")
self._check_login()
return
dialog = CloudFileTreeDialog(self.client, self)
dialog.exec_()
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: 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
)
if ret == QMessageBox.Yes:
self._save_current_tab()
elif ret == QMessageBox.Cancel:
return
self.tabs.removeTab(index)
self.editor_tabs.pop(index)
for i, t in enumerate(self.editor_tabs):
t.index = i
current_idx = self.tabs.currentIndex()
if current_idx >= 0 and current_idx < len(self.editor_tabs):
self.current_editor = self.editor_tabs[current_idx]
else:
self.current_editor = None
def _about(self):
QMessageBox.about(
self, "关于云笔记",
"<h2>☁️ 云笔记 v5.0</h2>"
"<p><b>作者:</b> dvs (dvsxt)</p>"
"<br>"
"<h3>✨ 版本 5.0 新特性</h3>"
"<ul>"
"<li>🎨 全新的现代化界面设计</li>"
"<li>🌓 支持暗色/亮色主题切换</li>"
"<li>📱 完全自适应布局,支持窗口缩放</li>"
"<li>✨ 圆角卡片设计和流畅动画效果</li>"
"</ul>"
"<br>"
"<h3>🔐 功能特性</h3>"
"<ul>"
"<li>支持 .txt 文本文件</li>"
"<li>支持 .bjb 加密笔记</li>"
"<li>云端同步功能</li>"
"<li>多选项卡编辑</li>"
"<li>加密当前文件</li>"
"<li>TXT转BJB加密</li>"
"<li>端到端加密:服务器不存储私钥</li>"
"</ul>"
"<br>"
"<h3>⚡ 快捷键</h3>"
"<ul>"
"<li>Ctrl+N: 新建文本</li>"
"<li>Ctrl+Shift+N: 新建加密笔记</li>"
"<li>Ctrl+E: 加密当前文件</li>"
"<li>Ctrl+Shift+S: 另存为加密笔记</li>"
"<li>Ctrl+S: 保存</li>"
"<li>Ctrl+O: 打开文件</li>"
"</ul>"
"<br>"
f"<p><b>密钥存储位置:</b><br>"
f"用户目录: {USER_KEY_DIR}<br>"
f"程序目录: {APP_KEY_DIR}</p>"
)
# ============================================================================
# 程序入口
# ============================================================================
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
ensure_global_keys()
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()