From fcba640c4aba3d6cb67f582b87ef313b41166838 Mon Sep 17 00:00:00 2001 From: Yuyi Wang Date: Sat, 10 Dec 2022 11:19:35 +0800 Subject: [PATCH] Decouple settings manager. --- bins/Cargo.lock | 2 +- bins/ayaka-gui/src-tauri/Cargo.toml | 1 + bins/ayaka-gui/src-tauri/src/main.rs | 44 ++++---- bins/ayaka-gui/src-tauri/src/settings.rs | 84 ++++++++++++++++ utils/ayaka-runtime/Cargo.toml | 1 - utils/ayaka-runtime/src/context.rs | 1 + utils/ayaka-runtime/src/lib.rs | 3 +- utils/ayaka-runtime/src/settings.rs | 123 ++++++++++------------- 8 files changed, 164 insertions(+), 95 deletions(-) create mode 100644 bins/ayaka-gui/src-tauri/src/settings.rs diff --git a/bins/Cargo.lock b/bins/Cargo.lock index 9175e199..f12b3a0f 100644 --- a/bins/Cargo.lock +++ b/bins/Cargo.lock @@ -397,6 +397,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-window-state", + "tryiterator", "trylog", ] @@ -456,7 +457,6 @@ dependencies = [ "ayaka-plugin-wasmtime", "ayaka-primitive", "cfg-if", - "dirs", "fallback", "futures-util", "icu_locid", diff --git a/bins/ayaka-gui/src-tauri/Cargo.toml b/bins/ayaka-gui/src-tauri/Cargo.toml index 7ceb4e14..df9858cd 100644 --- a/bins/ayaka-gui/src-tauri/Cargo.toml +++ b/bins/ayaka-gui/src-tauri/Cargo.toml @@ -17,6 +17,7 @@ actix-web = "4" actix-files = "0.6" actix-cors = "0.6" trylog = "0.3" +tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" } [features] default = [ "custom-protocol" ] diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs index 13fd281e..47512cbd 100644 --- a/bins/ayaka-gui/src-tauri/src/main.rs +++ b/bins/ayaka-gui/src-tauri/src/main.rs @@ -3,23 +3,28 @@ windows_subsystem = "windows" )] #![feature(once_cell)] +#![feature(type_alias_impl_trait)] mod asset_resolver; +mod settings; use ayaka_runtime::{ anyhow::{self, Result}, log::{debug, info}, + settings::*, *, }; use flexi_logger::{FileSpec, LogSpecification, Logger}; use serde::{Deserialize, Serialize}; +use settings::*; use std::{ collections::{HashMap, HashSet}, fmt::Display, net::TcpListener, }; use tauri::{ - async_runtime::Mutex, command, utils::config::AppUrl, AppHandle, Manager, State, WindowUrl, + async_runtime::Mutex, command, utils::config::AppUrl, AppHandle, Manager, PathResolver, State, + WindowUrl, }; use trylog::macros::*; @@ -72,9 +77,9 @@ impl OpenGameStatus { #[derive(Default)] struct Storage { - ident: String, config: String, dist_port: u16, + manager: FileSettingsManager, records: Mutex>, context: Mutex>, current: Mutex>, @@ -83,11 +88,11 @@ struct Storage { } impl Storage { - pub fn new(ident: impl Into, config: impl Into, dist_port: u16) -> Self { + pub fn new(resolver: &PathResolver, config: impl Into, dist_port: u16) -> Self { Self { - ident: ident.into(), config: config.into(), dist_port, + manager: FileSettingsManager::new(resolver), ..Default::default() } } @@ -140,20 +145,20 @@ async fn open_game(handle: AppHandle, storage: State<'_, Storage>) -> CommandRes window.set_title(&ctx.game.config.title)?; let settings = { OpenGameStatus::LoadSettings.emit(&handle)?; - unwrap_or_default_log!(load_settings(&storage.ident), "Load settings failed") + unwrap_or_default_log!(storage.manager.load_settings(), "Load settings failed") }; *storage.settings.lock().await = Some(settings); OpenGameStatus::LoadGlobalRecords.emit(&handle)?; let global_record = unwrap_or_default_log!( - load_global_record(&storage.ident, &ctx.game.config.title), + storage.manager.load_global_record(&ctx.game.config.title), "Load global records failed" ); *storage.global_record.lock().await = Some(global_record); OpenGameStatus::LoadRecords.emit(&handle)?; *storage.records.lock().await = unwrap_or_default_log!( - load_records(&storage.ident, &ctx.game.config.title), + storage.manager.load_records(&ctx.game.config.title), "Load records failed" ); *storage.context.lock().await = Some(ctx); @@ -215,16 +220,15 @@ async fn save_record_to(index: usize, storage: State<'_, Storage>) -> CommandRes async fn save_all(storage: State<'_, Storage>) -> CommandResult<()> { let context = storage.context.lock().await; let game = &context.as_ref().unwrap().game.config.title; - save_settings( - &storage.ident, - storage.settings.lock().await.as_ref().unwrap(), - )?; - save_global_record( - &storage.ident, - game, - storage.global_record.lock().await.as_ref().unwrap(), - )?; - save_records(&storage.ident, game, &storage.records.lock().await)?; + storage + .manager + .save_settings(storage.settings.lock().await.as_ref().unwrap())?; + storage + .manager + .save_global_record(game, storage.global_record.lock().await.as_ref().unwrap())?; + storage + .manager + .save_records(game, &storage.records.lock().await)?; Ok(()) } @@ -433,7 +437,7 @@ fn main() -> Result<()> { .plugin(asset_resolver::init(listener)) .plugin(tauri_plugin_window_state::Builder::default().build()) .setup(move |app| { - let ident = app.config().tauri.bundle.identifier.clone(); + let resolver = app.path_resolver(); let spec = LogSpecification::parse("warn,ayaka=debug")?; let log_handle = if cfg!(debug_assertions) { Logger::with(spec) @@ -445,7 +449,7 @@ fn main() -> Result<()> { Logger::with(spec) .log_to_file( FileSpec::default() - .directory(app.path_resolver().app_log_dir().unwrap()) + .directory(resolver.app_log_dir().unwrap()) .basename("ayaka-gui"), ) .use_utc() @@ -477,7 +481,7 @@ fn main() -> Result<()> { .unwrap() .to_path_buf(); asset_resolver::ROOT_PATH.set(root_path).unwrap(); - app.manage(Storage::new(ident, config, port)); + app.manage(Storage::new(&resolver, config, port)); Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/bins/ayaka-gui/src-tauri/src/settings.rs b/bins/ayaka-gui/src-tauri/src/settings.rs new file mode 100644 index 00000000..8791d11f --- /dev/null +++ b/bins/ayaka-gui/src-tauri/src/settings.rs @@ -0,0 +1,84 @@ +use ayaka_runtime::{ + anyhow::{self, Result}, + settings::*, +}; +use serde::{de::DeserializeOwned, Serialize}; +use std::path::{Path, PathBuf}; +use tauri::PathResolver; +use tryiterator::TryIteratorExt; + +#[derive(Default)] +pub struct FileSettingsManager { + local_data_dir: PathBuf, + config_dir: PathBuf, +} + +impl FileSettingsManager { + pub fn new(resolver: &PathResolver) -> Self { + Self { + local_data_dir: resolver.app_local_data_dir().unwrap(), + config_dir: resolver.app_config_dir().unwrap(), + } + } + + fn records_path_root(&self, game: &str) -> PathBuf { + self.local_data_dir.join("save").join(game) + } +} + +impl SettingsManager for FileSettingsManager { + fn load_file(&self, path: impl AsRef) -> Result { + let buffer = std::fs::read(path)?; + Ok(serde_json::from_slice(&buffer)?) + } + + fn save_file( + &self, + path: impl AsRef, + data: &T, + pretty: bool, + ) -> Result<()> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let buffer = if pretty { + serde_json::to_vec_pretty(data) + } else { + serde_json::to_vec(data) + }?; + std::fs::write(path, buffer)?; + Ok(()) + } + + fn settings_path(&self) -> Result { + Ok(self.config_dir.join("settings.json")) + } + + fn global_record_path(&self, game: &str) -> Result { + Ok(self.records_path_root(game).join("global.json")) + } + + type RecordsPathIter = impl Iterator>; + + fn records_path(&self, game: &str) -> Result { + let ctx_path = self.records_path_root(game); + Ok(std::fs::read_dir(ctx_path)? + .map_err(anyhow::Error::from) + .try_filter_map(|entry| { + let p = entry.path(); + if p.is_file() && p.file_name().unwrap_or_default() != "global.json" { + Ok(Some(p)) + } else { + Ok(None) + } + })) + } + + fn record_path(&self, game: &str, i: usize) -> Result { + Ok(self + .records_path_root(game) + .join(i.to_string()) + .with_extension("json")) + } +} diff --git a/utils/ayaka-runtime/Cargo.toml b/utils/ayaka-runtime/Cargo.toml index 85cdb6cf..dc98773e 100644 --- a/utils/ayaka-runtime/Cargo.toml +++ b/utils/ayaka-runtime/Cargo.toml @@ -18,7 +18,6 @@ anyhow = { workspace = true } stream-future = "0.3" futures-util = "0.3" tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" } -dirs = "4.0" log = { workspace = true } trylog = { workspace = true } cfg-if = "1.0" diff --git a/utils/ayaka-runtime/src/context.rs b/utils/ayaka-runtime/src/context.rs index fcf3d5a0..dd54cbef 100644 --- a/utils/ayaka-runtime/src/context.rs +++ b/utils/ayaka-runtime/src/context.rs @@ -1,5 +1,6 @@ use crate::{ plugin::{LoadStatus, Runtime}, + settings::*, *, }; use anyhow::{anyhow, bail, Result}; diff --git a/utils/ayaka-runtime/src/lib.rs b/utils/ayaka-runtime/src/lib.rs index c573893f..42c69211 100644 --- a/utils/ayaka-runtime/src/lib.rs +++ b/utils/ayaka-runtime/src/lib.rs @@ -13,7 +13,7 @@ mod config; mod context; mod locale; pub mod plugin; -mod settings; +pub mod settings; #[doc(no_inline)] pub use anyhow; @@ -29,7 +29,6 @@ pub use futures_util::{pin_mut, StreamExt, TryStreamExt}; pub use locale::*; #[doc(no_inline)] pub use log; -pub use settings::*; /// Get the version of Ayaka runtime. /// This version string is exacted from `CARGO_PKG_VERSION`. diff --git a/utils/ayaka-runtime/src/settings.rs b/utils/ayaka-runtime/src/settings.rs index 12f01faa..23d25fab 100644 --- a/utils/ayaka-runtime/src/settings.rs +++ b/utils/ayaka-runtime/src/settings.rs @@ -1,7 +1,8 @@ +//! Settings of the runtime. + use crate::*; -use anyhow::{anyhow, Result}; +use anyhow::Result; use ayaka_bindings_types::RawContext; -use dirs::{config_dir, data_local_dir}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ collections::HashMap, @@ -71,85 +72,65 @@ impl ActionRecord { } } -fn load_file(path: impl AsRef) -> Result { - let buffer = std::fs::read(path)?; - Ok(serde_json::from_slice(&buffer)?) -} +/// A settings manager trait. +/// +/// This type should handle the file loading and saving, +/// and manage the paths of the files. +pub trait SettingsManager { + /// Load a file from specified path. + fn load_file(&self, path: impl AsRef) -> Result; + + /// Save a file to specified path. + fn save_file(&self, path: impl AsRef, data: &T, pretty: bool) + -> Result<()>; -fn save_file(data: &T, path: impl AsRef, pretty: bool) -> Result<()> { - let path = path.as_ref(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; + /// Get the settings path. + fn settings_path(&self) -> Result; + + /// Load [`Settings`]. + fn load_settings(&self) -> Result { + self.load_file(self.settings_path()?) } - let buffer = if pretty { - serde_json::to_vec_pretty(data) - } else { - serde_json::to_vec(data) - }?; - std::fs::write(path, buffer)?; - Ok(()) -} -fn settings_path(ident: &str) -> Result { - let path = config_dir().ok_or_else(|| anyhow!("Cannot find config path"))?; - Ok(path.join(ident).join("settings.json")) -} + /// Save [`Settings`]. + fn save_settings(&self, data: &Settings) -> Result<()> { + self.save_file(self.settings_path()?, data, true) + } -/// Load settings from JSON file. -pub fn load_settings(ident: &str) -> Result { - load_file(settings_path(ident)?) -} + /// Get the global record path. + fn global_record_path(&self, game: &str) -> Result; -/// Save settings into pretty JSON file. -pub fn save_settings(ident: &str, data: &Settings) -> Result<()> { - save_file(data, settings_path(ident)?, true) -} + /// Load [`GlobalRecord`]. + fn load_global_record(&self, game: &str) -> Result { + self.load_file(self.global_record_path(game)?) + } -fn records_path(ident: &str, game: &str) -> Result { - let path = data_local_dir().ok_or_else(|| anyhow!("Cannot find config path"))?; - Ok(path.join(ident).join("save").join(game)) -} + /// Save [`GlobalRecord`]. + fn save_global_record(&self, game: &str, data: &GlobalRecord) -> Result<()> { + self.save_file(self.global_record_path(game)?, data, false) + } -fn global_record_path(ident: &str, game: &str) -> Result { - Ok(records_path(ident, game)?.join("global.json")) -} + #[doc(hidden)] + type RecordsPathIter: Iterator>; -/// Load [`GlobalRecord`] from the records folder. -pub fn load_global_record(ident: &str, game: &str) -> Result { - load_file(global_record_path(ident, game)?) -} + /// Get an iterator of record paths. + fn records_path(&self, game: &str) -> Result; -/// Save [`GlobalRecord`] into the records folder. -pub fn save_global_record(ident: &str, game: &str, data: &GlobalRecord) -> Result<()> { - save_file(data, global_record_path(ident, game)?, false) -} + /// Get the record path from index. + fn record_path(&self, game: &str, i: usize) -> Result; -/// Load all [`ActionRecord`] from the records folder. -pub fn load_records(ident: &str, game: &str) -> Result> { - let ctx_path = records_path(ident, game)?; - let contexts = std::fs::read_dir(ctx_path)? - .map_err(anyhow::Error::from) - .try_filter_map(|entry| { - let p = entry.path(); - if p.is_file() && p.file_name().unwrap_or_default() != "global.json" { - Ok(Some(load_file(&p)?)) - } else { - Ok(None) - } - }) - .try_collect()?; - Ok(contexts) -} + /// Load all [`ActionRecord`]. + fn load_records(&self, game: &str) -> Result> { + self.records_path(game)? + .try_filter_map(|path| Ok(Some(self.load_file(path)?))) + .try_collect() + } -/// Save all [`ActionRecord`] into the records folder. -pub fn save_records(ident: &str, game: &str, contexts: &[ActionRecord]) -> Result<()> { - let ctx_path = records_path(ident, game)?; - for (i, ctx) in contexts.iter().enumerate() { - save_file( - ctx, - ctx_path.join(i.to_string()).with_extension("json"), - false, - )?; + /// Save all [`ActionRecord`]. + fn save_records(&self, game: &str, contexts: &[ActionRecord]) -> Result<()> { + for (i, ctx) in contexts.iter().enumerate() { + self.save_file(self.record_path(game, i)?, ctx, false)?; + } + Ok(()) } - Ok(()) }