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 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 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 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 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 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 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}