完善图片画布素材库持久化
新增账号级素材文件夹和素材表,并接入 SpacetimeDB procedure、spacetime-client facade 与 api-server BFF。 编辑器素材栏支持文件夹新建、折叠、重命名、删除、多文件上传、拖拽定向上传、框选和批量删除。 画布支持拖拽上传落点创建图层、图层打组、小地图拖拽、普通滚轮纵向滚动和 Ctrl 滚轮缩放。 更新图片画布技术方案、后端数据契约、TRACKING 和团队决策记录。
This commit is contained in:
@@ -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<String>,
|
||||
image_src: String,
|
||||
object_key: Option<String>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
source_type: String,
|
||||
prompt: Option<String>,
|
||||
actual_prompt: Option<String>,
|
||||
model: Option<String>,
|
||||
provider: Option<String>,
|
||||
task_id: Option<String>,
|
||||
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<String>,
|
||||
pub image_src: String,
|
||||
pub object_key: Option<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub source_type: String,
|
||||
pub prompt: Option<String>,
|
||||
pub actual_prompt: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub task_id: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorAssetLibrarySnapshot {
|
||||
pub folders: Vec<EditorAssetFolderSnapshot>,
|
||||
pub assets: Vec<EditorAssetSnapshot>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub collapsed: Option<bool>,
|
||||
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<String>,
|
||||
pub image_src: String,
|
||||
pub object_key: Option<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub source_type: String,
|
||||
pub prompt: Option<String>,
|
||||
pub actual_prompt: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
pub task_id: Option<String>,
|
||||
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<String>,
|
||||
pub folder_id: Option<String>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorAssetLibraryProcedureResult {
|
||||
pub ok: bool,
|
||||
pub library: Option<EditorAssetLibrarySnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorAssetFolderProcedureResult {
|
||||
pub ok: bool,
|
||||
pub folder: Option<EditorAssetFolderSnapshot>,
|
||||
pub library: Option<EditorAssetLibrarySnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct EditorAssetProcedureResult {
|
||||
pub ok: bool,
|
||||
pub asset: Option<EditorAssetSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[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<EditorAssetLibrarySnapshot, String> {
|
||||
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<EditorAssetFolderSnapshot, String> {
|
||||
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<EditorAssetFolderSnapshot, String> {
|
||||
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<EditorAssetLibrarySnapshot, String> {
|
||||
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::<Vec<_>>();
|
||||
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<EditorAssetSnapshot, String> {
|
||||
let asset_id = normalize_required(&input.asset_id, "editor_asset.asset_id")?;
|
||||
let owner_user_id = normalize_required(&input.owner_user_id, "editor_asset.owner_user_id")?;
|
||||
let 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<EditorAssetSnapshot, String> {
|
||||
let asset_id = normalize_required(&input.asset_id, "editor_asset.asset_id")?;
|
||||
let owner_user_id = normalize_required(&input.owner_user_id, "editor_asset.owner_user_id")?;
|
||||
let asset = require_owned_asset(ctx, asset_id.as_str(), owner_user_id.as_str())?;
|
||||
let 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<EditorAssetSnapshot, String> {
|
||||
let asset_id = normalize_required(&input.asset_id, "editor_asset.asset_id")?;
|
||||
let owner_user_id = normalize_required(&input.owner_user_id, "editor_asset.owner_user_id")?;
|
||||
let asset = require_owned_asset(ctx, asset_id.as_str(), owner_user_id.as_str())?;
|
||||
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<EditorAssetFolder, String> {
|
||||
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<EditorAssetFolder, String> {
|
||||
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<EditorAsset, String> {
|
||||
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<EditorAssetLibrarySnapshot, String> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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<String, String> {
|
||||
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<EditorAssetFolderSnapshot>,
|
||||
library: Option<EditorAssetLibrarySnapshot>,
|
||||
) -> 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<EditorAssetSnapshot>) -> EditorAssetProcedureResult {
|
||||
EditorAssetProcedureResult {
|
||||
ok: true,
|
||||
asset,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_asset_error(message: String) -> EditorAssetProcedureResult {
|
||||
EditorAssetProcedureResult {
|
||||
ok: false,
|
||||
asset: None,
|
||||
error_message: Some(message),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user