import sys import os from PyQt6.QtWidgets import ( QDialog, QHBoxLayout, QVBoxLayout, QListWidget, QListWidgetItem, QStackedWidget, QWidget, QLabel, QSlider, QPushButton, QCheckBox, QGroupBox, QFormLayout, QSpinBox, QFrame, QLineEdit, QFileDialog ) from PyQt6.QtCore import Qt, QSize from PyQt6.QtWidgets import QMessageBox from PyQt6.QtGui import QIcon, QPixmap import qtawesome as qta from db import database import ui.theme as theme import ui.dialog_style as dialog_style from app_info import APP_NAME, __VERSION__ class SettingsWindow(QDialog): def __init__(self, panel, parent=None): super().__init__(parent) self._panel = panel self.setWindowTitle("设置") # 适当放大:让页面内容更舒展,并限制最小拖拽尺寸 self.setMinimumSize(500, 500) self.resize(760, 520) self.setWindowFlags( Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint ) self._build_ui() self._apply_theme() def _build_ui(self): root = QHBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) # ── 左侧导航 ───────────────────────────────────── self.nav = QListWidget() self.nav.setFixedWidth(130) self.nav.setSpacing(2) self.nav.setIconSize(QSize(18, 18)) self.nav.setStyleSheet(""" QListWidget { border:none; outline:none; padding:8px 4px; } QListWidget::item { height:36px; border-radius:6px; padding-left:10px; font-size:13px; } QListWidget::item:selected { background:#4a9eff; color:white; } QListWidget::item:hover:!selected { background:rgba(128,128,128,40); } """) pages = [ ("fa5s.paint-brush", "外观"), ("fa5s.window-maximize", "窗口"), ("fa5s.rocket", "启动"), ("fa5s.folder-open", "缓存"), ("fa5s.heart", "捐赠"), ("fa5s.info-circle", "关于"), ("fa5s.eraser", "初始化"), ] self._nav_ready = False self._block_nav = False for icon_name, label in pages: try: icon = qta.icon(icon_name, color="#888") item = QListWidgetItem(icon, label) except Exception: # 防止图标名不存在导致设置页直接报错 icon = qta.icon("fa5s.rocket", color="#888") item = QListWidgetItem(icon, label) if label == "初始化": item.setData(Qt.ItemDataRole.UserRole, "init") self.nav.addItem(item) self.nav.setCurrentRow(0) self.nav.currentRowChanged.connect(self._on_nav) # ── 右侧内容区 ─────────────────────────────────── self.stack = QStackedWidget() self.stack.addWidget(self._page_appearance()) self.stack.addWidget(self._page_window()) self.stack.addWidget(self._page_startup()) self.stack.addWidget(self._page_cache()) self.stack.addWidget(self._page_donate()) self.stack.addWidget(self._page_about()) self.stack.addWidget(self._page_initialization()) # 此时 stack 已就绪,允许导航回调正常工作 self._nav_ready = True self._last_nav_index = self.nav.currentRow() self.stack.setCurrentIndex(self._last_nav_index) # 分割线 line = QFrame() line.setFrameShape(QFrame.Shape.VLine) line.setStyleSheet("color: rgba(128,128,128,40);") root.addWidget(self.nav) root.addWidget(line) root.addWidget(self.stack) # ── 外观页 ─────────────────────────────────────────── def _page_appearance(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(20) layout.addWidget(self._section_title("主题")) theme_row = QHBoxLayout() theme_row.setSpacing(12) self._dark_btn = self._theme_card("🌙 暗色", "dark") self._light_btn = self._theme_card("☀️ 亮色", "light") self._dark_btn.clicked.connect(lambda: self._set_theme("dark")) self._light_btn.clicked.connect(lambda: self._set_theme("light")) theme_row.addWidget(self._dark_btn) theme_row.addWidget(self._light_btn) theme_row.addStretch() layout.addLayout(theme_row) self._update_theme_cards() layout.addWidget(self._divider()) layout.addWidget(self._section_title("透明度")) opacity_row = QHBoxLayout() opacity_row.setSpacing(12) self._opacity_slider = QSlider(Qt.Orientation.Horizontal) self._opacity_slider.setRange(30, 100) saved = int(database.get_setting("panel_opacity", "100")) self._opacity_slider.setValue(saved) self._opacity_slider.setFixedWidth(220) self._opacity_val = QLabel(f"{saved}%") self._opacity_val.setFixedWidth(36) self._opacity_slider.valueChanged.connect(self._on_opacity) opacity_row.addWidget(self._opacity_slider) opacity_row.addWidget(self._opacity_val) opacity_row.addStretch() layout.addLayout(opacity_row) layout.addStretch() return page def _theme_card(self, text: str, theme_name: str) -> QPushButton: btn = QPushButton(text) btn.setFixedSize(110, 44) btn.setCheckable(True) btn.setProperty("theme_name", theme_name) return btn def _update_theme_cards(self): cur = theme.name() for btn in (self._dark_btn, self._light_btn): active = btn.property("theme_name") == cur btn.setChecked(active) if active: btn.setStyleSheet(""" QPushButton { background:#4a9eff; color:white; border-radius:8px; font-size:13px; border:2px solid #4a9eff; } """) else: t = theme.current() btn.setStyleSheet(f""" QPushButton {{ background:{t['search_bg']}; color:{t['search_color']}; border-radius:8px; font-size:13px; border:1px solid {t['search_border']}; }} QPushButton:hover {{ border-color:#4a9eff; }} """) def _set_theme(self, name: str): theme.set_theme(name) self._panel._apply_theme() self._update_theme_cards() self._apply_theme() def _on_opacity(self, val: int): self._panel.setWindowOpacity(val / 100) self._opacity_val.setText(f"{val}%") database.set_setting("panel_opacity", str(val)) # ── 窗口页 ─────────────────────────────────────────── def _page_window(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(16) layout.addWidget(self._section_title("窗口行为")) self._pin_check = QCheckBox("固定面板(置顶、不自动隐藏)") self._pin_check.setChecked(self._panel._pinned) self._pin_check.toggled.connect(self._on_pin) layout.addWidget(self._pin_check) layout.addWidget(self._divider()) layout.addWidget(self._section_title("图标大小")) size_row = QHBoxLayout() size_row.setSpacing(12) size_lbl = QLabel("图标宽度:") self._icon_size_spin = QSpinBox() self._icon_size_spin.setRange(48, 120) self._icon_size_spin.setSingleStep(4) saved_size = int(database.get_setting("icon_width", "72")) self._icon_size_spin.setValue(saved_size) self._icon_size_spin.setSuffix(" px") self._icon_size_spin.setFixedWidth(90) apply_btn = QPushButton("应用") apply_btn.setFixedWidth(60) apply_btn.clicked.connect(self._on_icon_size) size_row.addWidget(size_lbl) size_row.addWidget(self._icon_size_spin) size_row.addWidget(apply_btn) size_row.addStretch() layout.addLayout(size_row) layout.addStretch() return page def _on_pin(self, checked: bool): self._panel._pinned = checked self._panel._sync_pin_dependent_ui() def _on_icon_size(self): val = self._icon_size_spin.value() database.set_setting("icon_width", str(val)) # 通知所有 ItemWidget 更新尺寸 from ui.flow_layout import ITEM_W import ui.flow_layout as fl fl.ITEM_W = val self._panel.refresh_groups() # ── 启动页 ─────────────────────────────────────────── def _page_startup(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(16) layout.addWidget(self._section_title("开机启动")) self._autostart_check = QCheckBox("开机自动启动") self._autostart_check.setChecked(self._is_autostart_enabled()) self._autostart_check.toggled.connect(self._on_autostart) layout.addWidget(self._autostart_check) layout.addStretch() return page def _page_about(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(16) layout.addWidget(self._section_title("关于")) about = QLabel( f"{APP_NAME} v{__VERSION__}

