From 7d22604144ee5897a2297e4a6dd94807393f8251 Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Mon, 2 Feb 2026 16:55:16 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E7=BA=BF?= =?UTF-8?q?=E7=A8=8B=E4=B8=8B=E8=BD=BD=E5=92=8C=E6=9A=82=E5=81=9C=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=8F=96=E6=B6=88=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.py | 1 + src/main_window.py | 478 +++++++++++++++++++++++++++++++---------- src/threading_utils.py | 21 ++ 3 files changed, 381 insertions(+), 119 deletions(-) diff --git a/src/api.py b/src/api.py index 5e96f82..aa186f1 100644 --- a/src/api.py +++ b/src/api.py @@ -514,6 +514,7 @@ def up_load(self, file_path): raise RuntimeError("同名文件存在") if res_code_up != 0: raise RuntimeError(f"上传请求失败: {up_res_json}") + up_file_id = up_res_json["data"]["FileId"] if up_res_json["data"].get("Reuse", False): return up_file_id diff --git a/src/main_window.py b/src/main_window.py index fc58b01..3bfdbec 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -8,6 +8,8 @@ import requests import sys import time +import concurrent.futures +import threading from log import get_logger from config import ConfigManager from ui_widgets import SidebarButton, LoginDialog, SettingsDialog, AboutDialog @@ -145,8 +147,8 @@ def __init__(self): sidebar_title = QtWidgets.QLabel("功能菜单") sidebar_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) sidebar_title.setStyleSheet( - "font-size: 20px; font-weight: bold; color: #334155; margin-bottom: 20px;" - "padding: 10px 0;" + "font-size: 18px; font-weight: bold; color: #1e293b; margin-bottom: 16px;" + "padding: 8px 0;" ) sidebar_layout.addWidget(sidebar_title) @@ -155,28 +157,38 @@ def __init__(self): self.sidebar_animations = {} self.sidebar_original_geoms = {} - # 文件页按钮 - self.btn_files = SidebarButton("📁 文件") - self.btn_files.setMinimumHeight(50) + # 文件页按钮 - 改为方形设计,图标居中,标签在下 + self.btn_files = SidebarButton("📁\n文件") + self.btn_files.setMinimumHeight(100) + self.btn_files.setMinimumWidth(140) + self.btn_files.setMaximumHeight(100) + self.btn_files.setMaximumWidth(140) self.btn_files.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" + "font-size: 13px; text-align: center; font-weight: 500;" "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" + "color: white; border-radius: 16px;" "border: none;" + "padding: 8px;" + "line-height: 1.4;" ) - sidebar_layout.addWidget(self.btn_files) + sidebar_layout.addWidget(self.btn_files, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) self.sidebar_buttons.append(self.btn_files) - # 传输页按钮 - self.btn_transfer = SidebarButton("🔄 传输") - self.btn_transfer.setMinimumHeight(50) + # 传输页按钮 - 改为方形设计,图标居中,标签在下 + self.btn_transfer = SidebarButton("🔄\n传输") + self.btn_transfer.setMinimumHeight(100) + self.btn_transfer.setMinimumWidth(140) + self.btn_transfer.setMaximumHeight(100) + self.btn_transfer.setMaximumWidth(140) self.btn_transfer.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(229, 231, 235, 0.8); color: #334155;" + "border-radius: 16px;" + "border: 1px solid rgba(0, 0, 0, 0.08);" + "padding: 8px;" + "line-height: 1.4;" ) - sidebar_layout.addWidget(self.btn_transfer) + sidebar_layout.addWidget(self.btn_transfer, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) self.sidebar_buttons.append(self.btn_transfer) # 为侧边栏按钮添加悬停和点击事件,实现动画效果 @@ -1175,17 +1187,21 @@ def switch_page(self, page_index): for i, btn in enumerate(self.sidebar_buttons): if i == page_index: btn.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" + "font-size: 13px; text-align: center; font-weight: 500;" "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" + "color: white; border-radius: 16px;" "border: none;" + "padding: 8px;" + "line-height: 1.4;" ) else: btn.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(229, 231, 235, 0.8); color: #334155;" + "border-radius: 16px;" + "border: 1px solid rgba(0, 0, 0, 0.08);" + "padding: 8px;" + "line-height: 1.4;" ) # 根据页面显示/隐藏路径栏和相关按钮 @@ -1213,99 +1229,128 @@ def switch_page(self, page_index): self.btn_mkdir.setVisible(False) def on_sidebar_button_hover(self, button): - """侧边栏按钮悬停效果""" - # 停止当前正在运行的动画 - if button in self.sidebar_animations: - self.sidebar_animations[button].stop() - - # 获取原始位置 - if button not in self.sidebar_original_geoms: - self.save_original_position(button) - original_geom = self.sidebar_original_geoms[button] - - # 创建缩放动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - scale_animation.setStartValue(button.geometry()) - scale_animation.setEndValue(QtCore.QRect( - original_geom.x() - 5, - original_geom.y() - 2, - original_geom.width() + 10, - original_geom.height() + 4 - )) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.sidebar_animations[button] = scale_animation + """侧边栏按钮悬停效果 - 改变背景色并增加阴影""" + if button == self.btn_files: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(37, 99, 235, 1);" + "color: white; border-radius: 16px;" + "border: none;" + "padding: 8px;" + "line-height: 1.4;" + "box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);" + ) + elif button == self.btn_transfer: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(59, 130, 246, 0.15); color: #1e40af;" + "border-radius: 16px;" + "border: 1px solid rgba(37, 99, 235, 0.3);" + "padding: 8px;" + "line-height: 1.4;" + ) def on_sidebar_button_leave(self, button): - """侧边栏按钮离开效果""" - # 停止当前正在运行的动画 - if button in self.sidebar_animations: - self.sidebar_animations[button].stop() - - # 获取原始位置 - if button not in self.sidebar_original_geoms: - self.save_original_position(button) - original_geom = self.sidebar_original_geoms[button] - - # 创建恢复动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - scale_animation.setStartValue(button.geometry()) - scale_animation.setEndValue(original_geom) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.sidebar_animations[button] = scale_animation + """侧边栏按钮离开效果 - 恢复原始样式""" + if button == self.btn_files: + if self.page_stack.currentIndex() == 0: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(59, 130, 246, 0.9);" + "color: white; border-radius: 16px;" + "border: none;" + "padding: 8px;" + "line-height: 1.4;" + ) + else: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(229, 231, 235, 0.8); color: #334155;" + "border-radius: 16px;" + "border: 1px solid rgba(0, 0, 0, 0.08);" + "padding: 8px;" + "line-height: 1.4;" + ) + elif button == self.btn_transfer: + if self.page_stack.currentIndex() == 1: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(59, 130, 246, 0.9);" + "color: white; border-radius: 16px;" + "border: none;" + "padding: 8px;" + "line-height: 1.4;" + ) + else: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(229, 231, 235, 0.8); color: #334155;" + "border-radius: 16px;" + "border: 1px solid rgba(0, 0, 0, 0.08);" + "padding: 8px;" + "line-height: 1.4;" + ) def on_sidebar_button_pressed(self, button): - """侧边栏按钮按下效果""" - # 改变背景色 - button.setStyleSheet( - button.styleSheet().replace( - "background-color: rgba(59, 130, 246, 0.9);", - "background-color: rgba(37, 99, 235, 0.9);" - ).replace( - "background-color: transparent;", - "background-color: rgba(59, 130, 246, 0.1);" + """侧边栏按钮按下效果 - 改变背景色与加深""" + if button == self.btn_files: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(29, 78, 216, 1);" + "color: white; border-radius: 16px;" + "border: none;" + "padding: 8px;" + "line-height: 1.4;" + ) + elif button == self.btn_transfer: + button.setStyleSheet( + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(37, 99, 235, 0.2); color: #1e40af;" + "border-radius: 16px;" + "border: 1px solid rgba(37, 99, 235, 0.4);" + "padding: 8px;" + "line-height: 1.4;" ) - ) def on_sidebar_button_released(self, button): - """侧边栏按钮释放效果""" - # 恢复背景色 + """侧边栏按钮释放效果 - 根据页面状态恢复样式""" if button == self.btn_files: if self.page_stack.currentIndex() == 0: button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" + "font-size: 13px; text-align: center; font-weight: 500;" "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" + "color: white; border-radius: 16px;" "border: none;" + "padding: 8px;" + "line-height: 1.4;" ) else: button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(229, 231, 235, 0.8); color: #334155;" + "border-radius: 16px;" + "border: 1px solid rgba(0, 0, 0, 0.08);" + "padding: 8px;" + "line-height: 1.4;" ) elif button == self.btn_transfer: if self.page_stack.currentIndex() == 1: button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" + "font-size: 13px; text-align: center; font-weight: 500;" "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" + "color: white; border-radius: 16px;" "border: none;" + "padding: 8px;" + "line-height: 1.4;" ) else: button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" + "font-size: 13px; text-align: center; font-weight: 500;" + "background-color: rgba(229, 231, 235, 0.8); color: #334155;" + "border-radius: 16px;" + "border: 1px solid rgba(0, 0, 0, 0.08);" + "padding: 8px;" + "line-height: 1.4;" ) def add_transfer_task(self, task_type, file_name, file_size): @@ -1350,7 +1395,31 @@ def add_transfer_task(self, task_type, file_name, file_size): "font-size: 12px;" ) cancel_btn.clicked.connect(lambda _, tid=task_id: self.cancel_transfer_task(tid)) - self.transfer_table.setCellWidget(row, 5, cancel_btn) + + # 添加暂停/继续按钮 + pause_btn = QtWidgets.QPushButton("暂停") + pause_btn.setStyleSheet( + "background-color: rgba(59, 130, 246, 0.08);" + "color: #2563EB;" + "border: 1px solid rgba(37, 99, 235, 0.2);" + "border-radius: 8px;" + "padding: 4px 12px;" + "font-size: 12px;" + ) + pause_btn.clicked.connect(lambda _, tid=task_id: self.pause_transfer_task(tid)) + + # 将两个按钮放在一个容器里 + btn_container = QtWidgets.QWidget() + btn_layout = QtWidgets.QHBoxLayout(btn_container) + btn_layout.setContentsMargins(0, 0, 0, 0) + btn_layout.setSpacing(6) + btn_layout.addWidget(pause_btn) + btn_layout.addWidget(cancel_btn) + self.transfer_table.setCellWidget(row, 5, btn_container) + + # 保存按钮引用,便于后续隐藏或修改文字 + task['cancel_button'] = cancel_btn + task['pause_button'] = pause_btn return task_id @@ -1394,16 +1463,57 @@ def cancel_transfer_task(self, task_id): self.transfer_table.setItem(i, 3, QtWidgets.QTableWidgetItem("0%")) self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem("已取消")) - # 移除取消按钮 + # 隐藏按钮容器 widget = self.transfer_table.cellWidget(i, 5) if widget: widget.setVisible(False) + # 也隐藏单独的按钮引用(若存在) + if task.get('pause_button'): + try: + task['pause_button'].setVisible(False) + except Exception: + pass + if task.get('cancel_button'): + try: + task['cancel_button'].setVisible(False) + except Exception: + pass # 从活动任务列表中移除 if task_id in self.active_tasks: del self.active_tasks[task_id] break + + def pause_transfer_task(self, task_id): + """切换暂停/继续传输任务""" + for i, task in enumerate(self.transfer_tasks): + if task["id"] == task_id: + threaded = task.get("threaded_task") + pause_btn = task.get('pause_button') + if not threaded: + return + # 切换状态 + if getattr(threaded, 'is_paused', False): + try: + threaded.resume() + except Exception: + pass + task["status"] = "下载中" + if pause_btn: + pause_btn.setText("暂停") + else: + try: + threaded.pause() + except Exception: + pass + task["status"] = "已暂停" + if pause_btn: + pause_btn.setText("继续") + + # 更新表格显示 + self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem(task["status"])) + break def remove_transfer_task(self, task_id): """移除传输任务""" @@ -1514,42 +1624,172 @@ def _task_get_download_and_stream(self, file_index, download_dir, task_id, signa fname = file_detail["FileName"] out_path = os.path.join(download_dir, fname) temp = out_path + ".123pan" - + # 保存文件路径到任务对象 for i, t in enumerate(self.transfer_tasks): if t["id"] == task_id: self.transfer_tasks[i]["file_path"] = temp break - + if os.path.exists(out_path): reply = QtWidgets.QMessageBox.question(None, "文件已存在", f"{fname} 已存在,是否覆盖?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) if reply == QtWidgets.QMessageBox.StandardButton.No: return "已取消" - with requests.get(redirect_url, stream=True, timeout=30) as r: - r.raise_for_status() - total = int(r.headers.get("Content-Length", 0) or 0) - done = 0 - with open(temp, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - # 检查是否被取消 - if task and task.is_cancelled: - f.close() - # 删除临时文件 - if os.path.exists(temp): - os.remove(temp) - return "已取消" - if chunk: - f.write(chunk) - done += len(chunk) - if total and signals: - signals.progress.emit(int(done * 100 / total)) - if task and task.is_cancelled: - # 删除临时文件 - if os.path.exists(temp): + + # 尝试获取头信息,判断是否支持 Range + total = 0 + accept_ranges = False + try: + head = requests.head(redirect_url, allow_redirects=True, timeout=30) + head.raise_for_status() + total = int(head.headers.get("Content-Length", 0) or 0) + accept_ranges = head.headers.get("Accept-Ranges", "").lower() == "bytes" + except Exception: + # 有些链接不支持 HEAD,使用 GET 获取 headers + try: + with requests.get(redirect_url, stream=True, timeout=30) as r: + r.raise_for_status() + total = int(r.headers.get("Content-Length", 0) or 0) + accept_ranges = r.headers.get("Accept-Ranges", "").lower() == "bytes" + except Exception: + total = 0 + accept_ranges = False + + # 如果支持分片并且文件较大,则采用多线程分片下载 + try: + if accept_ranges and total and total > 1024 * 1024 * 2: + # 计算分片数(最多 8 片) + num_threads = min(8, max(1, int(total / (5 * 1024 * 1024)))) + part_size = total // num_threads + parts = [] + downloaded = [0] + dl_lock = threading.Lock() + + def download_range(start, end, index): + part_path = f"{temp}.part{index}" + headers = {"Range": f"bytes={start}-{end}"} + try: + with requests.get(redirect_url, headers=headers, stream=True, timeout=30) as r: + r.raise_for_status() + with open(part_path, "wb") as pf: + for chunk in r.iter_content(chunk_size=8192): + # 支持暂停/继续 + if task: + # wait 如果被暂停,会在这里阻塞 + try: + task._pause_event.wait() + except Exception: + pass + if task.is_cancelled: + return False + if chunk: + pf.write(chunk) + with dl_lock: + downloaded[0] += len(chunk) + if total and signals: + signals.progress.emit(int(downloaded[0] * 100 / total)) + return True + except Exception: + # 出错时确保部分文件被删除 + if os.path.exists(part_path): + try: + os.remove(part_path) + except Exception: + pass + return False + + # 提交分片任务 + futures = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as exe: + for i in range(num_threads): + start = i * part_size + end = (start + part_size - 1) if i < num_threads - 1 else (total - 1) + futures.append(exe.submit(download_range, start, end, i)) + + # 等待完成 + ok = True + for f in concurrent.futures.as_completed(futures): + if not f.result(): + ok = False + break + + if not ok: + # 清理部分文件 + for i in range(num_threads): + p = f"{temp}.part{i}" + if os.path.exists(p): + try: + os.remove(p) + except Exception: + pass + raise RuntimeError("分片下载失败") + if task and task.is_cancelled: + for i in range(num_threads): + p = f"{temp}.part{i}" + if os.path.exists(p): + try: + os.remove(p) + except Exception: + pass + return "已取消" + + # 合并部分文件 + with open(temp, "wb") as out_f: + for i in range(num_threads): + p = f"{temp}.part{i}" + with open(p, "rb") as pf: + while True: + chunk = pf.read(8192) + if not chunk: + break + out_f.write(chunk) + try: + os.remove(p) + except Exception: + pass + + if task and task.is_cancelled: + if os.path.exists(temp): + os.remove(temp) + return "已取消" + os.replace(temp, out_path) + return out_path + else: + # 单线程流式下载,支持暂停/取消 + with requests.get(redirect_url, stream=True, timeout=30) as r: + r.raise_for_status() + done = 0 + with open(temp, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + if task: + try: + task._pause_event.wait() + except Exception: + pass + if task.is_cancelled: + f.close() + if os.path.exists(temp): + os.remove(temp) + return "已取消" + if chunk: + f.write(chunk) + done += len(chunk) + if total and signals: + signals.progress.emit(int(done * 100 / total)) + if task and task.is_cancelled: + if os.path.exists(temp): + os.remove(temp) + return "已取消" + os.replace(temp, out_path) + return out_path + except Exception as e: + # 如果发生异常,删除临时文件并抛出 + if os.path.exists(temp): + try: os.remove(temp) - return "已取消" - os.replace(temp, out_path) - return out_path + except Exception: + pass + raise def on_showlink(self): file_index, file_detail = self.get_selected_detail() diff --git a/src/threading_utils.py b/src/threading_utils.py index 1a1675f..a1eb1d3 100644 --- a/src/threading_utils.py +++ b/src/threading_utils.py @@ -2,6 +2,7 @@ # src/threading_utils.py from PyQt6 import QtCore +import threading class WorkerSignals(QtCore.QObject): @@ -12,6 +13,8 @@ class WorkerSignals(QtCore.QObject): progress = QtCore.pyqtSignal(int) log = QtCore.pyqtSignal(str) cancel = QtCore.pyqtSignal() + paused = QtCore.pyqtSignal() + resumed = QtCore.pyqtSignal() class ThreadedTask(QtCore.QRunnable): @@ -24,6 +27,10 @@ def __init__(self, fn, *args, **kwargs): self.kwargs = kwargs self.signals = WorkerSignals() self.is_cancelled = False + self.is_paused = False + # pause event: set() means running, clear() means paused + self._pause_event = threading.Event() + self._pause_event.set() @QtCore.pyqtSlot() def run(self): @@ -45,3 +52,17 @@ def cancel(self): """取消任务""" self.is_cancelled = True self.signals.cancel.emit() + + def pause(self): + """暂停任务""" + if not self.is_paused: + self.is_paused = True + self._pause_event.clear() + self.signals.paused.emit() + + def resume(self): + """恢复任务""" + if self.is_paused: + self.is_paused = False + self._pause_event.set() + self.signals.resumed.emit() From a9095df7d99a9030793ad3e5fe61faf085be26cd Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Mon, 2 Feb 2026 18:32:04 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=B9=B6=E8=87=AA=E5=8A=A8=E5=88=87=E6=8D=A2?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main_window.py | 621 ++++++++++++++++------------------------ src/themes.py | 535 ++++++++++++++++++++++++++++++++++ src/ui_theme_manager.py | 52 ++++ 3 files changed, 841 insertions(+), 367 deletions(-) create mode 100644 src/themes.py create mode 100644 src/ui_theme_manager.py diff --git a/src/main_window.py b/src/main_window.py index 3bfdbec..c3f2885 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -15,6 +15,7 @@ from ui_widgets import SidebarButton, LoginDialog, SettingsDialog, AboutDialog from api import Pan123 from threading_utils import ThreadedTask +from ui_theme_manager import ThemeManager logger = get_logger(__name__) @@ -120,7 +121,41 @@ def __init__(self): self.threadpool.setMaxThreadCount(64) # 应用123云盘主题 - self.apply_blue_white_theme() + self.theme_manager = ThemeManager(self) + + # 注册主题改变回调 + self.theme_manager.on_theme_changed = self.on_theme_changed + + # 主题颜色映射 + self.theme_colors = { + 'light': { + 'text_primary': '#2d3749', + 'text_secondary': '#4c4f69', + 'text_disabled': '#9ca0b0', + 'bg_primary': '#eff1f5', + 'bg_secondary': '#fafbfc', + 'accent': '#1e66f5', + 'success': '#40a02b', + 'warning': '#df8e1d', + 'error': '#d20f39', + }, + 'dark': { + 'text_primary': '#cdd6f4', + 'text_secondary': '#bac2de', + 'text_disabled': '#6c7086', + 'bg_primary': '#1e1e2e', + 'bg_secondary': '#313244', + 'accent': '#89b4fa', + 'success': '#a6e3a1', + 'warning': '#f9e2af', + 'error': '#f38ba8', + } + } + + # 监听系统主题变化 + self.theme_timer = QtCore.QTimer() + self.theme_timer.timeout.connect(self.theme_manager.check_theme_change) + self.theme_timer.start(5000) # 每5秒检查一次 # 中央布局 central = QtWidgets.QWidget() @@ -133,9 +168,13 @@ def __init__(self): self.sidebar = QtWidgets.QWidget() self.sidebar.setMinimumWidth(200) self.sidebar.setMaximumWidth(200) + + # 动态设置侧边栏样式 + sidebar_bg = "#fafbfc" if not self.theme_manager.is_dark_mode else "#181825" + sidebar_border = "#acb0be" if not self.theme_manager.is_dark_mode else "#313244" self.sidebar.setStyleSheet( - "background-color: rgba(255, 255, 255, 0.95);" - "border-right: 1px solid rgba(0, 0, 0, 0.05);" + f"background-color: {sidebar_bg};" + f"border-right: 1px solid {sidebar_border};" "border-radius: 0;" ) sidebar_layout = QtWidgets.QVBoxLayout(self.sidebar) @@ -144,13 +183,14 @@ def __init__(self): sidebar_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) # 侧边栏标题 - sidebar_title = QtWidgets.QLabel("功能菜单") - sidebar_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - sidebar_title.setStyleSheet( - "font-size: 18px; font-weight: bold; color: #1e293b; margin-bottom: 16px;" + self.sidebar_title = QtWidgets.QLabel("功能菜单") + self.sidebar_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + title_color = "#2d3749" if not self.theme_manager.is_dark_mode else "#cdd6f4" + self.sidebar_title.setStyleSheet( + f"font-size: 18px; font-weight: bold; color: {title_color}; margin-bottom: 16px; " "padding: 8px 0;" ) - sidebar_layout.addWidget(sidebar_title) + sidebar_layout.addWidget(self.sidebar_title) # 侧边栏按钮组 self.sidebar_buttons = [] @@ -163,14 +203,7 @@ def __init__(self): self.btn_files.setMinimumWidth(140) self.btn_files.setMaximumHeight(100) self.btn_files.setMaximumWidth(140) - self.btn_files.setStyleSheet( - "font-size: 13px; text-align: center; font-weight: 500;" - "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 16px;" - "border: none;" - "padding: 8px;" - "line-height: 1.4;" - ) + self.btn_files.setStyleSheet(self.get_sidebar_button_style(is_active=True)) sidebar_layout.addWidget(self.btn_files, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) self.sidebar_buttons.append(self.btn_files) @@ -180,14 +213,7 @@ def __init__(self): self.btn_transfer.setMinimumWidth(140) self.btn_transfer.setMaximumHeight(100) self.btn_transfer.setMaximumWidth(140) - self.btn_transfer.setStyleSheet( - "font-size: 13px; text-align: center; font-weight: 500;" - "background-color: rgba(229, 231, 235, 0.8); color: #334155;" - "border-radius: 16px;" - "border: 1px solid rgba(0, 0, 0, 0.08);" - "padding: 8px;" - "line-height: 1.4;" - ) + self.btn_transfer.setStyleSheet(self.get_sidebar_button_style(is_active=False)) sidebar_layout.addWidget(self.btn_transfer, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) self.sidebar_buttons.append(self.btn_transfer) @@ -279,9 +305,40 @@ def __init__(self): # 为每个按钮添加动画效果 self.button_animations = {} + # 记录按钮原始尺寸,避免动画使用未完成布局的宽度作为基准 + if not hasattr(self, 'button_original_sizes'): + self.button_original_sizes = {} + for b in btns: b.setMinimumHeight(30) - b.setMinimumWidth(110) + # 根据按钮文本自动计算最小宽度,保证中文/emoji不被截断 + fm = b.fontMetrics() + text_width = fm.horizontalAdvance(b.text()) + padding = 32 # 左右内边距预留(4px padding + 8px border margin) + calc_min_w = max(85, text_width + padding) + b.setMinimumWidth(calc_min_w) + # 设置较大的最大宽度,避免动画过程中被意外限制 + b.setMaximumWidth(max(calc_min_w + 20, 2000)) + # 初始记录为计算得到的最小宽度 + self.button_original_sizes[b] = calc_min_w + b.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + b.setStyleSheet( + "QPushButton {" + " padding: 4px 8px;" + " background-color: rgba(229, 231, 235, 0.8);" + " color: #334155;" + " border: 1px solid rgba(0, 0, 0, 0.08);" + " border-radius: 6px;" + " font-size: 12px;" + " font-weight: 500;" + "}" + "QPushButton:hover {" + " background-color: rgba(209, 213, 219, 0.9);" + "}" + "QPushButton:pressed {" + " background-color: rgba(189, 195, 204, 1.0);" + "}" + ) toolbar_h.addWidget(b) # 为按钮添加悬停和点击事件,实现动画效果 @@ -289,11 +346,6 @@ def __init__(self): b.leaveEvent = lambda event, btn=b: self.on_button_leave(btn) b.pressed.connect(lambda btn=b: self.on_button_pressed(btn)) b.released.connect(lambda btn=b: self.on_button_released(btn)) - - # 初始化按钮动画 - animation = QtCore.QPropertyAnimation(b, b"geometry") - animation.setDuration(100) - self.button_animations[b] = animation toolbar_h.addStretch() right_layout.addLayout(toolbar_h) @@ -359,7 +411,7 @@ def __init__(self): self.spinner_timer = QtCore.QTimer() self.spinner_angle = 0 self.spinner_timer.timeout.connect(self.update_spinner) - self.spinner_timer.start(50) # 每50毫秒更新一次 + # 不在这里启动计时器,延迟到loading_widget显示时再启动 loading_layout.addWidget(self.loading_spinner) @@ -382,7 +434,8 @@ def __init__(self): # 传输页面内容 transfer_title = QtWidgets.QLabel("传输任务") transfer_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - transfer_title.setStyleSheet("font-size: 24px; font-weight: bold; color: #334155; margin: 20px 0;") + title_color = self.get_theme_color('text_primary') + transfer_title.setStyleSheet(f"font-size: 24px; font-weight: bold; color: {title_color}; margin: 20px 0;") transfer_layout.addWidget(transfer_title) self.transfer_table = QtWidgets.QTableWidget(0, 6) @@ -436,234 +489,6 @@ def __init__(self): # 启动登录流程 self.startup_login_flow() - def apply_blue_white_theme(self): - """ - 123云盘主题样式表 - iOS 26 Liquid Glass 液态毛玻璃效果 - """ - style = """ - /* 全局样式 */ - QWidget { - background-color: rgba(255, 255, 255, 0.8); - color: #1E293B; - font-family: "SF Pro Display", "Segoe UI", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial; - font-size: 13px; - } - - /* 主窗口 */ - QMainWindow { - background-color: rgba(245, 245, 247, 0.95); - } - - /* 表格样式 - 液态毛玻璃效果(模拟) */ - QTableWidget { - background-color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: 12px; - padding: 8px; - gridline-color: rgba(0, 0, 0, 0.05); - } - - /* 表格行样式 */ - QTableWidget::item { - padding: 10px 6px; - border: none; - background-color: transparent; - border-radius: 6px; - } - - /* 表格行悬停效果 */ - QTableWidget::item:hover { - background-color: rgba(59, 130, 246, 0.1); - } - - /* 表格行选中效果 */ - QTableWidget::item:selected { - background-color: rgba(59, 130, 246, 0.9); - color: #FFFFFF; - } - - /* 表头样式 */ - QHeaderView::section { - background-color: rgba(255, 255, 255, 0.95); - color: #334155; - padding: 12px 16px; - border: none; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - font-weight: 600; - text-align: left; - border-radius: 8px 8px 0 0; - } - - QHeaderView { - background-color: transparent; - border: none; - } - - /* 按钮样式 - 液态毛玻璃效果(模拟) */ - QPushButton { - background-color: rgba(255, 255, 255, 0.95); - color: #3B82F6; - border: 1px solid rgba(59, 130, 246, 0.4); - border-radius: 12px; - padding: 10px 18px; - font-weight: 500; - font-size: 14px; - } - - QPushButton:hover { - background-color: rgba(255, 255, 255, 0.98); - border-color: rgba(59, 130, 246, 0.6); - } - - QPushButton:pressed { - background-color: rgba(230, 240, 255, 0.95); - border-color: rgba(59, 130, 246, 0.8); - } - - QPushButton:disabled { - background-color: rgba(240, 240, 245, 0.8); - border-color: rgba(148, 163, 184, 0.4); - color: rgba(148, 163, 184, 0.8); - } - - /* 输入控件样式 - 液态毛玻璃效果(模拟) */ - QLineEdit, QTextEdit, QComboBox { - background-color: rgba(255, 255, 255, 0.95); - border: 1px solid rgba(0, 0, 0, 0.08); - padding: 10px 14px; - border-radius: 12px; - } - - QLineEdit:focus, QTextEdit:focus, QComboBox:focus { - border-color: rgba(59, 130, 246, 0.6); - } - - /* 状态栏样式 - 液态毛玻璃效果(模拟) */ - QStatusBar { - background-color: rgba(255, 255, 255, 0.95); - color: #334155; - padding: 8px 16px; - border-top: 1px solid rgba(0, 0, 0, 0.05); - } - - /* 菜单样式 - 液态毛玻璃效果(模拟) */ - QMenu { - background-color: rgba(255, 255, 255, 0.98); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - padding: 8px 0; - } - - QMenu::item { - padding: 10px 24px; - background-color: transparent; - border: none; - border-radius: 8px; - margin: 2px 8px; - } - - QMenu::item:selected { - background-color: rgba(59, 130, 246, 0.15); - color: #3B82F6; - } - - /* 滚动条样式 - 液态毛玻璃效果(模拟) */ - QScrollBar { - background-color: rgba(255, 255, 255, 0.7); - border-radius: 10px; - width: 10px; - height: 10px; - } - - QScrollBar::handle { - background-color: rgba(59, 130, 246, 0.6); - border-radius: 10px; - min-width: 24px; - min-height: 24px; - } - - QScrollBar::handle:hover { - background-color: rgba(59, 130, 246, 0.8); - } - - QScrollBar::add-line, QScrollBar::sub-line { - background-color: transparent; - } - - /* 对话框样式 - 液态毛玻璃效果(模拟) */ - QDialog { - background-color: rgba(255, 255, 255, 0.98); - border: 1px solid rgba(255, 255, 255, 0.9); - border-radius: 16px; - } - - /* 分组框样式 - 液态毛玻璃效果(模拟) */ - QGroupBox { - background-color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - margin-top: 16px; - padding: 16px; - } - - QGroupBox::title { - color: #334155; - font-weight: 600; - subcontrol-origin: margin; - subcontrol-position: top left; - padding: 0 12px; - } - - /* 复选框样式 - 液态毛玻璃效果(模拟) */ - QCheckBox { - spacing: 8px; - } - - QCheckBox::indicator { - width: 20px; - height: 20px; - border: 2px solid rgba(59, 130, 246, 0.6); - border-radius: 6px; - background-color: rgba(255, 255, 255, 0.95); - } - - QCheckBox::indicator:checked { - background-color: rgba(59, 130, 246, 0.95); - border-color: rgba(59, 130, 246, 0.95); - } - - /* 标签样式 */ - QLabel { - color: #334155; - } - - /* 路径标签 */ - QLabel#lbl_path { - font-weight: 600; - color: #3B82F6; - font-size: 14px; - } - - /* 加载动画标签 */ - QLabel#loading_label { - color: #3B82F6; - } - - /* 设置按钮特殊样式 */ - QPushButton#btn_settings { - background-color: transparent; - border: none; - border-radius: 8px; - font-size: 18px; - padding: 6px; - color: #3B82F6; - } - - QPushButton#btn_settings:hover { - background-color: rgba(59, 130, 246, 0.1); - } - """ - self.setStyleSheet(style) def on_settings(self): """打开设置对话框""" @@ -957,6 +782,9 @@ def refresh_file_list(self, reset_page=True): # 显示加载动画 self.table.setVisible(False) self.loading_widget.setVisible(True) + # 启动加载动画计时器 + if not self.spinner_timer.isActive(): + self.spinner_timer.start(50) self.status.showMessage("正在获取目录...") task = ThreadedTask(self._task_get_dir) @@ -972,6 +800,9 @@ def _after_get_dir(self, code): # 隐藏加载动画,显示表格 self.loading_widget.setVisible(False) self.table.setVisible(True) + # 停止加载动画计时器 + if self.spinner_timer.isActive(): + self.spinner_timer.stop() if code != 0: self.status.showMessage(f"获取目录返回码: {code}", 5000) @@ -1015,116 +846,98 @@ def _after_fade_out_enter_folder(self): self._show_error("进入文件夹失败: " + str(e)) def on_button_hover(self, button): - """按钮悬停效果 - 修复动画冲突""" - # 停止当前正在运行的动画 - if button in self.button_animations: - self.button_animations[button].stop() - - # 保存原始位置,用于恢复 - if not hasattr(self, 'button_original_geoms'): - self.button_original_geoms = {} - if button not in self.button_original_geoms: - self.button_original_geoms[button] = button.geometry() - - # 创建放大动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - original_geom = self.button_original_geoms[button] - # 基于原始位置计算新位置,避免累积误差 - new_geom = QtCore.QRect( - original_geom.x() - 2, - original_geom.y() - 2, - original_geom.width() + 4, - original_geom.height() + 4 + """按钮悬停效果 - 改变背景色和边框""" + button.setStyleSheet( + "QPushButton {" + " padding: 4px 8px;" + " background-color: rgba(189, 195, 204, 0.95);" + " color: #334155;" + " border: 1px solid rgba(59, 130, 246, 0.3);" + " border-radius: 6px;" + " font-size: 12px;" + " font-weight: 500;" + "}" ) - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(new_geom) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation def on_button_leave(self, button): - """按钮离开效果 - 修复动画冲突""" + """按钮离开效果 - 恢复原始样式""" # 停止当前正在运行的动画 if button in self.button_animations: self.button_animations[button].stop() - - # 恢复到原始位置 - if hasattr(self, 'button_original_geoms') and button in self.button_original_geoms: - # 创建恢复动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - original_geom = self.button_original_geoms[button] - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(original_geom) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation + + button.setStyleSheet( + "QPushButton {" + " padding: 4px 8px;" + " background-color: rgba(229, 231, 235, 0.8);" + " color: #334155;" + " border: 1px solid rgba(0, 0, 0, 0.08);" + " border-radius: 6px;" + " font-size: 12px;" + " font-weight: 500;" + "}" + "QPushButton:hover {" + " background-color: rgba(209, 213, 219, 0.9);" + "}" + "QPushButton:pressed {" + " background-color: rgba(189, 195, 204, 1.0);" + "}" + ) def on_button_pressed(self, button): - """按钮按下效果 - 修复动画冲突""" + """按钮按下效果 - 改变样式""" # 停止当前正在运行的动画 if button in self.button_animations: self.button_animations[button].stop() - - # 创建按下动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - # 基于当前位置轻微缩小 - new_geom = QtCore.QRect( - current_geom.x() + 1, - current_geom.y() + 1, - current_geom.width() - 2, - current_geom.height() - 2 + + button.setStyleSheet( + "QPushButton {" + " padding: 4px 8px;" + " background-color: rgba(169, 177, 189, 1.0);" + " color: #334155;" + " border: 1px solid rgba(59, 130, 246, 0.4);" + " border-radius: 6px;" + " font-size: 12px;" + " font-weight: 500;" + "}" ) - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(new_geom) - scale_animation.setDuration(100) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.InQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation def on_button_released(self, button): - """按钮释放效果 - 修复动画冲突""" + """按钮释放效果 - 恢复样式""" # 停止当前正在运行的动画 if button in self.button_animations: self.button_animations[button].stop() - - # 恢复到原始放大状态(如果是悬停中)或原始状态 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - - if hasattr(self, 'button_original_geoms') and button in self.button_original_geoms: - # 检查鼠标是否仍然在按钮上 - if button.underMouse(): - # 恢复到悬停放大状态 - original_geom = self.button_original_geoms[button] - new_geom = QtCore.QRect( - original_geom.x() - 2, - original_geom.y() - 2, - original_geom.width() + 4, - original_geom.height() + 4 - ) - else: - # 恢复到原始状态 - new_geom = self.button_original_geoms[button] - - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(new_geom) - scale_animation.setDuration(100) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation + + # 检查鼠标是否仍在按钮上 + if button.underMouse(): + button.setStyleSheet( + "QPushButton {" + " padding: 4px 8px;" + " background-color: rgba(189, 195, 204, 0.95);" + " color: #334155;" + " border: 1px solid rgba(59, 130, 246, 0.3);" + " border-radius: 6px;" + " font-size: 12px;" + " font-weight: 500;" + "}" + ) + else: + button.setStyleSheet( + "QPushButton {" + " padding: 4px 8px;" + " background-color: rgba(229, 231, 235, 0.8);" + " color: #334155;" + " border: 1px solid rgba(0, 0, 0, 0.08);" + " border-radius: 6px;" + " font-size: 12px;" + " font-weight: 500;" + "}" + "QPushButton:hover {" + " background-color: rgba(209, 213, 219, 0.9);" + "}" + "QPushButton:pressed {" + " background-color: rgba(189, 195, 204, 1.0);" + "}" + ) def on_table_context_menu(self, pos): row = self.table.indexAt(pos).row() @@ -1229,7 +1042,7 @@ def switch_page(self, page_index): self.btn_mkdir.setVisible(False) def on_sidebar_button_hover(self, button): - """侧边栏按钮悬停效果 - 改变背景色并增加阴影""" + """侧边栏按钮悬停效果 - 改变背景色""" if button == self.btn_files: button.setStyleSheet( "font-size: 13px; text-align: center; font-weight: 500;" @@ -1238,7 +1051,6 @@ def on_sidebar_button_hover(self, button): "border: none;" "padding: 8px;" "line-height: 1.4;" - "box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);" ) elif button == self.btn_transfer: button.setStyleSheet( @@ -1385,26 +1197,32 @@ def add_transfer_task(self, task_type, file_name, file_size): self.transfer_table.setItem(row, 4, QtWidgets.QTableWidgetItem("等待中")) # 添加取消按钮 - cancel_btn = QtWidgets.QPushButton("取消") + cancel_btn = QtWidgets.QPushButton("⏹️") + cancel_btn.setToolTip("停止传输") cancel_btn.setStyleSheet( - "background-color: rgba(239, 68, 68, 0.1);" + "background-color: rgba(239, 68, 68, 0.08);" "color: #EF4444;" - "border: 1px solid rgba(239, 68, 68, 0.3);" - "border-radius: 8px;" - "padding: 4px 12px;" - "font-size: 12px;" + "border: 1px solid rgba(239, 68, 68, 0.2);" + "border-radius: 6px;" + "padding: 4px 8px;" + "font-size: 14px;" + "min-width: 30px;" + "max-width: 30px;" ) cancel_btn.clicked.connect(lambda _, tid=task_id: self.cancel_transfer_task(tid)) # 添加暂停/继续按钮 - pause_btn = QtWidgets.QPushButton("暂停") + pause_btn = QtWidgets.QPushButton("⏸️") + pause_btn.setToolTip("暂停传输") pause_btn.setStyleSheet( "background-color: rgba(59, 130, 246, 0.08);" "color: #2563EB;" "border: 1px solid rgba(37, 99, 235, 0.2);" - "border-radius: 8px;" - "padding: 4px 12px;" - "font-size: 12px;" + "border-radius: 6px;" + "padding: 4px 8px;" + "font-size: 14px;" + "min-width: 30px;" + "max-width: 30px;" ) pause_btn.clicked.connect(lambda _, tid=task_id: self.pause_transfer_task(tid)) @@ -1412,12 +1230,12 @@ def add_transfer_task(self, task_type, file_name, file_size): btn_container = QtWidgets.QWidget() btn_layout = QtWidgets.QHBoxLayout(btn_container) btn_layout.setContentsMargins(0, 0, 0, 0) - btn_layout.setSpacing(6) + btn_layout.setSpacing(4) btn_layout.addWidget(pause_btn) btn_layout.addWidget(cancel_btn) self.transfer_table.setCellWidget(row, 5, btn_container) - # 保存按钮引用,便于后续隐藏或修改文字 + # 保存按钮引用,便于后续隐藏或修改 task['cancel_button'] = cancel_btn task['pause_button'] = pause_btn @@ -2046,7 +1864,76 @@ def _show_error(self, msg): QtWidgets.QMessageBox.critical(self, "错误", msg) self.status.showMessage(msg, 8000) + def get_theme_color(self, color_key): + """获取当前主题的颜色""" + mode = 'dark' if self.theme_manager.is_dark_mode else 'light' + return self.theme_colors[mode].get(color_key, '#000000') + + def get_sidebar_button_style(self, is_active=True): + """生成侧边栏按钮的样式表""" + if is_active: + # 活跃按钮(文件页) + bg_color = self.get_theme_color('accent') + text_color = "#ffffff" + border = "none" + else: + # 非活跃按钮(传输页等) + if self.theme_manager.is_dark_mode: + bg_color = "#414559" + text_color = "#cdd6f4" + border = "1px solid #585b70" + else: + bg_color = "#e8edf5" + text_color = "#4c4f69" + border = "1px solid #bcc0cc" + + return ( + f"font-size: 13px; text-align: center; font-weight: 500; " + f"background-color: {bg_color}; " + f"color: {text_color}; " + f"border-radius: 16px; " + f"border: {border}; " + f"padding: 8px; " + f"line-height: 1.4;" + ) + + def on_theme_changed(self): + """当主题改变时的回调""" + # 重新应用侧边栏样式 + sidebar_bg = "#fafbfc" if not self.theme_manager.is_dark_mode else "#181825" + sidebar_border = "#acb0be" if not self.theme_manager.is_dark_mode else "#313244" + self.sidebar.setStyleSheet( + f"background-color: {sidebar_bg};" + f"border-right: 1px solid {sidebar_border};" + "border-radius: 0;" + ) + + # 重新应用侧边栏标题颜色 + if hasattr(self, 'sidebar_title'): + title_color = "#2d3749" if not self.theme_manager.is_dark_mode else "#cdd6f4" + self.sidebar_title.setStyleSheet( + f"font-size: 18px; font-weight: bold; color: {title_color}; margin-bottom: 16px; " + "padding: 8px 0;" + ) + + # 重新应用侧边栏按钮样式 + if hasattr(self, 'btn_files'): + self.btn_files.setStyleSheet(self.get_sidebar_button_style(is_active=True)) + if hasattr(self, 'btn_transfer'): + self.btn_transfer.setStyleSheet(self.get_sidebar_button_style(is_active=False)) + def closeEvent(self, event): + # 停止所有计时器,防止线程冲突 + try: + if hasattr(self, 'spinner_timer') and self.spinner_timer.isActive(): + self.spinner_timer.stop() + except Exception: + pass + try: + if hasattr(self, 'theme_timer') and self.theme_timer.isActive(): + self.theme_timer.stop() + except Exception: + pass try: if self.pan and getattr(self.pan, "user_name", "") and getattr(self.pan, "password", ""): self.pan.save_file() diff --git a/src/themes.py b/src/themes.py new file mode 100644 index 0000000..7b7a6a1 --- /dev/null +++ b/src/themes.py @@ -0,0 +1,535 @@ +# https://github.com/123panNextGen/123pan +# src/themes.py + +"""Catppuccin 主题配置 - https://catppuccin.com/ +Light: Latte (#eff1f5 base, #4c4f69 text) +Dark: Mocha (#1e1e2e base, #cdd6f4 text) +""" + +LIGHT_THEME = """ +/* Catppuccin Latte - Light Theme */ +/* Base: #eff1f5 | Text: #4c4f69 | Primary: #1e66f5 */ +/* 全局样式 */ +QWidget { + background-color: #eff1f5; + color: #4c4f69; + font-family: "SF Pro Display", "Segoe UI", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial; + font-size: 13px; +} + +/* 主窗口 */ +QMainWindow { + background-color: #eff1f5; +} + +/* 表格样式 */ +QTableWidget { + background-color: #ffffff; + border: 1px solid #ccd0da; + border-radius: 12px; + padding: 8px; + gridline-color: #e6e9ef; +} + +/* 表格行样式 */ +QTableWidget::item { + padding: 10px 6px; + border: none; + background-color: transparent; + border-radius: 6px; + color: #4c4f69; +} + +/* 表格行悬停效果 */ +QTableWidget::item:hover { + background-color: #dfe0ea; +} + +/* 表格行选中效果 */ +QTableWidget::item:selected { + background-color: #1e66f5; + color: #ffffff; +} + +/* 表头样式 */ +QHeaderView::section { + background-color: #e6e9ef; + color: #4c4f69; + padding: 12px 16px; + border: none; + border-bottom: 1px solid #ccd0da; + font-weight: 600; + text-align: left; +} + +QHeaderView { + background-color: transparent; + border: none; +} + +/* 按钮样式 */ +QPushButton { + background-color: #ffffff; + color: #1e66f5; + border: 1px solid #bcc0cc; + border-radius: 12px; + padding: 10px 18px; + font-weight: 500; + font-size: 14px; +} + +QPushButton:hover { + background-color: #f2f4f8; + border-color: #1e66f5; +} + +QPushButton:pressed { + background-color: #e8edf5; + border-color: #1e66f5; +} + +QPushButton:disabled { + background-color: #e6e9ef; + border-color: #ccd0da; + color: #9ca0b0; +} + +/* 输入控件样式 */ +QLineEdit, QTextEdit, QComboBox { + background-color: #ffffff; + border: 1px solid #bcc0cc; + padding: 10px 14px; + border-radius: 12px; + color: #4c4f69; +} + +QLineEdit:focus, QTextEdit:focus, QComboBox:focus { + border-color: #1e66f5; + background-color: #f8faff; +} + +/* 状态栏样式 */ +QStatusBar { + background-color: #e6e9ef; + color: #4c4f69; + padding: 8px 16px; + border-top: 1px solid #ccd0da; +} + +/* 菜单样式 */ +QMenu { + background-color: #fafbfc; + border: 1px solid #acb0be; + border-radius: 12px; + padding: 8px 0; + color: #2d3749; +} + +QMenu::item { + padding: 10px 24px; + background-color: transparent; + border: none; + border-radius: 8px; + margin: 2px 8px; + color: #2d3749; +} + +QMenu::item:hover { + background-color: #e8edf5; +} + +QMenu::item:selected { + background-color: #dce0e8; + color: #1e66f5; + font-weight: 500; +} + +QMenu::item:disabled { + color: #9ca0b0; +} + +QMenu::separator { + background-color: #acb0be; + height: 1px; + margin: 4px 0; +} + +/* 滚动条样式 */ +QScrollBar { + background-color: #eff1f5; + border-radius: 10px; + width: 10px; + height: 10px; +} + +QScrollBar::handle { + background-color: #bcc0cc; + border-radius: 10px; + min-width: 24px; + min-height: 24px; +} + +QScrollBar::handle:hover { + background-color: #acb0be; +} + +QScrollBar::add-line, QScrollBar::sub-line { + background-color: transparent; +} + +/* 对话框样式 */ +QDialog { + background-color: #eff1f5; + border: 1px solid #ccd0da; + border-radius: 16px; +} + +/* 分组框样式 */ +QGroupBox { + background-color: #f8f9fb; + border: 1px solid #ccd0da; + border-radius: 12px; + margin-top: 16px; + padding: 16px; + color: #4c4f69; +} + +QGroupBox::title { + color: #4c4f69; + font-weight: 600; + subcontrol-origin: margin; + subcontrol-position: top left; + padding: 0 12px; +} + +/* 复选框样式 */ +QCheckBox { + spacing: 8px; + color: #4c4f69; +} + +QCheckBox::indicator { + width: 20px; + height: 20px; + border: 2px solid #bcc0cc; + border-radius: 6px; + background-color: #ffffff; +} + +QCheckBox::indicator:checked { + background-color: #1e66f5; + border-color: #1e66f5; +} + +QCheckBox::indicator:unchecked:hover { + border-color: #acb0be; +} + +/* 标签样式 */ +QLabel { + color: #4c4f69; +} + +QLabel#lbl_path { + font-weight: 600; + color: #1e66f5; + font-size: 14px; +} + +QLabel#loading_label { + color: #1e66f5; +} + +/* 设置按钮特殊样式 */ +QPushButton#btn_settings { + background-color: transparent; + border: none; + border-radius: 8px; + font-size: 18px; + padding: 6px; + color: #1e66f5; +} + +QPushButton#btn_settings:hover { + background-color: #e8edf5; +} + +/* 提示文本 */ +QToolTip { + background-color: #4c4f69; + color: #eff1f5; + border: 1px solid #bcc0cc; + border-radius: 6px; + padding: 4px 8px; +} +""" + +DARK_THEME = """ +/* Catppuccin Mocha - Dark Theme */ +/* Base: #1e1e2e | Text: #cdd6f4 | Primary: #89b4fa */ +/* 全局样式 */ +QWidget { + background-color: #1e1e2e; + color: #cdd6f4; + font-family: "SF Pro Display", "Segoe UI", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial; + font-size: 13px; +} + +/* 主窗口 */ +QMainWindow { + background-color: #1e1e2e; +} + +/* 侧边栏 */ +QWidget#sidebar { + background-color: #181825; + border-right: 1px solid #313244; +} + +/* 表格样式 */ +QTableWidget { + background-color: #313244; + border: 1px solid #45475a; + border-radius: 12px; + padding: 8px; + gridline-color: #313244; +} + +/* 表格行样式 */ +QTableWidget::item { + padding: 10px 6px; + border: none; + background-color: transparent; + border-radius: 6px; + color: #cdd6f4; +} + +/* 表格行悬停效果 */ +QTableWidget::item:hover { + background-color: #45475a; +} + +/* 表格行选中效果 */ +QTableWidget::item:selected { + background-color: #89b4fa; + color: #1e1e2e; +} + +/* 表头样式 */ +QHeaderView::section { + background-color: #313244; + color: #bac2de; + padding: 12px 16px; + border: none; + border-bottom: 1px solid #45475a; + font-weight: 600; + text-align: left; +} + +QHeaderView { + background-color: transparent; + border: none; +} + +/* 按钮样式 */ +QPushButton { + background-color: #313244; + color: #89b4fa; + border: 1px solid #45475a; + border-radius: 12px; + padding: 10px 18px; + font-weight: 500; + font-size: 14px; +} + +QPushButton:hover { + background-color: #45475a; + border-color: #585b70; +} + +QPushButton:pressed { + background-color: #2d2d44; + border-color: #89b4fa; +} + +QPushButton:disabled { + background-color: #313244; + border-color: #45475a; + color: #6c7086; +} + +/* 输入控件样式 */ +QLineEdit, QTextEdit, QComboBox { + background-color: #313244; + border: 1px solid #45475a; + padding: 10px 14px; + border-radius: 12px; + color: #cdd6f4; +} + +QLineEdit:focus, QTextEdit:focus, QComboBox:focus { + border-color: #89b4fa; + background-color: #313244; +} + +/* 状态栏样式 */ +QStatusBar { + background-color: #313244; + color: #bac2de; + padding: 8px 16px; + border-top: 1px solid #45475a; +} + +/* 菜单样式 */ +QMenu { + background-color: #313244; + border: 1px solid #585b70; + border-radius: 12px; + padding: 8px 0; + color: #cdd6f4; +} + +QMenu::item { + padding: 10px 24px; + background-color: transparent; + border: none; + border-radius: 8px; + margin: 2px 8px; + color: #cdd6f4; +} + +QMenu::item:hover { + background-color: #414559; +} + +QMenu::item:selected { + background-color: #585b70; + color: #89b4fa; + font-weight: 500; +} + +QMenu::item:disabled { + color: #6c7086; +} + +QMenu::separator { + background-color: #585b70; + height: 1px; + margin: 4px 0; +} + +/* 滚动条样式 */ +QScrollBar { + background-color: #1e1e2e; + border-radius: 10px; + width: 10px; + height: 10px; +} + +QScrollBar::handle { + background-color: #45475a; + border-radius: 10px; + min-width: 24px; + min-height: 24px; +} + +QScrollBar::handle:hover { + background-color: #585b70; +} + +QScrollBar::add-line, QScrollBar::sub-line { + background-color: transparent; +} + +/* 对话框样式 */ +QDialog { + background-color: #1e1e2e; + border: 1px solid #313244; + border-radius: 16px; + color: #cdd6f4; +} + +/* 分组框样式 */ +QGroupBox { + background-color: #313244; + border: 1px solid #45475a; + border-radius: 12px; + margin-top: 16px; + padding: 16px; + color: #cdd6f4; +} + +QGroupBox::title { + color: #bac2de; + font-weight: 600; + subcontrol-origin: margin; + subcontrol-position: top left; + padding: 0 12px; +} + +/* 复选框样式 */ +QCheckBox { + spacing: 8px; + color: #cdd6f4; +} + +QCheckBox::indicator { + width: 20px; + height: 20px; + border: 2px solid #45475a; + border-radius: 6px; + background-color: #313244; +} + +QCheckBox::indicator:checked { + background-color: #89b4fa; + border-color: #89b4fa; +} + +QCheckBox::indicator:unchecked:hover { + border-color: #585b70; +} + +/* 标签样式 */ +QLabel { + color: #cdd6f4; +} + +QLabel#lbl_path { + font-weight: 600; + color: #89b4fa; + font-size: 14px; +} + +QLabel#loading_label { + color: #89b4fa; +} + +/* 设置按钮特殊样式 */ +QPushButton#btn_settings { + background-color: transparent; + border: none; + border-radius: 8px; + font-size: 18px; + padding: 6px; + color: #89b4fa; +} + +QPushButton#btn_settings:hover { + background-color: rgba(137, 180, 250, 0.1); +} + +/* 提示文本 */ +QToolTip { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 6px; + padding: 4px 8px; +} + +/* 分隔线 */ +QFrame[frameShape="4"] { + color: #45475a; +} +""" diff --git a/src/ui_theme_manager.py b/src/ui_theme_manager.py new file mode 100644 index 0000000..611b513 --- /dev/null +++ b/src/ui_theme_manager.py @@ -0,0 +1,52 @@ +# https://github.com/123panNextGen/123pan +# src/ui_theme_manager.py + +"""UI主题管理器""" + +from PyQt6 import QtGui +from themes import LIGHT_THEME, DARK_THEME + + +class ThemeManager: + """主题管理类""" + + def __init__(self, window): + self.window = window + self.is_dark_mode = False + self.on_theme_changed = None # 主题改变回调 + self.detect_and_apply_theme() + + def detect_system_theme(self): + """检测系统是否为深色模式""" + palette = self.window.palette() + bg_color = palette.color(QtGui.QPalette.ColorRole.Base) + brightness = (bg_color.red() + bg_color.green() + bg_color.blue()) / 3 + return brightness < 128 + + def detect_and_apply_theme(self): + """检测系统主题并应用""" + self.is_dark_mode = self.detect_system_theme() + self.apply_theme() + + def apply_theme(self): + """应用当前主题""" + if self.is_dark_mode: + self.window.setStyleSheet(DARK_THEME) + else: + self.window.setStyleSheet(LIGHT_THEME) + + # 触发主题改变回调,允许重新应用自定义样式 + if self.on_theme_changed: + self.on_theme_changed() + + def toggle_theme(self): + """切换主题""" + self.is_dark_mode = not self.is_dark_mode + self.apply_theme() + + def check_theme_change(self): + """检查系统主题是否已变化""" + new_dark_mode = self.detect_system_theme() + if new_dark_mode != self.is_dark_mode: + self.is_dark_mode = new_dark_mode + self.apply_theme() From 1e3abb5f95b57d4bd4a0420df0f638d003745cbf Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Mon, 2 Feb 2026 19:50:34 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9Bbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main_window.py | 233 +++++++++++++++++++++++++++------------------ src/themes.py | 30 ++++++ 2 files changed, 169 insertions(+), 94 deletions(-) diff --git a/src/main_window.py b/src/main_window.py index c3f2885..ac33558 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,5 +1,5 @@ # https://github.com/123panNextGen/123pan -# src/main.window.py +# src/main_window.py from PyQt6 import QtCore, QtGui, QtWidgets import os @@ -699,10 +699,9 @@ def populate_table(self): return self.table.setRowCount(0) - # 逐行添加,使用定时器实现动画效果 - for i, item in enumerate(self.pan.list): - # 使用定时器延迟添加,实现逐行出现的效果 - QtCore.QTimer.singleShot(i * 30, lambda idx=i: self._add_row(idx)) + # 直接添加所有行,避免延迟导致的数据错乱 + for index, item in enumerate(self.pan.list): + self._add_row(index) names = getattr(self.pan, "parent_file_name_list", []) path = "/" + "/".join(names) if names else "/" @@ -1164,96 +1163,107 @@ def on_sidebar_button_released(self, button): "padding: 8px;" "line-height: 1.4;" ) + - def add_transfer_task(self, task_type, file_name, file_size): - """添加传输任务到列表和表格""" - task_id = self.next_task_id - self.next_task_id += 1 - - # 创建任务对象 - task = { - "id": task_id, - "type": task_type, # "下载" 或 "上传" - "file_name": file_name, - "file_size": file_size, - "progress": 0, - "status": "等待中", - "file_path": "", # 用于保存下载文件路径,便于取消时删除 - "threaded_task": None # 保存线程任务引用 - } + def add_transfer_task(self, type_str, name, size): + # 确保 UI 操作在主线程 + row = self.transfer_table.rowCount() + self.transfer_table.insertRow(row) + + task_id = self.next_task_id + self.next_task_id += 1 + + # 把 task_id 绑定到 Item 上 + name_item = QtWidgets.QTableWidgetItem(name) + name_item.setData(QtCore.Qt.ItemDataRole.UserRole, task_id) + + self.transfer_table.setItem(row, 0, QtWidgets.QTableWidgetItem(type_str)) + self.transfer_table.setItem(row, 1, name_item) + + # 格式化大小 + s = f"{round(size / 1048576, 2)} MB" if size > 1048576 else f"{round(size / 1024, 2)} KB" + self.transfer_table.setItem(row, 2, QtWidgets.QTableWidgetItem(s)) + self.transfer_table.setItem(row, 3, QtWidgets.QTableWidgetItem("0%")) + self.transfer_table.setItem(row, 4, QtWidgets.QTableWidgetItem("等待中")) + + # 创建并初始化任务字典,添加到transfer_tasks列表中 + task = { + "id": task_id, + "type": type_str, + "name": name, + "size": size, + "progress": 0, + "status": "等待中", + "file_path": None, + "threaded_task": None, + "pause_button": None, + "cancel_button": None, + "row": row + } + self.transfer_tasks.append(task) + + # 添加操作按钮 + action_widget = self.create_action_buttons(task_id) + self.transfer_table.setCellWidget(row, 5, action_widget) + + return task_id + + def create_action_buttons(self, task_id): + container = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(container) - # 添加到任务列表 - self.transfer_tasks.append(task) - - # 添加到表格 - row = self.transfer_table.rowCount() - self.transfer_table.insertRow(row) - - # 设置表格内容 - self.transfer_table.setItem(row, 0, QtWidgets.QTableWidgetItem(task_type)) - self.transfer_table.setItem(row, 1, QtWidgets.QTableWidgetItem(file_name)) - self.transfer_table.setItem(row, 2, QtWidgets.QTableWidgetItem(self.format_file_size(file_size))) - self.transfer_table.setItem(row, 3, QtWidgets.QTableWidgetItem("0%")) - self.transfer_table.setItem(row, 4, QtWidgets.QTableWidgetItem("等待中")) - - # 添加取消按钮 - cancel_btn = QtWidgets.QPushButton("⏹️") - cancel_btn.setToolTip("停止传输") - cancel_btn.setStyleSheet( - "background-color: rgba(239, 68, 68, 0.08);" - "color: #EF4444;" - "border: 1px solid rgba(239, 68, 68, 0.2);" - "border-radius: 6px;" - "padding: 4px 8px;" - "font-size: 14px;" - "min-width: 30px;" - "max-width: 30px;" - ) - cancel_btn.clicked.connect(lambda _, tid=task_id: self.cancel_transfer_task(tid)) - - # 添加暂停/继续按钮 - pause_btn = QtWidgets.QPushButton("⏸️") - pause_btn.setToolTip("暂停传输") - pause_btn.setStyleSheet( - "background-color: rgba(59, 130, 246, 0.08);" - "color: #2563EB;" - "border: 1px solid rgba(37, 99, 235, 0.2);" - "border-radius: 6px;" - "padding: 4px 8px;" - "font-size: 14px;" - "min-width: 30px;" - "max-width: 30px;" - ) - pause_btn.clicked.connect(lambda _, tid=task_id: self.pause_transfer_task(tid)) - - # 将两个按钮放在一个容器里 - btn_container = QtWidgets.QWidget() - btn_layout = QtWidgets.QHBoxLayout(btn_container) - btn_layout.setContentsMargins(0, 0, 0, 0) - btn_layout.setSpacing(4) - btn_layout.addWidget(pause_btn) - btn_layout.addWidget(cancel_btn) - self.transfer_table.setCellWidget(row, 5, btn_container) - - # 保存按钮引用,便于后续隐藏或修改 - task['cancel_button'] = cancel_btn - task['pause_button'] = pause_btn - - return task_id + # 暂停/恢复按钮 + pause_btn = QtWidgets.QPushButton("暂停") + # 取消按钮 + cancel_btn = QtWidgets.QPushButton("取消") + + # 设置按钮固定大小,防止被 QSS 拉伸变形 + pause_btn.setFixedSize(60, 24) + cancel_btn.setFixedSize(60, 24) + + # 可以在这里为这些按钮添加特定的对象名,以便单独设置样式 + pause_btn.setObjectName("transferActionBtn") + cancel_btn.setObjectName("transferActionBtn") + + # 连接按钮信号到处理函数 + pause_btn.clicked.connect(lambda: self.toggle_task_pause(task_id, pause_btn)) + cancel_btn.clicked.connect(lambda: self.cancel_task(task_id)) + + # 保存按钮引用到任务中,便于后续更新 + for i, t in enumerate(self.transfer_tasks): + if t["id"] == task_id: + self.transfer_tasks[i]['pause_button'] = pause_btn + self.transfer_tasks[i]['cancel_button'] = cancel_btn + break + + layout.addWidget(pause_btn) + layout.addWidget(cancel_btn) + layout.setContentsMargins(5, 2, 5, 2) + layout.setSpacing(10) + layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + return container def update_transfer_task(self, task_id, progress, status): - """更新传输任务的进度和状态""" - # 查找任务 - for i, task in enumerate(self.transfer_tasks): - if task["id"] == task_id: - # 更新任务对象 - task["progress"] = progress - task["status"] = status + """根据 task_id 安全地更新传输列表中的某一行""" + # 遍历表格找到匹配 task_id 的行(假设我们将 task_id 存储在某列的数据角色中) + found_row = -1 + for row in range(self.transfer_table.rowCount()): + item = self.transfer_table.item(row, 1) # 获取文件名那一列 + if item and item.data(QtCore.Qt.ItemDataRole.UserRole) == task_id: + found_row = row + break + + if found_row == -1: + return + + # 更新进度 (第3列) + if progress is not None: + self.transfer_table.item(found_row, 3).setText(f"{progress}%") - # 更新表格 - self.transfer_table.setItem(i, 3, QtWidgets.QTableWidgetItem(f"{progress}%")) - self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem(status)) - break + # 更新状态 (第4列) + if status: + self.transfer_table.item(found_row, 4).setText(status) def cancel_transfer_task(self, task_id): """取消传输任务""" @@ -1628,8 +1638,9 @@ def _task_get_link(self, file_index, signals=None, task=None): return f"获取链接失败: {str(e)}" def _after_get_link(self, url): - if isinstance(url, int): - self._show_error("获取链接失败,返回码: " + str(url)) + if isinstance(url, int) or (isinstance(url, str) and url.startswith("获取链接失败")): + error_msg = str(url) if isinstance(url, str) else ("获取链接失败,返回码: " + str(url)) + self._show_error(error_msg) return dlg = QtWidgets.QDialog(self) dlg.setWindowTitle("下载链接") @@ -1941,6 +1952,41 @@ def closeEvent(self, event): pass event.accept() + def on_pause_clicked(self, task): + if task.is_paused: + task.resume() + else: + task.pause() + + def on_cancel_clicked(self, task_id): + if task_id in self.active_tasks: + self.active_tasks[task_id].cancel() + + def toggle_task_pause(self, task_id, button): + """处理暂停和恢复逻辑""" + task = self.active_tasks.get(task_id) + if not task: return + + if task.is_paused: + task.resume() + button.setText("暂停") + button.setStyleSheet(button.styleSheet().replace("#f9e2af", self.get_theme_color('accent'))) + else: + task.pause() + button.setText("恢复") + # 变成警告色(黄色) + button.setStyleSheet(button.styleSheet().replace(self.get_theme_color('accent'), "#f9e2af")) + self.update_transfer_task(task_id, None, "已暂停") + + def cancel_task(self, task_id): + """取消任务""" + task = self.active_tasks.get(task_id) + if task: + task.cancel() + self.update_transfer_task(task_id, 0, "已取消") + if task_id in self.active_tasks: + del self.active_tasks[task_id] + def main(): app = QtWidgets.QApplication(sys.argv) w = MainWindow() @@ -1948,5 +1994,4 @@ def main(): sys.exit(app.exec()) if __name__ == "__main__": - main() - + main() \ No newline at end of file diff --git a/src/themes.py b/src/themes.py index 7b7a6a1..33b08c8 100644 --- a/src/themes.py +++ b/src/themes.py @@ -40,6 +40,21 @@ color: #4c4f69; } +QPushButton#transferActionBtn { + background-color: #89b4fa; + color: white; + border-radius: 4px; + font-size: 11px; +} + +QPushButton#transferActionBtn:hover { + background-color: #b4befe; +} + +QPushButton#transferActionBtn:pressed { + background-color: #74c7ec; +} + /* 表格行悬停效果 */ QTableWidget::item:hover { background-color: #dfe0ea; @@ -304,6 +319,21 @@ color: #cdd6f4; } +QPushButton#transferActionBtn { + background-color: #89b4fa; + color: white; + border-radius: 4px; + font-size: 11px; +} + +QPushButton#transferActionBtn:hover { + background-color: #b4befe; +} + +QPushButton#transferActionBtn:pressed { + background-color: #74c7ec; +} + /* 表格行悬停效果 */ QTableWidget::item:hover { background-color: #45475a; From 54c592dd733500f357dc468daba36fe395def8de Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Mon, 2 Feb 2026 20:09:33 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8Daction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b53b050..87b23cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt pip install nuitka zstandard - + - name: Build with Nuitka working-directory: src run: | @@ -46,7 +46,7 @@ jobs: --windows-icon-from-ico=icon.ico shell: bash - # Windows + # Windows artifact upload - name: Upload Windows artifact if: runner.os == 'Windows' uses: actions/upload-artifact@v4 @@ -54,7 +54,7 @@ jobs: name: 123pan-windows path: src/123pan.exe - # Linux + # Linux artifact upload - name: Upload Linux artifact if: runner.os == 'Linux' uses: actions/upload-artifact@v4 @@ -68,15 +68,26 @@ jobs: runs-on: ubuntu-latest steps: - - name: Download artifacts + - name: Download Windows artifact + if: runner.os == 'Linux' + uses: actions/download-artifact@v4 + with: + name: 123pan-windows + path: ./artifacts/windows + + - name: Download Linux artifact + if: runner.os == 'Linux' uses: actions/download-artifact@v4 with: - path: artifacts + name: 123pan-linux + path: ./artifacts/linux - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: Release ${{ github.ref_name }} - files: all_binaries/* + files: | + ./artifacts/windows/123pan.exe + ./artifacts/linux/123pan generate_release_notes: true