Refactor code to remove Any

This commit is contained in:
2025-11-10 02:02:14 +08:00
parent 2538049f4a
commit fcb9297fdc
14 changed files with 98 additions and 104 deletions

View File

@@ -5,6 +5,7 @@ description = "manager for SuS gamers"
authors = ["fromost"] authors = ["fromost"]
license = "MIT" license = "MIT"
edition = "2024" edition = "2024"
rust-version = "1.91"
[profile.dev] [profile.dev]
debug = true debug = true
@@ -30,7 +31,6 @@ log = "0.4.28"
num_cpus = "1.17.0" num_cpus = "1.17.0"
sys-locale = "0.3.2" sys-locale = "0.3.2"
jemallocator = "0.5.4" jemallocator = "0.5.4"
reqwest_cookie_store = { version = "0.9.0", features = ["serde"] }
itertools = "0.14.0" itertools = "0.14.0"
dashmap = { version = "6.1.0", features = ["serde"] } dashmap = { version = "6.1.0", features = ["serde"] }

View File

@@ -1,13 +1,11 @@
use crate::config::types::ApplicationConfig; use crate::config::types::ApplicationConfig;
use crate::event::{AppEvent, EventHandler}; use crate::event::{AppEvent, EventHandler};
use crate::widgets::views::MainView; use crate::widgets::views::{AppView, MainView};
use crate::widgets::views::View; use crate::widgets::views::View;
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::Event as CrosstermEvent; use crossterm::event::{Event};
use crossterm::event::{Event, KeyEvent};
use rat_cursor::HasScreenCursor; use rat_cursor::HasScreenCursor;
use ratatui::{DefaultTerminal, Frame}; use ratatui::{DefaultTerminal, Frame};
use std::any::Any;
use std::time::Duration; use std::time::Duration;
pub(crate) struct App { pub(crate) struct App {
@@ -16,14 +14,14 @@ pub(crate) struct App {
} }
struct AppState { struct AppState {
view: Option<Box<dyn Any>>, view: Option<AppView>,
} }
impl App { impl App {
pub async fn create() -> Result<Self> { pub async fn create() -> Result<Self> {
let config = ApplicationConfig::get_config()?; let config = ApplicationConfig::get_config()?;
let state = AppState { let state = AppState {
view: Some(Box::new(MainView::new())), view: Some(AppView::MainView(MainView::new())),
}; };
let app = Self { let app = Self {
events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)), events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)),
@@ -38,7 +36,7 @@ impl App {
let event = self.events.next().await?; let event = self.events.next().await?;
self.update(event)?; self.update(event)?;
if let Some(view) = self.state.view.as_mut() if let Some(view) = self.state.view.as_mut()
&& let Some(main_view) = view.downcast_ref::<MainView>() && let AppView::MainView(main_view) = view
&& !main_view.is_running() && !main_view.is_running()
{ {
break Ok(()); break Ok(());
@@ -49,27 +47,17 @@ impl App {
fn update(&mut self, event: AppEvent) -> Result<()> { fn update(&mut self, event: AppEvent) -> Result<()> {
if let AppEvent::Raw(cross_event) = event { if let AppEvent::Raw(cross_event) = event {
self.handle_event(&cross_event)?; self.handle_event(&cross_event)?;
if let CrosstermEvent::Key(key) = cross_event {
self.handle_key_event(&key)?;
}
} }
Ok(()) Ok(())
} }
fn handle_key_event(&mut self, key: &KeyEvent) -> Result<()> {
if let Some(any) = self.state.view.as_mut() {
if let Some(main_view) = any.downcast_mut::<MainView>() {
main_view.handle_key_input(key)?;
}
}
Ok(())
}
fn handle_event(&mut self, key: &Event) -> Result<()> { fn handle_event(&mut self, key: &Event) -> Result<()> {
if let Some(any) = self.state.view.as_mut() { if let Some(current_view) = self.state.view.as_mut() {
if let Some(main_view) = any.downcast_mut::<MainView>() { match current_view {
main_view.handle_input(key)?; AppView::MainView(main_view) => {
main_view.handle_input(key)?
}
} }
} }
Ok(()) Ok(())
@@ -77,14 +65,16 @@ impl App {
fn draw(&mut self, frame: &mut Frame) { fn draw(&mut self, frame: &mut Frame) {
if let Some(view) = self.state.view.as_mut() { if let Some(view) = self.state.view.as_mut() {
if let Some(main_view) = view.downcast_mut::<MainView>() { match view {
frame.render_stateful_widget( AppView::MainView(main_view) => {
MainView::new(), frame.render_stateful_widget(
frame.area(), MainView::new(),
&mut main_view.state, frame.area(),
); &mut main_view.state,
if let Some(pos) = main_view.screen_cursor() { );
frame.set_cursor_position(pos); if let Some(pos) = main_view.screen_cursor() {
frame.set_cursor_position(pos);
}
} }
} }
} }

View File

@@ -2,7 +2,7 @@ mod folder;
mod sync; mod sync;
use crate::{app, helpers}; use crate::{app, helpers};
use clap::{command, Args, Command, Parser, Subcommand}; use clap::{command, Parser};
use color_eyre::Result; use color_eyre::Result;
use ratatui::crossterm; use ratatui::crossterm;
use crate::cli::folder::FolderCommand; use crate::cli::folder::FolderCommand;

View File

@@ -7,7 +7,6 @@ use futures::StreamExt;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools; use itertools::Itertools;
use tokio::time::Instant; use tokio::time::Instant;
use crate::models;
use crate::models::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation}; use crate::models::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation};
use crate::config::types::ApplicationConfig; use crate::config::types::ApplicationConfig;
use crate::constants::{DB_CF_OPTIONS, DB_OPTIONS}; use crate::constants::{DB_CF_OPTIONS, DB_OPTIONS};
@@ -113,7 +112,7 @@ impl DLSiteSyncCommand {
} }
async fn sync_works(&self, app_conf: &ApplicationConfig, db: &mut RocksDB, crawler: &DLSiteCrawler) -> Result<()> { async fn sync_works(&self, app_conf: &ApplicationConfig, db: &mut RocksDB, crawler: &DLSiteCrawler) -> Result<()> {
let existing_works = db.get_all_values::<models::DLSiteManiax>()?; let existing_works = db.get_all_values::<DLSiteManiax>()?;
let work_list = self.get_work_list(&app_conf, &existing_works).await?; let work_list = self.get_work_list(&app_conf, &existing_works).await?;
let rj_nums = work_list.clone().into_keys().collect::<Vec<_>>(); let rj_nums = work_list.clone().into_keys().collect::<Vec<_>>();
@@ -151,7 +150,7 @@ impl DLSiteSyncCommand {
Ok(()) Ok(())
} }
async fn get_work_list(&self, app_conf: &ApplicationConfig, existing_works: &[models::DLSiteManiax]) -> Result<HashMap<String, PathBuf>> { async fn get_work_list(&self, app_conf: &ApplicationConfig, existing_works: &[DLSiteManiax]) -> Result<HashMap<String, PathBuf>> {
let existing_nums = existing_works.iter() let existing_nums = existing_works.iter()
.map(|x| x.rj_num.clone()) .map(|x| x.rj_num.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@@ -17,8 +17,10 @@ pub(crate) struct GameList<T> {
impl ApplicationConfig { impl ApplicationConfig {
pub fn get_config() -> Result<Self> { pub fn get_config() -> Result<Self> {
if CACHE_MAP.contains_key(CONFIG_KEY) { if CACHE_MAP.contains_key(CONFIG_KEY) &&
Ok(serde_json::from_value(CACHE_MAP.get(CONFIG_KEY).unwrap().clone())?) let Some(cached_config) = CACHE_MAP.get(CONFIG_KEY)
{
Ok(serde_json::from_value(cached_config.clone())?)
} else if APP_CONIFG_FILE_PATH.exists() { } else if APP_CONIFG_FILE_PATH.exists() {
ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH) ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH)
} else { } else {
@@ -37,7 +39,6 @@ impl ApplicationConfig {
let default_locale = sys_locale::get_locale().unwrap_or(String::from("ja-JP")); let default_locale = sys_locale::get_locale().unwrap_or(String::from("ja-JP"));
let conf = Self { let conf = Self {
basic_config: BasicConfig { basic_config: BasicConfig {
db_path: APP_DB_DATA_DIR.to_str().unwrap().to_string(),
tick_rate: 250, tick_rate: 250,
locale: LanguageTag::parse(&default_locale)?, locale: LanguageTag::parse(&default_locale)?,
}, },
@@ -45,18 +46,19 @@ impl ApplicationConfig {
dlsite_paths: vec![], dlsite_paths: vec![],
}, },
}; };
conf.clone().write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())?; conf.clone().write_to_file(APP_CONIFG_FILE_PATH.to_path_buf())?;
Ok(conf) Ok(conf)
} }
fn write_to_file(self, path: &PathBuf) -> Result<()> { fn write_to_file(self, path: PathBuf) -> Result<()> {
let writer = std::fs::File::create(path)?; let writer = std::fs::File::create(path)?;
serde_json::to_writer_pretty(writer, &self)?; serde_json::to_writer_pretty(writer, &self)?;
Ok(()) Ok(())
} }
pub fn save(self) -> Result<()> { pub fn save(self) -> Result<()> {
self.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf()) 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())
} }
} }

View File

@@ -9,7 +9,6 @@ pub struct ApplicationConfig {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct BasicConfig { pub(crate) struct BasicConfig {
pub db_path: String,
pub locale: LanguageTag, pub locale: LanguageTag,
pub tick_rate: u64, pub tick_rate: u64,
} }

View File

@@ -12,12 +12,10 @@ use lazy_static::lazy_static;
use scraper::{Element, Html, Selector}; use scraper::{Element, Html, Selector};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::config::types::ApplicationConfig;
use crate::constants::{APP_DATA_DIR, JP_LOCALE}; use crate::constants::{APP_DATA_DIR, JP_LOCALE};
use crate::crawler::Crawler; use crate::crawler::Crawler;
use crate::helpers::matches_primary_language; use crate::helpers::matches_primary_language;
use crate::models; use crate::models::{PrimaryLanguage};
use crate::models::{DLSiteTranslation, PrimaryLanguage};
//TODO: override locale with user one //TODO: override locale with user one
const DLSITE_URL: &str = "https://www.dlsite.com/"; const DLSITE_URL: &str = "https://www.dlsite.com/";
@@ -167,7 +165,6 @@ impl DLSiteCrawler {
let result = html.select(&selector).next().unwrap(); let result = html.select(&selector).next().unwrap();
let genre_rows = result.child_elements().collect::<Vec<_>>(); let genre_rows = result.child_elements().collect::<Vec<_>>();
let t = genre_rows.iter().filter_map(|v| v.first_element_child().unwrap().text().next()).collect_vec();
let genre_row = genre_rows.iter() let genre_row = genre_rows.iter()
.find(|v| v.first_element_child().unwrap().text().next().unwrap() == genre_str) .find(|v| v.first_element_child().unwrap().text().next().unwrap() == genre_str)
.unwrap(); .unwrap();

View File

@@ -1,6 +1,5 @@
pub mod dlsite; pub mod dlsite;
use std::sync::Arc;
pub use dlsite::*; pub use dlsite::*;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use crate::constants::APP_CACHE_PATH; use crate::constants::APP_CACHE_PATH;

View File

@@ -3,7 +3,7 @@ use crossterm::event::EventStream;
use futures::FutureExt; use futures::FutureExt;
use futures::StreamExt; use futures::StreamExt;
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
#[derive(Clone)] #[derive(Clone)]
@@ -15,7 +15,7 @@ pub(crate) enum AppEvent {
pub(crate) struct EventHandler { pub(crate) struct EventHandler {
_tx: UnboundedSender<AppEvent>, _tx: UnboundedSender<AppEvent>,
rx: tokio::sync::mpsc::UnboundedReceiver<AppEvent>, rx: UnboundedReceiver<AppEvent>,
pub task: JoinHandle<()>, pub task: JoinHandle<()>,
} }

View File

@@ -1,11 +1,9 @@
use std::path::Path; use crate::constants::{APP_DB_DATA_DIR, DB_COLUMNS};
use crate::constants::{DB_COLUMNS};
use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options}; use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options};
use serde::{Serialize}; use serde::{Serialize};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use crate::models::{RocksColumn, RocksReference, RocksReferences}; use crate::models::{RocksColumn, RocksReference, RocksReferences};
use color_eyre::Result; use color_eyre::Result;
use crate::config::types::ApplicationConfig;
pub struct RocksDB { pub struct RocksDB {
db: OptimisticTransactionDB, db: OptimisticTransactionDB,
@@ -13,13 +11,12 @@ pub struct RocksDB {
impl RocksDB { impl RocksDB {
pub fn new(db_opts: Options, cf_opts: Options) -> Result<Self> { pub fn new(db_opts: Options, cf_opts: Options) -> Result<Self> {
let app_conf = ApplicationConfig::get_config()?;
let cfs = DB_COLUMNS.iter() let cfs = DB_COLUMNS.iter()
.map(|cf| ColumnFamilyDescriptor::new(cf.to_string(), cf_opts.clone())) .map(|cf| ColumnFamilyDescriptor::new(cf.to_string(), cf_opts.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let db = OptimisticTransactionDB::open_cf_descriptors( let db = OptimisticTransactionDB::open_cf_descriptors(
&db_opts, &db_opts,
Path::new(&app_conf.basic_config.db_path), APP_DB_DATA_DIR.as_path(),
cfs cfs
)?; )?;
let rocks = Self { let rocks = Self {

View File

@@ -5,13 +5,11 @@ use color_eyre::eyre::eyre;
use color_eyre::owo_colors::OwoColorize; use color_eyre::owo_colors::OwoColorize;
use language_tags::LanguageTag; use language_tags::LanguageTag;
use tokio::fs; use tokio::fs;
use crate::config::types::ApplicationConfig; use crate::constants::{APP_CONFIG_DIR, APP_DATA_DIR, APP_DB_DATA_DIR};
use crate::constants::{APP_CONFIG_DIR, APP_DATA_DIR, SUPPORTED_LOCALES};
use crate::crawler::DLSITE_IMG_FOLDER; use crate::crawler::DLSITE_IMG_FOLDER;
pub async fn initialize_folders() -> color_eyre::Result<()> { pub async fn initialize_folders() -> color_eyre::Result<()> {
let app_conf = ApplicationConfig::get_config()?;
if !APP_CONFIG_DIR.exists() { if !APP_CONFIG_DIR.exists() {
fs::create_dir_all(APP_CONFIG_DIR.as_path()).await?; fs::create_dir_all(APP_CONFIG_DIR.as_path()).await?;
} }
@@ -21,9 +19,8 @@ pub async fn initialize_folders() -> color_eyre::Result<()> {
if !DLSITE_IMG_FOLDER.exists() { if !DLSITE_IMG_FOLDER.exists() {
fs::create_dir_all(DLSITE_IMG_FOLDER.as_path()).await?; fs::create_dir_all(DLSITE_IMG_FOLDER.as_path()).await?;
} }
let db_path = Path::new(&app_conf.basic_config.db_path); if !APP_DB_DATA_DIR.exists() {
if !db_path.exists() { fs::create_dir_all(APP_DB_DATA_DIR.as_path()).await?;
fs::create_dir_all(db_path).await?;
} }
Ok(()) Ok(())
} }

View File

@@ -1 +1,5 @@
pub mod folder; pub mod folder;
pub enum AppPopup {
AddFolder(folder::AddFolderPopup)
}

View File

@@ -2,21 +2,20 @@ use crate::config::types::ApplicationConfig;
use crate::widgets::popups::folder::AddFolderPopup; use crate::widgets::popups::folder::AddFolderPopup;
use crate::widgets::views::View; use crate::widgets::views::View;
use crossterm::event::KeyCode::Char; use crossterm::event::KeyCode::Char;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{Event, KeyCode, KeyEventKind};
use rat_cursor::HasScreenCursor; use rat_cursor::HasScreenCursor;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Color, Line, Span, Style, Text, Widget}; use ratatui::prelude::{Color, Line, Span, Style, Text, Widget};
use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget}; use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget};
use std::any::Any; use crate::widgets::popups::AppPopup;
pub struct MainView { pub struct MainView {
pub state: MainViewState, pub state: MainViewState,
} }
#[derive(Debug)]
pub struct MainViewState { pub struct MainViewState {
popup: Option<Box<dyn Any>>, popup: Option<AppPopup>,
status: Status, status: Status,
} }
@@ -46,42 +45,45 @@ impl MainView {
} }
fn folder_popup(&mut self) { fn folder_popup(&mut self) {
self.state.popup = Some(Box::new(AddFolderPopup::new())); self.state.popup = Some(AppPopup::AddFolder(AddFolderPopup::new()));
self.state.status = Status::Popup; self.state.status = Status::Popup;
} }
} }
impl View for MainView { impl View for MainView {
fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> { fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> {
if let Some(any) = self.state.popup.as_mut() if let Some(current_popup) = self.state.popup.as_mut() {
&& let Some(popup) = any.downcast_mut::<AddFolderPopup>() match current_popup {
{ AppPopup::AddFolder(folder_popup) => {
popup.textarea.handle_input(event)?; folder_popup.textarea.handle_input(event)?;
} if let Event::Key(key) = event &&
Ok(()) key.code.is_enter() &&
} let Some(value) = folder_popup.get_folder_value()
{
let mut config = ApplicationConfig::get_config()?;
config.path_config.dlsite_paths.push(value);
fn handle_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()> { folder_popup.textarea.reset_value()?;
if matches!(self.state.status, Status::Popup) && matches!(key.code, KeyCode::Esc) { config.save()?;
self.state.status = Status::Running; }
self.state.popup = None; }
}
} }
if let Some(any) = self.state.popup.as_mut() && if let Event::Key(key_event) = event {
let Some(popup) = any.downcast_mut::<AddFolderPopup>() && if matches!(self.state.status, Status::Popup) &&
let Some(value) = popup.get_folder_value() && matches!(key_event.code, KeyCode::Esc)
key.code.is_enter() {
{ self.state.status = Status::Running;
let mut config = ApplicationConfig::get_config()?; self.state.popup = None;
config.path_config.dlsite_paths.push(value); }
if !matches!(self.state.status, Status::Popup) &&
popup.textarea.reset_value()?; matches!(key_event.kind, KeyEventKind::Press)
config.save()?; {
} match key_event.code {
if !matches!(self.state.status, Status::Popup) && matches!(key.kind, KeyEventKind::Press) { Char('q') => self.quit()?,
match key.code { Char('a') => self.folder_popup(),
Char('q') => self.quit()?, _ => {}
Char('a') => self.folder_popup(), }
_ => {}
} }
} }
Ok(()) Ok(())
@@ -111,22 +113,27 @@ impl StatefulWidget for MainView {
Self::render_game_list(chunks[1], buf); Self::render_game_list(chunks[1], buf);
Self::render_footer(state, chunks[2], buf); Self::render_footer(state, chunks[2], buf);
if let Some(boxed) = state.popup.as_mut() let Some(popup) = state.popup.as_mut() else {
&& let Some(popup) = boxed.downcast_mut::<AddFolderPopup>() return;
{ };
popup.clone().render(area, buf, popup); match popup {
AppPopup::AddFolder(popup) => {
popup.clone().render(area, buf, popup);
}
} }
} }
} }
impl HasScreenCursor for MainView { impl HasScreenCursor for MainView {
fn screen_cursor(&self) -> Option<(u16, u16)> { fn screen_cursor(&self) -> Option<(u16, u16)> {
if let Some(popup) = &self.state.popup let Some(popup) = &self.state.popup else {
&& let Some(add_folder) = popup.downcast_ref::<AddFolderPopup>() return None;
{ };
return add_folder.textarea.screen_cursor(); match popup {
AppPopup::AddFolder(popup) => {
popup.textarea.screen_cursor()
}
} }
None
} }
} }

View File

@@ -1,10 +1,13 @@
mod main_view; mod main_view;
use crossterm::event::{Event, KeyEvent}; use crossterm::event::{Event};
pub use main_view::MainView; pub use main_view::MainView;
pub trait View { pub trait View {
fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()>; fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()>;
fn handle_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()>;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
} }
pub enum AppView {
MainView(MainView),
}