chore: bootstrap nana-story M0
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "nana-story-app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
description = "Desktop shell for 听娜娜讲故事"
|
||||
|
||||
[lib]
|
||||
name = "nana_story_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
nana-domain.workspace = true
|
||||
nana-runtime.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri = { version = "2", features = [] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use nana_domain::{
|
||||
AppInfo, DemoPackSummary, DOMAIN_SCHEMA_VERSION, PlayerView, ResourceBundle, ValidationIssue,
|
||||
validate_bundle,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
const DEMO_PLAYER_VIEW: &str = include_str!("../../fixtures/player-view/initial.json");
|
||||
const DEMO_BUNDLE: &str = include_str!("../../content/nana-demo/bundle.json");
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CommandError {
|
||||
code: &'static str,
|
||||
message: String,
|
||||
issues: Vec<ValidationIssue>,
|
||||
}
|
||||
|
||||
impl CommandError {
|
||||
fn parse(target: &str, error: &serde_json::Error) -> Self {
|
||||
Self {
|
||||
code: "invalid_embedded_json",
|
||||
message: format!("failed to parse embedded {target}: {error}"),
|
||||
issues: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_app_info() -> AppInfo {
|
||||
AppInfo {
|
||||
name: "听娜娜讲故事".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
contract_version: DOMAIN_SCHEMA_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_demo_player_view() -> Result<PlayerView, CommandError> {
|
||||
serde_json::from_str(DEMO_PLAYER_VIEW)
|
||||
.map_err(|error| CommandError::parse("PlayerView fixture", &error))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_demo_pack_summary() -> Result<DemoPackSummary, CommandError> {
|
||||
let bundle: ResourceBundle = serde_json::from_str(DEMO_BUNDLE)
|
||||
.map_err(|error| CommandError::parse("content bundle", &error))?;
|
||||
let report = validate_bundle(&bundle);
|
||||
if !report.is_valid() {
|
||||
return Err(CommandError {
|
||||
code: "invalid_content_bundle",
|
||||
message: "the embedded content bundle failed domain validation".to_owned(),
|
||||
issues: report.issues,
|
||||
});
|
||||
}
|
||||
Ok(DemoPackSummary {
|
||||
id: bundle.id,
|
||||
display_name: bundle.display_name,
|
||||
revision: bundle.revision,
|
||||
characters: bundle.characters.len(),
|
||||
world_books: bundle.world_books.len(),
|
||||
personas: bundle.personas.len(),
|
||||
plot_modules: bundle.plot_modules.len(),
|
||||
item_specs: bundle.item_specs.len(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_app_info,
|
||||
get_demo_pack_summary,
|
||||
get_demo_player_view
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run nana-story");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{get_demo_pack_summary, get_demo_player_view};
|
||||
|
||||
#[test]
|
||||
fn embedded_player_view_is_valid() {
|
||||
let view = get_demo_player_view().expect("valid PlayerView fixture");
|
||||
assert_eq!(view.character_name, "娜娜");
|
||||
assert!(!view.beats.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_content_bundle_is_valid() {
|
||||
let summary = get_demo_pack_summary().expect("valid demo bundle");
|
||||
assert_eq!(summary.characters, 1);
|
||||
assert_eq!(summary.plot_modules, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
nana_story_app_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "听娜娜讲故事",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.nanastory.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "听娜娜讲故事",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' asset: data:; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"shortDescription": "本地优先的 AI 对话游戏"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user