- 简化项目定位:从智能工具转为极简截图上传工具 - 移除重型依赖: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>
540 lines
18 KiB
Python
540 lines
18 KiB
Python
"""
|
||
简化的主窗口
|
||
极简 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()
|