" "整理你的桌面快捷方式,保持桌面干净。

" "快捷键:Alt + ` 唤醒 / 隐藏主界面" ) about.setStyleSheet("color:#888; font-size:12px; line-height:1.8;") about.setTextFormat(Qt.TextFormat.RichText) layout.addWidget(about) layout.addStretch() return page # ── 缓存页 ─────────────────────────────────────────── def _page_cache(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(16) layout.addWidget(self._section_title("缓存目录")) desc = QLabel("下载的免安装软件/安装包默认保存到该目录。") desc.setWordWrap(True) desc.setStyleSheet("color:#888; font-size:12px; line-height:1.6;") layout.addWidget(desc) row = QHBoxLayout() row.setSpacing(10) self._cache_path_edit = QLineEdit() self._cache_path_edit.setPlaceholderText("请选择缓存目录…") self._cache_path_edit.setText(database.get_setting("cache_dir", "")) browse = QPushButton("浏览…") browse.clicked.connect(self._choose_cache_dir) save = QPushButton("保存") save.clicked.connect(self._save_cache_dir) row.addWidget(self._cache_path_edit) row.addWidget(browse) row.addWidget(save) layout.addLayout(row) layout.addStretch() return page def _choose_cache_dir(self): cur = "" if hasattr(self, "_cache_path_edit"): cur = self._cache_path_edit.text().strip() start = cur if cur else os.path.expanduser("~") p = QFileDialog.getExistingDirectory(self, "选择缓存目录", start) if p: self._cache_path_edit.setText(p) def _save_cache_dir(self): p = self._cache_path_edit.text().strip() if hasattr(self, "_cache_path_edit") else "" if p and not os.path.isdir(p): dialog_style.warning(self, "无效目录", "选择的缓存目录不存在,请重新选择。") return database.set_setting("cache_dir", p) dialog_style.information(self, "已保存", "缓存目录已保存。") # ── 捐赠页 ─────────────────────────────────────────── def _page_donate(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(16) layout.addWidget(self._section_title("捐赠支持")) info = QLabel("如果你喜欢这个软件,欢迎通过二维码支持开发者持续优化与维护。你的鼓励会成为重要的动力。
感谢你的支持!") info.setWordWrap(True) info.setStyleSheet("font-size:14px; line-height:1.1; background:transparent;") layout.addWidget(info) layout.addWidget(self._divider()) card = QFrame() card.setObjectName("donate_card") grid = QHBoxLayout(card) grid.setContentsMargins(14, 14, 14, 14) grid.setSpacing(18) grid.setAlignment(Qt.AlignmentFlag.AlignHCenter) donate_dir = os.path.join( os.path.dirname(os.path.dirname(__file__)), "assets", "imgs", "donate" ) wx_path = os.path.join(donate_dir, "wx.jpg") zfb_path = os.path.join(donate_dir, "zfb.jpg") def _qr_column(img_path: str, caption: str) -> QVBoxLayout: col = QVBoxLayout() col.setContentsMargins(0, 0, 0, 0) col.setSpacing(8) img = QLabel() img.setFixedSize(170, 170) img.setAlignment(Qt.AlignmentFlag.AlignCenter) img.setStyleSheet("background:transparent; border-radius:12px;") pm = QPixmap(img_path) if not pm.isNull(): img.setPixmap( pm.scaled( img.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) ) img.setCursor(Qt.CursorShape.PointingHandCursor) img.setToolTip("点击放大") # 直接覆盖点击事件:点击二维码弹出大图 img.mousePressEvent = ( # type: ignore[assignment] lambda e, p=img_path: self._open_donate_image(p) ) cap = QLabel(caption) cap.setAlignment(Qt.AlignmentFlag.AlignCenter) cap.setStyleSheet("font-size:12px; font-weight:700; background:transparent;") col.addWidget(img) col.addWidget(cap) return col grid.addLayout(_qr_column(wx_path, "微信支持")) grid.addLayout(_qr_column(zfb_path, "支付宝支持")) layout.addWidget(card) # 供 _apply_theme 动态设置样式 self._donate_card = card return page def _page_initialization(self) -> QWidget: page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(24, 20, 24, 20) layout.setSpacing(16) layout.addWidget(self._section_title("初始化")) desc = QLabel( "点击左侧「初始化」页签后,将弹出确认提醒。\n" "确认后会清空所有分组和程序(不可撤销)。" ) desc.setWordWrap(True) desc.setStyleSheet("color:#888; font-size:13px; line-height:1.6;") layout.addWidget(desc) self._init_status_lbl = QLabel("") self._init_status_lbl.setStyleSheet("font-size:12px; color:#f90;") layout.addWidget(self._init_status_lbl) layout.addStretch() return page def _open_donate_image(self, img_path: str): """点击二维码后的弹窗大图查看。""" t = theme.current() d = QDialog(self) d.setWindowTitle("二维码放大查看") d.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint) d.setFixedSize(800, 600) d.setStyleSheet(f""" QDialog {{ background: {t['panel_bg']}; border: 1px solid {t['panel_border']}; border-radius: 10px; color: {t['search_color']}; }} """) layout = QVBoxLayout(d) layout.setContentsMargins(14, 14, 14, 14) layout.setSpacing(0) lbl = QLabel() lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) lbl.setStyleSheet("background:transparent;") lbl.setScaledContents(False) # 不拉伸,保持等比 pm = QPixmap(img_path) if not pm.isNull(): # 适配框体:保持等比缩放(不会被拉伸) target_w = d.width() - 28 target_h = d.height() - 28 lbl.setPixmap( pm.scaled( target_w, target_h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) ) layout.addWidget(lbl) d.exec() def _is_autostart_enabled(self) -> bool: try: import winreg key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_READ ) winreg.QueryValueEx(key, "DesktopOrganizer") winreg.CloseKey(key) return True except Exception: return False def _on_autostart(self, checked: bool): from ui.dock import _set_autostart _set_autostart(checked) # ── 工具方法 ───────────────────────────────────────── def _section_title(self, text: str) -> QLabel: lbl = QLabel(text) lbl.setStyleSheet("font-size:14px; font-weight:bold;") return lbl def _divider(self) -> QFrame: line = QFrame() line.setFrameShape(QFrame.Shape.HLine) line.setStyleSheet("color: rgba(128,128,128,40);") return line def _on_nav(self, index: int): if self._block_nav: return if not getattr(self, "_nav_ready", False) or not hasattr(self, "stack"): return item = self.nav.item(index) is_init_tab = bool(item and item.data(Qt.ItemDataRole.UserRole) == "init") if not is_init_tab: self.stack.setCurrentIndex(index) self._last_nav_index = index return ret = dialog_style.question( self, "初始化", "提醒请谨慎操作!!!!!\n\n" "此操作会清空所有分组和程序,且无法撤销。是否继续?", icon=QMessageBox.Icon.Warning, ) if ret == QMessageBox.StandardButton.Yes: try: self._panel.reset_groups_and_items() except Exception as e: # 初始化失败也要尽量让用户知道原因 dialog_style.warning(self, "初始化失败", f"清空数据失败:{e}") # 初始化失败则回退到上一个页签 self._block_nav = True self.nav.setCurrentRow(self._last_nav_index) self._block_nav = False self.stack.setCurrentIndex(self._last_nav_index) return if hasattr(self, "_init_status_lbl"): self._init_status_lbl.setText("已完成初始化(分组和程序已清空)。") self.stack.setCurrentIndex(index) self._last_nav_index = index else: # 用户取消:回退到上一个页签 self._block_nav = True self.nav.setCurrentRow(self._last_nav_index) self._block_nav = False self.stack.setCurrentIndex(self._last_nav_index) def _apply_theme(self): t = theme.current() is_dark = theme.name() == "dark" # 把 rgba(r,g,b,a) 转成 rgb(r,g,b) panel_bg = t["panel_bg"] if panel_bg.startswith("rgba"): tmp = panel_bg.replace("rgba", "rgb") # "rgb(r,g,b,a)" -> 取前三项 "rgb(r,g,b" head = tmp.rsplit(",", 1)[0] panel_rgb = head + ")" else: panel_rgb = panel_bg self.setStyleSheet(f""" QDialog, QWidget {{ background: {panel_rgb}; color: {t['search_color']}; }} QListWidget {{ background: {'rgba(0,0,0,30)' if is_dark else 'rgba(0,0,0,6)'}; }} QSlider::groove:horizontal {{ height:4px; background:{t['scrollbar']}; border-radius:2px; }} QSlider::handle:horizontal {{ width:14px; height:14px; margin:-5px 0; background:#4a9eff; border-radius:7px; }} QSlider::sub-page:horizontal {{ background:#4a9eff; border-radius:2px; }} QCheckBox {{ font-size:13px; spacing:8px; }} QCheckBox::indicator {{ width:18px; height:18px; border-radius:4px; border:1px solid {t['search_border']}; background:{t['search_bg']}; }} QCheckBox::indicator:checked {{ background:#4a9eff; border-color:#4a9eff; image: url(none); }} QSpinBox {{ background:{t['search_bg']}; color:{t['search_color']}; border:1px solid {t['search_border']}; border-radius:5px; padding:4px 8px; font-size:12px; }} QPushButton {{ background:{t['search_bg']}; color:{t['search_color']}; border:1px solid {t['search_border']}; border-radius:6px; padding:6px 14px; font-size:12px; }} QPushButton:hover {{ border-color:#4a9eff; color:#4a9eff; }} """) # 导航背景 nav_bg = "rgba(0,0,0,30)" if is_dark else "rgba(0,0,0,6)" self.nav.setStyleSheet(f""" QListWidget {{ border:none; outline:none; padding:8px 4px; background:{nav_bg}; }} QListWidget::item {{ height:36px; border-radius:6px; padding-left:10px; font-size:13px; color:{t['search_color']}; }} QListWidget::item:selected {{ background:#4a9eff; color:white; }} QListWidget::item:hover:!selected {{ background:{'rgba(255,255,255,10)' if is_dark else 'rgba(0,0,0,6)'}; }} """) self._update_theme_cards() # 捐赠卡片样式 if hasattr(self, "_donate_card"): card_bg = "rgba(0,0,0,25)" if is_dark else "rgba(255,255,255,10)" self._donate_card.setStyleSheet(f""" QFrame#donate_card {{ background:{card_bg}; border: 1px solid {t['panel_border']}; border-radius:14px; }} """)