T-567 增加封面候选画廊

This commit is contained in:
chengma
2026-07-09 11:32:08 +08:00
parent 71e51d0608
commit a391d2db99
4 changed files with 573 additions and 42 deletions
+349 -36
View File
@@ -10,6 +10,343 @@ from ..workers import GenerateWorker as _RealGenerateWorker
def GenerateWorker(*args, **kwargs):
return _call_package_attr("GenerateWorker", _RealGenerateWorker, *args, **kwargs)
class _CoverThumbnailLabel(QLabel):
def __init__(self, image_path, open_callback, parent=None):
super().__init__(parent)
self.image_path = image_path
self.open_callback = open_callback
self.setCursor(Qt.PointingHandCursor)
def mouseDoubleClickEvent(self, event):
if self.open_callback is not None:
self.open_callback(self.image_path)
event.accept()
return
super().mouseDoubleClickEvent(event)
class OriginalImageDialog(QDialog):
def __init__(self, image_path, parent=None):
super().__init__(parent)
self.image_path = image_path
self.setWindowTitle(os.path.basename(str(image_path or "")) or "原图")
layout = QVBoxLayout(self)
image = QImage(str(image_path))
scroll = QScrollArea()
scroll.setWidgetResizable(False)
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
if image.isNull():
image_label.setText("图片读取失败")
else:
self.setWindowTitle(
f"{os.path.basename(str(image_path))} · {image.width()}x{image.height()}"
)
pixmap = QPixmap.fromImage(image)
image_label.setPixmap(pixmap)
image_label.resize(pixmap.size())
scroll.setWidget(image_label)
layout.addWidget(scroll, 1)
button_layout = QHBoxLayout()
button_layout.addStretch(1)
close_button = QPushButton("关闭")
close_button.clicked.connect(self.reject)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
self._fit_to_screen(image)
def _fit_to_screen(self, image):
available = _available_geometry()
if available is None:
self.resize(720, 520)
return
max_width = max(360, int(available.width() * 0.9))
max_height = max(300, int(available.height() * 0.9))
image_width = image.width() if not image.isNull() else 640
image_height = image.height() if not image.isNull() else 480
self.resize(min(max_width, image_width + 48), min(max_height, image_height + 92))
_center_dialog(self, available)
class CoverGalleryDialog(QDialog):
THUMBNAIL_SIZE = 180
OLD_THUMBNAIL_SIZE = 300
def __init__(self, task, image_root, db_path, account=None, parent=None):
super().__init__(parent)
self.task = task
self.image_root = image_root
self.db_path = db_path
self.account = account
self.current_path = _normalize_file_path(getattr(task, "new_cover_path", None))
self.selected_path = None
self.changed = False
self.candidate_buttons = {}
self.button_group = QButtonGroup(self)
self.button_group.setExclusive(True)
self.candidates = image_paths.list_task_cover_candidates(image_root, task, account=account)
self.setWindowTitle(f"封面画廊:{getattr(task, 'item_id', '')}")
layout = QVBoxLayout(self)
body_layout = QHBoxLayout()
body_layout.addWidget(self._old_cover_panel(), 0)
body_layout.addWidget(self._candidate_panel(), 1)
layout.addLayout(body_layout, 1)
button_layout = QHBoxLayout()
button_layout.addStretch(1)
self.save_button = QPushButton("保存")
self.cancel_button = QPushButton("取消")
self.save_button.clicked.connect(self.save_selection)
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self._sync_initial_selection()
self._update_status()
self._fit_to_screen()
def _old_cover_panel(self):
panel = QWidget()
layout = QVBoxLayout(panel)
layout.addWidget(QLabel("旧封面"))
old_path = getattr(self.task, "old_cover_path", None)
layout.addWidget(
_build_image_label(
old_path,
self.OLD_THUMBNAIL_SIZE,
empty_text="暂无旧封面",
)
)
layout.addStretch(1)
return panel
def _candidate_panel(self):
panel = QWidget()
layout = QVBoxLayout(panel)
layout.addWidget(QLabel("生成封面候选"))
self.status_label = QLabel()
self.status_label.setObjectName("coverGalleryStatusLabel")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
scroll = QScrollArea()
scroll.setObjectName("coverGalleryScrollArea")
scroll.setWidgetResizable(False)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
content = QWidget()
self.candidate_layout = QHBoxLayout(content)
self.candidate_layout.setContentsMargins(0, 0, 0, 0)
self.candidate_layout.setSpacing(12)
if self.candidates:
for index, candidate_path in enumerate(self.candidates):
self.candidate_layout.addWidget(self._candidate_item(candidate_path, index))
self.candidate_layout.addStretch(1)
else:
empty_label = QLabel("暂无生成封面图片")
empty_label.setObjectName("coverGalleryEmptyLabel")
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
empty_label.setAlignment(Qt.AlignCenter)
self.candidate_layout.addWidget(empty_label)
scroll.setWidget(content)
layout.addWidget(scroll, 1)
return panel
def _candidate_item(self, candidate_path, index):
item = QWidget()
item.setFixedWidth(self.THUMBNAIL_SIZE + 16)
layout = QVBoxLayout(item)
layout.setContentsMargins(0, 0, 0, 0)
thumbnail = _CoverThumbnailLabel(candidate_path, self.open_original_image)
thumbnail.setObjectName("coverCandidateThumbnail")
thumbnail.setAlignment(Qt.AlignCenter)
image = _load_image(candidate_path)
if image.isNull():
thumbnail.setText("图片读取失败")
thumbnail.setMinimumSize(self.THUMBNAIL_SIZE, self.THUMBNAIL_SIZE)
else:
thumbnail.setPixmap(
QPixmap.fromImage(
image.scaled(
self.THUMBNAIL_SIZE,
self.THUMBNAIL_SIZE,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
)
thumbnail.setMinimumSize(self.THUMBNAIL_SIZE, self.THUMBNAIL_SIZE)
radio = QRadioButton(_candidate_label(candidate_path, self.current_path))
radio.setObjectName(f"coverCandidateRadio{index}")
radio.toggled.connect(
lambda checked, path=candidate_path: self._select_candidate(path) if checked else None
)
self.button_group.addButton(radio, index)
self.candidate_buttons[candidate_path] = radio
meta = QLabel(_candidate_meta(candidate_path, self.current_path))
meta.setWordWrap(True)
meta.setObjectName("coverCandidateMetaLabel")
layout.addWidget(thumbnail)
layout.addWidget(radio)
layout.addWidget(meta)
layout.addStretch(1)
return item
def _sync_initial_selection(self):
for candidate_path, radio in self.candidate_buttons.items():
if _same_file(candidate_path, self.current_path):
radio.setChecked(True)
self.selected_path = candidate_path
break
self.save_button.setEnabled(self.selected_path is not None)
def _select_candidate(self, candidate_path):
self.selected_path = candidate_path
self.save_button.setEnabled(True)
self._update_status()
def _update_status(self):
if not self.candidates:
self.status_label.setText("暂无生成封面图片")
self.save_button.setEnabled(False)
return
if self.current_path and not os.path.exists(self.current_path):
self.status_label.setText("当前生效封面文件不存在,请选择一张后保存")
return
if self.current_path and self.selected_path is None:
self.status_label.setText("当前生效封面不在候选列表,请选择一张后保存")
return
if self.selected_path is None:
self.status_label.setText("请选择一张生成封面后保存")
return
if _same_file(self.selected_path, self.current_path):
self.status_label.setText("当前生效封面已选中")
return
self.status_label.setText("已选择新的生成封面,保存后用于后续更新蝦皮")
def save_selection(self, checked=False):
if not self.selected_path:
QMessageBox.warning(self, "封面画廊", "请先选择一张生成封面")
return False
if _same_file(self.selected_path, self.current_path):
self.changed = False
self.accept()
return True
if self._needs_committed_confirmation() and not self._confirm_committed_save():
return False
try:
db.update_generated_cover(self.task.id, self.selected_path, path=self.db_path)
except Exception as exc:
QMessageBox.warning(self, "封面画廊", f"保存当前封面失败:{exc}")
return False
self.current_path = _normalize_file_path(self.selected_path)
self.changed = True
self.accept()
return True
def _needs_committed_confirmation(self):
return int(getattr(self.task, "committed", 0) or 0) == 1 or getattr(self.task, "stage", None) == "applied"
def _confirm_committed_save(self):
box = QMessageBox(self)
box.setWindowTitle("确认保存封面")
box.setText(
"该商品已经提交过线上。本地换封面不会回滚蝦皮,重复更新会再次提交线上。"
)
save_button = box.addButton("确认保存", QMessageBox.AcceptRole)
cancel_button = box.addButton("取消", QMessageBox.RejectRole)
box.setDefaultButton(cancel_button)
box.exec()
return box.clickedButton() is save_button
def open_original_image(self, image_path):
dialog = OriginalImageDialog(image_path, self)
dialog.exec()
def _fit_to_screen(self):
available = _available_geometry()
if available is None:
self.resize(960, 580)
return
max_width = max(520, int(available.width() * 0.9))
max_height = max(420, int(available.height() * 0.9))
self.resize(min(960, max_width), min(580, max_height))
_center_dialog(self, available)
def _load_image(image_path):
return QImage(str(image_path or ""))
def _build_image_label(image_path, size, empty_text):
label = QLabel()
label.setAlignment(Qt.AlignCenter)
label.setMinimumSize(size, size)
image = _load_image(image_path) if image_path else QImage()
if image.isNull():
label.setText(empty_text)
return label
label.setPixmap(
QPixmap.fromImage(
image.scaled(
size,
size,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
)
return label
def _candidate_label(candidate_path, current_path):
return "当前生效" if _same_file(candidate_path, current_path) else os.path.basename(candidate_path)
def _candidate_meta(candidate_path, current_path):
if _same_file(candidate_path, current_path):
return "当前生效封面"
stem, _ext = os.path.splitext(os.path.basename(candidate_path))
parts = stem.rsplit("_", 2)
if len(parts) >= 2 and len(parts[-1]) == 14 and parts[-1].isdigit():
return _format_archive_timestamp(parts[-1])
if len(parts) >= 3 and len(parts[-2]) == 14 and parts[-2].isdigit() and parts[-1].isdigit():
return f"{_format_archive_timestamp(parts[-2])} · 同秒第 {parts[-1]} 张"
if stem.endswith("_new"):
return "当前输出槽"
return "历史候选"
def _format_archive_timestamp(value):
return f"{value[0:4]}-{value[4:6]}-{value[6:8]} {value[8:10]}:{value[10:12]}:{value[12:14]}"
def _normalize_file_path(path):
value = str(path or "").strip()
return os.path.abspath(value) if value else None
def _same_file(left, right):
if not left or not right:
return False
return os.path.normcase(os.path.abspath(str(left))) == os.path.normcase(os.path.abspath(str(right)))
def _available_geometry():
app = QApplication.instance()
screen = app.primaryScreen() if app is not None else None
return screen.availableGeometry() if screen is not None else None
def _center_dialog(dialog, available):
frame = dialog.frameGeometry()
frame.moveCenter(available.center())
dialog.move(frame.topLeft())
class GenerateTab(QWidget):
"""Tab 2: prompt area plus generation task filters/list."""
@@ -875,44 +1212,20 @@ class GenerateTab(QWidget):
if task is None:
self._set_status("没有可预览的任务")
return
dialog = QDialog(self)
dialog.setWindowTitle(f"封面对照:{task.item_id}")
layout = QVBoxLayout(dialog)
images_layout = QHBoxLayout()
images_layout.addWidget(self._image_panel("旧封面", task.old_cover_path))
images_layout.addWidget(self._image_panel("新封面", task.new_cover_path))
layout.addLayout(images_layout)
buttons = QDialogButtonBox(QDialogButtonBox.Close)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.resize(720, 420)
dialog.exec()
def _image_panel(self, title, path):
panel = QWidget()
layout = QVBoxLayout(panel)
layout.addWidget(QLabel(title))
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
image_label.setMinimumSize(260, 260)
image_label.setWordWrap(True)
if path and os.path.exists(str(path)):
pixmap = QPixmap(str(path))
if not pixmap.isNull():
image_label.setPixmap(
pixmap.scaled(
260,
260,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
account = self.model.account_by_alias.get(str(getattr(task, "alias", "") or "").strip())
dialog = CoverGalleryDialog(
task,
image_root=appconfig.image_dir(self.config),
db_path=self.db_path,
account=account,
parent=self,
)
if dialog.exec() == QDialog.Accepted:
self.refresh_tasks()
if dialog.changed:
self._set_status(f"已保存当前新封面:{task.item_id}")
else:
image_label.setText(f"图片无法读取\n{path}")
else:
image_label.setText(f"无图片\n{path or ''}".strip())
layout.addWidget(image_label, 1)
return panel
self._set_status(f"当前新封面未变更:{task.item_id}")
def _set_generate_running(self, running):
self.generate_button.setEnabled(not running)
+3 -1
View File
@@ -10,10 +10,11 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
try:
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPixmap
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QButtonGroup,
QCheckBox,
QComboBox,
QDialog,
@@ -32,6 +33,7 @@ try:
QPlainTextEdit,
QProgressBar,
QPushButton,
QRadioButton,
QScrollArea,
QSplitter,
QTableView,
+17 -2
View File
@@ -3,7 +3,7 @@ id: T-567
title: ②封面画廊基础:候选图查看、单选保存、原图弹窗
phase: 7
deps: [T-566]
status: TODO
status: DONE
created: 2026-07-09
---
@@ -81,4 +81,19 @@ T-566 已提供数据层能力:重置封面时归档旧图、`db.update_genera
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)
- 2026-07-09 完成 T-567。
- 修改 `app/gui/tabs/generate.py`:
- 将②任务列表双击封面查看入口重构为 `CoverGalleryDialog`,左侧显示旧封面参照,右侧横向滚动画廊显示该任务全部 `_new*.jpg` 候选。
- 画廊打开时调用 T-566 的 `image_paths.list_task_cover_candidates()` 现场扫描目录;默认选中 DB 当前 `new_cover_path`,当前路径缺失时不自动改库并提示选择后保存;无候选时显示空状态并禁用保存。
- 保存时未变更直接关闭;变更时调用 `db.update_generated_cover()`,保存成功后刷新②任务列表。已提交线上任务保存前弹中文二次确认,说明本地换封面不会回滚蝦皮、重复更新会再次提交线上。
- 缩略图和旧封面预览统一用 `QImage` 读入内存后转 `QPixmap`,不长期持有文件句柄;候选缩略图双击打开 `OriginalImageDialog` 原图弹窗,标题显示文件名与原始分辨率,超出屏幕时可滚动。
- 修改 `app/gui/widgets.py`:补充 `QImage`、`QButtonGroup`、`QRadioButton` 聚合导入,供画廊弹窗使用。
- 修改 `tests/test_gui.py`:
- 覆盖候选图展示和 `_old` 过滤、当前封面默认选中、打开画廊后候选文件可重命名、保存切换当前封面、已提交任务取消确认不写库、当前路径缺失/无候选提示和禁用保存、原图弹窗分辨率标题。
- 验证通过:
- `py -3.10 -m unittest tests.test_gui`
- `py -3.10 -m unittest tests.test_gui tests.test_db tests.test_image_paths`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`(290 tests;PySide6 字体目录警告不影响结果)
- `git diff --check`
+203 -2
View File
@@ -12,13 +12,13 @@ sys.path.insert(0, os.path.dirname(__file__))
from _helpers import TempDirMixin
from app import gui
from app import accounts, ai, appconfig, db, prompts, update_check
from app import accounts, ai, appconfig, db, image_paths, prompts, update_check
if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtCore import QItemSelectionModel, QRect
from PySide6.QtGui import QTextCursor
from PySide6.QtGui import QImage, QTextCursor
from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
from app.gui import (
@@ -39,6 +39,7 @@ from app.gui import (
TAB_TITLES,
WriteBackWorker,
)
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
from app.gui.main_window import _fit_and_center_window
@@ -81,6 +82,13 @@ class GuiTests(TempDirMixin, unittest.TestCase):
}
return cfg
def write_test_image(self, path, width=32, height=32):
os.makedirs(os.path.dirname(path), exist_ok=True)
image = QImage(width, height, QImage.Format_RGB32)
image.fill(0xFF336699)
self.assertTrue(image.save(path))
return path
def assert_foreground(self, model, row, column, color):
value = model.data(model.index(row, column), gui.Qt.ForegroundRole)
self.assertIsNotNone(value)
@@ -2068,6 +2076,199 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_cover_gallery_lists_candidates_and_selects_current_cover(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
old_cover = self.write_test_image(os.path.join(temp_dir, "old.jpg"))
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
prefix = os.path.splitext(canonical)[0]
archive_newer = self.write_test_image(f"{prefix}_20260709101000.jpg")
archive_older = self.write_test_image(f"{prefix}_20260709094800.jpg")
old_candidate = f"{prefix.replace('_new', '_old')}.jpg"
self.write_test_image(canonical)
self.write_test_image(old_candidate)
db.set_collected(task.id, "旧标题", old_cover, path=cfg["db_path"])
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
task = db.get_task(task.id, path=cfg["db_path"])
dialog = CoverGalleryDialog(task, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
self.assertEqual([canonical, archive_newer, archive_older], dialog.candidates)
self.assertTrue(dialog.candidate_buttons[canonical].isChecked())
self.assertEqual(canonical, dialog.selected_path)
self.assertNotIn(old_candidate, dialog.candidates)
renamed_archive = archive_newer.replace(".jpg", "_renamed.jpg")
os.rename(archive_newer, renamed_archive)
self.assertTrue(os.path.exists(renamed_archive))
self.assert_removed(temp_dir)
def test_cover_gallery_save_switches_current_cover(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
selected = self.write_test_image(os.path.splitext(canonical)[0] + "_20260709101000.jpg")
self.write_test_image(canonical)
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
task = db.get_task(task.id, path=cfg["db_path"])
dialog = CoverGalleryDialog(task, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
dialog.candidate_buttons[selected].setChecked(True)
self.assertTrue(dialog.save_selection())
updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual(os.path.abspath(selected), updated.new_cover_path)
self.assertEqual("generated", updated.stage)
self.assertEqual("pending", updated.status)
self.assertTrue(dialog.changed)
self.assert_removed(temp_dir)
def test_cover_gallery_committed_cancel_does_not_write_db(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
selected = self.write_test_image(os.path.splitext(canonical)[0] + "_20260709101000.jpg")
self.write_test_image(canonical)
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
db.set_applied(task.id, True, path=cfg["db_path"])
task = db.get_task(task.id, path=cfg["db_path"])
dialog = CoverGalleryDialog(task, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(dialog.close)
dialog.candidate_buttons[selected].setChecked(True)
message_box, boxes = self.make_fake_message_box("取消")
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box):
self.assertFalse(dialog.save_selection())
updated = db.get_task(task.id, path=cfg["db_path"])
self.assertEqual(canonical, updated.new_cover_path)
self.assertEqual("applied", updated.stage)
self.assertEqual(1, updated.committed)
self.assertIn("重复更新会再次提交线上", boxes[0].text)
self.assertFalse(dialog.changed)
self.assert_removed(temp_dir)
def test_cover_gallery_handles_missing_current_and_empty_candidates(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
},
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 3,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639511",
},
],
path=cfg["db_path"],
)
first, second = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
missing_current = os.path.join(temp_dir, "missing.jpg")
db.set_generated(first.id, "新标题A", missing_current, path=cfg["db_path"])
first_candidate = image_paths.task_image_path(cfg["image_dir"], first, account, "new")
self.write_test_image(first_candidate)
first = db.get_task(first.id, path=cfg["db_path"])
missing_dialog = CoverGalleryDialog(first, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(missing_dialog.close)
self.assertIsNone(missing_dialog.selected_path)
self.assertFalse(missing_dialog.save_button.isEnabled())
self.assertIn("当前生效封面文件不存在", missing_dialog.status_label.text())
db.set_generated(second.id, "新标题B", os.path.join(temp_dir, "none.jpg"), path=cfg["db_path"])
second = db.get_task(second.id, path=cfg["db_path"])
empty_dialog = CoverGalleryDialog(second, cfg["image_dir"], cfg["db_path"], account=account)
self.addCleanup(empty_dialog.close)
self.assertEqual([], empty_dialog.candidates)
self.assertFalse(empty_dialog.save_button.isEnabled())
self.assertIn("暂无生成封面图片", empty_dialog.status_label.text())
self.assert_removed(temp_dir)
def test_original_image_dialog_title_contains_resolution(self):
with self.make_temp_dir() as temp_dir:
image_path = self.write_test_image(os.path.join(temp_dir, "cover.jpg"), width=64, height=32)
dialog = OriginalImageDialog(image_path)
self.addCleanup(dialog.close)
self.assertIn("cover.jpg", dialog.windowTitle())
self.assertIn("64x32", dialog.windowTitle())
self.assert_removed(temp_dir)
def test_generate_tab_allows_editing_generated_title_locally(self):
with self.make_temp_dir() as temp_dir: