主要功能: - 账号管理:添加/编辑/删除账号,测试登录 - 浏览任务:批量浏览应读/选读内容并标记已读 - 截图管理:wkhtmltoimage截图,查看历史 - 金山文档:扫码登录/微信快捷登录,自动上传截图 技术栈: - PyQt6 GUI框架 - Playwright 浏览器自动化 - SQLite 本地数据存储 - wkhtmltoimage 网页截图 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
388 lines
14 KiB
Python
388 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Screenshot management panel - manual screenshot, view history, open directory
|
||
"""
|
||
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from datetime import datetime
|
||
from PyQt6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||
QListWidget, QListWidgetItem, QComboBox, QGroupBox, QMessageBox,
|
||
QFileDialog, QScrollArea, QFrame, QSizePolicy
|
||
)
|
||
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||
from PyQt6.QtGui import QPixmap, QIcon
|
||
|
||
from config import get_config, SCREENSHOTS_DIR
|
||
from utils.crypto import decrypt_password, is_encrypted
|
||
from .constants import (
|
||
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
|
||
GROUP_PADDING_BOTTOM, GROUP_SPACING, FORM_LABEL_WIDTH, FORM_H_SPACING,
|
||
INPUT_HEIGHT, INPUT_MIN_WIDTH, BUTTON_HEIGHT, BUTTON_HEIGHT_NORMAL,
|
||
BUTTON_MIN_WIDTH, BUTTON_SPACING, LIST_MIN_HEIGHT, ICON_SIZE_LARGE,
|
||
get_title_style
|
||
)
|
||
|
||
|
||
class ScreenshotWidget(QWidget):
|
||
"""Screenshot management panel"""
|
||
|
||
log_signal = pyqtSignal(str)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._worker = None
|
||
self._setup_ui()
|
||
self._refresh_screenshots()
|
||
|
||
def _setup_ui(self):
|
||
main_layout = QVBoxLayout(self)
|
||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||
main_layout.setSpacing(0)
|
||
|
||
# Scroll area
|
||
scroll = QScrollArea()
|
||
scroll.setWidgetResizable(True)
|
||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
|
||
content = QWidget()
|
||
layout = QVBoxLayout(content)
|
||
layout.setContentsMargins(PAGE_PADDING, PAGE_PADDING, PAGE_PADDING, PAGE_PADDING)
|
||
layout.setSpacing(SECTION_SPACING)
|
||
|
||
# Title
|
||
title = QLabel("📸 截图管理")
|
||
title.setStyleSheet(get_title_style())
|
||
layout.addWidget(title)
|
||
|
||
# ==================== Manual screenshot ====================
|
||
manual_group = QGroupBox("手动截图")
|
||
manual_layout = QVBoxLayout(manual_group)
|
||
manual_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
|
||
manual_layout.setSpacing(GROUP_SPACING)
|
||
|
||
# Account row
|
||
account_layout = QHBoxLayout()
|
||
account_layout.setSpacing(FORM_H_SPACING)
|
||
|
||
account_label = QLabel("选择账号:")
|
||
account_label.setFixedWidth(FORM_LABEL_WIDTH)
|
||
account_layout.addWidget(account_label)
|
||
|
||
self.account_combo = QComboBox()
|
||
self.account_combo.setMinimumWidth(INPUT_MIN_WIDTH + 100)
|
||
self.account_combo.setMinimumHeight(INPUT_HEIGHT)
|
||
account_layout.addWidget(self.account_combo)
|
||
|
||
refresh_account_btn = QPushButton("🔄 刷新")
|
||
refresh_account_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
|
||
refresh_account_btn.clicked.connect(self._refresh_accounts)
|
||
account_layout.addWidget(refresh_account_btn)
|
||
|
||
account_layout.addStretch()
|
||
manual_layout.addLayout(account_layout)
|
||
|
||
# Browse type row
|
||
type_layout = QHBoxLayout()
|
||
type_layout.setSpacing(FORM_H_SPACING)
|
||
|
||
type_label = QLabel("浏览类型:")
|
||
type_label.setFixedWidth(FORM_LABEL_WIDTH)
|
||
type_layout.addWidget(type_label)
|
||
|
||
self.browse_type_combo = QComboBox()
|
||
self.browse_type_combo.addItems(["应读", "注册前未读"])
|
||
self.browse_type_combo.setMinimumWidth(INPUT_MIN_WIDTH)
|
||
self.browse_type_combo.setMinimumHeight(INPUT_HEIGHT)
|
||
type_layout.addWidget(self.browse_type_combo)
|
||
|
||
type_layout.addStretch()
|
||
manual_layout.addLayout(type_layout)
|
||
|
||
# Screenshot button
|
||
btn_layout = QHBoxLayout()
|
||
btn_layout.setSpacing(BUTTON_SPACING)
|
||
|
||
self.screenshot_btn = QPushButton("📷 执行截图")
|
||
self.screenshot_btn.setObjectName("primary")
|
||
self.screenshot_btn.setMinimumWidth(BUTTON_MIN_WIDTH)
|
||
self.screenshot_btn.setMinimumHeight(BUTTON_HEIGHT)
|
||
self.screenshot_btn.clicked.connect(self._take_screenshot)
|
||
btn_layout.addWidget(self.screenshot_btn)
|
||
|
||
btn_layout.addStretch()
|
||
manual_layout.addLayout(btn_layout)
|
||
|
||
layout.addWidget(manual_group)
|
||
|
||
# ==================== Screenshot history ====================
|
||
history_group = QGroupBox("截图历史")
|
||
history_group.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
history_layout = QVBoxLayout(history_group)
|
||
history_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
|
||
history_layout.setSpacing(GROUP_SPACING)
|
||
|
||
# Toolbar
|
||
toolbar = QHBoxLayout()
|
||
toolbar.setSpacing(BUTTON_SPACING)
|
||
|
||
open_dir_btn = QPushButton("📂 打开目录")
|
||
open_dir_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
|
||
open_dir_btn.clicked.connect(self._open_screenshot_dir)
|
||
toolbar.addWidget(open_dir_btn)
|
||
|
||
toolbar.addStretch()
|
||
|
||
refresh_btn = QPushButton("🔄 刷新列表")
|
||
refresh_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
|
||
refresh_btn.clicked.connect(self._refresh_screenshots)
|
||
toolbar.addWidget(refresh_btn)
|
||
|
||
history_layout.addLayout(toolbar)
|
||
|
||
# Screenshot list - 图标模式,一行显示多个
|
||
self.screenshot_list = QListWidget()
|
||
self.screenshot_list.setViewMode(QListWidget.ViewMode.IconMode) # 图标模式
|
||
self.screenshot_list.setIconSize(QSize(150, 100)) # 缩略图大小
|
||
self.screenshot_list.setGridSize(QSize(170, 130)) # 网格大小(含文字)
|
||
self.screenshot_list.setResizeMode(QListWidget.ResizeMode.Adjust) # 自动调整
|
||
self.screenshot_list.setWrapping(True) # 自动换行
|
||
self.screenshot_list.setSpacing(8)
|
||
self.screenshot_list.setMinimumHeight(200)
|
||
self.screenshot_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
self.screenshot_list.itemDoubleClicked.connect(self._preview_screenshot)
|
||
history_layout.addWidget(self.screenshot_list, 1)
|
||
|
||
# Action buttons
|
||
action_layout = QHBoxLayout()
|
||
action_layout.setSpacing(BUTTON_SPACING)
|
||
|
||
self.delete_all_btn = QPushButton("🗑 删除全部")
|
||
self.delete_all_btn.setObjectName("danger")
|
||
self.delete_all_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
|
||
self.delete_all_btn.clicked.connect(self._delete_all)
|
||
action_layout.addWidget(self.delete_all_btn)
|
||
|
||
action_layout.addStretch()
|
||
|
||
self.preview_btn = QPushButton("👁 预览")
|
||
self.preview_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
|
||
self.preview_btn.clicked.connect(self._preview_selected)
|
||
action_layout.addWidget(self.preview_btn)
|
||
|
||
self.delete_btn = QPushButton("🗑 删除选中")
|
||
self.delete_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
|
||
self.delete_btn.clicked.connect(self._delete_selected)
|
||
action_layout.addWidget(self.delete_btn)
|
||
|
||
history_layout.addLayout(action_layout)
|
||
|
||
layout.addWidget(history_group, 1)
|
||
|
||
scroll.setWidget(content)
|
||
main_layout.addWidget(scroll)
|
||
|
||
# Load accounts
|
||
self._refresh_accounts()
|
||
|
||
def _refresh_accounts(self):
|
||
"""Refresh account list"""
|
||
self.account_combo.clear()
|
||
config = get_config()
|
||
|
||
for account in config.accounts:
|
||
if account.enabled:
|
||
display_name = account.remark if account.remark else account.username
|
||
self.account_combo.addItem(f"{display_name} ({account.username})", account)
|
||
|
||
def _refresh_screenshots(self):
|
||
"""Refresh screenshot list"""
|
||
self.screenshot_list.clear()
|
||
|
||
if not SCREENSHOTS_DIR.exists():
|
||
return
|
||
|
||
files = []
|
||
for f in SCREENSHOTS_DIR.iterdir():
|
||
if f.suffix.lower() in (".jpg", ".jpeg", ".png"):
|
||
files.append(f)
|
||
|
||
# Sort by modification time, newest first
|
||
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||
|
||
for f in files[:50]: # Only show last 50
|
||
item = QListWidgetItem()
|
||
# 文件名太长时截断显示
|
||
name = f.stem # 不含扩展名
|
||
if len(name) > 15:
|
||
name = name[:12] + "..."
|
||
item.setText(name)
|
||
item.setData(Qt.ItemDataRole.UserRole, str(f))
|
||
item.setToolTip(f.name) # 完整文件名显示在提示中
|
||
|
||
# Try to load thumbnail
|
||
try:
|
||
pixmap = QPixmap(str(f))
|
||
if not pixmap.isNull():
|
||
# 缩放到合适大小显示缩略图
|
||
scaled = pixmap.scaled(
|
||
150, 100,
|
||
Qt.AspectRatioMode.KeepAspectRatio,
|
||
Qt.TransformationMode.SmoothTransformation
|
||
)
|
||
item.setIcon(QIcon(scaled))
|
||
except Exception as e:
|
||
print(f"加载缩略图失败: {f.name}, {e}")
|
||
|
||
self.screenshot_list.addItem(item)
|
||
|
||
def _take_screenshot(self):
|
||
"""Execute screenshot"""
|
||
if self.account_combo.currentIndex() < 0:
|
||
QMessageBox.information(self, "提示", "请先选择一个账号")
|
||
return
|
||
|
||
account = self.account_combo.currentData()
|
||
if not account:
|
||
return
|
||
|
||
browse_type = self.browse_type_combo.currentText()
|
||
password = decrypt_password(account.password) if is_encrypted(account.password) else account.password
|
||
|
||
self.screenshot_btn.setEnabled(False)
|
||
self.screenshot_btn.setText("截图中...")
|
||
self.log_signal.emit(f"开始截图: {account.username}")
|
||
|
||
from utils.worker import Worker
|
||
|
||
def do_screenshot(_signals=None, _should_stop=None):
|
||
from core.screenshot import take_screenshot
|
||
from config import get_config as _get_config
|
||
|
||
cfg = _get_config()
|
||
proxy_config = None
|
||
if cfg.proxy.enabled and cfg.proxy.server:
|
||
proxy_config = {"server": cfg.proxy.server}
|
||
|
||
result = take_screenshot(
|
||
account.username,
|
||
password,
|
||
browse_type,
|
||
remark=account.remark,
|
||
log_callback=lambda msg: _signals.log.emit(msg),
|
||
proxy_config=proxy_config
|
||
)
|
||
return result
|
||
|
||
def on_result(result):
|
||
self.screenshot_btn.setEnabled(True)
|
||
self.screenshot_btn.setText("📷 执行截图")
|
||
if result and result.success:
|
||
self.log_signal.emit(f"[OK] 截图成功: {result.filename}")
|
||
self._refresh_screenshots()
|
||
else:
|
||
error = result.error_message if result else "未知错误"
|
||
self.log_signal.emit(f"[FAIL] 截图失败: {error}")
|
||
|
||
def on_error(error):
|
||
self.screenshot_btn.setEnabled(True)
|
||
self.screenshot_btn.setText("📷 执行截图")
|
||
self.log_signal.emit(f"截图出错: {error}")
|
||
|
||
self._worker = Worker(do_screenshot)
|
||
self._worker.signals.log.connect(self.log_signal.emit)
|
||
self._worker.signals.result.connect(on_result)
|
||
self._worker.signals.error.connect(on_error)
|
||
self._worker.start()
|
||
|
||
def _preview_screenshot(self, item):
|
||
"""Preview screenshot"""
|
||
filepath = item.data(Qt.ItemDataRole.UserRole)
|
||
if filepath and os.path.exists(filepath):
|
||
if sys.platform == "win32":
|
||
os.startfile(filepath)
|
||
elif sys.platform == "darwin":
|
||
subprocess.run(["open", filepath])
|
||
else:
|
||
subprocess.run(["xdg-open", filepath])
|
||
|
||
def _preview_selected(self):
|
||
"""Preview selected screenshot"""
|
||
current = self.screenshot_list.currentItem()
|
||
if current:
|
||
self._preview_screenshot(current)
|
||
else:
|
||
QMessageBox.information(self, "提示", "请先选择一个截图")
|
||
|
||
def _delete_selected(self):
|
||
"""Delete selected screenshot"""
|
||
current = self.screenshot_list.currentItem()
|
||
if not current:
|
||
QMessageBox.information(self, "提示", "请先选择一个截图")
|
||
return
|
||
|
||
filepath = current.data(Qt.ItemDataRole.UserRole)
|
||
filename = current.text()
|
||
|
||
reply = QMessageBox.question(
|
||
self, "确认删除",
|
||
f"确定要删除截图 {filename} 吗?",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||
)
|
||
|
||
if reply == QMessageBox.StandardButton.Yes:
|
||
try:
|
||
if os.path.exists(filepath):
|
||
os.remove(filepath)
|
||
self._refresh_screenshots()
|
||
self.log_signal.emit(f"已删除截图: {filename}")
|
||
except Exception as e:
|
||
QMessageBox.warning(self, "错误", f"删除失败: {e}")
|
||
|
||
def _delete_all(self):
|
||
"""Delete all screenshots"""
|
||
count = self.screenshot_list.count()
|
||
if count == 0:
|
||
QMessageBox.information(self, "提示", "没有截图可删除")
|
||
return
|
||
|
||
reply = QMessageBox.warning(
|
||
self, "⚠️ 确认删除全部",
|
||
f"确定要删除全部 {count} 张截图吗?\n\n此操作不可恢复!",
|
||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||
QMessageBox.StandardButton.No
|
||
)
|
||
|
||
if reply == QMessageBox.StandardButton.Yes:
|
||
deleted = 0
|
||
failed = 0
|
||
for i in range(count):
|
||
item = self.screenshot_list.item(i)
|
||
filepath = item.data(Qt.ItemDataRole.UserRole)
|
||
try:
|
||
if os.path.exists(filepath):
|
||
os.remove(filepath)
|
||
deleted += 1
|
||
except Exception:
|
||
failed += 1
|
||
|
||
self._refresh_screenshots()
|
||
msg = f"已删除 {deleted} 张截图"
|
||
if failed > 0:
|
||
msg += f",{failed} 张删除失败"
|
||
self.log_signal.emit(msg)
|
||
|
||
def _open_screenshot_dir(self):
|
||
"""Open screenshot directory"""
|
||
SCREENSHOTS_DIR.mkdir(exist_ok=True)
|
||
if sys.platform == "win32":
|
||
os.startfile(str(SCREENSHOTS_DIR))
|
||
elif sys.platform == "darwin":
|
||
subprocess.run(["open", str(SCREENSHOTS_DIR)])
|
||
else:
|
||
subprocess.run(["xdg-open", str(SCREENSHOTS_DIR)])
|