Files
cutThenThink/src/gui/main_window.py
congsh e853161975 refactor: 重构为极简截图上传工具
- 简化项目定位:从智能工具转为极简截图上传工具
- 移除重型依赖:torch、transformers、paddleocr、SQLAlchemy
- 新增轻量级核心模块:
  - config.py: 简化 YAML 配置管理
  - database.py: 原生 SQLite 存储
  - screenshot.py: 截图功能(全屏/区域)
  - uploader.py: 云端上传(支持 custom/telegraph/imgur)
  - plugins/ocr.py: 可选 RapidOCR 插件
- 重写主窗口:专注核心功能,移除复杂 UI
- 更新依赖:核心 ~50MB,OCR 可选
- 更新文档:新的 README 和需求分析 v2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:50:51 +08:00

540 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
简化的主窗口
极简 GUI专注核心功能截图、上传、浏览
"""
import os
from pathlib import Path
from datetime import datetime
from typing import List, Optional
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QListWidget, QListWidgetItem, QLabel, QStackedWidget,
QFileDialog, QMessageBox, QStatusBar, QToolBar,
QStyle, QLineEdit, QComboBox, QDialog, QDialogButtonBox,
QTextEdit, QCheckBox, QSpinBox, QGroupBox, QFormLayout
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QSize
from PyQt6.QtGui import QPixmap, QIcon, QAction, QKeySequence, QShortcut
from src.config import get_config
from src.core.database import Database, Record, get_db
from src.core.screenshot import Screenshot
from src.core.uploader import upload_file, UploadResult
from src.plugins.ocr import get_ocr_plugin
class UploadThread(QThread):
"""上传线程"""
finished = pyqtSignal(bool, str, str) # success, url, error
def __init__(self, filepath: str, provider: str, config: dict):
super().__init__()
self.filepath = filepath
self.provider = provider
self.config = config
def run(self):
"""执行上传"""
try:
result = upload_file(self.filepath, self.provider, self.config)
self.finished(result.success, result.url or '', result.error or '')
except Exception as e:
self.finished(False, '', str(e))
class OCRThread(QThread):
"""OCR 线程"""
finished = pyqtSignal(bool, str, str) # success, text, error
def __init__(self, filepath: str, ocr_plugin):
super().__init__()
self.filepath = filepath
self.ocr_plugin = ocr_plugin
def run(self):
"""执行 OCR"""
success, text, error = self.ocr_plugin.recognize(self.filepath)
self.finished(success, text or '', error or '')
class SettingsDialog(QDialog):
"""设置对话框"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("设置")
self.setMinimumWidth(500)
self.config = get_config().app
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# 上传设置
upload_group = QGroupBox("上传设置")
upload_layout = QFormLayout()
self.provider_combo = QComboBox()
self.provider_combo.addItems(["custom", "telegraph", "imgur"])
self.provider_combo.setCurrentText(self.config.upload.provider)
self.endpoint_edit = QLineEdit(self.config.upload.endpoint)
self.endpoint_edit.setPlaceholderText("https://your-server.com/upload")
self.api_key_edit = QLineEdit(self.config.upload.api_key)
self.api_key_edit.setPlaceholderText("可选的 API Key")
self.auto_copy_check = QCheckBox("上传后自动复制链接")
self.auto_copy_check.setChecked(self.config.upload.auto_copy)
upload_layout.addRow("上传方式:", self.provider_combo)
upload_layout.addRow("上传端点:", self.endpoint_edit)
upload_layout.addRow("API Key:", self.api_key_edit)
upload_layout.addRow("", self.auto_copy_check)
upload_group.setLayout(upload_layout)
# 截图设置
shot_group = QGroupBox("截图设置")
shot_layout = QFormLayout()
self.format_combo = QComboBox()
self.format_combo.addItems(["png", "jpg", "webp"])
self.format_combo.setCurrentText(self.config.screenshot.format)
self.path_edit = QLineEdit(self.config.screenshot.save_path)
self.path_edit.setPlaceholderText("~/Pictures/Screenshots")
shot_layout.addRow("保存格式:", self.format_combo)
shot_layout.addRow("保存路径:", self.path_edit)
shot_group.setLayout(shot_layout)
# OCR 设置
ocr_group = QGroupBox("OCR 设置(可选)")
ocr_layout = QFormLayout()
self.ocr_enabled_check = QCheckBox("启用 OCR")
self.ocr_enabled_check.setChecked(self.config.ocr.enabled)
self.ocr_auto_copy_check = QCheckBox("识别后自动复制文本")
self.ocr_auto_copy_check.setChecked(self.config.ocr.auto_copy)
ocr_layout.addRow("", self.ocr_enabled_check)
ocr_layout.addRow("", self.ocr_auto_copy_check)
ocr_group.setLayout(ocr_layout)
layout.addWidget(upload_group)
layout.addWidget(shot_group)
layout.addWidget(ocr_group)
# 按钮
buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
def get_config(self) -> dict:
"""获取配置"""
return {
'upload': {
'provider': self.provider_combo.currentText(),
'endpoint': self.endpoint_edit.text(),
'api_key': self.api_key_edit.text(),
'auto_copy': self.auto_copy_check.isChecked(),
},
'screenshot': {
'format': self.format_combo.currentText(),
'save_path': self.path_edit.text(),
},
'ocr': {
'enabled': self.ocr_enabled_check.isChecked(),
'auto_copy': self.ocr_auto_copy_check.isChecked(),
}
}
class MainWindow(QMainWindow):
"""主窗口"""
def __init__(self):
super().__init__()
self.config = get_config()
self.db = get_db()
self.screenshot = Screenshot(
self.config.app.screenshot.save_path,
self.config.app.screenshot.format
)
self.ocr_plugin = get_ocr_plugin()
self.current_records: List[Record] = []
self.upload_thread: Optional[UploadThread] = None
self.ocr_thread: Optional[OCRThread] = None
self._setup_ui()
self._setup_shortcuts()
self._load_records()
def _setup_ui(self):
"""设置 UI"""
self.setWindowTitle("CutThenThink")
self.setMinimumSize(900, 600)
# 创建中心部件
central = QWidget()
self.setCentralWidget(central)
layout = QHBoxLayout(central)
# 侧边栏
sidebar = self._create_sidebar()
layout.addWidget(sidebar, 1)
# 主内容区
self.content_stack = QStackedWidget()
# 记录列表页
self.records_page = self._create_records_page()
self.content_stack.addWidget(self.records_page)
layout.addWidget(self.content_stack, 3)
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("就绪")
# 工具栏
toolbar = QToolBar("主工具栏")
toolbar.setMovable(False)
self.addToolBar(toolbar)
# 截图按钮
capture_action = QAction("截全屏", self)
capture_action.setShortcut(QKeySequence(self.config.app.hotkeys.capture))
capture_action.triggered.connect(self.capture_fullscreen)
toolbar.addAction(capture_action)
# 区域截图按钮
region_action = QAction("区域截图", self)
region_action.setShortcut(QKeySequence(self.config.app.hotkeys.region))
region_action.triggered.connect(self.capture_region)
toolbar.addAction(region_action)
# 上传按钮
upload_action = QAction("上传最后截图", self)
upload_action.setShortcut(QKeySequence(self.config.app.hotkeys.upload))
upload_action.triggered.connect(self.upload_last)
toolbar.addAction(upload_action)
# 设置按钮
settings_action = QAction("设置", self)
settings_action.triggered.connect(self.show_settings)
toolbar.addAction(settings_action)
def _create_sidebar(self) -> QWidget:
"""创建侧边栏"""
sidebar = QWidget()
layout = QVBoxLayout(sidebar)
# 全部记录
all_btn = QPushButton("全部记录")
all_btn.clicked.connect(lambda: self._filter_records(None))
layout.addWidget(all_btn)
layout.addStretch()
return sidebar
def _create_records_page(self) -> QWidget:
"""创建记录列表页"""
page = QWidget()
layout = QVBoxLayout(page)
# 记录列表
self.records_list = QListWidget()
self.records_list.setIconSize(QSize(48, 48))
self.records_list.itemDoubleClicked.connect(self._open_record_detail)
layout.addWidget(self.records_list)
# 操作按钮
btn_layout = QHBoxLayout()
refresh_btn = QPushButton("刷新")
refresh_btn.clicked.connect(self._load_records)
btn_layout.addWidget(refresh_btn)
upload_btn = QPushButton("上传选中")
upload_btn.clicked.connect(self._upload_selected)
btn_layout.addWidget(upload_btn)
delete_btn = QPushButton("删除")
delete_btn.clicked.connect(self._delete_selected)
btn_layout.addWidget(delete_btn)
layout.addLayout(btn_layout)
return page
def _setup_shortcuts(self):
"""设置快捷键"""
# ESC 退出
esc_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self)
esc_shortcut.activated.connect(self.close)
def _load_records(self, category: Optional[str] = None):
"""加载记录"""
self.current_records = self.db.get_all(category=category)
self._update_records_list()
def _update_records_list(self):
"""更新记录列表"""
self.records_list.clear()
for record in self.current_records:
item = QListWidgetItem(record.filename)
# 设置图标(缩略图)
if Path(record.filepath).exists():
pixmap = QPixmap(record.filepath).scaled(
48, 48, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
item.setIcon(QIcon(pixmap))
# 添加上传状态标记
if record.upload_url:
item.setText(f"{record.filename}")
else:
item.setText(f" {record.filename}")
item.setData(Qt.UserRole, record)
self.records_list.addItem(item)
self.status_bar.showMessage(f"{len(self.current_records)} 条记录")
def _filter_records(self, category: Optional[str]):
"""筛选记录"""
self._load_records(category)
def capture_fullscreen(self):
"""全屏截图"""
filepath, error = self.screenshot.capture_fullscreen()
self._handle_screenshot_result(filepath, error)
def capture_region(self):
"""区域截图"""
pixmap, filepath = self.screenshot.capture_region()
if pixmap and filepath:
self._handle_screenshot_result(filepath, None)
else:
self.status_bar.showMessage("截图已取消")
def _handle_screenshot_result(self, filepath: Optional[str], error: Optional[str]):
"""处理截图结果"""
if error:
QMessageBox.warning(self, "截图失败", error)
return
if not filepath:
return
self.status_bar.showMessage(f"截图已保存: {filepath}")
# 保存到数据库
file_size = os.path.getsize(filepath)
record = Record(
id=0, # 临时 ID
filename=Path(filepath).name,
filepath=filepath,
file_size=file_size
)
record_id = self.db.add(record)
# 如果启用了 OCR执行识别
if self.config.app.ocr.enabled and self.ocr_plugin.is_available():
self._run_ocr(filepath, record_id)
else:
self._load_records()
def _run_ocr(self, filepath: str, record_id: int):
"""运行 OCR"""
self.status_bar.showMessage("正在识别文字...")
self.ocr_thread = OCRThread(filepath, self.ocr_plugin)
self.ocr_thread.finished.connect(
lambda success, text, error: self._handle_ocr_result(record_id, success, text, error)
)
self.ocr_thread.start()
def _handle_ocr_result(self, record_id: int, success: bool, text: str, error: str):
"""处理 OCR 结果"""
if success:
self.db.update(record_id, ocr_text=text)
self.status_bar.showMessage("OCR 识别完成")
if self.config.app.ocr.auto_copy:
QApplication.clipboard().setText(text)
self.status_bar.showMessage("已复制 OCR 文本")
else:
self.status_bar.showMessage(f"OCR 失败: {error}")
self._load_records()
def _upload_selected(self):
"""上传选中的记录"""
current_item = self.records_list.currentItem()
if not current_item:
return
record = current_item.data(Qt.UserRole)
self._upload_record(record)
def _upload_record(self, record: Record):
"""上传单条记录"""
self.status_bar.showMessage(f"正在上传: {record.filename}")
self.upload_thread = UploadThread(
record.filepath,
self.config.app.upload.provider,
{
'endpoint': self.config.app.upload.endpoint,
'api_key': self.config.app.upload.api_key,
}
)
self.upload_thread.finished.connect(
lambda success, url, error: self._handle_upload_result(record.id, success, url, error)
)
self.upload_thread.start()
def upload_last(self):
"""上传最后的截图"""
if not self.current_records:
QMessageBox.info(self, "提示", "没有可上传的记录")
return
# 上传最新的未上传记录
for record in self.current_records:
if not record.upload_url:
self._upload_record(record)
break
def _handle_upload_result(self, record_id: int, success: bool, url: str, error: str):
"""处理上传结果"""
if success:
self.db.update(record_id, upload_url=url, uploaded_at=datetime.now().isoformat())
self.status_bar.showMessage(f"上传成功: {url}")
if self.config.app.upload.auto_copy:
QApplication.clipboard().setText(url)
self.status_bar.showMessage("已复制上传链接")
else:
QMessageBox.critical(self, "上传失败", error)
self.status_bar.showMessage("上传失败")
self._load_records()
def _delete_selected(self):
"""删除选中的记录"""
current_item = self.records_list.currentItem()
if not current_item:
return
record = current_item.data(Qt.UserRole)
reply = QMessageBox.question(
self, "确认删除",
f"确定要删除 \"{record.filename}\" 吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.db.delete(record.id)
self._load_records()
def _open_record_detail(self, item: QListWidgetItem):
"""打开记录详情"""
record = item.data(Qt.UserRole)
dialog = QDialog(self)
dialog.setWindowTitle(record.filename)
dialog.setMinimumSize(500, 400)
layout = QVBoxLayout(dialog)
# 图片预览
if Path(record.filepath).exists():
pixmap = QPixmap(record.filepath)
label = QLabel()
label.setPixmap(pixmap.scaled(
450, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation
))
layout.addWidget(label)
# OCR 文本
if record.ocr_text:
text_edit = QTextEdit()
text_edit.setPlainText(record.ocr_text)
text_edit.setReadOnly(True)
layout.addWidget(QLabel("识别文字:"))
layout.addWidget(text_edit)
# 上传链接
if record.upload_url:
url_layout = QHBoxLayout()
url_edit = QLineEdit(record.upload_url)
url_edit.setReadOnly(True)
copy_btn = QPushButton("复制")
copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(record.upload_url))
url_layout.addWidget(url_edit)
url_layout.addWidget(copy_btn)
layout.addWidget(QLabel("上传链接:"))
layout.addLayout(url_layout)
dialog.exec()
def show_settings(self):
"""显示设置对话框"""
dialog = SettingsDialog(self)
if dialog.exec() == QDialog.Accepted:
config_data = dialog.get_config()
# 更新配置
self.config.app.upload.provider = config_data['upload']['provider']
self.config.app.upload.endpoint = config_data['upload']['endpoint']
self.config.app.upload.api_key = config_data['upload']['api_key']
self.config.app.upload.auto_copy = config_data['upload']['auto_copy']
self.config.app.screenshot.format = config_data['screenshot']['format']
self.config.app.screenshot.save_path = config_data['screenshot']['save_path']
self.config.app.ocr.enabled = config_data['ocr']['enabled']
self.config.app.ocr.auto_copy = config_data['ocr']['auto_copy']
# 保存配置
self.config.save()
# 更新截图器
self.screenshot = Screenshot(
self.config.app.screenshot.save_path,
self.config.app.screenshot.format
)
QMessageBox.information(self, "设置", "设置已保存,部分设置需要重启生效")
def closeEvent(self, event):
"""关闭事件"""
self.db.close()
event.accept()
def main():
"""主函数"""
import sys
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()