补充是否公开资源的逻辑,修改精选列表展示逻辑
新增画布项目资源公开展示开关和公开精选资源接口。 左侧素材右键菜单支持公开展示勾选与删除操作。 陶泥儿精选改为读取全站公开生成资源并过滤同源同媒体副本。 拖拽生成素材入画布时保留源资源关系并复用同源资源。 同步 SpacetimeDB 迁移、绑定、前端测试和相关文档。
This commit is contained in:
@@ -40,12 +40,12 @@
|
||||
- 验证方式:运行品牌标识、绑定手机号页和平台首页相关前端测试,确认品牌图 `src` 为 `/branding/taonier-product-ip.png`;执行 `npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`、`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md`。
|
||||
|
||||
## 2026-06-22 创作主页精选只展示项目内画布生成资源
|
||||
## 2026-06-22 创作主页精选展示全站公开画布生成资源
|
||||
|
||||
- 背景:`/creation` 的 `陶泥儿精选` 曾从账号级素材库读取,并在素材为空时用公开作品图片补充,导致新创作页出现不属于任何当前图片画布项目的素材。
|
||||
- 决策:`陶泥儿精选` 只从用户图片画布项目中的 `editor_project_resource` 构建素材包和素材,且只保留 `sourceType="generated"` 的画布生成资源;账号级 `editor_asset`、上传素材、公开作品图片和 `mock_generated` 都不作为精选来源。
|
||||
- 影响范围:`/creation` 创作主页、`creationShowcaseModel`、最近项目列表资源读取、创作主页改版计划和精选素材相关测试。
|
||||
- 验证方式:运行 `src/components/creation-home/creationShowcaseModel.test.ts` 与 `CreationLandingView.test.tsx`,确认项目内生成资源展示、上传素材过滤、公开作品不再 fallback。
|
||||
- 决策:`陶泥儿精选` 从全站 `editor_project_resource` 构建素材包和素材,读取路径固定为 `GET /api/editor/showcase/resources`;只保留 `sourceType="generated"`、图片源非空且 `public_showcase_enabled = true` 的画布生成资源,按创建时间倒序展示。账号级 `editor_asset`、上传素材、公开作品图片和 `mock_generated` 都不作为精选来源。任意画布左侧素材列表中,单素材右键菜单承接文字 `删除` 和 `公开展示该素材` 勾选项;勾选项默认打开,修改时写回对应 `editor_project_resource.public_showcase_enabled`。
|
||||
- 影响范围:`/creation` 创作主页、`creationShowcaseModel`、公开精选 BFF、图片画布素材列表右键菜单、账号素材库快照、创作主页改版计划和精选素材相关测试。
|
||||
- 验证方式:运行 `src/components/creation-home/creationShowcaseModel.test.ts`、`CreationLandingView.test.tsx`、`ImageCanvasAssetRowView.test.tsx`、`useImageCanvasAssetLibrary.test.tsx` 与 `src/services/image-editor/editorProjectClient.test.ts`,确认公开生成资源展示、上传素材过滤、公开开关隐藏资源、删除入口在右键菜单中、公开作品不再 fallback。
|
||||
- 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 2026-06-22 编辑器生成模型默认定价调整
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
- 验证:`ImageCanvasEditorModel.test.ts` 覆盖素材库 source resource 保留,`useImageCanvasAssetCanvasBridge.test.tsx` 覆盖资源 ID 级联清理,`ImageCanvasEditorAssetsIntegration.test.tsx` 覆盖删除后保存的新 layout 不再包含被删图层。
|
||||
- 关联:`src/components/image-editor/ImageCanvasEditorModel.ts`、`src/components/image-editor/useImageCanvasAssetCanvasBridge.ts`、`src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`。
|
||||
|
||||
## 陶泥儿精选重复先查同源同媒体画布副本
|
||||
|
||||
- 现象:每次从项目素材中把同一个生成素材拖到画布上,`陶泥儿精选` 都多出一张看起来相同的素材。
|
||||
- 原因:素材拖入画布会为图层实例准备 `editor_project_resource`;如果该素材本来带 `sourceResourceId` 指向原始生成资源,而新资源仍按普通 generated 资源公开,精选就会把原件和每次拖拽产生的同源同媒体副本都展示出来。
|
||||
- 处理:创建项目资源时保留 `source_resource_id`,并在同项目已有同源同媒体资源时复用已有 resource;确需创建同源同媒体副本时默认 `public_showcase_enabled = false`。公开精选读取和前端精选模型都跳过 `sourceResourceId` 指回同一媒体原件的副本,但不要按图片地址全局去重,避免不同生成步骤共享占位图时被误合并。
|
||||
- 验证:`creationShowcaseModel.test.ts` 覆盖同源同媒体副本只展示原件;`ImageCanvasEditorAssetsIntegration.test.tsx` 覆盖拖拽生成素材到画布时继续提交 `sourceResourceId`;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 确认后端资源复用 / 精选过滤逻辑可编译。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`src/components/creation-home/creationShowcaseModel.ts`、`src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`。
|
||||
|
||||
## 画板外部生成排队超时不是失败
|
||||
|
||||
- 现象:画板发起付费图片生成后,前端弹出 `生成任务仍在队列中,请稍后刷新画布查看结果`,但后端任务仍在队列或执行中,后续可能正常完成。
|
||||
|
||||
@@ -468,7 +468,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`EditorProjectResource`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind` 和 `generation_inputs_json`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
|
||||
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复;`public_showcase_enabled` 控制该生成资源是否进入 `/creation` 的 `陶泥儿精选`,新增和旧行默认 `true`,但通过 `source_resource_id` 指向同一 owner 源资源且媒体引用相同的画布副本默认不公开。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。公开精选读取走 `GET /api/editor/showcase/resources`,只返回 `source_type = generated`、图片源非空且 `public_showcase_enabled = true` 的全站资源,并跳过同源同媒体副本,按 `created_at` 倒序取前 36 条;公开开关修改走登录鉴权 `PATCH /api/editor/project-resources/{resource_id}/showcase`,只允许资源 owner 更新。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
|
||||
- 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。
|
||||
|
||||
### `editor_asset_folder`
|
||||
@@ -482,7 +482,7 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`EditorAsset`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind` 和 `generation_inputs_json`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,前端只用该快照更新素材栏。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。
|
||||
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind`、`generation_inputs_json` 和可选 `source_resource_id`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。素材库快照通过 `source_resource_id` 回查对应 project resource 的 `public_showcase_enabled`,供左侧素材菜单展示和切换公开状态;公开开关本身仍只落在 `editor_project_resource`,不在账号素材表复制真相。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。
|
||||
- 索引:`by_editor_asset_owner_user_id`、`by_editor_asset_folder_id`。
|
||||
|
||||
### `inventory_slot`
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- 顶级导航中“创作”进入 `/creation`,“草稿”改为“项目”并进入 `/project`。
|
||||
- 移动端隐藏“创作”和“项目”入口,只保留浏览和个人相关入口;移动端直达 `/creation` 时提示用户使用桌面端打开。
|
||||
- 登录后在创作主页展示最近项目,并通过新建项目进入 `/editor/canvas?projectid=xxx`。
|
||||
- `陶泥儿精选` 只展示已经存在于用户图片画布项目中的画布生成素材包和素材,不使用账号级非项目素材、公开作品补充或 mock 素材。
|
||||
- `陶泥儿精选` 展示全站用户图片画布项目中允许公开展示的画布生成素材包和素材,不使用账号级非项目素材、公开作品补充或 mock 素材。
|
||||
- 现有 `/creation/<play>` 玩法工作台、草稿、作品架、生成恢复和发布链路保持不变。
|
||||
|
||||
## 页面结构
|
||||
@@ -61,9 +61,9 @@
|
||||
|
||||
### 陶泥儿精选
|
||||
|
||||
`陶泥儿精选` 是页面底部的项目内画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/<play>` 工作台使用,但不作为本页精选区内容。
|
||||
`陶泥儿精选` 是页面底部的全站公开画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/<play>` 工作台使用,但不作为本页精选区内容。
|
||||
|
||||
精选内容只使用 `editor_project_resource` 中 `sourceType="generated"` 的项目资源,代表用户通过图片画布生成并已经落入某个项目的素材。账号级 `editor_asset` 只表示跨项目素材库,不单独作为精选来源;上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。若现有数据缺少提示词、作者名或成本字段,v1 显示保守占位,不伪造内容。
|
||||
精选内容只使用 `editor_project_resource` 中 `sourceType="generated"` 且 `public_showcase_enabled = true` 的项目资源,代表用户通过图片画布生成、已经落入某个项目并允许公开展示的素材。账号级 `editor_asset` 只表示跨项目素材库,不单独作为精选来源;上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。从项目素材把同一个生成素材再次拖入画布时,只是新增图层实例或复用同源同媒体项目资源,不应额外生成新的精选候选;历史上已经产生的同源同媒体副本也需要在精选读取时跳过。若现有数据缺少提示词、作者名或成本字段,v1 显示保守占位,不伪造内容。
|
||||
|
||||
Tab:
|
||||
|
||||
@@ -100,10 +100,11 @@ Tab:
|
||||
|
||||
数据来源:
|
||||
|
||||
- 读取用户项目列表中的 `editor_project_resource` 快照,只保留 `sourceType="generated"` 的画布生成资源。
|
||||
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_project_resource` 快照,只保留 `sourceType="generated"` 且 `publicShowcaseEnabled !== false` 的画布生成资源;若某条资源通过 `sourceResourceId` 指回同一 owner 的源资源且媒体引用相同,则视为拖拽画布副本,不进入精选。
|
||||
- 素材包、角色、UI、音乐、音效、视频和美宣都必须来自已存在项目资源;素材库里尚未放入任何项目的账号级素材不展示。
|
||||
- 未登录、项目为空或项目内没有画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
|
||||
- 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
|
||||
- 上传素材、公开作品图片、mock 资源和假组合都不进入精选。
|
||||
- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。菜单内 `公开展示该素材` 默认勾选,取消勾选后调用 `PATCH /api/editor/project-resources/{resourceId}/showcase` 写回 `public_showcase_enabled = false`,该资源从 `陶泥儿精选` 移除。
|
||||
- 不为了填满展示区创建假素材、假作者、假泥点成本或假组合关系。
|
||||
- 暂无真实数据的 Tab 保留 Tab 入口,但内容区显示简洁空态。
|
||||
|
||||
@@ -114,7 +115,7 @@ Tab:
|
||||
- 最近项目继续使用编辑器项目接口:`listEditorProjects`、`createEditorProject` 和 `/editor/canvas?projectid=xxx`。
|
||||
- 最近项目和项目页打开画布时,浏览器 history 只保留带 `projectid` 的最终画布路由。
|
||||
- 项目封面逻辑复用 `/project` 项目卡已有的画布中心缩略图算法。
|
||||
- `陶泥儿精选` 读取项目内画布生成资源;若字段缺失,前端只显示空值或隐藏对应元信息,不在前端推导业务真相。
|
||||
- `陶泥儿精选` 读取后端公开精选接口返回的全站公开画布生成资源;公开开关以后端 `editor_project_resource.public_showcase_enabled` 为准,前端只做展示过滤、同源同媒体副本兜底过滤和乐观交互回滚。
|
||||
- `/creation/<play>` 的玩法工作台、草稿、生成页、结果页、发布、运行态和作品架链路保持原状。
|
||||
|
||||
## 路由与导航
|
||||
|
||||
@@ -35,7 +35,8 @@ use spacetime_client::{
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourceRecord, SpacetimeClientError,
|
||||
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceShowcaseUpdateRecordInput, SpacetimeClientError,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -142,6 +143,12 @@ pub struct EditorProjectResourceCreateRequest {
|
||||
generation_inputs: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorProjectResourceShowcaseUpdateRequest {
|
||||
public_showcase_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorAssetFolderCreateRequest {
|
||||
@@ -298,6 +305,12 @@ pub struct EditorProjectResourceResponse {
|
||||
resource: EditorProjectResourcePayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorProjectResourceListResponse {
|
||||
resources: Vec<EditorProjectResourcePayload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorAssetLibraryResponse {
|
||||
@@ -479,6 +492,7 @@ pub struct EditorProjectResourcePayload {
|
||||
source_resource_id: Option<String>,
|
||||
asset_kind: Option<String>,
|
||||
generation_inputs: Option<Value>,
|
||||
public_showcase_enabled: bool,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -521,6 +535,8 @@ pub struct EditorAssetPayload {
|
||||
task_id: Option<String>,
|
||||
asset_kind: Option<String>,
|
||||
generation_inputs: Option<Value>,
|
||||
source_resource_id: Option<String>,
|
||||
public_showcase_enabled: Option<bool>,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -587,6 +603,28 @@ pub async fn list_editor_projects(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_public_editor_project_resources(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let resources = state
|
||||
.spacetime_client()
|
||||
.list_public_editor_project_resources(EditorProjectResourcePublicShowcaseListRecordInput {
|
||||
limit: 36,
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
let resources = resources
|
||||
.into_iter()
|
||||
.map(editor_project_resource_payload_from_record)
|
||||
.collect();
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
EditorProjectResourceListResponse { resources },
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn create_editor_project(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -763,6 +801,32 @@ pub async fn create_editor_project_resource(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn update_editor_project_resource_showcase(
|
||||
State(state): State<AppState>,
|
||||
Path(resource_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorProjectResourceShowcaseUpdateRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let resource = state
|
||||
.spacetime_client()
|
||||
.update_editor_project_resource_showcase(EditorProjectResourceShowcaseUpdateRecordInput {
|
||||
resource_id,
|
||||
owner_user_id: current_owner_user_id(&authenticated),
|
||||
public_showcase_enabled: payload.public_showcase_enabled,
|
||||
updated_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
EditorProjectResourceResponse {
|
||||
resource: editor_project_resource_payload_from_record(resource),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_editor_asset_library(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -892,6 +956,7 @@ pub async fn create_editor_asset(
|
||||
task_id: normalize_optional_string(payload.task_id),
|
||||
asset_kind: normalize_optional_string(payload.asset_kind),
|
||||
generation_inputs_json,
|
||||
source_resource_id: None,
|
||||
now_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
@@ -2431,6 +2496,7 @@ pub(crate) fn editor_project_resource_payload_from_record(
|
||||
generation_inputs: record
|
||||
.generation_inputs
|
||||
.map(sanitize_editor_payload_inline_media),
|
||||
public_showcase_enabled: record.public_showcase_enabled,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
@@ -2489,6 +2555,8 @@ pub(crate) fn editor_asset_payload_from_record(record: EditorAssetRecord) -> Edi
|
||||
generation_inputs: record
|
||||
.generation_inputs
|
||||
.map(sanitize_editor_payload_inline_media),
|
||||
source_resource_id: record.source_resource_id,
|
||||
public_showcase_enabled: record.public_showcase_enabled,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
@@ -2652,6 +2720,10 @@ async fn persist_editor_generated_asset(
|
||||
|
||||
let folder_id =
|
||||
normalize_generated_asset_folder_id(input.folder_id, input.owner_user_id.as_str());
|
||||
let created_resource_id = resource
|
||||
.as_ref()
|
||||
.map(|resource| resource.resource_id.clone())
|
||||
.or_else(|| normalize_optional_string(input.source_resource_id.clone()));
|
||||
let asset = if let Some(folder_id) = folder_id {
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
@@ -2673,6 +2745,7 @@ async fn persist_editor_generated_asset(
|
||||
task_id: Some(input.task_id),
|
||||
asset_kind: normalize_optional_string(input.asset_kind),
|
||||
generation_inputs_json,
|
||||
source_resource_id: created_resource_id,
|
||||
now_micros,
|
||||
})
|
||||
.await
|
||||
@@ -3798,6 +3871,7 @@ mod tests {
|
||||
source_resource_id: None,
|
||||
asset_kind: Some("spec".to_string()),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-23T00:00:00.000Z".to_string(),
|
||||
updated_at: "2026-06-23T00:00:00.000Z".to_string(),
|
||||
}];
|
||||
@@ -3868,6 +3942,7 @@ mod tests {
|
||||
source_resource_id: None,
|
||||
asset_kind: Some("spec".to_string()),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-23T00:00:00.000Z".to_string(),
|
||||
updated_at: "2026-06-23T00:00:00.000Z".to_string(),
|
||||
}];
|
||||
@@ -3934,6 +4009,7 @@ mod tests {
|
||||
source_resource_id: None,
|
||||
asset_kind: Some("character".to_string()),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-23T00:00:00.000Z".to_string(),
|
||||
updated_at: "2026-06-23T00:00:00.000Z".to_string(),
|
||||
};
|
||||
@@ -4393,6 +4469,7 @@ mod tests {
|
||||
"fields": [{"title": "角色设定", "value": "银发游侠"}],
|
||||
"references": []
|
||||
})),
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
updated_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
};
|
||||
@@ -4489,6 +4566,7 @@ mod tests {
|
||||
source_resource_id: None,
|
||||
asset_kind: Some("character".to_string()),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
updated_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
};
|
||||
@@ -4569,6 +4647,7 @@ mod tests {
|
||||
source_resource_id: None,
|
||||
asset_kind: Some("character".to_string()),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
updated_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
};
|
||||
@@ -4690,6 +4769,7 @@ mod tests {
|
||||
source_resource_id: None,
|
||||
asset_kind: Some("character".to_string()),
|
||||
generation_inputs: None,
|
||||
public_showcase_enabled: true,
|
||||
created_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
updated_at: "2026-06-21T12:16:22.000Z".to_string(),
|
||||
};
|
||||
|
||||
@@ -490,6 +490,7 @@ pub async fn create_external_editor_asset(
|
||||
task_id: normalize_optional_string(payload.task_id),
|
||||
asset_kind: normalize_optional_string(payload.asset_kind),
|
||||
generation_inputs_json,
|
||||
source_resource_id: None,
|
||||
now_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -13,8 +13,9 @@ use crate::{
|
||||
delete_editor_project, edit_editor_image, extract_editor_ui_design_assets,
|
||||
generate_editor_icon_spritesheet, generate_editor_image, get_editor_asset_library,
|
||||
get_editor_generation_pricing, get_editor_project, list_editor_projects,
|
||||
load_recent_editor_project, rename_editor_project, save_editor_project_layout,
|
||||
update_editor_asset, update_editor_asset_folder,
|
||||
list_public_editor_project_resources, load_recent_editor_project, rename_editor_project,
|
||||
save_editor_project_layout, update_editor_asset, update_editor_asset_folder,
|
||||
update_editor_project_resource_showcase,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -27,6 +28,10 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
"/api/editor/generation-pricing",
|
||||
get(get_editor_generation_pricing),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/showcase/resources",
|
||||
get(list_public_editor_project_resources),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/projects/recent",
|
||||
get(load_recent_editor_project).route_layer(middleware::from_fn_with_state(
|
||||
@@ -67,6 +72,12 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/project-resources/{resource_id}/showcase",
|
||||
patch(update_editor_project_resource_showcase).route_layer(
|
||||
middleware::from_fn_with_state(state.clone(), require_bearer_auth),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/assets/library",
|
||||
get(get_editor_asset_library).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -235,6 +235,56 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_editor_project_resource_showcase(
|
||||
&self,
|
||||
input: EditorProjectResourceShowcaseUpdateRecordInput,
|
||||
) -> Result<EditorProjectResourceRecord, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"update_editor_project_resource_showcase_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.update_editor_project_resource_showcase_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 list_public_editor_project_resources(
|
||||
&self,
|
||||
input: EditorProjectResourcePublicShowcaseListRecordInput,
|
||||
) -> Result<Vec<EditorProjectResourceRecord>, SpacetimeClientError> {
|
||||
let procedure_input = input.into();
|
||||
|
||||
self.call_after_connect(
|
||||
"list_public_editor_project_resources_and_return",
|
||||
move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.list_public_editor_project_resources_and_return_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_editor_project_resource_list_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_editor_asset_library(
|
||||
&self,
|
||||
owner_user_id: String,
|
||||
|
||||
@@ -38,7 +38,8 @@ pub use mapper::{
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveAckRecord,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourceRecord, ExternalApiKeyAuthenticateRecordInput,
|
||||
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceShowcaseUpdateRecordInput, ExternalApiKeyAuthenticateRecordInput,
|
||||
ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, ExternalApiKeyRevokeRecordInput,
|
||||
ExternalGenerationJobAcknowledgeRecordInput, ExternalGenerationJobClaimRecordInput,
|
||||
ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput,
|
||||
|
||||
@@ -81,7 +81,8 @@ pub use self::editor_project::{
|
||||
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveAckRecord,
|
||||
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
|
||||
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
|
||||
EditorProjectResourceRecord,
|
||||
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
|
||||
EditorProjectResourceShowcaseUpdateRecordInput,
|
||||
};
|
||||
pub use self::external_api_key::{
|
||||
ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord,
|
||||
@@ -210,7 +211,9 @@ pub(crate) use self::editor_project::{
|
||||
map_editor_asset_library_procedure_result, map_editor_asset_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,
|
||||
map_editor_project_required_procedure_result,
|
||||
map_editor_project_resource_list_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,
|
||||
|
||||
@@ -57,6 +57,7 @@ pub struct EditorProjectResourceRecord {
|
||||
pub source_resource_id: Option<String>,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs: Option<serde_json::Value>,
|
||||
pub public_showcase_enabled: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -90,6 +91,8 @@ pub struct EditorAssetRecord {
|
||||
pub task_id: Option<String>,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs: Option<serde_json::Value>,
|
||||
pub source_resource_id: Option<String>,
|
||||
pub public_showcase_enabled: Option<bool>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -169,6 +172,19 @@ pub struct EditorProjectResourceMediaRepairRecordInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorProjectResourceShowcaseUpdateRecordInput {
|
||||
pub resource_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub public_showcase_enabled: bool,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorProjectResourcePublicShowcaseListRecordInput {
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EditorAssetFolderCreateRecordInput {
|
||||
pub folder_id: String,
|
||||
@@ -213,6 +229,7 @@ pub struct EditorAssetCreateRecordInput {
|
||||
pub task_id: Option<String>,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs_json: Option<String>,
|
||||
pub source_resource_id: Option<String>,
|
||||
pub now_micros: i64,
|
||||
}
|
||||
|
||||
@@ -341,6 +358,27 @@ impl From<EditorProjectResourceMediaRepairRecordInput>
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorProjectResourceShowcaseUpdateRecordInput>
|
||||
for crate::module_bindings::EditorProjectResourceShowcaseUpdateInput
|
||||
{
|
||||
fn from(input: EditorProjectResourceShowcaseUpdateRecordInput) -> Self {
|
||||
Self {
|
||||
resource_id: input.resource_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
public_showcase_enabled: input.public_showcase_enabled,
|
||||
updated_at_micros: input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorProjectResourcePublicShowcaseListRecordInput>
|
||||
for crate::module_bindings::EditorProjectResourcePublicShowcaseListInput
|
||||
{
|
||||
fn from(input: EditorProjectResourcePublicShowcaseListRecordInput) -> Self {
|
||||
Self { limit: input.limit }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditorAssetFolderCreateRecordInput>
|
||||
for crate::module_bindings::EditorAssetFolderCreateInput
|
||||
{
|
||||
@@ -401,6 +439,7 @@ impl From<EditorAssetCreateRecordInput> for crate::module_bindings::EditorAssetC
|
||||
task_id: input.task_id,
|
||||
asset_kind: input.asset_kind,
|
||||
generation_inputs_json: input.generation_inputs_json,
|
||||
source_resource_id: input.source_resource_id,
|
||||
now_micros: input.now_micros,
|
||||
}
|
||||
}
|
||||
@@ -518,6 +557,20 @@ pub(crate) fn map_editor_project_resource_procedure_result(
|
||||
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布资源快照"))
|
||||
}
|
||||
|
||||
pub(crate) fn map_editor_project_resource_list_procedure_result(
|
||||
result: EditorProjectResourceListProcedureResult,
|
||||
) -> Result<Vec<EditorProjectResourceRecord>, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
result
|
||||
.resources
|
||||
.into_iter()
|
||||
.map(map_editor_project_resource_snapshot)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn map_editor_asset_library_procedure_result(
|
||||
result: EditorAssetLibraryProcedureResult,
|
||||
) -> Result<EditorAssetLibraryRecord, SpacetimeClientError> {
|
||||
@@ -638,6 +691,7 @@ fn map_editor_project_resource_snapshot(
|
||||
source_resource_id: snapshot.source_resource_id,
|
||||
asset_kind: snapshot.asset_kind,
|
||||
generation_inputs,
|
||||
public_showcase_enabled: snapshot.public_showcase_enabled,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
})
|
||||
@@ -698,6 +752,8 @@ fn map_editor_asset_snapshot(
|
||||
task_id: snapshot.task_id,
|
||||
asset_kind: snapshot.asset_kind,
|
||||
generation_inputs,
|
||||
source_resource_id: snapshot.source_resource_id,
|
||||
public_showcase_enabled: snapshot.public_showcase_enabled,
|
||||
created_at: format_timestamp_micros(snapshot.created_at_micros),
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
// This was generated using spacetimedb cli version 2.5.0 (commit ca16958ef0a5f8c816700d2255a0b20ecacff901).
|
||||
// This was generated using spacetimedb cli version 2.5.0 (commit 19cbf2b219ac9fd7fdca6c5e2c61a3a2f0319515).
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
@@ -391,8 +391,11 @@ 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_list_procedure_result_type;
|
||||
pub mod editor_project_resource_media_repair_input_type;
|
||||
pub mod editor_project_resource_procedure_result_type;
|
||||
pub mod editor_project_resource_public_showcase_list_input_type;
|
||||
pub mod editor_project_resource_showcase_update_input_type;
|
||||
pub mod editor_project_resource_snapshot_type;
|
||||
pub mod editor_project_resource_table;
|
||||
pub mod editor_project_resource_type;
|
||||
@@ -578,6 +581,7 @@ pub mod list_match_3_d_works_procedure;
|
||||
pub mod list_platform_browse_history_procedure;
|
||||
pub mod list_profile_save_archives_procedure;
|
||||
pub mod list_profile_wallet_ledger_procedure;
|
||||
pub mod list_public_editor_project_resources_and_return_procedure;
|
||||
pub mod list_puzzle_clear_works_procedure;
|
||||
pub mod list_puzzle_gallery_procedure;
|
||||
pub mod list_puzzle_works_procedure;
|
||||
@@ -1120,6 +1124,7 @@ pub mod unpublish_custom_world_profile_reducer;
|
||||
pub mod update_bark_battle_draft_config_procedure;
|
||||
pub mod update_editor_asset_and_return_procedure;
|
||||
pub mod update_editor_asset_folder_and_return_procedure;
|
||||
pub mod update_editor_project_resource_showcase_and_return_procedure;
|
||||
pub mod update_jump_hop_work_procedure;
|
||||
pub mod update_match_3_d_work_procedure;
|
||||
pub mod update_puzzle_clear_work_procedure;
|
||||
@@ -1614,8 +1619,11 @@ pub use editor_project_list_procedure_result_type::EditorProjectListProcedureRes
|
||||
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_list_procedure_result_type::EditorProjectResourceListProcedureResult;
|
||||
pub use editor_project_resource_media_repair_input_type::EditorProjectResourceMediaRepairInput;
|
||||
pub use editor_project_resource_procedure_result_type::EditorProjectResourceProcedureResult;
|
||||
pub use editor_project_resource_public_showcase_list_input_type::EditorProjectResourcePublicShowcaseListInput;
|
||||
pub use editor_project_resource_showcase_update_input_type::EditorProjectResourceShowcaseUpdateInput;
|
||||
pub use editor_project_resource_snapshot_type::EditorProjectResourceSnapshot;
|
||||
pub use editor_project_resource_table::*;
|
||||
pub use editor_project_resource_type::EditorProjectResource;
|
||||
@@ -1801,6 +1809,7 @@ pub use list_match_3_d_works_procedure::list_match_3_d_works;
|
||||
pub use list_platform_browse_history_procedure::list_platform_browse_history;
|
||||
pub use list_profile_save_archives_procedure::list_profile_save_archives;
|
||||
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
|
||||
pub use list_public_editor_project_resources_and_return_procedure::list_public_editor_project_resources_and_return;
|
||||
pub use list_puzzle_clear_works_procedure::list_puzzle_clear_works;
|
||||
pub use list_puzzle_gallery_procedure::list_puzzle_gallery;
|
||||
pub use list_puzzle_works_procedure::list_puzzle_works;
|
||||
@@ -2343,6 +2352,7 @@ pub use unpublish_custom_world_profile_reducer::unpublish_custom_world_profile;
|
||||
pub use update_bark_battle_draft_config_procedure::update_bark_battle_draft_config;
|
||||
pub use update_editor_asset_and_return_procedure::update_editor_asset_and_return;
|
||||
pub use update_editor_asset_folder_and_return_procedure::update_editor_asset_folder_and_return;
|
||||
pub use update_editor_project_resource_showcase_and_return_procedure::update_editor_project_resource_showcase_and_return;
|
||||
pub use update_jump_hop_work_procedure::update_jump_hop_work;
|
||||
pub use update_match_3_d_work_procedure::update_match_3_d_work;
|
||||
pub use update_puzzle_clear_work_procedure::update_puzzle_clear_work;
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ pub struct EditorAssetCreateInput {
|
||||
pub task_id: Option<String>,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs_json: Option<String>,
|
||||
pub source_resource_id: Option<String>,
|
||||
pub now_micros: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ pub struct EditorAssetSnapshot {
|
||||
pub task_id: Option<String>,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs_json: Option<String>,
|
||||
pub source_resource_id: Option<String>,
|
||||
pub public_showcase_enabled: Option<bool>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct EditorAsset {
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs_json: Option<String>,
|
||||
pub source_resource_id: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorAsset {
|
||||
@@ -55,6 +56,7 @@ pub struct EditorAssetCols {
|
||||
pub updated_at: __sdk::__query_builder::Col<EditorAsset, __sdk::Timestamp>,
|
||||
pub asset_kind: __sdk::__query_builder::Col<EditorAsset, Option<String>>,
|
||||
pub generation_inputs_json: __sdk::__query_builder::Col<EditorAsset, Option<String>>,
|
||||
pub source_resource_id: __sdk::__query_builder::Col<EditorAsset, Option<String>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for EditorAsset {
|
||||
@@ -83,6 +85,7 @@ impl __sdk::__query_builder::HasCols for EditorAsset {
|
||||
table_name,
|
||||
"generation_inputs_json",
|
||||
),
|
||||
source_resource_id: __sdk::__query_builder::Col::new(table_name, "source_resource_id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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_resource_snapshot_type::EditorProjectResourceSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct EditorProjectResourceListProcedureResult {
|
||||
pub ok: bool,
|
||||
pub resources: Vec<EditorProjectResourceSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectResourceListProcedureResult {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// 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 EditorProjectResourcePublicShowcaseListInput {
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectResourcePublicShowcaseListInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// 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 EditorProjectResourceShowcaseUpdateInput {
|
||||
pub resource_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub public_showcase_enabled: bool,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectResourceShowcaseUpdateInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+1
@@ -23,6 +23,7 @@ pub struct EditorProjectResourceSnapshot {
|
||||
pub source_resource_id: Option<String>,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs_json: Option<String>,
|
||||
pub public_showcase_enabled: bool,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct EditorProjectResource {
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
pub asset_kind: Option<String>,
|
||||
pub generation_inputs_json: Option<String>,
|
||||
pub public_showcase_enabled: bool,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorProjectResource {
|
||||
@@ -55,6 +56,7 @@ pub struct EditorProjectResourceCols {
|
||||
pub updated_at: __sdk::__query_builder::Col<EditorProjectResource, __sdk::Timestamp>,
|
||||
pub asset_kind: __sdk::__query_builder::Col<EditorProjectResource, Option<String>>,
|
||||
pub generation_inputs_json: __sdk::__query_builder::Col<EditorProjectResource, Option<String>>,
|
||||
pub public_showcase_enabled: __sdk::__query_builder::Col<EditorProjectResource, bool>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for EditorProjectResource {
|
||||
@@ -83,6 +85,10 @@ impl __sdk::__query_builder::HasCols for EditorProjectResource {
|
||||
table_name,
|
||||
"generation_inputs_json",
|
||||
),
|
||||
public_showcase_enabled: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"public_showcase_enabled",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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_list_procedure_result_type::EditorProjectResourceListProcedureResult;
|
||||
use super::editor_project_resource_public_showcase_list_input_type::EditorProjectResourcePublicShowcaseListInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct ListPublicEditorProjectResourcesAndReturnArgs {
|
||||
pub input: EditorProjectResourcePublicShowcaseListInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for ListPublicEditorProjectResourcesAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `list_public_editor_project_resources_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait list_public_editor_project_resources_and_return {
|
||||
fn list_public_editor_project_resources_and_return(
|
||||
&self,
|
||||
input: EditorProjectResourcePublicShowcaseListInput,
|
||||
) {
|
||||
self.list_public_editor_project_resources_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn list_public_editor_project_resources_and_return_then(
|
||||
&self,
|
||||
input: EditorProjectResourcePublicShowcaseListInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectResourceListProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl list_public_editor_project_resources_and_return for super::RemoteProcedures {
|
||||
fn list_public_editor_project_resources_and_return_then(
|
||||
&self,
|
||||
input: EditorProjectResourcePublicShowcaseListInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectResourceListProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorProjectResourceListProcedureResult>(
|
||||
"list_public_editor_project_resources_and_return",
|
||||
ListPublicEditorProjectResourcesAndReturnArgs { 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_procedure_result_type::EditorProjectResourceProcedureResult;
|
||||
use super::editor_project_resource_showcase_update_input_type::EditorProjectResourceShowcaseUpdateInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct UpdateEditorProjectResourceShowcaseAndReturnArgs {
|
||||
pub input: EditorProjectResourceShowcaseUpdateInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for UpdateEditorProjectResourceShowcaseAndReturnArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `update_editor_project_resource_showcase_and_return`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait update_editor_project_resource_showcase_and_return {
|
||||
fn update_editor_project_resource_showcase_and_return(
|
||||
&self,
|
||||
input: EditorProjectResourceShowcaseUpdateInput,
|
||||
) {
|
||||
self.update_editor_project_resource_showcase_and_return_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn update_editor_project_resource_showcase_and_return_then(
|
||||
&self,
|
||||
input: EditorProjectResourceShowcaseUpdateInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectResourceProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl update_editor_project_resource_showcase_and_return for super::RemoteProcedures {
|
||||
fn update_editor_project_resource_showcase_and_return_then(
|
||||
&self,
|
||||
input: EditorProjectResourceShowcaseUpdateInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<EditorProjectResourceProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, EditorProjectResourceProcedureResult>(
|
||||
"update_editor_project_resource_showcase_and_return",
|
||||
UpdateEditorProjectResourceShowcaseAndReturnArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1394,6 +1394,22 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "editor_project_resource" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:精选公开开关晚于画布资源表加入,旧生成资源按默认公开兼容。
|
||||
object
|
||||
.entry("public_showcase_enabled".to_string())
|
||||
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||
}
|
||||
}
|
||||
if table_name == "editor_asset" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:账号素材与项目资源绑定晚于素材库表加入,旧素材按未绑定兼容。
|
||||
object
|
||||
.entry("source_resource_id".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
next_value
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { ContextType } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AuthUiContext } from '../auth/AuthUiContext';
|
||||
import { CreationLandingView } from './CreationLandingView';
|
||||
|
||||
const listEditorProjectsMock = vi.hoisted(() => vi.fn());
|
||||
const listPublicEditorProjectResourcesMock = vi.hoisted(() => vi.fn());
|
||||
const createEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
type AuthValue = NonNullable<ContextType<typeof AuthUiContext>>;
|
||||
@@ -45,6 +46,7 @@ function createAuthValue(overrides: Partial<AuthValue> = {}): AuthValue {
|
||||
|
||||
vi.mock('../../services/image-editor/editorProjectClient', () => ({
|
||||
listEditorProjects: listEditorProjectsMock,
|
||||
listPublicEditorProjectResources: listPublicEditorProjectResourcesMock,
|
||||
createEditorProject: createEditorProjectMock,
|
||||
}));
|
||||
|
||||
@@ -88,8 +90,13 @@ function renderCreationLanding({
|
||||
}
|
||||
|
||||
describe('CreationLandingView', () => {
|
||||
beforeEach(() => {
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
listEditorProjectsMock.mockReset();
|
||||
listPublicEditorProjectResourcesMock.mockReset();
|
||||
createEditorProjectMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -212,28 +219,24 @@ describe('CreationLandingView', () => {
|
||||
|
||||
it('renders generated project resources in showcase tabs without mock rows', async () => {
|
||||
const user = userEvent.setup();
|
||||
listEditorProjectsMock.mockResolvedValueOnce([
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
{
|
||||
...projectItems[0]!,
|
||||
resources: [
|
||||
{
|
||||
resourceId: 'resource-character',
|
||||
projectId: 'project-newest',
|
||||
label: '角色英雄',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: 'character hero prompt',
|
||||
actualPrompt: null,
|
||||
model: 'character-model',
|
||||
provider: 'provider',
|
||||
taskId: 'task-1',
|
||||
assetKind: 'character',
|
||||
authorName: '创作者A',
|
||||
priceMudPoints: 20,
|
||||
},
|
||||
],
|
||||
resourceId: 'resource-character',
|
||||
projectId: 'project-newest',
|
||||
label: '角色英雄',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: 'character hero prompt',
|
||||
actualPrompt: null,
|
||||
model: 'character-model',
|
||||
provider: 'provider',
|
||||
taskId: 'task-1',
|
||||
assetKind: 'character',
|
||||
authorName: '创作者A',
|
||||
priceMudPoints: 20,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -251,55 +254,51 @@ describe('CreationLandingView', () => {
|
||||
|
||||
it('opens a fullscreen asset preview modal with thumbnail navigation', async () => {
|
||||
const user = userEvent.setup();
|
||||
listEditorProjectsMock.mockResolvedValueOnce([
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
{
|
||||
...projectItems[0]!,
|
||||
resources: [
|
||||
{
|
||||
resourceId: 'resource-character',
|
||||
projectId: 'project-newest',
|
||||
label: '角色英雄',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: 'character hero prompt',
|
||||
actualPrompt: null,
|
||||
model: 'character-model',
|
||||
provider: 'provider',
|
||||
taskId: 'task-1',
|
||||
assetKind: 'character',
|
||||
authorName: '创作者A',
|
||||
priceMudPoints: 20,
|
||||
},
|
||||
{
|
||||
resourceId: 'resource-animation',
|
||||
projectId: 'project-newest',
|
||||
label: '待机动画',
|
||||
imageSrc: 'data:image/png;base64,animation',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: 'idle animation prompt',
|
||||
actualPrompt: null,
|
||||
model: 'seedance2.0-fast',
|
||||
provider: 'ark',
|
||||
taskId: 'task-2',
|
||||
assetKind: 'character-animation',
|
||||
priceMudPoints: 40,
|
||||
generationInputs: {
|
||||
fields: [{ title: '动作描述', value: '待机动作' }],
|
||||
references: [
|
||||
{
|
||||
title: '角色图片',
|
||||
label: '角色英雄',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-character',
|
||||
},
|
||||
],
|
||||
resourceId: 'resource-character',
|
||||
projectId: 'project-newest',
|
||||
label: '角色英雄',
|
||||
imageSrc: 'data:image/png;base64,character',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: 'character hero prompt',
|
||||
actualPrompt: null,
|
||||
model: 'character-model',
|
||||
provider: 'provider',
|
||||
taskId: 'task-1',
|
||||
assetKind: 'character',
|
||||
authorName: '创作者A',
|
||||
priceMudPoints: 20,
|
||||
},
|
||||
{
|
||||
resourceId: 'resource-animation',
|
||||
projectId: 'project-newest',
|
||||
label: '待机动画',
|
||||
imageSrc: 'data:image/png;base64,animation',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: 'idle animation prompt',
|
||||
actualPrompt: null,
|
||||
model: 'seedance2.0-fast',
|
||||
provider: 'ark',
|
||||
taskId: 'task-2',
|
||||
assetKind: 'character-animation',
|
||||
priceMudPoints: 40,
|
||||
generationInputs: {
|
||||
fields: [{ title: '动作描述', value: '待机动作' }],
|
||||
references: [
|
||||
{
|
||||
title: '角色图片',
|
||||
label: '角色英雄',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-character',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -325,58 +324,49 @@ describe('CreationLandingView', () => {
|
||||
});
|
||||
|
||||
it('uses Taonier featured as the generated project resource waterfall instead of creation entries', async () => {
|
||||
listEditorProjectsMock.mockResolvedValueOnce([
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
{
|
||||
...projectItems[0]!,
|
||||
resources: [
|
||||
{
|
||||
resourceId: 'resource-pack-a',
|
||||
projectId: 'project-newest',
|
||||
label: '素材包 A',
|
||||
imageSrc: 'data:image/png;base64,a',
|
||||
width: 512,
|
||||
height: 640,
|
||||
sourceType: 'generated',
|
||||
prompt: '素材包提示词',
|
||||
actualPrompt: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
taskId: null,
|
||||
},
|
||||
{
|
||||
resourceId: 'resource-uploaded',
|
||||
projectId: 'project-newest',
|
||||
label: '上传素材',
|
||||
imageSrc: 'data:image/png;base64,uploaded',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'uploaded',
|
||||
prompt: null,
|
||||
actualPrompt: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
taskId: null,
|
||||
},
|
||||
],
|
||||
resourceId: 'resource-pack-a',
|
||||
projectId: 'project-newest',
|
||||
label: '素材包 A',
|
||||
imageSrc: 'data:image/png;base64,a',
|
||||
width: 512,
|
||||
height: 640,
|
||||
sourceType: 'generated',
|
||||
prompt: '素材包提示词',
|
||||
actualPrompt: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
taskId: null,
|
||||
},
|
||||
{
|
||||
...projectItems[1]!,
|
||||
resources: [
|
||||
{
|
||||
resourceId: 'resource-pack-b',
|
||||
projectId: 'project-old',
|
||||
label: '素材包 B',
|
||||
imageSrc: 'data:image/png;base64,b',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: '第二组素材',
|
||||
actualPrompt: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
taskId: null,
|
||||
},
|
||||
],
|
||||
resourceId: 'resource-uploaded',
|
||||
projectId: 'project-newest',
|
||||
label: '上传素材',
|
||||
imageSrc: 'data:image/png;base64,uploaded',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'uploaded',
|
||||
prompt: null,
|
||||
actualPrompt: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
taskId: null,
|
||||
},
|
||||
{
|
||||
resourceId: 'resource-pack-b',
|
||||
projectId: 'project-old',
|
||||
label: '素材包 B',
|
||||
imageSrc: 'data:image/png;base64,b',
|
||||
width: 512,
|
||||
height: 512,
|
||||
sourceType: 'generated',
|
||||
prompt: '第二组素材',
|
||||
actualPrompt: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
taskId: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user