feat(ai-studio): add final selection trays
This commit is contained in:
@@ -4,6 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QMimeData
|
||||
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
||||
|
||||
from ... import accounts, appconfig, db, image_studio, prompts
|
||||
from .. import file_manager
|
||||
from ..widgets import *
|
||||
@@ -14,6 +17,9 @@ from ..workers import ImageStudioGenerateJobsWorker as _RealImageStudioGenerateJ
|
||||
from ..workers import ImageStudioPullImagesWorker as _RealImageStudioPullImagesWorker
|
||||
|
||||
|
||||
ASSET_MIME_TYPE = "application/x-cmshopee-image-studio-asset"
|
||||
|
||||
|
||||
def ImageStudioPullImagesWorker(*args, **kwargs):
|
||||
return _call_package_attr(
|
||||
"ImageStudioPullImagesWorker",
|
||||
@@ -41,6 +47,132 @@ def ImageStudioGenerateJobsWorker(*args, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
def _drop_event_position(event):
|
||||
if hasattr(event, "position"):
|
||||
return event.position().toPoint()
|
||||
return event.pos()
|
||||
|
||||
|
||||
def _mime_asset_id(mime_data):
|
||||
if mime_data is None or not mime_data.hasFormat(ASSET_MIME_TYPE):
|
||||
return None
|
||||
raw = bytes(mime_data.data(ASSET_MIME_TYPE)).decode("utf-8", errors="ignore")
|
||||
try:
|
||||
return int(str(raw).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
class ImageStudioPoolTable(QTableWidget):
|
||||
"""Photo pool table that can drag available asset IDs into final trays."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(QAbstractItemView.DragOnly)
|
||||
self.setDefaultDropAction(Qt.CopyAction)
|
||||
|
||||
def mimeData(self, items):
|
||||
mime = QMimeData()
|
||||
if 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 {}
|
||||
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"))
|
||||
return mime
|
||||
|
||||
|
||||
class ImageStudioSelectionList(QListWidget):
|
||||
"""Ordered final tray for main/detail selected assets."""
|
||||
|
||||
def __init__(self, selection_type, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self.selection_type = selection_type
|
||||
self.tab = tab
|
||||
self.setAcceptDrops(True)
|
||||
self.setDragEnabled(True)
|
||||
self.setDefaultDropAction(Qt.MoveAction)
|
||||
self.setDragDropMode(QAbstractItemView.DragDrop)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._show_context_menu)
|
||||
|
||||
def mimeData(self, items):
|
||||
mime = QMimeData()
|
||||
if not items:
|
||||
return mime
|
||||
asset_id = items[0].data(Qt.UserRole)
|
||||
if asset_id is not None:
|
||||
mime.setData(ASSET_MIME_TYPE, str(asset_id).encode("utf-8"))
|
||||
return mime
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if _mime_asset_id(event.mimeData()) is not None:
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
if _mime_asset_id(event.mimeData()) is not None:
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
super().dragMoveEvent(event)
|
||||
|
||||
def dropEvent(self, event):
|
||||
asset_id = _mime_asset_id(event.mimeData())
|
||||
if asset_id is None:
|
||||
super().dropEvent(event)
|
||||
return
|
||||
index = self.indexAt(_drop_event_position(event))
|
||||
insert_index = index.row() if index.isValid() else self.count()
|
||||
if event.source() is self:
|
||||
current_index = self._row_for_asset(asset_id)
|
||||
if current_index is None:
|
||||
event.ignore()
|
||||
return
|
||||
self.tab.move_selection_asset(self.selection_type, current_index, insert_index)
|
||||
else:
|
||||
self.tab.add_asset_to_selection(self.selection_type, asset_id, insert_index=insert_index)
|
||||
event.acceptProposedAction()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Delete:
|
||||
self.remove_current_asset()
|
||||
event.accept()
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def remove_current_asset(self):
|
||||
item = self.currentItem()
|
||||
if item is None:
|
||||
return
|
||||
asset_id = item.data(Qt.UserRole)
|
||||
self.tab.remove_asset_from_selection(self.selection_type, asset_id)
|
||||
|
||||
def _show_context_menu(self, position):
|
||||
item = self.itemAt(position)
|
||||
if item is None:
|
||||
return
|
||||
menu = QMenu(self)
|
||||
remove_action = menu.addAction("移出终选")
|
||||
action = menu.exec(self.viewport().mapToGlobal(position))
|
||||
if action is remove_action:
|
||||
self.setCurrentItem(item)
|
||||
self.remove_current_asset()
|
||||
|
||||
def _row_for_asset(self, asset_id):
|
||||
for row in range(self.count()):
|
||||
item = self.item(row)
|
||||
if item is not None and int(item.data(Qt.UserRole)) == int(asset_id):
|
||||
return row
|
||||
return None
|
||||
|
||||
|
||||
class ImageStudioPreviewDialog(QDialog):
|
||||
"""Simple large image preview used by original and pool tables."""
|
||||
|
||||
@@ -220,7 +352,8 @@ class ImageStudioTab(QWidget):
|
||||
pool_header.addWidget(self.source_label)
|
||||
layout.addLayout(pool_header)
|
||||
|
||||
self.pool_table = QTableWidget(0, len(self.POOL_COLUMNS))
|
||||
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)
|
||||
@@ -314,13 +447,30 @@ class ImageStudioTab(QWidget):
|
||||
panel.setObjectName("imageStudioFinalPanel")
|
||||
layout = QHBoxLayout(panel)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_selection_label = QLabel("主图终选 0/9(拖放排序将在 T-592 接入)")
|
||||
layout.setSpacing(10)
|
||||
main_panel = QWidget()
|
||||
main_layout = QVBoxLayout(main_panel)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_selection_label = QLabel("主图终选 0/9")
|
||||
self.main_selection_label.setObjectName("imageStudioMainSelectionLabel")
|
||||
self.detail_selection_label = QLabel("详情图终选 0/12(拖放排序将在 T-592 接入)")
|
||||
self.main_selection_list = ImageStudioSelectionList("main", self)
|
||||
self.main_selection_list.setObjectName("imageStudioMainSelectionList")
|
||||
self.main_selection_list.setMinimumHeight(96)
|
||||
main_layout.addWidget(self.main_selection_label)
|
||||
main_layout.addWidget(self.main_selection_list)
|
||||
|
||||
detail_panel = QWidget()
|
||||
detail_layout = QVBoxLayout(detail_panel)
|
||||
detail_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.detail_selection_label = QLabel("详情图终选 0/12")
|
||||
self.detail_selection_label.setObjectName("imageStudioDetailSelectionLabel")
|
||||
layout.addWidget(self.main_selection_label)
|
||||
layout.addWidget(self.detail_selection_label)
|
||||
layout.addStretch(1)
|
||||
self.detail_selection_list = ImageStudioSelectionList("detail", self)
|
||||
self.detail_selection_list.setObjectName("imageStudioDetailSelectionList")
|
||||
self.detail_selection_list.setMinimumHeight(96)
|
||||
detail_layout.addWidget(self.detail_selection_label)
|
||||
detail_layout.addWidget(self.detail_selection_list)
|
||||
layout.addWidget(main_panel, 1)
|
||||
layout.addWidget(detail_panel, 1)
|
||||
return panel
|
||||
|
||||
def _connect_signals(self):
|
||||
@@ -544,6 +694,7 @@ class ImageStudioTab(QWidget):
|
||||
self.pool_table.setRowCount(len(rows))
|
||||
for row, (row_type, obj) in enumerate(rows):
|
||||
if row_type == "asset":
|
||||
draggable = _asset_is_usable(obj)
|
||||
values = [
|
||||
_asset_badge(obj.kind),
|
||||
obj.aspect_ratio or "未知",
|
||||
@@ -551,7 +702,7 @@ class ImageStudioTab(QWidget):
|
||||
_source_text(obj, self.assets),
|
||||
obj.local_path or "",
|
||||
]
|
||||
data = {"type": "asset", "asset_id": int(obj.id)}
|
||||
data = {"type": "asset", "asset_id": int(obj.id), "draggable": draggable}
|
||||
else:
|
||||
values = [
|
||||
"任务",
|
||||
@@ -567,6 +718,77 @@ class ImageStudioTab(QWidget):
|
||||
self.pool_table.setItem(row, column, item)
|
||||
self.pool_table.resizeColumnsToContents()
|
||||
|
||||
def _selection_asset_ids(self, selection_type):
|
||||
return [
|
||||
int(selection.asset_id)
|
||||
for selection in self.selections
|
||||
if selection.selection_type == selection_type
|
||||
]
|
||||
|
||||
def add_asset_to_selection(self, selection_type, asset_id, insert_index=None):
|
||||
if self.current_project is None:
|
||||
return False
|
||||
asset = self._asset_by_id(asset_id)
|
||||
if asset is None:
|
||||
self._message("不能加入终选", "照片不存在。")
|
||||
return False
|
||||
if not _asset_is_usable(asset):
|
||||
self._message("不能加入终选", "照片尚未下载或本地文件缺失。")
|
||||
return False
|
||||
ids = self._selection_asset_ids(selection_type)
|
||||
if int(asset.id) in ids:
|
||||
self._message("不能重复加入", "同一张照片在同一类终选中只能出现一次。")
|
||||
return False
|
||||
target = self._selection_target_count(selection_type)
|
||||
if len(ids) >= target:
|
||||
self._message("终选已满", f"{_selection_label(selection_type)}最多 {target} 张。")
|
||||
return False
|
||||
index = _clamp_insert_index(insert_index, len(ids))
|
||||
ids.insert(index, int(asset.id))
|
||||
return self._persist_selection(selection_type, ids, success_message="已加入终选")
|
||||
|
||||
def move_selection_asset(self, selection_type, from_index, to_index):
|
||||
ids = self._selection_asset_ids(selection_type)
|
||||
if not ids:
|
||||
return False
|
||||
from_index = int(from_index)
|
||||
if from_index < 0 or from_index >= len(ids):
|
||||
return False
|
||||
asset_id = ids.pop(from_index)
|
||||
target_index = _clamp_insert_index(to_index, len(ids))
|
||||
ids.insert(target_index, asset_id)
|
||||
return self._persist_selection(selection_type, ids, success_message="终选顺序已更新")
|
||||
|
||||
def remove_asset_from_selection(self, selection_type, asset_id):
|
||||
ids = self._selection_asset_ids(selection_type)
|
||||
filtered = [value for value in ids if int(value) != int(asset_id)]
|
||||
if len(filtered) == len(ids):
|
||||
return False
|
||||
return self._persist_selection(selection_type, filtered, success_message="已移出终选")
|
||||
|
||||
def _persist_selection(self, selection_type, asset_ids, success_message):
|
||||
try:
|
||||
image_studio.replace_selections(
|
||||
self.current_project.id,
|
||||
selection_type,
|
||||
asset_ids,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._message("终选保存失败", str(exc))
|
||||
self.refresh_project_assets()
|
||||
return False
|
||||
self.refresh_project_assets()
|
||||
self._status(success_message, "success")
|
||||
return True
|
||||
|
||||
def _selection_target_count(self, selection_type):
|
||||
if self.current_project is None:
|
||||
return 0
|
||||
if selection_type == "detail":
|
||||
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)
|
||||
if asset is not None:
|
||||
@@ -861,6 +1083,8 @@ class ImageStudioTab(QWidget):
|
||||
self.project_table.setEnabled(not running)
|
||||
self.original_table.setEnabled(not running)
|
||||
self.pool_table.setEnabled(not running)
|
||||
self.main_selection_list.setEnabled(not running)
|
||||
self.detail_selection_list.setEnabled(not running)
|
||||
self.template_combo.setEnabled(not running)
|
||||
self.template_new_button.setEnabled(not running)
|
||||
self.template_rename_button.setEnabled(not running)
|
||||
@@ -898,8 +1122,38 @@ class ImageStudioTab(QWidget):
|
||||
detail_count = sum(1 for item in self.selections if item.selection_type == "detail")
|
||||
main_target = getattr(self.current_project, "target_main_count", 9) if self.current_project else 9
|
||||
detail_target = getattr(self.current_project, "target_detail_count", 12) if self.current_project else 12
|
||||
self.main_selection_label.setText(f"主图终选 {main_count}/{main_target}(拖放排序将在 T-592 接入)")
|
||||
self.detail_selection_label.setText(f"详情图终选 {detail_count}/{detail_target}(拖放排序将在 T-592 接入)")
|
||||
self.main_selection_label.setText(f"主图终选 {main_count}/{main_target}")
|
||||
self.detail_selection_label.setText(f"详情图终选 {detail_count}/{detail_target}")
|
||||
self._fill_selection_list(self.main_selection_list, "main")
|
||||
self._fill_selection_list(self.detail_selection_list, "detail")
|
||||
|
||||
def _fill_selection_list(self, widget, selection_type):
|
||||
widget.clear()
|
||||
for index, selection in enumerate(
|
||||
[item for item in self.selections if item.selection_type == selection_type],
|
||||
start=1,
|
||||
):
|
||||
asset = self._asset_by_id(selection.asset_id)
|
||||
if asset is None:
|
||||
text = f"{index}. 缺失照片 #{selection.asset_id}"
|
||||
item = QListWidgetItem(text)
|
||||
item.setData(Qt.UserRole, int(selection.asset_id))
|
||||
item.setToolTip("终选引用的照片记录不存在")
|
||||
item.setForeground(_qcolor(COLOR_DANGER))
|
||||
widget.addItem(item)
|
||||
continue
|
||||
text = f"{index}. {_asset_badge(asset.kind)} #{asset.id} · {asset.aspect_ratio or '未知比例'}"
|
||||
item = QListWidgetItem(text)
|
||||
item.setData(Qt.UserRole, int(asset.id))
|
||||
tooltip = _selection_tooltip(selection_type, asset)
|
||||
item.setToolTip(tooltip)
|
||||
if _selection_ratio_warning(selection_type, asset):
|
||||
item.setBackground(QColor("#fff8c5"))
|
||||
item.setToolTip(tooltip + "\n比例与主图推荐比例不一致,仅提示,不阻止导出。")
|
||||
if not _asset_is_usable(asset):
|
||||
item.setForeground(_qcolor(COLOR_MUTED))
|
||||
item.setToolTip(tooltip + "\n本地文件缺失,不能用于导出。")
|
||||
widget.addItem(item)
|
||||
|
||||
def _asset_from_table_row(self, table, row):
|
||||
data = self._row_data(table, row)
|
||||
@@ -974,6 +1228,15 @@ def _asset_status_text(asset):
|
||||
return "待生成"
|
||||
|
||||
|
||||
def _asset_is_usable(asset):
|
||||
if asset is None:
|
||||
return False
|
||||
if str(getattr(asset, "status", "") or "") == image_studio.ASSET_STATUS_MISSING:
|
||||
return False
|
||||
local_path = str(getattr(asset, "local_path", "") or "")
|
||||
return bool(local_path and os.path.isfile(local_path))
|
||||
|
||||
|
||||
def _source_text(asset, assets):
|
||||
parent_id = getattr(asset, "parent_asset_id", None)
|
||||
if not parent_id:
|
||||
@@ -982,3 +1245,29 @@ def _source_text(asset, assets):
|
||||
if int(item.id) == int(parent_id):
|
||||
return f"{_asset_badge(item.kind)} #{item.id}"
|
||||
return f"源图 #{parent_id}"
|
||||
|
||||
|
||||
def _selection_label(selection_type):
|
||||
return "详情图终选" if selection_type == "detail" else "主图终选"
|
||||
|
||||
|
||||
def _clamp_insert_index(value, length):
|
||||
try:
|
||||
index = int(value)
|
||||
except (TypeError, ValueError):
|
||||
index = length
|
||||
return max(0, min(index, int(length)))
|
||||
|
||||
|
||||
def _selection_ratio_warning(selection_type, asset):
|
||||
if selection_type != "main":
|
||||
return False
|
||||
ratio = str(getattr(asset, "aspect_ratio", "") or "").strip()
|
||||
return bool(ratio and ratio != "1:1")
|
||||
|
||||
|
||||
def _selection_tooltip(selection_type, asset):
|
||||
return (
|
||||
f"{_selection_label(selection_type)}:{_asset_badge(getattr(asset, 'kind', ''))} "
|
||||
f"#{getattr(asset, 'id', '')},比例 {getattr(asset, 'aspect_ratio', None) or '未知'}"
|
||||
)
|
||||
|
||||
+6
-3
@@ -186,7 +186,7 @@
|
||||
│ [完整提示词输入框] │
|
||||
│ 类型[主图▼] 数量[4] 比例[1:1▼] cmhub扣点/余额提示 │
|
||||
│ [开始生成][停止] 进度条 运行日志 │
|
||||
│ 底部:主图终选 / 详情图终选占位(T-592 接拖放排序) │
|
||||
│ 底部:主图终选 / 详情图终选(拖入、插入、重排、移出) │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -196,8 +196,11 @@
|
||||
- 照片池展示原图、生成主图、生成详情图和在途/失败任务状态;单击可用图片设为源图,双击打开大图;右键移除只删除未被任务或终选引用的照片池记录,不删除本地图片文件。
|
||||
- 右侧只有一个完整提示词框;模板目录固定为 `data/prompts/image_studio/`,与②标题/封面模板隔离。界面不显示“主提示词 / 每张动作词”。
|
||||
- 生图固定走 cmhub 托管模型,使用⑤设置里的 cmhub Base URL/API Key/生图别名和图片并发;界面只显示扣点、余额、进度、失败,不展示自定义 Provider、API Key、生成来源选择或“导入本地图片”入口。
|
||||
- 拉主图、下载原图、生图 submit/poll/download 均通过 worker 执行,主线程只刷新 UI;运行中禁用项目切换、模板编辑、源图选择和生成设置,停止为协作式停止。
|
||||
- 本小节只覆盖 T-591 第一版:终选拖放排序由 T-592 接入,导出 JPEG 由 T-593 接入,故底部终选盘当前是只读占位。
|
||||
- 底部终选盘分为主图和详情图两列;照片池中已下载/已生成且本地文件可用的图片可拖入终选,落到已有位置时按插入顺延,同一类别内同一照片只能出现一次,主图和详情图之间允许复用同一照片。
|
||||
- 终选列表内可拖动重排,Delete 或右键「移出终选」只移出终选,不删除照片池资产或本地文件;拖放/移出失败时刷新回 SQLite 中的持久化顺序。
|
||||
- 主图推荐 1:1;比例不匹配只用黄色轻提示和 tooltip 提醒,不硬拦。文件缺失或尚未下载的照片不能拖入终选。
|
||||
- 拉主图、下载原图、生图 submit/poll/download 均通过 worker 执行,主线程只刷新 UI;运行中禁用项目切换、模板编辑、源图选择、终选拖放和生成设置,停止为协作式停止。
|
||||
- 本小节当前覆盖 T-591/T-592:导出 JPEG 由 T-593 接入,不自动上传或修改蝦皮。
|
||||
|
||||
## 流程导航
|
||||
|
||||
|
||||
+7
-2
@@ -3,7 +3,7 @@ id: T-592
|
||||
title: AI工场主图/详情图终选拖放、插入排序与比例轻提示
|
||||
phase: 7
|
||||
deps: [T-591]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-11
|
||||
---
|
||||
|
||||
@@ -36,4 +36,9 @@ created: 2026-07-11
|
||||
|
||||
## 执行记录
|
||||
|
||||
(完成后记录拖放模型、事务和 GUI 测试。)
|
||||
- 2026-07-11:完成 AI工场主图/详情图终选盘。
|
||||
- `ImageStudioTab` 底部终选占位升级为主图/详情图两个可拖放列表;照片池可用图片提供 asset_id 拖拽,终选列表支持外部拖入、落点插入顺延、列表内重排、Delete/右键移出。
|
||||
- 写库统一走 `image_studio.replace_selections()`;同类别重复拖入、满额、文件未下载/缺失会中文提示并拒绝;主图/详情图跨类别复用允许。
|
||||
- 主图非 1:1 比例显示黄色轻提示和 tooltip,不阻止加入;运行中禁用终选列表,避免生成/下载时状态并发修改。
|
||||
- 更新 `docs/routes.md` 的 ⑥ AI工场底部终选说明,明确 T-593 才做导出。
|
||||
- 验证:当前主工作区仍有无关默认提示词脏文件会影响全量 unittest;已在干净 worktree 仅套用 T-592 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`(363 tests)和 `git diff --check`,全部通过。主工作区也运行了 T-592 相关 targeted unittest,已通过。
|
||||
|
||||
@@ -542,6 +542,64 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_image_studio_final_selection_order_and_guards(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"],
|
||||
)
|
||||
first_path = self.write_test_image(os.path.join(temp_dir, "first.jpg"))
|
||||
second_path = self.write_test_image(os.path.join(temp_dir, "second.jpg"))
|
||||
first = image_studio.add_asset(
|
||||
project.id,
|
||||
"generated_main",
|
||||
local_path=first_path,
|
||||
aspect_ratio="1:1",
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
second = image_studio.add_asset(
|
||||
project.id,
|
||||
"generated_main",
|
||||
local_path=second_path,
|
||||
aspect_ratio="3:4",
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
tab = ImageStudioTab(config=cfg, db_path=cfg["db_path"])
|
||||
self.addCleanup(tab.close)
|
||||
messages = []
|
||||
tab._message = lambda title, text: messages.append((title, text))
|
||||
tab._select_project(project.id)
|
||||
|
||||
self.assertTrue(tab.add_asset_to_selection("main", first.id))
|
||||
self.assertTrue(tab.add_asset_to_selection("main", second.id, insert_index=0))
|
||||
main = image_studio.list_selections(project.id, "main", path=cfg["db_path"])
|
||||
self.assertEqual([second.id, first.id], [selection.asset_id for selection in main])
|
||||
self.assertEqual([second.id, first.id], [
|
||||
tab.main_selection_list.item(row).data(gui.Qt.UserRole)
|
||||
for row in range(tab.main_selection_list.count())
|
||||
])
|
||||
self.assertEqual("#fff8c5", tab.main_selection_list.item(0).background().color().name())
|
||||
|
||||
self.assertFalse(tab.add_asset_to_selection("main", first.id))
|
||||
self.assertIn("不能重复加入", messages[-1][0])
|
||||
self.assertTrue(tab.add_asset_to_selection("detail", first.id))
|
||||
detail = image_studio.list_selections(project.id, "detail", path=cfg["db_path"])
|
||||
self.assertEqual([first.id], [selection.asset_id for selection in detail])
|
||||
|
||||
self.assertTrue(tab.move_selection_asset("main", 1, 0))
|
||||
main = image_studio.list_selections(project.id, "main", path=cfg["db_path"])
|
||||
self.assertEqual([first.id, second.id], [selection.asset_id for selection in main])
|
||||
|
||||
self.assertTrue(tab.remove_asset_from_selection("main", first.id))
|
||||
main = image_studio.list_selections(project.id, "main", path=cfg["db_path"])
|
||||
self.assertEqual([second.id], [selection.asset_id for selection in main])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_startup_update_gate_forced_blocks_and_opens_download(self):
|
||||
boxes = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user