diff --git a/Cargo.toml b/Cargo.toml index 7e674e5..5f7157c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ build = "src/ui/build.rs" [dependencies] slint = "1.17.1" rodio = "0.17" -tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread", "macros", "sync", "time"] } +tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread", "macros", "sync", "time", "fs"] } serde = { version = "1.0.219", features = ["derive"] } toml = "0.9.5" lazy_static = "1.5.0" diff --git a/savedata/data.toml b/savedata/data.toml new file mode 100644 index 0000000..e108565 --- /dev/null +++ b/savedata/data.toml @@ -0,0 +1,6 @@ +cg = [1022] + +[read] +ky02 = 11 +ky01 = 3 +ky03 = 3 diff --git a/savedata/extra.toml b/savedata/extra.toml deleted file mode 100644 index d44936b..0000000 --- a/savedata/extra.toml +++ /dev/null @@ -1,2 +0,0 @@ -[cg] -cg = [1022] diff --git a/savedata/user.toml b/savedata/user.toml index 3bb0f67..2c9b4f7 100644 --- a/savedata/user.toml +++ b/savedata/user.toml @@ -7,6 +7,7 @@ speed = 50.0 opacity = 0.8 is_bold = false show_shadow = true +font = "PingFang SC" [volume] main = 100.0 diff --git a/source/cg/length.toml b/source/cg/length.toml index be29d59..845beea 100644 --- a/source/cg/length.toml +++ b/source/cg/length.toml @@ -1,11 +1,10 @@ -cast = [ - {name = "ev_rir_03_01", index = 1, length = 3}, - {name = "ev_rir_03_02", index = 2, length = 1}, - {name = "ev_rir_03_03", index = 3, length = 1}, - {name = "ev_rir_04_01", index = 4, length = 5}, - {name = "ev_rir_04_02", index = 5, length = 1}, - {name = "ev_rir_04_03", index = 6, length = 1}, - {name = "ev_rir_04_04", index = 7, length = 1}, - {name = "ev_rir_04_05", index = 8, length = 1}, - {name = "ev_rir_06_01", index = 9, length = 1}, -] \ No newline at end of file +[cast] +ev_rir_03_01 = { index = 1, length = 3 } +ev_rir_03_02 = { index = 2, length = 1 } +ev_rir_03_03 = { index = 3, length = 1 } +ev_rir_04_01 = { index = 4, length = 5 } +ev_rir_04_02 = { index = 5, length = 1 } +ev_rir_04_03 = { index = 6, length = 1 } +ev_rir_04_04 = { index = 7, length = 1 } +ev_rir_04_05 = { index = 8, length = 1 } +ev_rir_06_01 = { index = 9, length = 1 } diff --git a/source/figure/rar/rar.toml b/source/figure/rar/rar.toml index f7deb60..29d4553 100755 --- a/source/figure/rar/rar.toml +++ b/source/figure/rar/rar.toml @@ -1,26 +1,22 @@ [body] -cast = [ - {name = "rar_noa0200", rate = 0.412}, - {name = "rar_z1a0200", rate = 0.414}, -] +rar_noa0200 = 0.412 +rar_z1a0200 = 0.414 [face] -cast = [ - {name = "noa0002", x = 0.361, y = 0.0825}, - {name = "noa0015", x = 0.357, y = 0.0907}, - {name = "noa0039", x = 0.363, y = 0.0907}, - {name = "noa0041", x = 0.363, y = 0.0874}, - {name = "noa0052", x = 0.363, y = 0.0874}, - {name = "noa0053", x = 0.363, y = 0.0874}, - {name = "noa0054", x = 0.369, y = 0.0899}, - {name = "z1a0002", x = 0.356, y = 0.0823}, - {name = "z1a0015", x = 0.352, y = 0.0900}, - {name = "z1a0039", x = 0.355, y = 0.0905}, - {name = "z1a0041", x = 0.355, y = 0.0868}, - {name = "z1a0052", x = 0.355, y = 0.0873}, - {name = "z1a0053", x = 0.355, y = 0.0873}, - {name = "z1a0054", x = 0.363, y = 0.0895}, -] +noa0002 = [0.361, 0.0825] +noa0015 = [0.357, 0.0907] +noa0039 = [0.363, 0.0907] +noa0041 = [0.363, 0.0874] +noa0052 = [0.363, 0.0874] +noa0053 = [0.363, 0.0874] +noa0054 = [0.369, 0.0899] +z1a0002 = [0.356, 0.0823] +z1a0015 = [0.352, 0.0900] +z1a0039 = [0.355, 0.0905] +z1a0041 = [0.355, 0.0868] +z1a0052 = [0.355, 0.0873] +z1a0053 = [0.355, 0.0873] +z1a0054 = [0.363, 0.0895] [offset] -offset = 0.0298 \ No newline at end of file +offset = 0.0298 diff --git a/source/figure/rir/rir.toml b/source/figure/rir/rir.toml index 60df34d..e462ada 100644 --- a/source/figure/rir/rir.toml +++ b/source/figure/rir/rir.toml @@ -1,42 +1,36 @@ [body] -cast = [ - {name = "rir_z1a0200", rate = 0.363}, - {name = "rir_z1b0200", rate = 0.389}, - {name = "rir_z1b0210", rate = 0.389}, - {name = "rir_noa0200", rate = 0.363}, - {name = "rir_nob0200", rate = 0.386}, -] +rir_z1a0200 = 0.363 +rir_z1b0200 = 0.389 +rir_z1b0210 = 0.389 +rir_noa0200 = 0.363 +rir_nob0200 = 0.386 [face] -cast = [ - {name = "z1a0050", x = 0.343, y = 0.0507}, - {name = "z1a0043", x = 0.337, y = 0.0808}, - {name = "z1a0049", x = 0.339, y = 0.0838}, - {name = "z1b0058", x = 0.332, y = 0.0728}, - {name = "z1b0059", x = 0.327, y = 0.0757}, - {name = "z1b0050", x = 0.337, y = 0.0530}, - {name = "z1a0002", x = 0.328, y = 0.0784}, - {name = "z1a0009", x = 0.326, y = 0.0735}, - {name = "z1a0038", x = 0.334, y = 0.0814}, - {name = "z1a0039", x = 0.337, y = 0.0804}, - {name = "z1a0050", x = 0.343, y = 0.0507}, - {name = "z1a0052", x = 0.330, y = 0.0764}, - {name = "z1b0050", x = 0.337, y = 0.0530}, - {name = "z1b0008", x = 0.318, y = 0.0655}, - {name = "z1b0010", x = 0.318, y = 0.0664}, - {name = "z1b0012", x = 0.332, y = 0.0758}, - {name = "z1b0031", x = 0.332, y = 0.0817}, - {name = "noa0002", x = 0.328, y = 0.0793}, - {name = "noa0009", x = 0.325, y = 0.0733}, - {name = "noa0038", x = 0.332, y = 0.0819}, - {name = "noa0039", x = 0.335, y = 0.0810}, - {name = "noa0050", x = 0.343, y = 0.0507}, - {name = "noa0052", x = 0.330, y = 0.0776}, - {name = "nob0050", x = 0.337, y = 0.0530}, - {name = "nob0008", x = 0.319, y = 0.0655}, - {name = "nob0010", x = 0.319, y = 0.0655}, - {name = "nob0031", x = 0.335, y = 0.0810}, -] +z1a0050 = [0.343, 0.0507] +z1a0043 = [0.337, 0.0808] +z1a0049 = [0.339, 0.0838] +z1b0058 = [0.332, 0.0728] +z1b0059 = [0.327, 0.0757] +z1b0050 = [0.337, 0.0530] +z1a0002 = [0.328, 0.0784] +z1a0009 = [0.326, 0.0735] +z1a0038 = [0.334, 0.0814] +z1a0039 = [0.337, 0.0804] +z1a0052 = [0.330, 0.0764] +z1b0008 = [0.318, 0.0655] +z1b0010 = [0.318, 0.0664] +z1b0012 = [0.332, 0.0758] +z1b0031 = [0.332, 0.0817] +noa0002 = [0.328, 0.0793] +noa0009 = [0.325, 0.0733] +noa0038 = [0.332, 0.0819] +noa0039 = [0.335, 0.0810] +noa0050 = [0.343, 0.0507] +noa0052 = [0.330, 0.0776] +nob0050 = [0.337, 0.0530] +nob0008 = [0.319, 0.0655] +nob0010 = [0.319, 0.0655] +nob0031 = [0.335, 0.0810] [offset] -offset = 0.0 \ No newline at end of file +offset = 0.0 diff --git a/source/voice/rar/length.toml b/source/voice/rar/length.toml index b46995f..a916e1b 100644 --- a/source/voice/rar/length.toml +++ b/source/voice/rar/length.toml @@ -1,4 +1,3 @@ -cast = [ - {name = "fem_rar_50055", length = 4}, - {name = "fem_rar_50056", length = 6}, -] \ No newline at end of file +[cast] +fem_rar_50055 = 4 +fem_rar_50056 = 6 diff --git a/source/voice/rir/length.toml b/source/voice/rir/length.toml index 4c004d1..5c10e10 100644 --- a/source/voice/rir/length.toml +++ b/source/voice/rir/length.toml @@ -1,10 +1,9 @@ -cast = [ - {name = "fem_rir_50236", length = 2}, - {name = "fem_rir_50237", length = 10}, - {name = "fem_rir_50238", length = 10}, - {name = "fem_rir_50239", length = 3}, - {name = "fem_rir_50240", length = 6}, - {name = "fem_rir_50520", length = 11}, - {name = "fem_rir_50521", length = 11}, - {name = "fem_rir_50522", length = 9}, -] \ No newline at end of file +[cast] +fem_rir_50236 = 2 +fem_rir_50237 = 10 +fem_rir_50238 = 10 +fem_rir_50239 = 3 +fem_rir_50240 = 6 +fem_rir_50520 = 11 +fem_rir_50521 = 11 +fem_rir_50522 = 9 diff --git a/src/config/cg.rs b/src/config/cg.rs index 32bb301..e9f347d 100644 --- a/src/config/cg.rs +++ b/src/config/cg.rs @@ -3,38 +3,22 @@ use crate::error::{EngineError, ExecutorError}; use crate::ui::initialize::ExItem; use serde::{Deserialize, Serialize}; use slint::{Image, ModelRc, VecModel}; -use std::{cell::RefCell, path::Path, rc::Rc}; use std::{collections::HashMap, fs}; +use std::{path::Path, rc::Rc}; lazy_static::lazy_static! { pub(crate) static ref CG_CONFIG: CgConfig = load_cg(); } -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct CgMap { - cg: Vec, -} - -impl CgMap { - pub(crate) fn new(cg: Vec) -> Self { - Self { cg } - } - - pub(crate) fn cg(self) -> Vec { - self.cg - } -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -struct Length { - name: String, +#[derive(Debug, Deserialize)] +struct CgEntry { index: usize, length: u64, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] struct LengthWrapper { - cast: Vec, + cast: HashMap, } #[derive(Debug, Deserialize, Serialize)] @@ -59,27 +43,23 @@ impl CgConfig { } fn load_cg() -> CgConfig { - let content = fs::read_to_string(format!("{}length.toml", ENGINE_CONFIG.cg_path(),)).unwrap(); - let name_item: LengthWrapper = toml::from_str(&content).unwrap(); - let index_item = name_item.cast.clone(); - let length = index_item.len(); + let content = fs::read_to_string(format!("{}length.toml", ENGINE_CONFIG.cg_path())).unwrap(); + let item: LengthWrapper = toml::from_str(&content).unwrap(); + let length = item.cast.len(); + let mut cg_by_name = HashMap::new(); + let mut cg_by_id = HashMap::new(); + for (name, entry) in item.cast { + cg_by_name.insert(name.clone(), (entry.index, entry.length)); + cg_by_id.insert(entry.index, (name, entry.length)); + } CgConfig { - cg_by_name: name_item - .cast - .into_iter() - .map(|length| (length.name, (length.index, length.length))) - .collect(), - cg_by_id: index_item - .into_iter() - .map(|length| (length.index, (length.name, length.length))) - .collect(), + cg_by_name, + cg_by_id, length, } } -pub(crate) fn get_cg(cg: Rc>>) -> Result>, EngineError> { - let cg_map = cg.borrow(); - +pub(crate) fn get_cg(cg_map: &[u64]) -> Result>, EngineError> { let mut ex_items: Vec> = Vec::with_capacity(10); let mut i = 1; let mut ex_page: Vec = Vec::new(); diff --git a/src/config/extra.rs b/src/config/extra.rs deleted file mode 100644 index f8a96d8..0000000 --- a/src/config/extra.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::config::cg::CG_CONFIG; -use crate::config::{cg::CgMap, ENGINE_CONFIG}; -use crate::error::{EngineError, SaveError}; -use crate::executors::executor::Executor; -use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::fs; -use std::rc::Rc; - -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct ExtraConfig { - cg: CgMap, -} - -impl ExtraConfig { - pub(crate) fn cg(self) -> Vec { - self.cg.cg() - } -} - -impl Executor { - pub(crate) fn load_extra(&mut self) { - let extra_config = load_extra_config(); - self.set_cg(extra_config.cg()); - } -} - -fn load_extra_config() -> ExtraConfig { - if let Ok(content) = fs::read_to_string(format!("{}/extra.toml", ENGINE_CONFIG.save_path())) { - toml::from_str(&content).unwrap() - } else { - let num = CG_CONFIG.length() / 64 + 1; - ExtraConfig { - cg: CgMap::new(vec![0; num]), - } - } -} - -pub(crate) fn save_extra_config(cg: Rc>>) -> Result<(), EngineError> { - let cg = cg.borrow(); - let path = format!("{}/extra.toml", ENGINE_CONFIG.save_path()); - let content = toml::to_string(&ExtraConfig { - cg: CgMap::new(cg.clone()), - }) - .map_err(SaveError::from)?; - fs::write(&path, content).map_err(|e| SaveError::Write { path, source: e })?; - - Ok(()) -} diff --git a/src/config/figure.rs b/src/config/figure.rs index 1560c2a..8c3dc96 100644 --- a/src/config/figure.rs +++ b/src/config/figure.rs @@ -6,38 +6,15 @@ lazy_static::lazy_static! { pub(crate) static ref FIGURE_CONFIG: FigureConfig = load_figure(); } -#[derive(Debug, Deserialize, Serialize)] -struct Face { - name: String, - x: f32, - y: f32, -} - -#[derive(Debug, Deserialize, Serialize)] -struct FaceWrapper { - cast: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] -struct Body { - name: String, - rate: f32, -} - -#[derive(Debug, Deserialize, Serialize)] -struct BodyWrapper { - cast: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] struct Offset { offset: f32, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] struct FigureRead { - body: BodyWrapper, - face: FaceWrapper, + body: HashMap, + face: HashMap, offset: Offset, } @@ -77,22 +54,8 @@ fn load_figure() -> FigureConfig { )) .unwrap(); let item: FigureRead = toml::from_str(&content).unwrap(); - face_list.insert( - char.to_string(), - item.face - .cast - .into_iter() - .map(|face| (face.name, (face.x, face.y))) - .collect(), - ); - body_list.insert( - char.to_string(), - item.body - .cast - .into_iter() - .map(|body| (body.name, body.rate)) - .collect(), - ); + face_list.insert(char.to_string(), item.face); + body_list.insert(char.to_string(), item.body); offset_list.insert(char.to_string(), item.offset.offset); } diff --git a/src/config/mod.rs b/src/config/mod.rs index 15b4940..c36b127 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,7 +11,6 @@ pub(crate) mod save_load; pub(crate) mod cg; pub(crate) mod character_volume; -pub(crate) mod extra; pub(crate) mod font; pub(crate) mod system; pub(crate) mod text; diff --git a/src/config/voice.rs b/src/config/voice.rs index 197ac59..52df3b8 100644 --- a/src/config/voice.rs +++ b/src/config/voice.rs @@ -1,5 +1,5 @@ use crate::config::ENGINE_CONFIG; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs}; use tokio::time::Duration; @@ -7,16 +7,9 @@ lazy_static::lazy_static! { pub(crate) static ref VOICE_LENGTH: VoiceLength = load_voice(); } -#[derive(Debug, Deserialize, Serialize)] -struct Length { - name: String, - #[serde(deserialize_with = "deserialize_duration_from_secs")] - length: Duration, -} - -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] struct LengthWrapper { - cast: Vec, + cast: HashMap, } #[derive(Debug, Deserialize, Serialize)] @@ -44,18 +37,10 @@ fn load_voice() -> VoiceLength { char.to_string(), item.cast .into_iter() - .map(|length| (length.name, length.length)) + .map(|(name, secs)| (name, Duration::from_secs(secs))) .collect(), ); } VoiceLength { voice_length } } - -pub(crate) fn deserialize_duration_from_secs<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let seconds = u64::deserialize(deserializer)?; - Ok(Duration::from_secs(seconds)) -} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..86e2cff --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,59 @@ +use crate::config::{cg::CG_CONFIG, ENGINE_CONFIG}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use tokio::sync::watch; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub(crate) struct UserData { + #[serde(default)] + pub(crate) cg: Vec, + #[serde(default)] + pub(crate) read: HashMap, +} + +impl Default for UserData { + fn default() -> Self { + let num = CG_CONFIG.length() / 64 + 1; + Self { + cg: vec![0; num], + read: HashMap::new(), + } + } +} + +impl UserData { + pub(crate) fn load() -> Self { + let path = format!("{}/data.toml", ENGINE_CONFIG.save_path()); + let mut data = fs::read_to_string(&path) + .ok() + .and_then(|content| toml::from_str::(&content).ok()) + .unwrap_or_default(); + let num = CG_CONFIG.length() / 64 + 1; + if data.cg.len() < num { + data.cg.resize(num, 0); + } + data + } +} + +pub(crate) fn spawn_save_task() -> watch::Sender> { + let (tx, mut rx) = watch::channel::>(None); + tokio::spawn(async move { + while rx.changed().await.is_ok() { + let snapshot = rx.borrow_and_update().clone(); + if let Some(data) = snapshot { + let path = format!("{}/data.toml", ENGINE_CONFIG.save_path()); + match toml::to_string(&data) { + Ok(content) => { + if let Err(e) = tokio::fs::write(&path, content).await { + eprintln!("save user data failed: {e}"); + } + } + Err(e) => eprintln!("serialize user data failed: {e}"), + } + } + } + }); + tx +} diff --git a/src/executors/executor.rs b/src/executors/executor.rs index a1b816d..0041374 100644 --- a/src/executors/executor.rs +++ b/src/executors/executor.rs @@ -1,12 +1,14 @@ use crate::config::cg::get_cg; use crate::config::{ - cg::CG_CONFIG, extra::save_extra_config, figure::FIGURE_CONFIG, save_load::SaveData, - user::save_user_config, voice::VOICE_LENGTH, ENGINE_CONFIG, + cg::CG_CONFIG, figure::FIGURE_CONFIG, save_load::SaveData, user::save_user_config, + voice::VOICE_LENGTH, ENGINE_CONFIG, }; +use crate::data::UserData; use crate::error::{EngineError, SaveError}; use crate::executors::{ delay_executor::{DelayChannels, DelayTX}, text_executor::{DisplayText, TextTX}, + Status, }; use crate::media::{ player::{MediaPlayer, PreBgm, PreBgm::Play}, @@ -25,6 +27,7 @@ use std::{ time::Duration, }; use tokio::sync::mpsc::Sender; +use tokio::sync::watch; pub(crate) enum Jump { Label(Label), @@ -72,7 +75,8 @@ fn parse_position(position: &str, distance: &str) -> (f32, f32, f32) { pub(crate) struct Executor { script: Rc>, media_player: Rc>, - cg: Rc>>, + user_data: Rc>, + save_tx: Option>>, weak: Weak, text: Arc>, choose_lock: Rc>, @@ -82,11 +86,16 @@ pub(crate) struct Executor { text_tx: Option, auto_tx: Option>, delay_channels: Option, + status_channel: Option<(Sender<()>, Sender<()>)>, } impl Executor { pub(crate) fn new(weak: Weak) -> Result { - let script = Parser::load("ky01")?; + let mut script = Parser::load("ky01")?; + + let user_data = UserData::load(); + let read_block = user_data.read.get(script.name()).copied().unwrap_or(0); + script.set_read_block(read_block); let figure_items = Rc::new(VecModel::::default()); if let Some(window) = weak.upgrade() { @@ -96,7 +105,8 @@ impl Executor { Ok(Executor { script: Rc::new(RefCell::new(script)), media_player: Rc::new(RefCell::new(MediaPlayer::new()?)), - cg: Rc::new(RefCell::new(Vec::new())), + user_data: Rc::new(RefCell::new(user_data)), + save_tx: None, weak, text: Arc::new(RwLock::new(DisplayText::new())), choose_lock: Rc::new(RefCell::new(false)), @@ -106,6 +116,7 @@ impl Executor { text_tx: None, auto_tx: None, delay_channels: None, + status_channel: None, }) } @@ -113,8 +124,20 @@ impl Executor { self.weak.clone() } - pub(crate) fn set_cg(&mut self, cg: Vec) { - *self.cg.borrow_mut() = cg; + pub(crate) fn set_save_tx(&mut self, save_tx: watch::Sender>) { + self.save_tx = Some(save_tx); + } + + /// 读取某剧本已记录的已读进度(不存在则为 0) + fn read_of(&self, name: &str) -> usize { + self.user_data.borrow().read.get(name).copied().unwrap_or(0) + } + + /// 向异步保存 task 推送最新快照(非阻塞) + fn request_save(&self) { + if let Some(tx) = &self.save_tx { + let _ = tx.send(Some(self.user_data.borrow().clone())); + } } pub(crate) fn set_text_tx(&mut self, text_tx: Sender>>) { @@ -138,12 +161,34 @@ impl Executor { }); } + pub(crate) fn set_status_channel(&mut self, auto_tx: Sender<()>, skip_tx: Sender<()>) { + self.status_channel = Some((auto_tx, skip_tx)); + } + + pub(crate) fn get_status_channel(&self) -> Option<(Sender<()>, Sender<()>)> { + if let Some((auto_tx, skip_tx)) = self.status_channel.as_ref() { + return Some((auto_tx.clone(), skip_tx.clone())); + } + None + } + + pub(crate) fn can_skip(&self) -> bool { + let scr = self.script.borrow(); + if let Some(window) = self.weak.upgrade() { + if window.get_skip_conf() { + return true; + } + } + scr.read_block() > scr.index() + } + pub(crate) fn unlock(&mut self, index: usize) { - let mut cg = self.cg.borrow_mut(); - cg[index / 64] |= 1u64 << index; + let mut ud = self.user_data.borrow_mut(); + ud.cg[index / 64] |= 1u64 << index; } - pub(crate) fn execute_backlog(&self) -> Result<(), EngineError> { + pub(crate) fn execute_backlog(&mut self) -> Result<(), EngineError> { + self.execute_status(Status::Normal, false)?; if let Some(window) = self.weak.upgrade() { let script = self.script.borrow(); let backlog = script.backlog(); @@ -173,6 +218,7 @@ impl Executor { } pub(crate) fn execute_replay(&mut self) -> Result<(), EngineError> { + self.execute_status(Status::Normal, false)?; let script = self.script.borrow(); if let Some((name, voice)) = script.last_voice() { self.play_voice(&name, &voice)?; @@ -231,7 +277,7 @@ impl Executor { } pub(crate) fn execute_get_ex(&self) -> Result<(), EngineError> { - let ex_items = get_cg(self.cg.clone())?; + let ex_items = get_cg(&self.user_data.borrow().cg)?; if let Some(window) = self.weak.upgrade() { window.set_ex_items(ex_items); @@ -308,6 +354,7 @@ impl Executor { if name != script.name() { let mut scr = Parser::load(&name)?; scr.set_backlog(backlog); + scr.set_read_block(self.read_of(&name)); *script = scr; } script.find_label(&label).copied() @@ -316,6 +363,7 @@ impl Executor { if name != script.name() { let mut scr = Parser::load(&name)?; scr.set_backlog(backlog); + scr.set_read_block(self.read_of(&name)); *script = scr; } Some(index as usize) @@ -329,34 +377,57 @@ impl Executor { Ok(()) } - pub(crate) fn execute_auto(&mut self, tx: Sender<()>, source: bool) -> Result<(), EngineError> { + pub(crate) fn execute_status( + &mut self, + status: Status, + symbol: bool, + ) -> Result<(), EngineError> { + let (auto_tx, skip_tx) = self.get_status_channel().unwrap(); + if let Some(window) = self.weak.upgrade() { - if source { - self.auto_tx - .clone() - .unwrap() - .try_send(Duration::from_secs(1))?; - tx.try_send(())?; - } else { - if window.get_is_auto() { - tx.try_send(())?; + match status { + Status::Auto => { + if symbol { + window.set_is_auto(false); + } else { + if window.get_is_skip() { + skip_tx.try_send(())?; + window.set_is_skip(false); + } + window.set_is_auto(true); + self.auto_tx + .clone() + .unwrap() + .try_send(Duration::from_secs(1))?; + } + auto_tx.try_send(())?; } - window.set_is_auto(false); - } - } - - Ok(()) - } + Status::Skip => { + if !self.can_skip() { + return Ok(()); + } - pub(crate) fn execute_skip(&mut self, tx: Sender<()>, source: bool) -> Result<(), EngineError> { - if let Some(window) = self.weak.upgrade() { - if source { - tx.try_send(())?; - } else { - if window.get_is_skip() { - tx.try_send(())?; + if symbol { + window.set_is_skip(false); + } else { + if window.get_is_auto() { + auto_tx.try_send(())?; + window.set_is_auto(false); + } + window.set_is_skip(true); + } + skip_tx.try_send(())?; + } + Status::Normal => { + if window.get_is_auto() { + auto_tx.try_send(())?; + window.set_is_auto(false); + } + if window.get_is_skip() { + skip_tx.try_send(())?; + window.set_is_skip(false); + } } - window.set_is_skip(false); } } @@ -432,6 +503,20 @@ impl Executor { } }; + // 同步当前剧本的已读进度到 user_data,并异步实时保存 + { + let (name, rb) = { + let scr = self.script.borrow(); + (scr.name().to_string(), scr.read_block()) + }; + let mut ud = self.user_data.borrow_mut(); + let entry = ud.read.entry(name).or_insert(0); + if rb > *entry { + *entry = rb; + } + } + self.request_save(); + if is_wait { duration += delay; } @@ -508,6 +593,7 @@ impl Executor { text.replace("{nns}", "").to_shared_string(), voice, ); + window.set_is_read(script.read_block() > script.index()); } window.set_speaker(SharedString::from(speaker)); { @@ -609,7 +695,7 @@ impl Executor { let path = if *is_cg { if let Some((index, _)) = CG_CONFIG.find_by_name(name) { self.unlock(*index); - save_extra_config(self.cg.clone())?; + self.request_save(); } ENGINE_CONFIG.cg_path() } else { diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 81e0837..cf60755 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1,9 +1,9 @@ +use crate::data::spawn_save_task; use crate::error::EngineError; use crate::executors::{ auto_executor::AutoExecutor, delay_executor::DelayExecutor, executor::Executor, skip_executor::SkipExecutor, text_executor::TextExecutor, }; -use tokio::sync::mpsc::Sender; pub(crate) mod auto_executor; pub(crate) mod delay_executor; @@ -11,9 +11,13 @@ pub(crate) mod executor; pub(crate) mod skip_executor; pub(crate) mod text_executor; -pub(crate) struct ExecutorTX { - auto_tx: Sender<()>, - skip_tx: Sender<()>, +pub(crate) enum Status { + Auto, + Skip, + Normal, +} + +pub(crate) struct ExecutorCluster { _text_executor: TextExecutor, _auto_executor: AutoExecutor, _skip_executor: SkipExecutor, @@ -22,17 +26,7 @@ pub(crate) struct ExecutorTX { _loop_move_executor: DelayExecutor, } -impl ExecutorTX { - pub(crate) fn auto_tx(&self) -> Sender<()> { - self.auto_tx.clone() - } - - pub(crate) fn skip_tx(&self) -> Sender<()> { - self.skip_tx.clone() - } -} - -pub(crate) fn load_data(executor: &mut Executor) -> Result { +pub(crate) fn load_data(executor: &mut Executor) -> Result { let (mut text_executor, text_tx) = TextExecutor::new(executor.get_weak()); executor.set_text_tx(text_tx); @@ -69,18 +63,21 @@ pub(crate) fn load_data(executor: &mut Executor) -> Result, commands: Vec, current_block: usize, + read_block: usize, current_bgm: String, pre_voice: Option<(SharedString, SharedString)>, timeline: Timeline, @@ -109,6 +110,7 @@ impl Script { backlog: Vec::new(), commands: Vec::new(), current_block: 0, + read_block: 0, current_bgm: String::new(), pre_voice: None, timeline: Timeline::default(), @@ -123,8 +125,19 @@ impl Script { self.name = name.to_string(); } + pub(crate) fn set_read_block(&mut self, read_block: usize) { + self.read_block = read_block; + } + + pub(crate) fn read_block(&self) -> usize { + self.read_block + } + pub(crate) fn next_command(&mut self) -> Option<&Commands> { let command = self.commands.get(self.current_block); + if self.read_block <= self.current_block { + self.read_block += 1; + } self.current_block += 1; command } diff --git a/src/ui/initialize.rs b/src/ui/initialize.rs index 64b9c32..ac481c7 100644 --- a/src/ui/initialize.rs +++ b/src/ui/initialize.rs @@ -1,5 +1,5 @@ use crate::error::EngineError; -use crate::executors::{executor::Executor, load_data}; +use crate::executors::{executor::Executor, load_data, Status}; slint::include_modules!(); pub(crate) async fn ui() -> Result<(), EngineError> { @@ -8,7 +8,7 @@ pub(crate) async fn ui() -> Result<(), EngineError> { let mut executor = Executor::new(weak)?; - let executor_tx = load_data(&mut executor)?; + let _executor_cluster = load_data(&mut executor)?; let mut is_fullscreen = false; let weak_for_fullscreen = executor.get_weak(); @@ -95,7 +95,7 @@ pub(crate) async fn ui() -> Result<(), EngineError> { }); window.on_backlog({ - let executor = executor.clone(); + let mut executor = executor.clone(); move || { executor.execute_backlog().expect("Backlog panicked"); } @@ -144,22 +144,27 @@ pub(crate) async fn ui() -> Result<(), EngineError> { window.on_auto_play({ let mut executor = executor.clone(); - let tx = executor_tx.auto_tx(); - move |source| { - let tx = tx.clone(); + move |symbol| { executor - .execute_auto(tx, source) + .execute_status(Status::Auto, symbol) .expect("TODO: panic message"); } }); window.on_skip_play({ let mut executor = executor.clone(); - let tx = executor_tx.skip_tx(); - move |source| { - let tx = tx.clone(); + move |symbol| { + executor + .execute_status(Status::Skip, symbol) + .expect("TODO: panic message"); + } + }); + + window.on_normal_play({ + let mut executor = executor.clone(); + move || { executor - .execute_skip(tx, source) + .execute_status(Status::Normal, false) .expect("TODO: panic message"); } }); diff --git a/ui/components/common/switch_toggle.slint b/ui/components/common/switch_toggle.slint new file mode 100644 index 0000000..8911ef7 --- /dev/null +++ b/ui/components/common/switch_toggle.slint @@ -0,0 +1,32 @@ +import { Switch } from "std-widgets.slint"; +import { Colors } from "../../styles/colors.slint"; + +// 文本 + 开关:label 在左,Switch 紧随其后 +export component SwitchToggle { + in property label; + in-out property checked; + in property texts; + + callback toggled(); + + Rectangle { + Text { + x: 0; + y: 0; + width: parent.width * 0.3; + text: label; + font-size: parent.height * 0.8; + color: Colors.text-light; + vertical-alignment: center; + } + + Switch { + x: parent.height * 1.2 * texts; + y: 0; + checked <=> root.checked; + toggled => { + root.toggled(); + } + } + } +} diff --git a/ui/components/config/screen.slint b/ui/components/config/screen.slint index 49a1a4d..64421ab 100644 --- a/ui/components/config/screen.slint +++ b/ui/components/config/screen.slint @@ -1,5 +1,4 @@ -import { Switch } from "std-widgets.slint"; -import { Colors } from "../../styles/colors.slint"; +import { SwitchToggle } from "../common/switch_toggle.slint"; export component ScreenConfig { in property is-fullscreen; @@ -7,33 +6,16 @@ export component ScreenConfig { callback toggle-fullscreen(); Rectangle { - Rectangle { + SwitchToggle { x: parent.width * 0.02; y: 0; + width: parent.width * 0.35; height: parent.height * 0.03; - - Text { - x: 0; - y: 0; - text: "全屏开关"; - font-size: parent.height * 0.8; - color: Colors.text-light; - vertical-alignment: center; - width: parent.width * 0.12; - } - - Rectangle { - x: parent.width * 0.08; - y: 0; - width: parent.width * 0.032; - height: parent.height; - - Switch { - checked: root.is-fullscreen; - width: parent.width; - height: parent.height; - toggled => { root.toggle-fullscreen(); } - } + label: "全屏开关"; + texts: 6; + checked: root.is-fullscreen; + toggled => { + root.toggle-fullscreen(); } } } diff --git a/ui/components/config/system.slint b/ui/components/config/system.slint index 0727e52..f9c22be 100644 --- a/ui/components/config/system.slint +++ b/ui/components/config/system.slint @@ -1,55 +1,45 @@ -import { Switch } from "std-widgets.slint"; import { VolumeSlider } from "../common/volume_slider.slint"; -import { Colors } from "../../styles/colors.slint"; +import { SwitchToggle } from "../common/switch_toggle.slint"; export component SystemConfig { in-out property is-wait; + in-out property skip-conf; in-out property delay; Rectangle { - Rectangle { + SwitchToggle { x: parent.width * 0.02; y: 0; + width: parent.width * 0.35; height: parent.height * 0.035; - - Text { - x: 0; - y: 0; - text: "自动等待语音"; - font-size: parent.height * 0.8; - color: Colors.text-light; - vertical-alignment: center; - width: parent.width * 0.12; - } - - Rectangle { - x: parent.height * 1.2 * 5.5; - y: 0; - width: parent.width * 0.032; - height: parent.height; - - Switch { - checked: root.is-wait; - width: parent.width; - height: parent.height; - toggled => { root.is-wait = !root.is-wait } - } - } + label: "自动等待语音"; + texts: 6; + checked <=> root.is-wait; } VolumeSlider { - y: parent.height * 0.05; + y: parent.height * 0.08; x: parent.width * 0.02; width: parent.width * 0.35; height: parent.height * 0.035; max: 5.0; min: 1.0; label: "自动等待时长"; - texts: 5.5; + texts: 6; volume: root.delay; volume-changed(value) => { root.delay = value; } } + + SwitchToggle { + x: parent.width * 0.02; + y: parent.height * 0.16; + width: parent.width * 0.35; + height: parent.height * 0.035; + label: "跳过未读文本"; + texts: 6; + checked <=> root.skip-conf; + } } } \ No newline at end of file diff --git a/ui/components/config/text.slint b/ui/components/config/text.slint index d47c146..4f55a46 100644 --- a/ui/components/config/text.slint +++ b/ui/components/config/text.slint @@ -2,7 +2,7 @@ import { CustomButton } from "../common/button.slint"; import { VolumeSlider } from "../common/volume_slider.slint"; import { FontDrawer } from "../common/drawer.slint"; import { Colors } from "../../styles/colors.slint"; -import { Switch } from "std-widgets.slint"; +import { SwitchToggle } from "../common/switch_toggle.slint"; export component TextConfig { in-out property text-speed; @@ -45,33 +45,23 @@ export component TextConfig { } } - Text { + SwitchToggle { y: parent.height * 0.16; x: parent.width * 0.02; width: parent.width * 0.35; height: parent.height * 0.035; - text: "文本加粗"; - font-size: parent.height * 0.035 * 0.8; - color: Colors.text-light; - } - - Switch { - y: parent.height * 0.16; - x: parent.height * 0.252 + parent.width * 0.02; + label: "文本加粗"; + texts: 6; checked <=> root.is-bold; } - Text { + SwitchToggle { y: parent.height * 0.24; x: parent.width * 0.02; - text: "文本阴影"; - font-size: parent.height * 0.035 * 0.8; - color: Colors.text-light; - } - - Switch { - y: parent.height * 0.24; - x: parent.height * 0.252 + parent.width * 0.02; + width: parent.width * 0.35; + height: parent.height * 0.035; + label: "文本阴影"; + texts: 6; checked <=> root.show-shadow; } diff --git a/ui/components/main_config.slint b/ui/components/main_config.slint index 66ec7da..cb9ff3f 100644 --- a/ui/components/main_config.slint +++ b/ui/components/main_config.slint @@ -18,6 +18,7 @@ export component SettingsView { in-out property voice-volume; in-out property <[CharacterVolume]> character_volumes: []; in-out property is-wait; + in-out property skip-conf; in-out property delay; in-out property text-speed; in-out property dialogue-opacity; @@ -134,6 +135,7 @@ export component SettingsView { width: parent.width; is-wait <=> root.is-wait; + skip-conf <=> root.skip-conf; delay <=> root.delay; } diff --git a/ui/components/story.slint b/ui/components/story.slint index c73e5a0..740abb3 100644 --- a/ui/components/story.slint +++ b/ui/components/story.slint @@ -40,6 +40,7 @@ export component StoryView { in property is-bold; in property show-shadow; in property dialogue-font; + in property is-read; in-out property is-video: false; in property video-frame; @@ -172,7 +173,7 @@ export component StoryView { font-size: parent.height * 2 / 15; font-weight: root.is-bold ? 700 : 400; font-family: root.dialogue-font; - color: Colors.text-light; + color: root.is-read ? Colors.read-color : Colors.text-light; wrap: word-wrap; width: parent.width * 31 / 32; } @@ -195,7 +196,7 @@ export component StoryView { font-size: parent.height * 2 / 15; font-weight: root.is-bold ? 700 : 400; font-family: root.dialogue-font; - color: Colors.text-light; + color: root.is-read ? Colors.read-color : Colors.text-light; wrap: word-wrap; width: parent.width * 31 / 32; } @@ -218,7 +219,7 @@ export component StoryView { font-size: parent.height * 2 / 15; font-weight: root.is-bold ? 700 : 400; font-family: root.dialogue-font; - color: Colors.text-light; + color: root.is-read ? Colors.read-color : Colors.text-light; wrap: word-wrap; width: parent.width * 31 / 32; } @@ -286,11 +287,7 @@ export component StoryView { y: 0; text: "自动"; is-on: is-auto; - clicked => { - is-auto = !is-auto; - is-skip = false; - root.auto-play(true); - } + clicked => { root.auto-play(is-auto); } } CustomRoundButton { @@ -300,11 +297,7 @@ export component StoryView { y: 0; text: "快进"; is-on: is-skip; - clicked => { - is-skip = !is-skip; - is-auto = false; - root.skip-play(true); - } + clicked => { root.skip-play(is-skip); } } CustomRoundButton { @@ -315,8 +308,6 @@ export component StoryView { text: "履历"; clicked => { is-backlog = !is-backlog; - is-auto = false; - is-skip = false; root.backlog(); } } diff --git a/ui/main_window.slint b/ui/main_window.slint index ce450a0..0f7cb01 100644 --- a/ui/main_window.slint +++ b/ui/main_window.slint @@ -25,6 +25,7 @@ export component MainWindow inherits Window { in-out property is_backlog: false; in-out property is-auto; in-out property is-wait; + in-out property skip-conf; in-out property delay; in-out property text-speed; in-out property is-skip; @@ -36,6 +37,7 @@ export component MainWindow inherits Window { in-out property is-bold; in-out property show-shadow; in-out property dialogue-font; + in-out property is-read; in property <[string]> font-list; in-out property current-screen: 0; @@ -55,8 +57,6 @@ export component MainWindow inherits Window { out property container-height: container-size * 9 / 16; function esc_story(screen: int) { - root.auto-play(false); - root.skip-play(false); root.last-screen = 2; root.current-screen = screen; } @@ -139,6 +139,7 @@ export component MainWindow inherits Window { voice-volume <=> root.voice-volume; character_volumes <=> root.character_volumes; is-wait <=> root.is-wait; + skip-conf <=> root.skip-conf; delay <=> root.delay; dialogue-opacity <=> root.dialogue-opacity; is-bold <=> root.is-bold; @@ -172,6 +173,7 @@ export component MainWindow inherits Window { is-bold: root.is-bold; show-shadow: root.show-shadow; dialogue-font: root.dialogue-font; + is-read: root.is-read; is-video <=> root.is-video; video-frame: root.video-frame; @@ -182,19 +184,9 @@ export component MainWindow inherits Window { is-auto <=> root.is-auto; is-skip <=> root.is-skip; - auto-play(b) => { - root.auto-play(b); - root.skip-play(false); - } - skip-play(b) => { - root.skip-play(b); - root.auto-play(false); - } - backlog => { - root.auto-play(false); - root.skip-play(false); - root.backlog(); - } + auto-play(b) => { root.auto-play(b); } + skip-play(b) => { root.skip-play(b); } + backlog => { root.backlog(); } backlog-change(i) => { root.backlog-change(i); } backlog-jump(s, i) => { root.backlog-jump(s, i); } backlog-replay(n, v) => { root.backlog-replay(n, v) } @@ -203,12 +195,15 @@ export component MainWindow inherits Window { replay-voice => { root.replay-voice(); } save-game => { esc_story(3); + root.normal-play(); } load-game => { esc_story(4); + root.normal-play(); } settings => { esc_story(1); + root.normal-play(); } } @@ -243,6 +238,7 @@ export component MainWindow inherits Window { callback auto-play(bool); callback skip-play(bool); + callback normal-play(); callback clicked(); callback toggle-fullscreen(); callback volume-changed(); diff --git a/ui/styles/colors.slint b/ui/styles/colors.slint index 49f052e..c04e492 100644 --- a/ui/styles/colors.slint +++ b/ui/styles/colors.slint @@ -8,4 +8,5 @@ export global Colors { out property slider-background: #ddd; out property border-color: #ccc; out property light-purple: #E0D0FF; + out property read-color: yellow; } \ No newline at end of file