diff --git a/requirements.txt b/requirements.txt index 96c661b..17e7112 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ PyQt5>=5.15.0 requests>=2.25.1 beautifulsoup4>=4.11.0 pillow>=9.0.0 -chardet>=4.0.0 \ No newline at end of file +chardet>=4.0.0 diff --git a/src/file_manager/file_operations.py b/src/file_manager/file_operations.py index 603cb8e..17c67e3 100644 --- a/src/file_manager/file_operations.py +++ b/src/file_manager/file_operations.py @@ -6,146 +6,253 @@ from pathlib import Path class FileManager: def __init__(self): - """ - 初始化文件管理器 - - 设置工作目录 - - 初始化文件缓存 - """ - # TODO: 实现构造函数逻辑 + + # 实现构造函数逻辑123 # 1. 设置默认工作目录 + self.working_directory = Path.cwd() # 2. 初始化文件缓存 + self.file_cache = {} # 3. 创建必要的目录结构 pass def list_files(self, directory: str, extensions: Optional[List[str]] = None) -> List[str]: - """ - 列出目录中的文件 - - 遍历指定目录 - - 根据扩展名过滤文件(如果提供) - - 返回文件路径列表 - """ - # TODO: 实现文件列表逻辑 + + # 实现文件列表逻辑 # 1. 检查目录是否存在 + if not os.path.exists(directory): + raise FileNotFoundError(f"目录 {directory} 不存在") + # 2. 遍历目录中的所有文件 - # 3. 根据扩展名过滤文件(如果提供) + file_list = [] + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + # 3. 根据扩展名过滤文件(如果提供) + if extensions: + _, ext = os.path.splitext(file) + if ext.lower() in [e.lower() for e in extensions]: + file_list.append(file_path) + else: + file_list.append(file_path) + # 4. 返回文件路径列表 - pass + return file_list def copy_file(self, source: str, destination: str) -> bool: - """ - 复制文件 - - 将文件从源路径复制到目标路径 - - 返回操作结果 - """ - # TODO: 实现文件复制逻辑 + + # 实现文件复制逻辑 # 1. 检查源文件是否存在 - # 2. 创建目标目录(如果不存在) - # 3. 执行文件复制操作 - # 4. 处理异常情况 + if not os.path.exists(source): + print(f"源文件 {source} 不存在") + return False + + try: + # 2. 创建目标目录(如果不存在) + dest_dir = os.path.dirname(destination) + if dest_dir and not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + # 3. 执行文件复制操作 + shutil.copy2(source, destination) + # 4. 处理异常情况 + except Exception as e: + print(f"复制文件时出错: {e}") + return False + # 5. 返回操作结果 - pass + return True def move_file(self, source: str, destination: str) -> bool: - """ - 移动文件 - - 将文件从源路径移动到目标路径 - - 返回操作结果 - """ - # TODO: 实现文件移动逻辑 + + # 实现文件移动逻辑 # 1. 检查源文件是否存在 - # 2. 创建目标目录(如果不存在) - # 3. 执行文件移动操作 - # 4. 处理异常情况 + if not os.path.exists(source): + print(f"源文件 {source} 不存在") + return False + + try: + # 2. 创建目标目录(如果不存在) + dest_dir = os.path.dirname(destination) + if dest_dir and not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + # 3. 执行文件移动操作 + shutil.move(source, destination) + # 4. 处理异常情况 + except Exception as e: + print(f"移动文件时出错: {e}") + return False + # 5. 返回操作结果 - pass + return True def delete_file(self, file_path: str) -> bool: - """ - 删除文件 - - 删除指定路径的文件 - - 返回操作结果 - """ - # TODO: 实现文件删除逻辑 + + # 实现文件删除逻辑 # 1. 检查文件是否存在 - # 2. 执行文件删除操作 - # 3. 处理异常情况(如权限不足) + if not os.path.exists(file_path): + print(f"文件 {file_path} 不存在") + return False + + try: + # 2. 执行文件删除操作 + os.remove(file_path) + # 3. 处理异常情况(如权限不足) + except PermissionError: + print(f"没有权限删除文件 {file_path}") + return False + except Exception as e: + print(f"删除文件时出错: {e}") + return False + # 4. 返回操作结果 - pass + return True def get_file_info(self, file_path: str) -> Optional[Dict[str, Any]]: - """ - 获取文件信息 - - 获取文件大小、修改时间等信息 - - 返回信息字典 - """ - # TODO: 实现文件信息获取逻辑 + + # 实现文件信息获取逻辑 # 1. 检查文件是否存在 - # 2. 获取文件基本信息(大小、修改时间等) - # 3. 获取文件扩展名和类型 - # 4. 返回信息字典 - pass + if not os.path.exists(file_path): + print(f"文件 {file_path} 不存在") + return None + + try: + # 2. 获取文件基本信息(大小、修改时间等) + stat_info = os.stat(file_path) + file_size = stat_info.st_size + modification_time = stat_info.st_mtime + + # 3. 获取文件扩展名和类型 + _, ext = os.path.splitext(file_path) + + # 4. 返回信息字典 + file_info = { + "path": file_path, + "size": file_size, + "modification_time": modification_time, + "extension": ext.lower(), + "name": os.path.basename(file_path) + } + return file_info + except Exception as e: + print(f"获取文件信息时出错: {e}") + return None class DocumentOrganizer: def __init__(self): - """ - 初始化文档整理器 - - 设置分类规则 - - 初始化标签系统 - """ - # TODO: 实现构造函数逻辑 + + # 实现构造函数逻辑 # 1. 设置默认分类规则 + self.categorization_rules = { + "images": [".jpg", ".jpeg", ".png", ".gif", ".bmp"], + "documents": [".pdf", ".doc", ".docx", ".txt", ".md"], + "videos": [".mp4", ".avi", ".mkv", ".mov"], + "audio": [".mp3", ".wav", ".flac"], + "archives": [".zip", ".rar", ".7z", ".tar"] + } # 2. 初始化标签系统 + self.tags = {} # 3. 创建必要的目录结构 pass def categorize_documents(self, directory: str) -> Dict[str, List[str]]: - """ - 分类文档 - - 根据预设规则对文档进行分类 - - 返回分类结果字典 - """ - # TODO: 实现文档分类逻辑 + # 1. 遍历目录中的所有文件 + if not os.path.exists(directory): + raise FileNotFoundError(f"目录 {directory} 不存在") + # 2. 根据文件类型或内容特征进行分类 + categorized_files = {category: [] for category in self.categorization_rules} + uncategorized = [] + + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + _, ext = os.path.splitext(file) + + # 根据扩展名分类 + categorized = False + for category, extensions in self.categorization_rules.items(): + if ext.lower() in extensions: + categorized_files[category].append(file_path) + categorized = True + break + + if not categorized: + uncategorized.append(file_path) + + categorized_files["uncategorized"] = uncategorized + # 3. 返回分类结果字典 {类别: [文件列表]} - pass + return categorized_files def add_tag_to_file(self, file_path: str, tag: str) -> bool: - """ - 为文件添加标签 - - 在文件元数据中添加标签信息 - - 返回操作结果 - """ - # TODO: 实现标签添加逻辑 + + # 实现标签添加逻辑 # 1. 检查文件是否存在 + if not os.path.exists(file_path): + print(f"文件 {file_path} 不存在") + return False + # 2. 读取文件元数据 # 3. 添加新标签 + if file_path not in self.tags: + self.tags[file_path] = [] + + if tag not in self.tags[file_path]: + self.tags[file_path].append(tag) + # 4. 保存更新后的元数据 # 5. 返回操作结果 - pass + return True def search_files_by_tag(self, tag: str) -> List[str]: - """ - 根据标签搜索文件 - - 查找具有指定标签的所有文件 - - 返回文件路径列表 - """ - # TODO: 实现标签搜索逻辑 + + # 实现标签搜索逻辑 # 1. 遍历文件数据库或目录 # 2. 查找包含指定标签的文件 + matching_files = [] + for file_path, tags in self.tags.items(): + if tag in tags: + matching_files.append(file_path) + # 3. 返回文件路径列表 - pass + return matching_files def backup_documents(self, source_dir: str, backup_dir: str) -> bool: - """ - 备份文档 - - 将源目录中的文档备份到备份目录 - - 返回操作结果 - """ - # TODO: 实现文档备份逻辑 + + # 实现文档备份逻辑 # 1. 创建备份目录(如果不存在) + if not os.path.exists(backup_dir): + os.makedirs(backup_dir) + # 2. 遍历源目录中的所有文件 - # 3. 复制文件到备份目录 - # 4. 处理异常情况 - # 5. 返回操作结果 - pass \ No newline at end of file + if not os.path.exists(source_dir): + print(f"源目录 {source_dir} 不存在") + return False + + try: + # 使用shutil.copytree进行目录复制 + # 如果备份目录已存在且不为空,需要先清空或使用其他方法 + for root, dirs, files in os.walk(source_dir): + # 计算相对路径 + rel_path = os.path.relpath(root, source_dir) + dest_path = os.path.join(backup_dir, rel_path) if rel_path != '.' else backup_dir + + # 创建目标目录 + if not os.path.exists(dest_path): + os.makedirs(dest_path) + + # 复制文件 + for file in files: + src_file = os.path.join(root, file) + dest_file = os.path.join(dest_path, file) + shutil.copy2(src_file, dest_file) + + # 3. 处理异常情况 + except Exception as e: + print(f"备份文档时出错: {e}") + return False + + # 4. 返回操作结果 + return True \ No newline at end of file diff --git a/src/file_parser.py b/src/file_parser.py index 84b3c04..fc44729 100644 --- a/src/file_parser.py +++ b/src/file_parser.py @@ -4,54 +4,132 @@ from typing import Union class FileParser: @staticmethod def parse_file(file_path: str) -> str: - """ - 主解析函数,根据文件扩展名路由到具体解析器 - - 调用validate_file_path(file_path)验证路径 - - 根据扩展名调用对应解析函数 - - 统一异常处理 - """ - # TODO: 实现主解析函数逻辑 - pass + + # 验证文件路径 + if not FileParser.validate_file_path(file_path): + raise ValueError(f"Invalid file path: {file_path}") + + # 获取文件扩展名 + _, ext = os.path.splitext(file_path) + ext = ext.lower() + + # 根据扩展名调用对应的解析函数 + try: + if ext == '.txt': + return FileParser.parse_txt(file_path) + elif ext == '.docx': + return FileParser.parse_docx(file_path) + elif ext == '.pdf': + return FileParser.parse_pdf(file_path) + else: + raise ValueError(f"Unsupported file format: {ext}") + except Exception as e: + # 统一异常处理 + raise Exception(f"Error parsing file {file_path}: {str(e)}") @staticmethod def parse_txt(file_path: str) -> str: - """ - 解析纯文本文件 - - 自动检测编码(utf-8, gbk等) - - 处理不同换行符 - - 返回纯文本内容 - """ - # TODO: 实现实现txt文件解析逻辑 - pass + # 验证文件路径 + if not FileParser.validate_file_path(file_path): + raise ValueError(f"Invalid file path: {file_path}") + + # 导入工具函数来检测编码 + try: + from utils.helper_functions import Utils + except ImportError: + # 如果无法导入,使用默认方法检测编码 + import chardet + with open(file_path, 'rb') as f: + raw_data = f.read(1024) + encoding = chardet.detect(raw_data)['encoding'] or 'utf-8' + else: + # 使用工具函数检测编码 + encoding = Utils.detect_encoding(file_path) + + # 读取文件内容 + with open(file_path, 'r', encoding=encoding, errors='ignore') as f: + content = f.read() + + # 统一换行符为\n + content = content.replace('\r\n', '\n').replace('\r', '\n') + + return content @staticmethod def parse_docx(file_path: str) -> str: - """ - 解析Word文档 - - 提取所有段落文本 - - 保留基本格式(换行) - - 忽略图片、表格等非文本元素 - """ - # TODO: 实现docx文件解析逻辑 - pass + + # 验证文件路径 + if not FileParser.validate_file_path(file_path): + raise ValueError(f"Invalid file path: {file_path}") + + # 尝试导入python-docx库 + try: + from docx import Document + except ImportError: + raise ImportError("python-docx library is required for parsing .docx files. Please install it using 'pip install python-docx'") + + # 打开并解析docx文件 + try: + doc = Document(file_path) + + # 提取所有段落文本 + paragraphs = [] + for paragraph in doc.paragraphs: + paragraphs.append(paragraph.text) + + # 用换行符连接所有段落 + content = '\n'.join(paragraphs) + + return content + except Exception as e: + raise Exception(f"Error parsing docx file {file_path}: {str(e)}") @staticmethod def parse_pdf(file_path: str) -> str: - """ - 解析PDF文档 - - 提取文本内容 - - 保留基本格式(换行) - """ - # TODO: 实现PDF文件解析逻辑 - pass + + # 验证文件路径 + if not FileParser.validate_file_path(file_path): + raise ValueError(f"Invalid file path: {file_path}") + + # 尝试导入PyPDF2库 + try: + import PyPDF2 + except ImportError: + raise ImportError("PyPDF2 library is required for parsing .pdf files. Please install it using 'pip install PyPDF2'") + + # 打开并解析pdf文件 + try: + content = "" + with open(file_path, 'rb') as file: + pdf_reader = PyPDF2.PdfReader(file) + + # 提取每一页的文本 + for page in pdf_reader.pages: + content += page.extract_text() + content += "\n" + + return content + except Exception as e: + raise Exception(f"Error parsing pdf file {file_path}: {str(e)}") @staticmethod def validate_file_path(file_path: str) -> bool: - """ - 验证文件路径的有效性 - - 检查文件是否存在 - - 检查文件是否可读 - - 检查文件大小是否合理 - """ - # TODO: 实现文件路径验证逻辑 - pass \ No newline at end of file + + # 检查文件是否存在 + if not os.path.exists(file_path): + return False + + # 检查是否为文件(而非目录) + if not os.path.isfile(file_path): + return False + + # 检查文件是否可读 + if not os.access(file_path, os.R_OK): + return False + + # 检查文件大小是否合理(小于10MB) + file_size = os.path.getsize(file_path) + if file_size > 10 * 1024 * 1024: # 10MB + return False + + return True \ No newline at end of file diff --git a/src/input_handler/input_processor.py b/src/input_handler/input_processor.py index d60790a..542bc0d 100644 --- a/src/input_handler/input_processor.py +++ b/src/input_handler/input_processor.py @@ -9,111 +9,134 @@ class InputProcessor(QObject): input_completed = pyqtSignal() # 输入完成信号 def __init__(self): - """ - 初始化输入处理器 - - 设置初始状态 - - 初始化输入缓冲区 - """ + super().__init__() - # TODO: 实现构造函数逻辑 + # 实现构造函数逻辑 # 1. 初始化输入缓冲区 + self.input_buffer = "" # 2. 设置初始状态 + self.is_input_active = False # 3. 初始化相关属性 - pass + self.expected_text = "" + self.current_position = 0 def process_key_event(self, key: str) -> bool: - """ - 处理按键事件 - - 检查按键有效性 - - 更新输入缓冲区 - - 发送相关信号 - - 返回处理结果 - """ - # TODO: 实现按键事件处理逻辑 + + # 实现按键事件处理逻辑 # 1. 检查按键是否有效 + if not key: + return False + # 2. 根据按键类型处理(字符、功能键等) # 3. 更新输入缓冲区 + self.input_buffer += key + self.current_position += 1 + # 4. 发送text_changed信号 + self.text_changed.emit(key) + # 5. 检查是否完成输入,如是则发送input_completed信号 + if self.expected_text and self.input_buffer == self.expected_text: + self.input_completed.emit() + # 6. 返回处理结果 - pass + return True def get_current_input(self) -> str: - """ - 获取当前输入 - - 返回输入缓冲区内容 - """ - # TODO: 实现获取当前输入逻辑 + + # 实现获取当前输入逻辑 # 1. 返回输入缓冲区内容 - pass + return self.input_buffer def reset_input(self): - """ - 重置输入 - - 清空输入缓冲区 - - 重置相关状态 - """ - # TODO: 实现输入重置逻辑 + + # 实现输入重置逻辑 # 1. 清空输入缓冲区 + self.input_buffer = "" # 2. 重置相关状态变量 + self.current_position = 0 + self.is_input_active = False # 3. 发送重置信号(如需要) - pass def set_expected_text(self, text: str): - """ - 设置期望文本 - - 用于后续输入验证 - """ - # TODO: 实现设置期望文本逻辑 + + # 实现设置期望文本逻辑 # 1. 保存期望文本 + self.expected_text = text # 2. 初始化匹配相关状态 - pass + self.current_position = 0 + self.input_buffer = "" + self.is_input_active = True class InputValidator: def __init__(self): - """ - 初始化输入验证器 - - 设置验证规则 - """ - # TODO: 实现构造函数逻辑 + + # 实现构造函数逻辑 # 1. 初始化验证规则 + self.case_sensitive = True # 2. 设置默认验证参数 - pass + self.min_accuracy = 0.0 def validate_character(self, input_char: str, expected_char: str) -> bool: - """ - 验证字符输入 - - 比较输入字符与期望字符 - - 返回验证结果 - """ - # TODO: 实现字符验证逻辑 + + # 实现字符验证逻辑 # 1. 比较输入字符与期望字符 # 2. 考虑大小写敏感性设置 + if self.case_sensitive: + return input_char == expected_char + else: + return input_char.lower() == expected_char.lower() # 3. 返回验证结果 - pass def validate_word(self, input_word: str, expected_word: str) -> dict: - """ - 验证单词输入 - - 比较输入单词与期望单词 - - 返回详细验证结果(正确字符数、错误字符数等) - """ - # TODO: 实现单词验证逻辑 + + # 实现单词验证逻辑 # 1. 逐字符比较输入单词与期望单词 + correct_count = 0 + incorrect_count = 0 + total_chars = max(len(input_word), len(expected_word)) + # 2. 统计正确/错误字符数 + for i in range(total_chars): + input_char = input_word[i] if i < len(input_word) else "" + expected_char = expected_word[i] if i < len(expected_word) else "" + + if self.validate_character(input_char, expected_char): + correct_count += 1 + else: + incorrect_count += 1 + # 3. 计算准确率 + accuracy = correct_count / total_chars if total_chars > 0 else 0.0 + # 4. 返回验证结果字典 - pass + return { + "correct_count": correct_count, + "incorrect_count": incorrect_count, + "total_chars": total_chars, + "accuracy": accuracy + } def calculate_accuracy(self, input_text: str, expected_text: str) -> float: - """ - 计算输入准确率 - - 比较输入文本与期望文本 - - 返回准确率百分比 - """ - # TODO: 实现准确率计算逻辑 + + # 实现准确率计算逻辑 # 1. 比较输入文本与期望文本 # 2. 统计正确字符数 + correct_count = 0 + total_chars = max(len(input_text), len(expected_text)) + + if total_chars == 0: + return 1.0 # 两个空字符串认为是完全匹配 + + for i in range(total_chars): + input_char = input_text[i] if i < len(input_text) else "" + expected_char = expected_text[i] if i < len(expected_text) else "" + + if self.validate_character(input_char, expected_char): + correct_count += 1 + # 3. 计算准确率百分比 + accuracy = correct_count / total_chars + # 4. 返回准确率 - pass \ No newline at end of file + return accuracy \ No newline at end of file diff --git a/src/main.py b/src/main.py index 40e96f8..177583a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,22 +1,46 @@ # main.py import sys +import traceback from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import Qt from src.main_window import MainWindow def main(): """ 应用程序主入口点 - 创建QApplication实例 + - 设置应用程序属性 - 创建MainWindow实例 - 显示窗口 - 启动事件循环 - 返回退出码 """ - # TODO: 实现主函数逻辑 - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec_()) + try: + # 创建QApplication实例 + app = QApplication(sys.argv) + + # 设置应用程序属性 + app.setApplicationName("隐私学习软件") + app.setApplicationVersion("1.0") + app.setOrganizationName("个人开发者") + + # 设置高DPI支持 + app.setAttribute(Qt.AA_EnableHighDpiScaling, True) + app.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + # 创建主窗口 + window = MainWindow() + window.show() + + # 启动事件循环并返回退出码 + exit_code = app.exec_() + sys.exit(exit_code) + + except Exception as e: + # 打印详细的错误信息 + print(f"应用程序发生未捕获的异常: {e}") + traceback.print_exc() + sys.exit(1) if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/main_window.py b/src/main_window.py index 2459aa2..918e808 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,8 +1,10 @@ import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QAction, - QFileDialog, QVBoxLayout, QWidget, QLabel, QStatusBar) -from PyQt5.QtGui import QFont, QTextCharFormat, QColor + QFileDialog, QVBoxLayout, QWidget, QLabel, QStatusBar, QMessageBox) +from PyQt5.QtGui import QFont, QTextCharFormat, QColor, QTextCursor from PyQt5.QtCore import Qt +from src.file_parser import FileParser +from src.typing_logic import TypingLogic class MainWindow(QMainWindow): def __init__(self): @@ -14,8 +16,13 @@ class MainWindow(QMainWindow): - 初始化当前输入位置 - 调用initUI()方法 """ - # TODO: 实现构造函数逻辑 - pass + super().__init__() + self.learning_content = "" + self.current_position = 0 + self.typing_logic = None + self.text_edit = None + self.status_bar = None + self.initUI() def initUI(self): """ @@ -25,8 +32,24 @@ class MainWindow(QMainWindow): - 创建状态栏并显示"就绪" - 连接文本变化信号到onTextChanged """ - # TODO: 实现UI初始化逻辑 - pass + # 设置窗口属性 + self.setWindowTitle("隐私学习软件 - 仿Word") + self.setGeometry(100, 100, 800, 600) + + # 创建中央文本编辑区域 + self.text_edit = QTextEdit() + self.text_edit.setFont(QFont("Arial", 12)) + self.setCentralWidget(self.text_edit) + + # 创建菜单栏 + self.createMenuBar() + + # 创建状态栏 + self.status_bar = self.statusBar() + self.status_bar.showMessage("就绪") + + # 连接文本变化信号 + self.text_edit.textChanged.connect(self.onTextChanged) def createMenuBar(self): """ @@ -35,8 +58,39 @@ class MainWindow(QMainWindow): - 帮助菜单:关于 - 为每个菜单项连接对应的槽函数 """ - # TODO: 实现菜单栏创建逻辑 - pass + menu_bar = self.menuBar() + + # 文件菜单 + file_menu = menu_bar.addMenu('文件') + + # 打开动作 + open_action = QAction('打开', self) + open_action.setShortcut('Ctrl+O') + open_action.triggered.connect(self.openFile) + file_menu.addAction(open_action) + + # 保存动作 + save_action = QAction('保存', self) + save_action.setShortcut('Ctrl+S') + save_action.triggered.connect(self.saveFile) + file_menu.addAction(save_action) + + # 分隔线 + file_menu.addSeparator() + + # 退出动作 + exit_action = QAction('退出', self) + exit_action.setShortcut('Ctrl+Q') + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # 帮助菜单 + help_menu = menu_bar.addMenu('帮助') + + # 关于动作 + about_action = QAction('关于', self) + about_action.triggered.connect(self.showAbout) + help_menu.addAction(about_action) def openFile(self): """ @@ -46,8 +100,33 @@ class MainWindow(QMainWindow): - 成功时:将内容显示在文本区域,重置打字状态 - 失败时:显示错误消息框 """ - # TODO: 实现打开文件逻辑 - pass + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName( + self, + "打开文件", + "", + "文本文件 (*.txt);;Word文档 (*.docx);;所有文件 (*)", + options=options + ) + + if file_path: + try: + # 解析文件内容 + content = FileParser.parse_file(file_path) + self.learning_content = content + + # 显示内容到文本编辑区域 + self.text_edit.setPlainText(content) + + # 重置打字状态 + self.typing_logic = TypingLogic(content) + self.current_position = 0 + + # 更新状态栏 + self.status_bar.showMessage(f"已打开文件: {file_path}") + except Exception as e: + # 显示错误消息框 + QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}") def saveFile(self): """ @@ -56,16 +135,49 @@ class MainWindow(QMainWindow): - 将文本区域内容写入选定文件 - 返回操作结果 """ - # TODO: 实现保存文件逻辑 - pass + options = QFileDialog.Options() + file_path, _ = QFileDialog.getSaveFileName( + self, + "保存文件", + "", + "文本文件 (*.txt);;所有文件 (*)", + options=options + ) + + if file_path: + try: + # 获取文本编辑区域的内容 + content = self.text_edit.toPlainText() + + # 写入文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + # 更新状态栏 + self.status_bar.showMessage(f"文件已保存: {file_path}") + + return True + except Exception as e: + # 显示错误消息框 + QMessageBox.critical(self, "错误", f"无法保存文件:\n{str(e)}") + return False + + return False def showAbout(self): """ 显示关于对话框 - 显示消息框,包含软件名称、版本、描述 """ - # TODO: 实现关于对话框逻辑 - pass + QMessageBox.about( + self, + "关于", + "隐私学习软件 - 仿Word\n\n" + "版本: 1.0\n\n" + "这是一个用于隐私学习的打字练习软件,\n" + "可以加载文档并进行打字练习,\n" + "帮助提高打字速度和准确性。" + ) def onTextChanged(self): """ @@ -74,8 +186,30 @@ class MainWindow(QMainWindow): - 调用打字逻辑检查输入正确性 - 更新高亮显示和状态栏 """ - # TODO: 实现文本变化处理逻辑 - pass + if self.typing_logic is None: + return + + # 获取当前文本内容 + current_text = self.text_edit.toPlainText() + + # 调用打字逻辑检查输入正确性 + result = self.typing_logic.check_input(current_text) + + # 更新高亮显示 + if result['correct']: + self.highlightText(len(current_text), QColor('lightgreen')) + else: + # 高亮显示错误部分 + self.highlightText(len(current_text), QColor('lightcoral')) + + # 更新状态栏 + progress = self.typing_logic.get_progress() + accuracy = result.get('accuracy', 0) * 100 + self.status_bar.showMessage( + f"进度: {progress['percentage']:.1f}% | " + f"准确率: {accuracy:.1f}% | " + f"位置: {result['position']}/{progress['total']}" + ) def highlightText(self, position, color): """ @@ -84,5 +218,24 @@ class MainWindow(QMainWindow): - 应用背景颜色格式 - 恢复光标位置 """ - # TODO: 实现文本高亮逻辑 - pass \ No newline at end of file + # 创建文本格式 + format = QTextCharFormat() + format.setBackground(color) + + # 获取文本游标 + cursor = self.text_edit.textCursor() + + # 保存当前光标位置 + current_pos = cursor.position() + + # 选择从开始到指定位置的文本 + cursor.select(QTextCursor.Document) + cursor.setPosition(0, QTextCursor.MoveAnchor) + cursor.setPosition(position, QTextCursor.KeepAnchor) + + # 应用格式 + cursor.mergeCharFormat(format) + + # 恢复光标位置 + cursor.setPosition(current_pos) + self.text_edit.setTextCursor(cursor) \ No newline at end of file diff --git a/src/services/network_service.py b/src/services/network_service.py index 76095ea..e8ee040 100644 --- a/src/services/network_service.py +++ b/src/services/network_service.py @@ -1,87 +1,207 @@ # services/network_service.py import requests import json +import os from typing import Optional, Dict, Any class NetworkService: def __init__(self): - """ - 初始化网络服务 - - 设置API密钥 - - 初始化缓存 - """ - # TODO: 实现构造函数逻辑 - pass + + # 实现构造函数逻辑 + self.api_key = None + self.cache = {} + self.session = requests.Session() def get_weather_info(self) -> Optional[Dict[str, Any]]: - """ - 获取天气信息 - - 调用天气API - - 解析返回数据 - - 返回格式化的天气信息 - """ - # TODO: 实现天气信息获取逻辑 + + # 实现天气信息获取逻辑 # 1. 获取用户IP地址 - # 2. 根据IP获取地理位置 - # 3. 调用天气API获取天气数据 - # 4. 解析并格式化数据 - # 5. 返回天气信息字典 - pass + try: + ip_response = self.session.get("https://httpbin.org/ip", timeout=5) + ip_data = ip_response.json() + ip = ip_data.get("origin", "") + + # 2. 根据IP获取地理位置 + # 注意:这里使用免费的IP地理位置API,实际应用中可能需要更精确的服务 + location_response = self.session.get(f"http://ip-api.com/json/{ip}", timeout=5) + location_data = location_response.json() + + if location_data.get("status") != "success": + return None + + city = location_data.get("city", "Unknown") + + # 3. 调用天气API获取天气数据 + # 注意:这里使用OpenWeatherMap API作为示例,需要API密钥 + # 在实际应用中,需要设置有效的API密钥 + if self.api_key: + weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn" + weather_response = self.session.get(weather_url, timeout=5) + weather_data = weather_response.json() + + # 4. 解析并格式化数据 + if weather_response.status_code == 200: + formatted_weather = { + "city": city, + "temperature": weather_data["main"]["temp"], + "description": weather_data["weather"][0]["description"], + "humidity": weather_data["main"]["humidity"], + "wind_speed": weather_data["wind"]["speed"] + } + # 5. 返回天气信息字典 + return formatted_weather + else: + # 模拟天气数据(无API密钥时) + return { + "city": city, + "temperature": 20, + "description": "晴天", + "humidity": 60, + "wind_speed": 3.5 + } + except Exception as e: + print(f"获取天气信息时出错: {e}") + return None def get_daily_quote(self) -> Optional[str]: - """ - 获取每日一句 - - 调用名言API - - 返回格式化的名言 - """ - # TODO: 实现每日一句获取逻辑 + + # 实现每日一句获取逻辑 # 1. 调用名言API - # 2. 解析返回的名言数据 - # 3. 格式化名言文本 - # 4. 返回名言字符串 - pass + try: + # 使用一个免费的名言API + response = self.session.get("https://api.quotable.io/random", timeout=5) + + # 2. 解析返回的名言数据 + if response.status_code == 200: + quote_data = response.json() + content = quote_data.get("content", "") + author = quote_data.get("author", "") + + # 3. 格式化名言文本 + formatted_quote = f'"{content}" - {author}' + + # 4. 返回名言字符串 + return formatted_quote + else: + # 如果API调用失败,返回默认名言 + return "书山有路勤为径,学海无涯苦作舟。" + except Exception as e: + print(f"获取每日一句时出错: {e}") + # 出错时返回默认名言 + return "书山有路勤为径,学海无涯苦作舟。" def download_image(self, url: str) -> Optional[bytes]: - """ - 下载图片 - - 从指定URL下载图片 - - 返回图片二进制数据 - """ - # TODO: 实现图片下载逻辑 - # 1. 发送HTTP GET请求获取图片 - # 2. 检查响应状态码 - # 3. 返回图片二进制数据 - pass + + # 实现图片下载逻辑 + # 1. 发送GET请求下载图片 + try: + response = self.session.get(url, timeout=10) + + # 2. 检查响应状态码 + if response.status_code == 200: + # 3. 返回图片的二进制数据 + return response.content + else: + print(f"下载图片失败,状态码: {response.status_code}") + return None + except Exception as e: + print(f"下载图片时出错: {e}") + return None class ImageService: def __init__(self): - """ - 初始化图片服务 - """ - # TODO: 实现构造函数逻辑 - pass + + # 实现构造函数逻辑 + self.supported_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.gif'} + self.image_cache = {} + self.max_cache_size = 100 # 最大缓存图片数量 def extract_images_from_document(self, file_path: str) -> list: - """ - 从文档中提取图片 - - 解析文档中的图片 - - 返回图片列表 - """ - # TODO: 实现图片提取逻辑 - # 1. 根据文件类型选择解析方法 - # 2. 提取文档中的图片数据 - # 3. 返回图片信息列表 - pass + + # 实现从文档提取图片逻辑 + # 1. 检查文件是否存在 + if not os.path.exists(file_path): + print(f"文件不存在: {file_path}") + return [] + + # 2. 检查文件扩展名 + _, ext = os.path.splitext(file_path) + ext = ext.lower() + + # 3. 根据不同文档类型提取图片 + images = [] + + try: + # 简化实现:仅处理PDF文件 + if ext == '.pdf': + # 注意:这需要安装PyMuPDF或pdfplumber库 + # 示例使用PyMuPDF (fitz) + try: + import fitz # PyMuPDF + pdf_document = fitz.open(file_path) + + for page_num in range(len(pdf_document)): + page = pdf_document[page_num] + image_list = page.get_images() + + for img_index, img in enumerate(image_list): + xref = img[0] + base_image = pdf_document.extract_image(xref) + image_bytes = base_image["image"] + images.append(image_bytes) + + pdf_document.close() + except ImportError: + print("需要安装PyMuPDF库: pip install PyMuPDF") + return [] + else: + print(f"不支持的文件格式: {ext}") + return [] + + # 4. 返回提取的图片数据列表 + return images + + except Exception as e: + print(f"从文档提取图片时出错: {e}") + return [] def display_image_at_position(self, image_data: bytes, position: int) -> bool: - """ - 在指定位置显示图片 - - 将图片插入到文本中的指定位置 - - 返回操作结果 - """ - # TODO: 实现图片显示逻辑 - # 1. 创建图片对象 - # 2. 在指定位置插入图片 - # 3. 更新UI显示 - # 4. 返回操作结果 - pass \ No newline at end of file + + # 实现图片显示逻辑 + # 1. 验证图片数据 + if not image_data: + print("无效的图片数据") + return False + + # 2. 验证位置参数 + if position < 0: + print("无效的位置参数") + return False + + # 3. 尝试解析图片数据 + try: + # 使用PIL库处理图片 + try: + from PIL import Image + from io import BytesIO + image = Image.open(BytesIO(image_data)) + + # 4. 缓存图片(如果需要) + if len(self.image_cache) >= self.max_cache_size: + # 移除最旧的缓存项 + oldest_key = next(iter(self.image_cache)) + del self.image_cache[oldest_key] + + self.image_cache[position] = image_data + + # 5. 显示图片(简化实现,实际应用中需要与UI框架集成) + print(f"图片已缓存到位置 {position},尺寸: {image.size},格式: {image.format}") + # 在实际应用中,这里会调用UI框架的相关方法在指定位置显示图片 + + return True + except ImportError: + print("需要安装Pillow库: pip install Pillow") + return False + except Exception as e: + print(f"解析或显示图片时出错: {e}") + return False \ No newline at end of file diff --git a/src/settings/resources/config/test_config.json b/src/settings/resources/config/test_config.json new file mode 100644 index 0000000..9f16c30 --- /dev/null +++ b/src/settings/resources/config/test_config.json @@ -0,0 +1,45 @@ +{ + "application": { + "name": "MagicWord", + "version": "2.0.0", + "author": "MagicWord Team", + "description": "隐私学习软件" + }, + "window": { + "default_size": { + "width": 800, + "height": 600 + }, + "minimum_size": { + "width": 600, + "height": 400 + }, + "title": "MagicWord - 隐私学习软件" + }, + "typing": { + "default_time_limit": 300, + "show_progress_bar": true, + "highlight_current_line": true, + "auto_save_progress": true + }, + "files": { + "supported_formats": [ + ".txt", + ".docx" + ], + "auto_backup_enabled": true, + "backup_interval_minutes": 30 + }, + "network": { + "weather_api_key": "YOUR_WEATHER_API_KEY", + "quote_api_url": "https://api.quotable.io/random", + "timeout_seconds": 10 + }, + "appearance": { + "theme": "light", + "font_family": "Arial", + "font_size": 12, + "text_color": "#000000", + "background_color": "#FFFFFF" + } +} \ No newline at end of file diff --git a/src/settings/settings_manager.py b/src/settings/settings_manager.py index e5fdd76..d26a64a 100644 --- a/src/settings/settings_manager.py +++ b/src/settings/settings_manager.py @@ -10,8 +10,63 @@ class SettingsManager: - 指定配置文件路径 - 加载现有配置或创建默认配置 """ - # TODO: 实现构造函数逻辑 - pass + # 指定配置文件路径 + self.config_file = config_file + self.config_path = os.path.join("resources", "config", config_file) + + # 加载现有配置或创建默认配置 + self.settings = self.load_settings() + if not self.settings: + self.settings = self._create_default_settings() + self.save_settings(self.settings) + + def _create_default_settings(self) -> Dict[str, Any]: + """ + 创建默认配置 + - 返回包含默认设置的字典 + """ + return { + "application": { + "name": "MagicWord", + "version": "1.0.0", + "author": "MagicWord Team", + "description": "好东西" + }, + "window": { + "default_size": { + "width": 800, + "height": 600 + }, + "minimum_size": { + "width": 600, + "height": 400 + }, + "title": "MagicWord" + }, + "typing": { + "default_time_limit": 300, + "show_progress_bar": True, + "highlight_current_line": True, + "auto_save_progress": True + }, + "files": { + "supported_formats": [".txt", ".docx"], + "auto_backup_enabled": True, + "backup_interval_minutes": 30 + }, + "network": { + "weather_api_key": "YOUR_WEATHER_API_KEY", + "quote_api_url": "https://api.quotable.io/random", + "timeout_seconds": 10 + }, + "appearance": { + "theme": "light", + "font_family": "Arial", + "font_size": 12, + "text_color": "#000000", + "background_color": "#FFFFFF" + } + } def load_settings(self) -> Dict[str, Any]: """ @@ -19,8 +74,15 @@ class SettingsManager: - 从配置文件读取设置 - 返回设置字典 """ - # TODO: 实现设置加载逻辑 - pass + try: + if os.path.exists(self.config_path): + with open(self.config_path, 'r', encoding='utf-8') as f: + return json.load(f) + else: + return {} + except (json.JSONDecodeError, IOError) as e: + print(f"加载配置文件时出错: {e}") + return {} def save_settings(self, settings: Dict[str, Any]) -> bool: """ @@ -28,8 +90,19 @@ class SettingsManager: - 将设置保存到配置文件 - 返回保存结果 """ - # TODO: 实现设置保存逻辑 - pass + try: + # 确保目录存在 + config_dir = os.path.dirname(self.config_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 保存设置到文件 + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(settings, f, ensure_ascii=False, indent=4) + return True + except (IOError, TypeError) as e: + print(f"保存配置文件时出错: {e}") + return False def get_setting(self, key: str, default: Any = None) -> Any: """ @@ -37,8 +110,14 @@ class SettingsManager: - 根据键名获取设置值 - 如果不存在返回默认值 """ - # TODO: 实现获取设置项逻辑 - pass + keys = key.split('.') + value = self.settings + try: + for k in keys: + value = value[k] + return value + except (KeyError, TypeError): + return default def set_setting(self, key: str, value: Any) -> bool: """ @@ -46,5 +125,21 @@ class SettingsManager: - 设置指定键的值 - 保存到配置文件 """ - # TODO: 实现设置设置项逻辑 - pass \ No newline at end of file + keys = key.split('.') + setting_dict = self.settings + + # 导航到倒数第二个键 + try: + for k in keys[:-1]: + if k not in setting_dict: + setting_dict[k] = {} + setting_dict = setting_dict[k] + + # 设置最后一个键的值 + setting_dict[keys[-1]] = value + + # 保存到配置文件 + return self.save_settings(self.settings) + except (KeyError, TypeError) as e: + print(f"设置配置项时出错: {e}") + return False \ No newline at end of file diff --git a/src/typing_logic.py b/src/typing_logic.py index 6bf0a83..ce3e659 100644 --- a/src/typing_logic.py +++ b/src/typing_logic.py @@ -1,13 +1,16 @@ class TypingLogic: def __init__(self, learning_content: str): """ - 初始化打字逻辑状态 + 初始化打字逻辑状态。。。。 - 存储学习材料 - 初始化当前索引为0 - 初始化错误计数为0 """ - # TODO: 实现构造函数逻辑 - pass + self.learning_content = learning_content + self.current_index = 0 + self.error_count = 0 + self.total_chars = len(learning_content) + self.typed_chars = 0 def check_input(self, user_text: str) -> dict: """ @@ -22,17 +25,54 @@ class TypingLogic: * completed: 布尔值,是否完成 * accuracy: 浮点数,准确率 """ - # TODO: 实现输入检查逻辑 - pass + # 更新已输入字符数 + self.typed_chars = len(user_text) + + # 如果用户输入的字符数超过了学习材料的长度,截取到相同长度 + if len(user_text) > self.total_chars: + user_text = user_text[:self.total_chars] + + # 检查当前输入是否正确 + correct = True + expected_char = '' + if self.current_index < self.total_chars: + expected_char = self.learning_content[self.current_index] + if len(user_text) > self.current_index and user_text[self.current_index] != expected_char: + correct = False + self.error_count += 1 + else: + # 已经完成所有输入 + return { + "correct": True, + "expected": "", + "position": self.current_index, + "completed": True, + "accuracy": self._calculate_accuracy() + } + + # 更新当前索引 + self.current_index = len(user_text) + + # 检查是否完成 + completed = self.current_index >= self.total_chars + + return { + "correct": correct, + "expected": expected_char, + "position": self.current_index, + "completed": completed, + "accuracy": self._calculate_accuracy() + } - def get_expected_text(self) -> str: + def get_expected_text(self, length: int = 10) -> str: """ 获取用户接下来应该输入的内容 - 返回从当前位置开始的一定长度文本(如10个字符) - 处理文本结束情况 """ - # TODO: 实现期望文本获取逻辑 - pass + start_pos = self.current_index + end_pos = min(start_pos + length, self.total_chars) + return self.learning_content[start_pos:end_pos] def get_progress(self) -> dict: """ @@ -42,8 +82,17 @@ class TypingLogic: - percentage: 浮点数,完成百分比 - remaining: 整数,剩余字符数 """ - # TODO: 实现进度获取逻辑 - pass + current = self.current_index + total = self.total_chars + percentage = (current / total * 100) if total > 0 else 0 + remaining = max(0, total - current) + + return { + "current": current, + "total": total, + "percentage": percentage, + "remaining": remaining + } def reset(self, new_content: str = None): """ @@ -52,8 +101,12 @@ class TypingLogic: - 重置错误计数 - 如果提供了新内容,更新学习材料 """ - # TODO: 实现重置逻辑 - pass + if new_content is not None: + self.learning_content = new_content + self.total_chars = len(new_content) + self.current_index = 0 + self.error_count = 0 + self.typed_chars = 0 def get_statistics(self) -> dict: """ @@ -63,5 +116,19 @@ class TypingLogic: - error_count: 整数,错误次数 - accuracy_rate: 浮点数,准确率 """ - # TODO: 实现统计信息获取逻辑 - pass \ No newline at end of file + return { + "total_chars": self.total_chars, + "typed_chars": self.typed_chars, + "error_count": self.error_count, + "accuracy_rate": self._calculate_accuracy() + } + + def _calculate_accuracy(self) -> float: + """ + 计算准确率 + """ + if self.typed_chars == 0: + return 0.0 + # 准确率 = (已输入字符数 - 错误次数) / 已输入字符数 + accuracy = (self.typed_chars - self.error_count) / self.typed_chars + return max(0.0, accuracy) # 确保准确率不为负数 \ No newline at end of file diff --git a/src/ui/components.py b/src/ui/components.py index 2763f93..948839f 100644 --- a/src/ui/components.py +++ b/src/ui/components.py @@ -10,12 +10,8 @@ class CustomTitleBar(QWidget): - 添加窗口控制按钮 """ super().__init__(parent) - # TODO: 实现标题栏UI - # 1. 创建标题标签 - # 2. 创建最小化、最大化、关闭按钮 - # 3. 设置布局和样式 - # 4. 连接按钮事件 - pass + self.parent = parent + self.setup_ui() def setup_ui(self): """ @@ -23,42 +19,92 @@ class CustomTitleBar(QWidget): - 初始化所有UI组件 - 设置组件属性和样式 """ - # TODO: 实现UI设置逻辑 - # 1. 创建水平布局 - # 2. 添加标题标签和控制按钮 - # 3. 设置组件样式 - pass + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(10) + + # 创建标题标签 + self.title_label = QLabel("MagicWord") + self.title_label.setStyleSheet("color: #333333; font-size: 12px; font-weight: normal;") + + # 创建控制按钮 + self.minimize_button = QPushButton("—") + self.maximize_button = QPushButton("□") + self.close_button = QPushButton("×") + + # 设置按钮样式 + button_style = """ + QPushButton { + background-color: transparent; + border: none; + color: #333333; + font-size: 12px; + font-weight: normal; + width: 30px; + height: 30px; + } + QPushButton:hover { + background-color: #d0d0d0; + } + """ + + self.minimize_button.setStyleSheet(button_style) + self.maximize_button.setStyleSheet(button_style) + self.close_button.setStyleSheet(button_style + "QPushButton:hover { background-color: #ff5555; color: white; }") + + # 添加组件到布局 + layout.addWidget(self.title_label) + layout.addStretch() + layout.addWidget(self.minimize_button) + layout.addWidget(self.maximize_button) + layout.addWidget(self.close_button) + + self.setLayout(layout) + + # 连接按钮事件 + self.minimize_button.clicked.connect(self.minimize_window) + self.maximize_button.clicked.connect(self.maximize_window) + self.close_button.clicked.connect(self.close_window) + + # 设置标题栏样式 + self.setStyleSheet(""" + CustomTitleBar { + background-color: #f0f0f0; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom: 1px solid #d0d0d0; + } + """) def minimize_window(self): """ 最小化窗口 - 触发窗口最小化事件 """ - # TODO: 实现窗口最小化逻辑 - # 1. 获取父窗口 - # 2. 调用窗口最小化方法 - pass + if self.parent: + self.parent.showMinimized() def maximize_window(self): """ 最大化窗口 - 切换窗口最大化状态 """ - # TODO: 实现窗口最大化逻辑 - # 1. 获取父窗口 - # 2. 检查当前窗口状态 - # 3. 切换最大化/还原状态 - pass + if self.parent: + if self.parent.isMaximized(): + self.parent.showNormal() + self.maximize_button.setText("□") + else: + self.parent.showMaximized() + self.maximize_button.setText("❐") def close_window(self): """ 关闭窗口 - 触发窗口关闭事件 """ - # TODO: 实现窗口关闭逻辑 - # 1. 获取父窗口 - # 2. 调用窗口关闭方法 - pass + if self.parent: + self.parent.close() class ProgressBarWidget(QWidget): def __init__(self, parent=None): @@ -68,11 +114,69 @@ class ProgressBarWidget(QWidget): - 显示统计信息 """ super().__init__(parent) - # TODO: 实现进度条组件初始化 - # 1. 创建进度条UI元素 - # 2. 创建统计信息标签 - # 3. 设置布局 - pass + self.setup_ui() + + def setup_ui(self): + """ + 设置进度条UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建垂直布局 + layout = QVBoxLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(5) + + # 导入需要的模块 + from PyQt5.QtWidgets import QProgressBar, QHBoxLayout + + # 创建水平布局用于统计信息 + stats_layout = QHBoxLayout() + stats_layout.setSpacing(15) + + # 创建统计信息标签 + self.wpm_label = QLabel("WPM: 0") + self.accuracy_label = QLabel("准确率: 0%") + self.time_label = QLabel("用时: 0s") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.wpm_label.setStyleSheet(label_style) + self.accuracy_label.setStyleSheet(label_style) + self.time_label.setStyleSheet(label_style) + + # 添加标签到统计布局 + stats_layout.addWidget(self.wpm_label) + stats_layout.addWidget(self.accuracy_label) + stats_layout.addWidget(self.time_label) + stats_layout.addStretch() + + # 创建进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(True) + self.progress_bar.setFormat("进度: %p%") + + # 设置进度条样式 + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid #c0c0c0; + border-radius: 0px; + text-align: center; + background-color: #f0f0f0; + } + QProgressBar::chunk { + background-color: #0078d7; + border-radius: 0px; + } + """) + + # 添加组件到主布局 + layout.addLayout(stats_layout) + layout.addWidget(self.progress_bar) + + self.setLayout(layout) def update_progress(self, progress: float): """ @@ -80,21 +184,18 @@ class ProgressBarWidget(QWidget): - 设置进度值 - 更新显示 """ - # TODO: 实现进度更新逻辑 - # 1. 更新进度条数值 - # 2. 刷新UI显示 - pass + self.progress_bar.setValue(int(progress)) def update_stats(self, wpm: int, accuracy: float, time_elapsed: int): """ 更新统计信息 - - 显示WPM、准确率、用时等信息 + - wpm: 每分钟字数 + - accuracy: 准确率(%) + - time_elapsed: 用时(秒) """ - # TODO: 实现统计信息更新逻辑 - # 1. 更新WPM标签 - # 2. 更新准确率标签 - # 3. 更新用时标签 - pass + self.wpm_label.setText(f"WPM: {wpm}") + self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") + self.time_label.setText(f"用时: {time_elapsed}s") class TextDisplayWidget(QWidget): def __init__(self, parent=None): @@ -102,47 +203,123 @@ class TextDisplayWidget(QWidget): 文本显示组件 - 显示待练习文本 - 高亮当前字符 - - 显示用户输入 + - 显示用户输入反馈 """ super().__init__(parent) - # TODO: 实现文本显示组件初始化 - # 1. 创建文本显示区域 - # 2. 设置文本样式 - # 3. 初始化高亮相关属性 - pass + self.text_content = "" + self.current_index = 0 + self.setup_ui() + + def setup_ui(self): + """ + 设置文本显示UI + - 初始化文本显示区域 + - 设置样式和布局 + """ + # 创建垂直布局 + layout = QVBoxLayout() + layout.setContentsMargins(20, 20, 20, 20) + + # 导入需要的模块 + from PyQt5.QtWidgets import QTextEdit + from PyQt5.QtCore import Qt + + # 创建文本显示区域 + self.text_display = QTextEdit() + self.text_display.setReadOnly(True) + self.text_display.setLineWrapMode(QTextEdit.WidgetWidth) + + # 设置文本显示样式 + self.text_display.setStyleSheet(""" + QTextEdit { + font-family: 'Calibri', 'Segoe UI', 'Microsoft YaHei', sans-serif; + font-size: 12pt; + border: 1px solid #d0d0d0; + border-radius: 0px; + padding: 15px; + background-color: white; + color: black; + } + """) + + # 添加组件到布局 + layout.addWidget(self.text_display) + self.setLayout(layout) def set_text(self, text: str): """ 设置显示文本 - - 更新显示内容 - - 重置高亮状态 - """ - # TODO: 实现文本设置逻辑 - # 1. 更新内部文本内容 - # 2. 重置高亮位置 - # 3. 刷新UI显示 - pass + - text: 要显示的文本内容 + """ + self.text_content = text + self.current_index = 0 + self._update_display() def highlight_character(self, position: int): """ - 高亮指定位置字符 - - 更新高亮位置 - - 刷新显示 + 高亮指定位置的字符 + - position: 字符位置索引 """ - # TODO: 实现字符高亮逻辑 - # 1. 计算高亮范围 - # 2. 应用高亮样式 - # 3. 滚动到高亮位置 - pass + if 0 <= position < len(self.text_content): + self.current_index = position + self._update_display() + def _update_display(self, user_input: str = ""): + """ + 更新文本显示 + - user_input: 用户输入文本(可选) + """ + # 导入需要的模块 + from PyQt5.QtGui import QTextCursor + import sys + import os + sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + from constants import COLOR_CORRECT, COLOR_WRONG, COLOR_HIGHLIGHT + + if not self.text_content: + self.text_display.clear() + return + + # 创建带格式的HTML文本 + formatted_text = "" + + # 如果有用户输入,对比显示 + if user_input: + for i, char in enumerate(self.text_content): + if i < len(user_input): + if char == user_input[i]: + # 正确字符 + formatted_text += f'{char}' + else: + # 错误字符 + formatted_text += f'{char}' + elif i == len(user_input): + # 当前字符 + formatted_text += f'{char}' + else: + # 未到达的字符 + formatted_text += char + else: + # 没有用户输入,只显示原文和当前高亮字符 + for i, char in enumerate(self.text_content): + if i == self.current_index: + # 当前字符高亮 + formatted_text += f'{char}' + else: + formatted_text += char + + # 更新文本显示 + self.text_display.setHtml(formatted_text) + + # 滚动到当前高亮字符位置 + cursor = self.text_display.textCursor() + cursor.setPosition(min(self.current_index + 5, len(self.text_content))) + self.text_display.setTextCursor(cursor) + self.text_display.ensureCursorVisible() + def show_user_input(self, input_text: str): """ - 显示用户输入 - - 在文本下方显示用户输入 - - 高亮正确/错误字符 + 显示用户输入反馈 + - input_text: 用户输入的文本 """ - # TODO: 实现用户输入显示逻辑 - # 1. 对比用户输入与原文 - # 2. 分别高亮正确和错误字符 - # 3. 更新输入显示区域 - pass \ No newline at end of file + self._update_display(input_text) \ No newline at end of file diff --git a/src/utils/helper_functions.py b/src/utils/helper_functions.py index af3cb2f..db80af9 100644 --- a/src/utils/helper_functions.py +++ b/src/utils/helper_functions.py @@ -11,8 +11,21 @@ class Utils: - 尝试多种编码格式 - 返回最可能的编码 """ - # TODO: 实现编码检测逻辑 - pass + import chardet + + # 读取文件的前1024字节用于编码检测 + with open(file_path, 'rb') as f: + raw_data = f.read(1024) + + # 使用chardet检测编码 + result = chardet.detect(raw_data) + encoding = result['encoding'] + + # 如果chardet无法确定编码,则默认使用utf-8 + if encoding is None: + encoding = 'utf-8' + + return encoding @staticmethod def format_file_size(size_bytes: int) -> str: @@ -20,9 +33,25 @@ class Utils: 格式化文件大小 - 将字节数转换为可读格式 - 返回格式化字符串 + 参数: + size_bytes (int): 需要格式化的文件大小,单位为字节 + 返回: + str: 格式化后的文件大小字符串,如 "1.5 MB" """ - # TODO: 实现文件大小格式化逻辑 - pass + # 如果文件大小为0字节,直接返回 "0 B" + if size_bytes == 0: + return "0 B" + + # 定义文件大小单位列表 + size_names = ["B", "KB", "MB", "GB", "TB"] + i = 0 + # 当文件大小大于等于1024且未到达最大单位时,循环除以1024 + while size_bytes >= 1024.0 and i < len(size_names) - 1: + size_bytes /= 1024.0 + i += 1 + + # 返回格式化后的字符串,保留一位小数 + return f"{size_bytes:.1f} {size_names[i]}" @staticmethod def calculate_file_hash(file_path: str) -> str: @@ -31,5 +60,11 @@ class Utils: - 使用SHA256算法 - 返回哈希字符串 """ - # TODO: 实现文件哈希计算逻辑 - pass \ No newline at end of file + sha256_hash = hashlib.sha256() + + # 分块读取文件以避免大文件占用过多内存 + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha256_hash.update(chunk) + + return sha256_hash.hexdigest() \ No newline at end of file