diff --git a/CleanDesktopOrganizer.spec b/CleanDesktopOrganizer.spec index c5f88d0..62aab32 100644 --- a/CleanDesktopOrganizer.spec +++ b/CleanDesktopOrganizer.spec @@ -34,7 +34,7 @@ exe = EXE( debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + upx=False, upx_exclude=[], runtime_tmpdir=None, console=False, @@ -43,5 +43,5 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon=['logo.png'], + icon=['logo.ico'], ) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..1e6bbe8 --- /dev/null +++ b/Readme.md @@ -0,0 +1,5 @@ +# 编译命令 +python -m PyInstaller CleanDesktopOrganizer.spec + +# 清除缓存重新打包命令 +python -m PyInstaller CleanDesktopOrganizer.spec --clean diff --git a/main.py b/main.py index 2865d52..d8145ce 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from ui.ball import FloatBall, BALL_SIZE import ui.theme as theme from db import database -__VERSION__ = "0.0.2" +__VERSION__ = "0.0.3" # ===================== 打包兼容核心函数 ===================== def get_resource_path(relative_path): diff --git a/ui/__pycache__/dock.cpython-313.pyc b/ui/__pycache__/dock.cpython-313.pyc index 833556d..372abeb 100644 Binary files a/ui/__pycache__/dock.cpython-313.pyc and b/ui/__pycache__/dock.cpython-313.pyc differ diff --git a/ui/dock.py b/ui/dock.py index af3126d..e866c57 100644 --- a/ui/dock.py +++ b/ui/dock.py @@ -507,6 +507,7 @@ class PanelWindow(QWidget): 每项: (tooltip, qtawesome_icon, callback) 排除重启/退出。""" return [ + ("微信多开", "fa5b.weixin", self._open_wechat_multi), ("管理员运行 CMD", "fa5s.terminal", self._open_admin_cmd), ("管理员运行 PowerShell", "fa5b.windows", self._open_admin_powershell), ("打开默认浏览器", "fa5s.globe", self._open_default_browser), @@ -913,6 +914,11 @@ class PanelWindow(QWidget): import webbrowser webbrowser.open("https://www.baidu.com") + def _open_wechat_multi(self): + from ui.wechat_multi import WechatMultiDialog + dlg = WechatMultiDialog(self) + dlg.exec() + def _toggle_theme(self): theme.set_theme("light" if theme.name() == "dark" else "dark") self._apply_theme() diff --git a/ui/updater.py b/ui/updater.py index 4c4777a..cfda538 100644 --- a/ui/updater.py +++ b/ui/updater.py @@ -16,7 +16,6 @@ UPDATE_CHECK_URL = "https://api.yunzer.cn/api/softwareupgrade/check?code=niumaso def _current_version() -> str: - """从 main 模块取当前版本号,避免循环导入。""" try: import importlib m = importlib.import_module("__main__") @@ -35,13 +34,12 @@ def _is_newer(latest: str, current: str) -> bool: class _CheckWorker(QThread): - result = pyqtSignal(dict) # 成功:返回 data 字段 + result = pyqtSignal(dict) error = pyqtSignal(str) def run(self): try: import json - import urllib.request with urllib.request.urlopen(UPDATE_CHECK_URL, timeout=10) as resp: body = json.loads(resp.read().decode()) if body.get("code") == 200: @@ -53,59 +51,90 @@ class _CheckWorker(QThread): class _DownloadWorker(QThread): - progress = pyqtSignal(int) # 0-100 - finished = pyqtSignal(str) # 下载完成,返回临时文件路径 + progress = pyqtSignal(int) + finished = pyqtSignal(str) error = pyqtSignal(str) - def __init__(self, url: str): + def __init__(self, url: str, dest: str): super().__init__() - self._url = url + self._url = url + self._dest = dest def run(self): try: - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".exe") - tmp.close() - dst = tmp.name - def _reporthook(count, block_size, total_size): if total_size > 0: pct = min(100, int(count * block_size * 100 / total_size)) self.progress.emit(pct) - urllib.request.urlretrieve(self._url, dst, _reporthook) + urllib.request.urlretrieve(self._url, self._dest, _reporthook) self.progress.emit(100) - self.finished.emit(dst) + self.finished.emit(self._dest) except Exception as e: self.error.emit(str(e)) def _replace_and_restart(new_exe: str): """ - 写一个批处理脚本:等待当前进程退出 → 覆盖 exe → 启动新版。 - 仅在打包为 exe 时执行真正替换;开发环境直接启动下载文件。 + onedir 模式:只替换 exe 本身,dll 等文件不变。 + 用 PowerShell 后台脚本等待原进程退出后替换并重启,完全无窗口。 """ - current_exe = sys.executable if getattr(sys, "frozen", False) else None - - if current_exe: - bat = tempfile.NamedTemporaryFile(delete=False, suffix=".bat", mode="w", encoding="gbk") - bat.write(f"""@echo off -ping 127.0.0.1 -n 3 >nul -move /y "{new_exe}" "{current_exe}" -start "" "{current_exe}" -del "%~f0" -""") - bat.close() - subprocess.Popen(["cmd", "/c", bat.name], creationflags=subprocess.CREATE_NO_WINDOW) - else: - # 开发环境:直接运行下载的 exe + if not getattr(sys, "frozen", False): subprocess.Popen([new_exe]) + QApplication.quit() + return + current_exe = sys.executable + pid = os.getpid() + + ps_script = f""" +$pid_target = {pid} +$src = '{new_exe.replace(chr(92), chr(92)*2)}' +$dst = '{current_exe.replace(chr(92), chr(92)*2)}' + +# 等待原进程退出,最多 30 秒 +$waited = 0 +while ((Get-Process -Id $pid_target -ErrorAction SilentlyContinue) -and $waited -lt 30) {{ + Start-Sleep -Milliseconds 500 + $waited += 0.5 +}} + +# 重试覆盖,最多 10 次 +$ok = $false +for ($i = 0; $i -lt 10; $i++) {{ + try {{ + Copy-Item -Path $src -Destination $dst -Force + $ok = $true + break + }} catch {{ + Start-Sleep -Milliseconds 800 + }} +}} + +if ($ok) {{ + Remove-Item $src -Force -ErrorAction SilentlyContinue + Start-Process $dst +}} +""" + + tmp = tempfile.NamedTemporaryFile( + delete=False, suffix=".ps1", mode="w", encoding="utf-8" + ) + tmp.write(ps_script) + tmp.close() + + subprocess.Popen( + [ + "powershell", "-WindowStyle", "Hidden", + "-NonInteractive", "-ExecutionPolicy", "Bypass", + "-File", tmp.name, + ], + creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW, + ) QApplication.quit() class Updater(QObject): - """对外接口:调用 check() 即可。""" - def __init__(self, parent=None): super().__init__(parent) self._parent_widget = parent @@ -141,6 +170,7 @@ class Updater(QObject): if ret != QMessageBox.StandardButton.Yes: return + self._url = url self._download(url) def _on_check_error(self, err: str): @@ -148,12 +178,19 @@ class Updater(QObject): QMessageBox.warning(self._parent_widget, "检查更新失败", f"无法连接更新服务器:\n{err}") def _download(self, url: str): + # 下载到原 exe 同目录,避免跨盘 move 失败 + if getattr(sys, "frozen", False): + dest_dir = os.path.dirname(sys.executable) + else: + dest_dir = tempfile.gettempdir() + dest = os.path.join(dest_dir, "_update_new.exe") + self._progress = QProgressDialog("正在下载更新...", "取消", 0, 100, self._parent_widget) self._progress.setWindowTitle("下载更新") self._progress.setMinimumDuration(0) self._progress.setValue(0) - self._dl_worker = _DownloadWorker(url) + self._dl_worker = _DownloadWorker(url, dest) self._dl_worker.progress.connect(self._progress.setValue) self._dl_worker.finished.connect(self._on_download_done) self._dl_worker.error.connect(self._on_download_error) diff --git a/ui/wechat_multi.py b/ui/wechat_multi.py new file mode 100644 index 0000000..8c462ce --- /dev/null +++ b/ui/wechat_multi.py @@ -0,0 +1,234 @@ +""" +微信多开对话框 +""" +import os +import subprocess +import tempfile + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QSpinBox, QFileDialog, QTextEdit, QWidget +) +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QIcon +import ui.theme as theme +from db import database + +_DB_KEY_PATH = "wechat_multi_path" +_DB_KEY_COUNT = "wechat_multi_count" + +_DEFAULT_PATHS = [ + r"D:\Softwares\Tencent\Weixin\Weixin.exe", + r"C:\Program Files\Tencent\WeChat\WeChat.exe", + r"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe", +] + +_USAGE = """使用说明: + +1. 填写微信 Weixin.exe 的完整路径。 + 点击右侧「浏览」按钮可以直接选择文件。 + +2. 设置多开数量(建议不超过 5 个, + 数量过多可能导致电脑卡顿)。 + +3. 点击「开始多开」,程序会依次启动 + 对应数量的微信进程。 + +4. 每个微信实例需要单独登录账号。 + +注意:微信官方不支持多开,使用本功能 +请自行承担相关风险。 +""" + + +def _detect_wechat() -> str: + """尝试自动检测微信路径。""" + saved = database.get_setting(_DB_KEY_PATH, "") + if saved and os.path.isfile(saved): + return saved + for p in _DEFAULT_PATHS: + if os.path.isfile(p): + return p + return "" + + +class WechatMultiDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("微信多开") + self.setMinimumWidth(460) + self.setWindowFlags( + Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self._drag_pos = None + self._build() + self._apply_theme() + + def _build(self): + saved_path = _detect_wechat() + saved_count = int(database.get_setting(_DB_KEY_COUNT, "2")) + + root = QVBoxLayout(self) + root.setContentsMargins(0, 0, 0, 0) + + self._card = QWidget() + self._card.setObjectName("wechat_card") + card_layout = QVBoxLayout(self._card) + card_layout.setContentsMargins(20, 16, 20, 16) + card_layout.setSpacing(12) + + # 标题栏 + title_row = QHBoxLayout() + lbl_title = QLabel("微信多开") + lbl_title.setStyleSheet("font-size:14px; font-weight:bold;") + close_btn = QPushButton("✕") + close_btn.setFixedSize(24, 24) + close_btn.setStyleSheet("border:none; background:transparent; font-size:13px;") + close_btn.clicked.connect(self.reject) + title_row.addWidget(lbl_title) + title_row.addStretch() + title_row.addWidget(close_btn) + card_layout.addLayout(title_row) + + # 路径 + card_layout.addWidget(QLabel("微信程序路径(Weixin.exe):")) + path_row = QHBoxLayout() + self._path_edit = QLineEdit(saved_path) + self._path_edit.setPlaceholderText("请输入或浏览 Weixin.exe 路径") + browse_btn = QPushButton("浏览") + browse_btn.setFixedWidth(56) + browse_btn.clicked.connect(self._browse) + path_row.addWidget(self._path_edit) + path_row.addWidget(browse_btn) + card_layout.addLayout(path_row) + + # 数量 + count_row = QHBoxLayout() + count_row.addWidget(QLabel("多开数量:")) + self._spin = QSpinBox() + self._spin.setRange(2, 9) + self._spin.setValue(saved_count) + self._spin.setFixedWidth(70) + count_row.addWidget(self._spin) + count_row.addStretch() + card_layout.addLayout(count_row) + + # 使用说明 + usage = QTextEdit() + usage.setReadOnly(True) + usage.setPlainText(_USAGE) + usage.setFixedHeight(150) + usage.setStyleSheet("font-size:11px; border-radius:6px;") + card_layout.addWidget(usage) + + # 按钮行 + btn_row = QHBoxLayout() + btn_row.addStretch() + self._start_btn = QPushButton("开始多开") + self._start_btn.setFixedHeight(32) + self._start_btn.setMinimumWidth(90) + self._start_btn.clicked.connect(self._start) + cancel_btn = QPushButton("取消") + cancel_btn.setFixedHeight(32) + cancel_btn.setMinimumWidth(70) + cancel_btn.clicked.connect(self.reject) + btn_row.addWidget(self._start_btn) + btn_row.addWidget(cancel_btn) + card_layout.addLayout(btn_row) + + root.addWidget(self._card) + + def _apply_theme(self): + t = theme.current() + is_dark = theme.name() == "dark" + self._card.setStyleSheet(f""" + QWidget#wechat_card {{ + background: {t['panel_bg']}; + border-radius: 10px; + border: 1px solid {t['panel_border']}; + }} + QLabel {{ color: {t['search_color']}; background: transparent; }} + QLineEdit {{ + background: {t['search_bg']}; + border: 1px solid {t['search_border']}; + border-radius: 5px; + color: {t['search_color']}; + padding: 4px 8px; + }} + QSpinBox {{ + background: {t['search_bg']}; + border: 1px solid {t['search_border']}; + border-radius: 5px; + color: {t['search_color']}; + padding: 2px 4px; + }} + QTextEdit {{ + background: {t['search_bg']}; + color: {t['search_color']}; + border: 1px solid {t['search_border']}; + }} + QPushButton {{ + border: 1px solid {t['search_border']}; + border-radius: 5px; + color: {t['btn_color']}; + background: {t['search_bg']}; + font-size: 12px; + padding: 0 10px; + }} + QPushButton:hover {{ background: {t['header_hover']}; }} + """) + + def _browse(self): + path, _ = QFileDialog.getOpenFileName( + self, "选择 Weixin.exe", "", "可执行文件 (*.exe)" + ) + if path: + self._path_edit.setText(path) + + def _start(self): + exe = self._path_edit.text().strip() + count = self._spin.value() + + if not exe: + self._path_edit.setPlaceholderText("⚠ 请先填写路径") + return + if not os.path.isfile(exe): + self._path_edit.setStyleSheet( + self._path_edit.styleSheet() + "border-color: red;" + ) + return + + # 保存设置 + database.set_setting(_DB_KEY_PATH, exe) + database.set_setting(_DB_KEY_COUNT, str(count)) + + # 写临时 bat,连续 start 多次 + lines = ["@echo off"] + for _ in range(count): + lines.append(f'start "" "{exe}"') + bat_content = "\r\n".join(lines) + "\r\n" + + tmp = tempfile.NamedTemporaryFile( + delete=False, suffix=".bat", mode="w", encoding="gbk" + ) + tmp.write(bat_content) + tmp.close() + + subprocess.Popen( + ["cmd", "/c", tmp.name], + creationflags=subprocess.CREATE_NO_WINDOW + ) + self.accept() + + # 无边框拖动 + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft() + + def mouseMoveEvent(self, event): + if self._drag_pos and event.buttons() & Qt.MouseButton.LeftButton: + self.move(event.globalPosition().toPoint() - self._drag_pos) + + def mouseReleaseEvent(self, event): + self._drag_pos = None