From 27cb9fa32fbe66c030b1ec49af1756364f50b9b0 Mon Sep 17 00:00:00 2001 From: fromost Date: Sun, 14 Dec 2025 21:34:06 +0800 Subject: [PATCH] Refactor structure --- Cargo.toml | 69 ++---- crawler/Cargo.toml | 23 ++ {src/crawler => crawler/src}/dlsite.rs | 45 +--- src/crawler/mod.rs => crawler/src/lib.rs | 14 +- db/Cargo.toml | 13 ++ src/helpers/db.rs => db/src/lib.rs | 113 +++++++-- db/src/types.rs | 17 ++ models/Cargo.toml | 16 ++ models/src/config.rs | 72 ++++++ models/src/dlsite/category.rs | 55 +++++ models/src/dlsite/crawler.rs | 39 ++++ models/src/dlsite/genre.rs | 26 +++ models/src/dlsite/maniax.rs | 59 +++++ {src/models => models/src/dlsite}/mod.rs | 29 +-- models/src/dlsite/translation.rs | 56 +++++ models/src/lib.rs | 24 ++ scripts/deps.sh | 3 + src/constants.rs | 57 ----- src/main.rs | 23 +- src/models/game.rs | 219 ------------------ ui/Cargo.toml | 43 ++++ {src => ui/src}/app.rs | 15 +- {src => ui/src}/cli/folder.rs | 2 +- {src => ui/src}/cli/mod.rs | 12 +- {src => ui/src}/cli/sync.rs | 48 ++-- {src => ui/src}/event.rs | 10 +- {src => ui/src}/helpers/mod.rs | 15 +- ui/src/lib.rs | 8 + ui/src/models/game_list.rs | 33 +++ ui/src/models/mod.rs | 2 + {src => ui/src}/widgets/components/mod.rs | 0 .../src}/widgets/components/textarea.rs | 0 {src => ui/src}/widgets/mod.rs | 0 {src => ui/src}/widgets/popups/folder.rs | 0 {src => ui/src}/widgets/popups/mod.rs | 3 +- {src => ui/src}/widgets/views/main_view.rs | 34 ++- {src => ui/src}/widgets/views/mod.rs | 1 + 37 files changed, 712 insertions(+), 486 deletions(-) create mode 100755 crawler/Cargo.toml rename {src/crawler => crawler/src}/dlsite.rs (86%) rename src/crawler/mod.rs => crawler/src/lib.rs (94%) create mode 100755 db/Cargo.toml rename src/helpers/db.rs => db/src/lib.rs (53%) create mode 100755 db/src/types.rs create mode 100755 models/Cargo.toml create mode 100755 models/src/config.rs create mode 100755 models/src/dlsite/category.rs create mode 100755 models/src/dlsite/crawler.rs create mode 100755 models/src/dlsite/genre.rs create mode 100755 models/src/dlsite/maniax.rs rename {src/models => models/src/dlsite}/mod.rs (52%) create mode 100755 models/src/dlsite/translation.rs create mode 100755 models/src/lib.rs create mode 100755 scripts/deps.sh delete mode 100755 src/constants.rs delete mode 100755 src/models/game.rs create mode 100755 ui/Cargo.toml rename {src => ui/src}/app.rs (80%) rename {src => ui/src}/cli/folder.rs (97%) rename {src => ui/src}/cli/mod.rs (83%) rename {src => ui/src}/cli/sync.rs (82%) rename {src => ui/src}/event.rs (86%) rename {src => ui/src}/helpers/mod.rs (71%) create mode 100755 ui/src/lib.rs create mode 100755 ui/src/models/game_list.rs create mode 100755 ui/src/models/mod.rs rename {src => ui/src}/widgets/components/mod.rs (100%) rename {src => ui/src}/widgets/components/textarea.rs (100%) rename {src => ui/src}/widgets/mod.rs (100%) rename {src => ui/src}/widgets/popups/folder.rs (100%) rename {src => ui/src}/widgets/popups/mod.rs (66%) rename {src => ui/src}/widgets/views/main_view.rs (88%) rename {src => ui/src}/widgets/views/mod.rs (96%) diff --git a/Cargo.toml b/Cargo.toml index ef47d22..7d111d6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "sus-manager" version = "0.1.0" description = "manager for SuS gamers" +repository = "https://git.ouoweb.xyz/fromost/sus-manager" authors = ["fromost"] license = "MIT" edition = "2024" @@ -17,60 +18,24 @@ opt-level = 0 lto = "fat" opt-level = 3 -[dependencies] -color-eyre = "0.6.3" -futures = "0.3.28" +[workspace] +resolver = "3" +members = ["crawler","db","models","ui"] + +[workspace.dependencies] directories = "6.0.0" +tokio = { version = "1.48.0", features = ["full"] } lazy_static = "1.5.0" -robotstxt = "0.3.0" -scraper = "0.24.0" -rat-cursor = "1.2.1" +color-eyre = { version = "0.6.5" } +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" -image = "0.25.8" -log = "0.4.28" -num_cpus = "1.17.0" -sys-locale = "0.3.2" -jemallocator = "0.5.4" -itertools = "0.14.0" +log = "0.4.29" +ratatui = { version = "0.29.0", features = ["all-widgets"] } dashmap = { version = "6.1.0", features = ["serde"] } -[dependencies.language-tags] -version = "0.3.2" -features = ["serde"] - -[dependencies.indicatif] -version = "0.18.1" -features = ["futures", "tokio"] - -[dependencies.rocksdb] -version = "0.24.0" -features = ["multi-threaded-cf"] - -[dependencies.serde] -version = "1.0.228" -features = ["derive"] - -[dependencies.tui-input] -version = "0.14.0" -features = ["crossterm"] -default-features = false - -[dependencies.crossterm] -version = "0.29.0" -features = ["event-stream"] - -[dependencies.ratatui] -version = "0.29.0" -features = ["all-widgets"] - -[dependencies.clap] -version = "4.5.48" -features = ["derive", "cargo"] - -[dependencies.reqwest] -version = "0.12.23" -features = ["blocking", "json", "rustls-tls"] - -[dependencies.tokio] -version = "1.47.1" -features = ["full"] +[dependencies] +color-eyre = "0.6.5" +jemallocator = "0.5.4" +tokio = { version = "1.48.0", features = ["macros"] } +clap_builder = "4.5.53" +ui = { path = "./ui" } diff --git a/crawler/Cargo.toml b/crawler/Cargo.toml new file mode 100755 index 0000000..ce9475e --- /dev/null +++ b/crawler/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "crawler" +version = "0.1.0" +edition = "2024" + +[dependencies] +image = "0.25.9" +reqwest = { version = "0.12.25", features = ["json"] } +scraper = "0.25.0" +robotstxt = "0.3.0" + +models = { path = "../models" } + +tokio.workspace = true +serde.workspace = true +color-eyre.workspace = true +lazy_static.workspace = true +serde_json.workspace = true + +futures = "0.3.31" +itertools = "0.14.0" +language-tags = "0.3.2" + diff --git a/src/crawler/dlsite.rs b/crawler/src/dlsite.rs similarity index 86% rename from src/crawler/dlsite.rs rename to crawler/src/dlsite.rs index 3df80fa..a16e372 100755 --- a/src/crawler/dlsite.rs +++ b/crawler/src/dlsite.rs @@ -10,12 +10,11 @@ use itertools::Itertools; use language_tags::LanguageTag; use lazy_static::lazy_static; use scraper::{Element, Html, Selector}; -use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::constants::{APP_DATA_DIR, JP_LOCALE}; -use crate::crawler::Crawler; -use crate::helpers::matches_primary_language; -use crate::models::{PrimaryLanguage}; +use models::APP_DATA_DIR; +use models::dlsite::{matches_primary_language, PrimaryLanguage, JP_LOCALE}; +use super::Crawler; +use models::dlsite::crawler::*; //TODO: override locale with user one const DLSITE_URL: &str = "https://www.dlsite.com/"; @@ -31,42 +30,6 @@ pub struct DLSiteCrawler { crawler: Crawler } -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct DLSiteManiax { - #[serde(rename = "work_name")] - pub(crate) title: String, - #[serde(rename = "work_image")] - pub(crate) work_image_url: String, - #[serde(rename = "dl_count")] - pub(crate) sells_count: u32, - #[serde(skip)] - pub(crate) genre_ids: Vec, - #[serde(skip)] - pub(crate) rj_num: String, - #[serde(skip)] - pub(crate) folder_path: PathBuf, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct DLSiteFilter { - pub(crate) genre_all: Value -} - - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct DLSiteGenreCategory { - pub(crate) category_name: String, - pub(crate) values: Vec, - #[serde(skip)] - pub(crate) id: u8 -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct DLSiteGenre { - pub(crate) value: String, - pub(crate) name: String -} - impl DLSiteCrawler { pub fn new() -> Result { let url = Url::parse(DLSITE_URL)?; diff --git a/src/crawler/mod.rs b/crawler/src/lib.rs similarity index 94% rename from src/crawler/mod.rs rename to crawler/src/lib.rs index 577ec3a..6f89ec0 100755 --- a/src/crawler/mod.rs +++ b/crawler/src/lib.rs @@ -1,8 +1,6 @@ -pub mod dlsite; -pub use dlsite::*; +mod dlsite; use color_eyre::eyre::eyre; -use crate::constants::APP_CACHE_PATH; use color_eyre::Result; use image::DynamicImage; use reqwest::{Client, StatusCode, Url}; @@ -10,6 +8,8 @@ use robotstxt::DefaultMatcher; use scraper::Html; use serde::de::DeserializeOwned; +pub use dlsite::*; + #[derive(Clone, Debug)] struct Crawler { id: String, @@ -47,7 +47,7 @@ impl Crawler { return Ok(txt.clone()); } - let local_robots_path = APP_CACHE_PATH.clone().join(&self.id).join("robots.txt"); + let local_robots_path = models::APP_CACHE_PATH.clone().join(&self.id).join("robots.txt"); if !local_robots_path.exists() { let mut robots_url = self.base_url.clone(); robots_url.set_path("/robots.txt"); @@ -56,7 +56,7 @@ impl Crawler { "Failed to get robots.txt in `{}/robots.txt`", self.base_url.as_str() ) - .as_str(), + .as_str(), ); let content = response.text().await?; tokio::fs::create_dir_all(local_robots_path.parent().unwrap()).await?; @@ -79,7 +79,7 @@ impl Crawler { } pub async fn get_json(&self, path: &str, query: Option<&str>) -> Result<(T, StatusCode)> - where T : DeserializeOwned { + where T : DeserializeOwned { let mut url = self.base_url.clone(); url.set_path(path); url.set_query(query); @@ -107,5 +107,5 @@ impl Crawler { let status = res.status(); let bytes = res.bytes().await?; Ok((bytes.to_vec(), status)) - } + } } diff --git a/db/Cargo.toml b/db/Cargo.toml new file mode 100755 index 0000000..2a22134 --- /dev/null +++ b/db/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "db" +version = "0.1.0" +edition = "2024" + +[dependencies] +rocksdb = "0.24.0" +num_cpus = "1.17.0" +lazy_static.workspace = true +color-eyre.workspace = true +serde.workspace = true +serde_json.workspace = true +directories.workspace = true diff --git a/src/helpers/db.rs b/db/src/lib.rs similarity index 53% rename from src/helpers/db.rs rename to db/src/lib.rs index 26df820..b6a96b7 100755 --- a/src/helpers/db.rs +++ b/db/src/lib.rs @@ -1,38 +1,105 @@ -use crate::constants::{APP_DB_DATA_DIR, DB_CF_OPTIONS, DB_COLUMNS, DB_OPTIONS}; +pub mod types; + +use std::path::PathBuf; +use std::sync::Arc; use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options}; use serde::{Serialize}; use serde::de::DeserializeOwned; -use crate::models::{RocksColumn, RocksReference, RocksReferences}; +use crate::types::{RocksColumn, RocksReference, RocksReferences}; use color_eyre::Result; +use directories::BaseDirs; +use lazy_static::lazy_static; -pub struct RocksDB { - db: OptimisticTransactionDB, +const APP_DIR_NAME: &str = "sus_manager"; +lazy_static! { + static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap(); + static ref APP_DB_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf().join(APP_DIR_NAME).join("db"); } -impl Default for RocksDB { - fn default() -> Self { - RocksDB::new(DB_OPTIONS.clone(), DB_CF_OPTIONS.clone()).unwrap() +fn get_db_options() -> Options { + let mut opts = Options::default(); + + opts.create_missing_column_families(true); + opts.create_if_missing(true); + opts.increase_parallelism(num_cpus::get() as i32); + + opts +} + +fn get_db_read_options() -> rocksdb::ReadOptions { + let mut opts = rocksdb::ReadOptions::default(); + opts.set_async_io(true); + opts +} + +#[derive(Clone)] +pub struct RocksDBFactory { + cfs: Vec, + path: PathBuf, + db_opts: Options, + cf_opts: Options, + context: Option +} + +impl RocksDBFactory { + pub fn new(path: PathBuf, db_opts: Options, cf_opts: Options) -> Result { + let instance = Self { + cfs: vec![], + path, + db_opts, + cf_opts, + context: None + }; + if !instance.path.exists() { + std::fs::create_dir_all(instance.path.as_path())?; + } + Ok(instance) + } + + pub fn register(&mut self) where T: RocksColumn { + self.cfs.push(T::get_column_name()); + } + + pub fn get_current_context(&mut self) -> Result { + if let Some(context) = &self.context { + return Ok(context.clone()); + } + let cfs = self.cfs + .iter() + .map(|cf| ColumnFamilyDescriptor::new(cf, self.cf_opts.clone())) + .collect::>(); + let context = RocksDB::new(cfs, self.path.clone(), self.db_opts.clone())?; + self.context = Some(context.clone()); + Ok(context) } } +impl Default for RocksDBFactory { + fn default() -> Self { + Self::new(APP_DB_DATA_DIR.to_path_buf(), get_db_options(), Options::default()).unwrap() + } +} + +#[derive(Clone)] +pub struct RocksDB { + db: Arc, +} + impl RocksDB { - pub fn new(db_opts: Options, cf_opts: Options) -> Result { - let cfs = DB_COLUMNS.iter() - .map(|cf| ColumnFamilyDescriptor::new(cf.to_string(), cf_opts.clone())) - .collect::>(); + pub fn new(cfs: Vec, path: PathBuf, db_opts: Options) -> Result { let db = OptimisticTransactionDB::open_cf_descriptors( &db_opts, - APP_DB_DATA_DIR.as_path(), + path.as_path(), cfs )?; let rocks = Self { - db + db: Arc::new(db) }; Ok(rocks) } pub fn get_value(&self, id: &TColumn::Id) -> Result> - where TColumn: RocksColumn + DeserializeOwned + where TColumn: RocksColumn + DeserializeOwned { let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap(); let query_res = self.db.get_cf(&cf, serde_json::to_string(id)?)?; @@ -45,7 +112,7 @@ impl RocksDB { } pub fn set_value(&self, value: &TColumn) -> Result<()> - where TColumn: RocksColumn + Serialize + where TColumn: RocksColumn + Serialize { let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap(); self.db.put_cf(&cf, serde_json::to_string(&value.get_id())?, serde_json::to_string(value)?)?; @@ -53,7 +120,7 @@ impl RocksDB { } pub fn get_values(&self, ids: &[TColumn::Id]) -> Result> - where TColumn: RocksColumn + DeserializeOwned + where TColumn: RocksColumn + DeserializeOwned { let transaction = self.db.transaction(); let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap(); @@ -70,8 +137,8 @@ impl RocksDB { } pub fn get_reference_value(&self, id: &TReference::Id) -> Result> - where TReference: RocksColumn + DeserializeOwned, - TColumn: RocksColumn + RocksReference + where TReference: RocksColumn + DeserializeOwned, + TColumn: RocksColumn + RocksReference { let reference = self.get_value::(id)?; if reference.is_none() { @@ -81,17 +148,17 @@ impl RocksDB { } pub fn get_reference_values(&self, ids: &[TReference::Id]) -> Result> - where TReference: RocksColumn + DeserializeOwned, - TColumn: RocksColumn + RocksReferences + where TReference: RocksColumn + DeserializeOwned, + TColumn: RocksColumn + RocksReferences { self.get_values::(ids) } pub fn get_all_values(&self) -> Result> - where TColumn: RocksColumn + DeserializeOwned + where TColumn: RocksColumn + DeserializeOwned { let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap(); - let values = self.db.iterator_cf_opt(&cf, crate::constants::get_db_read_options(), IteratorMode::Start) + let values = self.db.iterator_cf_opt(&cf, get_db_read_options(), IteratorMode::Start) .filter_map(Result::ok) .map(|(k, v)| { let id = serde_json::from_slice::(&k).unwrap(); @@ -104,7 +171,7 @@ impl RocksDB { } pub fn set_values(&mut self, values: &[TColumn]) -> Result<()> - where TColumn: RocksColumn + Serialize + where TColumn: RocksColumn + Serialize { let transaction = self.db.transaction(); let cf = self.db.cf_handle(TColumn::get_column_name().as_str()).unwrap(); diff --git a/db/src/types.rs b/db/src/types.rs new file mode 100755 index 0000000..69fb16b --- /dev/null +++ b/db/src/types.rs @@ -0,0 +1,17 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub trait RocksColumn { + type Id: Serialize + DeserializeOwned + Clone; + fn get_id(&self) -> Self::Id; + fn set_id(&mut self, id: Self::Id); + fn get_column_name() -> String; +} + +pub trait RocksReference where T: RocksColumn { + fn get_reference_id(&self) -> T::Id; +} + +pub trait RocksReferences where T: RocksColumn { + fn get_reference_ids(&self) -> Vec; +} \ No newline at end of file diff --git a/models/Cargo.toml b/models/Cargo.toml new file mode 100755 index 0000000..277db78 --- /dev/null +++ b/models/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "models" +version = "0.1.0" +edition = "2024" + +[dependencies] +directories.workspace = true +color-eyre.workspace = true +serde.workspace = true +lazy_static.workspace = true +ratatui.workspace = true +serde_json.workspace = true +dashmap.workspace = true +db = { path = "../db" } +language-tags = { version = "0.3.2", features = ["serde"] } +sys-locale = "0.3.2" diff --git a/models/src/config.rs b/models/src/config.rs new file mode 100755 index 0000000..a5d28b4 --- /dev/null +++ b/models/src/config.rs @@ -0,0 +1,72 @@ +use std::path::PathBuf; +use language_tags::LanguageTag; +use serde::{Deserialize, Serialize}; +use color_eyre::Result; +use crate::{APP_CONIFG_FILE_PATH, CACHE_MAP}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ApplicationConfig { + pub basic_config: BasicConfig, + pub path_config: PathConfig, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BasicConfig { + pub locale: LanguageTag, + pub tick_rate: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PathConfig { + pub dlsite_paths: Vec +} + +const CONFIG_KEY: &str = "app_conf"; + +impl ApplicationConfig { + pub fn get_config() -> Result { + if CACHE_MAP.contains_key(CONFIG_KEY) && + let Some(cached_config) = CACHE_MAP.get(CONFIG_KEY) + { + Ok(serde_json::from_value(cached_config.clone())?) + } else if APP_CONIFG_FILE_PATH.exists() { + ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH) + } else { + ApplicationConfig::new() + } + } + + fn from_file(path: &PathBuf) -> Result { + let reader = std::fs::File::open(path)?; + let result: serde_json::Value = serde_json::from_reader(reader)?; + CACHE_MAP.insert(CONFIG_KEY.to_string(), result.clone()); + Ok(serde_json::from_value(result)?) + } + + fn new() -> Result { + let default_locale = sys_locale::get_locale().unwrap_or(String::from("ja-JP")); + let conf = Self { + basic_config: BasicConfig { + tick_rate: 250, + locale: LanguageTag::parse(&default_locale)?, + }, + path_config: PathConfig { + dlsite_paths: vec![], + }, + }; + conf.clone().write_to_file(APP_CONIFG_FILE_PATH.to_path_buf())?; + Ok(conf) + } + + fn write_to_file(self, path: PathBuf) -> Result<()> { + let writer = std::fs::File::create(path)?; + serde_json::to_writer_pretty(writer, &self)?; + Ok(()) + } + + pub fn save(self) -> Result<()> { + let current_value = serde_json::to_value(&self)?; + CACHE_MAP.alter(CONFIG_KEY, |_, _| current_value); + self.write_to_file(APP_CONIFG_FILE_PATH.to_path_buf()) + } +} \ No newline at end of file diff --git a/models/src/dlsite/category.rs b/models/src/dlsite/category.rs new file mode 100755 index 0000000..e3266a3 --- /dev/null +++ b/models/src/dlsite/category.rs @@ -0,0 +1,55 @@ +use color_eyre::Report; +use serde::{Deserialize, Serialize}; +use db::types::{RocksColumn, RocksReferences}; +use crate::config::ApplicationConfig; +use crate::dlsite::genre::DLSiteGenre; +use crate::dlsite::translation::DLSiteTranslation; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DLSiteCategory { + #[serde(skip)] + pub id: String, + pub genre_ids: Vec, + pub name: DLSiteTranslation +} + +impl TryFrom for DLSiteCategory { + type Error = Report; + + fn try_from(value: super::crawler::DLSiteGenreCategory) -> Result { + let category = Self { + id: format!( + "{}/{}", + value.id, + ApplicationConfig::get_config()?.basic_config.locale.primary_language() + ), + genre_ids: value.values.iter() + .map(|v| v.value.parse::()) + .filter_map(Result::ok) + .collect(), + name: DLSiteTranslation::try_from(value.category_name.as_str())?, + }; + Ok(category) + } +} + +impl RocksReferences for DLSiteCategory { + fn get_reference_ids(&self) -> Vec<::Id> { + self.genre_ids.clone() + } +} + +impl RocksColumn for DLSiteCategory { + type Id = String; + fn get_id(&self) -> Self::Id { + self.id.clone() + } + + fn set_id(&mut self, id: Self::Id) { + self.id = id; + } + + fn get_column_name() -> String { + String::from("dl_categories") + } +} \ No newline at end of file diff --git a/models/src/dlsite/crawler.rs b/models/src/dlsite/crawler.rs new file mode 100755 index 0000000..07a5e1e --- /dev/null +++ b/models/src/dlsite/crawler.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct DLSiteManiax { + #[serde(rename = "work_name")] + pub title: String, + #[serde(rename = "work_image")] + pub work_image_url: String, + #[serde(rename = "dl_count")] + pub sells_count: u32, + #[serde(skip)] + pub genre_ids: Vec, + #[serde(skip)] + pub rj_num: String, + #[serde(skip)] + pub folder_path: PathBuf, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct DLSiteFilter { + pub genre_all: Value +} + + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct DLSiteGenreCategory { + pub category_name: String, + pub values: Vec, + #[serde(skip)] + pub id: u8 +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct DLSiteGenre { + pub value: String, + pub name: String +} \ No newline at end of file diff --git a/models/src/dlsite/genre.rs b/models/src/dlsite/genre.rs new file mode 100755 index 0000000..1e657d8 --- /dev/null +++ b/models/src/dlsite/genre.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use db::types::RocksColumn; +use super::translation::DLSiteTranslation; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DLSiteGenre { + #[serde(skip)] + pub id: u16, + pub name: Vec +} + +impl RocksColumn for DLSiteGenre { + type Id = u16; + + fn get_id(&self) -> Self::Id { + self.id.clone() + } + + fn set_id(&mut self, id: Self::Id) { + self.id = id; + } + + fn get_column_name() -> String { + String::from("dl_genres") + } +} \ No newline at end of file diff --git a/models/src/dlsite/maniax.rs b/models/src/dlsite/maniax.rs new file mode 100755 index 0000000..008f590 --- /dev/null +++ b/models/src/dlsite/maniax.rs @@ -0,0 +1,59 @@ +use std::path::PathBuf; +use ratatui::text::Text; +use serde::{Deserialize, Serialize}; +use db::types::{RocksColumn, RocksReferences}; +use super::genre::DLSiteGenre; +use super::translation::DLSiteTranslation; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DLSiteManiax { + #[serde(skip)] + pub rj_num: String, + pub genre_ids: Vec, + pub name: Vec, + pub sells_count: u32, + pub folder_path: PathBuf, + pub version: Option +} + +impl From for DLSiteManiax { + fn from(value: super::crawler::DLSiteManiax) -> Self { + let title = DLSiteTranslation::try_from(value.title.as_str()).unwrap(); + Self { + rj_num: value.rj_num, + genre_ids: value.genre_ids, + name: vec![title], + sells_count: value.sells_count, + folder_path: value.folder_path, + version: None + } + } +} + +impl RocksColumn for DLSiteManiax { + type Id = String; + + fn get_id(&self) -> Self::Id { + self.rj_num.clone() + } + + fn set_id(&mut self, id: Self::Id) { + self.rj_num = id; + } + + fn get_column_name() -> String { + String::from("dl_games") + } +} + +impl RocksReferences for DLSiteManiax { + fn get_reference_ids(&self) -> Vec<::Id> { + self.genre_ids.clone() + } +} + +impl Into> for &DLSiteManiax { + fn into(self) -> Text<'static> { + Text::from(self.rj_num.to_string()) + } +} \ No newline at end of file diff --git a/src/models/mod.rs b/models/src/dlsite/mod.rs similarity index 52% rename from src/models/mod.rs rename to models/src/dlsite/mod.rs index a04f582..133399a 100755 --- a/src/models/mod.rs +++ b/models/src/dlsite/mod.rs @@ -1,27 +1,20 @@ -mod game; - use color_eyre::eyre::eyre; use color_eyre::Report; use language_tags::LanguageTag; -use serde::de::DeserializeOwned; -use serde::Serialize; -pub(crate) use game::*; -use crate::constants::{EN_LOCALE, JP_LOCALE}; -use crate::helpers::matches_primary_language; -pub trait RocksColumn { - type Id: Serialize + DeserializeOwned + Clone; - fn get_id(&self) -> Self::Id; - fn set_id(&mut self, id: Self::Id); - fn get_column_name() -> String; -} +mod translation; +mod category; +mod genre; +mod maniax; +pub mod crawler; -pub trait RocksReference where T: RocksColumn { - fn get_reference_id(&self) -> T::Id; -} +pub use translation::{EN_LOCALE, JP_LOCALE, DLSiteTranslation}; +pub use category::DLSiteCategory; +pub use genre::DLSiteGenre; +pub use maniax::DLSiteManiax; -pub trait RocksReferences where T: RocksColumn { - fn get_reference_ids(&self) -> Vec; +pub fn matches_primary_language(left: &LanguageTag, right: &LanguageTag) -> bool { + left.primary_language() == right.primary_language() } #[derive(Debug, Clone)] diff --git a/models/src/dlsite/translation.rs b/models/src/dlsite/translation.rs new file mode 100755 index 0000000..b3e6bc4 --- /dev/null +++ b/models/src/dlsite/translation.rs @@ -0,0 +1,56 @@ +use color_eyre::eyre::eyre; +use color_eyre::Report; +use language_tags::LanguageTag; +use serde::{Deserialize, Serialize}; +use lazy_static::lazy_static; +use crate::config::ApplicationConfig; +use super::matches_primary_language; + +lazy_static! { + pub static ref EN_LOCALE: LanguageTag = LanguageTag::parse("en").unwrap(); + pub static ref JP_LOCALE: LanguageTag = LanguageTag::parse("ja").unwrap(); + pub static ref SUPPORTED_LOCALES: [LanguageTag; 2] = [JP_LOCALE.clone(), EN_LOCALE.clone()]; +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum DLSiteTranslation { + EN(String), JP(String) +} + +impl TryFrom<&str> for DLSiteTranslation { + type Error = Report; + fn try_from(value: &str) -> color_eyre::Result { + Self::try_from(value.to_string()) + } +} + +impl TryFrom for DLSiteTranslation { + type Error = Report; + fn try_from(value: String) -> color_eyre::Result { + let app_conf = ApplicationConfig::get_config()?; + let locale = app_conf.basic_config.locale; + + if matches_primary_language(&locale, &EN_LOCALE) { + return Ok(DLSiteTranslation::EN(value)); + } + if matches_primary_language(&locale, &JP_LOCALE) { + return Ok(DLSiteTranslation::JP(value)); + } + Err(eyre!( + "Invalid Locale: {:?}; Support {:?}", + locale, + [EN_LOCALE.to_string(), JP_LOCALE.to_string()]) + ) + } +} + +impl TryInto for DLSiteTranslation { + type Error = Report; + + fn try_into(self) -> Result { + match self { + DLSiteTranslation::EN(val) => Ok(val), + DLSiteTranslation::JP(val) => Ok(val), + } + } +} \ No newline at end of file diff --git a/models/src/lib.rs b/models/src/lib.rs new file mode 100755 index 0000000..fdfa936 --- /dev/null +++ b/models/src/lib.rs @@ -0,0 +1,24 @@ +use std::hash::RandomState; +use std::path::PathBuf; +use std::sync::Arc; +use dashmap::DashMap; +use directories::BaseDirs; +use lazy_static::lazy_static; + +pub mod dlsite; +pub mod config; + +const APP_DIR_NAME: &str = "sus_manager"; +lazy_static! { + static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap(); + pub static ref APP_CONFIG_DIR: PathBuf = + BASE_DIRS.config_dir().to_path_buf().join(APP_DIR_NAME); + pub static ref APP_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf().join(APP_DIR_NAME); + pub static ref APP_CACHE_PATH: PathBuf = BASE_DIRS.cache_dir().to_path_buf().join(APP_DIR_NAME); + pub static ref APP_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone().join("config.json"); +} + +lazy_static! { + pub static ref CACHE_MAP: Arc> = + Arc::new(DashMap::with_hasher(RandomState::default())); +} \ No newline at end of file diff --git a/scripts/deps.sh b/scripts/deps.sh new file mode 100755 index 0000000..9aa6471 --- /dev/null +++ b/scripts/deps.sh @@ -0,0 +1,3 @@ +if ! (command -v rpgmaker-linux & > /dev/null 2>&1) then + wget -qO- "https://raw.githubusercontent.com/bakustarver/rpgmakermlinux-cicpoffs/main/installgithub.sh" | bash +fi \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs deleted file mode 100755 index 0fd4fec..0000000 --- a/src/constants.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::hash::RandomState; -use directories::BaseDirs; -use lazy_static::lazy_static; -use std::path::PathBuf; -use std::sync::Arc; -use dashmap::DashMap; -use language_tags::LanguageTag; -use crate::models::{DLSiteCategory, DLSiteGenre, DLSiteManiax, RocksColumn}; - -const APP_DIR_NAME: &str = "sus_manager"; -lazy_static! { - static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap(); - pub static ref APP_CONFIG_DIR: PathBuf = - BASE_DIRS.config_dir().to_path_buf().join(APP_DIR_NAME); - pub static ref APP_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf().join(APP_DIR_NAME); - pub static ref APP_CACHE_PATH: PathBuf = BASE_DIRS.cache_dir().to_path_buf().join(APP_DIR_NAME); - pub static ref APP_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone().join("config.json"); - pub static ref APP_DB_DATA_DIR: PathBuf = APP_DATA_DIR.clone().join("db"); - - pub static ref DB_OPTIONS: rocksdb::Options = get_db_options(); - pub static ref DB_CF_OPTIONS: rocksdb::Options = rocksdb::Options::default(); -} - -lazy_static! { - pub static ref DB_COLUMNS: [String; 3] = [ - DLSiteManiax::get_column_name().to_string(), - DLSiteGenre::get_column_name().to_string(), - DLSiteCategory::get_column_name().to_string(), - ]; -} - -lazy_static! { - pub static ref EN_LOCALE: LanguageTag = LanguageTag::parse("en").unwrap(); - pub static ref JP_LOCALE: LanguageTag = LanguageTag::parse("ja").unwrap(); - pub static ref SUPPORTED_LOCALES: [LanguageTag; 2] = [JP_LOCALE.clone(), EN_LOCALE.clone()]; -} - -lazy_static! { - pub static ref CACHE_MAP: Arc> = - Arc::new(DashMap::with_hasher(RandomState::default())); -} - -fn get_db_options() -> rocksdb::Options { - let mut opts = rocksdb::Options::default(); - - opts.create_missing_column_families(true); - opts.create_if_missing(true); - opts.increase_parallelism(num_cpus::get() as i32); - - opts -} - -pub(crate) fn get_db_read_options() -> rocksdb::ReadOptions { - let mut opts = rocksdb::ReadOptions::default(); - opts.set_async_io(true); - opts -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8054051..a956bd1 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,8 @@ -mod app; -mod cli; -mod config; -mod constants; -mod crawler; -mod event; -mod helpers; -mod models; -mod widgets; - -use crate::cli::Cli; -use clap::Parser; -use color_eyre::Result; -use tokio; - +use clap_builder::Parser; #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> color_eyre::Result<()> { color_eyre::install()?; - let cli = Cli::parse(); - cli.run().await + let cli = ui::Cli::parse(); + cli.run().await?; + Ok(()) } \ No newline at end of file diff --git a/src/models/game.rs b/src/models/game.rs deleted file mode 100755 index f3844ae..0000000 --- a/src/models/game.rs +++ /dev/null @@ -1,219 +0,0 @@ -use color_eyre::Result; -use std::path::PathBuf; -use color_eyre::{eyre, Report}; -use ratatui::prelude::Text; -use ratatui::widgets::ListState; -use serde::{Deserialize, Serialize}; -use serde::de::DeserializeOwned; -use crate::config::types::ApplicationConfig; -use crate::constants::{EN_LOCALE, JP_LOCALE}; -use crate::crawler::DLSiteGenreCategory; -use crate::helpers::db::RocksDB; -use crate::helpers::matches_primary_language; -use crate::models::{RocksColumn, RocksReferences}; - -#[derive(Debug)] -pub(crate) struct GameList { - pub games: Vec, - pub state: ListState, -} - -impl Default for GameList { - fn default() -> Self { - Self { - games: Vec::new(), - state: ListState::default(), - } - } -} - -impl GameList - where T: DeserializeOwned + RocksColumn -{ - pub fn new() -> Result { - let db = RocksDB::default(); - let mut state = ListState::default(); - state.select_first(); - let game_list = GameList { - games: db.get_all_values::()?, - state - }; - Ok(game_list) - } -} - -//region Maniax -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct DLSiteManiax { - #[serde(skip)] - pub rj_num: String, - pub genre_ids: Vec, - pub name: Vec, - pub sells_count: u32, - pub folder_path: PathBuf, - pub version: Option -} - -impl From for DLSiteManiax { - fn from(value: crate::crawler::DLSiteManiax) -> Self { - let title = DLSiteTranslation::try_from(value.title.as_str()).unwrap(); - Self { - rj_num: value.rj_num, - genre_ids: value.genre_ids, - name: vec![title], - sells_count: value.sells_count, - folder_path: value.folder_path, - version: None - } - } -} - -impl RocksColumn for DLSiteManiax { - type Id = String; - - fn get_id(&self) -> Self::Id { - self.rj_num.clone() - } - - fn set_id(&mut self, id: Self::Id) { - self.rj_num = id; - } - - fn get_column_name() -> String { - String::from("dl_games") - } -} - -impl RocksReferences for DLSiteManiax { - fn get_reference_ids(&self) -> Vec<::Id> { - self.genre_ids.clone() - } -} - -impl Into> for &DLSiteManiax { - fn into(self) -> Text<'static> { - Text::from(self.rj_num.to_string()) - } -} -//endregion - -//region Genre -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DLSiteGenre { - #[serde(skip)] - pub id: u16, - pub name: Vec -} - -impl RocksColumn for DLSiteGenre { - type Id = u16; - - fn get_id(&self) -> Self::Id { - self.id.clone() - } - - fn set_id(&mut self, id: Self::Id) { - self.id = id; - } - - fn get_column_name() -> String { - String::from("dl_genres") - } -} -//endregion - -//region Category -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct DLSiteCategory { - #[serde(skip)] - pub id: String, - pub genre_ids: Vec, - pub name: DLSiteTranslation -} - -impl TryFrom for DLSiteCategory { - type Error = Report; - - fn try_from(value: DLSiteGenreCategory) -> Result { - let category = Self { - id: format!( - "{}/{}", - value.id, - ApplicationConfig::get_config()?.basic_config.locale.primary_language() - ), - genre_ids: value.values.iter() - .map(|v| v.value.parse::()) - .filter_map(Result::ok) - .collect(), - name: DLSiteTranslation::try_from(value.category_name.as_str())?, - }; - Ok(category) - } -} - -impl RocksReferences for DLSiteCategory { - fn get_reference_ids(&self) -> Vec<::Id> { - self.genre_ids.clone() - } -} - -impl RocksColumn for DLSiteCategory { - type Id = String; - fn get_id(&self) -> Self::Id { - self.id.clone() - } - - fn set_id(&mut self, id: Self::Id) { - self.id = id; - } - - fn get_column_name() -> String { - String::from("dl_categories") - } -} -//endregion - -//region Translation -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub(crate) enum DLSiteTranslation { - EN(String), JP(String) -} - -impl TryFrom<&str> for DLSiteTranslation { - type Error = Report; - fn try_from(value: &str) -> color_eyre::Result { - Self::try_from(value.to_string()) - } -} - -impl TryFrom for DLSiteTranslation { - type Error = Report; - fn try_from(value: String) -> color_eyre::Result { - let app_conf = ApplicationConfig::get_config()?; - let locale = app_conf.basic_config.locale; - - if matches_primary_language(&locale, &EN_LOCALE) { - return Ok(DLSiteTranslation::EN(value)); - } - if matches_primary_language(&locale, &JP_LOCALE) { - return Ok(DLSiteTranslation::JP(value)); - } - Err(eyre::eyre!( - "Invalid Locale: {:?}; Support {:?}", - locale, - [EN_LOCALE.to_string(), JP_LOCALE.to_string()]) - ) - } -} - -impl TryInto for DLSiteTranslation { - type Error = Report; - - fn try_into(self) -> Result { - match self { - DLSiteTranslation::EN(val) => Ok(val), - DLSiteTranslation::JP(val) => Ok(val), - } - } -} -//endregion \ No newline at end of file diff --git a/ui/Cargo.toml b/ui/Cargo.toml new file mode 100755 index 0000000..4946de1 --- /dev/null +++ b/ui/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "ui" +version = "0.1.0" +edition = "2024" + +[lib] +name = "ui" +path = "src/lib.rs" + +[dependencies] +futures = "0.3.28" +rat-cursor = "1.2.1" +itertools = "0.14.0" + +color-eyre.workspace = true +serde.workspace = true +tokio.workspace = true +ratatui.workspace = true + +models = { path = "../models" } +db = { path = "../db" } +crawler = { path = "../crawler" } + +[dependencies.ratatui-image] +version = "8.0.2" +features = ["tokio", "serde"] + +[dependencies.indicatif] +version = "0.18.1" +features = ["futures", "tokio"] + +[dependencies.tui-input] +version = "0.14.0" +features = ["crossterm"] +default-features = false + +[dependencies.crossterm] +version = "0.29.0" +features = ["event-stream"] + +[dependencies.clap] +version = "4.5.48" +features = ["derive", "cargo"] diff --git a/src/app.rs b/ui/src/app.rs similarity index 80% rename from src/app.rs rename to ui/src/app.rs index de5e87a..039a8be 100755 --- a/src/app.rs +++ b/ui/src/app.rs @@ -1,4 +1,3 @@ -use crate::config::types::ApplicationConfig; use crate::event::{AppEvent, EventHandler}; use crate::widgets::views::{AppView, MainView}; use color_eyre::Result; @@ -6,12 +5,17 @@ use crossterm::event::{Event}; use ratatui::{DefaultTerminal, Frame}; use std::time::Duration; use color_eyre::eyre::eyre; +use db::RocksDBFactory; +use models::config::ApplicationConfig; +use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax}; pub(crate) struct App { events: EventHandler, state: AppState, + db_factory: RocksDBFactory } +#[derive(Clone)] pub struct AppState { view: Option, } @@ -19,12 +23,17 @@ pub struct AppState { impl App { pub async fn create() -> Result { let config = ApplicationConfig::get_config()?; + let mut db_factory = RocksDBFactory::default(); + db_factory.register::(); + db_factory.register::(); + db_factory.register::(); let state = AppState { - view: Some(AppView::Main(MainView::new()?)), + view: Some(AppView::Main(MainView::new(db_factory.clone())?)), }; let app = Self { events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)), state, + db_factory }; Ok(app) } @@ -69,7 +78,7 @@ impl App { match current_view { AppView::Main(main_view) => { frame.render_stateful_widget( - MainView::new().unwrap(), + main_view.clone(), frame.area(), &mut main_view.state, ); diff --git a/src/cli/folder.rs b/ui/src/cli/folder.rs similarity index 97% rename from src/cli/folder.rs rename to ui/src/cli/folder.rs index 1db43da..edd272f 100755 --- a/src/cli/folder.rs +++ b/ui/src/cli/folder.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::{Args, Command, Parser, Subcommand}; use color_eyre::eyre::eyre; -use crate::config::types::ApplicationConfig; +use models::config::ApplicationConfig; #[derive(Parser, Debug)] pub(super) struct FolderAddCommand { diff --git a/src/cli/mod.rs b/ui/src/cli/mod.rs similarity index 83% rename from src/cli/mod.rs rename to ui/src/cli/mod.rs index b5cd930..a37ab98 100755 --- a/src/cli/mod.rs +++ b/ui/src/cli/mod.rs @@ -17,7 +17,7 @@ enum CliSubCommand { #[derive(Parser, Debug)] #[command(version, about)] -pub(crate) struct Cli { +pub struct Cli { #[command(subcommand)] subcommand: Option, } @@ -36,12 +36,18 @@ impl Cli { async fn start_tui(&self) -> Result<()> { crossterm::terminal::enable_raw_mode()?; - let mut terminal = ratatui::init(); + struct TuiCleanup; + impl Drop for TuiCleanup { + fn drop(&mut self) { + ratatui::restore(); + let _ = crossterm::terminal::disable_raw_mode(); + } + } + let _cleanup = TuiCleanup; let app = app::App::create().await?; let result = app.run(&mut terminal).await; ratatui::restore(); - crossterm::terminal::disable_raw_mode()?; result } diff --git a/src/cli/sync.rs b/ui/src/cli/sync.rs similarity index 82% rename from src/cli/sync.rs rename to ui/src/cli/sync.rs index 4be3c2a..bda4418 100755 --- a/src/cli/sync.rs +++ b/ui/src/cli/sync.rs @@ -6,13 +6,13 @@ use crossterm::style::{style, Stylize}; use futures::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; use itertools::Itertools; +use tokio::sync::Mutex; use tokio::time::Instant; -use crate::models::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation}; -use crate::config::types::ApplicationConfig; -use crate::constants::{DB_CF_OPTIONS, DB_OPTIONS}; -use crate::crawler::{dlsite, DLSiteCrawler}; +use crawler::DLSiteCrawler; +use db::{RocksDBFactory}; +use models::config::ApplicationConfig; +use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation}; use crate::helpers; -use crate::helpers::db::RocksDB; #[derive(Parser, Debug)] pub(super) struct DLSiteCommand { @@ -48,11 +48,11 @@ impl DLSiteSyncCommand { pub async fn handle(&self) -> Result<()> { let now = Instant::now(); let app_conf = ApplicationConfig::get_config()?; - let mut db = RocksDB::default(); + let db_factory = RocksDBFactory::default(); let crawler = DLSiteCrawler::new()?; if self.do_sync_genre { let genre_now = Instant::now(); - Self::sync_genres(&mut db, &app_conf, &crawler).await?; + Self::sync_genres(db_factory.clone(), &app_conf, &crawler).await?; println!( "{} {} Done in {:.2?}", style("Genres").cyan(), @@ -62,7 +62,7 @@ impl DLSiteSyncCommand { } if self.do_sync_work { let work_now = Instant::now(); - self.sync_works(&app_conf, &mut db, &crawler).await?; + self.sync_works(&app_conf, db_factory.clone(), &crawler).await?; println!( "{} {} Done in {:.2?}", style("Works").cyan(), @@ -74,7 +74,8 @@ impl DLSiteSyncCommand { Ok(()) } - async fn sync_genres(db: &mut RocksDB, app_conf: &ApplicationConfig, crawler: &DLSiteCrawler) -> Result<()> { + async fn sync_genres(mut db_factory: RocksDBFactory, app_conf: &ApplicationConfig, crawler: &DLSiteCrawler) -> Result<()> { + let mut db = db_factory.get_current_context()?; let requested_categories = crawler.get_all_genres(&app_conf.basic_config.locale).await?; let categories: Vec = requested_categories.iter() .map(|g| g.clone().try_into()) @@ -111,7 +112,8 @@ impl DLSiteSyncCommand { Ok(()) } - async fn sync_works(&self, app_conf: &ApplicationConfig, db: &mut RocksDB, crawler: &DLSiteCrawler) -> Result<()> { + async fn sync_works(&self, app_conf: &ApplicationConfig, mut db_factory: RocksDBFactory, crawler: &DLSiteCrawler) -> Result<()> { + let mut db = db_factory.get_current_context()?; let existing_works = db.get_all_values::()?; let work_list = self.get_work_list(&app_conf, &existing_works).await?; @@ -123,6 +125,7 @@ impl DLSiteSyncCommand { let progress = ProgressBar::new(game_infos.len() as u64) .with_style(ProgressStyle::default_bar()); + let shared_progress = Mutex::new(progress); while let Some(info) = game_infos.next().await { let maniax = info?; let existing_maniax = existing_game_infos.iter() @@ -136,21 +139,22 @@ impl DLSiteSyncCommand { let mut modified_maniax = existing_maniax.clone(); modified_maniax.name.push(name); modified_maniaxes.push(modified_maniax); - } - else { + } else { let mut value: DLSiteManiax = maniax.into(); let maniax_folder = work_list.get(&value.rj_num).unwrap().to_owned(); value.folder_path = maniax_folder; modified_maniaxes.push(value); } + let progress = shared_progress.lock().await; progress.inc(1); } db.set_values(&modified_maniaxes)?; - Ok(()) } - async fn get_work_list(&self, app_conf: &ApplicationConfig, existing_works: &[DLSiteManiax]) -> Result> { + async fn get_work_list(&self, app_conf: &ApplicationConfig, existing_works: &[DLSiteManiax]) + -> Result> + { let existing_nums = existing_works.iter() .map(|x| x.rj_num.clone()) .collect::>(); @@ -176,7 +180,7 @@ impl DLSiteSyncCommand { .file_name().unwrap() .to_str().unwrap() .to_string(); - if !dlsite::is_valid_rj_number(&dir_name) && !existing_folders.contains(&dir_path_str) { + if !is_valid_rj_number(&dir_name) && !existing_folders.contains(&dir_path_str) { println!( "{} {}", style(dir_path.to_str().unwrap()).blue(), @@ -191,4 +195,18 @@ impl DLSiteSyncCommand { } Ok(works_list) } +} + +fn is_valid_rj_number(rj_num: &str) -> bool { + let len = rj_num.len(); + if len != 8 && len != 10 { + return false; + } + if !rj_num.starts_with("RJ") { + return false; + } + if !rj_num.chars().skip(2).all(|c| c.is_numeric()) { + return false; + } + true } \ No newline at end of file diff --git a/src/event.rs b/ui/src/event.rs similarity index 86% rename from src/event.rs rename to ui/src/event.rs index ff43064..8eaa989 100755 --- a/src/event.rs +++ b/ui/src/event.rs @@ -4,11 +4,10 @@ use futures::FutureExt; use futures::StreamExt; use std::time::Duration; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::task::JoinHandle; #[derive(Clone)] pub(crate) enum AppEvent { - Error, + Error(String), Tick, Raw(crossterm::event::Event), } @@ -16,7 +15,6 @@ pub(crate) enum AppEvent { pub(crate) struct EventHandler { _tx: UnboundedSender, rx: UnboundedReceiver, - pub task: JoinHandle<()>, } impl EventHandler { @@ -33,8 +31,8 @@ impl EventHandler { let crossterm_event = event_reader.next().fuse(); tokio::select! { maybe_event = crossterm_event => { - if let Some(Err(_)) = maybe_event { - tx.send(AppEvent::Error).unwrap() + if let Some(Err(e)) = maybe_event { + tx.send(AppEvent::Error(e.to_string())).unwrap() } else if let Some(Ok(event)) = maybe_event { tx.send(AppEvent::Raw(event)).unwrap(); } @@ -45,7 +43,7 @@ impl EventHandler { } } }); - Self { _tx, rx, task } + Self { _tx, rx } } pub(crate) async fn next(&mut self) -> Result { diff --git a/src/helpers/mod.rs b/ui/src/helpers/mod.rs similarity index 71% rename from src/helpers/mod.rs rename to ui/src/helpers/mod.rs index 3821ada..ff086e8 100755 --- a/src/helpers/mod.rs +++ b/ui/src/helpers/mod.rs @@ -1,13 +1,9 @@ -pub mod db; - use std::path::{Path, PathBuf}; use color_eyre::eyre::eyre; use color_eyre::owo_colors::OwoColorize; -use language_tags::LanguageTag; use tokio::fs; -use crate::constants::{APP_CONFIG_DIR, APP_DATA_DIR, APP_DB_DATA_DIR}; -use crate::crawler::DLSITE_IMG_FOLDER; - +use crawler::DLSITE_IMG_FOLDER; +use models::{APP_CONFIG_DIR, APP_DATA_DIR}; pub async fn initialize_folders() -> color_eyre::Result<()> { if !APP_CONFIG_DIR.exists() { @@ -19,9 +15,6 @@ pub async fn initialize_folders() -> color_eyre::Result<()> { if !DLSITE_IMG_FOLDER.exists() { fs::create_dir_all(DLSITE_IMG_FOLDER.as_path()).await?; } - if !APP_DB_DATA_DIR.exists() { - fs::create_dir_all(APP_DB_DATA_DIR.as_path()).await?; - } Ok(()) } @@ -39,8 +32,4 @@ pub async fn get_all_folders(paths: &[&Path]) -> color_eyre::Result } } Ok(folders) -} - -pub fn matches_primary_language(left: &LanguageTag, right: &LanguageTag) -> bool { - left.primary_language() == right.primary_language() } \ No newline at end of file diff --git a/ui/src/lib.rs b/ui/src/lib.rs new file mode 100755 index 0000000..6c1326b --- /dev/null +++ b/ui/src/lib.rs @@ -0,0 +1,8 @@ +mod app; +mod cli; +mod event; +mod helpers; +mod models; +mod widgets; + +pub use cli::Cli; \ No newline at end of file diff --git a/ui/src/models/game_list.rs b/ui/src/models/game_list.rs new file mode 100755 index 0000000..85f9c7d --- /dev/null +++ b/ui/src/models/game_list.rs @@ -0,0 +1,33 @@ +use color_eyre::Result; +use ratatui::widgets::ListState; +use serde::de::DeserializeOwned; +use db::types::RocksColumn; + +#[derive(Debug, Clone)] +pub(crate) struct GameList { + pub games: Vec, + pub state: ListState, +} + +impl Default for GameList { + fn default() -> Self { + Self { + games: Vec::new(), + state: ListState::default(), + } + } +} + +impl GameList + where T: DeserializeOwned + RocksColumn +{ + pub fn new(games: Vec) -> Result { + let mut state = ListState::default(); + state.select_first(); + let game_list = GameList { + games, + state + }; + Ok(game_list) + } +} \ No newline at end of file diff --git a/ui/src/models/mod.rs b/ui/src/models/mod.rs new file mode 100755 index 0000000..f3fadf8 --- /dev/null +++ b/ui/src/models/mod.rs @@ -0,0 +1,2 @@ +mod game_list; +pub use game_list::*; \ No newline at end of file diff --git a/src/widgets/components/mod.rs b/ui/src/widgets/components/mod.rs similarity index 100% rename from src/widgets/components/mod.rs rename to ui/src/widgets/components/mod.rs diff --git a/src/widgets/components/textarea.rs b/ui/src/widgets/components/textarea.rs similarity index 100% rename from src/widgets/components/textarea.rs rename to ui/src/widgets/components/textarea.rs diff --git a/src/widgets/mod.rs b/ui/src/widgets/mod.rs similarity index 100% rename from src/widgets/mod.rs rename to ui/src/widgets/mod.rs diff --git a/src/widgets/popups/folder.rs b/ui/src/widgets/popups/folder.rs similarity index 100% rename from src/widgets/popups/folder.rs rename to ui/src/widgets/popups/folder.rs diff --git a/src/widgets/popups/mod.rs b/ui/src/widgets/popups/mod.rs similarity index 66% rename from src/widgets/popups/mod.rs rename to ui/src/widgets/popups/mod.rs index 470d1ba..f0c8f9c 100755 --- a/src/widgets/popups/mod.rs +++ b/ui/src/widgets/popups/mod.rs @@ -1,7 +1,6 @@ -use ratatui::widgets::StatefulWidget; - pub mod folder; +#[derive(Clone)] pub enum AppPopup { AddFolder(folder::AddFolderPopup) } \ No newline at end of file diff --git a/src/widgets/views/main_view.rs b/ui/src/widgets/views/main_view.rs similarity index 88% rename from src/widgets/views/main_view.rs rename to ui/src/widgets/views/main_view.rs index 9d1f500..de44cf2 100755 --- a/src/widgets/views/main_view.rs +++ b/ui/src/widgets/views/main_view.rs @@ -1,4 +1,3 @@ -use crate::config::types::ApplicationConfig; use crate::widgets::popups::folder::AddFolderPopup; use crossterm::event::KeyCode::Char; use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; @@ -9,14 +8,20 @@ use ratatui::prelude::{Color, Line, Span, Style, Text, Widget}; use ratatui::style::Modifier; use ratatui::style::palette::tailwind::SLATE; use ratatui::widgets::{Block, Borders, HighlightSpacing, List, Paragraph, StatefulWidget}; -use crate::models::{DLSiteManiax, GameList}; +use db::RocksDBFactory; +use models::config::ApplicationConfig; +use models::dlsite::DLSiteManiax; +use crate::models::GameList; use crate::widgets::popups::AppPopup; use crate::widgets::views::View; +#[derive(Clone)] pub struct MainView { - pub state: MainViewState + pub state: MainViewState, + db_factory: RocksDBFactory } +#[derive(Clone)] pub struct MainViewState { popup: Option, status: Status, @@ -32,15 +37,18 @@ enum Status { } impl MainView { - pub fn new() -> color_eyre::Result { - let dl_game_list = GameList::new()?; + pub fn new(mut db_factory: RocksDBFactory) -> color_eyre::Result { + let db = db_factory.get_current_context()?; + let games = db.get_all_values::()?; + let dl_game_list = GameList::new(games)?; let view = Self { state: MainViewState { popup: None, status: Status::Running, list_page_size: 0, - dl_game_list - } + dl_game_list, + }, + db_factory }; Ok(view) } @@ -189,10 +197,12 @@ impl MainView { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Length(13), + Constraint::Length(14), + Constraint::Fill(0), ]) .split(area); Self::render_game_list(chunks[0], buf, state); + Self::render_game_box(chunks[1], buf, state); } fn render_game_list(area: Rect, buf: &mut Buffer, state: &mut MainViewState) { @@ -207,6 +217,14 @@ impl MainView { .highlight_spacing(HighlightSpacing::WhenSelected); StatefulWidget::render(game_list, area, buf, &mut state.dl_game_list.state); } + + fn render_game_box(area: Rect, buf: &mut Buffer, state: &mut MainViewState) { + let block = Block::new() + .title(Line::raw("Info")) + .borders(Borders::ALL) + .style(Style::default()); + block.render(area, buf); + } fn render_header(area: Rect, buf: &mut Buffer) { let title = Paragraph::new(Text::styled( diff --git a/src/widgets/views/mod.rs b/ui/src/widgets/views/mod.rs similarity index 96% rename from src/widgets/views/mod.rs rename to ui/src/widgets/views/mod.rs index d481203..2a7d0d4 100755 --- a/src/widgets/views/mod.rs +++ b/ui/src/widgets/views/mod.rs @@ -9,6 +9,7 @@ pub trait View: HasScreenCursor { fn is_running(&self) -> bool; } +#[derive(Clone)] pub enum AppView { Main(MainView), }