From b11452aea820e824d32471b8193737f685b0b2e9 Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Fri, 14 Feb 2025 16:15:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=20v3.5.0=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.bat | 138 +++++++++++++++++++++++++------------ build_nezha.spec | 70 ------------------- gui/main_window_new.py | 145 +++++++++++++++++++++++++++++++++++++++ main.py | 29 ++++++++ testbuild.bat | 139 +++++++++++++++++++++++-------------- utils/version_manager.py | 10 ++- version.txt | 2 +- 7 files changed, 369 insertions(+), 164 deletions(-) delete mode 100644 build_nezha.spec create mode 100644 gui/main_window_new.py diff --git a/build.bat b/build.bat index 40ed820..dbab6ad 100644 --- a/build.bat +++ b/build.bat @@ -1,65 +1,119 @@ @echo off chcp 65001 -echo 开始打包流程... +setlocal EnableDelayedExpansion +echo 开始正式版本打包... + +:: 设置工作目录为脚本所在目录 +cd /d "%~dp0" + +:: 激活虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo 激活虚拟环境... + call venv\Scripts\activate.bat +) else ( + echo 警告: 未找到虚拟环境,使用系统 Python +) + +:: 确保安装了必要的包 +echo 检查依赖包... +pip install -r requirements.txt :: 更新版本号 python update_version.py :: 读取版本号 set /p VERSION= 3.4) -for /f "tokens=1,2 delims=." %%a in ("%VERSION%") do ( +:: 提取主版本号和次版本号 (3.4.7 -> 3.4) +for /f "tokens=1,2 delims=." %%a in ("!VERSION!") do ( set MAJOR_VERSION=%%a.%%b ) -echo 主版本目录: %MAJOR_VERSION% +echo 主版本目录: !MAJOR_VERSION! :: 创建版本目录 -set VERSION_DIR=dist\%MAJOR_VERSION% -if not exist "%VERSION_DIR%" ( - mkdir "%VERSION_DIR%" - echo 创建目录: %VERSION_DIR% +set VERSION_DIR=dist\!MAJOR_VERSION! +if not exist "!VERSION_DIR!" ( + mkdir "!VERSION_DIR!" + echo 创建目录: !VERSION_DIR! ) -:: 使用新的spec文件进行打包 -pyinstaller --noconfirm build_nezha.spec +:: 清理 Python 缓存文件 +echo 清理Python缓存文件... +for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d" +del /s /q *.pyc >nul 2>&1 +del /s /q *.pyo >nul 2>&1 -:: 检查源文件是否存在 -echo 检查文件: dist\听泉cursor助手%VERSION%.exe -if not exist "dist\听泉cursor助手%VERSION%.exe" ( - echo 错误: 打包后的文件不存在 - echo 预期文件路径: dist\听泉cursor助手%VERSION%.exe +:: 清理旧的打包文件 +echo 清理旧文件... +if exist "build" rd /s /q "build" +if exist "*.spec" del /f /q "*.spec" + +:: 使用优化选项进行打包 +echo 开始打包... +pyinstaller ^ + --noconfirm ^ + --clean ^ + --onefile ^ + --noconsole ^ + --icon=icon/two.ico ^ + --name "听泉cursor助手!VERSION!" ^ + --add-data "icon;icon" ^ + --add-data "version.txt;." ^ + --add-data "testversion.txt;." ^ + --add-data "requirements.txt;." ^ + --exclude-module _tkinter ^ + --exclude-module tkinter ^ + --exclude-module PIL.ImageTk ^ + --exclude-module PIL.ImageWin ^ + --exclude-module numpy ^ + --exclude-module pandas ^ + --exclude-module matplotlib ^ + --exclude "__pycache__" ^ + --exclude "*.pyc" ^ + --exclude "*.pyo" ^ + --exclude "*.pyd" ^ + main.py + +:: 检查打包结果并移动文件 +set TEMP_FILE=dist\听泉cursor助手!VERSION!.exe +set TARGET_FILE=!VERSION_DIR!\听泉cursor助手v!VERSION!.exe + +echo 检查文件: !TEMP_FILE! +if exist "!TEMP_FILE!" ( + echo 正式版本打包成功! + + :: 移动到版本目录 + echo 移动文件到: !TARGET_FILE! + move "!TEMP_FILE!" "!TARGET_FILE!" + + :: 显示文件大小 + for %%I in ("!TARGET_FILE!") do ( + echo 文件大小: %%~zI 字节 + ) + + echo. + echo 正式版本构建完成! + echo 版本号: v!VERSION! + echo 文件位置: !TARGET_FILE! +) else ( + echo 错误: 打包失败,文件不存在 + echo 预期文件路径: !TEMP_FILE! dir /b dist - pause exit /b 1 ) -:: 检查目标目录是否存在 -echo 检查目标目录: %VERSION_DIR% -if not exist "%VERSION_DIR%" ( - echo 错误: 目标目录不存在 - pause - exit /b 1 +:: 清理临时文件 +echo 清理临时文件... +if exist "build" rd /s /q "build" +if exist "dist\听泉cursor助手!VERSION!.exe" del /f /q "dist\听泉cursor助手!VERSION!.exe" +if exist "*.spec" del /f /q "*.spec" + +:: 退出虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo 退出虚拟环境... + deactivate ) -:: 移动文件到版本目录 -echo 移动文件: -echo 源文件: dist\听泉cursor助手%VERSION%.exe -echo 目标文件: %VERSION_DIR%\听泉cursor助手v%VERSION%.exe -move "dist\听泉cursor助手%VERSION%.exe" "%VERSION_DIR%\听泉cursor助手v%VERSION%.exe" -if errorlevel 1 ( - echo 移动文件失败,请检查: - echo 1. 源文件是否存在: dist\听泉cursor助手%VERSION%.exe - echo 2. 目标目录是否可写: %VERSION_DIR% - echo 3. 目标文件是否已存在: %VERSION_DIR%\听泉cursor助手v%VERSION%.exe - dir /b dist - dir /b "%VERSION_DIR%" - pause - exit /b 1 -) - -echo. -echo 打包完成! -echo 文件保存在: %VERSION_DIR%\听泉cursor助手v%VERSION%.exe +endlocal pause \ No newline at end of file diff --git a/build_nezha.spec b/build_nezha.spec deleted file mode 100644 index 02d02d6..0000000 --- a/build_nezha.spec +++ /dev/null @@ -1,70 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- -import os -import sys -from PyInstaller.utils.hooks import collect_all - -def get_version(): - with open('version.txt', 'r', encoding='utf-8') as f: - version = f.read().strip() - return version - -version = get_version() - -# 收集所有需要的依赖 -datas = [('icon', 'icon'), ('version.txt', '.')] -binaries = [] -hiddenimports = [ - 'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关 - 'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关 - 'PyQt5.sip', # PyQt5 必需 - 'PyQt5.QtNetwork', # 网络相关 - 'PIL', # Pillow 相关 - 'PIL._imaging', # Pillow 核心 - 'requests', 'urllib3', 'certifi', # 网络请求相关 - 'json', 'uuid', 'hashlib', 'logging', # 基础功能相关 - 'importlib', # 导入相关 - 'pkg_resources', # 包资源 -] - -# 主要的分析对象 -a = Analysis( - ['main.py'], - pathex=[], - binaries=binaries, - datas=datas, - hiddenimports=hiddenimports, - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=['_tkinter', 'tkinter', 'Tkinter'], - win_no_prefer_redirects=False, - win_private_assemblies=False, - noarchive=False, - module_collection_mode={'PyQt5': 'pyz+py'}, -) - -# 创建PYZ -pyz = PYZ(a.pure) - -# 创建EXE -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name=f'听泉cursor助手{version}', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - icon=['icon\\two.ico'], -) \ No newline at end of file diff --git a/gui/main_window_new.py b/gui/main_window_new.py new file mode 100644 index 0000000..e8bba7c --- /dev/null +++ b/gui/main_window_new.py @@ -0,0 +1,145 @@ +import sys +from pathlib import Path +import logging +import os +from PIL import Image +from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QFrame, QTextEdit, + QMessageBox, QApplication, QSystemTrayIcon, QMenu, + QDialog, QProgressBar, QStyle) +from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal +from PyQt5.QtGui import QIcon, QPixmap +import time +import requests +from urllib.parse import quote +import subprocess + +sys.path.append(str(Path(__file__).parent.parent)) + +from utils.config import Config +from utils.version_manager import VersionManager +from account_switcher import AccountSwitcher + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.config = Config() + self.switcher = AccountSwitcher() + self.version_manager = VersionManager() + + # 添加激活状态缓存 + self._activation_status = None # 缓存的激活状态 + self._status_timer = None # 状态更新定时器 + + # 添加心跳定时器 + self._heartbeat_timer = QTimer() + self._heartbeat_timer.timeout.connect(self.send_heartbeat) + self._heartbeat_timer.start(5 * 60 * 1000) # 每5分钟发送一次心跳 + + # 添加请求锁,防止重复提交 + self._is_requesting = False + self._last_request_time = 0 + self._request_cooldown = 2 # 请求冷却时间(秒) + + version = get_version() + cursor_version = self.switcher.get_cursor_version() + self.setWindowTitle(f"听泉Cursor助手 v{version} (本机Cursor版本: {cursor_version})") + self.setMinimumSize(600, 680) # 增加最小宽度到600 + + # 设置窗口图标 + icon_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icon", "two.ico") + if os.path.exists(icon_path): + self.window_icon = QIcon(icon_path) + if not self.window_icon.isNull(): + self.setWindowIcon(self.window_icon) + logging.info(f"成功设置窗口图标: {icon_path}") + else: + logging.warning("图标文件加载失败") + + # 创建系统托盘图标 + self.create_tray_icon() + + # 创建主窗口部件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 设置主窗口样式 + central_widget.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 #f8f9fa, + stop:0.5 #ffffff, + stop:1 #f8f9fa); + } + QFrame { + border: none; + background: transparent; + } + """) + + # 创建主布局 + main_layout = QVBoxLayout(central_widget) + main_layout.setSpacing(12) # 减小区域间距 + main_layout.setContentsMargins(20, 15, 20, 15) # 调整外边距 + + # 启动时检查一次状态 + QTimer.singleShot(0, self.check_status) + + # 启动时自动检查更新 + QTimer.singleShot(1000, self.check_for_updates) + + def send_heartbeat(self): + """发送心跳请求""" + if not self._check_request_throttle(): + return + + def heartbeat_func(): + return self.switcher.send_heartbeat() + + # 创建工作线程 + self.heartbeat_worker = ApiWorker(heartbeat_func) + self.heartbeat_worker.finished.connect(self.on_heartbeat_complete) + self.heartbeat_worker.start() + + def on_heartbeat_complete(self, result): + """心跳完成回调""" + success, message = result + self._request_complete() + + if success: + logging.info(f"心跳发送成功: {message}") + # 更新状态显示 + self.check_status() + else: + logging.error(f"心跳发送失败: {message}") + + def closeEvent(self, event): + """窗口关闭事件""" + try: + if hasattr(self, 'tray_icon') and self.tray_icon.isVisible(): + event.ignore() + self.hide() + # 确保托盘图标显示 + self.tray_icon.show() + self.tray_icon.showMessage( + "听泉Cursor助手", + "程序已最小化到系统托盘", + QSystemTrayIcon.Information, + 2000 + ) + else: + # 如果托盘图标不可用,则正常退出 + if self._status_timer: + self._status_timer.stop() + if hasattr(self, '_heartbeat_timer'): + self._heartbeat_timer.stop() + event.accept() + + except Exception as e: + logging.error(f"处理关闭事件时发生错误: {str(e)}") + # 发生错误时,接受关闭事件 + if self._status_timer: + self._status_timer.stop() + if hasattr(self, '_heartbeat_timer'): + self._heartbeat_timer.stop() + event.accept() \ No newline at end of file diff --git a/main.py b/main.py index ec90c86..168aaed 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,9 @@ import shutil import tempfile import urllib3 import ctypes +import win32event +import win32api +import winerror from pathlib import Path from PyQt5.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon, QMenu from PyQt5.QtGui import QIcon @@ -18,6 +21,28 @@ from account_switcher import AccountSwitcher urllib3.disable_warnings() logging.getLogger('urllib3').setLevel(logging.ERROR) +def prevent_multiple_instances(): + """防止程序多开 + + Returns: + bool: 如果是第一个实例返回True,否则返回False + """ + try: + # 创建一个唯一的互斥锁名称 + mutex_name = "Global\\CursorHelper_SingleInstance_Lock" + + # 尝试创建互斥锁 + handle = win32event.CreateMutex(None, 1, mutex_name) + if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: + # 互斥锁已存在,说明程序已经在运行 + logging.warning("程序已经在运行") + QMessageBox.warning(None, "警告", "程序已经在运行!\n请不要重复打开程序。") + return False + return True + except Exception as e: + logging.error(f"检查程序实例失败: {str(e)}") + return True # 如果检查失败,允许程序运行 + def cleanup_temp(): """清理临时文件""" try: @@ -93,6 +118,10 @@ def print_banner(): def main(): """主函数""" try: + # 0. 检查是否已有实例在运行 + if not prevent_multiple_instances(): + return 1 + # 1. 首先检查管理员权限 if not is_admin(): if run_as_admin(): diff --git a/testbuild.bat b/testbuild.bat index ce3595a..d19f49f 100644 --- a/testbuild.bat +++ b/testbuild.bat @@ -1,84 +1,123 @@ @echo off -chcp 65001 >nul +chcp 65001 setlocal EnableDelayedExpansion +echo 开始测试打包... -REM 激活虚拟环境 -call venv\Scripts\activate.bat +:: 设置工作目录为脚本所在目录 +cd /d "%~dp0" -REM 确保安装了必要的包 +:: 激活虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo 激活虚拟环境... + call venv\Scripts\activate.bat +) else ( + echo 警告: 未找到虚拟环境,使用系统 Python +) + +:: 确保安装了必要的包 +echo 检查依赖包... pip install -r requirements.txt -REM 读取当前版本号 +:: 读取版本号 set /p VERSION= 3.4) -for /f "tokens=1,2 delims=." %%a in ("%VERSION%") do ( +:: 提取主版本号和次版本号 (3.4.7 -> 3.4) +for /f "tokens=1,2 delims=." %%a in ("!VERSION!") do ( set MAJOR_VERSION=%%a.%%b ) -echo 主版本目录: %MAJOR_VERSION% +echo 主版本目录: !MAJOR_VERSION! -REM 读取测试版本号(如果存在) +:: 读取测试版本号(如果存在) if exist testversion.txt ( set /p TEST_VERSION=testversion.txt echo 测试版本号: !TEST_VERSION! -REM 组合完整版本号 -set FULL_VERSION=%VERSION%.!TEST_VERSION! +:: 组合完整版本号 +set FULL_VERSION=!VERSION!.!TEST_VERSION! echo 完整版本号: !FULL_VERSION! -REM 创建测试版本输出目录 -set TEST_DIR=dist\test\%MAJOR_VERSION% +:: 创建测试版本目录 +set TEST_DIR=dist\test\!MAJOR_VERSION! if not exist "!TEST_DIR!" ( mkdir "!TEST_DIR!" echo 创建目录: !TEST_DIR! ) -REM 清理旧文件 -if exist "dist\听泉cursor助手%VERSION%.exe" del "dist\听泉cursor助手%VERSION%.exe" -if exist "build" rmdir /s /q "build" +:: 清理 Python 缓存文件 +echo 清理Python缓存文件... +for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d" +del /s /q *.pyc >nul 2>&1 +del /s /q *.pyo >nul 2>&1 -REM 执行打包 -venv\Scripts\python.exe -m PyInstaller build_nezha.spec --clean +:: 清理旧的打包文件 +echo 清理旧文件... +if exist "build" rd /s /q "build" +if exist "*.spec" del /f /q "*.spec" -REM 检查源文件是否存在 -echo 检查文件: dist\听泉cursor助手%VERSION%.exe -if not exist "dist\听泉cursor助手%VERSION%.exe" ( - echo 错误: 打包后的文件不存在 - echo 预期文件路径: dist\听泉cursor助手%VERSION%.exe - dir /b dist - pause - exit /b 1 +:: 使用优化选项进行打包 +echo 开始打包... +pyinstaller ^ + --noconfirm ^ + --clean ^ + --onefile ^ + --noconsole ^ + --icon=icon/two.ico ^ + --name "听泉cursor助手_test" ^ + --add-data "icon;icon" ^ + --add-data "version.txt;." ^ + --add-data "testversion.txt;." ^ + --add-data "requirements.txt;." ^ + --exclude-module _tkinter ^ + --exclude-module tkinter ^ + --exclude-module PIL.ImageTk ^ + --exclude-module PIL.ImageWin ^ + --exclude-module numpy ^ + --exclude-module pandas ^ + --exclude-module matplotlib ^ + --exclude "__pycache__" ^ + --exclude "*.pyc" ^ + --exclude "*.pyo" ^ + --exclude "*.pyd" ^ + main.py + +:: 检查打包结果并移动文件 +set TEMP_FILE=dist\听泉cursor助手_test.exe +set TARGET_FILE=!TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe + +echo 检查文件: !TEMP_FILE! +if exist "!TEMP_FILE!" ( + echo 测试打包成功! + + :: 移动到版本目录 + echo 移动文件到: !TARGET_FILE! + move "!TEMP_FILE!" "!TARGET_FILE!" + + :: 显示文件大小 + for %%I in ("!TARGET_FILE!") do ( + echo 文件大小: %%~zI 字节 + ) + + echo. + echo 测试版本构建完成! + echo 版本号: v!FULL_VERSION! + echo 文件位置: !TARGET_FILE! +) else ( + echo 错误: 打包失败,文件不存在 ) -REM 移动并重命名文件 -echo 移动文件: -echo 源文件: dist\听泉cursor助手%VERSION%.exe -echo 目标文件: !TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe -move "dist\听泉cursor助手%VERSION%.exe" "!TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe" -if errorlevel 1 ( - echo 移动文件失败,请检查: - echo 1. 源文件是否存在: dist\听泉cursor助手%VERSION%.exe - echo 2. 目标目录是否可写: !TEST_DIR! - echo 3. 目标文件是否已存在: !TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe - dir /b dist - dir /b "!TEST_DIR!" - pause - exit /b 1 +:: 退出虚拟环境 +if exist "venv\Scripts\activate.bat" ( + echo 退出虚拟环境... + deactivate ) -echo. -echo 测试版本构建完成! -echo 版本号: v!FULL_VERSION! -echo 文件位置: !TEST_DIR!\听泉cursor助手v!FULL_VERSION!.exe - -REM 退出虚拟环境 -deactivate -pause \ No newline at end of file +endlocal +pause \ No newline at end of file diff --git a/utils/version_manager.py b/utils/version_manager.py index c97fc41..bb7c545 100644 --- a/utils/version_manager.py +++ b/utils/version_manager.py @@ -29,7 +29,15 @@ class VersionManager: def _get_current_version(self) -> str: """获取当前版本号""" try: - version_file = self.root_path / "version.txt" + # 首先尝试从打包后的路径读取 + if getattr(sys, 'frozen', False): + # 如果是打包后的程序 + base_path = Path(sys._MEIPASS) + else: + # 如果是开发环境 + base_path = self.root_path + + version_file = base_path / "version.txt" if not version_file.exists(): logging.error(f"版本文件不存在: {version_file}") return "0.0.0" diff --git a/version.txt b/version.txt index 81f1b89..e5b8203 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.4.7 \ No newline at end of file +3.5.0 \ No newline at end of file