T-567 增加封面候选画廊
This commit is contained in:
+350
-37
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user