perf: cache public gallery views
This commit is contained in:
@@ -8,9 +8,42 @@ use crate::runtime::{
|
||||
};
|
||||
use crate::*;
|
||||
use module_big_fish::{EvaluateBigFishPublishReadinessCommand, evaluate_publish_readiness};
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
const INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT: u32 = 0;
|
||||
|
||||
/// 大鱼吃小鱼公开广场列表投影。
|
||||
///
|
||||
/// 公开列表从已发布 creation session 生成卡片字段;7 日播放数由
|
||||
/// `api-server` 订阅 `public_work_play_daily_stat` 后在本地聚合。
|
||||
#[spacetimedb::view(accessor = big_fish_gallery_view, public)]
|
||||
pub fn big_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec<BigFishWorkSummarySnapshot> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.by_big_fish_session_stage()
|
||||
.filter(BigFishCreationStage::Published)
|
||||
.filter_map(|row| match build_big_fish_gallery_view_row(ctx, &row) {
|
||||
Ok(snapshot) => Some(snapshot),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"大鱼吃小鱼公开广场 view 跳过损坏的作品投影 session_id={}: {}",
|
||||
row.session_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.source_session_id.cmp(&right.source_session_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_big_fish_session(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -988,6 +1021,16 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
now_micros: i64,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let mut summary = build_big_fish_work_summary_without_recent_count(ctx, row)?;
|
||||
summary.recent_play_count_7d =
|
||||
count_recent_public_work_plays(ctx, "big-fish", &row.session_id, now_micros);
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn build_big_fish_work_summary_without_recent_count(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let draft = row
|
||||
.draft_json
|
||||
@@ -1052,12 +1095,7 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: count_recent_public_work_plays(
|
||||
ctx,
|
||||
"big-fish",
|
||||
&row.session_id,
|
||||
now_micros,
|
||||
),
|
||||
recent_play_count_7d: 0,
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
|
||||
@@ -1065,6 +1103,113 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_big_fish_gallery_view_row(
|
||||
ctx: &AnonymousViewContext,
|
||||
row: &BigFishCreationSession,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let draft = row
|
||||
.draft_json
|
||||
.as_deref()
|
||||
.map(deserialize_draft)
|
||||
.transpose()
|
||||
.map_err(|error| format!("big_fish.draft_json 非法: {error}"))?;
|
||||
let asset_slots = list_big_fish_asset_slots_for_view(ctx, &row.session_id);
|
||||
let coverage = build_asset_coverage(draft.as_ref(), &asset_slots);
|
||||
let cover_image_src = asset_slots
|
||||
.iter()
|
||||
.find(|slot| slot.asset_kind == BigFishAssetKind::StageBackground)
|
||||
.and_then(|slot| slot.asset_url.clone())
|
||||
.or_else(|| {
|
||||
asset_slots
|
||||
.iter()
|
||||
.find(|slot| slot.asset_kind == BigFishAssetKind::LevelMainImage)
|
||||
.and_then(|slot| slot.asset_url.clone())
|
||||
});
|
||||
let title = draft
|
||||
.as_ref()
|
||||
.map(|value| value.title.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "未命名大鱼草稿".to_string());
|
||||
let subtitle = draft
|
||||
.as_ref()
|
||||
.map(|value| value.subtitle.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "等待整理玩法草稿".to_string());
|
||||
let summary = draft
|
||||
.as_ref()
|
||||
.map(|value| value.core_fun.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
row.last_assistant_reply
|
||||
.clone()
|
||||
.unwrap_or_else(|| "继续补齐锚点后即可生成玩法草稿。".to_string())
|
||||
});
|
||||
|
||||
Ok(BigFishWorkSummarySnapshot {
|
||||
work_id: format!("big-fish-work-{}", row.session_id),
|
||||
source_session_id: row.session_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
title,
|
||||
subtitle,
|
||||
summary,
|
||||
cover_image_src,
|
||||
status: if row.stage == BigFishCreationStage::Published {
|
||||
"published".to_string()
|
||||
} else {
|
||||
"draft".to_string()
|
||||
},
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
publish_ready: coverage.publish_ready,
|
||||
level_count: draft
|
||||
.as_ref()
|
||||
.map(|value| value.runtime_params.level_count)
|
||||
.unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT),
|
||||
level_main_image_ready_count: coverage.level_main_image_ready_count,
|
||||
level_motion_ready_count: coverage.level_motion_ready_count,
|
||||
background_ready: coverage.background_ready,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: 0,
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
fn list_big_fish_asset_slots_for_view(
|
||||
ctx: &AnonymousViewContext,
|
||||
session_id: &str,
|
||||
) -> Vec<BigFishAssetSlotSnapshot> {
|
||||
let mut slots = ctx
|
||||
.db
|
||||
.big_fish_asset_slot()
|
||||
.by_big_fish_asset_session_id()
|
||||
.filter(session_id)
|
||||
.map(|slot| BigFishAssetSlotSnapshot {
|
||||
slot_id: slot.slot_id,
|
||||
session_id: slot.session_id,
|
||||
asset_kind: slot.asset_kind,
|
||||
level: slot.level,
|
||||
motion_key: slot.motion_key,
|
||||
status: slot.status,
|
||||
asset_url: slot.asset_url,
|
||||
prompt_snapshot: slot.prompt_snapshot,
|
||||
updated_at_micros: slot.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
slots.sort_by_key(|slot| {
|
||||
(
|
||||
slot.level.unwrap_or(0),
|
||||
slot.asset_kind.as_str().to_string(),
|
||||
slot.motion_key.clone().unwrap_or_default(),
|
||||
slot.slot_id.clone(),
|
||||
)
|
||||
});
|
||||
slots
|
||||
}
|
||||
|
||||
fn build_public_big_fish_gallery_list_input() -> BigFishWorksListInput {
|
||||
BigFishWorksListInput {
|
||||
// 中文注释:published_only 分支不会按 owner 过滤;非空占位用于兼容旧部署模块的前置校验。
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::*;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = big_fish_creation_session,
|
||||
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_big_fish_session_stage, btree(columns = [stage]))
|
||||
)]
|
||||
pub struct BigFishCreationSession {
|
||||
#[primary_key]
|
||||
|
||||
@@ -19,6 +19,62 @@ use module_match3d::{
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
/// 抓大鹅公开广场列表投影。
|
||||
///
|
||||
/// `match3d_work_profile` 是玩法源表,HTTP gallery 只订阅这个轻量 view,
|
||||
/// 避免每个公开列表请求重新调用 procedure 扫描和组装全量列表。
|
||||
#[spacetimedb::view(accessor = match3d_gallery_view, public)]
|
||||
pub fn match3d_gallery_view(ctx: &AnonymousViewContext) -> Vec<Match3DGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.match3d_work_profile()
|
||||
.by_match3d_work_publication_status()
|
||||
.filter(MATCH3D_PUBLICATION_PUBLISHED)
|
||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"抓大鹅公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct Match3DGalleryViewRow {
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
pub cover_asset_id: String,
|
||||
pub reference_image_src: Option<String>,
|
||||
pub clear_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
pub generated_item_assets_json: Option<String>,
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_match3d_agent_session(
|
||||
@@ -1004,6 +1060,35 @@ fn build_work_snapshot(row: &Match3DWorkProfileRow) -> Result<Match3DWorkSnapsho
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gallery_view_row(row: &Match3DWorkProfileRow) -> Result<Match3DGalleryViewRow, String> {
|
||||
let config = parse_config(&row.config_json)?;
|
||||
Ok(Match3DGalleryViewRow {
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
game_name: row.game_name.clone(),
|
||||
theme_text: row.theme_text.clone(),
|
||||
summary_text: row.summary_text.clone(),
|
||||
tags: parse_tags(&row.tags_json)?,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
reference_image_src: config.reference_image_src,
|
||||
clear_count: row.clear_count,
|
||||
difficulty: row.difficulty,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: is_work_publish_ready(row),
|
||||
play_count: row.play_count,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
generated_item_assets_json: normalize_generated_item_assets_json(
|
||||
row.generated_item_assets_json.as_deref(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_initial_run_snapshot(
|
||||
run_id: &str,
|
||||
work: &Match3DWorkProfileRow,
|
||||
@@ -1908,8 +1993,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let row_json = to_json_string(&draft);
|
||||
let restored =
|
||||
parse_json::<Match3DDraftSnapshot>(&row_json, "match3d draft_json").unwrap();
|
||||
let restored = parse_json::<Match3DDraftSnapshot>(&row_json, "match3d draft_json").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
restored.generated_item_assets_json.as_deref(),
|
||||
|
||||
@@ -125,17 +125,19 @@ pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec<PuzzleWorkProfile>
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter_map(|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
|
||||
Ok(profile) => Some(profile),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"拼图广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(
|
||||
|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
|
||||
Ok(profile) => Some(profile),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"拼图广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
items
|
||||
|
||||
@@ -26,6 +26,65 @@ use module_square_hole::{
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
/// 方洞挑战公开广场列表投影。
|
||||
///
|
||||
/// HTTP gallery 通过 `spacetime-client` 订阅该 view 后读本地 cache,
|
||||
/// 不再在每个公开列表请求里调用 `list_square_hole_works` procedure。
|
||||
#[spacetimedb::view(accessor = square_hole_gallery_view, public)]
|
||||
pub fn square_hole_gallery_view(ctx: &AnonymousViewContext) -> Vec<SquareHoleGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.square_hole_work_profile()
|
||||
.by_square_hole_work_publication_status()
|
||||
.filter(SQUARE_HOLE_PUBLICATION_PUBLISHED)
|
||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"方洞挑战公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct SquareHoleGalleryViewRow {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: String,
|
||||
pub author_display_name: String,
|
||||
pub game_name: String,
|
||||
pub theme_text: String,
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
pub background_prompt: String,
|
||||
pub background_image_src: String,
|
||||
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_square_hole_agent_session(
|
||||
@@ -880,6 +939,38 @@ fn build_work_snapshot(row: &SquareHoleWorkProfileRow) -> Result<SquareHoleWorkS
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gallery_view_row(
|
||||
row: &SquareHoleWorkProfileRow,
|
||||
) -> Result<SquareHoleGalleryViewRow, String> {
|
||||
let config = parse_config(&row.config_json)?;
|
||||
Ok(SquareHoleGalleryViewRow {
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
game_name: row.game_name.clone(),
|
||||
theme_text: row.theme_text.clone(),
|
||||
twist_rule: row.twist_rule.clone(),
|
||||
summary_text: row.summary_text.clone(),
|
||||
tags: parse_tags(&row.tags_json)?,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
background_prompt: config.background_prompt,
|
||||
background_image_src: config.background_image_src,
|
||||
shape_options: config.shape_options,
|
||||
hole_options: config.hole_options,
|
||||
shape_count: row.shape_count,
|
||||
difficulty: row.difficulty,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: is_work_publish_ready(row),
|
||||
play_count: row.play_count,
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_run_row(
|
||||
ctx: &ReducerContext,
|
||||
row: SquareHoleRuntimeRunRow,
|
||||
|
||||
@@ -222,7 +222,7 @@ pub struct SquareHoleCreatorConfigSnapshot {
|
||||
pub background_image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeOptionSnapshot {
|
||||
pub option_id: String,
|
||||
@@ -235,7 +235,7 @@ pub struct SquareHoleShapeOptionSnapshot {
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleOptionSnapshot {
|
||||
pub hole_id: String,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::*;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
pub const VISUAL_NOVEL_SOURCE_IDEA: &str = "idea";
|
||||
pub const VISUAL_NOVEL_SOURCE_DOCUMENT: &str = "document";
|
||||
@@ -166,6 +167,58 @@ pub struct VisualNovelRuntimeEvent {
|
||||
pub(crate) occurred_at: Timestamp,
|
||||
}
|
||||
|
||||
/// 视觉小说公开广场列表投影。
|
||||
///
|
||||
/// 该 view 只暴露已发布作品卡片需要的公开字段,HTTP gallery 订阅后
|
||||
/// 从本地 cache 读取,避免每个列表请求调用 `list_visual_novel_works` procedure。
|
||||
#[spacetimedb::view(accessor = visual_novel_gallery_view, public)]
|
||||
pub fn visual_novel_gallery_view(ctx: &AnonymousViewContext) -> Vec<VisualNovelGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.visual_novel_work_profile()
|
||||
.by_visual_novel_work_publication_status()
|
||||
.filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED)
|
||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"视觉小说公开广场 view 跳过损坏的作品投影 profile_id={}: {}",
|
||||
row.profile_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct VisualNovelGalleryViewRow {
|
||||
pub work_id: String,
|
||||
pub profile_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_session_id: Option<String>,
|
||||
pub author_display_name: String,
|
||||
pub work_title: String,
|
||||
pub work_description: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: Option<String>,
|
||||
pub source_asset_ids: Vec<String>,
|
||||
pub publication_status: String,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct VisualNovelAgentSessionCreateInput {
|
||||
pub session_id: String,
|
||||
@@ -1445,6 +1498,31 @@ fn build_work_snapshot(row: &VisualNovelWorkProfileRow) -> Result<VisualNovelWor
|
||||
})
|
||||
}
|
||||
|
||||
fn build_gallery_view_row(
|
||||
row: &VisualNovelWorkProfileRow,
|
||||
) -> Result<VisualNovelGalleryViewRow, String> {
|
||||
Ok(VisualNovelGalleryViewRow {
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_session_id: empty_to_none(&row.source_session_id),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
work_title: row.work_title.clone(),
|
||||
work_description: row.work_description.clone(),
|
||||
tags: parse_string_vec_or_empty(&row.tags_json)?,
|
||||
cover_image_src: empty_to_none(&row.cover_image_src),
|
||||
source_asset_ids: parse_string_vec_or_empty(&row.source_asset_ids_json)?,
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
play_count: row.play_count,
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_run_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
row: &VisualNovelRuntimeRunRow,
|
||||
|
||||
Reference in New Issue
Block a user