瘦身画板媒体保存链路
改造画板项目保存响应为轻量 ack 统一生成上传媒体以 objectKey 持久化 补齐素材库缩略图换签与本地轻量缓存 刷新生成后泥点余额并补充回归测试
This commit is contained in:
@@ -143,6 +143,14 @@
|
||||
- 验证:`cargo test -p api-server editor_canvas_generation_completion --manifest-path server-rs/Cargo.toml` 覆盖后端完成态写 layout;`npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand` 覆盖前端提交 `canvasCompletion`、应用后端快照、项目加载不推断完成态和 `objectKey` 资源创建不提交大 URL。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasProjectPersistence.ts`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 图片编辑器项目和素材 payload 不能持久化内联媒体
|
||||
|
||||
- 现象:`/api/editor/projects*`、素材库、项目资源或 layout payload 里出现数 MB 的 `data:image/*`、`data:video/*`、`data:audio/*`,刷新恢复变慢,发布入口可能 OOM / 413,素材库缩略图还可能只显示文件名。
|
||||
- 原因:生成、规范图、角色图、图标 / UI spritesheet、音视频或动画帧如果直接把 Data URL / signed URL 写入 `editor_project_resource`、`editor_asset` 或 `editor_canvas.layers_json`,就把媒体本体塞进了项目快照;signed URL 还会过期,素材库也无法稳定换签。
|
||||
- 处理:登录态媒体必须先上传 OSS / asset object,持久化只写 `imageSrc: "/<objectKey>"`、`objectKey`、`assetObjectId`;素材库和图层缩略图都通过 `PlatformMediaFrame -> ResolvedAssetImage` 传 `objectKey` 并调用 `/api/assets/read-url`。layout 序列化和后端保存要递归拒绝 `data:*` / `blob:`;旧行有 `objectKey` 时读出归一成 `/<objectKey>`,没有 `objectKey` 的旧 Data URL 必须走修复上传后回写轻量引用。刷新恢复可先用 session 轻量缓存显示,但缓存不得含内联媒体,也不能在后端快照回来前自动保存。生成扣费、失败退款或 queue 终态后,右上角泥点余额通过 `/profile/dashboard` 回读,不做本地乐观扣减。
|
||||
- 验证:Network 中 `/api/editor/projects*`、`PATCH /api/editor/projects/{id}`、素材库接口不应出现 `data:image` / `data:video` / `data:audio`;素材库和图层面板缩略图都能换签显示;`npm run test -- src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/useImageCanvasProjectPersistence.test.tsx src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/common/PlatformMediaFrame.test.tsx src/services/assetReadUrlService.test.ts src/services/image-editor/editorProjectClient.test.ts`,后端跑 `cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`src/components/image-editor/ImageCanvasEditorModel.ts`、`src/components/image-editor/useImageCanvasProjectPersistence.ts`、`src/components/common/PlatformMediaFrame.tsx`、`src/services/assetReadUrlService.ts`。
|
||||
|
||||
## 图片画布发布入口 429 先查自动保存 PATCH 并发
|
||||
|
||||
- 现象:发布域名访问画板时出现短时间密集 `429`,Nginx access log 中 `PATCH /api/editor/projects/<projectId>`、生成接口和资料接口混杂,429 行常见 `request_time=0.000`、`upstream_status=-`,error log 写 `limiting connections by zone "genarrative_api_conn"`。
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
- 吸附阈值以屏幕像素为准,换算到世界坐标后参与拖拽计算;边缘 / 中心线和等距吸附共用同一阈值。拖拽结束后只保存最终图层或生成占位布局,不保存临时参考线。
|
||||
- 项目页封面和画布图片图层必须先渲染项目卡、图层外框、标题、尺寸和操作 chrome;图片换签或解码未完成时,只在图片区域显示轻量加载态,不阻塞外框和文字等低成本信息先出现。
|
||||
- 素材量增大时,拖拽吸附热路径不得对所有素材做全量两两配对。边缘 / 中心线吸附保持线性扫描;等距吸附只在跨轴相交且轴向邻近的候选图层之间计算,避免大量远处素材拖慢 pointermove。
|
||||
- 画布自动保存使用防抖 + 串行队列:图层拖拽、缩放、资源新增和修改结果创建后延迟保存工程快照;如果上一次 `PATCH /api/editor/projects/{projectId}` 尚未完成,只保留最新待保存快照,待当前请求结束后再发送下一次保存,避免慢保存请求并发堆积触发发布入口连接限流。
|
||||
- 画布自动保存使用防抖 + 串行队列:图层拖拽、缩放、资源新增和修改结果创建后延迟保存工程快照;如果上一次 `PATCH /api/editor/projects/{projectId}` 尚未完成,只保留最新待保存快照,待当前请求结束后再发送下一次保存,避免慢保存请求并发堆积触发发布入口连接限流。`PATCH /api/editor/projects/{projectId}` 只返回 `{ projectId, canvasId, updatedAt }` 轻量 ack,不再返回完整 project,前端必须以后续显式读取或生成完成返回的后端快照作为项目真相。
|
||||
- 移动端保留同一套状态模型,底部工具栏可横向滚动,侧边栏默认可收起。
|
||||
- 项目页卡片默认点击打开工程;hover 项目卡片右下角显示 `...` 菜单,菜单承载重命名和删除。选择模式下项目卡片只切换选中态,不进入画布;底部批量工具栏提供全选 / 取消全选、已选数量、批量删除和退出选择模式。
|
||||
|
||||
@@ -46,11 +46,12 @@
|
||||
- 新增 `editor_asset_folder` 表保存账号级素材文件夹:`folderId`、`ownerUserId`、名称、排序、折叠状态、系统默认标记、创建时间和更新时间。素材文件夹不归属于 project,同一个账号进入任一项目都能看到。
|
||||
- 新增 `editor_asset` 表保存账号级素材:`assetId`、`ownerUserId`、`folderId`、名称、图片读取地址、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、`assetKind`、`generationInputs`、创建时间和更新时间。素材只跟账号走,不跟 project 走;角色、图标、UI 设计图、视频和音频等生成结果的用户可见输入快照随素材保存。
|
||||
- `editor_project_resource` 表保存工程画布引用过的资源快照:`resourceId`、`projectId`、`ownerUserId`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、sourceResourceId、`assetKind`、`generationInputs`、创建时间和更新时间。上传素材被拖入画布时会复制为 project resource,图层只引用 resourceId;图片、图标和 UI 素材生成 BFF 在请求携带 `projectId` 时由后端直接创建新 resource,并把 `resourceId` 随生成响应返回给前端。图片生成请求如果同时携带 `canvasCompletion`(生成器 `dialogId`、标题和占位框,或无 dialog 的右侧完成占位),BFF / worker 在生成成功后必须直接读取当前项目布局,优先使用最新 `generation-dialog` 占位框位置;只有当前布局仍存在对应 `generation-dialog` 时才插入轻量结果图层、把生成器标记为 `idle` 并写入 `generatedLayerId`,沿用后端当前 viewport 保存布局,再返回或刷新最新项目快照;前端只应用该快照刷新显示,不把生成完成态作为本地业务真相,也不在项目加载时根据资源行推断完成态。有项目上下文但后端没有返回项目快照时,前端不得本地补结果图层,只保留当前生成器交互状态等待下一次项目刷新。
|
||||
- 图片文件本体继续走 OSS,浏览器读取私有 generated 对象仍经 `/api/assets/read-url` 换签。
|
||||
- 当前 MVP 的本地上传先以 data URL 持久化在素材记录中,保证刷新和跨项目可见;后续接入正式 OSS 上传时,只替换 `imageSrc/objectKey/assetObjectId` 的写入方式,账号级素材表和画布资源表不变。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId` 或 `assetId` 写入参考图临时状态,生成请求仍使用临时状态中的图片源或 `objectKey`。
|
||||
- 图片、音频、视频和角色动画帧文件本体继续走 OSS / asset object;浏览器读取私有 generated 对象统一经 `/api/assets/read-url` 换签,签名 URL 可在 session 内复用,但不得作为持久化真相。
|
||||
- 登录态上传和生成结果必须先落 OSS / asset object,再向 `editor_project_resource` / `editor_asset` 写入轻量 `imageSrc: "/<objectKey>"`、`objectKey` 和 `assetObjectId`;未登录演示态可以在内存里使用 Data URL 预览,但项目、素材库、项目资源和 `editor_canvas.layers_json` 不得写入 `data:image/*`、`data:video/*`、`data:audio/*` 或 `blob:`。旧数据读取时如果已有 `objectKey`,`imageSrc` 归一成 `/<objectKey>`;没有 `objectKey` 的旧 Data URL 需要走修复上传并回写轻量引用。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId` 或 `assetId` 写入参考图临时状态,生成请求仍使用临时状态中的图片源或 `objectKey`。
|
||||
- 资源表保存资源和素材级元数据;图层位置、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。布局 JSON 是混合数组:普通图层按 `layerId/resourceId` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存,不新增单独表。普通图层的新保存不再把 `assetKind/generationInputs` 写入布局 JSON;刷新时优先从 `editor_project_resource` 恢复,旧布局中的同名字段只作为兼容兜底。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;宣发素材生成器还必须保存并恢复 `publicationWorkflowId`、`publicationGameInfo` 和 `publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口所需的图片 Data URL、signed URL 或 `objectKey` 只存在于提交前的临时参考图状态和请求体字段,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution`(`originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。
|
||||
- 图片类生成结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。视频结果当前只保存为画布视频资源,不进入图片素材库。
|
||||
- 图片类生成结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。视频结果当前只保存为画布视频资源,不进入图片素材库。
|
||||
- 前端不直接订阅 SpacetimeDB,统一通过 api-server 的 `/api/editor/projects*` BFF 读写。
|
||||
- 工程刷新恢复可先应用 session 级轻量项目快照缓存,让画布和素材 chrome 尽快显示;缓存快照必须排除 `data:*` / `blob:` 内联媒体,且在后端项目快照返回前不得触发自动保存。后端快照回来后覆盖本地缓存显示并恢复正常保存队列。
|
||||
- 未登录用户可以使用本地演示态,但不触发工程自动保存;真实图片生成 / 修改需要登录。编辑器 API 请求允许使用 refresh cookie 静默补 access token,但 401 / 403 只在编辑器局部提示登录,不清空整站登录态,也不把后端 requestId 直接作为生图弹窗主文案。
|
||||
|
||||
## 后端接口
|
||||
@@ -59,7 +60,7 @@
|
||||
- `GET /api/editor/projects`:读取当前用户所有图片画布工程,按更新时间倒序返回。
|
||||
- `POST /api/editor/projects`:创建图片画布工程。
|
||||
- `GET /api/editor/projects/{projectId}`:读取指定工程及资源列表。
|
||||
- `PATCH /api/editor/projects/{projectId}`:保存 viewport 与图层布局快照。
|
||||
- `PATCH /api/editor/projects/{projectId}`:保存 viewport 与图层布局快照;响应只包含 `{ projectId, canvasId, updatedAt }` ack,不返回完整工程快照。
|
||||
- `PATCH /api/editor/projects/{projectId}/metadata`:重命名指定工程。
|
||||
- `DELETE /api/editor/projects/{projectId}`:删除指定工程,并级联删除默认画布和资源元数据。
|
||||
- `POST /api/editor/projects/{projectId}/resources`:创建画布资源记录,接收上传资源或真实生成资源元数据。
|
||||
@@ -67,7 +68,7 @@
|
||||
- `POST /api/editor/assets/folders`:新建素材文件夹。
|
||||
- `PATCH /api/editor/assets/folders/{folderId}`:重命名、折叠 / 展开素材文件夹。
|
||||
- `DELETE /api/editor/assets/folders/{folderId}`:删除素材文件夹;系统默认文件夹不能删除,普通文件夹删除时素材移入默认文件夹。
|
||||
- `POST /api/editor/assets`:批量或单个创建账号级素材,支持按钮上传和拖拽上传后的 data URL / 后续 OSS 元数据。
|
||||
- `POST /api/editor/assets`:批量或单个创建账号级素材,登录态上传必须写入 OSS / asset object 引用和 `/<objectKey>` 轻量路径,不允许把 Data URL / signed URL 写入素材库。
|
||||
- `PATCH /api/editor/assets/{assetId}`:重命名素材或移动素材到文件夹。
|
||||
- `DELETE /api/editor/assets/{assetId}`:删除素材。已放入画布的 project resource 不被级联删除,避免旧画布丢图。
|
||||
- `POST /api/editor/images/generations`:按提示词调用 VectorEngine 生成图片;角色生成可携带 `model`、`aspectRatio`、`imageSize` 和 `referenceImageSrcs`,宣发素材携带 `kind: "publication-material"` 时固定归一为 `gpt-image-2`,不支持 `nanobanana2`。`nanobanana2` 参考图作为 `inline_data` 进入 `generateContent`,`gpt-image-2` 参考图进入 edits。携带参考图的快速编辑也走该接口,前端必须把参考图源预读成图片 Data URL 后放入 `referenceImageSrcs`;请求可携带 `projectId`、`assetFolderId`、`assetKind`、`generationInputs` 和 `sourceResourceId`,后端生成成功后创建 project resource / 账号素材并在响应中返回 resource / asset 快照。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,8 +31,8 @@ use crate::{
|
||||
editor_asset_payload_from_record, editor_project_payload_from_record,
|
||||
editor_project_resource_payload_from_record, extract_editor_ui_design_assets_for_owner,
|
||||
generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner,
|
||||
map_editor_project_error, normalize_optional_string, serialize_editor_asset_metadata,
|
||||
serialize_editor_layers,
|
||||
map_editor_project_error, normalize_editor_persisted_media_src, normalize_optional_string,
|
||||
serialize_editor_asset_metadata, serialize_editor_layers,
|
||||
},
|
||||
external_api_auth::ExternalApiPrincipal,
|
||||
http_error::AppError,
|
||||
@@ -468,6 +468,8 @@ pub async fn create_external_editor_asset(
|
||||
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
|
||||
let generation_inputs_json =
|
||||
serialize_editor_asset_metadata(payload.generation_inputs.clone())?;
|
||||
let object_key = normalize_optional_string(payload.object_key);
|
||||
let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?;
|
||||
let asset = state
|
||||
.spacetime_client()
|
||||
.create_editor_asset(EditorAssetCreateRecordInput {
|
||||
@@ -476,8 +478,8 @@ pub async fn create_external_editor_asset(
|
||||
folder_id: payload.folder_id,
|
||||
label: payload.label,
|
||||
asset_object_id: normalize_optional_string(payload.asset_object_id),
|
||||
image_src: payload.image_src,
|
||||
object_key: normalize_optional_string(payload.object_key),
|
||||
image_src,
|
||||
object_key,
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
source_type: payload.source_type,
|
||||
@@ -563,6 +565,8 @@ pub async fn create_external_editor_project_resource(
|
||||
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
|
||||
let generation_inputs_json =
|
||||
serialize_editor_asset_metadata(payload.generation_inputs.clone())?;
|
||||
let object_key = normalize_optional_string(payload.object_key);
|
||||
let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?;
|
||||
let resource = state
|
||||
.spacetime_client()
|
||||
.create_editor_project_resource(EditorProjectResourceCreateRecordInput {
|
||||
@@ -570,8 +574,8 @@ pub async fn create_external_editor_project_resource(
|
||||
project_id,
|
||||
owner_user_id: principal.owner_user_id().to_string(),
|
||||
asset_object_id: normalize_optional_string(payload.asset_object_id),
|
||||
image_src: payload.image_src,
|
||||
object_key: normalize_optional_string(payload.object_key),
|
||||
image_src,
|
||||
object_key,
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
source_type: payload.source_type,
|
||||
|
||||
@@ -162,6 +162,29 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_editor_project_layout_ack(
|
||||
&self,
|
||||
input: EditorProjectLayoutSaveRecordInput,
|
||||
) -> Result<EditorProjectLayoutSaveAckRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"save_editor_project_layout_ack",
|
||||
move |connection, sender| {
|
||||
connection.procedures().save_editor_project_layout_ack_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_editor_project_layout_save_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_editor_project_resource(
|
||||
&self,
|
||||
input: EditorProjectResourceCreateRecordInput,
|
||||
@@ -187,6 +210,31 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn repair_editor_project_resource_media(
|
||||
&self,
|
||||
input: EditorProjectResourceMediaRepairRecordInput,
|
||||
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"repair_editor_project_resource_media_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.repair_editor_project_resource_media_and_return_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_editor_project_resource_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_editor_asset_library(
|
||||
&self,
|
||||
owner_user_id: String,
|
||||
@@ -334,6 +382,31 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn repair_editor_asset_media(
|
||||
&self,
|
||||
input: EditorAssetMediaRepairRecordInput,
|
||||
) -> Result<EditorAssetRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"repair_editor_asset_media_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.repair_editor_asset_media_and_return_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_editor_asset_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_editor_asset(
|
||||
&self,
|
||||
input: EditorAssetDeleteRecordInput,
|
||||
|
||||
@@ -33,10 +33,11 @@ pub use mapper::{
|
||||
CustomWorldWorkSummaryRecord, EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput,
|
||||
EditorAssetFolderCreateRecordInput, EditorAssetFolderDeleteRecordInput,
|
||||
EditorAssetFolderRecord, EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord,
|
||||
EditorAssetRecord, EditorAssetUpdateRecordInput, EditorCanvasRecord,
|
||||
EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
|
||||
EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord,
|
||||
EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput,
|
||||
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
|
||||
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveAckRecord,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourceRecord, ExternalApiKeyAuthenticateRecordInput,
|
||||
ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, ExternalApiKeyRevokeRecordInput,
|
||||
ExternalGenerationJobClaimRecordInput, ExternalGenerationJobCompleteRecordInput,
|
||||
|
||||
@@ -75,11 +75,13 @@ pub use self::common::{
|
||||
pub use self::editor_project::{
|
||||
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
|
||||
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
|
||||
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord, EditorAssetRecord,
|
||||
EditorAssetUpdateRecordInput, EditorCanvasRecord, EditorCanvasViewportRecord,
|
||||
EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
|
||||
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord,
|
||||
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
|
||||
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveAckRecord,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourceRecord,
|
||||
};
|
||||
pub use self::external_api_key::{
|
||||
ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord,
|
||||
@@ -204,9 +206,9 @@ pub(crate) use self::custom_world::{
|
||||
pub(crate) use self::editor_project::{
|
||||
map_editor_asset_folder_library_procedure_result, map_editor_asset_folder_procedure_result,
|
||||
map_editor_asset_library_procedure_result, map_editor_asset_procedure_result,
|
||||
map_editor_project_delete_procedure_result, map_editor_project_list_procedure_result,
|
||||
map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result,
|
||||
map_editor_project_resource_procedure_result,
|
||||
map_editor_project_delete_procedure_result, map_editor_project_layout_save_procedure_result,
|
||||
map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result,
|
||||
map_editor_project_required_procedure_result, map_editor_project_resource_procedure_result,
|
||||
};
|
||||
pub(crate) use self::external_api_key::{
|
||||
map_external_api_key_list_procedure_result, map_external_api_key_single_procedure_result,
|
||||
|
||||
@@ -31,6 +31,14 @@ pub struct EditorCanvasRecord {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorProjectLayoutSaveAckRecord {
|
||||
pub project_id: String,
|
||||
pub canvas_id: String,
|
||||
pub updated_at: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorProjectResourceRecord {
|
||||
pub resource_id: String,
|
||||
@@ -151,6 +159,16 @@ pub struct EditorProjectResourceCreateRecordInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorProjectResourceMediaRepairRecordInput {
|
||||
pub resource_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub image_src: String,
|
||||
pub object_key: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorAssetFolderCreateRecordInput {
|
||||
pub folder_id: String,
|
||||
@@ -207,6 +225,16 @@ pub struct EditorAssetUpdateRecordInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorAssetMediaRepairRecordInput {
|
||||
pub asset_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub image_src: String,
|
||||
pub object_key: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorAssetDeleteRecordInput {
|
||||
pub asset_id: String,
|
||||
@@ -298,6 +326,21 @@ impl From<EditorProjectResourceCreateRecordInput>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorProjectResourceMediaRepairRecordInput>
|
||||
for crate::module_bindings::EditorProjectResourceMediaRepairInput
|
||||
{
|
||||
fn from(input: EditorProjectResourceMediaRepairRecordInput) -> Self {
|
||||
Self {
|
||||
resource_id: input.resource_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
asset_object_id: input.asset_object_id,
|
||||
image_src: input.image_src,
|
||||
object_key: input.object_key,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorAssetFolderCreateRecordInput>
|
||||
for crate::module_bindings::EditorAssetFolderCreateInput
|
||||
{
|
||||
@@ -375,6 +418,21 @@ impl From<EditorAssetUpdateRecordInput> for crate::module_bindings::EditorAssetU
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorAssetMediaRepairRecordInput>
|
||||
for crate::module_bindings::EditorAssetMediaRepairInput
|
||||
{
|
||||
fn from(input: EditorAssetMediaRepairRecordInput) -> Self {
|
||||
Self {
|
||||
asset_id: input.asset_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
asset_object_id: input.asset_object_id,
|
||||
image_src: input.image_src,
|
||||
object_key: input.object_key,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorAssetDeleteRecordInput> for crate::module_bindings::EditorAssetDeleteInput {
|
||||
fn from(input: EditorAssetDeleteRecordInput) -> Self {
|
||||
Self {
|
||||
@@ -401,6 +459,24 @@ pub(crate) fn map_editor_project_required_procedure_result(
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布工程快照"))
|
||||
}
|
||||
|
||||
pub(crate) fn map_editor_project_layout_save_procedure_result(
|
||||
result: EditorProjectLayoutSaveProcedureResult,
|
||||
) -> Result<EditorProjectLayoutSaveAckRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
let ack = result
|
||||
.ack
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布保存结果"))?;
|
||||
Ok(EditorProjectLayoutSaveAckRecord {
|
||||
project_id: ack.project_id,
|
||||
canvas_id: ack.canvas_id,
|
||||
updated_at: format_timestamp_micros(ack.updated_at_micros),
|
||||
updated_at_micros: ack.updated_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_editor_project_list_procedure_result(
|
||||
result: EditorProjectListProcedureResult,
|
||||
) -> Result<Vec<EditorProjectRecord>, SpacetimeClientError> {
|
||||
|
||||
@@ -366,6 +366,7 @@ pub mod editor_asset_folder_update_input_type;
|
||||
pub mod editor_asset_library_get_input_type;
|
||||
pub mod editor_asset_library_procedure_result_type;
|
||||
pub mod editor_asset_library_snapshot_type;
|
||||
pub mod editor_asset_media_repair_input_type;
|
||||
pub mod editor_asset_procedure_result_type;
|
||||
pub mod editor_asset_snapshot_type;
|
||||
pub mod editor_asset_table;
|
||||
@@ -379,12 +380,15 @@ pub mod editor_project_delete_input_type;
|
||||
pub mod editor_project_delete_procedure_result_type;
|
||||
pub mod editor_project_get_input_type;
|
||||
pub mod editor_project_get_recent_input_type;
|
||||
pub mod editor_project_layout_save_ack_type;
|
||||
pub mod editor_project_layout_save_input_type;
|
||||
pub mod editor_project_layout_save_procedure_result_type;
|
||||
pub mod editor_project_list_input_type;
|
||||
pub mod editor_project_list_procedure_result_type;
|
||||
pub mod editor_project_procedure_result_type;
|
||||
pub mod editor_project_rename_input_type;
|
||||
pub mod editor_project_resource_create_input_type;
|
||||
pub mod editor_project_resource_media_repair_input_type;
|
||||
pub mod editor_project_resource_procedure_result_type;
|
||||
pub mod editor_project_resource_snapshot_type;
|
||||
pub mod editor_project_resource_table;
|
||||
@@ -859,6 +863,8 @@ pub mod remix_custom_world_profile_procedure;
|
||||
pub mod remix_puzzle_work_procedure;
|
||||
pub mod rename_editor_project_and_return_procedure;
|
||||
pub mod renew_external_generation_job_lease_and_return_procedure;
|
||||
pub mod repair_editor_asset_media_and_return_procedure;
|
||||
pub mod repair_editor_project_resource_media_and_return_procedure;
|
||||
pub mod resolve_combat_action_and_return_procedure;
|
||||
pub mod resolve_combat_action_input_type;
|
||||
pub mod resolve_combat_action_procedure_result_type;
|
||||
@@ -997,6 +1003,7 @@ pub mod runtime_tracking_event_batch_procedure_result_type;
|
||||
pub mod runtime_tracking_event_input_type;
|
||||
pub mod runtime_tracking_event_procedure_result_type;
|
||||
pub mod runtime_tracking_scope_kind_type;
|
||||
pub mod save_editor_project_layout_ack_procedure;
|
||||
pub mod save_editor_project_layout_and_return_procedure;
|
||||
pub mod save_puzzle_form_draft_procedure;
|
||||
pub mod save_puzzle_generated_images_procedure;
|
||||
@@ -1568,6 +1575,7 @@ pub use editor_asset_folder_update_input_type::EditorAssetFolderUpdateInput;
|
||||
pub use editor_asset_library_get_input_type::EditorAssetLibraryGetInput;
|
||||
pub use editor_asset_library_procedure_result_type::EditorAssetLibraryProcedureResult;
|
||||
pub use editor_asset_library_snapshot_type::EditorAssetLibrarySnapshot;
|
||||
pub use editor_asset_media_repair_input_type::EditorAssetMediaRepairInput;
|
||||
pub use editor_asset_procedure_result_type::EditorAssetProcedureResult;
|
||||
pub use editor_asset_snapshot_type::EditorAssetSnapshot;
|
||||
pub use editor_asset_table::*;
|
||||
@@ -1581,12 +1589,15 @@ pub use editor_project_delete_input_type::EditorProjectDeleteInput;
|
||||
pub use editor_project_delete_procedure_result_type::EditorProjectDeleteProcedureResult;
|
||||
pub use editor_project_get_input_type::EditorProjectGetInput;
|
||||
pub use editor_project_get_recent_input_type::EditorProjectGetRecentInput;
|
||||
pub use editor_project_layout_save_ack_type::EditorProjectLayoutSaveAck;
|
||||
pub use editor_project_layout_save_input_type::EditorProjectLayoutSaveInput;
|
||||
pub use editor_project_layout_save_procedure_result_type::EditorProjectLayoutSaveProcedureResult;
|
||||
pub use editor_project_list_input_type::EditorProjectListInput;
|
||||
pub use editor_project_list_procedure_result_type::EditorProjectListProcedureResult;
|
||||
pub use editor_project_procedure_result_type::EditorProjectProcedureResult;
|
||||
pub use editor_project_rename_input_type::EditorProjectRenameInput;
|
||||
pub use editor_project_resource_create_input_type::EditorProjectResourceCreateInput;
|
||||
pub use editor_project_resource_media_repair_input_type::EditorProjectResourceMediaRepairInput;
|
||||
pub use editor_project_resource_procedure_result_type::EditorProjectResourceProcedureResult;
|
||||
pub use editor_project_resource_snapshot_type::EditorProjectResourceSnapshot;
|
||||
pub use editor_project_resource_table::*;
|
||||
@@ -2061,6 +2072,8 @@ pub use remix_custom_world_profile_procedure::remix_custom_world_profile;
|
||||
pub use remix_puzzle_work_procedure::remix_puzzle_work;
|
||||
pub use rename_editor_project_and_return_procedure::rename_editor_project_and_return;
|
||||
pub use renew_external_generation_job_lease_and_return_procedure::renew_external_generation_job_lease_and_return;
|
||||
pub use repair_editor_asset_media_and_return_procedure::repair_editor_asset_media_and_return;
|
||||
pub use repair_editor_project_resource_media_and_return_procedure::repair_editor_project_resource_media_and_return;
|
||||
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
|
||||
pub use resolve_combat_action_input_type::ResolveCombatActionInput;
|
||||
pub use resolve_combat_action_procedure_result_type::ResolveCombatActionProcedureResult;
|
||||
@@ -2199,6 +2212,7 @@ pub use runtime_tracking_event_batch_procedure_result_type::RuntimeTrackingEvent
|
||||
pub use runtime_tracking_event_input_type::RuntimeTrackingEventInput;
|
||||
pub use runtime_tracking_event_procedure_result_type::RuntimeTrackingEventProcedureResult;
|
||||
pub use runtime_tracking_scope_kind_type::RuntimeTrackingScopeKind;
|
||||
pub use save_editor_project_layout_ack_procedure::save_editor_project_layout_ack;
|
||||
pub use save_editor_project_layout_and_return_procedure::save_editor_project_layout_and_return;
|
||||
pub use save_puzzle_form_draft_procedure::save_puzzle_form_draft;
|
||||
pub use save_puzzle_generated_images_procedure::save_puzzle_generated_images;
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct EditorAssetMediaRepairInput {
|
||||
pub asset_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub image_src: String,
|
||||
pub object_key: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorAssetMediaRepairInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct EditorProjectLayoutSaveAck {
|
||||
pub project_id: String,
|
||||
pub canvas_id: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectLayoutSaveAck {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::editor_project_layout_save_ack_type::EditorProjectLayoutSaveAck;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct EditorProjectLayoutSaveProcedureResult {
|
||||
pub ok: bool,
|
||||
pub ack: Option<EditorProjectLayoutSaveAck>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectLayoutSaveProcedureResult {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct EditorProjectResourceMediaRepairInput {
|
||||
pub resource_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub image_src: String,
|
||||
pub object_key: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectResourceMediaRepairInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::editor_asset_media_repair_input_type::EditorAssetMediaRepairInput;
|
||||
use super::editor_asset_procedure_result_type::EditorAssetProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RepairEditorAssetMediaAndReturnArgs {
|
||||
pub input: EditorAssetMediaRepairInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RepairEditorAssetMediaAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `repair_editor_asset_media_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait repair_editor_asset_media_and_return {
|
||||
fn repair_editor_asset_media_and_return(&self, input: EditorAssetMediaRepairInput) {
|
||||
self.repair_editor_asset_media_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn repair_editor_asset_media_and_return_then(
|
||||
&self,
|
||||
input: EditorAssetMediaRepairInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorAssetProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl repair_editor_asset_media_and_return for super::RemoteProcedures {
|
||||
fn repair_editor_asset_media_and_return_then(
|
||||
&self,
|
||||
input: EditorAssetMediaRepairInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorAssetProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorAssetProcedureResult>(
|
||||
"repair_editor_asset_media_and_return",
|
||||
RepairEditorAssetMediaAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::editor_project_resource_media_repair_input_type::EditorProjectResourceMediaRepairInput;
|
||||
use super::editor_project_resource_procedure_result_type::EditorProjectResourceProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RepairEditorProjectResourceMediaAndReturnArgs {
|
||||
pub input: EditorProjectResourceMediaRepairInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RepairEditorProjectResourceMediaAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `repair_editor_project_resource_media_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait repair_editor_project_resource_media_and_return {
|
||||
fn repair_editor_project_resource_media_and_return(
|
||||
&self,
|
||||
input: EditorProjectResourceMediaRepairInput,
|
||||
) {
|
||||
self.repair_editor_project_resource_media_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn repair_editor_project_resource_media_and_return_then(
|
||||
&self,
|
||||
input: EditorProjectResourceMediaRepairInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectResourceProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl repair_editor_project_resource_media_and_return for super::RemoteProcedures {
|
||||
fn repair_editor_project_resource_media_and_return_then(
|
||||
&self,
|
||||
input: EditorProjectResourceMediaRepairInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectResourceProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorProjectResourceProcedureResult>(
|
||||
"repair_editor_project_resource_media_and_return",
|
||||
RepairEditorProjectResourceMediaAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::editor_project_layout_save_input_type::EditorProjectLayoutSaveInput;
|
||||
use super::editor_project_layout_save_procedure_result_type::EditorProjectLayoutSaveProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct SaveEditorProjectLayoutAckArgs {
|
||||
pub input: EditorProjectLayoutSaveInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for SaveEditorProjectLayoutAckArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `save_editor_project_layout_ack`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait save_editor_project_layout_ack {
|
||||
fn save_editor_project_layout_ack(&self, input: EditorProjectLayoutSaveInput) {
|
||||
self.save_editor_project_layout_ack_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn save_editor_project_layout_ack_then(
|
||||
&self,
|
||||
input: EditorProjectLayoutSaveInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectLayoutSaveProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl save_editor_project_layout_ack for super::RemoteProcedures {
|
||||
fn save_editor_project_layout_ack_then(
|
||||
&self,
|
||||
input: EditorProjectLayoutSaveInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectLayoutSaveProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorProjectLayoutSaveProcedureResult>(
|
||||
"save_editor_project_layout_ack",
|
||||
SaveEditorProjectLayoutAckArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,16 @@ pub struct EditorProjectResourceCreateInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorProjectResourceMediaRepairInput {
|
||||
pub resource_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub image_src: String,
|
||||
pub object_key: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorProjectResourceSnapshot {
|
||||
pub resource_id: String,
|
||||
@@ -320,6 +330,16 @@ pub struct EditorAssetUpdateInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorAssetMediaRepairInput {
|
||||
pub asset_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub asset_object_id: String,
|
||||
pub image_src: String,
|
||||
pub object_key: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorAssetDeleteInput {
|
||||
pub asset_id: String,
|
||||
@@ -355,6 +375,20 @@ pub struct EditorProjectProcedureResult {
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorProjectLayoutSaveAck {
|
||||
pub project_id: String,
|
||||
pub canvas_id: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorProjectLayoutSaveProcedureResult {
|
||||
pub ok: bool,
|
||||
pub ack: Option<EditorProjectLayoutSaveAck>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
|
||||
pub struct EditorProjectListProcedureResult {
|
||||
pub ok: bool,
|
||||
@@ -461,6 +495,17 @@ pub fn save_editor_project_layout_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn save_editor_project_layout_ack(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: EditorProjectLayoutSaveInput,
|
||||
) -> EditorProjectLayoutSaveProcedureResult {
|
||||
match ctx.try_with_tx(|tx| persist_editor_project_layout(tx, input.clone())) {
|
||||
Ok(ack) => editor_project_layout_save_ok(ack),
|
||||
Err(message) => editor_project_layout_save_error(message),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn rename_editor_project_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -510,6 +555,25 @@ pub fn create_editor_project_resource_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn repair_editor_project_resource_media_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: EditorProjectResourceMediaRepairInput,
|
||||
) -> EditorProjectResourceProcedureResult {
|
||||
match ctx.try_with_tx(|tx| repair_editor_project_resource_media(tx, input.clone())) {
|
||||
Ok(resource) => EditorProjectResourceProcedureResult {
|
||||
ok: true,
|
||||
resource: Some(resource),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => EditorProjectResourceProcedureResult {
|
||||
ok: false,
|
||||
resource: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_editor_asset_library_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -576,6 +640,17 @@ pub fn update_editor_asset_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn repair_editor_asset_media_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: EditorAssetMediaRepairInput,
|
||||
) -> EditorAssetProcedureResult {
|
||||
match ctx.try_with_tx(|tx| repair_editor_asset_media(tx, input.clone())) {
|
||||
Ok(asset) => editor_asset_ok(Some(asset)),
|
||||
Err(message) => editor_asset_error(message),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn delete_editor_asset_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -750,6 +825,14 @@ fn save_editor_project_layout(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorProjectLayoutSaveInput,
|
||||
) -> Result<EditorProjectSnapshot, String> {
|
||||
let ack = persist_editor_project_layout(ctx, input)?;
|
||||
build_project_snapshot(ctx, ack.project_id.as_str())
|
||||
}
|
||||
|
||||
fn persist_editor_project_layout(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorProjectLayoutSaveInput,
|
||||
) -> Result<EditorProjectLayoutSaveAck, String> {
|
||||
let project_id = normalize_required(&input.project_id, "editor_project.project_id")?;
|
||||
let owner_user_id = normalize_required(&input.owner_user_id, "editor_project.owner_user_id")?;
|
||||
let input_layers_json = normalize_layout_json(input.layers_json)?;
|
||||
@@ -762,6 +845,7 @@ fn save_editor_project_layout(
|
||||
Some(&project),
|
||||
now,
|
||||
)?;
|
||||
let canvas_id = canvas.canvas_id.clone();
|
||||
let _ = migrate_canvas_layer_asset_metadata(
|
||||
ctx,
|
||||
project.project_id.as_str(),
|
||||
@@ -803,7 +887,11 @@ fn save_editor_project_layout(
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
build_project_snapshot(ctx, project_id.as_str())
|
||||
Ok(EditorProjectLayoutSaveAck {
|
||||
project_id,
|
||||
canvas_id,
|
||||
updated_at_micros: now.to_micros_since_unix_epoch(),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_editor_project_resource(
|
||||
@@ -870,6 +958,62 @@ fn create_editor_project_resource(
|
||||
.ok_or_else(|| "画布资源创建失败".to_string())
|
||||
}
|
||||
|
||||
fn repair_editor_project_resource_media(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorProjectResourceMediaRepairInput,
|
||||
) -> Result<EditorProjectResourceSnapshot, String> {
|
||||
let resource_id =
|
||||
normalize_required(&input.resource_id, "editor_project_resource.resource_id")?;
|
||||
let owner_user_id = normalize_required(
|
||||
&input.owner_user_id,
|
||||
"editor_project_resource.owner_user_id",
|
||||
)?;
|
||||
let resource =
|
||||
require_owned_project_resource(ctx, resource_id.as_str(), owner_user_id.as_str())?;
|
||||
let image_src = normalize_required(&input.image_src, "editor_project_resource.image_src")?;
|
||||
let object_key = normalize_required(&input.object_key, "editor_project_resource.object_key")?;
|
||||
let asset_object_id = normalize_required(
|
||||
&input.asset_object_id,
|
||||
"editor_project_resource.asset_object_id",
|
||||
)?;
|
||||
let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
|
||||
ctx.db
|
||||
.editor_project_resource()
|
||||
.resource_id()
|
||||
.delete(&resource_id);
|
||||
ctx.db
|
||||
.editor_project_resource()
|
||||
.insert(EditorProjectResource {
|
||||
resource_id: resource.resource_id.clone(),
|
||||
project_id: resource.project_id,
|
||||
owner_user_id: resource.owner_user_id,
|
||||
asset_object_id: Some(asset_object_id),
|
||||
image_src,
|
||||
object_key: Some(object_key),
|
||||
width: resource.width,
|
||||
height: resource.height,
|
||||
source_type: resource.source_type,
|
||||
prompt: resource.prompt,
|
||||
actual_prompt: resource.actual_prompt,
|
||||
model: resource.model,
|
||||
provider: resource.provider,
|
||||
task_id: resource.task_id,
|
||||
source_resource_id: resource.source_resource_id,
|
||||
created_at: resource.created_at,
|
||||
updated_at: now,
|
||||
asset_kind: resource.asset_kind,
|
||||
generation_inputs_json: resource.generation_inputs_json,
|
||||
});
|
||||
|
||||
ctx.db
|
||||
.editor_project_resource()
|
||||
.resource_id()
|
||||
.find(&resource_id)
|
||||
.map(resource_snapshot_from_row)
|
||||
.ok_or_else(|| "画布资源媒体修复失败".to_string())
|
||||
}
|
||||
|
||||
fn get_editor_asset_library(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorAssetLibraryGetInput,
|
||||
@@ -1076,6 +1220,50 @@ fn update_editor_asset(
|
||||
.ok_or_else(|| "素材更新失败".to_string())
|
||||
}
|
||||
|
||||
fn repair_editor_asset_media(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorAssetMediaRepairInput,
|
||||
) -> Result<EditorAssetSnapshot, String> {
|
||||
let asset_id = normalize_required(&input.asset_id, "editor_asset.asset_id")?;
|
||||
let owner_user_id = normalize_required(&input.owner_user_id, "editor_asset.owner_user_id")?;
|
||||
let asset = require_owned_asset(ctx, asset_id.as_str(), owner_user_id.as_str())?;
|
||||
let image_src = normalize_required(&input.image_src, "editor_asset.image_src")?;
|
||||
let object_key = normalize_required(&input.object_key, "editor_asset.object_key")?;
|
||||
let asset_object_id =
|
||||
normalize_required(&input.asset_object_id, "editor_asset.asset_object_id")?;
|
||||
let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
|
||||
ctx.db.editor_asset().asset_id().delete(&asset_id);
|
||||
ctx.db.editor_asset().insert(EditorAsset {
|
||||
asset_id: asset.asset_id.clone(),
|
||||
owner_user_id: asset.owner_user_id,
|
||||
folder_id: asset.folder_id,
|
||||
label: asset.label,
|
||||
asset_object_id: Some(asset_object_id),
|
||||
image_src,
|
||||
object_key: Some(object_key),
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
source_type: asset.source_type,
|
||||
prompt: asset.prompt,
|
||||
actual_prompt: asset.actual_prompt,
|
||||
model: asset.model,
|
||||
provider: asset.provider,
|
||||
task_id: asset.task_id,
|
||||
created_at: asset.created_at,
|
||||
updated_at: now,
|
||||
asset_kind: asset.asset_kind,
|
||||
generation_inputs_json: asset.generation_inputs_json,
|
||||
});
|
||||
|
||||
ctx.db
|
||||
.editor_asset()
|
||||
.asset_id()
|
||||
.find(&asset_id)
|
||||
.map(asset_snapshot_from_row)
|
||||
.ok_or_else(|| "素材媒体修复失败".to_string())
|
||||
}
|
||||
|
||||
fn delete_editor_asset(
|
||||
ctx: &ReducerContext,
|
||||
input: EditorAssetDeleteInput,
|
||||
@@ -1211,6 +1399,24 @@ fn require_owned_project(
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
fn require_owned_project_resource(
|
||||
ctx: &ReducerContext,
|
||||
resource_id: &str,
|
||||
owner_user_id: &str,
|
||||
) -> Result<EditorProjectResource, String> {
|
||||
let resource_key = resource_id.to_string();
|
||||
let resource = ctx
|
||||
.db
|
||||
.editor_project_resource()
|
||||
.resource_id()
|
||||
.find(&resource_key)
|
||||
.ok_or_else(|| "画布资源不存在".to_string())?;
|
||||
if resource.owner_user_id != owner_user_id {
|
||||
return Err("无权访问该画布资源".to_string());
|
||||
}
|
||||
Ok(resource)
|
||||
}
|
||||
|
||||
fn ensure_default_asset_folder(
|
||||
ctx: &ReducerContext,
|
||||
owner_user_id: &str,
|
||||
@@ -1540,6 +1746,24 @@ fn editor_project_error(message: String) -> EditorProjectProcedureResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_project_layout_save_ok(
|
||||
ack: EditorProjectLayoutSaveAck,
|
||||
) -> EditorProjectLayoutSaveProcedureResult {
|
||||
EditorProjectLayoutSaveProcedureResult {
|
||||
ok: true,
|
||||
ack: Some(ack),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_project_layout_save_error(message: String) -> EditorProjectLayoutSaveProcedureResult {
|
||||
EditorProjectLayoutSaveProcedureResult {
|
||||
ok: false,
|
||||
ack: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_asset_library_ok(
|
||||
library: EditorAssetLibrarySnapshot,
|
||||
) -> EditorAssetLibraryProcedureResult {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PlatformMediaFrame } from './PlatformMediaFrame';
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: ({
|
||||
src,
|
||||
objectKey,
|
||||
fallbackSrc,
|
||||
alt,
|
||||
className,
|
||||
@@ -16,6 +17,7 @@ vi.mock('../ResolvedAssetImage', () => ({
|
||||
...imageProps
|
||||
}: {
|
||||
src?: string;
|
||||
objectKey?: string;
|
||||
fallbackSrc?: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
@@ -24,6 +26,7 @@ vi.mock('../ResolvedAssetImage', () => ({
|
||||
<img
|
||||
{...imageProps}
|
||||
src={src ?? fallbackSrc}
|
||||
data-object-key={objectKey ?? undefined}
|
||||
data-fallback-src={fallbackSrc}
|
||||
data-refresh-key={refreshKey ?? undefined}
|
||||
alt={alt}
|
||||
@@ -222,6 +225,45 @@ test('supports standard aspect, bare surface and refresh key', () => {
|
||||
expect(image.getAttribute('data-refresh-key')).toBe('session-1:level-1');
|
||||
});
|
||||
|
||||
test('passes objectKey to the resolved asset image', () => {
|
||||
render(
|
||||
<PlatformMediaFrame
|
||||
src="/generated-character-drafts/editor/spec.png"
|
||||
objectKey="generated-character-drafts/editor/spec.png"
|
||||
alt="规范图"
|
||||
fallbackLabel="规范图"
|
||||
aspect="square"
|
||||
surface="bare"
|
||||
/>,
|
||||
);
|
||||
|
||||
const image = screen.getByRole('img', { name: '规范图' });
|
||||
|
||||
expect(image.getAttribute('data-object-key')).toBe(
|
||||
'generated-character-drafts/editor/spec.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('renders an image when only objectKey is available', () => {
|
||||
render(
|
||||
<PlatformMediaFrame
|
||||
objectKey="generated-character-drafts/editor/spec-only.png"
|
||||
alt="仅 objectKey 规范图"
|
||||
fallbackLabel="规范图"
|
||||
aspect="square"
|
||||
surface="bare"
|
||||
/>,
|
||||
);
|
||||
|
||||
const image = screen.getByRole('img', { name: '仅 objectKey 规范图' });
|
||||
|
||||
expect(image.getAttribute('src')).toBe(null);
|
||||
expect(image.getAttribute('data-object-key')).toBe(
|
||||
'generated-character-drafts/editor/spec-only.png',
|
||||
);
|
||||
expect(screen.queryByText('规范图')).toBeNull();
|
||||
});
|
||||
|
||||
test('supports portrait aspect for vertical preview assets', () => {
|
||||
render(
|
||||
<PlatformMediaFrame
|
||||
|
||||
@@ -29,6 +29,7 @@ type PlatformMediaFrameProps = Omit<
|
||||
'children' | 'className'
|
||||
> & {
|
||||
src?: string | null;
|
||||
objectKey?: string | null;
|
||||
fallbackSrc?: string | null;
|
||||
alt: string;
|
||||
fallbackLabel: string;
|
||||
@@ -88,6 +89,7 @@ export const PlatformMediaFrame = forwardRef<
|
||||
>(function PlatformMediaFrame(
|
||||
{
|
||||
src,
|
||||
objectKey,
|
||||
fallbackSrc,
|
||||
alt,
|
||||
fallbackLabel,
|
||||
@@ -108,7 +110,9 @@ export const PlatformMediaFrame = forwardRef<
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const imageSrc = src?.trim() || fallbackSrc?.trim() || '';
|
||||
const hasImageSource = Boolean(
|
||||
src?.trim() || objectKey?.trim() || fallbackSrc?.trim(),
|
||||
);
|
||||
const hasOverlay = Boolean(children) || Boolean(previewOverlay);
|
||||
|
||||
return (
|
||||
@@ -124,10 +128,11 @@ export const PlatformMediaFrame = forwardRef<
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{imageSrc ? (
|
||||
{hasImageSource ? (
|
||||
<ResolvedAssetImage
|
||||
{...imageProps}
|
||||
src={src?.trim() || undefined}
|
||||
objectKey={objectKey?.trim() || undefined}
|
||||
fallbackSrc={fallbackSrc?.trim() || undefined}
|
||||
alt={alt}
|
||||
loading={loading}
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { EditorAsset } from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasAssetRowView } from './ImageCanvasAssetRowView';
|
||||
|
||||
vi.mock('../common/PlatformMediaFrame', () => ({
|
||||
PlatformMediaFrame: ({
|
||||
src,
|
||||
objectKey,
|
||||
alt,
|
||||
previewOverlay,
|
||||
}: {
|
||||
src?: string | null;
|
||||
objectKey?: string | null;
|
||||
alt: string;
|
||||
previewOverlay?: ReactNode;
|
||||
}) => (
|
||||
<div className="platform-media-frame">
|
||||
{src?.trim() ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
data-object-key={objectKey?.trim() || undefined}
|
||||
/>
|
||||
) : null}
|
||||
{previewOverlay}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
function createAsset(overrides: Partial<EditorAsset> = {}): EditorAsset {
|
||||
return {
|
||||
id: 'asset-1',
|
||||
@@ -164,4 +190,21 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
?.className,
|
||||
).toContain('image-canvas-editor__media-preview--variant-');
|
||||
});
|
||||
|
||||
it('passes objectKey to image thumbnails in the asset library', () => {
|
||||
renderAssetRow({
|
||||
asset: createAsset({
|
||||
label: '规范图',
|
||||
src: '/generated-character-drafts/editor/spec.png',
|
||||
objectKey: 'generated-character-drafts/editor/spec.png',
|
||||
mediaType: 'image',
|
||||
}),
|
||||
});
|
||||
|
||||
const image = screen.getByRole('img', { name: '素材:规范图' });
|
||||
|
||||
expect(image.getAttribute('data-object-key')).toBe(
|
||||
'generated-character-drafts/editor/spec.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,6 +203,7 @@ export function ImageCanvasAssetRowView({
|
||||
? asset.src
|
||||
: (asset.thumbnailSrc ?? '')
|
||||
}
|
||||
objectKey={asset.mediaType === 'image' ? asset.objectKey : undefined}
|
||||
titleNode={
|
||||
isUploadingAsset || isFailedUpload ? <span>{asset.label}</span> : titleNode
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||
const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn());
|
||||
const uploadEditorMediaAssetFileMock = vi.hoisted(() => vi.fn());
|
||||
const getSignedAssetReadUrlMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
const actual = await vi.importActual<
|
||||
@@ -69,6 +70,17 @@ vi.mock('../../services/image-editor/editorMediaAssetUploadClient', () => ({
|
||||
uploadEditorMediaAssetFile: uploadEditorMediaAssetFileMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/assetReadUrlService', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../../services/assetReadUrlService')>(
|
||||
'../../services/assetReadUrlService',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
getSignedAssetReadUrl: getSignedAssetReadUrlMock,
|
||||
};
|
||||
});
|
||||
|
||||
function createSizedPngFile(name: string, width: number, height: number) {
|
||||
const bytes = new Uint8Array([
|
||||
0x89,
|
||||
@@ -108,6 +120,27 @@ function createSizedPngFile(name: string, width: number, height: number) {
|
||||
return new File([bytes], name, { type: 'image/png' });
|
||||
}
|
||||
|
||||
function createTestImageAsset({
|
||||
assetId,
|
||||
label,
|
||||
}: {
|
||||
assetId: string;
|
||||
label: string;
|
||||
}) {
|
||||
const objectKey = `generated-character-drafts/editor/asset-library/image/${assetId}.png`;
|
||||
return {
|
||||
assetId,
|
||||
folderId: 'project',
|
||||
label,
|
||||
imageSrc: `/${objectKey}`,
|
||||
objectKey,
|
||||
assetObjectId: `assetobj-${assetId}`,
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded' as const,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ImageCanvasEditorView asset library integration', () => {
|
||||
setupImageCanvasEditorViewTestLifecycle({
|
||||
generateEditorImageMock,
|
||||
@@ -129,8 +162,12 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getSignedAssetReadUrlMock.mockImplementation(
|
||||
async ({ objectKey }: { objectKey?: string }) =>
|
||||
`https://signed.example.com/${objectKey ?? 'asset'}`,
|
||||
);
|
||||
uploadEditorMediaAssetFileMock.mockImplementation(
|
||||
async (file: File, mediaType: 'video' | 'audio') => ({
|
||||
async (file: File, mediaType: 'image' | 'video' | 'audio') => ({
|
||||
src: `https://signed.example.com/editor/${mediaType}/${file.name}`,
|
||||
legacyPublicPath: `/generated-character-drafts/editor/asset-library/${mediaType}/${file.name}`,
|
||||
objectKey: `generated-character-drafts/editor/asset-library/${mediaType}/${file.name}`,
|
||||
@@ -577,6 +614,8 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
folderId: string;
|
||||
label: string;
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
sourceType: 'uploaded';
|
||||
@@ -599,7 +638,11 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
assetId: 'asset-upload-progress',
|
||||
folderId: 'project',
|
||||
label: '素材上传进度.png',
|
||||
imageSrc: 'data:image/png;base64,cHJvZ3Jlc3M=',
|
||||
imageSrc:
|
||||
'/generated-character-drafts/editor/asset-library/image/素材上传进度.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/image/素材上传进度.png',
|
||||
assetObjectId: 'assetobj-editor-image',
|
||||
width: 420,
|
||||
height: 315,
|
||||
sourceType: 'uploaded',
|
||||
@@ -667,24 +710,8 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
assetId: 'asset-a',
|
||||
folderId: 'project',
|
||||
label: '账号素材A',
|
||||
imageSrc: 'data:image/png;base64,YQ==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
{
|
||||
assetId: 'asset-b',
|
||||
folderId: 'project',
|
||||
label: '账号素材B',
|
||||
imageSrc: 'data:image/png;base64,Yg==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
createTestImageAsset({ assetId: 'asset-a', label: '账号素材A' }),
|
||||
createTestImageAsset({ assetId: 'asset-b', label: '账号素材B' }),
|
||||
],
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
@@ -718,24 +745,8 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
assetId: 'asset-a',
|
||||
folderId: 'project',
|
||||
label: '账号素材A',
|
||||
imageSrc: 'data:image/png;base64,YQ==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
{
|
||||
assetId: 'asset-b',
|
||||
folderId: 'project',
|
||||
label: '账号素材B',
|
||||
imageSrc: 'data:image/png;base64,Yg==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
createTestImageAsset({ assetId: 'asset-a', label: '账号素材A' }),
|
||||
createTestImageAsset({ assetId: 'asset-b', label: '账号素材B' }),
|
||||
],
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
@@ -781,7 +792,9 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
createEditorProjectResourceMock.mockResolvedValueOnce({
|
||||
resourceId: 'resource-added-asset-a',
|
||||
projectId: 'editor-project-default',
|
||||
imageSrc: 'data:image/png;base64,YQ==',
|
||||
imageSrc: '/generated-character-drafts/editor/asset-library/image/asset-a.png',
|
||||
objectKey: 'generated-character-drafts/editor/asset-library/image/asset-a.png',
|
||||
assetObjectId: 'assetobj-asset-a',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
@@ -805,15 +818,7 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
assetId: 'asset-a',
|
||||
folderId: 'project',
|
||||
label: '账号素材A',
|
||||
imageSrc: 'data:image/png;base64,YQ==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
createTestImageAsset({ assetId: 'asset-a', label: '账号素材A' }),
|
||||
],
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
@@ -852,24 +857,8 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
{
|
||||
assetId: 'asset-a',
|
||||
folderId: 'project',
|
||||
label: '账号素材A',
|
||||
imageSrc: 'data:image/png;base64,YQ==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
{
|
||||
assetId: 'asset-b',
|
||||
folderId: 'project',
|
||||
label: '账号素材B',
|
||||
imageSrc: 'data:image/png;base64,Yg==',
|
||||
width: 320,
|
||||
height: 240,
|
||||
sourceType: 'uploaded',
|
||||
},
|
||||
createTestImageAsset({ assetId: 'asset-a', label: '账号素材A' }),
|
||||
createTestImageAsset({ assetId: 'asset-b', label: '账号素材B' }),
|
||||
],
|
||||
});
|
||||
render(<ImageCanvasEditorView />);
|
||||
@@ -970,7 +959,11 @@ describe('ImageCanvasEditorView asset library integration', () => {
|
||||
expect(createEditorAssetMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
label: '测试上传.png',
|
||||
imageSrc: expect.stringMatching(/^data:image\/png;base64,/u),
|
||||
imageSrc:
|
||||
'/generated-character-drafts/editor/asset-library/image/测试上传.png',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/image/测试上传.png',
|
||||
assetObjectId: 'assetobj-editor-image',
|
||||
}),
|
||||
);
|
||||
expect(screen.getByRole('heading', { name: '素材' })).toBeTruthy();
|
||||
|
||||
@@ -331,12 +331,16 @@ describe('ImageCanvasEditorModel', () => {
|
||||
{
|
||||
frameIndex: 1,
|
||||
imageSrc: '/generated-character-drafts/editor/frame01.png',
|
||||
objectKey: 'generated-character-drafts/editor/frame01.png',
|
||||
assetObjectId: 'assetobj-frame-01',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
{
|
||||
frameIndex: 2,
|
||||
imageSrc: '/generated-character-drafts/editor/frame02.png',
|
||||
objectKey: 'generated-character-drafts/editor/frame02.png',
|
||||
assetObjectId: 'assetobj-frame-02',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
|
||||
@@ -186,6 +186,69 @@ export function createLayerFromAsset(
|
||||
} satisfies CanvasLayer;
|
||||
}
|
||||
|
||||
export function isInlineEditorMediaSource(value: string | null | undefined) {
|
||||
const normalizedValue = value?.trim().toLowerCase() ?? '';
|
||||
return (
|
||||
normalizedValue.startsWith('data:image/') ||
|
||||
normalizedValue.startsWith('data:video/') ||
|
||||
normalizedValue.startsWith('data:audio/') ||
|
||||
normalizedValue.startsWith('blob:')
|
||||
);
|
||||
}
|
||||
|
||||
function isSignedEditorMediaSource(value: string | null | undefined) {
|
||||
const normalizedValue = value?.trim().toLowerCase() ?? '';
|
||||
return (
|
||||
/^(?:https?:)?\/\//u.test(normalizedValue) &&
|
||||
(normalizedValue.includes('x-oss-signature') ||
|
||||
normalizedValue.includes('x-oss-credential') ||
|
||||
normalizedValue.includes('expires=') ||
|
||||
normalizedValue.includes('signature='))
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeObjectKeyPath(objectKey: string | null | undefined) {
|
||||
const normalizedObjectKey = objectKey?.trim().replace(/^\/+/u, '') ?? '';
|
||||
return normalizedObjectKey ? `/${normalizedObjectKey}` : undefined;
|
||||
}
|
||||
|
||||
function serializeOptionalMediaSource(
|
||||
source: string | null | undefined,
|
||||
objectKey?: string | null,
|
||||
) {
|
||||
const normalizedSource = source?.trim() ?? '';
|
||||
if (!normalizedSource) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
isInlineEditorMediaSource(normalizedSource) ||
|
||||
isSignedEditorMediaSource(normalizedSource)
|
||||
) {
|
||||
return normalizeObjectKeyPath(objectKey);
|
||||
}
|
||||
return normalizedSource;
|
||||
}
|
||||
|
||||
function serializeImageSequenceFrames(
|
||||
frames: CanvasLayer['imageSequenceFrames'],
|
||||
) {
|
||||
if (!frames?.length) {
|
||||
return undefined;
|
||||
}
|
||||
const serializedFrames = frames.flatMap((frame) => {
|
||||
const imageSrc = serializeOptionalMediaSource(frame.imageSrc, frame.objectKey);
|
||||
return imageSrc
|
||||
? [
|
||||
{
|
||||
...frame,
|
||||
imageSrc,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
return serializedFrames.length ? serializedFrames : undefined;
|
||||
}
|
||||
|
||||
export function serializeLayer(
|
||||
layer: CanvasLayer,
|
||||
): EditorProjectLayerSnapshot {
|
||||
@@ -202,9 +265,15 @@ export function serializeLayer(
|
||||
zIndex: layer.zIndex,
|
||||
sourceType: layer.sourceType,
|
||||
mediaType: layer.mediaType,
|
||||
thumbnailSrc: layer.thumbnailSrc,
|
||||
imageSequenceFrames: layer.imageSequenceFrames,
|
||||
previewVideoPath: layer.previewVideoPath,
|
||||
thumbnailSrc: serializeOptionalMediaSource(
|
||||
layer.thumbnailSrc,
|
||||
layer.objectKey,
|
||||
),
|
||||
imageSequenceFrames: serializeImageSequenceFrames(layer.imageSequenceFrames),
|
||||
previewVideoPath: serializeOptionalMediaSource(
|
||||
layer.previewVideoPath,
|
||||
layer.objectKey,
|
||||
),
|
||||
prompt: layer.prompt,
|
||||
actualPrompt: layer.actualPrompt,
|
||||
model: layer.model,
|
||||
@@ -834,6 +903,8 @@ function hydrateImageSequenceFrames(
|
||||
{
|
||||
frameIndex: numberFromSnapshot(snapshot.frameIndex, index + 1),
|
||||
imageSrc,
|
||||
objectKey: stringOrUndefined(snapshot.objectKey),
|
||||
assetObjectId: stringOrUndefined(snapshot.assetObjectId),
|
||||
width: numberFromSnapshot(snapshot.width, 0),
|
||||
height: numberFromSnapshot(snapshot.height, 0),
|
||||
},
|
||||
|
||||
@@ -59,6 +59,7 @@ export type SidebarMediaItemProps = {
|
||||
title: string;
|
||||
detail: string;
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
imageAlt: string;
|
||||
selected?: boolean;
|
||||
primaryLabel: string;
|
||||
@@ -85,6 +86,7 @@ export function SidebarMediaItem({
|
||||
title,
|
||||
detail,
|
||||
imageSrc,
|
||||
objectKey,
|
||||
imageAlt,
|
||||
selected = false,
|
||||
primaryLabel,
|
||||
@@ -129,6 +131,7 @@ export function SidebarMediaItem({
|
||||
>
|
||||
<PlatformMediaFrame
|
||||
src={imageSrc}
|
||||
objectKey={objectKey}
|
||||
alt={imageAlt}
|
||||
fallbackLabel={title}
|
||||
aspect="square"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user