1pub mod bookshelf;
2pub mod build;
3pub mod check;
4pub mod completions;
5pub mod download;
6pub mod epub;
7pub mod info;
8pub mod read;
9pub mod real_cugan;
10pub mod search;
11pub mod sign;
12pub mod template;
13pub mod transform;
14pub mod unzip;
15pub mod update;
16pub mod zip;
17
18use std::collections::HashMap;
19use std::fs::File;
20use std::io::{self, Read, Seek, Write};
21use std::path::Path;
22use std::sync::Arc;
23use std::{cmp, env, fs, process};
24
25use ::zip::write::SimpleFileOptions;
26use ::zip::{CompressionMethod, ZipArchive, ZipWriter};
27use clap::ValueEnum;
28use color_eyre::eyre::Result;
29use fluent_templates::Loader;
30use fluent_templates::fluent_bundle::FluentValue;
31use novel_api::Client;
32use ratatui::buffer::Buffer;
33use ratatui::layout::{Rect, Size};
34use ratatui::text::Text;
35use ratatui::widgets::block::Title;
36use ratatui::widgets::{Block, Paragraph, StatefulWidget, Widget, Wrap};
37use strum::AsRefStr;
38use tokio::signal;
39use tui_widgets::scrollview::{ScrollView, ScrollViewState};
40use url::Url;
41use walkdir::DirEntry;
42
43use crate::{LANG_ID, LOCALES, utils};
44
45const DEFAULT_PROXY: &str = "http://127.0.0.1:8080";
46
47const DEFAULT_PROXY_SURGE: &str = "http://127.0.0.1:6152";
48
49#[must_use]
50#[derive(Clone, PartialEq, ValueEnum, AsRefStr)]
51pub enum Source {
52 #[strum(serialize = "sfacg")]
53 Sfacg,
54 #[strum(serialize = "ciweimao")]
55 Ciweimao,
56 #[strum(serialize = "ciyuanji")]
57 Ciyuanji,
58}
59
60#[must_use]
61#[derive(Clone, PartialEq, ValueEnum, AsRefStr)]
62pub enum Format {
63 Pandoc,
64 Mdbook,
65}
66
67#[must_use]
68#[derive(Clone, PartialEq, ValueEnum, AsRefStr)]
69pub enum Convert {
70 S2T,
71 T2S,
72 JP2T2S,
73 CUSTOM,
74}
75
76#[inline]
77#[must_use]
78fn default_cert_path() -> String {
79 novel_api::home_dir_path()
80 .unwrap()
81 .join(".mitmproxy")
82 .join("mitmproxy-ca-cert.pem")
83 .display()
84 .to_string()
85}
86
87fn set_options<T, E>(client: &mut T, proxy: &Option<Url>, no_proxy: &bool, cert: &Option<E>)
88where
89 T: Client,
90 E: AsRef<Path>,
91{
92 if let Some(proxy) = proxy {
93 client.proxy(proxy.clone());
94 }
95
96 if *no_proxy {
97 client.no_proxy();
98 }
99
100 if let Some(cert) = cert {
101 client.cert(cert.as_ref().to_path_buf())
102 }
103}
104
105fn handle_ctrl_c<T>(client: &Arc<T>)
106where
107 T: Client + Send + Sync + 'static,
108{
109 let client = Arc::clone(client);
110
111 tokio::spawn(async move {
112 signal::ctrl_c().await.unwrap();
113
114 tracing::warn!("Download terminated, login data will be saved");
115
116 client.shutdown().await.unwrap();
117 process::exit(128 + libc::SIGINT);
118 });
119}
120
121fn cert_help_msg() -> String {
122 let args = {
123 let mut map = HashMap::new();
124 map.insert(
125 "cert_path".into(),
126 FluentValue::String(default_cert_path().into()),
127 );
128 map
129 };
130
131 LOCALES.lookup_with_args(&LANG_ID, "cert", &args)
132}
133
134#[derive(Default, PartialEq)]
135enum Mode {
136 #[default]
137 Running,
138 Quit,
139}
140
141pub struct ScrollableParagraph<'a> {
142 title: Option<Title<'a>>,
143 text: Text<'a>,
144}
145
146impl<'a> ScrollableParagraph<'a> {
147 pub fn new<T>(text: T) -> Self
148 where
149 T: Into<Text<'a>>,
150 {
151 ScrollableParagraph {
152 title: None,
153 text: text.into(),
154 }
155 }
156
157 pub fn title<T>(self, title: T) -> Self
158 where
159 T: Into<Title<'a>>,
160 {
161 ScrollableParagraph {
162 title: Some(title.into()),
163 ..self
164 }
165 }
166}
167
168impl StatefulWidget for ScrollableParagraph<'_> {
169 type State = ScrollViewState;
170
171 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
172 let mut block = Block::bordered();
173 if self.title.is_some() {
174 block = block.title(self.title.as_ref().unwrap().clone());
175 }
176
177 let paragraph = Paragraph::new(self.text.clone()).wrap(Wrap { trim: false });
178 let mut scroll_view = ScrollView::new(Size::new(area.width - 1, area.height));
179 let mut block_area = block.inner(scroll_view.buf().area);
180
181 let scroll_height = cmp::max(
182 paragraph.line_count(block_area.width) as u16
183 + (scroll_view.buf().area.height - block_area.height),
184 area.height,
185 );
186
187 let scroll_width = if area.height >= scroll_height {
188 area.width
190 } else {
191 area.width - 1
192 };
193
194 scroll_view = ScrollView::new(Size::new(scroll_width, scroll_height));
195
196 let scroll_view_buf = scroll_view.buf_mut();
197 block_area = block.inner(scroll_view_buf.area);
198
199 Widget::render(block, scroll_view_buf.area, scroll_view_buf);
200 Widget::render(paragraph, block_area, scroll_view_buf);
201 StatefulWidget::render(scroll_view, area, buf, state);
202 }
203}
204
205fn unzip<T>(path: T) -> Result<()>
206where
207 T: AsRef<Path>,
208{
209 let path = path.as_ref();
210
211 let output_dir = env::current_dir()?.join(path.file_stem().unwrap());
212 if output_dir.try_exists()? {
213 tracing::warn!("The epub output directory already exists and will be deleted");
214 utils::remove_file_or_dir(&output_dir)?;
215 }
216
217 let file = File::open(path)?;
218 let mut archive = ZipArchive::new(file)?;
219
220 for i in 0..archive.len() {
221 let mut file = archive.by_index(i)?;
222 let outpath = match file.enclosed_name() {
223 Some(path) => path.to_owned(),
224 None => continue,
225 };
226 let outpath = output_dir.join(outpath);
227
228 if (*file.name()).ends_with('/') {
229 fs::create_dir_all(&outpath)?;
230 } else {
231 if let Some(p) = outpath.parent()
232 && !p.try_exists()?
233 {
234 fs::create_dir_all(p)?;
235 }
236 let mut outfile = fs::File::create(&outpath)?;
237 io::copy(&mut file, &mut outfile)?;
238 }
239
240 #[cfg(unix)]
241 {
242 use std::fs::Permissions;
243 use std::os::unix::fs::PermissionsExt;
244
245 if let Some(mode) = file.unix_mode() {
246 fs::set_permissions(&outpath, Permissions::from_mode(mode))?;
247 }
248 }
249 }
250
251 Ok(())
252}
253
254fn zip_dir<T, E>(iter: &mut dyn Iterator<Item = DirEntry>, prefix: T, writer: E) -> Result<()>
255where
256 T: AsRef<Path>,
257 E: Write + Seek,
258{
259 let mut zip = ZipWriter::new(writer);
260 let options = SimpleFileOptions::default()
261 .compression_method(CompressionMethod::Deflated)
262 .compression_level(Some(9));
263
264 let mut buffer = Vec::new();
265 for entry in iter {
266 let path = entry.path();
267 let name = path.strip_prefix(prefix.as_ref())?;
268
269 if path.is_file() {
270 zip.start_file(name.to_str().unwrap(), options)?;
271 let mut f = File::open(path)?;
272
273 f.read_to_end(&mut buffer)?;
274 zip.write_all(&buffer)?;
275 buffer.clear();
276 } else if !name.as_os_str().is_empty() {
277 zip.add_directory(name.to_str().unwrap(), options)?;
278 }
279 }
280 zip.finish()?;
281
282 Ok(())
283}