diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 1325929f..854c60bd 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -2238,3 +2238,11 @@ - 影响范围:主站路由、平台创作入口、图片画布编辑器组件、editor project API client、`api-server` BFF、`spacetime-client` facade、`spacetime-module` 表 / procedure、后端数据契约文档和前端架构文档。 - 验证方式:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run test -- src/services/image-editor/editorProjectClient.test.ts`、`npm run typecheck`、`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check`、headless Playwright smoke。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 + +## 2026-06-14 图片画布素材库按账号级持久化 + +- 背景:图片画布需要 Lovart 式素材管理,素材不应只挂在单个 project 临时状态里;用户在任意项目上传的图片素材,都应作为账号素材库在其它项目中可见,同时画布自身的图层、视图和分组仍属于项目下的画布数据。 +- 决策:新增 `editor_asset_folder` / `editor_asset` 作为账号级素材库表,以 `owner_user_id` 为归属;`editor_project` 继续承载工程元数据,`editor_canvas` 继续承载 project 下的画布视图和图层布局,`editor_project_resource` 继续承载具体画布资源引用。素材库 CRUD 统一经 `spacetime-client` facade 和 `/api/editor/assets*` BFF,前端只保留选择模式、框选、拖拽上传、图层打组和小地图拖拽等交互状态,不直接绕过后端持久化。 +- 影响范围:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/editor_project.rs`、`server-rs/crates/api-server/src/editor_project.rs`、`src/services/image-editor/editorProjectClient.ts`、`src/components/image-editor/ImageCanvasEditorView.tsx`、后端数据契约文档和图片画布前端技术方案。 +- 验证方式:`npm run spacetime:generate -- --rust-only`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/services/image-editor/editorProjectClient.test.ts`、`npm run typecheck`、`npm run check:spacetime-schema`、`npm run check:encoding`、`cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml`、`git diff --check`。 +- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 diff --git a/TRACKING.md b/TRACKING.md index e943dd0c..242988f2 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -1,6 +1,6 @@ # 图片画布编辑器 Lovart 化执行跟踪 -更新时间:`2026-06-13` +更新时间:`2026-06-14` ## 目标 @@ -15,6 +15,8 @@ | 测试用例 | 已完成 | 已补前端交互测试和 editor project client 测试。 | | 后端持久化 | 已完成 | 已新增 editor project/resource 表、SpacetimeDB procedure、spacetime-client facade 与 api-server BFF。 | | 前端交互 | 已完成 | 已实现缩放菜单、工具模式、Space 抓手、中键平移、吸附线、元数据弹窗和右侧真实修改结果。 | +| 素材库增强 | 已完成 | 已实现账号级素材库持久化、文件夹新建 / 折叠 / 重命名 / 删除、多文件上传、拖拽定向上传、素材框选与批量删除。 | +| 画布增强 | 已完成 | 已实现拖拽上传到画布并创建图层、图层打组、Ctrl/Cmd 滚轮缩放、普通滚轮纵向滚动和小地图拖拽移动视图。 | | 验证 | 已完成 | 聚焦测试、类型检查、Rust 检查、schema guard、编码检查、diff 空白检查和浏览器 smoke 已通过。 | ## 待办清单 @@ -34,6 +36,13 @@ - [x] 执行并记录验证命令。 - [x] 新增 `/project` 项目页,接入项目列表、单项重命名 / 删除、批量选择和批量删除。 - [x] 接入“我的”页项目入口与 `/editor/canvas?projectid=` 精准加载。 +- [x] 新增账号级素材库表、API、前端服务和编辑器接入。 +- [x] 素材文件夹支持新建、折叠、重命名和删除。 +- [x] 上传按钮与拖拽上传均支持多文件;拖到文件夹 / 素材行进入对应文件夹,拖到画布进入默认文件夹并创建图层。 +- [x] 素材选择模式支持框选多选和批量删除。 +- [x] 图层面板支持对当前选中图层打组并持久化 groupId。 +- [x] 小地图支持拖拽移动画布视图。 +- [x] 滚轮语义调整为普通滚轮纵向滚动,Ctrl/Cmd 滚轮缩放并阻止浏览器缩放。 ## 决策记录 @@ -85,3 +94,4 @@ - 2026-06-14 组件复用修正:编辑器画布背景色菜单改为复用 `PlatformFloatingMenu` 和 `PlatformFloatingMenuItem`,删除局部菜单容器定位 / 边框 / 阴影样式;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformFloatingMenu.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 - 2026-06-14 组件复用修正:编辑器“修改图片”弹窗提交按钮改为复用 `PlatformActionButton`,删除局部提交按钮颜色、边框和禁用态样式;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformActionButton.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 - 2026-06-14 组件复用修正:项目卡片预览改为复用 `PlatformMediaFrame`,删除项目页局部预览框比例、图片填充和背景样式;验证命令:`npm run test -- src/components/project/ProjectGalleryView.test.tsx src/components/common/PlatformMediaFrame.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +- 2026-06-14 素材库与画布持久化修正:新增 `editor_asset_folder` / `editor_asset` 账号级素材库表、SpacetimeDB procedure、spacetime-client facade 和 api-server `/api/editor/assets*` BFF;编辑器接入文件夹新建 / 折叠 / 重命名 / 删除、素材重命名 / 删除、多文件上传、拖拽定向上传、拖入画布生成图层、素材框选批量删除、图层打组、小地图拖拽、普通滚轮纵向滚动与 Ctrl/Cmd 滚轮缩放。验证命令:`npm run spacetime:generate -- --rust-only`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/services/image-editor/editorProjectClient.test.ts`、`npm run typecheck`、`npm run check:spacetime-schema`、`npm run check:encoding`、`cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml`、`git diff --check`。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 3c99af4f..f11eb11b 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -35,9 +35,12 @@ - 新增 `editor_project` 表保存图片画布工程:`projectId`、`ownerUserId`、标题、创建时间和更新时间;历史 layout 字段暂保留为兼容列,不再作为权威画布数据。 - 新增 `editor_canvas` 表保存工程下的画布:`canvasId`、`projectId`、`ownerUserId`、标题、viewport、图层布局 JSON、创建时间和更新时间。当前编辑器使用项目默认画布,后续可扩展为一个 project 下多个 canvas。 -- 新增 `editor_project_resource` 表保存画布资源:`resourceId`、`projectId`、`ownerUserId`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、sourceResourceId、创建时间和更新时间。 +- 新增 `editor_asset_folder` 表保存账号级素材文件夹:`folderId`、`ownerUserId`、名称、排序、折叠状态、系统默认标记、创建时间和更新时间。素材文件夹不归属于 project,同一个账号进入任一项目都能看到。 +- 新增 `editor_asset` 表保存账号级素材:`assetId`、`ownerUserId`、`folderId`、名称、图片读取地址、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、创建时间和更新时间。素材只跟账号走,不跟 project 走。 +- `editor_project_resource` 表保存工程画布引用过的资源快照:`resourceId`、`projectId`、`ownerUserId`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、sourceResourceId、创建时间和更新时间。上传素材被拖入画布时会复制为 project resource,图层只引用 resourceId。 - 图片文件本体继续走 OSS,浏览器读取私有 generated 对象仍经 `/api/assets/read-url` 换签。 -- 资源表只保存资源元数据;图层位置、尺寸、缩放、层级和选中所需 ID 保存在 `editor_canvas` 的布局 JSON。 +- 当前 MVP 的本地上传先以 data URL 持久化在素材记录中,保证刷新和跨项目可见;后续接入正式 OSS 上传时,只替换 `imageSrc/objectKey/assetObjectId` 的写入方式,账号级素材表和画布资源表不变。 +- 资源表只保存资源元数据;图层位置、尺寸、缩放、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。图层组第一版是画布内布局语义,不单独建表。 - 前端不直接订阅 SpacetimeDB,统一通过 api-server 的 `/api/editor/projects*` BFF 读写。 - 未登录用户可以使用本地演示态,但不触发工程自动保存;真实图片生成 / 修改需要登录。编辑器 API 请求允许使用 refresh cookie 静默补 access token,但 401 / 403 只在编辑器局部提示登录,不清空整站登录态,也不把后端 requestId 直接作为生图弹窗主文案。 @@ -51,6 +54,13 @@ - `PATCH /api/editor/projects/{projectId}/metadata`:重命名指定工程。 - `DELETE /api/editor/projects/{projectId}`:删除指定工程,并级联删除默认画布和资源元数据。 - `POST /api/editor/projects/{projectId}/resources`:创建画布资源记录,接收上传资源或真实生成资源元数据。 +- `GET /api/editor/assets/library`:读取当前账号的素材文件夹和素材。首次读取时自动创建“项目素材”默认文件夹。 +- `POST /api/editor/assets/folders`:新建素材文件夹。 +- `PATCH /api/editor/assets/folders/{folderId}`:重命名、折叠 / 展开素材文件夹。 +- `DELETE /api/editor/assets/folders/{folderId}`:删除素材文件夹;系统默认文件夹不能删除,普通文件夹删除时素材移入默认文件夹。 +- `POST /api/editor/assets`:批量或单个创建账号级素材,支持按钮上传和拖拽上传后的 data URL / 后续 OSS 元数据。 +- `PATCH /api/editor/assets/{assetId}`:重命名素材或移动素材到文件夹。 +- `DELETE /api/editor/assets/{assetId}`:删除素材。已放入画布的 project resource 不被级联删除,避免旧画布丢图。 - `POST /api/editor/images/generations`:按提示词调用 VectorEngine `gpt-image-2` 生成图片,返回 data URL、尺寸、prompt、model、provider 和 taskId。 - `POST /api/editor/images/edits`:按提示词和当前生成图 Data URL 调用 VectorEngine edits,返回新的生成图片元数据。 @@ -74,6 +84,12 @@ - 生成工具点击后显示画布内 `Image Generator` 占位框和跟随占位框的生成输入框,生成失败保留占位和输入状态,生成成功后在占位位置创建真实图层,并让输入框继续跟随该生成图。 - 生成资源显示元数据按钮,元数据窗口展示来源、prompt、model、provider、task、尺寸和 OSS 引用。 - 修改生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图。 +- 素材文件夹可以新建、折叠、重命名和删除;删除普通文件夹后,其素材移动到“项目素材”。 +- 上传按钮和拖拽上传都支持多文件;拖到文件夹或该文件夹内素材时进入目标文件夹;拖到画布时进入默认文件夹并在投放点创建画布图层。 +- 素材面板支持选择模式框选,一次选中多个素材,并可批量移动或删除上传素材。 +- 图层面板支持选择多个图层后创建图层组,组名和 groupId 随画布布局保存。 +- 小地图支持拖拽视口框,拖动时画布 viewport 跟随移动。 +- 鼠标滚轮默认垂直滚动画布视口;按住 Ctrl / Cmd 滚轮才缩放画布,并阻止浏览器页面缩放。 - 工程刷新后能从后端恢复资源、图层布局和 viewport。 - “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=` 并按 query 加载该工程。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 02b9cde2..6ba95baf 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -447,9 +447,23 @@ npm run check:server-rs-ddd - Rust 结构体:`EditorProjectResource` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` -- 说明:图片画布资源元数据表,保存上传 / 生成 / mock 生成图片的 OSS 引用、尺寸、来源类型、prompt、provider、task 和源资源关系;图片本体仍由资产 / OSS 链路承载。 +- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task 和源资源关系;账号级素材删除不级联删除该表,避免历史画布丢图。 - 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。 +### `editor_asset_folder` + +- Rust 结构体:`EditorAssetFolder` +- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` +- 说明:图片画布账号级素材文件夹表,归属于用户账号而不是 project;首次读取素材库时自动创建系统默认“项目素材”文件夹。文件夹支持重命名、折叠和删除,系统默认文件夹不能删除。 +- 索引:`by_editor_asset_folder_owner_user_id`。 + +### `editor_asset` + +- Rust 结构体:`EditorAsset` +- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` +- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、OSS 引用、尺寸、来源类型和生成元数据;素材在同一账号的所有项目中可见。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId。 +- 索引:`by_editor_asset_owner_user_id`、`by_editor_asset_folder_id`。 + ### `inventory_slot` - Rust 结构体:`InventorySlot` diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 07ca790b..34065c8c 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -8,8 +8,11 @@ use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use shared_kernel::build_prefixed_uuid_id; use spacetime_client::{ - EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput, - EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, + EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput, + EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord, EditorAssetFolderUpdateRecordInput, + EditorAssetLibraryRecord, EditorAssetRecord, EditorAssetUpdateRecordInput, EditorCanvasRecord, + EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, + EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord, SpacetimeClientError, }; @@ -29,6 +32,8 @@ use crate::{ const EDITOR_PROJECT_ID_PREFIX: &str = "editor-project-"; const EDITOR_RESOURCE_ID_PREFIX: &str = "editor-resource-"; +const EDITOR_ASSET_FOLDER_ID_PREFIX: &str = "editor-asset-folder-"; +const EDITOR_ASSET_ID_PREFIX: &str = "editor-asset-"; const EDITOR_LAYOUT_MAX_BYTES: usize = 256 * 1024; const EDITOR_PROJECT_DEFAULT_TITLE: &str = "未命名画布"; const EDITOR_IMAGE_GENERATION_SIZE: &str = "1024x1024"; @@ -77,6 +82,45 @@ pub struct EditorProjectResourceCreateRequest { source_resource_id: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetFolderCreateRequest { + label: String, + sort_order: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetFolderUpdateRequest { + label: Option, + collapsed: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetCreateRequest { + folder_id: String, + label: String, + image_src: String, + object_key: Option, + asset_object_id: Option, + width: u32, + height: u32, + source_type: String, + prompt: Option, + actual_prompt: Option, + model: Option, + provider: Option, + task_id: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetUpdateRequest { + label: Option, + folder_id: Option, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EditorImageGenerationRequest { @@ -120,6 +164,30 @@ pub struct EditorProjectResourceResponse { resource: EditorProjectResourcePayload, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetLibraryResponse { + library: EditorAssetLibraryPayload, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetFolderResponse { + folder: EditorAssetFolderPayload, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetFolderDeleteResponse { + library: EditorAssetLibraryPayload, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetResponse { + asset: EditorAssetPayload, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct EditorImageGenerationResponse { @@ -179,6 +247,46 @@ pub struct EditorProjectResourcePayload { updated_at: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetLibraryPayload { + folders: Vec, + assets: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetFolderPayload { + folder_id: String, + label: String, + sort_order: u32, + collapsed: bool, + system_default: bool, + created_at: String, + updated_at: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorAssetPayload { + asset_id: String, + folder_id: String, + label: String, + image_src: String, + object_key: Option, + asset_object_id: Option, + width: u32, + height: u32, + source_type: String, + prompt: Option, + actual_prompt: Option, + model: Option, + provider: Option, + task_id: Option, + created_at: String, + updated_at: String, +} + pub async fn load_recent_editor_project( State(state): State, Extension(request_context): Extension, @@ -386,6 +494,189 @@ pub async fn create_editor_project_resource( )) } +pub async fn get_editor_asset_library( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, AppError> { + let library = state + .spacetime_client() + .get_editor_asset_library(current_owner_user_id(&authenticated), current_utc_micros()) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetLibraryResponse { + library: editor_asset_library_payload_from_record(library), + }, + )) +} + +pub async fn create_editor_asset_folder( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, AppError> { + let folder = state + .spacetime_client() + .create_editor_asset_folder(EditorAssetFolderCreateRecordInput { + folder_id: build_prefixed_uuid_id(EDITOR_ASSET_FOLDER_ID_PREFIX), + owner_user_id: current_owner_user_id(&authenticated), + label: payload.label, + sort_order: payload.sort_order.unwrap_or(100), + now_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetFolderResponse { + folder: editor_asset_folder_payload_from_record(folder), + }, + )) +} + +pub async fn update_editor_asset_folder( + State(state): State, + Path(folder_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, AppError> { + let folder = state + .spacetime_client() + .update_editor_asset_folder(EditorAssetFolderUpdateRecordInput { + folder_id, + owner_user_id: current_owner_user_id(&authenticated), + label: normalize_optional_string(payload.label), + collapsed: payload.collapsed, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetFolderResponse { + folder: editor_asset_folder_payload_from_record(folder), + }, + )) +} + +pub async fn delete_editor_asset_folder( + State(state): State, + Path(folder_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, AppError> { + let library = state + .spacetime_client() + .delete_editor_asset_folder(EditorAssetFolderDeleteRecordInput { + folder_id, + owner_user_id: current_owner_user_id(&authenticated), + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetFolderDeleteResponse { + library: editor_asset_library_payload_from_record(library), + }, + )) +} + +pub async fn create_editor_asset( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, AppError> { + let asset = state + .spacetime_client() + .create_editor_asset(EditorAssetCreateRecordInput { + asset_id: build_prefixed_uuid_id(EDITOR_ASSET_ID_PREFIX), + owner_user_id: current_owner_user_id(&authenticated), + 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), + width: payload.width, + height: payload.height, + source_type: payload.source_type, + prompt: normalize_optional_string(payload.prompt), + actual_prompt: normalize_optional_string(payload.actual_prompt), + model: normalize_optional_string(payload.model), + provider: normalize_optional_string(payload.provider), + task_id: normalize_optional_string(payload.task_id), + now_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetResponse { + asset: editor_asset_payload_from_record(asset), + }, + )) +} + +pub async fn update_editor_asset( + State(state): State, + Path(asset_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, AppError> { + let asset = state + .spacetime_client() + .update_editor_asset(EditorAssetUpdateRecordInput { + asset_id, + owner_user_id: current_owner_user_id(&authenticated), + label: normalize_optional_string(payload.label), + folder_id: normalize_optional_string(payload.folder_id), + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetResponse { + asset: editor_asset_payload_from_record(asset), + }, + )) +} + +pub async fn delete_editor_asset( + State(state): State, + Path(asset_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, AppError> { + let asset = state + .spacetime_client() + .delete_editor_asset(EditorAssetDeleteRecordInput { + asset_id, + owner_user_id: current_owner_user_id(&authenticated), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorAssetResponse { + asset: editor_asset_payload_from_record(asset), + }, + )) +} + pub async fn generate_editor_image( State(state): State, Extension(request_context): Extension, @@ -576,6 +867,58 @@ fn editor_project_resource_payload_from_record( } } +fn editor_asset_library_payload_from_record( + record: EditorAssetLibraryRecord, +) -> EditorAssetLibraryPayload { + EditorAssetLibraryPayload { + folders: record + .folders + .into_iter() + .map(editor_asset_folder_payload_from_record) + .collect(), + assets: record + .assets + .into_iter() + .map(editor_asset_payload_from_record) + .collect(), + } +} + +fn editor_asset_folder_payload_from_record( + record: EditorAssetFolderRecord, +) -> EditorAssetFolderPayload { + EditorAssetFolderPayload { + folder_id: record.folder_id, + label: record.label, + sort_order: record.sort_order, + collapsed: record.collapsed, + system_default: record.system_default, + created_at: record.created_at, + updated_at: record.updated_at, + } +} + +fn editor_asset_payload_from_record(record: EditorAssetRecord) -> EditorAssetPayload { + EditorAssetPayload { + asset_id: record.asset_id, + folder_id: record.folder_id, + label: record.label, + image_src: record.image_src, + object_key: record.object_key, + asset_object_id: record.asset_object_id, + width: record.width, + height: record.height, + source_type: record.source_type, + prompt: record.prompt, + actual_prompt: record.actual_prompt, + model: record.model, + provider: record.provider, + task_id: record.task_id, + created_at: record.created_at, + updated_at: record.updated_at, + } +} + impl EditorCanvasViewportPayload { fn into_record(self) -> EditorCanvasViewportRecord { EditorCanvasViewportRecord { @@ -586,6 +929,10 @@ impl EditorCanvasViewportPayload { } } +fn current_owner_user_id(authenticated: &AuthenticatedAccessToken) -> String { + authenticated.claims().user_id().to_string() +} + fn serialize_editor_layers(layers: Value) -> Result { let payload = serde_json::to_string(&layers).map_err(|error| { AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ diff --git a/server-rs/crates/api-server/src/modules/editor_project.rs b/server-rs/crates/api-server/src/modules/editor_project.rs index d1de7506..76f6e3d8 100644 --- a/server-rs/crates/api-server/src/modules/editor_project.rs +++ b/server-rs/crates/api-server/src/modules/editor_project.rs @@ -6,9 +6,12 @@ use axum::{ use crate::{ auth::require_bearer_auth, editor_project::{ - create_editor_project, create_editor_project_resource, delete_editor_project, - edit_editor_image, generate_editor_image, get_editor_project, list_editor_projects, + create_editor_asset, create_editor_asset_folder, create_editor_project, + create_editor_project_resource, delete_editor_asset, delete_editor_asset_folder, + delete_editor_project, edit_editor_image, generate_editor_image, + get_editor_asset_library, get_editor_project, list_editor_projects, load_recent_editor_project, rename_editor_project, save_editor_project_layout, + update_editor_asset, update_editor_asset_folder, }, state::AppState, }; @@ -55,6 +58,45 @@ pub fn router(state: AppState) -> Router { require_bearer_auth, )), ) + .route( + "/api/editor/assets/library", + get(get_editor_asset_library).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/editor/assets/folders", + post(create_editor_asset_folder).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/editor/assets/folders/{folder_id}", + patch(update_editor_asset_folder) + .delete(delete_editor_asset_folder) + .route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/editor/assets", + post(create_editor_asset).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/editor/assets/{asset_id}", + patch(update_editor_asset) + .delete(delete_editor_asset) + .route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) .route( "/api/editor/images/generations", post(generate_editor_image).route_layer(middleware::from_fn_with_state( diff --git a/server-rs/crates/spacetime-client/src/editor_project.rs b/server-rs/crates/spacetime-client/src/editor_project.rs index 9712f231..1aa57732 100644 --- a/server-rs/crates/spacetime-client/src/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/editor_project.rs @@ -188,4 +188,174 @@ impl SpacetimeClient { ) .await } + + pub async fn get_editor_asset_library( + &self, + owner_user_id: String, + now_micros: i64, + ) -> Result { + let procedure_input = EditorAssetLibraryGetInput { + owner_user_id, + now_micros, + }; + + self.call_after_connect( + "get_editor_asset_library_and_return", + move |connection, sender| { + connection + .procedures() + .get_editor_asset_library_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_asset_library_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn create_editor_asset_folder( + &self, + input: EditorAssetFolderCreateRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "create_editor_asset_folder_and_return", + move |connection, sender| { + connection + .procedures() + .create_editor_asset_folder_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_asset_folder_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn update_editor_asset_folder( + &self, + input: EditorAssetFolderUpdateRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "update_editor_asset_folder_and_return", + move |connection, sender| { + connection + .procedures() + .update_editor_asset_folder_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_asset_folder_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn delete_editor_asset_folder( + &self, + input: EditorAssetFolderDeleteRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "delete_editor_asset_folder_and_return", + move |connection, sender| { + connection + .procedures() + .delete_editor_asset_folder_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_asset_folder_library_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn create_editor_asset( + &self, + input: EditorAssetCreateRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "create_editor_asset_and_return", + move |connection, sender| { + connection + .procedures() + .create_editor_asset_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 update_editor_asset( + &self, + input: EditorAssetUpdateRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "update_editor_asset_and_return", + move |connection, sender| { + connection + .procedures() + .update_editor_asset_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, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "delete_editor_asset_and_return", + move |connection, sender| { + connection + .procedures() + .delete_editor_asset_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 + } } diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index ed32edcb..9036707a 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -30,10 +30,14 @@ pub use mapper::{ CustomWorldPublishGateRecord, CustomWorldPublishWorldRecord, CustomWorldPublishWorldRecordInput, CustomWorldPublishedProfileCompileRecord, CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord, - CustomWorldWorkSummaryRecord, EditorCanvasRecord, EditorCanvasViewportRecord, - EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, - EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, - EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord, + CustomWorldWorkSummaryRecord, EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, + EditorAssetFolderCreateRecordInput, EditorAssetFolderDeleteRecordInput, + EditorAssetFolderRecord, EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord, + EditorAssetRecord, EditorAssetUpdateRecordInput, EditorCanvasRecord, + EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, + EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord, + EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, + EditorProjectResourceRecord, ExternalGenerationJobClaimRecordInput, ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 970a27c4..cbd0a822 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -43,8 +43,11 @@ pub use self::combat::{ ResolveCombatActionRecord, }; pub use self::editor_project::{ - EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput, - EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, + EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput, + EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord, EditorAssetFolderUpdateRecordInput, + EditorAssetLibraryRecord, EditorAssetRecord, EditorAssetUpdateRecordInput, EditorCanvasRecord, + EditorCanvasViewportRecord, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput, + EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceRecord, }; @@ -194,9 +197,11 @@ pub(crate) use self::custom_world::{ parse_rpg_agent_stage_record, }; pub(crate) use self::editor_project::{ - 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_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, }; pub(crate) use self::external_generation::{ map_external_generation_job_claim_result, map_external_generation_job_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs index 1a692a81..3db100f8 100644 --- a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs @@ -51,6 +51,43 @@ pub struct EditorProjectResourceRecord { pub updated_at: String, } +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorAssetFolderRecord { + pub folder_id: String, + pub label: String, + pub sort_order: u32, + pub collapsed: bool, + pub system_default: bool, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorAssetRecord { + pub asset_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorAssetLibraryRecord { + pub folders: Vec, + pub assets: Vec, +} + #[derive(Clone, Debug, PartialEq)] pub struct EditorProjectCreateRecordInput { pub project_id: String, @@ -108,6 +145,66 @@ pub struct EditorProjectResourceCreateRecordInput { pub updated_at_micros: i64, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorAssetFolderCreateRecordInput { + pub folder_id: String, + pub owner_user_id: String, + pub label: String, + pub sort_order: u32, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorAssetFolderUpdateRecordInput { + pub folder_id: String, + pub owner_user_id: String, + pub label: Option, + pub collapsed: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorAssetFolderDeleteRecordInput { + pub folder_id: String, + pub owner_user_id: String, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorAssetCreateRecordInput { + pub asset_id: String, + pub owner_user_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorAssetUpdateRecordInput { + pub asset_id: String, + pub owner_user_id: String, + pub label: Option, + pub folder_id: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorAssetDeleteRecordInput { + pub asset_id: String, + pub owner_user_id: String, +} + impl From for crate::module_bindings::EditorProjectCreateInput { fn from(input: EditorProjectCreateRecordInput) -> Self { Self { @@ -191,6 +288,90 @@ impl From } } +impl From + for crate::module_bindings::EditorAssetFolderCreateInput +{ + fn from(input: EditorAssetFolderCreateRecordInput) -> Self { + Self { + folder_id: input.folder_id, + owner_user_id: input.owner_user_id, + label: input.label, + sort_order: input.sort_order, + now_micros: input.now_micros, + } + } +} + +impl From + for crate::module_bindings::EditorAssetFolderUpdateInput +{ + fn from(input: EditorAssetFolderUpdateRecordInput) -> Self { + Self { + folder_id: input.folder_id, + owner_user_id: input.owner_user_id, + label: input.label, + collapsed: input.collapsed, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From + for crate::module_bindings::EditorAssetFolderDeleteInput +{ + fn from(input: EditorAssetFolderDeleteRecordInput) -> Self { + Self { + folder_id: input.folder_id, + owner_user_id: input.owner_user_id, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From for crate::module_bindings::EditorAssetCreateInput { + fn from(input: EditorAssetCreateRecordInput) -> Self { + Self { + asset_id: input.asset_id, + owner_user_id: input.owner_user_id, + folder_id: input.folder_id, + label: input.label, + asset_object_id: input.asset_object_id, + image_src: input.image_src, + object_key: input.object_key, + width: input.width, + height: input.height, + source_type: input.source_type, + prompt: input.prompt, + actual_prompt: input.actual_prompt, + model: input.model, + provider: input.provider, + task_id: input.task_id, + now_micros: input.now_micros, + } + } +} + +impl From for crate::module_bindings::EditorAssetUpdateInput { + fn from(input: EditorAssetUpdateRecordInput) -> Self { + Self { + asset_id: input.asset_id, + owner_user_id: input.owner_user_id, + label: input.label, + folder_id: input.folder_id, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From for crate::module_bindings::EditorAssetDeleteInput { + fn from(input: EditorAssetDeleteRecordInput) -> Self { + Self { + asset_id: input.asset_id, + owner_user_id: input.owner_user_id, + } + } +} + pub(crate) fn map_editor_project_optional_procedure_result( result: EditorProjectProcedureResult, ) -> Result, SpacetimeClientError> { @@ -248,6 +429,58 @@ pub(crate) fn map_editor_project_resource_procedure_result( .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布资源快照")) } +pub(crate) fn map_editor_asset_library_procedure_result( + result: EditorAssetLibraryProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + result + .library + .map(map_editor_asset_library_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材库快照")) +} + +pub(crate) fn map_editor_asset_folder_procedure_result( + result: EditorAssetFolderProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + result + .folder + .map(map_editor_asset_folder_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材文件夹快照")) +} + +pub(crate) fn map_editor_asset_folder_library_procedure_result( + result: EditorAssetFolderProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + result + .library + .map(map_editor_asset_library_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材库快照")) +} + +pub(crate) fn map_editor_asset_procedure_result( + result: EditorAssetProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + result + .asset + .map(map_editor_asset_snapshot) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材快照")) +} + fn map_editor_project_snapshot( snapshot: EditorProjectSnapshot, ) -> Result { @@ -309,3 +542,55 @@ fn map_editor_project_resource_snapshot( updated_at: format_timestamp_micros(snapshot.updated_at_micros), } } + +fn map_editor_asset_library_snapshot( + snapshot: EditorAssetLibrarySnapshot, +) -> EditorAssetLibraryRecord { + EditorAssetLibraryRecord { + folders: snapshot + .folders + .into_iter() + .map(map_editor_asset_folder_snapshot) + .collect(), + assets: snapshot + .assets + .into_iter() + .map(map_editor_asset_snapshot) + .collect(), + } +} + +fn map_editor_asset_folder_snapshot( + snapshot: EditorAssetFolderSnapshot, +) -> EditorAssetFolderRecord { + EditorAssetFolderRecord { + folder_id: snapshot.folder_id, + label: snapshot.label, + sort_order: snapshot.sort_order, + collapsed: snapshot.collapsed, + system_default: snapshot.system_default, + created_at: format_timestamp_micros(snapshot.created_at_micros), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_editor_asset_snapshot(snapshot: EditorAssetSnapshot) -> EditorAssetRecord { + EditorAssetRecord { + asset_id: snapshot.asset_id, + folder_id: snapshot.folder_id, + label: snapshot.label, + asset_object_id: snapshot.asset_object_id, + image_src: snapshot.image_src, + object_key: snapshot.object_key, + width: snapshot.width, + height: snapshot.height, + source_type: snapshot.source_type, + prompt: snapshot.prompt, + actual_prompt: snapshot.actual_prompt, + model: snapshot.model, + provider: snapshot.provider, + task_id: snapshot.task_id, + created_at: format_timestamp_micros(snapshot.created_at_micros), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index d85293d0..9f964c90 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -236,6 +236,8 @@ pub mod create_battle_state_and_return_procedure; pub mod create_battle_state_reducer; pub mod create_big_fish_session_procedure; pub mod create_custom_world_agent_session_procedure; +pub mod create_editor_asset_and_return_procedure; +pub mod create_editor_asset_folder_and_return_procedure; pub mod create_editor_project_and_return_procedure; pub mod create_editor_project_resource_and_return_procedure; pub mod create_jump_hop_agent_session_procedure; @@ -338,6 +340,8 @@ pub mod delete_bark_battle_work_procedure; pub mod delete_big_fish_work_procedure; pub mod delete_custom_world_agent_session_procedure; pub mod delete_custom_world_profile_and_return_procedure; +pub mod delete_editor_asset_and_return_procedure; +pub mod delete_editor_asset_folder_and_return_procedure; pub mod delete_editor_project_and_return_procedure; pub mod delete_jump_hop_work_procedure; pub mod delete_match_3_d_work_procedure; @@ -348,6 +352,23 @@ pub mod delete_visual_novel_work_procedure; pub mod delete_wooden_fish_work_procedure; pub mod drag_puzzle_piece_or_group_procedure; pub mod drop_square_hole_shape_procedure; +pub mod editor_asset_create_input_type; +pub mod editor_asset_delete_input_type; +pub mod editor_asset_folder_create_input_type; +pub mod editor_asset_folder_delete_input_type; +pub mod editor_asset_folder_procedure_result_type; +pub mod editor_asset_folder_snapshot_type; +pub mod editor_asset_folder_table; +pub mod editor_asset_folder_type; +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_procedure_result_type; +pub mod editor_asset_snapshot_type; +pub mod editor_asset_table; +pub mod editor_asset_type; +pub mod editor_asset_update_input_type; pub mod editor_canvas_snapshot_type; pub mod editor_canvas_table; pub mod editor_canvas_type; @@ -414,6 +435,7 @@ pub mod get_custom_world_agent_session_procedure; pub mod get_custom_world_gallery_detail_by_code_procedure; pub mod get_custom_world_gallery_detail_procedure; pub mod get_custom_world_library_detail_procedure; +pub mod get_editor_asset_library_and_return_procedure; pub mod get_editor_project_and_return_procedure; pub mod get_external_generation_job_and_return_procedure; pub mod get_external_generation_queue_stats_and_return_procedure; @@ -1063,6 +1085,8 @@ pub mod unequip_inventory_item_input_type; pub mod unpublish_custom_world_profile_and_return_procedure; 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_jump_hop_work_procedure; pub mod update_match_3_d_work_procedure; pub mod update_puzzle_clear_work_procedure; @@ -1402,6 +1426,8 @@ pub use create_battle_state_and_return_procedure::create_battle_state_and_return pub use create_battle_state_reducer::create_battle_state; pub use create_big_fish_session_procedure::create_big_fish_session; pub use create_custom_world_agent_session_procedure::create_custom_world_agent_session; +pub use create_editor_asset_and_return_procedure::create_editor_asset_and_return; +pub use create_editor_asset_folder_and_return_procedure::create_editor_asset_folder_and_return; pub use create_editor_project_and_return_procedure::create_editor_project_and_return; pub use create_editor_project_resource_and_return_procedure::create_editor_project_resource_and_return; pub use create_jump_hop_agent_session_procedure::create_jump_hop_agent_session; @@ -1504,6 +1530,8 @@ pub use delete_bark_battle_work_procedure::delete_bark_battle_work; pub use delete_big_fish_work_procedure::delete_big_fish_work; pub use delete_custom_world_agent_session_procedure::delete_custom_world_agent_session; pub use delete_custom_world_profile_and_return_procedure::delete_custom_world_profile_and_return; +pub use delete_editor_asset_and_return_procedure::delete_editor_asset_and_return; +pub use delete_editor_asset_folder_and_return_procedure::delete_editor_asset_folder_and_return; pub use delete_editor_project_and_return_procedure::delete_editor_project_and_return; pub use delete_jump_hop_work_procedure::delete_jump_hop_work; pub use delete_match_3_d_work_procedure::delete_match_3_d_work; @@ -1514,6 +1542,23 @@ pub use delete_visual_novel_work_procedure::delete_visual_novel_work; pub use delete_wooden_fish_work_procedure::delete_wooden_fish_work; pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group; pub use drop_square_hole_shape_procedure::drop_square_hole_shape; +pub use editor_asset_create_input_type::EditorAssetCreateInput; +pub use editor_asset_delete_input_type::EditorAssetDeleteInput; +pub use editor_asset_folder_create_input_type::EditorAssetFolderCreateInput; +pub use editor_asset_folder_delete_input_type::EditorAssetFolderDeleteInput; +pub use editor_asset_folder_procedure_result_type::EditorAssetFolderProcedureResult; +pub use editor_asset_folder_snapshot_type::EditorAssetFolderSnapshot; +pub use editor_asset_folder_table::*; +pub use editor_asset_folder_type::EditorAssetFolder; +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_procedure_result_type::EditorAssetProcedureResult; +pub use editor_asset_snapshot_type::EditorAssetSnapshot; +pub use editor_asset_table::*; +pub use editor_asset_type::EditorAsset; +pub use editor_asset_update_input_type::EditorAssetUpdateInput; pub use editor_canvas_snapshot_type::EditorCanvasSnapshot; pub use editor_canvas_table::*; pub use editor_canvas_type::EditorCanvas; @@ -1580,6 +1625,7 @@ pub use get_custom_world_agent_session_procedure::get_custom_world_agent_session pub use get_custom_world_gallery_detail_by_code_procedure::get_custom_world_gallery_detail_by_code; pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_detail; pub use get_custom_world_library_detail_procedure::get_custom_world_library_detail; +pub use get_editor_asset_library_and_return_procedure::get_editor_asset_library_and_return; pub use get_editor_project_and_return_procedure::get_editor_project_and_return; pub use get_external_generation_job_and_return_procedure::get_external_generation_job_and_return; pub use get_external_generation_queue_stats_and_return_procedure::get_external_generation_queue_stats_and_return; @@ -2229,6 +2275,8 @@ pub use unequip_inventory_item_input_type::UnequipInventoryItemInput; pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return; 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_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; @@ -2649,6 +2697,8 @@ pub struct DbUpdate { custom_world_session: __sdk::TableUpdate, database_migration_import_chunk: __sdk::TableUpdate, database_migration_operator: __sdk::TableUpdate, + editor_asset: __sdk::TableUpdate, + editor_asset_folder: __sdk::TableUpdate, editor_canvas: __sdk::TableUpdate, editor_project: __sdk::TableUpdate, editor_project_resource: __sdk::TableUpdate, @@ -2865,6 +2915,12 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "database_migration_operator" => db_update.database_migration_operator.append( database_migration_operator_table::parse_table_update(table_update)?, ), + "editor_asset" => db_update + .editor_asset + .append(editor_asset_table::parse_table_update(table_update)?), + "editor_asset_folder" => db_update + .editor_asset_folder + .append(editor_asset_folder_table::parse_table_update(table_update)?), "editor_canvas" => db_update .editor_canvas .append(editor_canvas_table::parse_table_update(table_update)?), @@ -3337,6 +3393,15 @@ impl __sdk::DbUpdate for DbUpdate { &self.database_migration_operator, ) .with_updates_by_pk(|row| &row.operator_identity); + diff.editor_asset = cache + .apply_diff_to_table::("editor_asset", &self.editor_asset) + .with_updates_by_pk(|row| &row.asset_id); + diff.editor_asset_folder = cache + .apply_diff_to_table::( + "editor_asset_folder", + &self.editor_asset_folder, + ) + .with_updates_by_pk(|row| &row.folder_id); diff.editor_canvas = cache .apply_diff_to_table::("editor_canvas", &self.editor_canvas) .with_updates_by_pk(|row| &row.canvas_id); @@ -3882,6 +3947,12 @@ impl __sdk::DbUpdate for DbUpdate { "database_migration_operator" => db_update .database_migration_operator .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "editor_asset" => db_update + .editor_asset + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "editor_asset_folder" => db_update + .editor_asset_folder + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "editor_canvas" => db_update .editor_canvas .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -4261,6 +4332,12 @@ impl __sdk::DbUpdate for DbUpdate { "database_migration_operator" => db_update .database_migration_operator .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "editor_asset" => db_update + .editor_asset + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "editor_asset_folder" => db_update + .editor_asset_folder + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "editor_canvas" => db_update .editor_canvas .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -4566,6 +4643,8 @@ pub struct AppliedDiff<'r> { custom_world_session: __sdk::TableAppliedDiff<'r, CustomWorldSession>, database_migration_import_chunk: __sdk::TableAppliedDiff<'r, DatabaseMigrationImportChunk>, database_migration_operator: __sdk::TableAppliedDiff<'r, DatabaseMigrationOperator>, + editor_asset: __sdk::TableAppliedDiff<'r, EditorAsset>, + editor_asset_folder: __sdk::TableAppliedDiff<'r, EditorAssetFolder>, editor_canvas: __sdk::TableAppliedDiff<'r, EditorCanvas>, editor_project: __sdk::TableAppliedDiff<'r, EditorProject>, editor_project_resource: __sdk::TableAppliedDiff<'r, EditorProjectResource>, @@ -4850,6 +4929,16 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.database_migration_operator, event, ); + callbacks.invoke_table_row_callbacks::( + "editor_asset", + &self.editor_asset, + event, + ); + callbacks.invoke_table_row_callbacks::( + "editor_asset_folder", + &self.editor_asset_folder, + event, + ); callbacks.invoke_table_row_callbacks::( "editor_canvas", &self.editor_canvas, @@ -5952,6 +6041,8 @@ impl __sdk::SpacetimeModule for RemoteModule { custom_world_session_table::register_table(client_cache); database_migration_import_chunk_table::register_table(client_cache); database_migration_operator_table::register_table(client_cache); + editor_asset_table::register_table(client_cache); + editor_asset_folder_table::register_table(client_cache); editor_canvas_table::register_table(client_cache); editor_project_table::register_table(client_cache); editor_project_resource_table::register_table(client_cache); @@ -6076,6 +6167,8 @@ impl __sdk::SpacetimeModule for RemoteModule { "custom_world_session", "database_migration_import_chunk", "database_migration_operator", + "editor_asset", + "editor_asset_folder", "editor_canvas", "editor_project", "editor_project_resource", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_editor_asset_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_editor_asset_and_return_procedure.rs new file mode 100644 index 00000000..8ca47543 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_editor_asset_and_return_procedure.rs @@ -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_create_input_type::EditorAssetCreateInput; +use super::editor_asset_procedure_result_type::EditorAssetProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreateEditorAssetAndReturnArgs { + pub input: EditorAssetCreateInput, +} + +impl __sdk::InModule for CreateEditorAssetAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_editor_asset_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_editor_asset_and_return { + fn create_editor_asset_and_return(&self, input: EditorAssetCreateInput) { + self.create_editor_asset_and_return_then(input, |_, _| {}); + } + + fn create_editor_asset_and_return_then( + &self, + input: EditorAssetCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_editor_asset_and_return for super::RemoteProcedures { + fn create_editor_asset_and_return_then( + &self, + input: EditorAssetCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetProcedureResult>( + "create_editor_asset_and_return", + CreateEditorAssetAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_editor_asset_folder_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_editor_asset_folder_and_return_procedure.rs new file mode 100644 index 00000000..33ec7f85 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_editor_asset_folder_and_return_procedure.rs @@ -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_folder_create_input_type::EditorAssetFolderCreateInput; +use super::editor_asset_folder_procedure_result_type::EditorAssetFolderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CreateEditorAssetFolderAndReturnArgs { + pub input: EditorAssetFolderCreateInput, +} + +impl __sdk::InModule for CreateEditorAssetFolderAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_editor_asset_folder_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_editor_asset_folder_and_return { + fn create_editor_asset_folder_and_return(&self, input: EditorAssetFolderCreateInput) { + self.create_editor_asset_folder_and_return_then(input, |_, _| {}); + } + + fn create_editor_asset_folder_and_return_then( + &self, + input: EditorAssetFolderCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl create_editor_asset_folder_and_return for super::RemoteProcedures { + fn create_editor_asset_folder_and_return_then( + &self, + input: EditorAssetFolderCreateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetFolderProcedureResult>( + "create_editor_asset_folder_and_return", + CreateEditorAssetFolderAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/delete_editor_asset_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/delete_editor_asset_and_return_procedure.rs new file mode 100644 index 00000000..4c71ecd5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/delete_editor_asset_and_return_procedure.rs @@ -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_delete_input_type::EditorAssetDeleteInput; +use super::editor_asset_procedure_result_type::EditorAssetProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct DeleteEditorAssetAndReturnArgs { + pub input: EditorAssetDeleteInput, +} + +impl __sdk::InModule for DeleteEditorAssetAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `delete_editor_asset_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait delete_editor_asset_and_return { + fn delete_editor_asset_and_return(&self, input: EditorAssetDeleteInput) { + self.delete_editor_asset_and_return_then(input, |_, _| {}); + } + + fn delete_editor_asset_and_return_then( + &self, + input: EditorAssetDeleteInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl delete_editor_asset_and_return for super::RemoteProcedures { + fn delete_editor_asset_and_return_then( + &self, + input: EditorAssetDeleteInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetProcedureResult>( + "delete_editor_asset_and_return", + DeleteEditorAssetAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/delete_editor_asset_folder_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/delete_editor_asset_folder_and_return_procedure.rs new file mode 100644 index 00000000..6500a7c7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/delete_editor_asset_folder_and_return_procedure.rs @@ -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_folder_delete_input_type::EditorAssetFolderDeleteInput; +use super::editor_asset_folder_procedure_result_type::EditorAssetFolderProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct DeleteEditorAssetFolderAndReturnArgs { + pub input: EditorAssetFolderDeleteInput, +} + +impl __sdk::InModule for DeleteEditorAssetFolderAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `delete_editor_asset_folder_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait delete_editor_asset_folder_and_return { + fn delete_editor_asset_folder_and_return(&self, input: EditorAssetFolderDeleteInput) { + self.delete_editor_asset_folder_and_return_then(input, |_, _| {}); + } + + fn delete_editor_asset_folder_and_return_then( + &self, + input: EditorAssetFolderDeleteInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl delete_editor_asset_folder_and_return for super::RemoteProcedures { + fn delete_editor_asset_folder_and_return_then( + &self, + input: EditorAssetFolderDeleteInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetFolderProcedureResult>( + "delete_editor_asset_folder_and_return", + DeleteEditorAssetFolderAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs new file mode 100644 index 00000000..5c5df754 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs @@ -0,0 +1,30 @@ +// 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 EditorAssetCreateInput { + pub asset_id: String, + pub owner_user_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub now_micros: i64, +} + +impl __sdk::InModule for EditorAssetCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_delete_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_delete_input_type.rs new file mode 100644 index 00000000..98f46bd6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_delete_input_type.rs @@ -0,0 +1,16 @@ +// 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 EditorAssetDeleteInput { + pub asset_id: String, + pub owner_user_id: String, +} + +impl __sdk::InModule for EditorAssetDeleteInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_create_input_type.rs new file mode 100644 index 00000000..91b85d8c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_create_input_type.rs @@ -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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetFolderCreateInput { + pub folder_id: String, + pub owner_user_id: String, + pub label: String, + pub sort_order: u32, + pub now_micros: i64, +} + +impl __sdk::InModule for EditorAssetFolderCreateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_delete_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_delete_input_type.rs new file mode 100644 index 00000000..aecacc93 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_delete_input_type.rs @@ -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 EditorAssetFolderDeleteInput { + pub folder_id: String, + pub owner_user_id: String, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorAssetFolderDeleteInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_procedure_result_type.rs new file mode 100644 index 00000000..5507cb7d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_procedure_result_type.rs @@ -0,0 +1,21 @@ +// 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_folder_snapshot_type::EditorAssetFolderSnapshot; +use super::editor_asset_library_snapshot_type::EditorAssetLibrarySnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetFolderProcedureResult { + pub ok: bool, + pub folder: Option, + pub library: Option, + pub error_message: Option, +} + +impl __sdk::InModule for EditorAssetFolderProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_snapshot_type.rs new file mode 100644 index 00000000..193ce49f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_snapshot_type.rs @@ -0,0 +1,21 @@ +// 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 EditorAssetFolderSnapshot { + pub folder_id: String, + pub label: String, + pub sort_order: u32, + pub collapsed: bool, + pub system_default: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorAssetFolderSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_table.rs new file mode 100644 index 00000000..d90c6375 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_table.rs @@ -0,0 +1,161 @@ +// 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 super::editor_asset_folder_type::EditorAssetFolder; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `editor_asset_folder`. +/// +/// Obtain a handle from the [`EditorAssetFolderTableAccess::editor_asset_folder`] method on [`super::RemoteTables`], +/// like `ctx.db.editor_asset_folder()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_asset_folder().on_insert(...)`. +pub struct EditorAssetFolderTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `editor_asset_folder`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait EditorAssetFolderTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`EditorAssetFolderTableHandle`], which mediates access to the table `editor_asset_folder`. + fn editor_asset_folder(&self) -> EditorAssetFolderTableHandle<'_>; +} + +impl EditorAssetFolderTableAccess for super::RemoteTables { + fn editor_asset_folder(&self) -> EditorAssetFolderTableHandle<'_> { + EditorAssetFolderTableHandle { + imp: self + .imp + .get_table::("editor_asset_folder"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct EditorAssetFolderInsertCallbackId(__sdk::CallbackId); +pub struct EditorAssetFolderDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for EditorAssetFolderTableHandle<'ctx> { + type Row = EditorAssetFolder; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = EditorAssetFolderInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorAssetFolderInsertCallbackId { + EditorAssetFolderInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: EditorAssetFolderInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = EditorAssetFolderDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorAssetFolderDeleteCallbackId { + EditorAssetFolderDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: EditorAssetFolderDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct EditorAssetFolderUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for EditorAssetFolderTableHandle<'ctx> { + type UpdateCallbackId = EditorAssetFolderUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> EditorAssetFolderUpdateCallbackId { + EditorAssetFolderUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: EditorAssetFolderUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `folder_id` unique index on the table `editor_asset_folder`, +/// which allows point queries on the field of the same name +/// via the [`EditorAssetFolderFolderIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_asset_folder().folder_id().find(...)`. +pub struct EditorAssetFolderFolderIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> EditorAssetFolderTableHandle<'ctx> { + /// Get a handle on the `folder_id` unique index on the table `editor_asset_folder`. + pub fn folder_id(&self) -> EditorAssetFolderFolderIdUnique<'ctx> { + EditorAssetFolderFolderIdUnique { + imp: self.imp.get_unique_constraint::("folder_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> EditorAssetFolderFolderIdUnique<'ctx> { + /// Find the subscribed row whose `folder_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("editor_asset_folder"); + _table.add_unique_constraint::("folder_id", |row| &row.folder_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `EditorAssetFolder`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait editor_asset_folderQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `EditorAssetFolder`. + fn editor_asset_folder(&self) -> __sdk::__query_builder::Table; +} + +impl editor_asset_folderQueryTableAccess for __sdk::QueryTableAccessor { + fn editor_asset_folder(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("editor_asset_folder") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_type.rs new file mode 100644 index 00000000..70b838d7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_type.rs @@ -0,0 +1,72 @@ +// 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 EditorAssetFolder { + pub folder_id: String, + pub owner_user_id: String, + pub label: String, + pub sort_order: u32, + pub collapsed: bool, + pub system_default: bool, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for EditorAssetFolder { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `EditorAssetFolder`. +/// +/// Provides typed access to columns for query building. +pub struct EditorAssetFolderCols { + pub folder_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub label: __sdk::__query_builder::Col, + pub sort_order: __sdk::__query_builder::Col, + pub collapsed: __sdk::__query_builder::Col, + pub system_default: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for EditorAssetFolder { + type Cols = EditorAssetFolderCols; + fn cols(table_name: &'static str) -> Self::Cols { + EditorAssetFolderCols { + folder_id: __sdk::__query_builder::Col::new(table_name, "folder_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + label: __sdk::__query_builder::Col::new(table_name, "label"), + sort_order: __sdk::__query_builder::Col::new(table_name, "sort_order"), + collapsed: __sdk::__query_builder::Col::new(table_name, "collapsed"), + system_default: __sdk::__query_builder::Col::new(table_name, "system_default"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `EditorAssetFolder`. +/// +/// Provides typed access to indexed columns for query building. +pub struct EditorAssetFolderIxCols { + pub folder_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for EditorAssetFolder { + type IxCols = EditorAssetFolderIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + EditorAssetFolderIxCols { + folder_id: __sdk::__query_builder::IxCol::new(table_name, "folder_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for EditorAssetFolder {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_update_input_type.rs new file mode 100644 index 00000000..9004985c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_folder_update_input_type.rs @@ -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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetFolderUpdateInput { + pub folder_id: String, + pub owner_user_id: String, + pub label: Option, + pub collapsed: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorAssetFolderUpdateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_get_input_type.rs new file mode 100644 index 00000000..c26a3223 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_get_input_type.rs @@ -0,0 +1,16 @@ +// 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 EditorAssetLibraryGetInput { + pub owner_user_id: String, + pub now_micros: i64, +} + +impl __sdk::InModule for EditorAssetLibraryGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_procedure_result_type.rs new file mode 100644 index 00000000..3238b319 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_procedure_result_type.rs @@ -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_asset_library_snapshot_type::EditorAssetLibrarySnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetLibraryProcedureResult { + pub ok: bool, + pub library: Option, + pub error_message: Option, +} + +impl __sdk::InModule for EditorAssetLibraryProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_snapshot_type.rs new file mode 100644 index 00000000..6c59ede1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_library_snapshot_type.rs @@ -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_asset_folder_snapshot_type::EditorAssetFolderSnapshot; +use super::editor_asset_snapshot_type::EditorAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetLibrarySnapshot { + pub folders: Vec, + pub assets: Vec, +} + +impl __sdk::InModule for EditorAssetLibrarySnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_procedure_result_type.rs new file mode 100644 index 00000000..e8de2959 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_procedure_result_type.rs @@ -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_asset_snapshot_type::EditorAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetProcedureResult { + pub ok: bool, + pub asset: Option, + pub error_message: Option, +} + +impl __sdk::InModule for EditorAssetProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs new file mode 100644 index 00000000..2bcb7ced --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs @@ -0,0 +1,30 @@ +// 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 EditorAssetSnapshot { + pub asset_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorAssetSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_table.rs new file mode 100644 index 00000000..2bb93bb2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_table.rs @@ -0,0 +1,159 @@ +// 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 super::editor_asset_type::EditorAsset; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `editor_asset`. +/// +/// Obtain a handle from the [`EditorAssetTableAccess::editor_asset`] method on [`super::RemoteTables`], +/// like `ctx.db.editor_asset()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_asset().on_insert(...)`. +pub struct EditorAssetTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `editor_asset`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait EditorAssetTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`EditorAssetTableHandle`], which mediates access to the table `editor_asset`. + fn editor_asset(&self) -> EditorAssetTableHandle<'_>; +} + +impl EditorAssetTableAccess for super::RemoteTables { + fn editor_asset(&self) -> EditorAssetTableHandle<'_> { + EditorAssetTableHandle { + imp: self.imp.get_table::("editor_asset"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct EditorAssetInsertCallbackId(__sdk::CallbackId); +pub struct EditorAssetDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for EditorAssetTableHandle<'ctx> { + type Row = EditorAsset; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = EditorAssetInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorAssetInsertCallbackId { + EditorAssetInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: EditorAssetInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = EditorAssetDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorAssetDeleteCallbackId { + EditorAssetDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: EditorAssetDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct EditorAssetUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for EditorAssetTableHandle<'ctx> { + type UpdateCallbackId = EditorAssetUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> EditorAssetUpdateCallbackId { + EditorAssetUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: EditorAssetUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `asset_id` unique index on the table `editor_asset`, +/// which allows point queries on the field of the same name +/// via the [`EditorAssetAssetIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_asset().asset_id().find(...)`. +pub struct EditorAssetAssetIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> EditorAssetTableHandle<'ctx> { + /// Get a handle on the `asset_id` unique index on the table `editor_asset`. + pub fn asset_id(&self) -> EditorAssetAssetIdUnique<'ctx> { + EditorAssetAssetIdUnique { + imp: self.imp.get_unique_constraint::("asset_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> EditorAssetAssetIdUnique<'ctx> { + /// Find the subscribed row whose `asset_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("editor_asset"); + _table.add_unique_constraint::("asset_id", |row| &row.asset_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `EditorAsset`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait editor_assetQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `EditorAsset`. + fn editor_asset(&self) -> __sdk::__query_builder::Table; +} + +impl editor_assetQueryTableAccess for __sdk::QueryTableAccessor { + fn editor_asset(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("editor_asset") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs new file mode 100644 index 00000000..f824530e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs @@ -0,0 +1,101 @@ +// 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 EditorAsset { + pub asset_id: String, + pub owner_user_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for EditorAsset { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `EditorAsset`. +/// +/// Provides typed access to columns for query building. +pub struct EditorAssetCols { + pub asset_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub folder_id: __sdk::__query_builder::Col, + pub label: __sdk::__query_builder::Col, + pub asset_object_id: __sdk::__query_builder::Col>, + pub image_src: __sdk::__query_builder::Col, + pub object_key: __sdk::__query_builder::Col>, + pub width: __sdk::__query_builder::Col, + pub height: __sdk::__query_builder::Col, + pub source_type: __sdk::__query_builder::Col, + pub prompt: __sdk::__query_builder::Col>, + pub actual_prompt: __sdk::__query_builder::Col>, + pub model: __sdk::__query_builder::Col>, + pub provider: __sdk::__query_builder::Col>, + pub task_id: __sdk::__query_builder::Col>, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for EditorAsset { + type Cols = EditorAssetCols; + fn cols(table_name: &'static str) -> Self::Cols { + EditorAssetCols { + asset_id: __sdk::__query_builder::Col::new(table_name, "asset_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + folder_id: __sdk::__query_builder::Col::new(table_name, "folder_id"), + label: __sdk::__query_builder::Col::new(table_name, "label"), + asset_object_id: __sdk::__query_builder::Col::new(table_name, "asset_object_id"), + image_src: __sdk::__query_builder::Col::new(table_name, "image_src"), + object_key: __sdk::__query_builder::Col::new(table_name, "object_key"), + width: __sdk::__query_builder::Col::new(table_name, "width"), + height: __sdk::__query_builder::Col::new(table_name, "height"), + source_type: __sdk::__query_builder::Col::new(table_name, "source_type"), + prompt: __sdk::__query_builder::Col::new(table_name, "prompt"), + actual_prompt: __sdk::__query_builder::Col::new(table_name, "actual_prompt"), + model: __sdk::__query_builder::Col::new(table_name, "model"), + provider: __sdk::__query_builder::Col::new(table_name, "provider"), + task_id: __sdk::__query_builder::Col::new(table_name, "task_id"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `EditorAsset`. +/// +/// Provides typed access to indexed columns for query building. +pub struct EditorAssetIxCols { + pub asset_id: __sdk::__query_builder::IxCol, + pub folder_id: __sdk::__query_builder::IxCol, + pub owner_user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for EditorAsset { + type IxCols = EditorAssetIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + EditorAssetIxCols { + asset_id: __sdk::__query_builder::IxCol::new(table_name, "asset_id"), + folder_id: __sdk::__query_builder::IxCol::new(table_name, "folder_id"), + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for EditorAsset {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_update_input_type.rs new file mode 100644 index 00000000..950222db --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_update_input_type.rs @@ -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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorAssetUpdateInput { + pub asset_id: String, + pub owner_user_id: String, + pub label: Option, + pub folder_id: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorAssetUpdateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_editor_asset_library_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_editor_asset_library_and_return_procedure.rs new file mode 100644 index 00000000..dca803bb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_editor_asset_library_and_return_procedure.rs @@ -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_library_get_input_type::EditorAssetLibraryGetInput; +use super::editor_asset_library_procedure_result_type::EditorAssetLibraryProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetEditorAssetLibraryAndReturnArgs { + pub input: EditorAssetLibraryGetInput, +} + +impl __sdk::InModule for GetEditorAssetLibraryAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_editor_asset_library_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_editor_asset_library_and_return { + fn get_editor_asset_library_and_return(&self, input: EditorAssetLibraryGetInput) { + self.get_editor_asset_library_and_return_then(input, |_, _| {}); + } + + fn get_editor_asset_library_and_return_then( + &self, + input: EditorAssetLibraryGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_editor_asset_library_and_return for super::RemoteProcedures { + fn get_editor_asset_library_and_return_then( + &self, + input: EditorAssetLibraryGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetLibraryProcedureResult>( + "get_editor_asset_library_and_return", + GetEditorAssetLibraryAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_editor_asset_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_editor_asset_and_return_procedure.rs new file mode 100644 index 00000000..1ec66291 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_editor_asset_and_return_procedure.rs @@ -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_procedure_result_type::EditorAssetProcedureResult; +use super::editor_asset_update_input_type::EditorAssetUpdateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdateEditorAssetAndReturnArgs { + pub input: EditorAssetUpdateInput, +} + +impl __sdk::InModule for UpdateEditorAssetAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_editor_asset_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_editor_asset_and_return { + fn update_editor_asset_and_return(&self, input: EditorAssetUpdateInput) { + self.update_editor_asset_and_return_then(input, |_, _| {}); + } + + fn update_editor_asset_and_return_then( + &self, + input: EditorAssetUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_editor_asset_and_return for super::RemoteProcedures { + fn update_editor_asset_and_return_then( + &self, + input: EditorAssetUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetProcedureResult>( + "update_editor_asset_and_return", + UpdateEditorAssetAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_editor_asset_folder_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_editor_asset_folder_and_return_procedure.rs new file mode 100644 index 00000000..a5ec7143 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_editor_asset_folder_and_return_procedure.rs @@ -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_folder_procedure_result_type::EditorAssetFolderProcedureResult; +use super::editor_asset_folder_update_input_type::EditorAssetFolderUpdateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdateEditorAssetFolderAndReturnArgs { + pub input: EditorAssetFolderUpdateInput, +} + +impl __sdk::InModule for UpdateEditorAssetFolderAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_editor_asset_folder_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_editor_asset_folder_and_return { + fn update_editor_asset_folder_and_return(&self, input: EditorAssetFolderUpdateInput) { + self.update_editor_asset_folder_and_return_then(input, |_, _| {}); + } + + fn update_editor_asset_folder_and_return_then( + &self, + input: EditorAssetFolderUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_editor_asset_folder_and_return for super::RemoteProcedures { + fn update_editor_asset_folder_and_return_then( + &self, + input: EditorAssetFolderUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorAssetFolderProcedureResult>( + "update_editor_asset_folder_and_return", + UpdateEditorAssetFolderAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index 0cf4d4db..fe0b9578 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -2,7 +2,10 @@ use crate::*; const EDITOR_PROJECT_DEFAULT_TITLE: &str = "未命名画布"; const EDITOR_CANVAS_DEFAULT_TITLE: &str = "默认画布"; +const EDITOR_ASSET_DEFAULT_FOLDER_ID: &str = "project"; +const EDITOR_ASSET_DEFAULT_FOLDER_LABEL: &str = "项目素材"; const EDITOR_PROJECT_MAX_TITLE_CHARS: usize = 80; +const EDITOR_ASSET_MAX_LABEL_CHARS: usize = 80; const EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES: usize = 256 * 1024; const EDITOR_PROJECT_SOURCE_TYPES: [&str; 3] = ["uploaded", "generated", "mock_generated"]; @@ -68,6 +71,48 @@ pub struct EditorProjectResource { updated_at: Timestamp, } +#[spacetimedb::table( + accessor = editor_asset_folder, + index(accessor = by_editor_asset_folder_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct EditorAssetFolder { + #[primary_key] + folder_id: String, + owner_user_id: String, + label: String, + sort_order: u32, + collapsed: bool, + system_default: bool, + created_at: Timestamp, + updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = editor_asset, + index(accessor = by_editor_asset_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_editor_asset_folder_id, btree(columns = [folder_id])) +)] +pub struct EditorAsset { + #[primary_key] + asset_id: String, + owner_user_id: String, + folder_id: String, + label: String, + asset_object_id: Option, + image_src: String, + object_key: Option, + width: u32, + height: u32, + source_type: String, + prompt: Option, + actual_prompt: Option, + model: Option, + provider: Option, + task_id: Option, + created_at: Timestamp, + updated_at: Timestamp, +} + #[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct EditorProjectViewportSnapshot { pub x: f64, @@ -162,6 +207,109 @@ pub struct EditorProjectResourceSnapshot { pub updated_at_micros: i64, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetFolderSnapshot { + pub folder_id: String, + pub label: String, + pub sort_order: u32, + pub collapsed: bool, + pub system_default: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetSnapshot { + pub asset_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetLibrarySnapshot { + pub folders: Vec, + pub assets: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetLibraryGetInput { + pub owner_user_id: String, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetFolderCreateInput { + pub folder_id: String, + pub owner_user_id: String, + pub label: String, + pub sort_order: u32, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetFolderUpdateInput { + pub folder_id: String, + pub owner_user_id: String, + pub label: Option, + pub collapsed: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetFolderDeleteInput { + pub folder_id: String, + pub owner_user_id: String, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetCreateInput { + pub asset_id: String, + pub owner_user_id: String, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetUpdateInput { + pub asset_id: String, + pub owner_user_id: String, + pub label: Option, + pub folder_id: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetDeleteInput { + pub asset_id: String, + pub owner_user_id: String, +} + #[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct EditorCanvasSnapshot { pub canvas_id: String, @@ -212,6 +360,28 @@ pub struct EditorProjectResourceProcedureResult { pub error_message: Option, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetLibraryProcedureResult { + pub ok: bool, + pub library: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetFolderProcedureResult { + pub ok: bool, + pub folder: Option, + pub library: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorAssetProcedureResult { + pub ok: bool, + pub asset: Option, + pub error_message: Option, +} + #[spacetimedb::procedure] pub fn create_editor_project_and_return( ctx: &mut ProcedureContext, @@ -324,6 +494,83 @@ pub fn create_editor_project_resource_and_return( } } +#[spacetimedb::procedure] +pub fn get_editor_asset_library_and_return( + ctx: &mut ProcedureContext, + input: EditorAssetLibraryGetInput, +) -> EditorAssetLibraryProcedureResult { + match ctx.try_with_tx(|tx| get_editor_asset_library(tx, input.clone())) { + Ok(library) => editor_asset_library_ok(library), + Err(message) => editor_asset_library_error(message), + } +} + +#[spacetimedb::procedure] +pub fn create_editor_asset_folder_and_return( + ctx: &mut ProcedureContext, + input: EditorAssetFolderCreateInput, +) -> EditorAssetFolderProcedureResult { + match ctx.try_with_tx(|tx| create_editor_asset_folder(tx, input.clone())) { + Ok(folder) => editor_asset_folder_ok(Some(folder), None), + Err(message) => editor_asset_folder_error(message), + } +} + +#[spacetimedb::procedure] +pub fn update_editor_asset_folder_and_return( + ctx: &mut ProcedureContext, + input: EditorAssetFolderUpdateInput, +) -> EditorAssetFolderProcedureResult { + match ctx.try_with_tx(|tx| update_editor_asset_folder(tx, input.clone())) { + Ok(folder) => editor_asset_folder_ok(Some(folder), None), + Err(message) => editor_asset_folder_error(message), + } +} + +#[spacetimedb::procedure] +pub fn delete_editor_asset_folder_and_return( + ctx: &mut ProcedureContext, + input: EditorAssetFolderDeleteInput, +) -> EditorAssetFolderProcedureResult { + match ctx.try_with_tx(|tx| delete_editor_asset_folder(tx, input.clone())) { + Ok(library) => editor_asset_folder_ok(None, Some(library)), + Err(message) => editor_asset_folder_error(message), + } +} + +#[spacetimedb::procedure] +pub fn create_editor_asset_and_return( + ctx: &mut ProcedureContext, + input: EditorAssetCreateInput, +) -> EditorAssetProcedureResult { + match ctx.try_with_tx(|tx| create_editor_asset(tx, input.clone())) { + Ok(asset) => editor_asset_ok(Some(asset)), + Err(message) => editor_asset_error(message), + } +} + +#[spacetimedb::procedure] +pub fn update_editor_asset_and_return( + ctx: &mut ProcedureContext, + input: EditorAssetUpdateInput, +) -> EditorAssetProcedureResult { + match ctx.try_with_tx(|tx| update_editor_asset(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, + input: EditorAssetDeleteInput, +) -> EditorAssetProcedureResult { + match ctx.try_with_tx(|tx| delete_editor_asset(tx, input.clone())) { + Ok(asset) => editor_asset_ok(Some(asset)), + Err(message) => editor_asset_error(message), + } +} + fn create_editor_project( ctx: &ReducerContext, input: EditorProjectCreateInput, @@ -601,6 +848,225 @@ fn create_editor_project_resource( .ok_or_else(|| "画布资源创建失败".to_string()) } +fn get_editor_asset_library( + ctx: &ReducerContext, + input: EditorAssetLibraryGetInput, +) -> Result { + let owner_user_id = normalize_required(&input.owner_user_id, "editor_asset.owner_user_id")?; + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + ensure_default_asset_folder(ctx, owner_user_id.as_str(), now)?; + build_asset_library_snapshot(ctx, owner_user_id.as_str()) +} + +fn create_editor_asset_folder( + ctx: &ReducerContext, + input: EditorAssetFolderCreateInput, +) -> Result { + let folder_id = normalize_required(&input.folder_id, "editor_asset_folder.folder_id")?; + let owner_user_id = normalize_required( + &input.owner_user_id, + "editor_asset_folder.owner_user_id", + )?; + if ctx + .db + .editor_asset_folder() + .folder_id() + .find(&folder_id) + .is_some() + { + return Err("素材文件夹已存在".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + ensure_default_asset_folder(ctx, owner_user_id.as_str(), now)?; + ctx.db.editor_asset_folder().insert(EditorAssetFolder { + folder_id: folder_id.clone(), + owner_user_id, + label: normalize_asset_label(&input.label), + sort_order: input.sort_order, + collapsed: false, + system_default: false, + created_at: now, + updated_at: now, + }); + ctx.db + .editor_asset_folder() + .folder_id() + .find(&folder_id) + .map(asset_folder_snapshot_from_row) + .ok_or_else(|| "素材文件夹创建失败".to_string()) +} + +fn update_editor_asset_folder( + ctx: &ReducerContext, + input: EditorAssetFolderUpdateInput, +) -> Result { + let folder_id = normalize_required(&input.folder_id, "editor_asset_folder.folder_id")?; + let owner_user_id = normalize_required( + &input.owner_user_id, + "editor_asset_folder.owner_user_id", + )?; + let folder = require_owned_asset_folder(ctx, folder_id.as_str(), owner_user_id.as_str())?; + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + ctx.db.editor_asset_folder().folder_id().delete(&folder_id); + ctx.db.editor_asset_folder().insert(EditorAssetFolder { + folder_id: folder.folder_id.clone(), + owner_user_id: folder.owner_user_id, + label: input + .label + .map(|label| normalize_asset_label(label.as_str())) + .unwrap_or(folder.label), + sort_order: folder.sort_order, + collapsed: input.collapsed.unwrap_or(folder.collapsed), + system_default: folder.system_default, + created_at: folder.created_at, + updated_at: now, + }); + ctx.db + .editor_asset_folder() + .folder_id() + .find(&folder_id) + .map(asset_folder_snapshot_from_row) + .ok_or_else(|| "素材文件夹更新失败".to_string()) +} + +fn delete_editor_asset_folder( + ctx: &ReducerContext, + input: EditorAssetFolderDeleteInput, +) -> Result { + let folder_id = normalize_required(&input.folder_id, "editor_asset_folder.folder_id")?; + let owner_user_id = normalize_required( + &input.owner_user_id, + "editor_asset_folder.owner_user_id", + )?; + let folder = require_owned_asset_folder(ctx, folder_id.as_str(), owner_user_id.as_str())?; + if folder.system_default { + return Err("默认素材文件夹不能删除".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + let default_folder = ensure_default_asset_folder(ctx, owner_user_id.as_str(), now)?; + let moved_assets = ctx + .db + .editor_asset() + .by_editor_asset_folder_id() + .filter(&folder_id) + .filter(|asset| asset.owner_user_id == owner_user_id) + .collect::>(); + for asset in moved_assets { + ctx.db.editor_asset().asset_id().delete(&asset.asset_id); + ctx.db.editor_asset().insert(EditorAsset { + folder_id: default_folder.folder_id.clone(), + updated_at: now, + ..asset + }); + } + ctx.db.editor_asset_folder().folder_id().delete(&folder_id); + build_asset_library_snapshot(ctx, owner_user_id.as_str()) +} + +fn create_editor_asset( + ctx: &ReducerContext, + input: EditorAssetCreateInput, +) -> Result { + 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 folder_id = normalize_required(&input.folder_id, "editor_asset.folder_id")?; + let image_src = normalize_required(&input.image_src, "editor_asset.image_src")?; + let source_type = normalize_required(&input.source_type, "editor_asset.source_type")?; + if !EDITOR_PROJECT_SOURCE_TYPES.contains(&source_type.as_str()) { + return Err("素材来源类型只支持 uploaded、generated 或 mock_generated".to_string()); + } + if input.width == 0 || input.height == 0 { + return Err("素材尺寸必须大于 0".to_string()); + } + if ctx.db.editor_asset().asset_id().find(&asset_id).is_some() { + return Err("素材已存在".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + ensure_default_asset_folder(ctx, owner_user_id.as_str(), now)?; + require_owned_asset_folder(ctx, folder_id.as_str(), owner_user_id.as_str())?; + ctx.db.editor_asset().insert(EditorAsset { + asset_id: asset_id.clone(), + owner_user_id, + folder_id, + label: normalize_asset_label(&input.label), + asset_object_id: normalize_optional(input.asset_object_id), + image_src, + object_key: normalize_optional(input.object_key), + width: input.width, + height: input.height, + source_type, + prompt: normalize_optional(input.prompt), + actual_prompt: normalize_optional(input.actual_prompt), + model: normalize_optional(input.model), + provider: normalize_optional(input.provider), + task_id: normalize_optional(input.task_id), + created_at: now, + updated_at: now, + }); + ctx.db + .editor_asset() + .asset_id() + .find(&asset_id) + .map(asset_snapshot_from_row) + .ok_or_else(|| "素材创建失败".to_string()) +} + +fn update_editor_asset( + ctx: &ReducerContext, + input: EditorAssetUpdateInput, +) -> Result { + 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 folder_id = input + .folder_id + .map(|value| normalize_required(&value, "editor_asset.folder_id")) + .transpose()? + .unwrap_or_else(|| asset.folder_id.clone()); + require_owned_asset_folder(ctx, folder_id.as_str(), owner_user_id.as_str())?; + 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, + label: input + .label + .map(|label| normalize_asset_label(label.as_str())) + .unwrap_or(asset.label), + asset_object_id: asset.asset_object_id, + image_src: asset.image_src, + object_key: asset.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, + }); + 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, +) -> Result { + 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())?; + ctx.db.editor_asset().asset_id().delete(&asset_id); + Ok(asset_snapshot_from_row(asset)) +} + fn build_project_snapshot( ctx: &ReducerContext, project_id: &str, @@ -725,6 +1191,138 @@ fn require_owned_project( Ok(project) } +fn ensure_default_asset_folder( + ctx: &ReducerContext, + owner_user_id: &str, + now: Timestamp, +) -> Result { + let folder_id = default_asset_folder_id(owner_user_id); + if let Some(folder) = ctx.db.editor_asset_folder().folder_id().find(&folder_id) { + return Ok(folder); + } + ctx.db.editor_asset_folder().insert(EditorAssetFolder { + folder_id: folder_id.clone(), + owner_user_id: owner_user_id.to_string(), + label: EDITOR_ASSET_DEFAULT_FOLDER_LABEL.to_string(), + sort_order: 0, + collapsed: false, + system_default: true, + created_at: now, + updated_at: now, + }); + ctx.db + .editor_asset_folder() + .folder_id() + .find(&folder_id) + .ok_or_else(|| "默认素材文件夹创建失败".to_string()) +} + +fn default_asset_folder_id(owner_user_id: &str) -> String { + format!("{owner_user_id}:asset-folder:{EDITOR_ASSET_DEFAULT_FOLDER_ID}") +} + +fn require_owned_asset_folder( + ctx: &ReducerContext, + folder_id: &str, + owner_user_id: &str, +) -> Result { + let folder_key = folder_id.to_string(); + let folder = ctx + .db + .editor_asset_folder() + .folder_id() + .find(&folder_key) + .ok_or_else(|| "素材文件夹不存在".to_string())?; + if folder.owner_user_id != owner_user_id { + return Err("无权访问该素材文件夹".to_string()); + } + Ok(folder) +} + +fn require_owned_asset( + ctx: &ReducerContext, + asset_id: &str, + owner_user_id: &str, +) -> Result { + let asset_key = asset_id.to_string(); + let asset = ctx + .db + .editor_asset() + .asset_id() + .find(&asset_key) + .ok_or_else(|| "素材不存在".to_string())?; + if asset.owner_user_id != owner_user_id { + return Err("无权访问该素材".to_string()); + } + Ok(asset) +} + +fn build_asset_library_snapshot( + ctx: &ReducerContext, + owner_user_id: &str, +) -> Result { + let owner_key = owner_user_id.to_string(); + let mut folders = ctx + .db + .editor_asset_folder() + .by_editor_asset_folder_owner_user_id() + .filter(&owner_key) + .map(asset_folder_snapshot_from_row) + .collect::>(); + folders.sort_by(|left, right| { + left.sort_order + .cmp(&right.sort_order) + .then_with(|| left.created_at_micros.cmp(&right.created_at_micros)) + .then_with(|| left.folder_id.cmp(&right.folder_id)) + }); + let mut assets = ctx + .db + .editor_asset() + .by_editor_asset_owner_user_id() + .filter(&owner_key) + .map(asset_snapshot_from_row) + .collect::>(); + assets.sort_by(|left, right| { + left.created_at_micros + .cmp(&right.created_at_micros) + .then_with(|| left.asset_id.cmp(&right.asset_id)) + }); + Ok(EditorAssetLibrarySnapshot { folders, assets }) +} + +fn asset_folder_snapshot_from_row(row: EditorAssetFolder) -> EditorAssetFolderSnapshot { + EditorAssetFolderSnapshot { + folder_id: row.folder_id, + label: row.label, + sort_order: row.sort_order, + collapsed: row.collapsed, + system_default: row.system_default, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn asset_snapshot_from_row(row: EditorAsset) -> EditorAssetSnapshot { + EditorAssetSnapshot { + asset_id: row.asset_id, + folder_id: row.folder_id, + label: row.label, + asset_object_id: row.asset_object_id, + image_src: row.image_src, + object_key: row.object_key, + width: row.width, + height: row.height, + source_type: row.source_type, + prompt: row.prompt, + actual_prompt: row.actual_prompt, + model: row.model, + provider: row.provider, + task_id: row.task_id, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + fn resource_snapshot_from_row(row: EditorProjectResource) -> EditorProjectResourceSnapshot { EditorProjectResourceSnapshot { resource_id: row.resource_id, @@ -768,6 +1366,14 @@ fn normalize_title(value: &str) -> String { title.chars().take(EDITOR_PROJECT_MAX_TITLE_CHARS).collect() } +fn normalize_asset_label(value: &str) -> String { + let label = value.trim(); + if label.is_empty() { + return "未命名素材".to_string(); + } + label.chars().take(EDITOR_ASSET_MAX_LABEL_CHARS).collect() +} + fn normalize_layout_json(value: String) -> Result { if value.len() > EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES { return Err("图片画布图层布局过大".to_string()); @@ -792,3 +1398,58 @@ fn editor_project_error(message: String) -> EditorProjectProcedureResult { error_message: Some(message), } } + +fn editor_asset_library_ok( + library: EditorAssetLibrarySnapshot, +) -> EditorAssetLibraryProcedureResult { + EditorAssetLibraryProcedureResult { + ok: true, + library: Some(library), + error_message: None, + } +} + +fn editor_asset_library_error(message: String) -> EditorAssetLibraryProcedureResult { + EditorAssetLibraryProcedureResult { + ok: false, + library: None, + error_message: Some(message), + } +} + +fn editor_asset_folder_ok( + folder: Option, + library: Option, +) -> EditorAssetFolderProcedureResult { + EditorAssetFolderProcedureResult { + ok: true, + folder, + library, + error_message: None, + } +} + +fn editor_asset_folder_error(message: String) -> EditorAssetFolderProcedureResult { + EditorAssetFolderProcedureResult { + ok: false, + folder: None, + library: None, + error_message: Some(message), + } +} + +fn editor_asset_ok(asset: Option) -> EditorAssetProcedureResult { + EditorAssetProcedureResult { + ok: true, + asset, + error_message: None, + } +} + +fn editor_asset_error(message: String) -> EditorAssetProcedureResult { + EditorAssetProcedureResult { + ok: false, + asset: None, + error_message: Some(message), + } +} diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index bf0a3fbc..4f192702 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -232,6 +232,8 @@ macro_rules! migration_tables { editor_project, editor_canvas, editor_project_resource, + editor_asset_folder, + editor_asset, puzzle_agent_session, puzzle_background_compile_task, puzzle_agent_message, diff --git a/src/components/image-editor/ImageCanvasEditorPrimitives.tsx b/src/components/image-editor/ImageCanvasEditorPrimitives.tsx index c84fd5a8..1ec3feb9 100644 --- a/src/components/image-editor/ImageCanvasEditorPrimitives.tsx +++ b/src/components/image-editor/ImageCanvasEditorPrimitives.tsx @@ -1,4 +1,9 @@ -import type { ComponentType, ReactNode } from 'react'; +import type { + ComponentType, + DragEventHandler, + PointerEventHandler, + ReactNode, +} from 'react'; type IconComponent = ComponentType<{ className?: string }>; @@ -55,6 +60,9 @@ export type SidebarMediaItemProps = { primaryClassName?: string; actions?: ReactNode; titleNode?: ReactNode; + onDragOver?: DragEventHandler; + onDrop?: DragEventHandler; + onPointerEnter?: PointerEventHandler; }; export function SidebarMediaItem({ @@ -71,10 +79,16 @@ export function SidebarMediaItem({ primaryClassName, actions, titleNode, + onDragOver, + onDrop, + onPointerEnter, }: SidebarMediaItemProps) { return (
{activeSidebarPanel === 'assets' ? ( +
+ + setIsAssetSelectionMode((currentMode) => !currentMode) + } + /> + setCreatingFolder(true)} + /> +
+ ) : ( setCreatingFolder(true)} + disabled={!selectedLayerId && selectedLayerIds.length === 0} + onClick={groupSelectedLayers} /> - ) : null} + )} {activeSidebarPanel === 'assets' ? ( -
+
{creatingFolder ? (
{ event.preventDefault(); - commitNewAssetFolder(); + void commitNewAssetFolder(); }} > { + if (event.dataTransfer.types.includes('Files')) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + } + }} + onDrop={(event) => { + if (!event.dataTransfer.files.length) { + return; + } + event.preventDefault(); + addUploadedFiles(event.dataTransfer.files, { folderId: folder.id }); + }} >
toggleAssetFolder(folder.id)} /> - {folder.label} + {renamingFolder?.folderId === folder.id ? ( + + setRenamingFolder({ + folderId: folder.id, + value: event.target.value, + }) + } + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + commitFolderRename(folder); + } + if (event.key === 'Escape') { + event.preventDefault(); + setRenamingFolder(null); + } + }} + /> + ) : ( + {folder.label} + )} {folder.assets.length} + {renamingFolder?.folderId === folder.id ? ( + <> + commitFolderRename(folder)} + /> + setRenamingFolder(null)} + /> + + ) : ( + startRenamingFolder(folder)} + /> + )} + {!folder.systemDefault ? ( + deleteAssetFolder(folder)} + /> + ) : null} ); return ( - addAssetLayer(asset)} - rowClassName="image-canvas-editor__asset-row" - primaryClassName="image-canvas-editor__asset-button" - thumbnailClassName="image-canvas-editor__asset-thumb" - metaClassName="image-canvas-editor__asset-meta" - titleNode={titleNode} - actions={actions} - /> +
+ { + if (isAssetSelectionMode) { + toggleAssetSelected(asset.id); + return; + } + addAssetLayer(asset); + }} + selected={selectedAssetIds.has(asset.id)} + rowClassName="image-canvas-editor__asset-row" + primaryClassName="image-canvas-editor__asset-button" + thumbnailClassName="image-canvas-editor__asset-thumb" + metaClassName="image-canvas-editor__asset-meta" + titleNode={titleNode} + actions={actions} + onPointerEnter={(event) => { + if (isAssetSelectionMode && event.buttons === 1) { + setSelectedAssetIds((currentIds) => { + const nextIds = new Set(currentIds); + nextIds.add(asset.id); + return nextIds; + }); + } + }} + onDragOver={(event) => { + if (event.dataTransfer.types.includes('Files')) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + } + }} + onDrop={(event) => { + if (!event.dataTransfer.files.length) { + return; + } + event.preventDefault(); + addUploadedFiles(event.dataTransfer.files, { + folderId: asset.folderId, + }); + }} + /> +
); })}
))} + {isAssetSelectionMode ? ( + + + {allSelectableAssetsSelected ? ( + + ) : ( + + )} + {selectedAssetIds.size > 0 + ? `${allSelectableAssetsSelected ? '取消全选' : '全选'} · 已选 ${selectedAssetIds.size}` + : '全选'} + + + + 删除 + + + 取消 + + + ) : null} + {assetMarquee ? ( + ) : (
@@ -1643,12 +2404,12 @@ export function ImageCanvasEditorView() { setSelectedLayerId(layer.id)} + onPrimaryClick={() => selectSingleLayer(layer.id)} rowClassName="image-canvas-editor__layer-row" primaryClassName="image-canvas-editor__layer-row-button" thumbnailClassName="image-canvas-editor__layer-row-thumb" @@ -1733,6 +2494,8 @@ export function ImageCanvasEditorView() { onPointerUp={finishDrag} onPointerCancel={finishDrag} onWheel={handleWheel} + onDragOver={handleCanvasDragOver} + onDrop={handleCanvasDrop} >
{ event.stopPropagation(); setMetadataLayer(layer); - setSelectedLayerId(layer.id); + selectSingleLayer(layer.id); }} onPointerDown={(event) => event.stopPropagation()} onKeyDown={(event) => { @@ -1802,7 +2565,7 @@ export function ImageCanvasEditorView() { event.preventDefault(); event.stopPropagation(); setMetadataLayer(layer); - setSelectedLayerId(layer.id); + selectSingleLayer(layer.id); } }} > @@ -1967,9 +2730,8 @@ export function ImageCanvasEditorView() { type="button" className="image-canvas-editor__minimap" aria-label="画布小地图" - title="显示画布所有元素" - onPointerDown={(event) => event.stopPropagation()} - onClick={() => fitLayers()} + title="拖拽移动视图" + onPointerDown={handleMinimapPointerDown} > {minimapModel.layers.map((layer) => ( diff --git a/src/index.css b/src/index.css index 568bca0e..fe7c1f45 100644 --- a/src/index.css +++ b/src/index.css @@ -3363,7 +3363,13 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { border-radius: 0.45rem; } +.image-canvas-editor__sidebar-header-actions { + display: inline-flex; + gap: 0.35rem; +} + .image-canvas-editor__asset-list { + position: relative; display: grid; min-height: 0; align-content: start; @@ -3378,15 +3384,33 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { } .image-canvas-editor__asset-folder-header { - display: grid; - grid-template-columns: auto auto minmax(0, 1fr) auto auto; + display: flex; align-items: center; + min-width: 0; gap: 0.35rem; color: #475569; font-size: 0.76rem; font-weight: 850; } +.image-canvas-editor__asset-folder-header > span:first-of-type, +.image-canvas-editor__asset-folder-header > input { + min-width: 0; + flex: 1; +} + +.image-canvas-editor__asset-folder-header input { + height: 1.8rem; + border: 1px solid #8fb8ff; + border-radius: 0.35rem; + background: #ffffff; + padding: 0 0.45rem; + color: #1f2937; + font: inherit; + font-size: 0.76rem; + outline: none; +} + .image-canvas-editor__asset-folder-header button { display: inline-flex; width: 1.8rem; @@ -3445,6 +3469,26 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { background: #eef5ff; } +.image-canvas-editor__asset-row--selected { + border-color: #2563eb; + background: #dbeafe; +} + +.image-canvas-editor__asset-batch-toolbar { + position: sticky; + bottom: 0; + justify-content: center; + margin-top: 0.35rem; +} + +.image-canvas-editor__asset-marquee { + position: absolute; + z-index: 4; + border: 1px solid #2563eb; + background: rgb(37 99 235 / 0.12); + pointer-events: none; +} + .image-canvas-editor__asset-button { display: block; border: 0; diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index c385df1f..1e4ff65f 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -1,16 +1,23 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { + createEditorAsset, + createEditorAssetFolder, createEditorProject, createEditorProjectResource, + deleteEditorAsset, + deleteEditorAssetFolder, deleteEditorProject, editEditorImage, generateEditorImage, + loadEditorAssetLibrary, listEditorProjects, loadEditorProject, loadOrCreateRecentEditorProject, renameEditorProject, saveEditorProjectLayout, + updateEditorAsset, + updateEditorAssetFolder, } from './editorProjectClient'; const requestJsonMock = vi.hoisted(() => vi.fn()); @@ -308,6 +315,180 @@ describe('editorProjectClient', () => { ); }); + it('loads and mutates the account-level asset library', async () => { + requestJsonMock + .mockResolvedValueOnce({ + library: { + folders: [ + { + folderId: 'folder-project', + label: '项目素材', + sortOrder: 0, + collapsed: false, + systemDefault: true, + }, + ], + assets: [], + }, + }) + .mockResolvedValueOnce({ + folder: { + folderId: 'folder-role', + label: '角色', + sortOrder: 100, + collapsed: false, + systemDefault: false, + }, + }) + .mockResolvedValueOnce({ + folder: { + folderId: 'folder-role', + label: '角色参考', + sortOrder: 100, + collapsed: true, + systemDefault: false, + }, + }) + .mockResolvedValueOnce({ + library: { + folders: [], + assets: [], + }, + }); + + await loadEditorAssetLibrary(); + await createEditorAssetFolder('角色', 100); + await updateEditorAssetFolder('folder-role', { + label: '角色参考', + collapsed: true, + }); + await deleteEditorAssetFolder('folder-role'); + + expect(requestJsonMock).toHaveBeenNthCalledWith( + 1, + '/api/editor/assets/library', + { method: 'GET' }, + '读取图片画布素材库失败', + expect.any(Object), + ); + expect(requestJsonMock).toHaveBeenNthCalledWith( + 2, + '/api/editor/assets/folders', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ label: '角色', sortOrder: 100 }), + }), + '创建图片画布素材文件夹失败', + expect.any(Object), + ); + expect(requestJsonMock).toHaveBeenNthCalledWith( + 3, + '/api/editor/assets/folders/folder-role', + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ label: '角色参考', collapsed: true }), + }), + '更新图片画布素材文件夹失败', + expect.any(Object), + ); + expect(requestJsonMock).toHaveBeenNthCalledWith( + 4, + '/api/editor/assets/folders/folder-role', + { method: 'DELETE' }, + '删除图片画布素材文件夹失败', + expect.any(Object), + ); + }); + + it('creates, updates, and deletes account-level image assets', async () => { + requestJsonMock + .mockResolvedValueOnce({ + asset: { + assetId: 'asset-1', + folderId: 'folder-project', + label: '主视觉.png', + imageSrc: 'data:image/png;base64,ZmFrZQ==', + width: 640, + height: 480, + sourceType: 'uploaded', + }, + }) + .mockResolvedValueOnce({ + asset: { + assetId: 'asset-1', + folderId: 'folder-role', + label: '角色主视觉.png', + imageSrc: 'data:image/png;base64,ZmFrZQ==', + width: 640, + height: 480, + sourceType: 'uploaded', + }, + }) + .mockResolvedValueOnce({ + asset: { + assetId: 'asset-1', + folderId: 'folder-role', + label: '角色主视觉.png', + imageSrc: 'data:image/png;base64,ZmFrZQ==', + width: 640, + height: 480, + sourceType: 'uploaded', + }, + }); + + await createEditorAsset({ + folderId: 'folder-project', + label: '主视觉.png', + imageSrc: 'data:image/png;base64,ZmFrZQ==', + width: 640, + height: 480, + sourceType: 'uploaded', + }); + await updateEditorAsset('asset-1', { + label: '角色主视觉.png', + folderId: 'folder-role', + }); + await deleteEditorAsset('asset-1'); + + expect(requestJsonMock).toHaveBeenNthCalledWith( + 1, + '/api/editor/assets', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ + folderId: 'folder-project', + label: '主视觉.png', + imageSrc: 'data:image/png;base64,ZmFrZQ==', + width: 640, + height: 480, + sourceType: 'uploaded', + }), + }), + '创建图片画布素材失败', + expect.any(Object), + ); + expect(requestJsonMock).toHaveBeenNthCalledWith( + 2, + '/api/editor/assets/asset-1', + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ + label: '角色主视觉.png', + folderId: 'folder-role', + }), + }), + '更新图片画布素材失败', + expect.any(Object), + ); + expect(requestJsonMock).toHaveBeenNthCalledWith( + 3, + '/api/editor/assets/asset-1', + { method: 'DELETE' }, + '删除图片画布素材失败', + expect.any(Object), + ); + }); + it('creates an explicit project from title input', async () => { requestJsonMock.mockResolvedValueOnce({ project: { diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index 63ba674b..7e43bef1 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -1,6 +1,7 @@ import { requestJson } from '../apiClient'; const EDITOR_PROJECT_API_BASE = '/api/editor/projects'; +const EDITOR_ASSET_API_BASE = '/api/editor/assets'; const EDITOR_IMAGE_GENERATION_API = '/api/editor/images/generations'; const EDITOR_IMAGE_EDIT_API = '/api/editor/images/edits'; const DEFAULT_PROJECT_TITLE = '未命名画布'; @@ -44,6 +45,40 @@ export type EditorProjectResourceSnapshot = { updatedAt?: string; }; +export type EditorAssetFolderSnapshot = { + folderId: string; + label: string; + sortOrder: number; + collapsed: boolean; + systemDefault: boolean; + createdAt?: string; + updatedAt?: string; +}; + +export type EditorAssetSnapshot = { + assetId: string; + folderId: string; + label: string; + imageSrc: string; + objectKey?: string | null; + assetObjectId?: string | null; + width: number; + height: number; + sourceType: EditorProjectResourceSourceType; + prompt?: string | null; + actualPrompt?: string | null; + model?: string | null; + provider?: string | null; + taskId?: string | null; + createdAt?: string; + updatedAt?: string; +}; + +export type EditorAssetLibrarySnapshot = { + folders: EditorAssetFolderSnapshot[]; + assets: EditorAssetSnapshot[]; +}; + export type EditorImageGenerationInput = { prompt: string; }; @@ -109,6 +144,27 @@ export type EditorProjectResourceCreateInput = { sourceResourceId?: string | null; }; +export type EditorAssetCreateInput = { + folderId: string; + label: string; + imageSrc: string; + objectKey?: string | null; + assetObjectId?: string | null; + width: number; + height: number; + sourceType: EditorProjectResourceSourceType; + prompt?: string | null; + actualPrompt?: string | null; + model?: string | null; + provider?: string | null; + taskId?: string | null; +}; + +export type EditorAssetUpdateInput = { + label?: string; + folderId?: string; +}; + type EditorProjectResponse = { project: EditorProjectSnapshot; }; @@ -121,6 +177,22 @@ type EditorProjectResourceResponse = { resource: EditorProjectResourceSnapshot; }; +type EditorAssetLibraryResponse = { + library: EditorAssetLibrarySnapshot; +}; + +type EditorAssetFolderResponse = { + folder: EditorAssetFolderSnapshot; +}; + +type EditorAssetFolderDeleteResponse = { + library: EditorAssetLibrarySnapshot; +}; + +type EditorAssetResponse = { + asset: EditorAssetSnapshot; +}; + type EditorImageGenerationResponse = EditorImageGenerationResult; function jsonRequest(method: 'POST' | 'PATCH', body: Record) { @@ -227,6 +299,79 @@ export async function createEditorProjectResource( return response.resource; } +export async function loadEditorAssetLibrary() { + const response = await requestJson( + `${EDITOR_ASSET_API_BASE}/library`, + { method: 'GET' }, + '读取图片画布素材库失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.library; +} + +export async function createEditorAssetFolder(label: string, sortOrder?: number) { + const response = await requestJson( + `${EDITOR_ASSET_API_BASE}/folders`, + jsonRequest('POST', { label, sortOrder }), + '创建图片画布素材文件夹失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.folder; +} + +export async function updateEditorAssetFolder( + folderId: string, + input: { label?: string; collapsed?: boolean }, +) { + const response = await requestJson( + `${EDITOR_ASSET_API_BASE}/folders/${encodeURIComponent(folderId)}`, + jsonRequest('PATCH', input), + '更新图片画布素材文件夹失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.folder; +} + +export async function deleteEditorAssetFolder(folderId: string) { + const response = await requestJson( + `${EDITOR_ASSET_API_BASE}/folders/${encodeURIComponent(folderId)}`, + { method: 'DELETE' }, + '删除图片画布素材文件夹失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.library; +} + +export async function createEditorAsset(input: EditorAssetCreateInput) { + const response = await requestJson( + EDITOR_ASSET_API_BASE, + jsonRequest('POST', input), + '创建图片画布素材失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.asset; +} + +export async function updateEditorAsset(assetId: string, input: EditorAssetUpdateInput) { + const response = await requestJson( + `${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}`, + jsonRequest('PATCH', input), + '更新图片画布素材失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.asset; +} + +export async function deleteEditorAsset(assetId: string) { + const response = await requestJson( + `${EDITOR_ASSET_API_BASE}/${encodeURIComponent(assetId)}`, + { method: 'DELETE' }, + '删除图片画布素材失败', + EDITOR_PROJECT_REQUEST_OPTIONS, + ); + return response.asset; +} + export async function generateEditorImage(input: EditorImageGenerationInput) { return requestJson( EDITOR_IMAGE_GENERATION_API,