Files
Genarrative/server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs

231 lines
7.6 KiB
Rust

use crate::*;
#[spacetimedb::table(accessor = creation_entry_config)]
pub struct CreationEntryConfig {
#[primary_key]
pub(crate) config_id: String,
pub(crate) start_title: String,
pub(crate) start_description: String,
pub(crate) start_idle_badge: String,
pub(crate) start_busy_badge: String,
pub(crate) modal_title: String,
pub(crate) modal_description: String,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = creation_entry_type_config,
index(accessor = by_creation_entry_type_sort_order, btree(columns = [sort_order]))
)]
pub struct CreationEntryTypeConfig {
#[primary_key]
pub(crate) id: String,
pub(crate) title: String,
pub(crate) subtitle: String,
pub(crate) badge: String,
pub(crate) image_src: String,
pub(crate) visible: bool,
pub(crate) open: bool,
pub(crate) sort_order: i32,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::procedure]
pub fn get_creation_entry_config(ctx: &mut ProcedureContext) -> CreationEntryConfigProcedureResult {
match ctx.try_with_tx(|tx| get_or_seed_creation_entry_config_snapshot(tx)) {
Ok(record) => CreationEntryConfigProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => CreationEntryConfigProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn upsert_creation_entry_type_config(
ctx: &mut ProcedureContext,
input: CreationEntryTypeAdminUpsertInput,
) -> CreationEntryConfigProcedureResult {
match ctx.try_with_tx(|tx| upsert_creation_entry_type_config_in_tx(tx, input.clone())) {
Ok(record) => CreationEntryConfigProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => CreationEntryConfigProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
fn upsert_creation_entry_type_config_in_tx(
ctx: &ReducerContext,
input: CreationEntryTypeAdminUpsertInput,
) -> Result<CreationEntryConfigSnapshot, String> {
seed_creation_entry_config_if_missing(ctx);
let now = ctx.timestamp;
let id = input.id.trim().to_string();
if id.is_empty() {
return Err("入口 ID 不能为空".to_string());
}
if input.title.trim().is_empty() {
return Err("入口标题不能为空".to_string());
}
let row = CreationEntryTypeConfig {
id: id.clone(),
title: input.title.trim().to_string(),
subtitle: input.subtitle.trim().to_string(),
badge: input.badge.trim().to_string(),
image_src: input.image_src.trim().to_string(),
visible: input.visible,
open: input.open,
sort_order: input.sort_order,
updated_at: now,
};
if ctx.db.creation_entry_type_config().id().find(&id).is_some() {
ctx.db.creation_entry_type_config().id().update(row);
} else {
ctx.db.creation_entry_type_config().insert(row);
}
get_or_seed_creation_entry_config_snapshot(ctx)
}
fn get_or_seed_creation_entry_config_snapshot(
ctx: &ReducerContext,
) -> Result<CreationEntryConfigSnapshot, String> {
seed_creation_entry_config_if_missing(ctx);
let header = ctx
.db
.creation_entry_config()
.config_id()
.find(CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string())
.ok_or_else(|| "创作入口配置初始化失败".to_string())?;
let mut creation_types = ctx
.db
.creation_entry_type_config()
.iter()
.map(|row| CreationEntryTypeSnapshot {
id: row.id,
title: row.title,
subtitle: row.subtitle,
badge: row.badge,
image_src: row.image_src,
visible: row.visible,
open: row.open,
sort_order: row.sort_order,
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
})
.collect::<Vec<_>>();
creation_types.sort_by(|left, right| {
left.sort_order
.cmp(&right.sort_order)
.then_with(|| left.id.cmp(&right.id))
});
Ok(CreationEntryConfigSnapshot {
config_id: header.config_id,
start_card: CreationEntryStartCardSnapshot {
title: header.start_title,
description: header.start_description,
idle_badge: header.start_idle_badge,
busy_badge: header.start_busy_badge,
},
type_modal: CreationEntryTypeModalSnapshot {
title: header.modal_title,
description: header.modal_description,
},
creation_types,
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
})
}
fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
let now = ctx.timestamp;
if ctx
.db
.creation_entry_config()
.config_id()
.find(CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string())
.is_none()
{
ctx.db.creation_entry_config().insert(CreationEntryConfig {
config_id: CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string(),
start_title: DEFAULT_CREATION_ENTRY_START_TITLE.to_string(),
start_description: DEFAULT_CREATION_ENTRY_START_DESCRIPTION.to_string(),
start_idle_badge: DEFAULT_CREATION_ENTRY_START_IDLE_BADGE.to_string(),
start_busy_badge: DEFAULT_CREATION_ENTRY_START_BUSY_BADGE.to_string(),
modal_title: DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
modal_description: DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
updated_at: now,
});
}
for seed in default_creation_entry_type_configs(now) {
if ctx
.db
.creation_entry_type_config()
.id()
.find(&seed.id)
.is_none()
{
ctx.db.creation_entry_type_config().insert(seed);
}
}
migrate_visual_novel_entry_from_old_visible_default(ctx, now);
}
fn migrate_visual_novel_entry_from_old_visible_default(ctx: &ReducerContext, now: Timestamp) {
let id = "visual-novel".to_string();
let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else {
return;
};
// 中文注释:只纠偏历史默认种子,不覆盖后台入口开关里后续手动调整过的视觉小说配置。
let still_old_visible_default = row.title == "视觉小说"
&& row.subtitle == "分支叙事体验"
&& row.image_src == "/creation-type-references/visual-novel.webp"
&& row.visible
&& ((row.badge == "可创建" && row.open)
|| (row.badge == "敬请期待" && !row.open))
&& row.sort_order == 60;
if !still_old_visible_default {
return;
}
ctx.db
.creation_entry_type_config()
.id()
.update(CreationEntryTypeConfig {
badge: "敬请期待".to_string(),
visible: false,
open: false,
updated_at: now,
..row
});
}
fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeConfig> {
module_runtime::default_creation_entry_type_snapshots(now.to_micros_since_unix_epoch())
.into_iter()
.map(|snapshot| CreationEntryTypeConfig {
id: snapshot.id,
title: snapshot.title,
subtitle: snapshot.subtitle,
badge: snapshot.badge,
image_src: snapshot.image_src,
visible: snapshot.visible,
open: snapshot.open,
sort_order: snapshot.sort_order,
updated_at: now,
})
.collect()
}