mal/config/
app_config.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4};
5
6use super::*;
7use crate::{
8    api::model::{AnimeRankingType, MangaRankingType},
9    event::key::Key,
10};
11use log::LevelFilter;
12use ratatui::style::Color;
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Deserialize, Serialize)]
16pub struct AppConfig {
17    #[serde(skip_deserializing, skip_serializing)]
18    pub paths: CachePaths,
19    pub keys: KeyBindings,
20    pub theme: Theme,
21    pub behavior: BehaviorConfig,
22    pub nsfw: bool,
23    pub title_language: TitleLanguage,
24    pub manga_display_type: MangaDisplayType,
25    // pub first_top_three_block: TopThreeBlock,
26    pub top_three_anime_types: Vec<AnimeRankingType>,
27    pub top_three_manga_types: Vec<MangaRankingType>,
28    pub navigation_stack_limit: u32,
29    pub search_limit: u64,
30    pub log_level: LevelFilter,
31    pub max_cached_images: u16,
32}
33
34#[derive(Clone, Deserialize, Serialize, Debug)]
35pub enum TitleLanguage {
36    Japanese,
37    English,
38}
39
40#[derive(Copy, Deserialize, Serialize, Clone, Debug)]
41pub struct Theme {
42    pub mal_color: Color,
43    pub active: Color,
44    pub banner: Color,
45    pub hovered: Color,
46    pub text: Color,
47    pub selected: Color,
48    pub error_border: Color,
49    pub error_text: Color,
50    pub inactive: Color,
51    pub status_completed: Color,
52    pub status_dropped: Color,
53    pub status_on_hold: Color,
54    pub status_watching: Color,
55    pub status_plan_to_watch: Color,
56    pub status_other: Color,
57}
58
59impl Default for Theme {
60    fn default() -> Self {
61        Self {
62            mal_color: Color::Rgb(46, 81, 162),
63            active: Color::Cyan,
64            banner: Color::Rgb(46, 81, 162),
65            hovered: Color::Magenta,
66            selected: Color::LightCyan,
67            text: Color::White,
68            error_border: Color::Red,
69            error_text: Color::LightRed,
70            inactive: Color::Gray,
71            status_completed: Color::Green,
72            status_dropped: Color::Gray,
73            status_on_hold: Color::Yellow,
74            status_watching: Color::Blue,
75            status_plan_to_watch: Color::LightMagenta,
76            status_other: Color::DarkGray,
77        }
78    }
79}
80
81#[derive(Clone, Deserialize, Serialize)]
82pub struct KeyBindings {
83    pub help: Key,
84    pub back: Key,
85    pub search: Key,
86    pub toggle: Key,
87    pub next_state: Key,
88    pub open_popup: Key,
89}
90
91#[derive(Clone, Deserialize, Serialize)]
92pub struct BehaviorConfig {
93    // pub show_loading_indicator: bool,
94    // pub seek_milliseconds: u64,
95    pub tick_rate_milliseconds: u64,
96    pub show_logger: bool,
97}
98
99#[derive(Clone, Debug, Deserialize, Serialize)]
100pub enum MangaDisplayType {
101    Vol,
102    Ch,
103    Both,
104}
105
106impl AppConfig {
107    pub fn new() -> Result<Self, ConfigError> {
108        let paths = get_cache_dir()?;
109
110        Ok(Self {
111            paths,
112            theme: Theme::default(),
113            keys: KeyBindings {
114                help: Key::Char('?'),
115                back: Key::Char('q'),
116                search: Key::Char('/'),
117                toggle: Key::Char('s'),
118                open_popup: Key::Char('r'),
119                next_state: Key::Ctrl('p'),
120            },
121            behavior: BehaviorConfig {
122                tick_rate_milliseconds: 500,
123                show_logger: false,
124            },
125            nsfw: false,
126            title_language: TitleLanguage::English,
127            manga_display_type: MangaDisplayType::Both,
128            top_three_anime_types: vec![
129                AnimeRankingType::Airing,
130                AnimeRankingType::All,
131                AnimeRankingType::Upcoming,
132                AnimeRankingType::Movie,
133                AnimeRankingType::Special,
134                AnimeRankingType::OVA,
135                AnimeRankingType::TV,
136                AnimeRankingType::ByPopularity,
137                AnimeRankingType::Favorite,
138            ],
139            top_three_manga_types: vec![
140                MangaRankingType::All,
141                MangaRankingType::Manga,
142                MangaRankingType::Novels,
143                MangaRankingType::OneShots,
144                MangaRankingType::Doujinshi,
145                MangaRankingType::Manhwa,
146                MangaRankingType::Manhua,
147                MangaRankingType::ByPopularity,
148                MangaRankingType::Favorite,
149            ],
150            navigation_stack_limit: 15,
151            search_limit: 30,
152            max_cached_images: 15,
153            log_level: LevelFilter::Debug,
154        })
155    }
156
157    pub fn load() -> Result<Self, ConfigError> {
158        // check file exists
159        // do not get paths from config file,always use the default paths
160        let config_file = dirs::home_dir()
161            .ok_or(ConfigError::PathError)?
162            .join(CONFIG_DIR)
163            .join(APP_CONFIG_DIR)
164            .join(_CONFIG_FILE);
165        if !config_file.exists() {
166            // if config file doesn't exist, create default config
167            fs::create_dir_all(config_file.parent().unwrap())?;
168            let default_config = Self::new()?;
169
170            fs::write(&config_file, serde_yaml::to_string(&default_config)?)?;
171            Ok(default_config)
172        } else {
173            // if config file exists, read it
174            let content = fs::read_to_string(&config_file).map_err(|_| ConfigError::ReadError)?;
175            let config: Self = serde_yaml::from_str(&content).map_err(ConfigError::ParseError)?;
176
177            Ok(config)
178        }
179    }
180}
181
182fn get_cache_dir() -> Result<CachePaths, ConfigError> {
183    match dirs::home_dir() {
184        Some(home) => {
185            let path = Path::new(&home);
186
187            // cache dir:
188            let home_cache_dir = path.join(CACHE_DIR);
189
190            let cache_dir = home_cache_dir.join(APP_CACHE_DIR);
191
192            let picture_cache_dir = cache_dir.join(PICTURE_CACHE_DIR);
193
194            let data_file_path = cache_dir.join(DATA_FILE);
195
196            if !home_cache_dir.exists() {
197                fs::create_dir(&home_cache_dir)?;
198            }
199            if !cache_dir.exists() {
200                fs::create_dir(&cache_dir)?;
201            }
202
203            if !picture_cache_dir.exists() {
204                fs::create_dir(&picture_cache_dir)?;
205            }
206
207            let paths = CachePaths {
208                picture_cache_dir_path: picture_cache_dir.to_path_buf(),
209                data_file_path,
210            };
211
212            Ok(paths)
213        }
214        None => Err(ConfigError::PathError),
215    }
216}
217
218#[derive(Clone, Debug, Deserialize, Serialize)]
219pub struct CachePaths {
220    pub picture_cache_dir_path: PathBuf,
221    pub data_file_path: PathBuf,
222}
223impl Default for CachePaths {
224    fn default() -> Self {
225        get_cache_dir().ok().unwrap()
226    }
227}