546 lines
21 KiB
Python
546 lines
21 KiB
Python
import sys
|
|
import os
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QHBoxLayout, QVBoxLayout, QListWidget, QListWidgetItem,
|
|
QStackedWidget, QWidget, QLabel, QSlider, QPushButton,
|
|
QCheckBox, QGroupBox, QFormLayout, QSpinBox, QFrame
|
|
)
|
|
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
|
|
|
|
|
|
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.heart", "捐赠"),
|
|
("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_donate())
|
|
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
|
|
ic = "#cccccc" if theme.name() == "dark" else "#555555"
|
|
import qtawesome as qta
|
|
self._panel.pin_btn.setIcon(
|
|
qta.icon("fa5s.thumbtack", color="#f90" if checked else ic)
|
|
)
|
|
|
|
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.addWidget(self._divider())
|
|
layout.addWidget(self._section_title("关于"))
|
|
|
|
about = QLabel("桌面文件整理 v1.0\n整理你的桌面快捷方式,保持桌面干净。")
|
|
about.setStyleSheet("color:#888; font-size:12px; line-height:1.6;")
|
|
layout.addWidget(about)
|
|
|
|
layout.addStretch()
|
|
return page
|
|
|
|
# ── 捐赠页 ───────────────────────────────────────────
|
|
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("如果你喜欢这个软件,欢迎通过二维码支持开发者持续优化与维护。你的鼓励会成为重要的动力。<br>感谢你的支持!")
|
|
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)
|
|
|
|
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;
|
|
}}
|
|
""")
|