feat(ai-studio): add thumbnail workspace grids
This commit is contained in:
+306
-152
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QMimeData, QSize
|
||||
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
||||
from PySide6.QtCore import QObject, QMimeData, QSize, Signal
|
||||
from PySide6.QtWidgets import QListView, QListWidget, QListWidgetItem
|
||||
|
||||
from ... import accounts, appconfig, cmhub_models, db, image_studio, image_studio_export, prompts
|
||||
from ... import accounts, appconfig, cmhub_models, db, image_studio, image_studio_export, image_studio_images, prompts
|
||||
from .. import file_manager
|
||||
from ..widgets import *
|
||||
from ..workers import (
|
||||
@@ -83,24 +83,38 @@ def _mime_asset_id(mime_data):
|
||||
return None
|
||||
|
||||
|
||||
class ImageStudioPoolTable(QTableWidget):
|
||||
"""Photo pool table that can drag available asset IDs into final trays."""
|
||||
class _ThumbnailSignals(QObject):
|
||||
"""Deliver executor callbacks to the GUI thread without exposing remote URLs."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
loaded = Signal(object)
|
||||
failed = Signal(object)
|
||||
|
||||
|
||||
class ImageStudioThumbnailGrid(QListWidget):
|
||||
"""Stable icon grid for original images, pool assets and job cards."""
|
||||
|
||||
def __init__(self, *, draggable_assets=False, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(QAbstractItemView.DragOnly)
|
||||
self._draggable_assets = bool(draggable_assets)
|
||||
self.setViewMode(QListView.IconMode)
|
||||
self.setFlow(QListView.LeftToRight)
|
||||
self.setWrapping(True)
|
||||
self.setResizeMode(QListView.Adjust)
|
||||
self.setMovement(QListView.Static)
|
||||
self.setSpacing(6)
|
||||
self.setUniformItemSizes(True)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setDragEnabled(self._draggable_assets)
|
||||
self.setDragDropMode(
|
||||
QAbstractItemView.DragOnly if self._draggable_assets else QAbstractItemView.NoDragDrop
|
||||
)
|
||||
self.setDefaultDropAction(Qt.CopyAction)
|
||||
|
||||
def mimeData(self, items):
|
||||
mime = QMimeData()
|
||||
if not items:
|
||||
if not self._draggable_assets or not items:
|
||||
return mime
|
||||
row = items[0].row()
|
||||
item = self.item(row, 0)
|
||||
if item is None:
|
||||
return mime
|
||||
data = item.data(Qt.UserRole) or {}
|
||||
data = items[0].data(Qt.UserRole) or {}
|
||||
if data.get("type") != "asset" or not data.get("draggable"):
|
||||
return mime
|
||||
mime.setData(ASSET_MIME_TYPE, str(data.get("asset_id")).encode("utf-8"))
|
||||
@@ -239,8 +253,6 @@ class ImageStudioTab(QWidget):
|
||||
"""Sixth tab: project-based AI image studio."""
|
||||
|
||||
PROJECT_COLUMNS = ["店铺", "商品ID", "更新时间"]
|
||||
ORIGINAL_COLUMNS = ["序号", "状态", "远程地址"]
|
||||
POOL_COLUMNS = ["类型", "比例", "状态", "来源", "本地文件"]
|
||||
JOB_STATUS_LABELS = {
|
||||
"pending": "排队中",
|
||||
"submitted": "已提交",
|
||||
@@ -259,6 +271,7 @@ class ImageStudioTab(QWidget):
|
||||
config_path=None,
|
||||
status_callback=None,
|
||||
prompts_dir=None,
|
||||
thumbnail_loader=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("imageStudioTab")
|
||||
@@ -280,6 +293,14 @@ class ImageStudioTab(QWidget):
|
||||
self._operation_running = False
|
||||
self._worker_error_handled = False
|
||||
self._download_open_after = {}
|
||||
self._thumbnail_loader = thumbnail_loader or image_studio_images.ThumbnailLoader()
|
||||
self._thumbnail_pixmaps = {}
|
||||
self._thumbnail_pending = set()
|
||||
self._thumbnail_errors = set()
|
||||
self._thumbnail_project_id = None
|
||||
self._thumbnail_signals = _ThumbnailSignals(self)
|
||||
self._thumbnail_signals.loaded.connect(self._on_thumbnail_loaded)
|
||||
self._thumbnail_signals.failed.connect(self._on_thumbnail_failed)
|
||||
|
||||
self._build_ui()
|
||||
self._connect_signals()
|
||||
@@ -397,17 +418,14 @@ class ImageStudioTab(QWidget):
|
||||
original_header.addWidget(self.original_hint_label)
|
||||
layout.addLayout(original_header)
|
||||
|
||||
self.original_table = QTableWidget(0, len(self.ORIGINAL_COLUMNS))
|
||||
self.original_table.setObjectName("imageStudioOriginalTable")
|
||||
self.original_table.setHorizontalHeaderLabels(self.ORIGINAL_COLUMNS)
|
||||
self.original_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.original_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.original_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.original_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.original_table.verticalHeader().setVisible(False)
|
||||
self.original_table.setIconSize(QSize(72, 72))
|
||||
self.original_table.setAlternatingRowColors(False)
|
||||
layout.addWidget(self.original_table, 1)
|
||||
self.original_grid = ImageStudioThumbnailGrid(parent=self)
|
||||
self.original_grid.setObjectName("imageStudioOriginalGrid")
|
||||
self.original_grid.setIconSize(QSize(62, 62))
|
||||
self.original_grid.setGridSize(QSize(78, 92))
|
||||
self.original_grid.setMinimumHeight(174)
|
||||
self.original_grid.setMaximumHeight(202)
|
||||
self.original_grid.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
layout.addWidget(self.original_grid, 0)
|
||||
|
||||
pool_header = QHBoxLayout()
|
||||
pool_title = QLabel("照片池")
|
||||
@@ -419,19 +437,12 @@ class ImageStudioTab(QWidget):
|
||||
pool_header.addWidget(self.source_label)
|
||||
layout.addLayout(pool_header)
|
||||
|
||||
self.pool_table = ImageStudioPoolTable()
|
||||
self.pool_table.setColumnCount(len(self.POOL_COLUMNS))
|
||||
self.pool_table.setObjectName("imageStudioPoolTable")
|
||||
self.pool_table.setHorizontalHeaderLabels(self.POOL_COLUMNS)
|
||||
self.pool_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.pool_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.pool_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.pool_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.pool_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.pool_table.verticalHeader().setVisible(False)
|
||||
self.pool_table.setIconSize(QSize(86, 86))
|
||||
self.pool_table.setAlternatingRowColors(False)
|
||||
layout.addWidget(self.pool_table, 2)
|
||||
self.pool_grid = ImageStudioThumbnailGrid(draggable_assets=True, parent=self)
|
||||
self.pool_grid.setObjectName("imageStudioPoolGrid")
|
||||
self.pool_grid.setIconSize(QSize(86, 86))
|
||||
self.pool_grid.setGridSize(QSize(108, 124))
|
||||
self.pool_grid.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
layout.addWidget(self.pool_grid, 2)
|
||||
return panel
|
||||
|
||||
def _build_generation_panel(self):
|
||||
@@ -450,14 +461,11 @@ class ImageStudioTab(QWidget):
|
||||
self.source_preview_label.setObjectName("imageStudioSourcePreview")
|
||||
self.source_preview_label.setAlignment(Qt.AlignCenter)
|
||||
self.source_preview_label.setMinimumSize(86, 86)
|
||||
source_note = QLabel("当前源图\n单击照片池图片设为源图,双击查看大图")
|
||||
source_note.setObjectName("imageStudioMutedLabel")
|
||||
source_note.setWordWrap(True)
|
||||
source_preview_tooltip = "当前源图。请在照片池单击选择源图,双击可查看大图。"
|
||||
self.source_preview_label.setToolTip(source_preview_tooltip)
|
||||
source_row.addWidget(self.source_preview_label, 0)
|
||||
source_row.addWidget(source_note, 1)
|
||||
layout.addLayout(source_row)
|
||||
|
||||
template_layout = QGridLayout()
|
||||
template_layout = QVBoxLayout()
|
||||
template_layout.setSpacing(6)
|
||||
self.template_combo = QComboBox()
|
||||
self.template_combo.setObjectName("imageStudioTemplateCombo")
|
||||
@@ -471,25 +479,32 @@ class ImageStudioTab(QWidget):
|
||||
self.template_delete_button.setObjectName("imageStudioTemplateDeleteButton")
|
||||
template_label = QLabel("提示词模板")
|
||||
template_label.setObjectName("imageStudioMutedLabel")
|
||||
template_layout.addWidget(template_label, 0, 0, 1, 3)
|
||||
template_layout.addWidget(self.template_combo, 1, 0, 1, 3)
|
||||
template_layout.addWidget(self.template_new_button, 2, 0)
|
||||
template_layout.addWidget(self.template_rename_button, 2, 1)
|
||||
template_layout.addWidget(self.template_save_button, 2, 2)
|
||||
template_layout.addWidget(self.template_delete_button, 3, 2)
|
||||
layout.addLayout(template_layout)
|
||||
template_select_row = QHBoxLayout()
|
||||
template_select_row.addWidget(template_label)
|
||||
template_select_row.addWidget(self.template_combo, 1)
|
||||
template_actions = QHBoxLayout()
|
||||
for button in (
|
||||
self.template_new_button,
|
||||
self.template_rename_button,
|
||||
self.template_save_button,
|
||||
self.template_delete_button,
|
||||
):
|
||||
template_actions.addWidget(button, 1)
|
||||
template_layout.addLayout(template_select_row)
|
||||
template_layout.addLayout(template_actions)
|
||||
source_row.addLayout(template_layout, 1)
|
||||
layout.addLayout(source_row)
|
||||
|
||||
self.prompt_edit = QPlainTextEdit()
|
||||
self.prompt_edit.setObjectName("imageStudioPromptEdit")
|
||||
self.prompt_edit.setPlaceholderText("输入完整图片生成提示词")
|
||||
self.prompt_edit.setMinimumHeight(160)
|
||||
layout.addWidget(self.prompt_edit, 2)
|
||||
self.prompt_edit.setMinimumHeight(210)
|
||||
layout.addWidget(self.prompt_edit, 3)
|
||||
prompt_hint = QLabel("程序原样提交完整提示词,不自动拆分,也不自动追加动作词。")
|
||||
prompt_hint.setObjectName("imageStudioMutedLabel")
|
||||
prompt_hint.setWordWrap(True)
|
||||
layout.addWidget(prompt_hint)
|
||||
|
||||
form = QFormLayout()
|
||||
self.job_type_combo = QComboBox()
|
||||
self.job_type_combo.setObjectName("imageStudioJobTypeCombo")
|
||||
self.job_type_combo.addItem("主图", "main")
|
||||
@@ -502,10 +517,18 @@ class ImageStudioTab(QWidget):
|
||||
self.aspect_combo.setObjectName("imageStudioAspectCombo")
|
||||
for value in ("1:1", "3:4", "4:3", "9:16", "16:9"):
|
||||
self.aspect_combo.addItem(value, value)
|
||||
form.addRow("类型", self.job_type_combo)
|
||||
form.addRow("数量", self.count_spin)
|
||||
form.addRow("比例", self.aspect_combo)
|
||||
layout.addLayout(form)
|
||||
generation_form = QHBoxLayout()
|
||||
generation_form.setSpacing(8)
|
||||
for label, field in (
|
||||
("类型", self.job_type_combo),
|
||||
("数量", self.count_spin),
|
||||
("比例", self.aspect_combo),
|
||||
):
|
||||
group = QHBoxLayout()
|
||||
group.addWidget(QLabel(label))
|
||||
group.addWidget(field, 1)
|
||||
generation_form.addLayout(group, 1)
|
||||
layout.addLayout(generation_form)
|
||||
|
||||
self.billing_label = QLabel("cmhub 托管默认档:扣点以返回结果为准")
|
||||
self.billing_label.setObjectName("imageStudioBillingLabel")
|
||||
@@ -638,8 +661,8 @@ class ImageStudioTab(QWidget):
|
||||
#imageStudioDeleteProjectButton {
|
||||
color: #b42318;
|
||||
}
|
||||
QTableWidget#imageStudioOriginalTable,
|
||||
QTableWidget#imageStudioPoolTable,
|
||||
QListWidget#imageStudioOriginalGrid,
|
||||
QListWidget#imageStudioPoolGrid,
|
||||
QListWidget#imageStudioMainSelectionList,
|
||||
QListWidget#imageStudioDetailSelectionList {
|
||||
border: 1px solid #d8dee4;
|
||||
@@ -657,11 +680,12 @@ class ImageStudioTab(QWidget):
|
||||
self.open_folder_button.clicked.connect(self.open_project_folder)
|
||||
self.delete_project_button.clicked.connect(self.delete_current_project)
|
||||
self.project_table.itemSelectionChanged.connect(self._on_project_selection_changed)
|
||||
self.original_table.cellClicked.connect(self._on_original_clicked)
|
||||
self.original_table.cellDoubleClicked.connect(self._on_original_double_clicked)
|
||||
self.pool_table.cellClicked.connect(self._on_pool_clicked)
|
||||
self.pool_table.cellDoubleClicked.connect(self._on_pool_double_clicked)
|
||||
self.pool_table.customContextMenuRequested.connect(self._show_pool_context_menu)
|
||||
self.original_grid.itemClicked.connect(self._on_original_clicked)
|
||||
self.original_grid.itemDoubleClicked.connect(self._on_original_double_clicked)
|
||||
self.original_grid.customContextMenuRequested.connect(self._show_original_context_menu)
|
||||
self.pool_grid.itemClicked.connect(self._on_pool_clicked)
|
||||
self.pool_grid.itemDoubleClicked.connect(self._on_pool_double_clicked)
|
||||
self.pool_grid.customContextMenuRequested.connect(self._show_pool_context_menu)
|
||||
self.template_combo.currentIndexChanged.connect(self.load_selected_template)
|
||||
self.template_new_button.clicked.connect(self.create_template)
|
||||
self.template_rename_button.clicked.connect(self.rename_template)
|
||||
@@ -821,6 +845,9 @@ class ImageStudioTab(QWidget):
|
||||
return
|
||||
if project is None:
|
||||
return
|
||||
previous_project_id = getattr(self.current_project, "id", None)
|
||||
if previous_project_id != project.id:
|
||||
self._reset_thumbnail_view(project.id)
|
||||
self.current_project = project
|
||||
self._set_account_combo(project.account_alias)
|
||||
self.item_id_edit.setText(project.item_id)
|
||||
@@ -854,88 +881,77 @@ class ImageStudioTab(QWidget):
|
||||
self.jobs = self._list_project_jobs(self.current_project.id)
|
||||
self.selections = image_studio.list_selections(self.current_project.id, path=self.db_path)
|
||||
self._refresh_project_summary()
|
||||
self._fill_original_table()
|
||||
self._fill_pool_table()
|
||||
self._fill_original_grid()
|
||||
self._fill_pool_grid()
|
||||
self._refresh_selection_labels()
|
||||
self._refresh_source_label()
|
||||
|
||||
def _fill_original_table(self):
|
||||
originals = [asset for asset in self.assets if asset.kind == image_studio.ASSET_KIND_ORIGINAL]
|
||||
self.original_table.setRowCount(len(originals))
|
||||
def _fill_original_grid(self):
|
||||
originals = sorted(
|
||||
[asset for asset in self.assets if asset.kind == image_studio.ASSET_KIND_ORIGINAL],
|
||||
key=lambda asset: (int(asset.source_order or 0), int(asset.id)),
|
||||
)
|
||||
self.original_grid.clear()
|
||||
for row, asset in enumerate(originals):
|
||||
order = asset.source_order or row + 1
|
||||
values = [
|
||||
f"主图 #{order}",
|
||||
_asset_status_text(asset),
|
||||
asset.remote_url or "",
|
||||
]
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(str(value or ""))
|
||||
item.setData(Qt.UserRole, {"type": "asset", "asset_id": int(asset.id)})
|
||||
if column == 0:
|
||||
item.setIcon(_asset_icon(asset, "原"))
|
||||
item.setToolTip("单击下载并加入照片池,双击查看大图")
|
||||
self.original_table.setItem(row, column, item)
|
||||
self.original_table.setRowHeight(row, 84)
|
||||
if originals:
|
||||
self.original_table.setColumnWidth(0, 132)
|
||||
self.original_table.setColumnWidth(1, 96)
|
||||
self.original_table.resizeColumnsToContents()
|
||||
item = QListWidgetItem(f"主图 {order}\n{self._original_thumbnail_state(asset)}")
|
||||
item.setData(Qt.UserRole, {"type": "asset", "asset_id": int(asset.id)})
|
||||
item.setIcon(
|
||||
_asset_icon(
|
||||
asset,
|
||||
"原",
|
||||
size=QSize(62, 62),
|
||||
cached_pixmap=self._thumbnail_pixmaps.get(int(asset.id)),
|
||||
)
|
||||
)
|
||||
item.setSizeHint(QSize(78, 92))
|
||||
tooltip = "单击下载并加入照片池,双击查看大图。缩略图仅用于预览,不保存原图。"
|
||||
if int(asset.id) in self._thumbnail_errors:
|
||||
tooltip += "\n缩略图加载失败,可右键重新加载,或直接单击下载原图。"
|
||||
item.setToolTip(tooltip)
|
||||
self.original_grid.addItem(item)
|
||||
self._queue_original_thumbnails(originals)
|
||||
for row, asset in enumerate(originals):
|
||||
item = self.original_grid.item(row)
|
||||
if item is not None:
|
||||
order = asset.source_order or row + 1
|
||||
item.setText(f"主图 {order}\n{self._original_thumbnail_state(asset)}")
|
||||
|
||||
def _fill_pool_table(self):
|
||||
def _fill_pool_grid(self):
|
||||
rows = []
|
||||
for asset in self.assets:
|
||||
if asset.status == image_studio.ASSET_STATUS_MISSING:
|
||||
continue
|
||||
if asset.kind not in {"original", "generated_main", "generated_detail"}:
|
||||
if asset.kind not in {"original", "generated_main", "generated_detail"} or not _asset_is_usable(asset):
|
||||
continue
|
||||
rows.append(("asset", asset))
|
||||
for job in self.jobs:
|
||||
if job.status in {"pending", "submitted", "running", "failed", "expired", "cancelled"}:
|
||||
rows.append(("job", job))
|
||||
self.pool_table.setRowCount(len(rows))
|
||||
for row, (row_type, obj) in enumerate(rows):
|
||||
self.pool_grid.clear()
|
||||
for row_type, obj in rows:
|
||||
if row_type == "asset":
|
||||
draggable = _asset_is_usable(obj)
|
||||
values = [
|
||||
f"{_asset_badge(obj.kind)} #{obj.id}",
|
||||
obj.aspect_ratio or "未知",
|
||||
_asset_status_text(obj),
|
||||
_source_text(obj, self.assets),
|
||||
obj.local_path or "",
|
||||
]
|
||||
data = {"type": "asset", "asset_id": int(obj.id), "draggable": draggable}
|
||||
text = f"{_asset_badge(obj.kind)} #{obj.id}\n{obj.aspect_ratio or '比例未知'}"
|
||||
tooltip = "单击设为源图,双击查看大图,可拖入终选槽。"
|
||||
else:
|
||||
values = [
|
||||
"任务",
|
||||
"-",
|
||||
_job_status_text(obj, self.JOB_STATUS_LABELS),
|
||||
f"源图 #{obj.source_asset_id or '-'}",
|
||||
obj.error or "",
|
||||
]
|
||||
data = {"type": "job", "job_id": int(obj.id)}
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(str(value or ""))
|
||||
item.setData(Qt.UserRole, data)
|
||||
if row_type == "asset" and column == 0:
|
||||
item.setIcon(_asset_icon(obj, _asset_badge(obj.kind)))
|
||||
item.setToolTip("单击设为源图,双击查看大图,可拖入终选槽。")
|
||||
if row_type == "job" and column == 0:
|
||||
item.setIcon(_job_icon(obj.status))
|
||||
item.setToolTip("cmhub 生图任务已保存,可继续查询。")
|
||||
if row_type == "asset" and obj.id == self.selected_source_asset_id:
|
||||
text = f"任务\n{_job_status_text(obj, self.JOB_STATUS_LABELS)}"
|
||||
tooltip = "生图任务已保存,可继续查询。"
|
||||
item = QListWidgetItem(text)
|
||||
item.setData(Qt.UserRole, data)
|
||||
item.setSizeHint(QSize(108, 124))
|
||||
item.setToolTip(tooltip)
|
||||
if row_type == "asset":
|
||||
item.setIcon(_asset_icon(obj, _asset_badge(obj.kind), size=QSize(86, 86)))
|
||||
if obj.id == self.selected_source_asset_id:
|
||||
item.setBackground(QColor("#eaf2ff"))
|
||||
if row_type == "job" and obj.status in {"failed", "expired"}:
|
||||
else:
|
||||
item.setIcon(_job_icon(obj.status))
|
||||
if obj.status in {"failed", "expired"}:
|
||||
item.setForeground(_qcolor(COLOR_DANGER))
|
||||
elif row_type == "job" and obj.status in {"pending", "submitted", "running"}:
|
||||
elif obj.status in {"pending", "submitted", "running"}:
|
||||
item.setForeground(_qcolor(COLOR_WARNING))
|
||||
self.pool_table.setItem(row, column, item)
|
||||
self.pool_table.setRowHeight(row, 104)
|
||||
if rows:
|
||||
self.pool_table.setColumnWidth(0, 150)
|
||||
self.pool_table.setColumnWidth(1, 68)
|
||||
self.pool_table.setColumnWidth(2, 170)
|
||||
self.pool_table.resizeColumnsToContents()
|
||||
self.pool_grid.addItem(item)
|
||||
|
||||
def _selection_asset_ids(self, selection_type):
|
||||
return [
|
||||
@@ -1008,32 +1024,43 @@ class ImageStudioTab(QWidget):
|
||||
return int(self.current_project.target_detail_count or 12)
|
||||
return int(self.current_project.target_main_count or 9)
|
||||
|
||||
def _on_original_clicked(self, row, column):
|
||||
asset = self._asset_from_table_row(self.original_table, row)
|
||||
def _on_original_clicked(self, item):
|
||||
asset = self._asset_from_grid_item(item)
|
||||
if asset is not None:
|
||||
self._ensure_original_in_pool(asset, open_after=False)
|
||||
|
||||
def _on_original_double_clicked(self, row, column):
|
||||
asset = self._asset_from_table_row(self.original_table, row)
|
||||
def _on_original_double_clicked(self, item):
|
||||
asset = self._asset_from_grid_item(item)
|
||||
if asset is not None:
|
||||
self._ensure_original_in_pool(asset, open_after=True)
|
||||
|
||||
def _on_pool_clicked(self, row, column):
|
||||
data = self._row_data(self.pool_table, row)
|
||||
def _on_pool_clicked(self, item):
|
||||
data = self._grid_data(item)
|
||||
if not data or data.get("type") != "asset":
|
||||
return
|
||||
asset = self._asset_by_id(data.get("asset_id"))
|
||||
if asset is not None:
|
||||
self._select_source_asset(asset)
|
||||
|
||||
def _on_pool_double_clicked(self, row, column):
|
||||
data = self._row_data(self.pool_table, row)
|
||||
def _on_pool_double_clicked(self, item):
|
||||
data = self._grid_data(item)
|
||||
if not data or data.get("type") != "asset":
|
||||
return
|
||||
asset = self._asset_by_id(data.get("asset_id"))
|
||||
if asset is not None:
|
||||
self._open_preview(asset)
|
||||
|
||||
def _show_original_context_menu(self, position):
|
||||
item = self.original_grid.itemAt(position)
|
||||
asset = self._asset_from_grid_item(item)
|
||||
if asset is None or int(asset.id) not in self._thumbnail_errors:
|
||||
return
|
||||
menu = QMenu(self)
|
||||
retry_action = menu.addAction("重新加载缩略图")
|
||||
action = menu.exec(self.original_grid.viewport().mapToGlobal(position))
|
||||
if action is retry_action:
|
||||
self._retry_original_thumbnail(asset)
|
||||
|
||||
def _ensure_original_in_pool(self, asset, open_after=False):
|
||||
if str(asset.local_path or "").strip() and os.path.isfile(asset.local_path):
|
||||
self._select_source_asset(asset)
|
||||
@@ -1097,6 +1124,7 @@ class ImageStudioTab(QWidget):
|
||||
self._update_project_action_buttons()
|
||||
|
||||
def _clear_current_project(self):
|
||||
self._reset_thumbnail_view(None)
|
||||
self.current_project = None
|
||||
self.assets = []
|
||||
self.jobs = []
|
||||
@@ -1108,8 +1136,8 @@ class ImageStudioTab(QWidget):
|
||||
self.prompt_edit.clear()
|
||||
finally:
|
||||
self.prompt_edit.blockSignals(False)
|
||||
self._fill_original_table()
|
||||
self._fill_pool_table()
|
||||
self._fill_original_grid()
|
||||
self._fill_pool_grid()
|
||||
self._refresh_selection_labels()
|
||||
self._refresh_source_label()
|
||||
self._refresh_project_summary()
|
||||
@@ -1119,6 +1147,110 @@ class ImageStudioTab(QWidget):
|
||||
self.open_folder_button.setEnabled(enabled)
|
||||
self.delete_project_button.setEnabled(enabled)
|
||||
|
||||
def _thumbnail_key(self, project_id, asset_id):
|
||||
return f"{int(project_id)}:{int(asset_id)}"
|
||||
|
||||
def _original_thumbnail_state(self, asset):
|
||||
asset_id = int(asset.id)
|
||||
if _asset_is_usable(asset):
|
||||
return "已下载"
|
||||
if asset_id in self._thumbnail_pixmaps:
|
||||
return "可预览"
|
||||
if asset_id in self._thumbnail_errors:
|
||||
return "加载失败"
|
||||
project_id = getattr(self.current_project, "id", None)
|
||||
key = self._thumbnail_key(project_id, asset_id) if project_id is not None else ""
|
||||
if key in self._thumbnail_pending:
|
||||
return "加载中"
|
||||
return "待加载"
|
||||
|
||||
def _queue_original_thumbnails(self, originals):
|
||||
project = self.current_project
|
||||
if project is None:
|
||||
return
|
||||
project_id = int(project.id)
|
||||
for asset in originals:
|
||||
asset_id = int(asset.id)
|
||||
remote_url = str(asset.remote_url or "").strip()
|
||||
key = self._thumbnail_key(project_id, asset_id)
|
||||
if (
|
||||
not remote_url
|
||||
or _asset_is_usable(asset)
|
||||
or asset_id in self._thumbnail_pixmaps
|
||||
or asset_id in self._thumbnail_errors
|
||||
or key in self._thumbnail_pending
|
||||
):
|
||||
continue
|
||||
self._thumbnail_pending.add(key)
|
||||
try:
|
||||
self._thumbnail_loader.submit(
|
||||
key,
|
||||
remote_url,
|
||||
lambda result, pid=project_id, aid=asset_id, thumbnail_key=key: self._thumbnail_signals.loaded.emit(
|
||||
{
|
||||
"project_id": pid,
|
||||
"asset_id": aid,
|
||||
"key": thumbnail_key,
|
||||
"result": result,
|
||||
}
|
||||
),
|
||||
lambda failed_key, exc, pid=project_id, aid=asset_id, thumbnail_key=key: self._thumbnail_signals.failed.emit(
|
||||
{
|
||||
"project_id": pid,
|
||||
"asset_id": aid,
|
||||
"key": thumbnail_key,
|
||||
}
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
self._thumbnail_signals.failed.emit(
|
||||
{"project_id": project_id, "asset_id": asset_id, "key": key}
|
||||
)
|
||||
|
||||
def _on_thumbnail_loaded(self, payload):
|
||||
project_id = int(payload.get("project_id") or 0)
|
||||
asset_id = int(payload.get("asset_id") or 0)
|
||||
key = str(payload.get("key") or "")
|
||||
self._thumbnail_pending.discard(key)
|
||||
if self.current_project is None or int(self.current_project.id) != project_id:
|
||||
return
|
||||
result = payload.get("result")
|
||||
image = QImage.fromData(bytes(getattr(result, "image_bytes", b"") or b""))
|
||||
if image.isNull():
|
||||
self._thumbnail_errors.add(asset_id)
|
||||
else:
|
||||
self._thumbnail_errors.discard(asset_id)
|
||||
self._thumbnail_pixmaps[asset_id] = QPixmap.fromImage(image)
|
||||
self._fill_original_grid()
|
||||
|
||||
def _on_thumbnail_failed(self, payload):
|
||||
project_id = int(payload.get("project_id") or 0)
|
||||
asset_id = int(payload.get("asset_id") or 0)
|
||||
self._thumbnail_pending.discard(str(payload.get("key") or ""))
|
||||
if self.current_project is None or int(self.current_project.id) != project_id:
|
||||
return
|
||||
self._thumbnail_errors.add(asset_id)
|
||||
self._fill_original_grid()
|
||||
|
||||
def _retry_original_thumbnail(self, asset):
|
||||
if self.current_project is None:
|
||||
return
|
||||
asset_id = int(asset.id)
|
||||
self._thumbnail_errors.discard(asset_id)
|
||||
self._thumbnail_pixmaps.pop(asset_id, None)
|
||||
self._fill_original_grid()
|
||||
|
||||
def _reset_thumbnail_view(self, project_id):
|
||||
for key in list(self._thumbnail_pending):
|
||||
try:
|
||||
self._thumbnail_loader.cancel(key)
|
||||
except Exception:
|
||||
pass
|
||||
self._thumbnail_pending.clear()
|
||||
self._thumbnail_pixmaps.clear()
|
||||
self._thumbnail_errors.clear()
|
||||
self._thumbnail_project_id = int(project_id) if project_id is not None else None
|
||||
|
||||
def _update_generation_action_text(self, *args):
|
||||
count = self.count_spin.value() if hasattr(self, "count_spin") else 0
|
||||
label = self.job_type_combo.currentText() if hasattr(self, "job_type_combo") else "图片"
|
||||
@@ -1129,8 +1261,8 @@ class ImageStudioTab(QWidget):
|
||||
dialog.exec()
|
||||
|
||||
def _show_pool_context_menu(self, position):
|
||||
row = self.pool_table.rowAt(position.y())
|
||||
data = self._row_data(self.pool_table, row)
|
||||
item = self.pool_grid.itemAt(position)
|
||||
data = self._grid_data(item)
|
||||
if not data or data.get("type") != "asset":
|
||||
return
|
||||
asset_id = data.get("asset_id")
|
||||
@@ -1144,7 +1276,7 @@ class ImageStudioTab(QWidget):
|
||||
"移除照片" if not referenced else "移除照片(已被任务或终选引用)"
|
||||
)
|
||||
remove_action.setEnabled(not referenced)
|
||||
action = menu.exec(self.pool_table.viewport().mapToGlobal(position))
|
||||
action = menu.exec(self.pool_grid.viewport().mapToGlobal(position))
|
||||
if action is remove_action and not referenced:
|
||||
self.remove_asset(asset_id)
|
||||
|
||||
@@ -1472,8 +1604,8 @@ class ImageStudioTab(QWidget):
|
||||
self._operation_running = bool(running)
|
||||
self.pull_images_button.setEnabled(not running)
|
||||
self.project_table.setEnabled(not running)
|
||||
self.original_table.setEnabled(not running)
|
||||
self.pool_table.setEnabled(not running)
|
||||
self.original_grid.setEnabled(not running)
|
||||
self.pool_grid.setEnabled(not running)
|
||||
self.main_selection_list.setEnabled(not running)
|
||||
self.detail_selection_list.setEnabled(not running)
|
||||
self.template_combo.setEnabled(not running)
|
||||
@@ -1557,16 +1689,13 @@ class ImageStudioTab(QWidget):
|
||||
item.setFlags(Qt.NoItemFlags)
|
||||
widget.addItem(item)
|
||||
|
||||
def _asset_from_table_row(self, table, row):
|
||||
data = self._row_data(table, row)
|
||||
def _asset_from_grid_item(self, item):
|
||||
data = self._grid_data(item)
|
||||
if not data or data.get("type") != "asset":
|
||||
return None
|
||||
return self._asset_by_id(data.get("asset_id"))
|
||||
|
||||
def _row_data(self, table, row):
|
||||
if row < 0 or row >= table.rowCount():
|
||||
return None
|
||||
item = table.item(row, 0)
|
||||
def _grid_data(self, item):
|
||||
if item is None:
|
||||
return None
|
||||
return item.data(Qt.UserRole)
|
||||
@@ -1616,6 +1745,13 @@ class ImageStudioTab(QWidget):
|
||||
def _status(self, message, level="muted"):
|
||||
_emit_status(self.status_callback, message, level=level)
|
||||
|
||||
def closeEvent(self, event):
|
||||
try:
|
||||
self._thumbnail_loader.close()
|
||||
except Exception:
|
||||
pass
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def _asset_badge(kind):
|
||||
return {
|
||||
@@ -1653,8 +1789,15 @@ def _asset_is_usable(asset):
|
||||
return bool(local_path and os.path.isfile(local_path))
|
||||
|
||||
|
||||
def _asset_icon(asset, fallback_label):
|
||||
return QIcon(_asset_pixmap(asset, QSize(86, 86), fallback_label=fallback_label))
|
||||
def _asset_icon(asset, fallback_label, size=None, cached_pixmap=None):
|
||||
return QIcon(
|
||||
_asset_pixmap(
|
||||
asset,
|
||||
size or QSize(86, 86),
|
||||
fallback_label=fallback_label,
|
||||
cached_pixmap=cached_pixmap,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _job_icon(status):
|
||||
@@ -1677,7 +1820,18 @@ def _job_icon(status):
|
||||
return QIcon(_placeholder_pixmap(label, QSize(86, 86), color))
|
||||
|
||||
|
||||
def _asset_pixmap(asset, size, fallback_label=None):
|
||||
def _asset_pixmap(asset, size, fallback_label=None, cached_pixmap=None):
|
||||
if cached_pixmap is not None and not cached_pixmap.isNull():
|
||||
scaled = cached_pixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
canvas = _placeholder_pixmap("", size, "#f6f8fa")
|
||||
painter = QPainter(canvas)
|
||||
painter.drawPixmap(
|
||||
(size.width() - scaled.width()) // 2,
|
||||
(size.height() - scaled.height()) // 2,
|
||||
scaled,
|
||||
)
|
||||
painter.end()
|
||||
return canvas
|
||||
path = str(getattr(asset, "local_path", "") or "")
|
||||
if path and os.path.isfile(path):
|
||||
image = QImage(path)
|
||||
|
||||
+31
-3
@@ -1,9 +1,9 @@
|
||||
---
|
||||
id: T-607
|
||||
title: AI工场原主图与照片池缩略图网格优化
|
||||
title: AI工场图片网格与生成设置布局优化
|
||||
phase: 7
|
||||
deps: [T-606]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-11
|
||||
---
|
||||
|
||||
@@ -52,6 +52,13 @@ T-605 已为 AI工场补了缩略图图标和状态,但「蝦皮原主图」
|
||||
- 预览缓存按当前 tab 生命周期或当前项目管理,项目切换/关闭时释放不再引用的 `QPixmap`,避免长时间使用导致内存无界增长。
|
||||
- 禁止使用嵌套卡片制造视觉层级;原图区/照片池是面板,单张图片才是卡片。
|
||||
|
||||
### 5. 收紧生成设置布局
|
||||
|
||||
- 去掉源图预览框右侧的说明文字;未选择源图时由预览框空状态和 tooltip 表达,避免右栏出现重复说明。
|
||||
- 源图预览框右侧第一行放「提示词模板」标签和模板下拉框;第二行把「新建」「重命名」「保存」「删除」四个按钮等宽并排。
|
||||
- 释放出的垂直空间分配给完整提示词输入框;提示词说明保留在输入框下方。
|
||||
- 「类型」「数量」「比例」改为提示词输入框下方的同一行紧凑控件组,保留现有下拉/数字输入语义和生成按钮动态文案。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 拉取有效商品后,原图区按原图顺序展示最多 9 张真实缩略图;缩略图加载不冻结 GUI。
|
||||
@@ -62,6 +69,7 @@ T-605 已为 AI工场补了缩略图图标和状态,但「蝦皮原主图」
|
||||
- 照片池的选源图、双击预览、右键移除、拖入终选、终选排序、生成、停止、继续查询和导出均不回归。
|
||||
- 项目切换、删除后切换到其他项目、后台任务完成后,不显示旧项目缩略图或错误的源图高亮。
|
||||
- 不改 CDP 拉图/Chrome/登录流程,不因缩略图预览批量保存九张完整原图,不暴露远程 URL 或敏感信息到用户日志。
|
||||
- 生成设置区不再出现源图右侧说明文字;模板选择与四个模板操作按钮按两行排列,提示词输入框面积增大,类型/数量/比例在同一行。
|
||||
|
||||
## 测试要求
|
||||
|
||||
@@ -100,4 +108,24 @@ git diff --check
|
||||
|
||||
## 执行记录
|
||||
|
||||
- 待执行。
|
||||
- 2026-07-11:已完成。
|
||||
- 代码:
|
||||
- `app/gui/tabs/image_studio.py`:原图区和照片池由字段表格改为固定尺寸的 `QListWidget` 图片网格。原图区按 `source_order` 展示主图;照片池只展示已落盘可用的原图/生成图和未完成或失败任务卡片,不再重复显示只有远程 URL 的原图。
|
||||
- `app/gui/tabs/image_studio.py`:接入既有 `image_studio_images.ThumbnailLoader`。远程缩略图使用受限后台线程池和内存缓存,结果通过 Qt signal 回到 GUI 线程;不保存完整原图、不在日志中输出远程 URL。单张失败只显示中文卡片状态,并可右键「重新加载缩略图」。
|
||||
- `app/gui/tabs/image_studio.py`:项目切换、软删除和窗口关闭会取消不再需要的缩略图回调并释放 GUI 缓存;原图区点击下载、照片池选源图/预览/右键移除/拖入终选、任务续查均保留。
|
||||
- `app/gui/tabs/image_studio.py`:生成设置去掉源图右侧说明文字;模板标签与下拉在源图右侧第一行,四个模板操作按钮同列第二行;提示词输入框扩展,类型/数量/比例调整为其下方同一行。
|
||||
- 测试:
|
||||
- `tests/test_gui.py`:更新 AI工场构建测试,覆盖网格替换、缩略图仅内存预览、原图落盘后才进入照片池、单张缩略图失败重试、9 张原图顺序、固定网格尺寸、模板按钮同排和三项生成参数同排。
|
||||
- `tests/test_gui.py`:更新已提交 cmhub 任务卡片断言,确认照片池保留可继续查询的任务状态和扣点信息。
|
||||
- `tests/test_image_studio_images.py`:既有缩略图下载、内存缓存、取消和不落盘原图测试继续通过。
|
||||
- 验证:
|
||||
- 主工作区定向验证通过:
|
||||
- `py -3.10 -m unittest tests.test_gui.GuiTests.test_image_studio_tab_builds_project_pool_and_template_controls tests.test_gui.GuiTests.test_image_studio_thumbnail_grids_keep_order_and_hide_remote_pool_duplicates tests.test_gui.GuiTests.test_image_studio_tab_shows_resume_and_job_billing_status tests.test_gui.GuiTests.test_image_studio_final_selection_order_and_guards tests.test_image_studio_images`
|
||||
- `python -m ruff check app tests main.py`
|
||||
- `py -3.10 -m compileall app main.py`
|
||||
- `git diff --check -- app/gui/tabs/image_studio.py tests/test_gui.py docs/tasks/T-607.md`
|
||||
- 主工作区存在未提交默认提示词文件改动;为隔离无关改动,在临时干净 worktree `D:\chengma\cmshopee-t607-verify` 应用本任务 diff 后通过:
|
||||
- `python -m ruff check app tests main.py`
|
||||
- `py -3.10 -m compileall app main.py`
|
||||
- `py -3.10 -m unittest discover -s tests`(381 tests)
|
||||
- `git diff --check`
|
||||
|
||||
+130
-13
@@ -17,7 +17,7 @@ from app import accounts, ai, appconfig, db, image_paths, image_studio, prompts,
|
||||
if gui.QT_IMPORT_ERROR is not None:
|
||||
raise unittest.SkipTest("PySide6 未安装")
|
||||
|
||||
from PySide6.QtCore import QItemSelectionModel, QModelIndex, QRect
|
||||
from PySide6.QtCore import QItemSelectionModel, QModelIndex, QRect, QSize
|
||||
from PySide6.QtGui import QImage, QKeyEvent, QTextCursor
|
||||
from PySide6.QtWidgets import QApplication, QComboBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
|
||||
|
||||
@@ -85,6 +85,31 @@ class FakeGenerateWorker:
|
||||
FakeGenerateWorker.instances.append(self)
|
||||
|
||||
|
||||
class FakeThumbnailLoader:
|
||||
def __init__(self):
|
||||
self.submissions = []
|
||||
self.cancelled = []
|
||||
self.closed = False
|
||||
|
||||
def submit(self, key, url, on_success=None, on_error=None):
|
||||
self.submissions.append((key, url, on_success, on_error))
|
||||
return None
|
||||
|
||||
def cancel(self, key=None):
|
||||
self.cancelled.append(key)
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def succeed(self, index, image_bytes):
|
||||
_, _, on_success, _ = self.submissions[index]
|
||||
on_success(SimpleNamespace(image_bytes=image_bytes))
|
||||
|
||||
def fail(self, index):
|
||||
key, _, _, on_error = self.submissions[index]
|
||||
on_error(key, RuntimeError("缩略图请求失败"))
|
||||
|
||||
|
||||
class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -496,11 +521,13 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
prompts_dir = os.path.join(temp_dir, "prompts", "image_studio")
|
||||
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
|
||||
prompts.save_image_studio_template("工场模板", "完整提示词", prompts_dir)
|
||||
thumbnail_loader = FakeThumbnailLoader()
|
||||
|
||||
tab = ImageStudioTab(
|
||||
config=cfg,
|
||||
db_path=cfg["db_path"],
|
||||
prompts_dir=prompts_dir,
|
||||
thumbnail_loader=thumbnail_loader,
|
||||
)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
@@ -541,6 +568,36 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
tab.template_combo.setCurrentIndex(template_index)
|
||||
tab.load_selected_template()
|
||||
self.assertEqual("完整提示词", tab.prompt_edit.toPlainText())
|
||||
tab.resize(1280, 820)
|
||||
tab.show()
|
||||
self.app.processEvents()
|
||||
self.assertLessEqual(
|
||||
max(
|
||||
button.y()
|
||||
for button in (
|
||||
tab.template_new_button,
|
||||
tab.template_rename_button,
|
||||
tab.template_save_button,
|
||||
tab.template_delete_button,
|
||||
)
|
||||
)
|
||||
- min(
|
||||
button.y()
|
||||
for button in (
|
||||
tab.template_new_button,
|
||||
tab.template_rename_button,
|
||||
tab.template_save_button,
|
||||
tab.template_delete_button,
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
self.assertLessEqual(
|
||||
max(tab.job_type_combo.y(), tab.count_spin.y(), tab.aspect_combo.y())
|
||||
- min(tab.job_type_combo.y(), tab.count_spin.y(), tab.aspect_combo.y()),
|
||||
1,
|
||||
)
|
||||
self.assertNotIn("当前源图\n单击照片池图片设为源图,双击查看大图", label_texts)
|
||||
|
||||
project = image_studio.create_or_get_project(
|
||||
account,
|
||||
@@ -570,15 +627,75 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
)[0]
|
||||
tab.refresh_project_assets()
|
||||
|
||||
self.assertEqual(1, tab.original_table.rowCount())
|
||||
self.assertEqual("主图 #1", tab.original_table.item(0, 0).text())
|
||||
self.assertFalse(tab.original_table.item(0, 0).icon().isNull())
|
||||
self.assertEqual("远程待下载", tab.original_table.item(0, 1).text())
|
||||
self.assertEqual(1, tab.pool_table.rowCount())
|
||||
self.assertEqual(f"原图 #{original.id}", tab.pool_table.item(0, 0).text())
|
||||
self.assertFalse(tab.pool_table.item(0, 0).icon().isNull())
|
||||
self.assertEqual("远程待下载", tab.pool_table.item(0, 2).text())
|
||||
self.assertEqual(original.id, tab.pool_table.item(0, 0).data(gui.Qt.UserRole)["asset_id"])
|
||||
self.assertEqual(1, tab.original_grid.count())
|
||||
self.assertIn("主图 1", tab.original_grid.item(0).text())
|
||||
self.assertIn("加载中", tab.original_grid.item(0).text())
|
||||
self.assertFalse(tab.original_grid.item(0).icon().isNull())
|
||||
self.assertEqual(1, len(thumbnail_loader.submissions))
|
||||
self.assertEqual(0, tab.pool_grid.count())
|
||||
|
||||
thumbnail_loader.fail(0)
|
||||
self.app.processEvents()
|
||||
self.assertIn("加载失败", tab.original_grid.item(0).text())
|
||||
self.assertIn("右键重新加载", tab.original_grid.item(0).toolTip())
|
||||
tab._retry_original_thumbnail(original)
|
||||
self.assertEqual(2, len(thumbnail_loader.submissions))
|
||||
self.assertIn("加载中", tab.original_grid.item(0).text())
|
||||
|
||||
thumbnail_path = self.write_test_image(os.path.join(temp_dir, "thumbnail.png"))
|
||||
with open(thumbnail_path, "rb") as image_file:
|
||||
thumbnail_loader.succeed(1, image_file.read())
|
||||
self.app.processEvents()
|
||||
self.assertIn("可预览", tab.original_grid.item(0).text())
|
||||
self.assertIsNone(image_studio.get_asset(original.id, path=cfg["db_path"]).local_path)
|
||||
|
||||
local_original_path = self.write_test_image(os.path.join(temp_dir, "original.png"))
|
||||
image_studio.update_asset_local_path(original.id, local_original_path, path=cfg["db_path"])
|
||||
tab.refresh_project_assets()
|
||||
self.assertEqual(1, tab.pool_grid.count())
|
||||
pool_item = tab.pool_grid.item(0)
|
||||
self.assertIn("原图", pool_item.text())
|
||||
self.assertEqual(original.id, pool_item.data(gui.Qt.UserRole)["asset_id"])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_image_studio_thumbnail_grids_keep_order_and_hide_remote_pool_duplicates(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
db.init_db(cfg["db_path"])
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias-a",
|
||||
account_slug="alias_a",
|
||||
item_id="51100639510",
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
image_studio.sync_original_asset_urls(
|
||||
project.id,
|
||||
[
|
||||
{"index": index, "src": f"https://susercontent.com/main-{index}.jpg"}
|
||||
for index in range(1, 10)
|
||||
],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
thumbnail_loader = FakeThumbnailLoader()
|
||||
tab = ImageStudioTab(
|
||||
config=cfg,
|
||||
db_path=cfg["db_path"],
|
||||
thumbnail_loader=thumbnail_loader,
|
||||
)
|
||||
self.addCleanup(tab.close)
|
||||
tab._select_project(project.id)
|
||||
|
||||
self.assertEqual(9, tab.original_grid.count())
|
||||
self.assertEqual(
|
||||
[f"主图 {index}" for index in range(1, 10)],
|
||||
[tab.original_grid.item(row).text().split("\n", 1)[0] for row in range(9)],
|
||||
)
|
||||
self.assertEqual(9, len(thumbnail_loader.submissions))
|
||||
self.assertEqual(0, tab.pool_grid.count())
|
||||
self.assertEqual(QSize(78, 92), tab.original_grid.gridSize())
|
||||
self.assertEqual(QSize(108, 124), tab.pool_grid.gridSize())
|
||||
self.assertGreaterEqual(tab.prompt_edit.minimumHeight(), 210)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
@@ -749,9 +866,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assertEqual("继续查询任务", tab.resume_button.text())
|
||||
statuses = [
|
||||
tab.pool_table.item(row, 2).text()
|
||||
for row in range(tab.pool_table.rowCount())
|
||||
if tab.pool_table.item(row, 0).text() == "任务"
|
||||
tab.pool_grid.item(row).text()
|
||||
for row in range(tab.pool_grid.count())
|
||||
if tab.pool_grid.item(row).data(gui.Qt.UserRole)["type"] == "job"
|
||||
]
|
||||
self.assertEqual(1, len(statuses))
|
||||
self.assertIn("已提交", statuses[0])
|
||||
|
||||
Reference in New Issue
Block a user