From 77ff636d9200162ca98add273fbdf7c3e8350315 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Mon, 12 Aug 2024 16:20:09 -0500 Subject: [PATCH 01/14] feat(MangaPage): add function to download all chapters of manga (rough) --- src/backend/download.rs | 46 ++++++++++++++++++++++++++++++++++++++++- src/backend/fetch.rs | 17 +++++++++++++++ src/view/pages/manga.rs | 40 ++++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index 817e4ad..c54e7d1 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -8,7 +8,8 @@ use crate::view::pages::manga::MangaPageEvents; use super::error_log::{write_to_error_log, ErrorType}; use super::fetch::MangadexClient; -use super::{ChapterPagesResponse, APP_DATA_DIR}; +use super::filter::Languages; +use super::{ChapterPagesResponse, ChapterResponse, APP_DATA_DIR}; pub struct DownloadChapter<'a> { pub id_chapter: &'a str, @@ -100,3 +101,46 @@ pub fn download_chapter( Ok(()) } + +pub struct DownloadAllChapters { + pub manga_id: String, + pub manga_title: String, +} + +pub fn download_all_chapters( + chapter_data: ChapterResponse, + manga_details: DownloadAllChapters, + tx: UnboundedSender, +) { + for chapter in chapter_data.data { + let id = chapter.id.clone(); + let manga_id = manga_details.manga_id.clone(); + let manga_title = manga_details.manga_title.clone(); + let tx = tx.clone(); + tokio::spawn(async move { + let pages_response = MangadexClient::global().get_chapter_pages(&id).await; + + match pages_response { + Ok(res) => { + download_chapter( + DownloadChapter { + id_chapter: &chapter.id, + manga_id: &manga_id, + manga_title: &manga_title, + title: chapter.attributes.title.unwrap_or_default().as_str(), + number: chapter.attributes.chapter.unwrap_or_default().as_str(), + scanlator: "some_scanlator", + lang: &Languages::default().as_human_readable(), + }, + res, + tx, + ) + .unwrap(); + } + Err(e) => { + write_to_error_log(ErrorType::FromError(Box::new(e))); + } + } + }); + } +} diff --git a/src/backend/fetch.rs b/src/backend/fetch.rs index 03df8d5..3191507 100644 --- a/src/backend/fetch.rs +++ b/src/backend/fetch.rs @@ -206,4 +206,21 @@ impl MangadexClient { Ok(self.client.get(endpoint).send().await?.status()) } + + pub async fn get_all_chapters_for_manga( + &self, + id: &str, + language: Languages, + ) -> Result { + let language = language.as_iso_code(); + + let order = "order[volume]=asc&order[chapter]=asc".to_string(); + + let endpoint = format!( + "{}/manga/{}/feed?limit=300&offset=0&{}&translatedLanguage[]={}&includes[]=scanlation_group&includeExternalUrl=0&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic", + API_URL_BASE, id, order, language + ); + + self.client.get(endpoint).send().await?.json().await + } } diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index bccaa12..8b44da5 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,6 +1,8 @@ use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; -use crate::backend::download::{download_chapter, DownloadChapter}; +use crate::backend::download::{ + download_all_chapters, download_chapter, DownloadAllChapters, DownloadChapter, +}; use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; use crate::backend::filter::Languages; @@ -25,6 +27,7 @@ use self::text::ToSpan; #[derive(PartialEq, Eq)] pub enum PageState { DownloadingChapters, + DownloadingAllChapters, SearchingChapters, SearchingChapterData, DisplayingChapters, @@ -33,6 +36,7 @@ pub enum PageState { pub enum MangaPageActions { DownloadChapter, + DownloadAllChapter, ScrollChapterDown, ScrollChapterUp, ToggleOrder, @@ -421,6 +425,11 @@ impl MangaPage { .send(MangaPageActions::DownloadChapter) .ok(); } + KeyCode::Esc => { + self.local_action_tx + .send(MangaPageActions::DownloadAllChapter) + .ok(); + } KeyCode::Char('c') => { self.local_action_tx .send(MangaPageActions::GoMangasAuthor) @@ -833,6 +842,34 @@ impl MangaPage { } } + fn download_all_chapters(&mut self) { + self.state == PageState::DownloadingAllChapters; + let id = self.manga.id.clone(); + let manga_title = self.manga.title.clone(); + let lang = self.get_current_selected_language(); + let tx = self.local_event_tx.clone(); + tokio::spawn(async move { + let chapter_response = MangadexClient::global() + .get_all_chapters_for_manga(&id, lang) + .await; + match chapter_response { + Ok(response) => { + download_all_chapters( + response, + DownloadAllChapters { + manga_title, + manga_id: id, + }, + tx, + ); + } + Err(e) => { + write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); + } + } + }); + } + fn handle_mouse_events(&mut self, mouse_event: MouseEvent) { if self.is_list_languages_open { match mouse_event.kind { @@ -918,6 +955,7 @@ impl Component for MangaPage { } fn update(&mut self, action: Self::Actions) { match action { + MangaPageActions::DownloadAllChapter => self.download_all_chapters(), MangaPageActions::SearchPreviousChapterPage => self.search_previous_chapters(), MangaPageActions::SearchNextChapterPage => self.search_next_chapters(), MangaPageActions::ScrollDownAvailbleLanguages => self.scroll_language_down(), From 4cc7194b5684da3fbc0801b12408bc8416028f54 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Tue, 13 Aug 2024 13:25:55 -0500 Subject: [PATCH 02/14] refactor(MangaPage): changing download all chapters to prevent api rate limits --- src/backend/download.rs | 91 +++++++++++++++++++-- src/common.rs | 12 ++- src/view/pages/manga.rs | 169 +++++++++++++++++++++++++-------------- src/view/pages/reader.rs | 9 +-- 4 files changed, 203 insertions(+), 78 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index c54e7d1..1001795 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -1,9 +1,11 @@ use manga_tui::exists; use std::fs::{create_dir, File}; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; +use crate::common::PageType; use crate::view::pages::manga::MangaPageEvents; use super::error_log::{write_to_error_log, ErrorType}; @@ -21,11 +23,9 @@ pub struct DownloadChapter<'a> { pub lang: &'a str, } -pub fn download_chapter( - chapter: DownloadChapter<'_>, - chapter_data: ChapterPagesResponse, - tx: UnboundedSender, -) -> Result<(), std::io::Error> { +fn create_manga_directory( + chapter: &DownloadChapter<'_>, +) -> Result<(PathBuf, String), std::io::Error> { // need directory with the manga's title, and its id to make it unique let chapter_id = chapter.id_chapter.to_string(); @@ -62,6 +62,16 @@ pub fn download_chapter( create_dir(&chapter_dir)?; } + Ok((chapter_dir, chapter_id)) +} + +pub fn download_single_chaper( + chapter: DownloadChapter<'_>, + chapter_data: ChapterPagesResponse, + tx: UnboundedSender, +) -> Result<(), std::io::Error> { + let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; + let total_chapters = chapter_data.chapter.data.len(); tokio::spawn(async move { @@ -102,6 +112,64 @@ pub fn download_chapter( Ok(()) } +pub fn download_chapter( + chapter: DownloadChapter<'_>, + chapter_data: ChapterPagesResponse, + chapter_quality: PageType, + tx: UnboundedSender, +) -> Result<(), std::io::Error> { + let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; + + let total_chapters = chapter_data.chapter.data.len(); + + let files = match chapter_quality { + PageType::HighQuality => chapter_data.chapter.data, + PageType::LowQuality => chapter_data.chapter.data_saver, + }; + + tokio::spawn(async move { + for (index, file_name) in files.into_iter().enumerate() { + let endpoint = format!( + "{}/{}/{}", + chapter_data.base_url, chapter_quality, chapter_data.chapter.hash + ); + + let image_response = MangadexClient::global() + .get_chapter_page(&endpoint, &file_name) + .await; + + let file_name = Path::new(&file_name); + + match image_response { + Ok(bytes) => { + let image_name = format!( + "{}.{}", + index + 1, + file_name.extension().unwrap().to_str().unwrap() + ); + if exists!(&chapter_dir.join(&image_name)) { + return; + } + + let mut image_created = File::create(chapter_dir.join(image_name)).unwrap(); + image_created.write_all(&bytes).unwrap(); + + // tx.send(MangaPageEvents::SetDownloadProgress( + // (index as f64) / (total_chapters as f64), + // chapter_id.clone(), + // )) + // .ok(); + } + Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), + } + } + // tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) + // .ok(); + }); + + Ok(()) +} + pub struct DownloadAllChapters { pub manga_id: String, pub manga_title: String, @@ -117,6 +185,13 @@ pub fn download_all_chapters( let manga_id = manga_details.manga_id.clone(); let manga_title = manga_details.manga_title.clone(); let tx = tx.clone(); + + let scanlator = chapter + .relationships + .iter() + .find(|rel| rel.type_field == "scanlation_group") + .map(|rel| rel.attributes.as_ref().unwrap().name.to_string()); + tokio::spawn(async move { let pages_response = MangadexClient::global().get_chapter_pages(&id).await; @@ -129,10 +204,11 @@ pub fn download_all_chapters( manga_title: &manga_title, title: chapter.attributes.title.unwrap_or_default().as_str(), number: chapter.attributes.chapter.unwrap_or_default().as_str(), - scanlator: "some_scanlator", + scanlator: &scanlator.unwrap_or_default(), lang: &Languages::default().as_human_readable(), }, res, + PageType::LowQuality, tx, ) .unwrap(); @@ -142,5 +218,6 @@ pub fn download_all_chapters( } } }); + std::thread::sleep(Duration::from_secs(3)); } } diff --git a/src/common.rs b/src/common.rs index a9241be..dc130c6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,3 +1,5 @@ +use strum::Display; + use crate::backend::filter::Languages; #[derive(Default, Clone, Debug)] @@ -18,7 +20,7 @@ pub struct Manga { pub title: String, pub description: String, pub content_rating: String, - pub publication_demographic : String, + pub publication_demographic: String, pub tags: Vec, pub status: String, pub img_url: Option, @@ -27,3 +29,11 @@ pub struct Manga { pub available_languages: Vec, pub created_at: String, } + +#[derive(Display)] +pub enum PageType { + #[strum(to_string = "data")] + HighQuality, + #[strum(to_string = "data-saver")] + LowQuality, +} diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 8b44da5..0dedb78 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,7 +1,7 @@ use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; use crate::backend::download::{ - download_all_chapters, download_chapter, DownloadAllChapters, DownloadChapter, + download_all_chapters, download_single_chaper, DownloadAllChapters, DownloadChapter, }; use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; @@ -14,7 +14,9 @@ use crate::utils::{set_status_style, set_tags_style}; use crate::view::widgets::manga::{ChapterItem, ChaptersListWidget}; use crate::view::widgets::Component; use crate::PICKER; -use crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind}; +use crossterm::event::{ + KeyCode, KeyEvent, KeyModifiers, ModifierKeyCode, MouseEvent, MouseEventKind, +}; use ratatui::{prelude::*, widgets::*}; use ratatui_image::protocol::StatefulProtocol; use ratatui_image::{Resize, StatefulImage}; @@ -27,7 +29,8 @@ use self::text::ToSpan; #[derive(PartialEq, Eq)] pub enum PageState { DownloadingChapters, - DownloadingAllChapters, + IsAskingDownloadAllChapters, + DownloadAllChapters, SearchingChapters, SearchingChapterData, DisplayingChapters, @@ -36,7 +39,9 @@ pub enum PageState { pub enum MangaPageActions { DownloadChapter, - DownloadAllChapter, + ConfirmDownloadAll, + NegateDownloadAll, + AskDownloadAllChapters, ScrollChapterDown, ScrollChapterUp, ToggleOrder, @@ -248,6 +253,18 @@ impl MangaPage { let [sorting_buttons_area, chapters_area] = layout.areas(area); + if self.state == PageState::IsAskingDownloadAllChapters { + Block::bordered().render(area, buf); + let download_information_area = area.inner(Margin { + horizontal: 2, + vertical: 2, + }); + Paragraph::new("do you want to download all chapters? Yes : , no : ") + .render(download_information_area, buf); + + return; + } + match self.chapters.as_mut() { Some(chapters) => { let tota_pages = chapters.total_result as f64 / 16_f64; @@ -397,66 +414,82 @@ impl MangaPage { _ => {} } } else if self.state != PageState::SearchingChapterData { - match key_event.code { - KeyCode::Char('j') | KeyCode::Down => { - self.local_action_tx - .send(MangaPageActions::ScrollChapterDown) - .ok(); - } - KeyCode::Char('k') | KeyCode::Up => { - self.local_action_tx - .send(MangaPageActions::ScrollChapterUp) - .ok(); - } - KeyCode::Char('t') => { - self.local_action_tx - .send(MangaPageActions::ToggleOrder) - .ok(); - } - KeyCode::Char('r') | KeyCode::Enter => { - if PICKER.is_some() { + if self.state == PageState::IsAskingDownloadAllChapters { + match key_event.code { + KeyCode::Esc => { self.local_action_tx - .send(MangaPageActions::ReadChapter) + .send(MangaPageActions::NegateDownloadAll) .ok(); } + KeyCode::Enter => { + self.local_action_tx + .send(MangaPageActions::ConfirmDownloadAll) + .ok(); + } + _ => {} } - KeyCode::Char('d') => { - self.local_action_tx - .send(MangaPageActions::DownloadChapter) - .ok(); - } - KeyCode::Esc => { - self.local_action_tx - .send(MangaPageActions::DownloadAllChapter) - .ok(); - } - KeyCode::Char('c') => { - self.local_action_tx - .send(MangaPageActions::GoMangasAuthor) - .ok(); - } - KeyCode::Char('v') => { - self.local_action_tx - .send(MangaPageActions::GoMangasArtist) - .ok(); - } - KeyCode::Char('l') | KeyCode::Esc => { - self.local_action_tx - .send(MangaPageActions::OpenAvailableLanguagesList) - .ok(); - } - KeyCode::Char('w') => { - self.local_action_tx - .send(MangaPageActions::SearchNextChapterPage) - .ok(); - } - KeyCode::Char('b') => { - self.local_action_tx - .send(MangaPageActions::SearchPreviousChapterPage) - .ok(); - } + } else { + match key_event.code { + KeyCode::Char('j') | KeyCode::Down => { + self.local_action_tx + .send(MangaPageActions::ScrollChapterDown) + .ok(); + } + KeyCode::Char('k') | KeyCode::Up => { + self.local_action_tx + .send(MangaPageActions::ScrollChapterUp) + .ok(); + } + KeyCode::Char('t') => { + self.local_action_tx + .send(MangaPageActions::ToggleOrder) + .ok(); + } + KeyCode::Char('r') | KeyCode::Enter => { + if PICKER.is_some() { + self.local_action_tx + .send(MangaPageActions::ReadChapter) + .ok(); + } + } + KeyCode::Char('d') => { + self.local_action_tx + .send(MangaPageActions::DownloadChapter) + .ok(); + } + KeyCode::Char('a') => { + self.local_action_tx + .send(MangaPageActions::AskDownloadAllChapters) + .ok(); + } + KeyCode::Char('c') => { + self.local_action_tx + .send(MangaPageActions::GoMangasAuthor) + .ok(); + } + KeyCode::Char('v') => { + self.local_action_tx + .send(MangaPageActions::GoMangasArtist) + .ok(); + } + KeyCode::Char('l') => { + self.local_action_tx + .send(MangaPageActions::OpenAvailableLanguagesList) + .ok(); + } + KeyCode::Char('w') => { + self.local_action_tx + .send(MangaPageActions::SearchNextChapterPage) + .ok(); + } + KeyCode::Char('b') => { + self.local_action_tx + .send(MangaPageActions::SearchPreviousChapterPage) + .ok(); + } - _ => {} + _ => {} + } } } } @@ -693,7 +726,7 @@ impl MangaPage { .await; match manga_response { Ok(res) => { - let download_chapter_task = download_chapter( + let download_chapter_task = download_single_chaper( DownloadChapter { id_chapter: &chapter_id, manga_id: &manga_id, @@ -843,7 +876,7 @@ impl MangaPage { } fn download_all_chapters(&mut self) { - self.state == PageState::DownloadingAllChapters; + self.state == PageState::IsAskingDownloadAllChapters; let id = self.manga.id.clone(); let manga_title = self.manga.title.clone(); let lang = self.get_current_selected_language(); @@ -870,6 +903,16 @@ impl MangaPage { }); } + fn ask_download_all_chapters(&mut self) { + if self.state == PageState::DisplayingChapters { + self.state = PageState::IsAskingDownloadAllChapters; + } + } + + fn negate_download_all_chapters(&mut self) { + self.state = PageState::DisplayingChapters + } + fn handle_mouse_events(&mut self, mouse_event: MouseEvent) { if self.is_list_languages_open { match mouse_event.kind { @@ -955,7 +998,9 @@ impl Component for MangaPage { } fn update(&mut self, action: Self::Actions) { match action { - MangaPageActions::DownloadAllChapter => self.download_all_chapters(), + MangaPageActions::NegateDownloadAll => self.negate_download_all_chapters(), + MangaPageActions::AskDownloadAllChapters => self.ask_download_all_chapters(), + MangaPageActions::ConfirmDownloadAll => self.download_all_chapters(), MangaPageActions::SearchPreviousChapterPage => self.search_previous_chapters(), MangaPageActions::SearchNextChapterPage => self.search_next_chapters(), MangaPageActions::ScrollDownAvailbleLanguages => self.scroll_language_down(), diff --git a/src/view/pages/reader.rs b/src/view/pages/reader.rs index a7ec444..2c0804d 100644 --- a/src/view/pages/reader.rs +++ b/src/view/pages/reader.rs @@ -1,6 +1,7 @@ use crate::backend::error_log::{write_to_error_log, ErrorType}; use crate::backend::fetch::MangadexClient; use crate::backend::tui::Events; +use crate::common::PageType; use crate::global::INSTRUCTIONS_STYLE; use crate::view::widgets::reader::{PageItemState, PagesItem, PagesList}; use crate::view::widgets::Component; @@ -35,14 +36,6 @@ pub enum MangaReaderEvents { LoadPage(Option), } -#[derive(Display)] -pub enum PageType { - #[strum(to_string = "data")] - HighQuality, - #[strum(to_string = "data-saver")] - LowQuality, -} - pub struct Page { pub image_state: Option>, pub url: String, From f66b1e3e67d3d6dc891654d53ad23d2c371f8f05 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Tue, 13 Aug 2024 15:37:50 -0500 Subject: [PATCH 03/14] feat(MangaPage): make functions to let user select image quality when downloading --- src/backend/download.rs | 6 +- src/common.rs | 21 ++++++- src/view/pages/manga.rs | 120 +++++++++++++++++++++++++++++++++++----- 3 files changed, 128 insertions(+), 19 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index 1001795..2d2e7fb 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -173,6 +173,7 @@ pub fn download_chapter( pub struct DownloadAllChapters { pub manga_id: String, pub manga_title: String, + pub quality: PageType, } pub fn download_all_chapters( @@ -184,6 +185,7 @@ pub fn download_all_chapters( let id = chapter.id.clone(); let manga_id = manga_details.manga_id.clone(); let manga_title = manga_details.manga_title.clone(); + let tx = tx.clone(); let scanlator = chapter @@ -208,7 +210,7 @@ pub fn download_all_chapters( lang: &Languages::default().as_human_readable(), }, res, - PageType::LowQuality, + manga_details.quality, tx, ) .unwrap(); @@ -218,6 +220,6 @@ pub fn download_all_chapters( } } }); - std::thread::sleep(Duration::from_secs(3)); + std::thread::sleep(Duration::from_secs(4)); } } diff --git a/src/common.rs b/src/common.rs index dc130c6..eed875f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,5 @@ -use strum::Display; +use ratatui::widgets::ListItem; +use strum::{Display, EnumIter}; use crate::backend::filter::Languages; @@ -30,10 +31,26 @@ pub struct Manga { pub created_at: String, } -#[derive(Display)] +#[derive(Display, Clone, Copy, EnumIter)] pub enum PageType { #[strum(to_string = "data")] HighQuality, #[strum(to_string = "data-saver")] LowQuality, } + +impl PageType { + pub fn toggle(self) -> Self { + match self { + Self::LowQuality => Self::HighQuality, + Self::HighQuality => Self::LowQuality, + } + } + + pub fn as_human_readable(&self) -> &str { + match self { + Self::LowQuality => "Low quality", + Self::HighQuality => "High quality", + } + } +} diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 0dedb78..c4f16c4 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -8,7 +8,7 @@ use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; use crate::backend::filter::Languages; use crate::backend::tui::Events; use crate::backend::{ChapterResponse, MangaStatisticsResponse, Statistics}; -use crate::common::Manga; +use crate::common::{Manga, PageType}; use crate::global::{ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::{set_status_style, set_tags_style}; use crate::view::widgets::manga::{ChapterItem, ChaptersListWidget}; @@ -30,7 +30,8 @@ use self::text::ToSpan; pub enum PageState { DownloadingChapters, IsAskingDownloadAllChapters, - DownloadAllChapters, + SettingDownloadQuality, + DownloadingAllChapters, SearchingChapters, SearchingChapterData, DisplayingChapters, @@ -39,6 +40,8 @@ pub enum PageState { pub enum MangaPageActions { DownloadChapter, + DownloadAllChapter, + ToggleImageQuality, ConfirmDownloadAll, NegateDownloadAll, AskDownloadAllChapters, @@ -60,8 +63,11 @@ pub enum MangaPageEvents { FethStatistics, CheckChapterStatus, ChapterFinishedDownloading(String), + /// Percentage, id chapter SetDownloadProgress(f64, String), + /// id_chapter, chapter_title SaveChapterDownloadStatus(String, String), + /// id_chapter DownloadError(String), ReadError(String), ReadSuccesful, @@ -101,6 +107,7 @@ pub struct MangaPage { state: PageState, statistics: Option, tasks: JoinSet<()>, + image_quality: PageType, available_languages_state: ListState, is_list_languages_open: bool, } @@ -156,6 +163,7 @@ impl MangaPage { tasks: JoinSet::new(), available_languages_state: ListState::default(), is_list_languages_open: false, + image_quality: PageType::LowQuality, chapter_language: chapter_language.unwrap_or(Languages::default()), } } @@ -253,16 +261,14 @@ impl MangaPage { let [sorting_buttons_area, chapters_area] = layout.areas(area); - if self.state == PageState::IsAskingDownloadAllChapters { - Block::bordered().render(area, buf); - let download_information_area = area.inner(Margin { - horizontal: 2, - vertical: 2, - }); - Paragraph::new("do you want to download all chapters? Yes : , no : ") - .render(download_information_area, buf); - - return; + match self.state { + PageState::IsAskingDownloadAllChapters + | PageState::SettingDownloadQuality + | PageState::DownloadingAllChapters => { + self.render_download_all_chapters_area(area, buf); + return; + } + _ => {} } match self.chapters.as_mut() { @@ -324,6 +330,51 @@ impl MangaPage { } } + fn render_download_all_chapters_area(&mut self, area: Rect, buf: &mut Buffer) { + Block::bordered().render(area, buf); + + let download_information_area = area.inner(Margin { + horizontal: 2, + vertical: 2, + }); + + match self.state { + PageState::IsAskingDownloadAllChapters => { + let instructions = vec![ + "Do you want to download all chapters? Yes: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + " no ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]; + + Paragraph::new(Line::from(instructions)).render(download_information_area, buf); + } + PageState::SettingDownloadQuality => { + Widget::render( + List::new([ + Line::from(vec![ + "Choose image quality ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]), + self.image_quality.as_human_readable().into(), + "Lower image quality is recommended for slow internet".into(), + Line::from(vec![ + "Start download: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]), + ]), + download_information_area, + buf, + ); + } + PageState::DownloadingAllChapters => { + Paragraph::new("Downloading all chapters, this will take a while") + .render(download_information_area, buf); + } + _ => {} + } + } + fn render_sorting_buttons(&mut self, area: Rect, buf: &mut Buffer) { let layout = Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)]); let [sorting_area, language_area] = layout.areas(area); @@ -388,6 +439,15 @@ impl MangaPage { } } + fn is_downloading_all_chapters(&self) -> bool { + matches!( + self.state, + PageState::DownloadingAllChapters + | PageState::SettingDownloadQuality + | PageState::IsAskingDownloadAllChapters + ) + } + fn handle_key_events(&mut self, key_event: KeyEvent) { if self.is_list_languages_open { match key_event.code { @@ -414,18 +474,30 @@ impl MangaPage { _ => {} } } else if self.state != PageState::SearchingChapterData { - if self.state == PageState::IsAskingDownloadAllChapters { + if self.is_downloading_all_chapters() { match key_event.code { KeyCode::Esc => { self.local_action_tx .send(MangaPageActions::NegateDownloadAll) .ok(); } + KeyCode::Char('t') => { + self.local_action_tx + .send(MangaPageActions::ToggleImageQuality) + .ok(); + } KeyCode::Enter => { self.local_action_tx .send(MangaPageActions::ConfirmDownloadAll) .ok(); } + KeyCode::Char(' ') => { + if self.state == PageState::SettingDownloadQuality { + self.local_action_tx + .send(MangaPageActions::DownloadAllChapter) + .ok(); + } + } _ => {} } } else { @@ -876,11 +948,12 @@ impl MangaPage { } fn download_all_chapters(&mut self) { - self.state == PageState::IsAskingDownloadAllChapters; + self.state == PageState::DownloadingAllChapters; let id = self.manga.id.clone(); let manga_title = self.manga.title.clone(); let lang = self.get_current_selected_language(); let tx = self.local_event_tx.clone(); + let quality = self.image_quality; tokio::spawn(async move { let chapter_response = MangadexClient::global() .get_all_chapters_for_manga(&id, lang) @@ -892,6 +965,7 @@ impl MangaPage { DownloadAllChapters { manga_title, manga_id: id, + quality, }, tx, ); @@ -909,10 +983,24 @@ impl MangaPage { } } + fn confirm_download_all(&mut self) { + self.state = PageState::SettingDownloadQuality; + } + fn negate_download_all_chapters(&mut self) { self.state = PageState::DisplayingChapters } + fn set_download_quality(&mut self) { + self.image_quality = PageType::LowQuality; + } + + fn toggle_image_quality(&mut self) { + self.image_quality = self.image_quality.toggle(); + } + + fn init_download_all_chapters(&mut self) {} + fn handle_mouse_events(&mut self, mouse_event: MouseEvent) { if self.is_list_languages_open { match mouse_event.kind { @@ -998,9 +1086,11 @@ impl Component for MangaPage { } fn update(&mut self, action: Self::Actions) { match action { + MangaPageActions::DownloadAllChapter => self.download_all_chapters(), + MangaPageActions::ToggleImageQuality => self.toggle_image_quality(), MangaPageActions::NegateDownloadAll => self.negate_download_all_chapters(), MangaPageActions::AskDownloadAllChapters => self.ask_download_all_chapters(), - MangaPageActions::ConfirmDownloadAll => self.download_all_chapters(), + MangaPageActions::ConfirmDownloadAll => self.confirm_download_all(), MangaPageActions::SearchPreviousChapterPage => self.search_previous_chapters(), MangaPageActions::SearchNextChapterPage => self.search_next_chapters(), MangaPageActions::ScrollDownAvailbleLanguages => self.scroll_language_down(), From 8b51f64458eddba5fda078d56e9b1f57d58fa26b Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Wed, 14 Aug 2024 11:36:34 -0500 Subject: [PATCH 04/14] refactor(MangaPage): move logic to its own widget made struct to save download all chapter state and a wiget to handle rendering logic --- src/backend.rs | 19 +++++ src/backend/download.rs | 32 +++++---- src/common.rs | 3 +- src/view/pages/manga.rs | 145 +++++++++++++++---------------------- src/view/widgets/manga.rs | 148 +++++++++++++++++++++++++++++++++++++- 5 files changed, 246 insertions(+), 101 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 192dddd..7cfc1ab 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -25,6 +25,25 @@ pub enum AppDirectories { History, } +impl AppDirectories { + pub fn into_path_buf(self) -> PathBuf { + let base_directory = APP_DATA_DIR.as_ref(); + match self { + Self::MangaDownloads => PathBuf::from( + &base_directory + .unwrap() + .join(Self::MangaDownloads.to_string()), + ), + Self::History => { + PathBuf::from(&base_directory.unwrap().join(Self::History.to_string())) + } + Self::ErrorLogs => { + PathBuf::from(&base_directory.unwrap().join(Self::ErrorLogs.to_string())) + } + } + } +} + pub static APP_DATA_DIR: Lazy> = Lazy::new(|| { directories::ProjectDirs::from("", "", "manga-tui").map(|dirs| { match std::env::var("MANGA_TUI_DATA_DIR").ok() { diff --git a/src/backend/download.rs b/src/backend/download.rs index 2d2e7fb..3fb92f3 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -116,12 +116,12 @@ pub fn download_chapter( chapter: DownloadChapter<'_>, chapter_data: ChapterPagesResponse, chapter_quality: PageType, + chapter_number: usize, + total_chapters: usize, tx: UnboundedSender, ) -> Result<(), std::io::Error> { let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; - let total_chapters = chapter_data.chapter.data.len(); - let files = match chapter_quality { PageType::HighQuality => chapter_data.chapter.data, PageType::LowQuality => chapter_data.chapter.data_saver, @@ -153,27 +153,24 @@ pub fn download_chapter( let mut image_created = File::create(chapter_dir.join(image_name)).unwrap(); image_created.write_all(&bytes).unwrap(); - - // tx.send(MangaPageEvents::SetDownloadProgress( - // (index as f64) / (total_chapters as f64), - // chapter_id.clone(), - // )) - // .ok(); } Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), } } - // tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) - // .ok(); + + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress(1_f64)) + .ok(); }); Ok(()) } +#[derive(Default)] pub struct DownloadAllChapters { pub manga_id: String, pub manga_title: String, pub quality: PageType, + pub lang: Languages, } pub fn download_all_chapters( @@ -181,7 +178,16 @@ pub fn download_all_chapters( manga_details: DownloadAllChapters, tx: UnboundedSender, ) { - for chapter in chapter_data.data { + let total_chapters = chapter_data.data.len(); + let download_chapter_delay = if total_chapters < 15 { + 1 + } else if total_chapters <= 40 { + 3 + } else { + 5 + }; + + for (index, chapter) in chapter_data.data.into_iter().enumerate() { let id = chapter.id.clone(); let manga_id = manga_details.manga_id.clone(); let manga_title = manga_details.manga_title.clone(); @@ -211,6 +217,8 @@ pub fn download_all_chapters( }, res, manga_details.quality, + index, + total_chapters, tx, ) .unwrap(); @@ -220,6 +228,6 @@ pub fn download_all_chapters( } } }); - std::thread::sleep(Duration::from_secs(4)); + std::thread::sleep(Duration::from_secs(download_chapter_delay)); } } diff --git a/src/common.rs b/src/common.rs index eed875f..854360a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -31,11 +31,12 @@ pub struct Manga { pub created_at: String, } -#[derive(Display, Clone, Copy, EnumIter)] +#[derive(Display, Clone, Copy, EnumIter, Default)] pub enum PageType { #[strum(to_string = "data")] HighQuality, #[strum(to_string = "data-saver")] + #[default] LowQuality, } diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index c4f16c4..06c0200 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -7,11 +7,14 @@ use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; use crate::backend::filter::Languages; use crate::backend::tui::Events; -use crate::backend::{ChapterResponse, MangaStatisticsResponse, Statistics}; +use crate::backend::{AppDirectories, ChapterResponse, MangaStatisticsResponse, Statistics}; use crate::common::{Manga, PageType}; use crate::global::{ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::{set_status_style, set_tags_style}; -use crate::view::widgets::manga::{ChapterItem, ChaptersListWidget}; +use crate::view::widgets::manga::{ + ChapterItem, ChaptersListWidget, DownloadAllChaptersState, DownloadAllChaptersWidget, + DownloadPhase, +}; use crate::view::widgets::Component; use crate::PICKER; use crossterm::event::{ @@ -29,9 +32,6 @@ use self::text::ToSpan; #[derive(PartialEq, Eq)] pub enum PageState { DownloadingChapters, - IsAskingDownloadAllChapters, - SettingDownloadQuality, - DownloadingAllChapters, SearchingChapters, SearchingChapterData, DisplayingChapters, @@ -63,8 +63,11 @@ pub enum MangaPageEvents { FethStatistics, CheckChapterStatus, ChapterFinishedDownloading(String), + DownloadAllChaptersFinished, /// Percentage, id chapter SetDownloadProgress(f64, String), + StartDownloadProgress(f64), + SetDownloadAllChaptersProgress(f64), /// id_chapter, chapter_title SaveChapterDownloadStatus(String, String), /// id_chapter @@ -107,9 +110,9 @@ pub struct MangaPage { state: PageState, statistics: Option, tasks: JoinSet<()>, - image_quality: PageType, available_languages_state: ListState, is_list_languages_open: bool, + download_all_chapters_state: DownloadAllChaptersState, } struct MangaStatistics { @@ -163,7 +166,7 @@ impl MangaPage { tasks: JoinSet::new(), available_languages_state: ListState::default(), is_list_languages_open: false, - image_quality: PageType::LowQuality, + download_all_chapters_state: DownloadAllChaptersState::default(), chapter_language: chapter_language.unwrap_or(Languages::default()), } } @@ -224,7 +227,7 @@ impl MangaPage { self.render_details(manga_information_area, frame.buffer_mut()); - self.render_chapters_area(manga_chapters_area, frame); + self.render_chapters_area(manga_chapters_area, frame.buffer_mut()); } fn render_details(&mut self, area: Rect, buf: &mut Buffer) { @@ -254,21 +257,15 @@ impl MangaPage { .render(description_area, buf); } - fn render_chapters_area(&mut self, area: Rect, frame: &mut Frame<'_>) { - let buf = frame.buffer_mut(); + fn render_chapters_area(&mut self, area: Rect, buf: &mut Buffer) { let layout = Layout::vertical([Constraint::Percentage(10), Constraint::Percentage(90)]).margin(2); let [sorting_buttons_area, chapters_area] = layout.areas(area); - match self.state { - PageState::IsAskingDownloadAllChapters - | PageState::SettingDownloadQuality - | PageState::DownloadingAllChapters => { - self.render_download_all_chapters_area(area, buf); - return; - } - _ => {} + if self.download_process_started() { + self.render_download_all_chapters_area(area, buf); + return; } match self.chapters.as_mut() { @@ -323,56 +320,18 @@ impl MangaPage { "Searching chapters".to_span() }; - Block::bordered() - .title(title) - .render(area, frame.buffer_mut()); + Block::bordered().title(title).render(area, buf); } } } fn render_download_all_chapters_area(&mut self, area: Rect, buf: &mut Buffer) { - Block::bordered().render(area, buf); - - let download_information_area = area.inner(Margin { - horizontal: 2, - vertical: 2, - }); - - match self.state { - PageState::IsAskingDownloadAllChapters => { - let instructions = vec![ - "Do you want to download all chapters? Yes: ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), - " no ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), - ]; - - Paragraph::new(Line::from(instructions)).render(download_information_area, buf); - } - PageState::SettingDownloadQuality => { - Widget::render( - List::new([ - Line::from(vec![ - "Choose image quality ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), - ]), - self.image_quality.as_human_readable().into(), - "Lower image quality is recommended for slow internet".into(), - Line::from(vec![ - "Start download: ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), - ]), - ]), - download_information_area, - buf, - ); - } - PageState::DownloadingAllChapters => { - Paragraph::new("Downloading all chapters, this will take a while") - .render(download_information_area, buf); - } - _ => {} - } + StatefulWidget::render( + DownloadAllChaptersWidget::new(&self.manga.title), + area, + buf, + &mut self.download_all_chapters_state, + ); } fn render_sorting_buttons(&mut self, area: Rect, buf: &mut Buffer) { @@ -439,13 +398,8 @@ impl MangaPage { } } - fn is_downloading_all_chapters(&self) -> bool { - matches!( - self.state, - PageState::DownloadingAllChapters - | PageState::SettingDownloadQuality - | PageState::IsAskingDownloadAllChapters - ) + fn download_process_started(&self) -> bool { + self.download_all_chapters_state.process_started() } fn handle_key_events(&mut self, key_event: KeyEvent) { @@ -474,7 +428,7 @@ impl MangaPage { _ => {} } } else if self.state != PageState::SearchingChapterData { - if self.is_downloading_all_chapters() { + if self.download_process_started() { match key_event.code { KeyCode::Esc => { self.local_action_tx @@ -492,7 +446,7 @@ impl MangaPage { .ok(); } KeyCode::Char(' ') => { - if self.state == PageState::SettingDownloadQuality { + if self.download_all_chapters_state.is_ready_to_download() { self.local_action_tx .send(MangaPageActions::DownloadAllChapter) .ok(); @@ -947,25 +901,34 @@ impl MangaPage { } } + fn set_manga_download_progress(&mut self, progress: f64) { + self.download_all_chapters_state.set_download_progress(); + } + fn download_all_chapters(&mut self) { - self.state == PageState::DownloadingAllChapters; let id = self.manga.id.clone(); let manga_title = self.manga.title.clone(); let lang = self.get_current_selected_language(); let tx = self.local_event_tx.clone(); - let quality = self.image_quality; + let quality = self.download_all_chapters_state.image_quality; tokio::spawn(async move { let chapter_response = MangadexClient::global() .get_all_chapters_for_manga(&id, lang) .await; match chapter_response { Ok(response) => { + let total_chapters = response.data.len(); + tx.send(MangaPageEvents::StartDownloadProgress( + total_chapters as f64, + )) + .ok(); download_all_chapters( response, DownloadAllChapters { manga_title, manga_id: id, quality, + lang, }, tx, ); @@ -978,28 +941,31 @@ impl MangaPage { } fn ask_download_all_chapters(&mut self) { - if self.state == PageState::DisplayingChapters { - self.state = PageState::IsAskingDownloadAllChapters; - } + self.download_all_chapters_state.ask_for_confirmation(); } fn confirm_download_all(&mut self) { - self.state = PageState::SettingDownloadQuality; + self.download_all_chapters_state.confirm(); } - fn negate_download_all_chapters(&mut self) { - self.state = PageState::DisplayingChapters + fn cancel_download_all_chapters(&mut self) { + self.state = PageState::DisplayingChapters; + self.download_all_chapters_state.cancel(); } - fn set_download_quality(&mut self) { - self.image_quality = PageType::LowQuality; + fn toggle_image_quality(&mut self) { + self.download_all_chapters_state.image_quality.toggle(); } - fn toggle_image_quality(&mut self) { - self.image_quality = self.image_quality.toggle(); + fn start_download_all_chapters(&mut self, total_chapters: f64) { + self.download_all_chapters_state + .set_total_chapters(total_chapters); + self.download_all_chapters_state.start_download(); } - fn init_download_all_chapters(&mut self) {} + fn finish_download_all_chapters(&mut self) { + self.state = PageState::DisplayingChapters; + } fn handle_mouse_events(&mut self, mouse_event: MouseEvent) { if self.is_list_languages_open { @@ -1036,6 +1002,13 @@ impl MangaPage { fn tick(&mut self) { if let Ok(background_event) = self.local_event_rx.try_recv() { match background_event { + MangaPageEvents::DownloadAllChaptersFinished => todo!(), + MangaPageEvents::StartDownloadProgress(total_chapters) => { + self.start_download_all_chapters(total_chapters) + } + MangaPageEvents::SetDownloadAllChaptersProgress(progress) => { + self.set_manga_download_progress(progress) + } MangaPageEvents::ReadError(chapter_id) => { self.set_chapter_read_error(chapter_id); } @@ -1088,7 +1061,7 @@ impl Component for MangaPage { match action { MangaPageActions::DownloadAllChapter => self.download_all_chapters(), MangaPageActions::ToggleImageQuality => self.toggle_image_quality(), - MangaPageActions::NegateDownloadAll => self.negate_download_all_chapters(), + MangaPageActions::NegateDownloadAll => self.cancel_download_all_chapters(), MangaPageActions::AskDownloadAllChapters => self.ask_download_all_chapters(), MangaPageActions::ConfirmDownloadAll => self.confirm_download_all(), MangaPageActions::SearchPreviousChapterPage => self.search_previous_chapters(), diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index 2f4f7b3..e6e1a95 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -1,6 +1,7 @@ use crate::backend::filter::Languages; -use crate::backend::ChapterResponse; -use crate::global::{CURRENT_LIST_ITEM_STYLE, ERROR_STYLE}; +use crate::backend::{AppDirectories, ChapterResponse}; +use crate::common::PageType; +use crate::global::{CURRENT_LIST_ITEM_STYLE, ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::display_dates_since_publication; use ratatui::{prelude::*, widgets::*}; use tui_widget_list::PreRender; @@ -241,3 +242,146 @@ impl StatefulWidget for ChaptersListWidget { StatefulWidget::render(chapters_list, area, buf, state); } } + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum DownloadPhase { + #[default] + ProccessNotStarted, + Asking, + SettingQuality, + FetchingChaptersData, + DownloadingChapters, +} + +#[derive(Default)] +pub struct DownloadAllChaptersState { + pub phase: DownloadPhase, + pub image_quality: PageType, + pub total_chapters: f64, + pub download_progress: f64, +} + +impl DownloadAllChaptersState { + pub fn is_downloading_chapters(&self) -> bool { + self.phase == DownloadPhase::DownloadingChapters + } + + pub fn process_started(&self) -> bool { + self.phase != DownloadPhase::ProccessNotStarted + } + + pub fn is_ready_to_download(&self) -> bool { + self.phase == DownloadPhase::SettingQuality + } + + pub fn set_download_progress(&mut self) { + self.download_progress += 1.0; + } + + pub fn ask_for_confirmation(&mut self) { + self.phase = DownloadPhase::Asking; + } + + pub fn confirm(&mut self) { + self.phase = DownloadPhase::SettingQuality; + } + + pub fn start_download(&mut self) { + self.phase = DownloadPhase::DownloadingChapters; + } + + pub fn cancel(&mut self) { + self.phase = DownloadPhase::ProccessNotStarted; + } + + pub fn set_total_chapters(&mut self, total_chapters : f64) { + self.total_chapters = total_chapters; + } +} + +pub struct DownloadAllChaptersWidget<'a> { + pub manga_title: &'a str, +} + +impl<'a> DownloadAllChaptersWidget<'a> { + pub fn new(manga_title: &'a str) -> Self { + Self { manga_title } + } +} + +impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { + type State = DownloadAllChaptersState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + Block::bordered().render(area, buf); + + let download_information_area = area.inner(Margin { + horizontal: 2, + vertical: 2, + }); + + match state.phase { + DownloadPhase::ProccessNotStarted => {} + DownloadPhase::Asking => { + let instructions = vec![ + "Do you want to download all chapters? Yes: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + " no ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]; + + Paragraph::new(Line::from(instructions)).render(download_information_area, buf); + } + DownloadPhase::SettingQuality => { + Widget::render( + List::new([ + Line::from(vec![ + "Choose image quality ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]), + "Lower image quality is recommended for slow internet".into(), + state.image_quality.as_human_readable().into(), + Line::from(vec![ + "Start download: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]), + ]), + download_information_area, + buf, + ); + } + DownloadPhase::FetchingChaptersData => { + // add throbber + "fetching manga data after this each chapter will be downloaded" + .render(download_information_area, buf); + } + DownloadPhase::DownloadingChapters => { + let [information_area, progress_area] = + Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]) + .areas(download_information_area); + Paragraph::new("Downloading all chapters, this will take a while") + .render(information_area, buf); + + LineGauge::default() + .block(Block::bordered().title(format!( + "Total chapters: {}, you can check the download at : {}", + state.total_chapters, + AppDirectories::MangaDownloads + .into_path_buf() + .join(self.manga_title) + .to_str() + .unwrap() + ))) + .filled_style( + Style::default() + .fg(Color::Blue) + .bg(Color::Black) + .add_modifier(Modifier::BOLD), + ) + .line_set(symbols::line::THICK) + .ratio(state.download_progress / state.total_chapters) + .render(progress_area, buf); + } + } + } +} From 7499024c2a61b8cfb16ed6dd9e79091af9560c99 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Wed, 14 Aug 2024 14:08:42 -0500 Subject: [PATCH 05/14] refactor(MangaPage-widgets): changed layout of manga download information --- src/backend/download.rs | 38 ++++++++---- src/view/app.rs | 10 ++- src/view/pages/manga.rs | 39 +++++++++--- src/view/widgets/manga.rs | 124 ++++++++++++++++++++++++++++++++------ 4 files changed, 172 insertions(+), 39 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index 3fb92f3..0190843 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -158,7 +158,7 @@ pub fn download_chapter( } } - tx.send(MangaPageEvents::SetDownloadAllChaptersProgress(1_f64)) + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) .ok(); }); @@ -179,12 +179,15 @@ pub fn download_all_chapters( tx: UnboundedSender, ) { let total_chapters = chapter_data.data.len(); - let download_chapter_delay = if total_chapters < 15 { + + let download_chapter_delay = if total_chapters <= 20 { 1 - } else if total_chapters <= 40 { - 3 + } else if total_chapters >= 40 { + 4 + } else if total_chapters >= 100 { + 6 } else { - 5 + 8 }; for (index, chapter) in chapter_data.data.into_iter().enumerate() { @@ -205,7 +208,7 @@ pub fn download_all_chapters( match pages_response { Ok(res) => { - download_chapter( + let download_proccess = download_chapter( DownloadChapter { id_chapter: &chapter.id, manga_id: &manga_id, @@ -213,18 +216,31 @@ pub fn download_all_chapters( title: chapter.attributes.title.unwrap_or_default().as_str(), number: chapter.attributes.chapter.unwrap_or_default().as_str(), scanlator: &scanlator.unwrap_or_default(), - lang: &Languages::default().as_human_readable(), + lang: &manga_details.lang.as_human_readable(), }, res, manga_details.quality, index, total_chapters, - tx, - ) - .unwrap(); + tx.clone(), + ); + + if let Err(e) = download_proccess { + let error_message = format!( + "Chapter: {} could not be downloaded, details: {}", + manga_title, e + ); + + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + + write_to_error_log(ErrorType::FromError(Box::from(error_message))); + } } Err(e) => { - write_to_error_log(ErrorType::FromError(Box::new(e))); + tx.send(MangaPageEvents::DownloadAllChaptersError).ok(); + + write_to_error_log(ErrorType::FromError(Box::from(e))); } } }); diff --git a/src/view/app.rs b/src/view/app.rs index ab18e6d..6672e21 100644 --- a/src/view/app.rs +++ b/src/view/app.rs @@ -4,7 +4,7 @@ use self::manga::MangaPage; use self::reader::MangaReader; use self::search::{InputMode, SearchPage}; use crate::backend::tui::{Action, Events}; -use crate::backend::{ChapterPagesResponse}; +use crate::backend::ChapterPagesResponse; use crate::global::INSTRUCTIONS_STYLE; use crate::view::pages::*; use ::crossterm::event::KeyCode; @@ -165,6 +165,14 @@ impl App { } fn handle_key_events(&mut self, key_event: KeyEvent) { + if self + .manga_page + .as_ref() + .is_some_and(|page| page.is_downloading_all_chapters()) + { + return; + } + if self.search_page.input_mode != InputMode::Typing && !self.search_page.is_typing_filter() && !self.feed_page.is_typing() diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 06c0200..f3f0730 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -63,11 +63,11 @@ pub enum MangaPageEvents { FethStatistics, CheckChapterStatus, ChapterFinishedDownloading(String), - DownloadAllChaptersFinished, + DownloadAllChaptersError, /// Percentage, id chapter SetDownloadProgress(f64, String), StartDownloadProgress(f64), - SetDownloadAllChaptersProgress(f64), + SetDownloadAllChaptersProgress, /// id_chapter, chapter_title SaveChapterDownloadStatus(String, String), /// id_chapter @@ -279,6 +279,8 @@ impl MangaPage { Span::raw(" / ").style(*INSTRUCTIONS_STYLE), " Download chapter ".into(), Span::raw(" ").style(*INSTRUCTIONS_STYLE), + " Download all chapters ".into(), + Span::raw(" ").style(*INSTRUCTIONS_STYLE), ]; if PICKER.is_some() { @@ -901,11 +903,12 @@ impl MangaPage { } } - fn set_manga_download_progress(&mut self, progress: f64) { + fn set_manga_download_progress(&mut self) { self.download_all_chapters_state.set_download_progress(); } fn download_all_chapters(&mut self) { + self.download_all_chapters_state.start_fectch(); let id = self.manga.id.clone(); let manga_title = self.manga.title.clone(); let lang = self.get_current_selected_language(); @@ -949,24 +952,39 @@ impl MangaPage { } fn cancel_download_all_chapters(&mut self) { - self.state = PageState::DisplayingChapters; - self.download_all_chapters_state.cancel(); + if !self.download_all_chapters_state.is_downloading() { + self.state = PageState::DisplayingChapters; + self.download_all_chapters_state.cancel(); + } } fn toggle_image_quality(&mut self) { - self.download_all_chapters_state.image_quality.toggle(); + self.download_all_chapters_state.toggle_image_quality(); } fn start_download_all_chapters(&mut self, total_chapters: f64) { self.download_all_chapters_state .set_total_chapters(total_chapters); + self.download_all_chapters_state.set_download_location( + AppDirectories::MangaDownloads + .into_path_buf() + .join(&self.manga.title), + ); self.download_all_chapters_state.start_download(); } + pub fn is_downloading_all_chapters(&self) -> bool { + self.download_all_chapters_state.is_downloading() + } + fn finish_download_all_chapters(&mut self) { self.state = PageState::DisplayingChapters; } + fn set_download_all_chapters_error(&mut self) { + self.download_all_chapters_state.set_download_error(); + } + fn handle_mouse_events(&mut self, mouse_event: MouseEvent) { if self.is_list_languages_open { match mouse_event.kind { @@ -1000,14 +1018,17 @@ impl MangaPage { } fn tick(&mut self) { + if self.download_process_started() { + self.download_all_chapters_state.tick(); + } if let Ok(background_event) = self.local_event_rx.try_recv() { match background_event { - MangaPageEvents::DownloadAllChaptersFinished => todo!(), + MangaPageEvents::DownloadAllChaptersError => self.set_download_all_chapters_error(), MangaPageEvents::StartDownloadProgress(total_chapters) => { self.start_download_all_chapters(total_chapters) } - MangaPageEvents::SetDownloadAllChaptersProgress(progress) => { - self.set_manga_download_progress(progress) + MangaPageEvents::SetDownloadAllChaptersProgress => { + self.set_manga_download_progress() } MangaPageEvents::ReadError(chapter_id) => { self.set_chapter_read_error(chapter_id); diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index e6e1a95..02a2d31 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -1,9 +1,12 @@ +use std::path::PathBuf; + use crate::backend::filter::Languages; use crate::backend::{AppDirectories, ChapterResponse}; use crate::common::PageType; use crate::global::{CURRENT_LIST_ITEM_STYLE, ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::display_dates_since_publication; use ratatui::{prelude::*, widgets::*}; +use throbber_widgets_tui::{Throbber, ThrobberState}; use tui_widget_list::PreRender; use self::text::ToSpan; @@ -251,6 +254,7 @@ pub enum DownloadPhase { SettingQuality, FetchingChaptersData, DownloadingChapters, + ErrorChaptersData, } #[derive(Default)] @@ -258,11 +262,13 @@ pub struct DownloadAllChaptersState { pub phase: DownloadPhase, pub image_quality: PageType, pub total_chapters: f64, + pub loader_state: ThrobberState, pub download_progress: f64, + pub download_location: PathBuf, } impl DownloadAllChaptersState { - pub fn is_downloading_chapters(&self) -> bool { + pub fn is_downloading(&self) -> bool { self.phase == DownloadPhase::DownloadingChapters } @@ -279,24 +285,58 @@ impl DownloadAllChaptersState { } pub fn ask_for_confirmation(&mut self) { - self.phase = DownloadPhase::Asking; + if !self.is_downloading() { + self.phase = DownloadPhase::Asking; + } } pub fn confirm(&mut self) { - self.phase = DownloadPhase::SettingQuality; + if !self.is_downloading() { + self.phase = DownloadPhase::SettingQuality; + } + } + + pub fn start_fectch(&mut self) { + if !self.is_downloading() { + self.total_chapters = 0.0; + self.download_progress = 0.0; + self.phase = DownloadPhase::FetchingChaptersData; + } } pub fn start_download(&mut self) { - self.phase = DownloadPhase::DownloadingChapters; + if !self.is_downloading() { + self.phase = DownloadPhase::DownloadingChapters; + } } pub fn cancel(&mut self) { self.phase = DownloadPhase::ProccessNotStarted; } - pub fn set_total_chapters(&mut self, total_chapters : f64) { + pub fn set_total_chapters(&mut self, total_chapters: f64) { self.total_chapters = total_chapters; } + + pub fn finished_downloading(&self) -> bool { + self.download_progress == self.total_chapters + } + + pub fn set_download_error(&mut self) { + self.phase = DownloadPhase::ErrorChaptersData; + } + + pub fn set_download_location(&mut self, location: PathBuf) { + self.download_location = location + } + + pub fn toggle_image_quality(&mut self) { + self.image_quality = self.image_quality.toggle(); + } + + pub fn tick(&mut self) { + self.loader_state.calc_next(); + } } pub struct DownloadAllChaptersWidget<'a> { @@ -309,10 +349,41 @@ impl<'a> DownloadAllChaptersWidget<'a> { } } +impl<'a> DownloadAllChaptersWidget<'a> { + fn render_download_information( + &mut self, + area: Rect, + buf: &mut Buffer, + state: &mut DownloadAllChaptersState, + ) { + let [information_area, loader_area] = + Layout::horizontal([Constraint::Fill(2), Constraint::Fill(1)]).areas(area); + + let download_location = format!( + "Download location : {}", + state.download_location.to_str().unwrap(), + ); + + Paragraph::new(Line::from(vec![ + "Downloading all chapters, this will take a while, ".into(), + download_location.into(), + ])) + .wrap(Wrap { trim: true }) + .render(information_area, buf); + + let loader = Throbber::default() + .label("Download in progress") + .style(Style::default().fg(Color::Yellow)) + .throbber_set(throbber_widgets_tui::BRAILLE_SIX) + .use_type(throbber_widgets_tui::WhichUse::Spin); + StatefulWidget::render(loader, loader_area, buf, &mut state.loader_state); + } +} + impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { type State = DownloadAllChaptersState; - fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { Block::bordered().render(area, buf); let download_information_area = area.inner(Margin { @@ -351,27 +422,44 @@ impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { ); } DownloadPhase::FetchingChaptersData => { - // add throbber - "fetching manga data after this each chapter will be downloaded" + let loader = Throbber::default() + .label( + "fetching manga data after this each chapter will begin to be downloaded", + ) + .style(Style::default().fg(Color::Yellow)) + .throbber_set(throbber_widgets_tui::BRAILLE_SIX) + .use_type(throbber_widgets_tui::WhichUse::Spin); + + StatefulWidget::render( + loader, + download_information_area, + buf, + &mut state.loader_state, + ); + } + DownloadPhase::ErrorChaptersData => { + "Could not get chapters data, press to try again" + .to_span() + .style(*ERROR_STYLE) .render(download_information_area, buf); } DownloadPhase::DownloadingChapters => { + if state.finished_downloading() { + state.cancel(); + return; + } + let [information_area, progress_area] = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]) .areas(download_information_area); - Paragraph::new("Downloading all chapters, this will take a while") - .render(information_area, buf); + + self.render_download_information(information_area, buf, state); LineGauge::default() .block(Block::bordered().title(format!( - "Total chapters: {}, you can check the download at : {}", - state.total_chapters, - AppDirectories::MangaDownloads - .into_path_buf() - .join(self.manga_title) - .to_str() - .unwrap() - ))) + "Total chapters: {}, chapters downloaded : {}", + state.total_chapters, state.download_progress + ))) .filled_style( Style::default() .fg(Color::Blue) From 8f9742d7eecf692e3bbe00df3ffe7d9330499bba Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Wed, 14 Aug 2024 21:25:52 -0500 Subject: [PATCH 06/14] fix(MangaPage): save chapter download status after all chapters have been downloaded --- src/backend/download.rs | 32 ++++++++++++++++++++++++-------- src/main.rs | 11 +++++++++-- src/view/pages/manga.rs | 23 +++++++++++++++++------ src/view/widgets/manga.rs | 25 ++++++++++++++++++++++--- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index 0190843..a81f1e5 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -89,12 +89,16 @@ pub fn download_single_chaper( match image_response { Ok(bytes) => { - let mut image_created = File::create(chapter_dir.join(format!( + let image_name = format!( "{}.{}", index + 1, file_name.extension().unwrap().to_str().unwrap() - ))) - .unwrap(); + ); + if exists!(&chapter_dir.join(&image_name)) { + return; + } + + let mut image_created = File::create(image_name).unwrap(); image_created.write_all(&bytes).unwrap(); tx.send(MangaPageEvents::SetDownloadProgress( (index as f64) / (total_chapters as f64), @@ -182,9 +186,9 @@ pub fn download_all_chapters( let download_chapter_delay = if total_chapters <= 20 { 1 - } else if total_chapters >= 40 { + } else if (40..100).contains(&total_chapters) { 4 - } else if total_chapters >= 100 { + } else if (100..200).contains(&total_chapters) { 6 } else { 8 @@ -208,12 +212,13 @@ pub fn download_all_chapters( match pages_response { Ok(res) => { + let chapter_title = chapter.attributes.title.unwrap_or_default(); let download_proccess = download_chapter( DownloadChapter { id_chapter: &chapter.id, manga_id: &manga_id, manga_title: &manga_title, - title: chapter.attributes.title.unwrap_or_default().as_str(), + title: chapter_title.as_str(), number: chapter.attributes.chapter.unwrap_or_default().as_str(), scanlator: &scanlator.unwrap_or_default(), lang: &manga_details.lang.as_human_readable(), @@ -235,12 +240,23 @@ pub fn download_all_chapters( .ok(); write_to_error_log(ErrorType::FromError(Box::from(error_message))); + return; } + tx.send(MangaPageEvents::SaveChapterDownloadStatus( + chapter.id, + chapter_title, + )) + .ok(); } Err(e) => { - tx.send(MangaPageEvents::DownloadAllChaptersError).ok(); + let error_message = format!( + "Chapter: {} could not be downloaded, details: {}", + manga_title, e + ); - write_to_error_log(ErrorType::FromError(Box::from(e))); + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + write_to_error_log(ErrorType::FromError(Box::from(error_message))); } } }); diff --git a/src/main.rs b/src/main.rs index 2de4f77..53c1df9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #![forbid(unsafe_code)] #![allow(dead_code)] #![allow(unused)] +use std::time::Duration; + use self::backend::error_log::init_error_hooks; use self::backend::fetch::{MangadexClient, MANGADEX_CLIENT_INSTANCE}; use self::backend::filter::Languages; @@ -91,8 +93,13 @@ async fn main() -> Result<(), Box> { std::env::consts::ARCH ); - let mangadex_client = - MangadexClient::new(Client::builder().user_agent(user_agent).build().unwrap()); + let mangadex_client = MangadexClient::new( + Client::builder() + .timeout(Duration::from_secs(10)) + .user_agent(user_agent) + .build() + .unwrap(), + ); println!("Checking mangadex status..."); diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index f3f0730..d3516ae 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -29,6 +29,8 @@ use tokio::task::JoinSet; use self::text::ToSpan; +use super::reader::MangaReaderEvents; + #[derive(PartialEq, Eq)] pub enum PageState { DownloadingChapters, @@ -43,7 +45,7 @@ pub enum MangaPageActions { DownloadAllChapter, ToggleImageQuality, ConfirmDownloadAll, - NegateDownloadAll, + CancelDownloadAll, AskDownloadAllChapters, ScrollChapterDown, ScrollChapterUp, @@ -68,6 +70,7 @@ pub enum MangaPageEvents { SetDownloadProgress(f64, String), StartDownloadProgress(f64), SetDownloadAllChaptersProgress, + FinishedDownloadingAllChapters, /// id_chapter, chapter_title SaveChapterDownloadStatus(String, String), /// id_chapter @@ -157,7 +160,7 @@ impl MangaPage { global_event_tx, local_action_tx, local_action_rx, - local_event_tx, + local_event_tx: local_event_tx.clone(), local_event_rx, chapters: None, chapter_order: ChapterOrder::default(), @@ -166,7 +169,7 @@ impl MangaPage { tasks: JoinSet::new(), available_languages_state: ListState::default(), is_list_languages_open: false, - download_all_chapters_state: DownloadAllChaptersState::default(), + download_all_chapters_state: DownloadAllChaptersState::new(local_event_tx), chapter_language: chapter_language.unwrap_or(Languages::default()), } } @@ -434,7 +437,7 @@ impl MangaPage { match key_event.code { KeyCode::Esc => { self.local_action_tx - .send(MangaPageActions::NegateDownloadAll) + .send(MangaPageActions::CancelDownloadAll) .ok(); } KeyCode::Char('t') => { @@ -448,7 +451,7 @@ impl MangaPage { .ok(); } KeyCode::Char(' ') => { - if self.download_all_chapters_state.is_ready_to_download() { + if self.download_all_chapters_state.is_ready_to_fetch_data() { self.local_action_tx .send(MangaPageActions::DownloadAllChapter) .ok(); @@ -937,6 +940,7 @@ impl MangaPage { ); } Err(e) => { + tx.send(MangaPageEvents::DownloadAllChaptersError).ok(); write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); } } @@ -978,7 +982,11 @@ impl MangaPage { } fn finish_download_all_chapters(&mut self) { + self.download_all_chapters_state.cancel(); self.state = PageState::DisplayingChapters; + self.local_event_tx + .send(MangaPageEvents::CheckChapterStatus) + .ok(); } fn set_download_all_chapters_error(&mut self) { @@ -1023,6 +1031,9 @@ impl MangaPage { } if let Ok(background_event) = self.local_event_rx.try_recv() { match background_event { + MangaPageEvents::FinishedDownloadingAllChapters => { + self.finish_download_all_chapters() + } MangaPageEvents::DownloadAllChaptersError => self.set_download_all_chapters_error(), MangaPageEvents::StartDownloadProgress(total_chapters) => { self.start_download_all_chapters(total_chapters) @@ -1082,7 +1093,7 @@ impl Component for MangaPage { match action { MangaPageActions::DownloadAllChapter => self.download_all_chapters(), MangaPageActions::ToggleImageQuality => self.toggle_image_quality(), - MangaPageActions::NegateDownloadAll => self.cancel_download_all_chapters(), + MangaPageActions::CancelDownloadAll => self.cancel_download_all_chapters(), MangaPageActions::AskDownloadAllChapters => self.ask_download_all_chapters(), MangaPageActions::ConfirmDownloadAll => self.confirm_download_all(), MangaPageActions::SearchPreviousChapterPage => self.search_previous_chapters(), diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index 02a2d31..31511d1 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -5,8 +5,10 @@ use crate::backend::{AppDirectories, ChapterResponse}; use crate::common::PageType; use crate::global::{CURRENT_LIST_ITEM_STYLE, ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::display_dates_since_publication; +use crate::view::pages::manga::MangaPageEvents; use ratatui::{prelude::*, widgets::*}; use throbber_widgets_tui::{Throbber, ThrobberState}; +use tokio::sync::mpsc::UnboundedSender; use tui_widget_list::PreRender; use self::text::ToSpan; @@ -257,7 +259,6 @@ pub enum DownloadPhase { ErrorChaptersData, } -#[derive(Default)] pub struct DownloadAllChaptersState { pub phase: DownloadPhase, pub image_quality: PageType, @@ -265,9 +266,22 @@ pub struct DownloadAllChaptersState { pub loader_state: ThrobberState, pub download_progress: f64, pub download_location: PathBuf, + pub tx: UnboundedSender, } impl DownloadAllChaptersState { + pub fn new(tx: UnboundedSender) -> Self { + Self { + phase: DownloadPhase::default(), + image_quality: PageType::default(), + total_chapters: 0.0, + loader_state: ThrobberState::default(), + download_progress: 0.0, + download_location: PathBuf::default(), + tx, + } + } + pub fn is_downloading(&self) -> bool { self.phase == DownloadPhase::DownloadingChapters } @@ -276,8 +290,10 @@ impl DownloadAllChaptersState { self.phase != DownloadPhase::ProccessNotStarted } - pub fn is_ready_to_download(&self) -> bool { + /// Either phase can start download + pub fn is_ready_to_fetch_data(&self) -> bool { self.phase == DownloadPhase::SettingQuality + || self.phase == DownloadPhase::ErrorChaptersData } pub fn set_download_progress(&mut self) { @@ -445,7 +461,10 @@ impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { } DownloadPhase::DownloadingChapters => { if state.finished_downloading() { - state.cancel(); + state + .tx + .send(MangaPageEvents::FinishedDownloadingAllChapters) + .ok(); return; } From 96ccd1cb86fa39de5b0b5864cc82318daad31735 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Thu, 15 Aug 2024 11:58:39 -0500 Subject: [PATCH 07/14] test(MangaPage-DownloadState): Added tests for DownloadState --- src/backend/download.rs | 6 +-- src/common.rs | 2 +- src/view/pages/manga.rs | 90 ++++++++++++++++++++++++++++--- src/view/widgets/manga.rs | 110 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 193 insertions(+), 15 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index a81f1e5..4ec8228 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -210,9 +210,9 @@ pub fn download_all_chapters( tokio::spawn(async move { let pages_response = MangadexClient::global().get_chapter_pages(&id).await; + let chapter_title = chapter.attributes.title.unwrap_or_default(); match pages_response { Ok(res) => { - let chapter_title = chapter.attributes.title.unwrap_or_default(); let download_proccess = download_chapter( DownloadChapter { id_chapter: &chapter.id, @@ -233,7 +233,7 @@ pub fn download_all_chapters( if let Err(e) = download_proccess { let error_message = format!( "Chapter: {} could not be downloaded, details: {}", - manga_title, e + chapter_title, e ); tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) @@ -251,7 +251,7 @@ pub fn download_all_chapters( Err(e) => { let error_message = format!( "Chapter: {} could not be downloaded, details: {}", - manga_title, e + chapter_title, e ); tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) diff --git a/src/common.rs b/src/common.rs index 854360a..3ab562f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -31,7 +31,7 @@ pub struct Manga { pub created_at: String, } -#[derive(Display, Clone, Copy, EnumIter, Default)] +#[derive(Display, Clone, Copy, EnumIter, Default, Debug, Eq, PartialEq)] pub enum PageType { #[strum(to_string = "data")] HighQuality, diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index d3516ae..88010ad 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -31,7 +31,7 @@ use self::text::ToSpan; use super::reader::MangaReaderEvents; -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Debug)] pub enum PageState { DownloadingChapters, SearchingChapters, @@ -40,6 +40,7 @@ pub enum PageState { ChaptersNotFound, } +#[derive(Debug, PartialEq, Eq)] pub enum MangaPageActions { DownloadChapter, DownloadAllChapter, @@ -60,6 +61,7 @@ pub enum MangaPageActions { SearchPreviousChapterPage, } +#[derive(Debug, PartialEq)] pub enum MangaPageEvents { SearchChapters, FethStatistics, @@ -451,11 +453,9 @@ impl MangaPage { .ok(); } KeyCode::Char(' ') => { - if self.download_all_chapters_state.is_ready_to_fetch_data() { - self.local_action_tx - .send(MangaPageActions::DownloadAllChapter) - .ok(); - } + self.local_action_tx + .send(MangaPageActions::DownloadAllChapter) + .ok(); } _ => {} } @@ -911,7 +911,7 @@ impl MangaPage { } fn download_all_chapters(&mut self) { - self.download_all_chapters_state.start_fectch(); + self.download_all_chapters_state.start_fetch(); let id = self.manga.id.clone(); let manga_title = self.manga.title.clone(); let lang = self.get_current_selected_language(); @@ -1133,3 +1133,79 @@ impl Component for MangaPage { self.manga.description = String::new(); } } + +#[cfg(test)] +mod test { + use crate::backend::{ChapterData, Data}; + use crate::view::widgets::press_key; + + use super::*; + + #[test] + fn manga_page_initialized_correctly() { + let manga = Manga::default(); + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut manga_page = MangaPage::new(manga, None, tx); + + assert_eq!(manga_page.chapter_language, Languages::default()); + + assert_eq!(PageState::SearchingChapters, manga_page.state); + + assert!(!manga_page.is_list_languages_open); + + let first_event = manga_page.local_event_rx.blocking_recv().unwrap(); + let second_event = manga_page.local_event_rx.blocking_recv().unwrap(); + + assert!( + first_event == MangaPageEvents::FethStatistics + || first_event == MangaPageEvents::SearchChapters + ); + assert!( + second_event == MangaPageEvents::FethStatistics + || second_event == MangaPageEvents::SearchChapters + ); + } + + #[test] + fn manga_page_key_events() { + let manga = Manga::default(); + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut manga_page = MangaPage::new(manga, None, tx); + + let mock_chapter_response = ChapterResponse { + data: vec![ChapterData::default(), ChapterData::default()], + ..Default::default() + }; + + // Assuming chapters were found + manga_page.load_chapters(Some(mock_chapter_response)); + + assert!(manga_page.chapters.is_some()); + + press_key(&mut manga_page, KeyCode::Char('j')); + + let action = manga_page.local_action_rx.blocking_recv().unwrap(); + + assert_eq!(MangaPageActions::ScrollChapterDown, action); + + manga_page.update(action); + + assert!(manga_page + .chapters + .as_ref() + .unwrap() + .state + .selected + .is_some()); + + press_key(&mut manga_page, KeyCode::Char('l')); + + let action = manga_page.local_action_rx.blocking_recv().unwrap(); + + assert_eq!(MangaPageActions::OpenAvailableLanguagesList, action); + + manga_page.update(action); + + assert!(manga_page.is_list_languages_open); + } +} diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index 31511d1..41cc551 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use crate::backend::filter::Languages; use crate::backend::{AppDirectories, ChapterResponse}; use crate::common::PageType; @@ -7,6 +5,7 @@ use crate::global::{CURRENT_LIST_ITEM_STYLE, ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::display_dates_since_publication; use crate::view::pages::manga::MangaPageEvents; use ratatui::{prelude::*, widgets::*}; +use std::path::PathBuf; use throbber_widgets_tui::{Throbber, ThrobberState}; use tokio::sync::mpsc::UnboundedSender; use tui_widget_list::PreRender; @@ -259,6 +258,7 @@ pub enum DownloadPhase { ErrorChaptersData, } +#[derive(Debug)] pub struct DownloadAllChaptersState { pub phase: DownloadPhase, pub image_quality: PageType, @@ -312,8 +312,8 @@ impl DownloadAllChaptersState { } } - pub fn start_fectch(&mut self) { - if !self.is_downloading() { + pub fn start_fetch(&mut self) { + if self.is_ready_to_fetch_data() { self.total_chapters = 0.0; self.download_progress = 0.0; self.phase = DownloadPhase::FetchingChaptersData; @@ -492,3 +492,105 @@ impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { } } } + +#[cfg(test)] +mod test { + use tokio::sync::mpsc; + + use super::*; + + #[tokio::test] + async fn download_state_works() { + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut download_all_chapters_state = DownloadAllChaptersState::new(tx); + + assert_eq!( + DownloadPhase::ProccessNotStarted, + download_all_chapters_state.phase + ); + assert!(!download_all_chapters_state.process_started()); + assert!(!download_all_chapters_state.is_downloading()); + assert_eq!( + PageType::default(), + download_all_chapters_state.image_quality + ); + assert_eq!(0.0, download_all_chapters_state.download_progress); + + download_all_chapters_state.ask_for_confirmation(); + + assert_eq!(DownloadPhase::Asking, download_all_chapters_state.phase); + + download_all_chapters_state.confirm(); + + assert_eq!( + DownloadPhase::SettingQuality, + download_all_chapters_state.phase + ); + + download_all_chapters_state.toggle_image_quality(); + + assert_eq!( + PageType::default().toggle(), + download_all_chapters_state.image_quality + ); + + download_all_chapters_state.start_fetch(); + + assert_eq!( + DownloadPhase::FetchingChaptersData, + download_all_chapters_state.phase + ); + + download_all_chapters_state.set_download_error(); + + assert_eq!( + DownloadPhase::ErrorChaptersData, + download_all_chapters_state.phase + ); + + download_all_chapters_state.start_fetch(); + + assert_eq!( + DownloadPhase::FetchingChaptersData, + download_all_chapters_state.phase + ); + + download_all_chapters_state.start_download(); + + download_all_chapters_state.set_total_chapters(3.0); + + assert_eq!( + DownloadPhase::DownloadingChapters, + download_all_chapters_state.phase + ); + assert_eq!(3.0, download_all_chapters_state.total_chapters); + + download_all_chapters_state.set_download_progress(); + download_all_chapters_state.set_download_progress(); + download_all_chapters_state.set_download_progress(); + + let area = Rect::new(0, 0, 50, 50); + let mut buf = Buffer::empty(area); + + StatefulWidget::render( + DownloadAllChaptersWidget::new("some_title"), + area, + &mut buf, + &mut download_all_chapters_state, + ); + + let download_finished = rx.recv().await.unwrap(); + + assert_eq!( + MangaPageEvents::FinishedDownloadingAllChapters, + download_finished + ); + + download_all_chapters_state.cancel(); + + assert_eq!( + DownloadPhase::ProccessNotStarted, + download_all_chapters_state.phase + ); + } +} From a0e51ca3780a725336f28d54c37b4d9721032db8 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Fri, 16 Aug 2024 14:34:21 -0500 Subject: [PATCH 08/14] test(MangaPage): improve manga page tests handle key events and actions in different tests and change implementation of async functions if they are run in tests --- bacon.toml | 101 ++++++++++ src/backend/download.rs | 18 +- src/backend/fetch.rs | 5 +- src/view.rs | 1 + src/view/pages/manga.rs | 401 ++++++++++++++++++++++++++++++++------ src/view/tasks.rs | 3 + src/view/tasks/manga.rs | 41 ++++ src/view/widgets/manga.rs | 6 +- 8 files changed, 502 insertions(+), 74 deletions(-) create mode 100644 bacon.toml create mode 100644 src/view/tasks.rs create mode 100644 src/view/tasks/manga.rs diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..8d73ac1 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,101 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +default_job = "check" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = [ + "cargo", "clippy", + "--color", "always", +] +need_stdout = false + +# Run clippy on all targets +# To disable some lints, you may change the job this way: +# [jobs.clippy-all] +# command = [ +# "cargo", "clippy", +# "--all-targets", +# "--color", "always", +# "--", +# "-A", "clippy::bool_to_int_with_if", +# "-A", "clippy::collapsible_if", +# "-A", "clippy::derive_partial_eq_without_eq", +# ] +# need_stdout = false +[jobs.clippy-all] +command = [ + "cargo", "clippy", + "--all-targets", + "--color", "always", +] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = [ + "cargo", "test", "--color", "always", + "--", "--color", "always", "--test-threads=1" # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. +# Don't forget the `--color always` part or the errors won't be +# properly parsed. +# If your program never stops (eg a server), you may set `background` +# to false to have the cargo run output immediately displayed instead +# of waiting for program's end. +[jobs.run] +command = [ + "cargo", "run", + "--color", "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--color", "always", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target diff --git a/src/backend/download.rs b/src/backend/download.rs index 4ec8228..f6420ff 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -72,17 +72,17 @@ pub fn download_single_chaper( ) -> Result<(), std::io::Error> { let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; - let total_chapters = chapter_data.chapter.data.len(); + let total_pages = chapter_data.chapter.data.len(); tokio::spawn(async move { - for (index, file_name) in chapter_data.chapter.data.iter().enumerate() { + for (index, file_name) in chapter_data.chapter.data.into_iter().enumerate() { let endpoint = format!( "{}/data/{}", chapter_data.base_url, chapter_data.chapter.hash ); let image_response = MangadexClient::global() - .get_chapter_page(&endpoint, file_name) + .get_chapter_page(&endpoint, &file_name) .await; let file_name = Path::new(&file_name); @@ -94,14 +94,11 @@ pub fn download_single_chaper( index + 1, file_name.extension().unwrap().to_str().unwrap() ); - if exists!(&chapter_dir.join(&image_name)) { - return; - } - - let mut image_created = File::create(image_name).unwrap(); + let mut image_created = File::create(chapter_dir.join(image_name)).unwrap(); image_created.write_all(&bytes).unwrap(); + tx.send(MangaPageEvents::SetDownloadProgress( - (index as f64) / (total_chapters as f64), + (index as f64) / (total_pages as f64), chapter_id.clone(), )) .ok(); @@ -151,6 +148,7 @@ pub fn download_chapter( index + 1, file_name.extension().unwrap().to_str().unwrap() ); + if exists!(&chapter_dir.join(&image_name)) { return; } @@ -187,7 +185,7 @@ pub fn download_all_chapters( let download_chapter_delay = if total_chapters <= 20 { 1 } else if (40..100).contains(&total_chapters) { - 4 + 3 } else if (100..200).contains(&total_chapters) { 6 } else { diff --git a/src/backend/fetch.rs b/src/backend/fetch.rs index 3191507..a0090df 100644 --- a/src/backend/fetch.rs +++ b/src/backend/fetch.rs @@ -1,9 +1,11 @@ +use std::time::Duration as StdDuration; + use super::filter::Languages; use super::{ChapterPagesResponse, ChapterResponse, MangaStatisticsResponse, SearchMangaResponse}; use crate::backend::filter::{Filters, IntoParam}; use crate::view::pages::manga::ChapterOrder; use bytes::Bytes; -use chrono::Months; +use chrono::{Duration, Months}; use once_cell::sync::OnceCell; use reqwest::StatusCode; @@ -94,6 +96,7 @@ impl MangadexClient { ) -> Result { self.client .get(format!("{}/{}", endpoint, file_name)) + .timeout(StdDuration::from_secs(20)) .send() .await? .bytes() diff --git a/src/view.rs b/src/view.rs index 3d0af1e..ba32c1d 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,3 +1,4 @@ pub mod app; pub mod pages; pub mod widgets; +pub mod tasks; diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 88010ad..9ceab6d 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -11,6 +11,7 @@ use crate::backend::{AppDirectories, ChapterResponse, MangaStatisticsResponse, S use crate::common::{Manga, PageType}; use crate::global::{ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::{set_status_style, set_tags_style}; +use crate::view::tasks::manga::search_chapters_operation; use crate::view::widgets::manga::{ ChapterItem, ChaptersListWidget, DownloadAllChaptersState, DownloadAllChaptersWidget, DownloadPhase, @@ -52,9 +53,10 @@ pub enum MangaPageActions { ScrollChapterUp, ToggleOrder, ReadChapter, - OpenAvailableLanguagesList, + ToggleAvailableLanguagesList, ScrollDownAvailbleLanguages, ScrollUpAvailbleLanguages, + SearchByLanguage, GoMangasAuthor, GoMangasArtist, SearchNextChapterPage, @@ -83,7 +85,7 @@ pub enum MangaPageEvents { LoadStatistics(Option), } -#[derive(Display, Default, Clone, Copy)] +#[derive(Display, Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum ChapterOrder { #[strum(to_string = "asc")] Ascending, @@ -131,6 +133,7 @@ impl MangaStatistics { } } +#[derive(Clone, Debug)] struct ChaptersData { state: tui_widget_list::ListState, widget: ChaptersListWidget, @@ -409,6 +412,12 @@ impl MangaPage { self.download_all_chapters_state.process_started() } + fn search_by_language(&mut self) { + self.chapters = None; + self.chapter_language = self.get_current_selected_language(); + self.search_chapters(); + } + fn handle_key_events(&mut self, key_event: KeyEvent) { if self.is_list_languages_open { match key_event.code { @@ -423,13 +432,13 @@ impl MangaPage { .ok(); } KeyCode::Enter | KeyCode::Char('s') => { - self.chapters = None; - self.chapter_language = self.get_current_selected_language(); - self.search_chapters(); + self.local_action_tx + .send(MangaPageActions::SearchByLanguage) + .ok(); } KeyCode::Char('l') | KeyCode::Esc => { self.local_action_tx - .send(MangaPageActions::OpenAvailableLanguagesList) + .send(MangaPageActions::ToggleAvailableLanguagesList) .ok(); } _ => {} @@ -477,11 +486,9 @@ impl MangaPage { .ok(); } KeyCode::Char('r') | KeyCode::Enter => { - if PICKER.is_some() { - self.local_action_tx - .send(MangaPageActions::ReadChapter) - .ok(); - } + self.local_action_tx + .send(MangaPageActions::ReadChapter) + .ok(); } KeyCode::Char('d') => { self.local_action_tx @@ -505,7 +512,7 @@ impl MangaPage { } KeyCode::Char('l') => { self.local_action_tx - .send(MangaPageActions::OpenAvailableLanguagesList) + .send(MangaPageActions::ToggleAvailableLanguagesList) .ok(); } KeyCode::Char('w') => { @@ -554,7 +561,7 @@ impl MangaPage { self.available_languages_state.select_previous(); } - fn open_available_languages_list(&mut self) { + fn toggle_available_languages_list(&mut self) { self.is_list_languages_open = !self.is_list_languages_open; } @@ -586,6 +593,9 @@ impl MangaPage { } fn read_chapter(&mut self) { + if PICKER.is_none() { + return; + } self.state = PageState::SearchingChapterData; match self.get_current_selected_chapter_mut() { Some(chapter_selected) => { @@ -674,22 +684,13 @@ impl MangaPage { 1 }; - self.tasks.spawn(async move { - let response = MangadexClient::global() - .get_manga_chapters(manga_id, page, language, chapter_order) - .await; - - match response { - Ok(chapters_response) => { - tx.send(MangaPageEvents::LoadChapters(Some(chapters_response))) - .ok(); - } - Err(e) => { - write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); - tx.send(MangaPageEvents::LoadChapters(None)).ok(); - } - } - }); + self.tasks.spawn(search_chapters_operation( + manga_id, + page, + language, + chapter_order, + tx, + )); } fn fetch_statistics(&mut self) { @@ -917,6 +918,7 @@ impl MangaPage { let lang = self.get_current_selected_language(); let tx = self.local_event_tx.clone(); let quality = self.download_all_chapters_state.image_quality; + #[cfg(not(test))] tokio::spawn(async move { let chapter_response = MangadexClient::global() .get_all_chapters_for_manga(&id, lang) @@ -1075,6 +1077,16 @@ impl MangaPage { } } } + + #[cfg(test)] + fn get_index_chapter_selected(&self) -> usize { + self.chapters.as_ref().unwrap().state.selected.unwrap() + } + + #[cfg(test)] + fn get_chapter_data(&self) -> ChaptersData { + self.chapters.as_ref().cloned().unwrap() + } } impl Component for MangaPage { @@ -1091,6 +1103,7 @@ impl Component for MangaPage { } fn update(&mut self, action: Self::Actions) { match action { + MangaPageActions::SearchByLanguage => self.search_by_language(), MangaPageActions::DownloadAllChapter => self.download_all_chapters(), MangaPageActions::ToggleImageQuality => self.toggle_image_quality(), MangaPageActions::CancelDownloadAll => self.cancel_download_all_chapters(), @@ -1100,7 +1113,9 @@ impl Component for MangaPage { MangaPageActions::SearchNextChapterPage => self.search_next_chapters(), MangaPageActions::ScrollDownAvailbleLanguages => self.scroll_language_down(), MangaPageActions::ScrollUpAvailbleLanguages => self.scroll_language_up(), - MangaPageActions::OpenAvailableLanguagesList => self.open_available_languages_list(), + MangaPageActions::ToggleAvailableLanguagesList => { + self.toggle_available_languages_list() + } MangaPageActions::GoMangasArtist => self.go_mangas_artist(), MangaPageActions::GoMangasAuthor => self.go_mangas_author(), MangaPageActions::ScrollChapterUp => self.scroll_chapter_up(), @@ -1136,25 +1151,223 @@ impl Component for MangaPage { #[cfg(test)] mod test { + use crate::backend::{ChapterData, Data}; use crate::view::widgets::press_key; use super::*; - #[test] - fn manga_page_initialized_correctly() { + fn get_manga_page() -> MangaPage { let manga = Manga::default(); let (tx, mut rx) = mpsc::unbounded_channel::(); - let mut manga_page = MangaPage::new(manga, None, tx); + MangaPage::new(manga, None, tx) + } + + fn get_chapters_response() -> ChapterResponse { + ChapterResponse { + data: vec![ + ChapterData::default(), + ChapterData::default(), + ChapterData::default(), + ], + total: 30, + ..Default::default() + } + } + + fn render_chapters(manga_page: &mut MangaPage) { + let area = Rect::new(0, 0, 50, 50); + let mut buf = Buffer::empty(area); + let chapters = manga_page.chapters.as_mut().unwrap(); + StatefulWidget::render(chapters.widget.clone(), area, &mut buf, &mut chapters.state); + } + + fn render_available_languages_list(manga_page: &mut MangaPage) { + let area = Rect::new(0, 0, 50, 50); + let mut buf = Buffer::empty(area); + let languages: Vec = manga_page + .manga + .available_languages + .iter() + .map(|lang| lang.as_human_readable()) + .collect(); + let list = List::new(languages); + + StatefulWidget::render( + list, + area, + &mut buf, + &mut manga_page.available_languages_state, + ); + } + + #[tokio::test] + async fn key_events_trigger_expected_actions() { + let mut manga_page = get_manga_page(); + + // Scroll down chapters list + press_key(&mut manga_page, KeyCode::Char('j')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ScrollChapterDown, action); + + // Scroll up chapters list + press_key(&mut manga_page, KeyCode::Char('k')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ScrollChapterUp, action); + + // toggle chapter order + press_key(&mut manga_page, KeyCode::Char('t')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ToggleOrder, action); + + // Go next chapter page + press_key(&mut manga_page, KeyCode::Char('w')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::SearchNextChapterPage, action); + + // Go previous chapter page + press_key(&mut manga_page, KeyCode::Char('b')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::SearchPreviousChapterPage, action); + + // Open available_languages list + press_key(&mut manga_page, KeyCode::Char('l')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ToggleAvailableLanguagesList, action); + + manga_page.toggle_available_languages_list(); + + // scroll down available languages list + press_key(&mut manga_page, KeyCode::Char('j')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ScrollDownAvailbleLanguages, action); + + // scroll down available languages list + press_key(&mut manga_page, KeyCode::Char('k')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ScrollUpAvailbleLanguages, action); + + // search by a language selected + press_key(&mut manga_page, KeyCode::Char('s')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::SearchByLanguage, action); + + // close available languages list + press_key(&mut manga_page, KeyCode::Esc); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ToggleAvailableLanguagesList, action); + + manga_page.toggle_available_languages_list(); + + // download chapter + press_key(&mut manga_page, KeyCode::Char('d')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::DownloadChapter, action); + + // start download all chapter proccess + press_key(&mut manga_page, KeyCode::Char('a')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::AskDownloadAllChapters, action); + + manga_page.ask_download_all_chapters(); + + // confirm download all chapters + press_key(&mut manga_page, KeyCode::Enter); + let action = manga_page.local_action_rx.recv().await.unwrap(); + assert_eq!(MangaPageActions::ConfirmDownloadAll, action); + + manga_page.confirm_download_all(); + + // toggle image quality for chapters to be downloaded + press_key(&mut manga_page, KeyCode::Char('t')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ToggleImageQuality, action); + + // download all chapters + press_key(&mut manga_page, KeyCode::Char(' ')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::DownloadAllChapter, action); + + // cancel download all chapters operation + press_key(&mut manga_page, KeyCode::Esc); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::CancelDownloadAll, action); + + manga_page.cancel_download_all_chapters(); + + // read a chapter + press_key(&mut manga_page, KeyCode::Char('r')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ReadChapter, action); + + // see more about author + press_key(&mut manga_page, KeyCode::Char('c')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::GoMangasAuthor, action); + + // see more about artist + press_key(&mut manga_page, KeyCode::Char('v')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::GoMangasArtist, action); + } + + #[tokio::test] + async fn listen_to_key_events_based_on_conditions() { + let mut manga_page = get_manga_page(); + + assert!(!manga_page.is_list_languages_open); + + press_key(&mut manga_page, KeyCode::Char('j')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ScrollChapterDown, action); + + manga_page.toggle_available_languages_list(); + + press_key(&mut manga_page, KeyCode::Char('j')); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ScrollDownAvailbleLanguages, action); + + manga_page.toggle_available_languages_list(); + manga_page.ask_download_all_chapters(); + + press_key(&mut manga_page, KeyCode::Enter); + let action = manga_page.local_action_rx.recv().await.unwrap(); + + assert_eq!(MangaPageActions::ConfirmDownloadAll, action); + } + + async fn manga_page_initialized_correctly(manga_page: &mut MangaPage) { assert_eq!(manga_page.chapter_language, Languages::default()); + assert_eq!(ChapterOrder::default(), manga_page.chapter_order); + assert_eq!(PageState::SearchingChapters, manga_page.state); assert!(!manga_page.is_list_languages_open); - let first_event = manga_page.local_event_rx.blocking_recv().unwrap(); - let second_event = manga_page.local_event_rx.blocking_recv().unwrap(); + let first_event = manga_page.local_event_rx.recv().await.unwrap(); + let second_event = manga_page.local_event_rx.recv().await.unwrap(); assert!( first_event == MangaPageEvents::FethStatistics @@ -1166,46 +1379,114 @@ mod test { ); } - #[test] - fn manga_page_key_events() { - let manga = Manga::default(); - let (tx, mut rx) = mpsc::unbounded_channel::(); - let mut manga_page = MangaPage::new(manga, None, tx); + #[tokio::test] + async fn handle_events() { + let mut manga_page = get_manga_page(); + manga_page_initialized_correctly(&mut manga_page).await; + } - let mock_chapter_response = ChapterResponse { - data: vec![ChapterData::default(), ChapterData::default()], - ..Default::default() - }; + #[tokio::test] + async fn handle_key_events() { + let mut manga_page = get_manga_page(); + manga_page.state = PageState::SearchingChapters; + manga_page.manga.available_languages = vec![ + Languages::default(), + Languages::Spanish, + Languages::German, + Languages::Japanese, + ]; - // Assuming chapters were found - manga_page.load_chapters(Some(mock_chapter_response)); + assert_eq!(ChapterOrder::default(), manga_page.chapter_order); - assert!(manga_page.chapters.is_some()); + let action = MangaPageActions::ToggleOrder; + manga_page.update(action); - press_key(&mut manga_page, KeyCode::Char('j')); + /// when searching chapters avoid triggering another search by toggling order + assert_eq!(ChapterOrder::default(), manga_page.chapter_order); - let action = manga_page.local_action_rx.blocking_recv().unwrap(); + manga_page.state = PageState::DisplayingChapters; + manga_page.load_chapters(Some(get_chapters_response())); + render_chapters(&mut manga_page); - assert_eq!(MangaPageActions::ScrollChapterDown, action); + let action = MangaPageActions::ToggleOrder; + manga_page.update(action); + + assert_eq!(ChapterOrder::Ascending, manga_page.chapter_order); + let action = MangaPageActions::ScrollChapterDown; manga_page.update(action); - assert!(manga_page - .chapters - .as_ref() - .unwrap() - .state - .selected - .is_some()); + assert_eq!(1, manga_page.get_index_chapter_selected()); - press_key(&mut manga_page, KeyCode::Char('l')); + let action = MangaPageActions::ScrollChapterUp; + manga_page.update(action); + + assert_eq!(0, manga_page.get_index_chapter_selected()); - let action = manga_page.local_action_rx.blocking_recv().unwrap(); + let action = MangaPageActions::SearchNextChapterPage; + manga_page.update(action); - assert_eq!(MangaPageActions::OpenAvailableLanguagesList, action); + assert_eq!(2, manga_page.get_chapter_data().page); + let action = MangaPageActions::SearchPreviousChapterPage; + manga_page.update(action); + + assert_eq!(1, manga_page.get_chapter_data().page); + + let action = MangaPageActions::ToggleAvailableLanguagesList; manga_page.update(action); assert!(manga_page.is_list_languages_open); + + let action = MangaPageActions::ScrollUpAvailbleLanguages; + manga_page.update(action); + + render_available_languages_list(&mut manga_page); + + assert_eq!(3, manga_page.available_languages_state.selected().unwrap()); + + manga_page.available_languages_state.select(Some(1)); + + let action = MangaPageActions::ScrollDownAvailbleLanguages; + manga_page.update(action); + + assert_eq!(2, manga_page.available_languages_state.selected().unwrap()); + + let action = MangaPageActions::SearchByLanguage; + manga_page.update(action); + + assert_eq!(PageState::SearchingChapters, manga_page.state); + assert!(manga_page.chapters.is_none()); + + let action = MangaPageActions::ToggleAvailableLanguagesList; + manga_page.update(action); + + assert!(!manga_page.is_list_languages_open); + + let action = MangaPageActions::AskDownloadAllChapters; + manga_page.update(action); + + assert!(manga_page.download_process_started()); + + let action = MangaPageActions::ConfirmDownloadAll; + manga_page.update(action); + + assert_eq!( + DownloadPhase::SettingQuality, + manga_page.download_all_chapters_state.phase + ); + + let action = MangaPageActions::DownloadAllChapter; + manga_page.update(action); + + assert_eq!( + DownloadPhase::FetchingChaptersData, + manga_page.download_all_chapters_state.phase + ); + + let action = MangaPageActions::CancelDownloadAll; + manga_page.update(action); + + assert!(!manga_page.download_process_started()); } } diff --git a/src/view/tasks.rs b/src/view/tasks.rs new file mode 100644 index 0000000..0a8dc20 --- /dev/null +++ b/src/view/tasks.rs @@ -0,0 +1,3 @@ +pub mod manga; + + diff --git a/src/view/tasks/manga.rs b/src/view/tasks/manga.rs new file mode 100644 index 0000000..621c051 --- /dev/null +++ b/src/view/tasks/manga.rs @@ -0,0 +1,41 @@ +use tokio::sync::mpsc::UnboundedSender; + +use crate::backend::error_log::{write_to_error_log, ErrorType}; +use crate::backend::fetch::MangadexClient; +use crate::backend::filter::Languages; +use crate::view::pages::manga::{ChapterOrder, MangaPageEvents}; + +#[cfg(not(test))] +pub async fn search_chapters_operation( + manga_id: String, + page: u32, + language: Languages, + chapter_order: ChapterOrder, + tx: UnboundedSender, +) { + let response = MangadexClient::global() + .get_manga_chapters(manga_id, page, language, chapter_order) + .await; + + match response { + Ok(chapters_response) => { + tx.send(MangaPageEvents::LoadChapters(Some(chapters_response))) + .ok(); + } + Err(e) => { + write_to_error_log(ErrorType::FromError(Box::new(e))); + tx.send(MangaPageEvents::LoadChapters(None)).ok(); + } + } +} + +#[cfg(test)] +pub async fn search_chapters_operation( + manga_id: String, + page: u32, + language: Languages, + chapter_order: ChapterOrder, + tx: UnboundedSender, +) { + tx.send(MangaPageEvents::LoadChapters(None)); +} diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index 41cc551..a410bd5 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -12,7 +12,7 @@ use tui_widget_list::PreRender; use self::text::ToSpan; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum ChapterItemState { Normal, /// When the user tried to download a chapter and there was an error @@ -21,7 +21,7 @@ pub enum ChapterItemState { ReadError, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ChapterItem { pub id: String, pub title: String, @@ -186,7 +186,7 @@ impl ChapterItem { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ChaptersListWidget { pub chapters: Vec, } From 40a75fc0c57950c6d4da11ff168785c6c9855457 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Sat, 17 Aug 2024 16:40:12 -0500 Subject: [PATCH 09/14] feat(Config): Add config file --- Cargo.lock | 312 ++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + src/backend.rs | 49 ++++--- src/backend/download.rs | 64 ++++++++- src/config.rs | 66 +++++++++ src/main.rs | 3 + src/view/pages/manga.rs | 4 +- 7 files changed, 462 insertions(+), 38 deletions(-) create mode 100644 src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 09b9110..311586a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -125,6 +136,9 @@ name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arg_enum_proc_macro" @@ -223,6 +237,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "built" version = "0.7.3" @@ -268,6 +291,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -324,6 +368,16 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.10" @@ -416,6 +470,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "core-foundation" version = "0.9.4" @@ -432,6 +492,30 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -498,6 +582,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "deranged" version = "0.3.11" @@ -507,6 +607,28 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "directories" version = "5.0.1" @@ -773,6 +895,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -860,6 +992,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.1.0" @@ -1170,6 +1311,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1332,6 +1482,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.21" @@ -1356,6 +1512,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "manga-tui" version = "0.2.0" @@ -1381,8 +1547,10 @@ dependencies = [ "strum_macros", "throbber-widgets-tui", "tokio", + "toml", "tui-input", "tui-widget-list", + "zip", ] [[package]] @@ -1690,6 +1858,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2193,9 +2371,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -2212,6 +2390,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2356,6 +2545,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.66" @@ -2573,9 +2768,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -2585,18 +2780,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", @@ -2698,6 +2893,12 @@ dependencies = [ "ratatui", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -3059,9 +3260,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -3153,6 +3354,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerovec" version = "0.10.2" @@ -3175,6 +3396,77 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "2.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 3b3d342..391f85d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,5 @@ chrono = "0.4.38" open = "5" rusqlite = { version = "0.31.0", features = ["bundled"] } clap = { version = "4.4.5", features = ["derive", "cargo"] } +zip = "2.1.6" +toml = "0.8.19" diff --git a/src/backend.rs b/src/backend.rs index 7cfc1ab..0349a65 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -4,7 +4,9 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::{create_dir, create_dir_all, File}; use std::path::{Path, PathBuf}; -use strum::Display; +use strum::{Display, EnumIter, IntoEnumIterator}; + +use crate::config::{MangaTuiConfig, CONFIG}; use self::error_log::ERROR_LOGS_FILE; @@ -15,7 +17,7 @@ pub mod fetch; pub mod filter; pub mod tui; -#[derive(Display)] +#[derive(Display, EnumIter)] pub enum AppDirectories { #[strum(to_string = "mangaDownloads")] MangaDownloads, @@ -23,24 +25,23 @@ pub enum AppDirectories { ErrorLogs, #[strum(to_string = "history")] History, + #[strum(to_string = "config")] + Config, } impl AppDirectories { pub fn into_path_buf(self) -> PathBuf { let base_directory = APP_DATA_DIR.as_ref(); - match self { - Self::MangaDownloads => PathBuf::from( - &base_directory - .unwrap() - .join(Self::MangaDownloads.to_string()), - ), - Self::History => { - PathBuf::from(&base_directory.unwrap().join(Self::History.to_string())) - } - Self::ErrorLogs => { - PathBuf::from(&base_directory.unwrap().join(Self::ErrorLogs.to_string())) + PathBuf::from(&base_directory.unwrap().join(self.to_string())) + } + + pub fn build_if_not_exists(base_directory: &Path) -> Result<(), std::io::Error> { + for dir in AppDirectories::iter() { + if !exists!(&base_directory.join(dir.to_string())) { + create_dir(base_directory.join(dir.to_string()))?; } } + Ok(()) } } @@ -60,18 +61,7 @@ pub fn build_data_dir() -> Result<(), std::io::Error> { if !exists!(dir) { create_dir_all(dir)?; } - - if !exists!(&dir.join(AppDirectories::MangaDownloads.to_string())) { - create_dir(dir.join(AppDirectories::MangaDownloads.to_string()))?; - } - - if !exists!(&dir.join(AppDirectories::ErrorLogs.to_string())) { - create_dir(dir.join(AppDirectories::ErrorLogs.to_string()))?; - } - - if !exists!(&dir.join(AppDirectories::History.to_string())) { - create_dir(dir.join(AppDirectories::History.to_string()))?; - } + AppDirectories::build_if_not_exists(dir)?; if !exists!(&dir .join(AppDirectories::ErrorLogs.to_string()) @@ -83,6 +73,15 @@ pub fn build_data_dir() -> Result<(), std::io::Error> { )?; } + MangaTuiConfig::write_config(dir)?; + + let config_contents = MangaTuiConfig::read_config(dir)?; + + let config_contents: MangaTuiConfig = + toml::from_str(&config_contents).map_err(std::io::Error::other)?; + + CONFIG.set(config_contents).unwrap(); + Ok(()) } None => Err(std::io::Error::other("data dir could not be found")), diff --git a/src/backend/download.rs b/src/backend/download.rs index f6420ff..5b881b2 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -4,6 +4,8 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; +use zip::write::{FileOptions, SimpleFileOptions}; +use zip::ZipWriter; use crate::common::PageType; use crate::view::pages::manga::MangaPageEvents; @@ -65,7 +67,7 @@ fn create_manga_directory( Ok((chapter_dir, chapter_id)) } -pub fn download_single_chaper( +pub fn download_single_chaper_raw_images( chapter: DownloadChapter<'_>, chapter_data: ChapterPagesResponse, tx: UnboundedSender, @@ -106,6 +108,66 @@ pub fn download_single_chaper( Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), } } + + tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) + .ok(); + }); + + Ok(()) +} + +pub fn download_single_chapter_cbz( + chapter: DownloadChapter<'_>, + chapter_data: ChapterPagesResponse, + tx: UnboundedSender, +) -> Result<(), std::io::Error> { + let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; + + let total_pages = chapter_data.chapter.data.len(); + + tokio::spawn(async move { + let chapter_zip_file = File::create(chapter_dir.join("test.zip")).unwrap(); + + let mut zip = ZipWriter::new(chapter_zip_file); + + let options = SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated) + .unix_permissions(0o755); + + for (index, file_name) in chapter_data.chapter.data.into_iter().enumerate() { + let endpoint = format!( + "{}/data/{}", + chapter_data.base_url, chapter_data.chapter.hash + ); + + let image_response = MangadexClient::global() + .get_chapter_page(&endpoint, &file_name) + .await; + + let file_name = Path::new(&file_name); + + match image_response { + Ok(bytes) => { + let image_name = format!( + "{}.{}", + index + 1, + file_name.extension().unwrap().to_str().unwrap() + ); + + zip.start_file(chapter_dir.join(image_name).to_str().unwrap(), options); + zip.write_all(&bytes).unwrap(); + + tx.send(MangaPageEvents::SetDownloadProgress( + (index as f64) / (total_pages as f64), + chapter_id.clone(), + )) + .ok(); + } + Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), + } + } + zip.finish().unwrap(); + tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) .ok(); }); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..68010ad --- /dev/null +++ b/src/config.rs @@ -0,0 +1,66 @@ +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; + +use manga_tui::exists; +use once_cell::sync::{Lazy, OnceCell}; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumIter}; + +use crate::backend::AppDirectories; + +#[derive(Default, Debug, Serialize, Deserialize, Display, EnumIter)] +#[serde(rename_all = "snake_case")] +pub enum DownloadType { + #[default] + Cbz, + Raw, + Pdf, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct MangaTuiConfig { + pub download_type: DownloadType, +} + +pub static CONFIG_FILE: &str = "manga-tui-config.toml"; + +pub static CONFIG: OnceCell = OnceCell::new(); + +impl MangaTuiConfig { + pub fn get() -> &'static Self { + CONFIG.get().expect("Could not get download type") + } + + pub fn read_config(base_directory: &Path) -> Result { + let config_file = base_directory + .join(AppDirectories::Config.to_string()) + .join(CONFIG_FILE); + + let mut config_file = File::open(config_file)?; + + let mut contents = String::new(); + config_file.read_to_string(&mut contents)?; + + Ok(contents) + } + + pub fn write_config(base_directory: &Path) -> Result<(), std::io::Error> { + let contents = r#" + # available values : cbz, raw, pdf + download_type = "cbz" + + "#; + + let config_file = base_directory + .join(AppDirectories::Config.to_string()) + .join(CONFIG_FILE); + + if !exists!(&config_file) { + let mut config_file = File::create(config_file)?; + config_file.write_all(contents.as_bytes())? + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 53c1df9..d7954fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod common; mod global; mod utils; mod view; +mod config; #[cfg(unix)] pub static PICKER: Lazy> = Lazy::new(|| { @@ -133,6 +134,8 @@ async fn main() -> Result<(), Box> { } } + + init_error_hooks()?; init()?; run_app(CrosstermBackend::new(std::io::stdout())).await?; diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 9ceab6d..12d8011 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,7 +1,7 @@ use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; use crate::backend::download::{ - download_all_chapters, download_single_chaper, DownloadAllChapters, DownloadChapter, + download_all_chapters, download_single_chaper_raw_images, DownloadAllChapters, DownloadChapter, }; use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; @@ -758,7 +758,7 @@ impl MangaPage { .await; match manga_response { Ok(res) => { - let download_chapter_task = download_single_chaper( + let download_chapter_task = download_single_chaper_raw_images( DownloadChapter { id_chapter: &chapter_id, manga_id: &manga_id, From 3dc3de8a1cf6830f1a635648ee24b68608f389a8 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Sat, 17 Aug 2024 20:43:24 -0500 Subject: [PATCH 10/14] refactor(Download): changed implementation download functions to support config file --- src/backend.rs | 2 +- src/backend/download.rs | 228 ++++++++++++++++++-------------------- src/config.rs | 28 ++++- src/view/pages/manga.rs | 74 +++++++------ src/view/widgets/manga.rs | 24 +--- 5 files changed, 178 insertions(+), 178 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 0349a65..c0100b6 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -78,7 +78,7 @@ pub fn build_data_dir() -> Result<(), std::io::Error> { let config_contents = MangaTuiConfig::read_config(dir)?; let config_contents: MangaTuiConfig = - toml::from_str(&config_contents).map_err(std::io::Error::other)?; + toml::from_str(&config_contents).unwrap_or_default(); CONFIG.set(config_contents).unwrap(); diff --git a/src/backend/download.rs b/src/backend/download.rs index 5b881b2..a40563a 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -8,6 +8,7 @@ use zip::write::{FileOptions, SimpleFileOptions}; use zip::ZipWriter; use crate::common::PageType; +use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; use crate::view::pages::manga::MangaPageEvents; use super::error_log::{write_to_error_log, ErrorType}; @@ -25,9 +26,7 @@ pub struct DownloadChapter<'a> { pub lang: &'a str, } -fn create_manga_directory( - chapter: &DownloadChapter<'_>, -) -> Result<(PathBuf, String), std::io::Error> { +fn create_manga_directory(chapter: &DownloadChapter<'_>) -> Result<(PathBuf), std::io::Error> { // need directory with the manga's title, and its id to make it unique let chapter_id = chapter.id_chapter.to_string(); @@ -52,37 +51,34 @@ fn create_manga_directory( // need directory with chapter's title, number and scanlator + Ok(chapter_language_dir) +} + +pub fn download_chapter_raw_images( + is_downloading_all_chapters: bool, + chapter: DownloadChapter<'_>, + files: Vec, + endpoint: String, + tx: UnboundedSender, +) -> Result<(), std::io::Error> { + let chapter_language_dir = create_manga_directory(&chapter)?; + let chapter_dir = chapter_language_dir.join(format!( "Ch. {} {} {} {}", chapter.number, chapter.title.trim().replace('/', "-"), chapter.scanlator.trim().replace('/', "-"), - chapter_id + chapter.id_chapter, )); if !exists!(&chapter_dir) { create_dir(&chapter_dir)?; } - - Ok((chapter_dir, chapter_id)) -} - -pub fn download_single_chaper_raw_images( - chapter: DownloadChapter<'_>, - chapter_data: ChapterPagesResponse, - tx: UnboundedSender, -) -> Result<(), std::io::Error> { - let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; - - let total_pages = chapter_data.chapter.data.len(); + let chapter_id = chapter.id_chapter.to_string(); tokio::spawn(async move { - for (index, file_name) in chapter_data.chapter.data.into_iter().enumerate() { - let endpoint = format!( - "{}/data/{}", - chapter_data.base_url, chapter_data.chapter.hash - ); - + let total_pages = files.len(); + for (index, file_name) in files.into_iter().enumerate() { let image_response = MangadexClient::global() .get_chapter_page(&endpoint, &file_name) .await; @@ -99,47 +95,61 @@ pub fn download_single_chaper_raw_images( let mut image_created = File::create(chapter_dir.join(image_name)).unwrap(); image_created.write_all(&bytes).unwrap(); - tx.send(MangaPageEvents::SetDownloadProgress( - (index as f64) / (total_pages as f64), - chapter_id.clone(), - )) - .ok(); + if !is_downloading_all_chapters { + tx.send(MangaPageEvents::SetDownloadProgress( + (index as f64) / (total_pages as f64), + chapter_id.clone(), + )) + .ok(); + } } Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), } } - tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) - .ok(); + if is_downloading_all_chapters { + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + } else { + tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) + .ok(); + } }); Ok(()) } -pub fn download_single_chapter_cbz( +pub fn download_chapter_cbz( + is_downloading_all_chapters: bool, chapter: DownloadChapter<'_>, - chapter_data: ChapterPagesResponse, + files: Vec, + endpoint: String, tx: UnboundedSender, ) -> Result<(), std::io::Error> { - let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; + let (chapter_dir_language) = create_manga_directory(&chapter)?; - let total_pages = chapter_data.chapter.data.len(); + let chapter_id = chapter.id_chapter.to_string(); + let chapter_name = format!( + "Ch. {} {} | {} | {}", + chapter.number, + chapter.title.trim().replace('/', "-"), + chapter.scanlator.trim().replace('/', "-"), + chapter.id_chapter, + ); - tokio::spawn(async move { - let chapter_zip_file = File::create(chapter_dir.join("test.zip")).unwrap(); + let chapter_name = format!("{}.cbz", chapter_name); + let chapter_zip_file = File::create(chapter_dir_language.join(chapter_name))?; + + tokio::spawn(async move { let mut zip = ZipWriter::new(chapter_zip_file); + let total_pages = files.len(); let options = SimpleFileOptions::default() .compression_method(zip::CompressionMethod::Deflated) .unix_permissions(0o755); - for (index, file_name) in chapter_data.chapter.data.into_iter().enumerate() { - let endpoint = format!( - "{}/data/{}", - chapter_data.base_url, chapter_data.chapter.hash - ); - + for (index, file_name) in files.into_iter().enumerate() { let image_response = MangadexClient::global() .get_chapter_page(&endpoint, &file_name) .await; @@ -154,76 +164,32 @@ pub fn download_single_chapter_cbz( file_name.extension().unwrap().to_str().unwrap() ); - zip.start_file(chapter_dir.join(image_name).to_str().unwrap(), options); + zip.start_file( + chapter_dir_language.join(image_name).to_str().unwrap(), + options, + ); zip.write_all(&bytes).unwrap(); - tx.send(MangaPageEvents::SetDownloadProgress( - (index as f64) / (total_pages as f64), - chapter_id.clone(), - )) - .ok(); + if !is_downloading_all_chapters { + tx.send(MangaPageEvents::SetDownloadProgress( + (index as f64) / (total_pages as f64), + chapter_id.to_string(), + )) + .ok(); + } } Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), } } zip.finish().unwrap(); - tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) - .ok(); - }); - - Ok(()) -} - -pub fn download_chapter( - chapter: DownloadChapter<'_>, - chapter_data: ChapterPagesResponse, - chapter_quality: PageType, - chapter_number: usize, - total_chapters: usize, - tx: UnboundedSender, -) -> Result<(), std::io::Error> { - let (chapter_dir, chapter_id) = create_manga_directory(&chapter)?; - - let files = match chapter_quality { - PageType::HighQuality => chapter_data.chapter.data, - PageType::LowQuality => chapter_data.chapter.data_saver, - }; - - tokio::spawn(async move { - for (index, file_name) in files.into_iter().enumerate() { - let endpoint = format!( - "{}/{}/{}", - chapter_data.base_url, chapter_quality, chapter_data.chapter.hash - ); - - let image_response = MangadexClient::global() - .get_chapter_page(&endpoint, &file_name) - .await; - - let file_name = Path::new(&file_name); - - match image_response { - Ok(bytes) => { - let image_name = format!( - "{}.{}", - index + 1, - file_name.extension().unwrap().to_str().unwrap() - ); - - if exists!(&chapter_dir.join(&image_name)) { - return; - } - - let mut image_created = File::create(chapter_dir.join(image_name)).unwrap(); - image_created.write_all(&bytes).unwrap(); - } - Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), - } + if is_downloading_all_chapters { + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + } else { + tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) + .ok(); } - - tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) - .ok(); }); Ok(()) @@ -233,7 +199,6 @@ pub fn download_chapter( pub struct DownloadAllChapters { pub manga_id: String, pub manga_title: String, - pub quality: PageType, pub lang: Languages, } @@ -253,11 +218,14 @@ pub fn download_all_chapters( } else { 8 }; + let config = MangaTuiConfig::get(); for (index, chapter) in chapter_data.data.into_iter().enumerate() { let id = chapter.id.clone(); + let chapter_number = chapter.attributes.chapter.unwrap_or_default(); let manga_id = manga_details.manga_id.clone(); let manga_title = manga_details.manga_title.clone(); + let lang = manga_details.lang; let tx = tx.clone(); @@ -272,24 +240,45 @@ pub fn download_all_chapters( let chapter_title = chapter.attributes.title.unwrap_or_default(); match pages_response { - Ok(res) => { - let download_proccess = download_chapter( - DownloadChapter { - id_chapter: &chapter.id, - manga_id: &manga_id, - manga_title: &manga_title, - title: chapter_title.as_str(), - number: chapter.attributes.chapter.unwrap_or_default().as_str(), - scanlator: &scanlator.unwrap_or_default(), - lang: &manga_details.lang.as_human_readable(), - }, - res, - manga_details.quality, - index, - total_chapters, - tx.clone(), + Ok(response) => { + let (files, quality) = match config.image_quality { + ImageQuality::Low => (response.chapter.data_saver, PageType::LowQuality), + ImageQuality::High => (response.chapter.data, PageType::HighQuality), + }; + + let endpoint = format!( + "{}/{}/{}", + response.base_url, quality, response.chapter.hash ); + let chapter_to_download = DownloadChapter { + id_chapter: &chapter.id, + manga_id: &manga_id, + manga_title: &manga_title, + title: &chapter_title, + number: &chapter_number, + scanlator: &scanlator.unwrap_or_default(), + lang: &lang.as_human_readable(), + }; + + let download_proccess = match config.download_type { + DownloadType::Cbz => download_chapter_cbz( + true, + chapter_to_download, + files, + endpoint, + tx.clone(), + ), + DownloadType::Raw => download_chapter_raw_images( + true, + chapter_to_download, + files, + endpoint, + tx.clone(), + ), + DownloadType::Pdf => Ok(()), + }; + if let Err(e) = download_proccess { let error_message = format!( "Chapter: {} could not be downloaded, details: {}", @@ -302,6 +291,7 @@ pub fn download_all_chapters( write_to_error_log(ErrorType::FromError(Box::from(error_message))); return; } + tx.send(MangaPageEvents::SaveChapterDownloadStatus( chapter.id, chapter_title, diff --git a/src/config.rs b/src/config.rs index 68010ad..d2e0cc1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,9 +18,18 @@ pub enum DownloadType { Pdf, } +#[derive(Default, Debug, Serialize, Deserialize, Display, EnumIter)] +#[serde(rename_all = "snake_case")] +pub enum ImageQuality { + #[default] + Low, + High, +} + #[derive(Default, Debug, Serialize, Deserialize)] pub struct MangaTuiConfig { pub download_type: DownloadType, + pub image_quality: ImageQuality, } pub static CONFIG_FILE: &str = "manga-tui-config.toml"; @@ -45,13 +54,28 @@ impl MangaTuiConfig { Ok(contents) } + #[allow(clippy::format_collect)] pub fn write_config(base_directory: &Path) -> Result<(), std::io::Error> { - let contents = r#" - # available values : cbz, raw, pdf + let default_config = MangaTuiConfig::default(); + + let contents = r#" + + # The format of the manga downloaded + # values : cbz , raw, pdf + # default : cbz download_type = "cbz" + # Download image quality, low quality means images are compressed and is recommended for slow internet connections + # values : low, high + # default : low + image_quality = "low" "#; + let contents: String = contents + .lines() + .map(|line| format!("{} \n", line.trim())) + .collect(); + let config_file = base_directory .join(AppDirectories::Config.to_string()) .join(CONFIG_FILE); diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 12d8011..6c7fa29 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,7 +1,8 @@ use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; use crate::backend::download::{ - download_all_chapters, download_single_chaper_raw_images, DownloadAllChapters, DownloadChapter, + download_all_chapters, download_chapter_cbz, download_chapter_raw_images, DownloadAllChapters, + DownloadChapter, }; use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; @@ -9,6 +10,7 @@ use crate::backend::filter::Languages; use crate::backend::tui::Events; use crate::backend::{AppDirectories, ChapterResponse, MangaStatisticsResponse, Statistics}; use crate::common::{Manga, PageType}; +use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; use crate::global::{ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::{set_status_style, set_tags_style}; use crate::view::tasks::manga::search_chapters_operation; @@ -45,7 +47,6 @@ pub enum PageState { pub enum MangaPageActions { DownloadChapter, DownloadAllChapter, - ToggleImageQuality, ConfirmDownloadAll, CancelDownloadAll, AskDownloadAllChapters, @@ -451,11 +452,7 @@ impl MangaPage { .send(MangaPageActions::CancelDownloadAll) .ok(); } - KeyCode::Char('t') => { - self.local_action_tx - .send(MangaPageActions::ToggleImageQuality) - .ok(); - } + KeyCode::Enter => { self.local_action_tx .send(MangaPageActions::ConfirmDownloadAll) @@ -757,21 +754,43 @@ impl MangaPage { .get_chapter_pages(&chapter_id) .await; match manga_response { - Ok(res) => { - let download_chapter_task = download_single_chaper_raw_images( - DownloadChapter { - id_chapter: &chapter_id, - manga_id: &manga_id, - manga_title: &manga_title, - title: &title, - number: &number, - scanlator: &scanlator, - lang: &lang, - }, - res, - tx.clone(), - ); + Ok(response) => { + let config = MangaTuiConfig::get(); + + let (files, quality) = match config.image_quality { + ImageQuality::Low => { + (response.chapter.data_saver, PageType::LowQuality) + } + ImageQuality::High => (response.chapter.data, PageType::HighQuality), + }; + let endpoint = format!( + "{}/{}/{}", + response.base_url, quality, response.chapter.hash + ); + let chapter = DownloadChapter { + id_chapter: &chapter_id, + manga_id: &manga_id, + manga_title: &manga_title, + title: &title, + number: &number, + scanlator: &scanlator, + lang: &lang, + }; + + let download_chapter_task = match config.download_type { + DownloadType::Raw => download_chapter_raw_images( + false, + chapter, + files, + endpoint, + tx.clone(), + ), + DownloadType::Cbz => { + download_chapter_cbz(false, chapter, files, endpoint, tx.clone()) + } + DownloadType::Pdf => Ok(()), + }; if let Err(e) = download_chapter_task { write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); tx.send(MangaPageEvents::DownloadError(chapter_id)).ok(); @@ -917,7 +936,6 @@ impl MangaPage { let manga_title = self.manga.title.clone(); let lang = self.get_current_selected_language(); let tx = self.local_event_tx.clone(); - let quality = self.download_all_chapters_state.image_quality; #[cfg(not(test))] tokio::spawn(async move { let chapter_response = MangadexClient::global() @@ -935,7 +953,6 @@ impl MangaPage { DownloadAllChapters { manga_title, manga_id: id, - quality, lang, }, tx, @@ -964,10 +981,6 @@ impl MangaPage { } } - fn toggle_image_quality(&mut self) { - self.download_all_chapters_state.toggle_image_quality(); - } - fn start_download_all_chapters(&mut self, total_chapters: f64) { self.download_all_chapters_state .set_total_chapters(total_chapters); @@ -1105,7 +1118,6 @@ impl Component for MangaPage { match action { MangaPageActions::SearchByLanguage => self.search_by_language(), MangaPageActions::DownloadAllChapter => self.download_all_chapters(), - MangaPageActions::ToggleImageQuality => self.toggle_image_quality(), MangaPageActions::CancelDownloadAll => self.cancel_download_all_chapters(), MangaPageActions::AskDownloadAllChapters => self.ask_download_all_chapters(), MangaPageActions::ConfirmDownloadAll => self.confirm_download_all(), @@ -1291,12 +1303,6 @@ mod test { manga_page.confirm_download_all(); - // toggle image quality for chapters to be downloaded - press_key(&mut manga_page, KeyCode::Char('t')); - let action = manga_page.local_action_rx.recv().await.unwrap(); - - assert_eq!(MangaPageActions::ToggleImageQuality, action); - // download all chapters press_key(&mut manga_page, KeyCode::Char(' ')); let action = manga_page.local_action_rx.recv().await.unwrap(); diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index a410bd5..9f48796 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -261,7 +261,6 @@ pub enum DownloadPhase { #[derive(Debug)] pub struct DownloadAllChaptersState { pub phase: DownloadPhase, - pub image_quality: PageType, pub total_chapters: f64, pub loader_state: ThrobberState, pub download_progress: f64, @@ -273,7 +272,6 @@ impl DownloadAllChaptersState { pub fn new(tx: UnboundedSender) -> Self { Self { phase: DownloadPhase::default(), - image_quality: PageType::default(), total_chapters: 0.0, loader_state: ThrobberState::default(), download_progress: 0.0, @@ -346,10 +344,6 @@ impl DownloadAllChaptersState { self.download_location = location } - pub fn toggle_image_quality(&mut self) { - self.image_quality = self.image_quality.toggle(); - } - pub fn tick(&mut self) { self.loader_state.calc_next(); } @@ -422,12 +416,6 @@ impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { DownloadPhase::SettingQuality => { Widget::render( List::new([ - Line::from(vec![ - "Choose image quality ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), - ]), - "Lower image quality is recommended for slow internet".into(), - state.image_quality.as_human_readable().into(), Line::from(vec![ "Start download: ".into(), "".to_span().style(*INSTRUCTIONS_STYLE), @@ -510,10 +498,7 @@ mod test { ); assert!(!download_all_chapters_state.process_started()); assert!(!download_all_chapters_state.is_downloading()); - assert_eq!( - PageType::default(), - download_all_chapters_state.image_quality - ); + assert_eq!(0.0, download_all_chapters_state.download_progress); download_all_chapters_state.ask_for_confirmation(); @@ -527,12 +512,7 @@ mod test { download_all_chapters_state.phase ); - download_all_chapters_state.toggle_image_quality(); - - assert_eq!( - PageType::default().toggle(), - download_all_chapters_state.image_quality - ); + download_all_chapters_state.start_fetch(); From bb1bde56b6ae73b6316266a38568dbcbda55fcdb Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Sun, 18 Aug 2024 20:49:41 -0500 Subject: [PATCH 11/14] feat(Download): added epub format for downloads --- Cargo.lock | 68 +++++++++++++++++- Cargo.toml | 1 + src/backend/download.rs | 151 +++++++++++++++++++++++++++++++++++++--- src/config.rs | 41 ++++++----- src/utils.rs | 12 ++++ src/view/pages/manga.rs | 17 +++-- 6 files changed, 255 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 311586a..7cb922e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -682,6 +682,23 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "epub-builder" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6fcc8fc7b93c7001e0d47c269aa5a30a78a1f44692dc09cc9d0f781378545e1" +dependencies = [ + "chrono", + "eyre", + "html-escape", + "log", + "once_cell", + "tempfile", + "upon", + "uuid", + "zip 0.6.6", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1001,6 +1018,15 @@ dependencies = [ "digest", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "1.1.0" @@ -1533,6 +1559,7 @@ dependencies = [ "color-eyre", "crossterm", "directories", + "epub-builder", "futures", "image", "once_cell", @@ -1550,7 +1577,7 @@ dependencies = [ "toml", "tui-input", "tui-widget-list", - "zip", + "zip 2.1.6", ] [[package]] @@ -2927,6 +2954,17 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "upon" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a9260fe394dfd8ab204a8eab40f88eb9a331bb852147d24fc0aff6b30daa02" +dependencies = [ + "serde", + "unicode-ident", + "unicode-width", +] + [[package]] name = "url" version = "2.5.1" @@ -2944,6 +2982,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2956,6 +3000,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + [[package]] name = "v_frame" version = "0.3.8" @@ -3396,6 +3449,19 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", + "time", +] + [[package]] name = "zip" version = "2.1.6" diff --git a/Cargo.toml b/Cargo.toml index 391f85d..629d938 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,3 +44,4 @@ rusqlite = { version = "0.31.0", features = ["bundled"] } clap = { version = "4.4.5", features = ["derive", "cargo"] } zip = "2.1.6" toml = "0.8.19" +epub-builder = "0.7.4" diff --git a/src/backend/download.rs b/src/backend/download.rs index a40563a..75fcaff 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -1,6 +1,7 @@ +use image::io::Reader; use manga_tui::exists; use std::fs::{create_dir, File}; -use std::io::Write; +use std::io::{BufRead, BufReader, Cursor, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; @@ -9,6 +10,7 @@ use zip::ZipWriter; use crate::common::PageType; use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; +use crate::utils::to_filename; use crate::view::pages::manga::MangaPageEvents; use super::error_log::{write_to_error_log, ErrorType}; @@ -66,8 +68,8 @@ pub fn download_chapter_raw_images( let chapter_dir = chapter_language_dir.join(format!( "Ch. {} {} {} {}", chapter.number, - chapter.title.trim().replace('/', "-"), - chapter.scanlator.trim().replace('/', "-"), + chapter.title.trim(), + chapter.scanlator.trim(), chapter.id_chapter, )); @@ -119,6 +121,128 @@ pub fn download_chapter_raw_images( Ok(()) } +pub fn download_chapter_epub( + is_downloading_all_chapters: bool, + chapter: DownloadChapter<'_>, + files: Vec, + endpoint: String, + tx: UnboundedSender, +) -> Result<(), std::io::Error> { + let (chapter_dir_language) = create_manga_directory(&chapter)?; + + let chapter_id = chapter.id_chapter.to_string(); + let chapter_name = format!( + "Ch. {} {} {} {}", + chapter.number, + chapter.title.trim(), + chapter.scanlator.trim(), + chapter.id_chapter, + ); + + tokio::spawn(async move { + let total_pages = files.len(); + + let mut epub_output = + File::create(chapter_dir_language.join(format!("{}.epub", chapter_name))).unwrap(); + + let mut epub = + epub_builder::EpubBuilder::new(epub_builder::ZipLibrary::new().unwrap()).unwrap(); + + epub.epub_version(epub_builder::EpubVersion::V30); + + epub.metadata("author", env!("CARGO_PKG_AUTHORS")); + epub.metadata("title", chapter_name); + + let image_style = r#" +div.centered_image { + width: 100%; + height : 100%; + margin: auto; +} +div.centered_image img { + width: 100%; + height : 100%; +} + "#; + + epub.stylesheet(image_style.as_bytes()).unwrap(); + + for (index, file_name) in files.into_iter().enumerate() { + let image_response = MangadexClient::global() + .get_chapter_page(&endpoint, &file_name) + .await; + + match image_response { + Ok(bytes) => { + let image_path = format!("data/{}", file_name); + + let file_name = Path::new(&file_name); + + let mime_type = + format!("image/{}", file_name.extension().unwrap().to_str().unwrap()); + + if index == 0 { + epub.add_cover_image(&image_path, bytes.as_ref(), &mime_type) + .unwrap(); + } + + epub.add_resource(&image_path, bytes.as_ref(), &mime_type) + .unwrap(); + + epub.add_content(epub_builder::EpubContent::new( + format!("{}.xhtml", index + 1), + format!( + r#" + + + + + + Panel + + + + + +
+ Panel +
+ + + + "#, + image_path + ) + .as_bytes(), + )) + .unwrap(); + + if !is_downloading_all_chapters { + tx.send(MangaPageEvents::SetDownloadProgress( + (index as f64) / (total_pages as f64), + chapter_id.to_string(), + )) + .ok(); + } + } + Err(e) => write_to_error_log(ErrorType::FromError(Box::new(e))), + } + } + + epub.generate(&mut epub_output).unwrap(); + + if is_downloading_all_chapters { + tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + } else { + tx.send(MangaPageEvents::ChapterFinishedDownloading(chapter_id)) + .ok(); + } + }); + + Ok(()) +} + pub fn download_chapter_cbz( is_downloading_all_chapters: bool, chapter: DownloadChapter<'_>, @@ -130,10 +254,10 @@ pub fn download_chapter_cbz( let chapter_id = chapter.id_chapter.to_string(); let chapter_name = format!( - "Ch. {} {} | {} | {}", + "Ch. {} {} {} {}", chapter.number, - chapter.title.trim().replace('/', "-"), - chapter.scanlator.trim().replace('/', "-"), + chapter.title.trim(), + chapter.scanlator.trim(), chapter.id_chapter, ); @@ -239,6 +363,7 @@ pub fn download_all_chapters( let pages_response = MangadexClient::global().get_chapter_pages(&id).await; let chapter_title = chapter.attributes.title.unwrap_or_default(); + let scanlator = scanlator.unwrap_or_default(); match pages_response { Ok(response) => { let (files, quality) = match config.image_quality { @@ -251,13 +376,17 @@ pub fn download_all_chapters( response.base_url, quality, response.chapter.hash ); + let manga_title = to_filename(&manga_title); + let chapter_title = to_filename(&chapter_title); + let scanlator = to_filename(&scanlator); + let chapter_to_download = DownloadChapter { id_chapter: &chapter.id, manga_id: &manga_id, manga_title: &manga_title, title: &chapter_title, number: &chapter_number, - scanlator: &scanlator.unwrap_or_default(), + scanlator: &scanlator, lang: &lang.as_human_readable(), }; @@ -276,7 +405,13 @@ pub fn download_all_chapters( endpoint, tx.clone(), ), - DownloadType::Pdf => Ok(()), + DownloadType::Epub => download_chapter_epub( + true, + chapter_to_download, + files, + endpoint, + tx.clone(), + ), }; if let Err(e) = download_proccess { diff --git a/src/config.rs b/src/config.rs index d2e0cc1..41c05ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::fs::File; use std::io::{Read, Write}; use std::path::Path; @@ -15,7 +16,7 @@ pub enum DownloadType { #[default] Cbz, Raw, - Pdf, + Epub, } #[derive(Default, Debug, Serialize, Deserialize, Display, EnumIter)] @@ -32,6 +33,7 @@ pub struct MangaTuiConfig { pub image_quality: ImageQuality, } + pub static CONFIG_FILE: &str = "manga-tui-config.toml"; pub static CONFIG: OnceCell = OnceCell::new(); @@ -56,31 +58,28 @@ impl MangaTuiConfig { #[allow(clippy::format_collect)] pub fn write_config(base_directory: &Path) -> Result<(), std::io::Error> { - let default_config = MangaTuiConfig::default(); - - let contents = r#" - - # The format of the manga downloaded - # values : cbz , raw, pdf - # default : cbz - download_type = "cbz" - - # Download image quality, low quality means images are compressed and is recommended for slow internet connections - # values : low, high - # default : low - image_quality = "low" - "#; - - let contents: String = contents - .lines() - .map(|line| format!("{} \n", line.trim())) - .collect(); - let config_file = base_directory .join(AppDirectories::Config.to_string()) .join(CONFIG_FILE); if !exists!(&config_file) { + let contents = r#" + # The format of the manga downloaded + # values : cbz , raw, epub + # default : cbz + download_type = "cbz" + + # Download image quality, low quality means images are compressed and is recommended for slow internet connections + # values : low, high + # default : low + image_quality = "low" + "#; + + let contents: String = contents + .trim() + .lines() + .map(|line| format!("{} \n", line.trim())) + .collect(); let mut config_file = File::create(config_file)?; config_file.write_all(contents.as_bytes())? } diff --git a/src/utils.rs b/src/utils.rs index 691bca3..bfbba0f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -225,3 +225,15 @@ pub fn render_search_bar( false => {} } } + +/// Remove special characteres that may cause errors +pub fn to_filename(title: &str) -> String { + let invalid_chars = ['\\', '/', ':', '*', '?', '"', '<', '>', '|']; + + let sanitized_title: String = title + .chars() + .map(|c| if invalid_chars.contains(&c) { '_' } else { c }) + .collect(); + + sanitized_title +} diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index 6c7fa29..fe89d09 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,8 +1,8 @@ use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; use crate::backend::download::{ - download_all_chapters, download_chapter_cbz, download_chapter_raw_images, DownloadAllChapters, - DownloadChapter, + download_all_chapters, download_chapter_cbz, download_chapter_epub, + download_chapter_raw_images, DownloadAllChapters, DownloadChapter, }; use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; @@ -12,7 +12,7 @@ use crate::backend::{AppDirectories, ChapterResponse, MangaStatisticsResponse, S use crate::common::{Manga, PageType}; use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; use crate::global::{ERROR_STYLE, INSTRUCTIONS_STYLE}; -use crate::utils::{set_status_style, set_tags_style}; +use crate::utils::{set_status_style, set_tags_style, to_filename}; use crate::view::tasks::manga::search_chapters_operation; use crate::view::widgets::manga::{ ChapterItem, ChaptersListWidget, DownloadAllChaptersState, DownloadAllChaptersWidget, @@ -768,11 +768,15 @@ impl MangaPage { "{}/{}/{}", response.base_url, quality, response.chapter.hash ); + let manga_title = to_filename(&manga_title); + let chapter_title = to_filename(&title); + let scanlator = to_filename(&scanlator); + let chapter = DownloadChapter { id_chapter: &chapter_id, manga_id: &manga_id, manga_title: &manga_title, - title: &title, + title: &chapter_title, number: &number, scanlator: &scanlator, lang: &lang, @@ -789,8 +793,11 @@ impl MangaPage { DownloadType::Cbz => { download_chapter_cbz(false, chapter, files, endpoint, tx.clone()) } - DownloadType::Pdf => Ok(()), + DownloadType::Epub => { + download_chapter_epub(false, chapter, files, endpoint, tx.clone()) + } }; + if let Err(e) = download_chapter_task { write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); tx.send(MangaPageEvents::DownloadError(chapter_id)).ok(); From d83bdd49afc0ac7894737772a13b8ed87153429d Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Mon, 19 Aug 2024 16:07:57 -0500 Subject: [PATCH 12/14] feat(MangaPage): add option to cancel download all chapters process --- src/backend/download.rs | 175 ++++---------------------------------- src/backend/fetch.rs | 2 +- src/view/pages/manga.rs | 144 +++++++++++++++---------------- src/view/tasks/manga.rs | 151 +++++++++++++++++++++++++++++++- src/view/widgets/manga.rs | 98 +++++++++++++-------- 5 files changed, 299 insertions(+), 271 deletions(-) diff --git a/src/backend/download.rs b/src/backend/download.rs index 75fcaff..b5c7ee3 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -22,7 +22,7 @@ pub struct DownloadChapter<'a> { pub id_chapter: &'a str, pub manga_id: &'a str, pub manga_title: &'a str, - pub title: &'a str, + pub chapter_title: &'a str, pub number: &'a str, pub scanlator: &'a str, pub lang: &'a str, @@ -68,7 +68,7 @@ pub fn download_chapter_raw_images( let chapter_dir = chapter_language_dir.join(format!( "Ch. {} {} {} {}", chapter.number, - chapter.title.trim(), + chapter.chapter_title.trim(), chapter.scanlator.trim(), chapter.id_chapter, )); @@ -134,7 +134,7 @@ pub fn download_chapter_epub( let chapter_name = format!( "Ch. {} {} {} {}", chapter.number, - chapter.title.trim(), + chapter.chapter_title.trim(), chapter.scanlator.trim(), chapter.id_chapter, ); @@ -150,22 +150,8 @@ pub fn download_chapter_epub( epub.epub_version(epub_builder::EpubVersion::V30); - epub.metadata("author", env!("CARGO_PKG_AUTHORS")); epub.metadata("title", chapter_name); - let image_style = r#" -div.centered_image { - width: 100%; - height : 100%; - margin: auto; -} -div.centered_image img { - width: 100%; - height : 100%; -} - "#; - - epub.stylesheet(image_style.as_bytes()).unwrap(); for (index, file_name) in files.into_iter().enumerate() { let image_response = MangadexClient::global() @@ -193,23 +179,19 @@ div.centered_image img { format!("{}.xhtml", index + 1), format!( r#" - - - - - - Panel - - - - - -
- Panel -
- - - + + + + + Panel + + + +
+ Panel +
+ + "#, image_path ) @@ -256,7 +238,7 @@ pub fn download_chapter_cbz( let chapter_name = format!( "Ch. {} {} {} {}", chapter.number, - chapter.title.trim(), + chapter.chapter_title.trim(), chapter.scanlator.trim(), chapter.id_chapter, ); @@ -325,126 +307,3 @@ pub struct DownloadAllChapters { pub manga_title: String, pub lang: Languages, } - -pub fn download_all_chapters( - chapter_data: ChapterResponse, - manga_details: DownloadAllChapters, - tx: UnboundedSender, -) { - let total_chapters = chapter_data.data.len(); - - let download_chapter_delay = if total_chapters <= 20 { - 1 - } else if (40..100).contains(&total_chapters) { - 3 - } else if (100..200).contains(&total_chapters) { - 6 - } else { - 8 - }; - let config = MangaTuiConfig::get(); - - for (index, chapter) in chapter_data.data.into_iter().enumerate() { - let id = chapter.id.clone(); - let chapter_number = chapter.attributes.chapter.unwrap_or_default(); - let manga_id = manga_details.manga_id.clone(); - let manga_title = manga_details.manga_title.clone(); - let lang = manga_details.lang; - - let tx = tx.clone(); - - let scanlator = chapter - .relationships - .iter() - .find(|rel| rel.type_field == "scanlation_group") - .map(|rel| rel.attributes.as_ref().unwrap().name.to_string()); - - tokio::spawn(async move { - let pages_response = MangadexClient::global().get_chapter_pages(&id).await; - - let chapter_title = chapter.attributes.title.unwrap_or_default(); - let scanlator = scanlator.unwrap_or_default(); - match pages_response { - Ok(response) => { - let (files, quality) = match config.image_quality { - ImageQuality::Low => (response.chapter.data_saver, PageType::LowQuality), - ImageQuality::High => (response.chapter.data, PageType::HighQuality), - }; - - let endpoint = format!( - "{}/{}/{}", - response.base_url, quality, response.chapter.hash - ); - - let manga_title = to_filename(&manga_title); - let chapter_title = to_filename(&chapter_title); - let scanlator = to_filename(&scanlator); - - let chapter_to_download = DownloadChapter { - id_chapter: &chapter.id, - manga_id: &manga_id, - manga_title: &manga_title, - title: &chapter_title, - number: &chapter_number, - scanlator: &scanlator, - lang: &lang.as_human_readable(), - }; - - let download_proccess = match config.download_type { - DownloadType::Cbz => download_chapter_cbz( - true, - chapter_to_download, - files, - endpoint, - tx.clone(), - ), - DownloadType::Raw => download_chapter_raw_images( - true, - chapter_to_download, - files, - endpoint, - tx.clone(), - ), - DownloadType::Epub => download_chapter_epub( - true, - chapter_to_download, - files, - endpoint, - tx.clone(), - ), - }; - - if let Err(e) = download_proccess { - let error_message = format!( - "Chapter: {} could not be downloaded, details: {}", - chapter_title, e - ); - - tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) - .ok(); - - write_to_error_log(ErrorType::FromError(Box::from(error_message))); - return; - } - - tx.send(MangaPageEvents::SaveChapterDownloadStatus( - chapter.id, - chapter_title, - )) - .ok(); - } - Err(e) => { - let error_message = format!( - "Chapter: {} could not be downloaded, details: {}", - chapter_title, e - ); - - tx.send(MangaPageEvents::SetDownloadAllChaptersProgress) - .ok(); - write_to_error_log(ErrorType::FromError(Box::from(error_message))); - } - } - }); - std::thread::sleep(Duration::from_secs(download_chapter_delay)); - } -} diff --git a/src/backend/fetch.rs b/src/backend/fetch.rs index a0090df..ea6694f 100644 --- a/src/backend/fetch.rs +++ b/src/backend/fetch.rs @@ -217,7 +217,7 @@ impl MangadexClient { ) -> Result { let language = language.as_iso_code(); - let order = "order[volume]=asc&order[chapter]=asc".to_string(); + let order = "order[volume]=asc&order[chapter]=asc"; let endpoint = format!( "{}/manga/{}/feed?limit=300&offset=0&{}&translatedLanguage[]={}&includes[]=scanlation_group&includeExternalUrl=0&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic", diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index fe89d09..ca67a81 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,10 +1,12 @@ +use std::time::Duration; + use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; use crate::backend::download::{ - download_all_chapters, download_chapter_cbz, download_chapter_epub, - download_chapter_raw_images, DownloadAllChapters, DownloadChapter, + download_chapter_cbz, download_chapter_epub, download_chapter_raw_images, DownloadAllChapters, + DownloadChapter, }; -use crate::backend::error_log::{self, write_to_error_log}; +use crate::backend::error_log::{self, write_to_error_log, ErrorType}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; use crate::backend::filter::Languages; use crate::backend::tui::Events; @@ -13,7 +15,9 @@ use crate::common::{Manga, PageType}; use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; use crate::global::{ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::{set_status_style, set_tags_style, to_filename}; -use crate::view::tasks::manga::search_chapters_operation; +use crate::view::tasks::manga::{ + download_all_chapters_task, search_chapters_operation, DownloadAllChaptersData, +}; use crate::view::widgets::manga::{ ChapterItem, ChaptersListWidget, DownloadAllChaptersState, DownloadAllChaptersWidget, DownloadPhase, @@ -46,10 +50,11 @@ pub enum PageState { #[derive(Debug, PartialEq, Eq)] pub enum MangaPageActions { DownloadChapter, - DownloadAllChapter, ConfirmDownloadAll, CancelDownloadAll, AskDownloadAllChapters, + AskAbortProcces, + AbortDownloadAllChapters, ScrollChapterDown, ScrollChapterUp, ToggleOrder, @@ -448,21 +453,34 @@ impl MangaPage { if self.download_process_started() { match key_event.code { KeyCode::Esc => { - self.local_action_tx - .send(MangaPageActions::CancelDownloadAll) - .ok(); + if self.is_downloading_all_chapters() { + self.local_action_tx + .send(MangaPageActions::AskAbortProcces) + .ok(); + } else if self.download_all_chapters_state.phase + == DownloadPhase::AskAbortProcess + { + self.download_all_chapters_state.continue_download(); + } else { + self.local_action_tx + .send(MangaPageActions::CancelDownloadAll) + .ok(); + } } KeyCode::Enter => { - self.local_action_tx - .send(MangaPageActions::ConfirmDownloadAll) - .ok(); - } - KeyCode::Char(' ') => { - self.local_action_tx - .send(MangaPageActions::DownloadAllChapter) - .ok(); + if self.download_all_chapters_state.phase == DownloadPhase::AskAbortProcess + { + self.local_action_tx + .send(MangaPageActions::AbortDownloadAllChapters) + .ok(); + } else { + self.local_action_tx + .send(MangaPageActions::ConfirmDownloadAll) + .ok(); + } } + _ => {} } } else { @@ -776,7 +794,7 @@ impl MangaPage { id_chapter: &chapter_id, manga_id: &manga_id, manga_title: &manga_title, - title: &chapter_title, + chapter_title: &chapter_title, number: &number, scanlator: &scanlator, lang: &lang, @@ -937,48 +955,23 @@ impl MangaPage { self.download_all_chapters_state.set_download_progress(); } - fn download_all_chapters(&mut self) { - self.download_all_chapters_state.start_fetch(); - let id = self.manga.id.clone(); - let manga_title = self.manga.title.clone(); - let lang = self.get_current_selected_language(); - let tx = self.local_event_tx.clone(); - #[cfg(not(test))] - tokio::spawn(async move { - let chapter_response = MangadexClient::global() - .get_all_chapters_for_manga(&id, lang) - .await; - match chapter_response { - Ok(response) => { - let total_chapters = response.data.len(); - tx.send(MangaPageEvents::StartDownloadProgress( - total_chapters as f64, - )) - .ok(); - download_all_chapters( - response, - DownloadAllChapters { - manga_title, - manga_id: id, - lang, - }, - tx, - ); - } - Err(e) => { - tx.send(MangaPageEvents::DownloadAllChaptersError).ok(); - write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); - } - } - }); - } - fn ask_download_all_chapters(&mut self) { self.download_all_chapters_state.ask_for_confirmation(); } - fn confirm_download_all(&mut self) { - self.download_all_chapters_state.confirm(); + fn confirm_download_all_chapters(&mut self) { + self.download_all_chapters_state.fetch_chapters_data(); + let manga_id = self.manga.id.clone(); + let manga_title = self.manga.title.clone(); + let lang = self.get_current_selected_language(); + let tx = self.local_event_tx.clone(); + self.tasks + .spawn(download_all_chapters_task(DownloadAllChaptersData { + tx, + manga_id, + manga_title, + lang, + })); } fn cancel_download_all_chapters(&mut self) { @@ -989,6 +982,7 @@ impl MangaPage { } fn start_download_all_chapters(&mut self, total_chapters: f64) { + self.download_all_chapters_state.start_download(); self.download_all_chapters_state .set_total_chapters(total_chapters); self.download_all_chapters_state.set_download_location( @@ -996,7 +990,6 @@ impl MangaPage { .into_path_buf() .join(&self.manga.title), ); - self.download_all_chapters_state.start_download(); } pub fn is_downloading_all_chapters(&self) -> bool { @@ -1004,13 +997,29 @@ impl MangaPage { } fn finish_download_all_chapters(&mut self) { - self.download_all_chapters_state.cancel(); + self.download_all_chapters_state.reset(); self.state = PageState::DisplayingChapters; self.local_event_tx .send(MangaPageEvents::CheckChapterStatus) .ok(); } + fn ask_abort_download_chapters(&mut self) { + self.download_all_chapters_state.ask_abort_proccess(); + } + + fn abort_download_all_chapters(&mut self) { + self.download_all_chapters_state.abort_proccess(); + self.tasks.abort_all(); + self.local_event_tx + .send(MangaPageEvents::CheckChapterStatus) + .ok(); + } + + fn continue_downloading_all_chapters(&mut self) { + self.download_all_chapters_state.continue_download(); + } + fn set_download_all_chapters_error(&mut self) { self.download_all_chapters_state.set_download_error(); } @@ -1123,11 +1132,12 @@ impl Component for MangaPage { } fn update(&mut self, action: Self::Actions) { match action { + MangaPageActions::AbortDownloadAllChapters => self.abort_download_all_chapters(), + MangaPageActions::AskAbortProcces => self.ask_abort_download_chapters(), MangaPageActions::SearchByLanguage => self.search_by_language(), - MangaPageActions::DownloadAllChapter => self.download_all_chapters(), MangaPageActions::CancelDownloadAll => self.cancel_download_all_chapters(), MangaPageActions::AskDownloadAllChapters => self.ask_download_all_chapters(), - MangaPageActions::ConfirmDownloadAll => self.confirm_download_all(), + MangaPageActions::ConfirmDownloadAll => self.confirm_download_all_chapters(), MangaPageActions::SearchPreviousChapterPage => self.search_previous_chapters(), MangaPageActions::SearchNextChapterPage => self.search_next_chapters(), MangaPageActions::ScrollDownAvailbleLanguages => self.scroll_language_down(), @@ -1308,13 +1318,7 @@ mod test { assert_eq!(MangaPageActions::ConfirmDownloadAll, action); - manga_page.confirm_download_all(); - - // download all chapters - press_key(&mut manga_page, KeyCode::Char(' ')); - let action = manga_page.local_action_rx.recv().await.unwrap(); - - assert_eq!(MangaPageActions::DownloadAllChapter, action); + manga_page.confirm_download_all_chapters(); // cancel download all chapters operation press_key(&mut manga_page, KeyCode::Esc); @@ -1484,14 +1488,6 @@ mod test { let action = MangaPageActions::ConfirmDownloadAll; manga_page.update(action); - assert_eq!( - DownloadPhase::SettingQuality, - manga_page.download_all_chapters_state.phase - ); - - let action = MangaPageActions::DownloadAllChapter; - manga_page.update(action); - assert_eq!( DownloadPhase::FetchingChaptersData, manga_page.download_all_chapters_state.phase diff --git a/src/view/tasks/manga.rs b/src/view/tasks/manga.rs index 621c051..98374c7 100644 --- a/src/view/tasks/manga.rs +++ b/src/view/tasks/manga.rs @@ -1,8 +1,16 @@ +use std::time::Duration; + use tokio::sync::mpsc::UnboundedSender; -use crate::backend::error_log::{write_to_error_log, ErrorType}; +use crate::backend::download::{ + download_chapter_cbz, download_chapter_epub, download_chapter_raw_images, DownloadChapter, +}; +use crate::backend::error_log::{self, write_to_error_log, ErrorType}; use crate::backend::fetch::MangadexClient; use crate::backend::filter::Languages; +use crate::common::PageType; +use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; +use crate::utils::to_filename; use crate::view::pages::manga::{ChapterOrder, MangaPageEvents}; #[cfg(not(test))] @@ -39,3 +47,144 @@ pub async fn search_chapters_operation( ) { tx.send(MangaPageEvents::LoadChapters(None)); } + +pub struct DownloadAllChaptersData { + pub tx: UnboundedSender, + pub manga_id: String, + pub manga_title: String, + pub lang: Languages, +} + +pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { + let chapter_response = MangadexClient::global() + .get_all_chapters_for_manga(&data.manga_id, data.lang) + .await; + match chapter_response { + Ok(response) => { + let total_chapters = response.data.len(); + data.tx + .send(MangaPageEvents::StartDownloadProgress( + total_chapters as f64, + )) + .ok(); + + let download_chapter_delay = if total_chapters <= 20 { + 1 + } else if (40..100).contains(&total_chapters) { + 3 + } else if (100..200).contains(&total_chapters) { + 6 + } else { + 8 + }; + + let config = MangaTuiConfig::get(); + + for (index, chapter_found) in response.data.into_iter().enumerate() { + let chapter_id = chapter_found.id; + let task = tokio::spawn(async move {}); + + let pages_response = MangadexClient::global() + .get_chapter_pages(&chapter_id) + .await; + + let chapter_number = chapter_found.attributes.chapter.unwrap_or_default(); + + let scanlator = chapter_found + .relationships + .iter() + .find(|rel| rel.type_field == "scanlation_group") + .map(|rel| rel.attributes.as_ref().unwrap().name.to_string()); + + let chapter_title = chapter_found.attributes.title.unwrap_or_default(); + let scanlator = scanlator.unwrap_or_default(); + + match pages_response { + Ok(res) => { + let (files, quality) = match config.image_quality { + ImageQuality::Low => (res.chapter.data_saver, PageType::LowQuality), + ImageQuality::High => (res.chapter.data, PageType::HighQuality), + }; + + let endpoint = format!("{}/{}/{}", res.base_url, quality, res.chapter.hash); + + let manga_title = to_filename(&data.manga_title); + let chapter_title = to_filename(&chapter_title); + let scanlator = to_filename(&scanlator); + + let chapter_to_download = DownloadChapter { + id_chapter: &chapter_id, + manga_id: &data.manga_id, + manga_title: &manga_title, + chapter_title: &chapter_title, + number: &chapter_number, + scanlator: &scanlator, + lang: &data.lang.as_human_readable(), + }; + + let download_proccess = match config.download_type { + DownloadType::Cbz => download_chapter_cbz( + true, + chapter_to_download, + files, + endpoint, + data.tx.clone(), + ), + DownloadType::Raw => download_chapter_raw_images( + true, + chapter_to_download, + files, + endpoint, + data.tx.clone(), + ), + DownloadType::Epub => download_chapter_epub( + true, + chapter_to_download, + files, + endpoint, + data.tx.clone(), + ), + }; + + if let Err(e) = download_proccess { + let error_message = format!( + "Chapter: {} could not be downloaded, details: {}", + chapter_title, e + ); + + data.tx + .send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + + write_to_error_log(ErrorType::FromError(Box::from(error_message))); + return; + } + + data.tx + .send(MangaPageEvents::SaveChapterDownloadStatus( + chapter_id, + chapter_title, + )) + .ok(); + } + Err(e) => { + let error_message = format!( + "Chapter: {} could not be downloaded, details: {}", + chapter_title, e + ); + + data.tx + .send(MangaPageEvents::SetDownloadAllChaptersProgress) + .ok(); + write_to_error_log(ErrorType::FromError(Box::from(error_message))); + } + } + std::thread::sleep(Duration::from_secs(download_chapter_delay)); + } + } + Err(e) => { + data.tx.send(MangaPageEvents::DownloadAllChaptersError).ok(); + write_to_error_log(error_log::ErrorType::FromError(Box::new(e))); + } + } +} diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index 9f48796..0f8eb26 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -252,9 +252,9 @@ pub enum DownloadPhase { #[default] ProccessNotStarted, Asking, - SettingQuality, FetchingChaptersData, DownloadingChapters, + AskAbortProcess, ErrorChaptersData, } @@ -290,8 +290,7 @@ impl DownloadAllChaptersState { /// Either phase can start download pub fn is_ready_to_fetch_data(&self) -> bool { - self.phase == DownloadPhase::SettingQuality - || self.phase == DownloadPhase::ErrorChaptersData + self.phase == DownloadPhase::Asking || self.phase == DownloadPhase::ErrorChaptersData } pub fn set_download_progress(&mut self) { @@ -304,28 +303,50 @@ impl DownloadAllChaptersState { } } - pub fn confirm(&mut self) { + pub fn fetch_chapters_data(&mut self) { if !self.is_downloading() { - self.phase = DownloadPhase::SettingQuality; + self.phase = DownloadPhase::FetchingChaptersData; } } - pub fn start_fetch(&mut self) { - if self.is_ready_to_fetch_data() { + pub fn start_download(&mut self) { + if !self.is_downloading() { + self.phase = DownloadPhase::DownloadingChapters; self.total_chapters = 0.0; self.download_progress = 0.0; - self.phase = DownloadPhase::FetchingChaptersData; } } - pub fn start_download(&mut self) { + pub fn cancel(&mut self) { if !self.is_downloading() { - self.phase = DownloadPhase::DownloadingChapters; + self.phase = DownloadPhase::ProccessNotStarted; } } - pub fn cancel(&mut self) { - self.phase = DownloadPhase::ProccessNotStarted; + pub fn reset(&mut self) { + if self.is_downloading() || self.phase == DownloadPhase::AskAbortProcess { + self.phase = DownloadPhase::ProccessNotStarted; + self.total_chapters = 0.0; + self.download_progress = 0.0; + } + } + + pub fn ask_abort_proccess(&mut self) { + if self.is_downloading() { + self.phase = DownloadPhase::AskAbortProcess; + } + } + + pub fn abort_proccess(&mut self) { + if self.is_downloading() || self.phase == DownloadPhase::AskAbortProcess { + self.reset(); + } + } + + pub fn continue_download(&mut self) { + if self.phase == DownloadPhase::AskAbortProcess { + self.phase = DownloadPhase::DownloadingChapters; + } } pub fn set_total_chapters(&mut self, total_chapters: f64) { @@ -371,12 +392,15 @@ impl<'a> DownloadAllChaptersWidget<'a> { let download_location = format!( "Download location : {}", - state.download_location.to_str().unwrap(), + state.download_location.as_path().display(), ); Paragraph::new(Line::from(vec![ "Downloading all chapters, this will take a while, ".into(), download_location.into(), + " ".into(), + "Cancel download: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), ])) .wrap(Wrap { trim: true }) .render(information_area, buf); @@ -413,17 +437,15 @@ impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { Paragraph::new(Line::from(instructions)).render(download_information_area, buf); } - DownloadPhase::SettingQuality => { - Widget::render( - List::new([ - Line::from(vec![ - "Start download: ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), - ]), - ]), - download_information_area, - buf, - ); + DownloadPhase::AskAbortProcess => { + let instructions = vec![ + "Are you sure you want to cancel? yes: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + " no: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]; + + Paragraph::new(Line::from(instructions)).render(download_information_area, buf); } DownloadPhase::FetchingChaptersData => { let loader = Throbber::default() @@ -498,29 +520,38 @@ mod test { ); assert!(!download_all_chapters_state.process_started()); assert!(!download_all_chapters_state.is_downloading()); - + assert_eq!(0.0, download_all_chapters_state.download_progress); download_all_chapters_state.ask_for_confirmation(); assert_eq!(DownloadPhase::Asking, download_all_chapters_state.phase); - download_all_chapters_state.confirm(); + // The user cancelled + download_all_chapters_state.cancel(); assert_eq!( - DownloadPhase::SettingQuality, + DownloadPhase::ProccessNotStarted, download_all_chapters_state.phase ); - + download_all_chapters_state.ask_for_confirmation(); - download_all_chapters_state.start_fetch(); + // The user confirmed + download_all_chapters_state.fetch_chapters_data(); assert_eq!( DownloadPhase::FetchingChaptersData, download_all_chapters_state.phase ); + download_all_chapters_state.start_download(); + + assert_eq!( + DownloadPhase::DownloadingChapters, + download_all_chapters_state.phase + ); + download_all_chapters_state.set_download_error(); assert_eq!( @@ -528,7 +559,7 @@ mod test { download_all_chapters_state.phase ); - download_all_chapters_state.start_fetch(); + download_all_chapters_state.fetch_chapters_data(); assert_eq!( DownloadPhase::FetchingChaptersData, @@ -565,12 +596,5 @@ mod test { MangaPageEvents::FinishedDownloadingAllChapters, download_finished ); - - download_all_chapters_state.cancel(); - - assert_eq!( - DownloadPhase::ProccessNotStarted, - download_all_chapters_state.phase - ); } } From 99c95642af045e4eae045bfa253d58893e23d2af Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Tue, 20 Aug 2024 12:26:28 -0500 Subject: [PATCH 13/14] chore(docs, refactor) update readme and refactor --- README.md | 25 +++++++++++++++--- src/backend/download.rs | 38 +++++++++------------------ src/backend/fetch.rs | 13 ++++++--- src/common.rs | 2 -- src/config.rs | 5 +--- src/main.rs | 1 - src/view/pages/manga.rs | 20 +++++--------- src/view/pages/reader.rs | 1 - src/view/tasks/manga.rs | 55 +++++++++++++++++++++++++-------------- src/view/widgets/manga.rs | 22 +++++++++------- 10 files changed, 98 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 91e0efb..efe6df6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@
crates io downloads + + downloads + License @@ -48,9 +51,13 @@ https://github.com/user-attachments/assets/70f321ff-13d1-4c4b-9c37-604271456ab2 https://github.com/user-attachments/assets/47e88e89-f73c-4575-9645-2abb80ca7d63 -- Download manga +- Download manga + +https://github.com/user-attachments/assets/ba785668-7cf1-4367-93f9-6e6e1f72c12c + +- Download all chapters of a manga -https://github.com/user-attachments/assets/64880a98-74c8-4656-8cf8-2c1daf5375d2 +https://github.com/user-attachments/assets/26ad493f-633c-41fc-9d09-49b316118923 ## Installation @@ -75,10 +82,14 @@ Download a binary from the [releases page](https://github.com/josueBarretogit/ma ## Image rendering -Use a terminal that can render images such as Wezterm (Personally I recommend using this one It's the one used in the videos), iTerm2 or Kitty,
+Use a terminal that can render images such as Wezterm (Personally I recommend using this one It's the one used in the videos), iTerm2
For more information see : [image-support](https://github.com/benjajaja/ratatui-image?tab=readme-ov-file#compatibility-matrix) +> [!NOTE] +> There is an issue with kitty terminal, see [#12](https://github.com/josueBarretogit/manga-tui/issues/12) + + No images will be displayed if the terminal does not have image support (but `manga-tui` will still work as a manga downloader) ## Usage @@ -102,8 +113,9 @@ manga-tui -d On linux it will output something like: `~/.local/share/manga-tui`
-On the `manga-tui` directory there will be 3 directories +On the `manga-tui` directory there will be 4 directories - `history`, which contains a sqlite database to store reading history +- `config`, which contains a TOML file with extra configuration - `mangaDownloads`, where manga will be downloaded - `errorLogs`, for storing posible errors / bugs @@ -116,6 +128,11 @@ export MANGA_TUI_DATA_DIR="/home/user/Desktop/mangas" ## Configuration +Go to the TOML file located at `config`, there you can change download format and image quality to know where it is run: +```shell +manga-tui --data-dir +``` + By default `manga-tui` will search mangas in english, you can change the language by running: diff --git a/src/backend/download.rs b/src/backend/download.rs index b5c7ee3..ee71c5c 100644 --- a/src/backend/download.rs +++ b/src/backend/download.rs @@ -1,22 +1,14 @@ -use image::io::Reader; +use crate::view::pages::manga::MangaPageEvents; use manga_tui::exists; use std::fs::{create_dir, File}; -use std::io::{BufRead, BufReader, Cursor, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; -use zip::write::{FileOptions, SimpleFileOptions}; +use zip::write::SimpleFileOptions; use zip::ZipWriter; - -use crate::common::PageType; -use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; -use crate::utils::to_filename; -use crate::view::pages::manga::MangaPageEvents; - use super::error_log::{write_to_error_log, ErrorType}; use super::fetch::MangadexClient; -use super::filter::Languages; -use super::{ChapterPagesResponse, ChapterResponse, APP_DATA_DIR}; +use super::APP_DATA_DIR; pub struct DownloadChapter<'a> { pub id_chapter: &'a str, @@ -28,9 +20,8 @@ pub struct DownloadChapter<'a> { pub lang: &'a str, } -fn create_manga_directory(chapter: &DownloadChapter<'_>) -> Result<(PathBuf), std::io::Error> { +fn create_manga_directory(chapter: &DownloadChapter<'_>) -> Result { // need directory with the manga's title, and its id to make it unique - let chapter_id = chapter.id_chapter.to_string(); let dir_manga_downloads = APP_DATA_DIR.as_ref().unwrap().join("mangaDownloads"); @@ -128,7 +119,7 @@ pub fn download_chapter_epub( endpoint: String, tx: UnboundedSender, ) -> Result<(), std::io::Error> { - let (chapter_dir_language) = create_manga_directory(&chapter)?; + let chapter_dir_language = create_manga_directory(&chapter)?; let chapter_id = chapter.id_chapter.to_string(); let chapter_name = format!( @@ -150,8 +141,7 @@ pub fn download_chapter_epub( epub.epub_version(epub_builder::EpubVersion::V30); - epub.metadata("title", chapter_name); - + let _ = epub.metadata("title", chapter_name); for (index, file_name) in files.into_iter().enumerate() { let image_response = MangadexClient::global() @@ -232,7 +222,7 @@ pub fn download_chapter_cbz( endpoint: String, tx: UnboundedSender, ) -> Result<(), std::io::Error> { - let (chapter_dir_language) = create_manga_directory(&chapter)?; + let chapter_dir_language = create_manga_directory(&chapter)?; let chapter_id = chapter.id_chapter.to_string(); let chapter_name = format!( @@ -270,11 +260,13 @@ pub fn download_chapter_cbz( file_name.extension().unwrap().to_str().unwrap() ); - zip.start_file( + let _ = zip.start_file( chapter_dir_language.join(image_name).to_str().unwrap(), options, ); - zip.write_all(&bytes).unwrap(); + + let _ = zip.write_all(&bytes); + if !is_downloading_all_chapters { tx.send(MangaPageEvents::SetDownloadProgress( @@ -301,9 +293,3 @@ pub fn download_chapter_cbz( Ok(()) } -#[derive(Default)] -pub struct DownloadAllChapters { - pub manga_id: String, - pub manga_title: String, - pub lang: Languages, -} diff --git a/src/backend/fetch.rs b/src/backend/fetch.rs index ea6694f..5e55191 100644 --- a/src/backend/fetch.rs +++ b/src/backend/fetch.rs @@ -1,13 +1,12 @@ -use std::time::Duration as StdDuration; - use super::filter::Languages; use super::{ChapterPagesResponse, ChapterResponse, MangaStatisticsResponse, SearchMangaResponse}; use crate::backend::filter::{Filters, IntoParam}; use crate::view::pages::manga::ChapterOrder; use bytes::Bytes; -use chrono::{Duration, Months}; +use chrono::Months; use once_cell::sync::OnceCell; use reqwest::StatusCode; +use std::time::Duration as StdDuration; #[derive(Clone, Debug)] pub struct MangadexClient { @@ -224,6 +223,12 @@ impl MangadexClient { API_URL_BASE, id, order, language ); - self.client.get(endpoint).send().await?.json().await + self.client + .get(endpoint) + .timeout(StdDuration::from_secs(10)) + .send() + .await? + .json() + .await } } diff --git a/src/common.rs b/src/common.rs index 3ab562f..cc818be 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,4 @@ -use ratatui::widgets::ListItem; use strum::{Display, EnumIter}; - use crate::backend::filter::Languages; #[derive(Default, Clone, Debug)] diff --git a/src/config.rs b/src/config.rs index 41c05ee..98ada2a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,10 @@ -use std::fmt::Display; use std::fs::File; use std::io::{Read, Write}; use std::path::Path; - use manga_tui::exists; -use once_cell::sync::{Lazy, OnceCell}; +use once_cell::sync::{ OnceCell}; use serde::{Deserialize, Serialize}; use strum::{Display, EnumIter}; - use crate::backend::AppDirectories; #[derive(Default, Debug, Serialize, Deserialize, Display, EnumIter)] diff --git a/src/main.rs b/src/main.rs index d7954fa..c79da34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ #![forbid(unsafe_code)] #![allow(dead_code)] -#![allow(unused)] use std::time::Duration; use self::backend::error_log::init_error_hooks; diff --git a/src/view/pages/manga.rs b/src/view/pages/manga.rs index ca67a81..1f53f50 100644 --- a/src/view/pages/manga.rs +++ b/src/view/pages/manga.rs @@ -1,12 +1,10 @@ -use std::time::Duration; - use crate::backend::database::{get_chapters_history_status, save_history, SetChapterDownloaded}; use crate::backend::database::{set_chapter_downloaded, MangaReadingHistorySave}; use crate::backend::download::{ - download_chapter_cbz, download_chapter_epub, download_chapter_raw_images, DownloadAllChapters, + download_chapter_cbz, download_chapter_epub, download_chapter_raw_images, DownloadChapter, }; -use crate::backend::error_log::{self, write_to_error_log, ErrorType}; +use crate::backend::error_log::{self, write_to_error_log}; use crate::backend::fetch::{MangadexClient, ITEMS_PER_PAGE_CHAPTERS}; use crate::backend::filter::Languages; use crate::backend::tui::Events; @@ -25,7 +23,7 @@ use crate::view::widgets::manga::{ use crate::view::widgets::Component; use crate::PICKER; use crossterm::event::{ - KeyCode, KeyEvent, KeyModifiers, ModifierKeyCode, MouseEvent, MouseEventKind, + KeyCode, KeyEvent, MouseEvent, MouseEventKind, }; use ratatui::{prelude::*, widgets::*}; use ratatui_image::protocol::StatefulProtocol; @@ -36,7 +34,6 @@ use tokio::task::JoinSet; use self::text::ToSpan; -use super::reader::MangaReaderEvents; #[derive(PartialEq, Eq, Debug)] pub enum PageState { @@ -453,7 +450,7 @@ impl MangaPage { if self.download_process_started() { match key_event.code { KeyCode::Esc => { - if self.is_downloading_all_chapters() { + if self.download_all_chapters_state.phase == DownloadPhase::DownloadingChapters { self.local_action_tx .send(MangaPageActions::AskAbortProcces) .ok(); @@ -1016,9 +1013,6 @@ impl MangaPage { .ok(); } - fn continue_downloading_all_chapters(&mut self) { - self.download_all_chapters_state.continue_download(); - } fn set_download_all_chapters_error(&mut self) { self.download_all_chapters_state.set_download_error(); @@ -1181,14 +1175,14 @@ impl Component for MangaPage { #[cfg(test)] mod test { - use crate::backend::{ChapterData, Data}; + use crate::backend::{ChapterData}; use crate::view::widgets::press_key; use super::*; fn get_manga_page() -> MangaPage { let manga = Manga::default(); - let (tx, mut rx) = mpsc::unbounded_channel::(); + let (tx, _) = mpsc::unbounded_channel::(); MangaPage::new(manga, None, tx) } @@ -1418,7 +1412,7 @@ mod test { let action = MangaPageActions::ToggleOrder; manga_page.update(action); - /// when searching chapters avoid triggering another search by toggling order + // when searching chapters avoid triggering another search by toggling order assert_eq!(ChapterOrder::default(), manga_page.chapter_order); manga_page.state = PageState::DisplayingChapters; diff --git a/src/view/pages/reader.rs b/src/view/pages/reader.rs index 2c0804d..766f272 100644 --- a/src/view/pages/reader.rs +++ b/src/view/pages/reader.rs @@ -12,7 +12,6 @@ use image::GenericImageView; use ratatui::{prelude::*, widgets::*}; use ratatui_image::protocol::StatefulProtocol; use ratatui_image::{Resize, StatefulImage}; -use strum::Display; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::task::JoinSet; diff --git a/src/view/tasks/manga.rs b/src/view/tasks/manga.rs index 98374c7..ee1b1cc 100644 --- a/src/view/tasks/manga.rs +++ b/src/view/tasks/manga.rs @@ -1,16 +1,5 @@ -use std::time::Duration; - use tokio::sync::mpsc::UnboundedSender; - -use crate::backend::download::{ - download_chapter_cbz, download_chapter_epub, download_chapter_raw_images, DownloadChapter, -}; -use crate::backend::error_log::{self, write_to_error_log, ErrorType}; -use crate::backend::fetch::MangadexClient; use crate::backend::filter::Languages; -use crate::common::PageType; -use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; -use crate::utils::to_filename; use crate::view::pages::manga::{ChapterOrder, MangaPageEvents}; #[cfg(not(test))] @@ -21,6 +10,9 @@ pub async fn search_chapters_operation( chapter_order: ChapterOrder, tx: UnboundedSender, ) { + use crate::backend::error_log::{write_to_error_log, ErrorType}; + use crate::backend::fetch::MangadexClient; + let response = MangadexClient::global() .get_manga_chapters(manga_id, page, language, chapter_order) .await; @@ -39,13 +31,13 @@ pub async fn search_chapters_operation( #[cfg(test)] pub async fn search_chapters_operation( - manga_id: String, - page: u32, - language: Languages, - chapter_order: ChapterOrder, + _manga_id: String, + _page: u32, + _language: Languages, + _chapter_order: ChapterOrder, tx: UnboundedSender, ) { - tx.send(MangaPageEvents::LoadChapters(None)); + tx.send(MangaPageEvents::LoadChapters(None)).ok(); } pub struct DownloadAllChaptersData { @@ -55,7 +47,20 @@ pub struct DownloadAllChaptersData { pub lang: Languages, } +#[cfg(not(test))] pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { + use std::time::Instant; + use std::time::Duration; + use crate::backend::download::{download_chapter_cbz, download_chapter_epub, download_chapter_raw_images, DownloadChapter}; + use crate::backend::error_log::ErrorType; + use crate::backend::error_log::{self, write_to_error_log}; + use crate::backend::fetch::MangadexClient; + use crate::common::PageType; + use crate::config::{DownloadType, ImageQuality, MangaTuiConfig}; + use crate::utils::to_filename; + + + let chapter_response = MangadexClient::global() .get_all_chapters_for_manga(&data.manga_id, data.lang) .await; @@ -68,7 +73,7 @@ pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { )) .ok(); - let download_chapter_delay = if total_chapters <= 20 { + let download_chapter_delay = if total_chapters < 40 { 1 } else if (40..100).contains(&total_chapters) { 3 @@ -80,9 +85,10 @@ pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { let config = MangaTuiConfig::get(); - for (index, chapter_found) in response.data.into_iter().enumerate() { + for chapter_found in response.data.into_iter() { let chapter_id = chapter_found.id; - let task = tokio::spawn(async move {}); + + let start_fetch_time = Instant::now(); let pages_response = MangadexClient::global() .get_chapter_pages(&chapter_id) @@ -179,7 +185,9 @@ pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { write_to_error_log(ErrorType::FromError(Box::from(error_message))); } } - std::thread::sleep(Duration::from_secs(download_chapter_delay)); + + let time_since = start_fetch_time.elapsed(); + std::thread::sleep(Duration::from_secs(download_chapter_delay).saturating_sub(time_since)); } } Err(e) => { @@ -188,3 +196,10 @@ pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { } } } + +#[cfg(test)] +pub async fn download_all_chapters_task(data: DownloadAllChaptersData) { + data.tx + .send(MangaPageEvents::StartDownloadProgress(10.0)) + .ok(); +} diff --git a/src/view/widgets/manga.rs b/src/view/widgets/manga.rs index 0f8eb26..93ebed1 100644 --- a/src/view/widgets/manga.rs +++ b/src/view/widgets/manga.rs @@ -1,6 +1,5 @@ use crate::backend::filter::Languages; -use crate::backend::{AppDirectories, ChapterResponse}; -use crate::common::PageType; +use crate::backend::{ChapterResponse}; use crate::global::{CURRENT_LIST_ITEM_STYLE, ERROR_STYLE, INSTRUCTIONS_STYLE}; use crate::utils::display_dates_since_publication; use crate::view::pages::manga::MangaPageEvents; @@ -281,7 +280,7 @@ impl DownloadAllChaptersState { } pub fn is_downloading(&self) -> bool { - self.phase == DownloadPhase::DownloadingChapters + self.phase == DownloadPhase::DownloadingChapters || self.phase == DownloadPhase::AskAbortProcess } pub fn process_started(&self) -> bool { @@ -399,8 +398,6 @@ impl<'a> DownloadAllChaptersWidget<'a> { "Downloading all chapters, this will take a while, ".into(), download_location.into(), " ".into(), - "Cancel download: ".into(), - "".to_span().style(*INSTRUCTIONS_STYLE), ])) .wrap(Wrap { trim: true }) .render(information_area, buf); @@ -484,11 +481,18 @@ impl<'a> StatefulWidget for DownloadAllChaptersWidget<'a> { self.render_download_information(information_area, buf, state); - LineGauge::default() - .block(Block::bordered().title(format!( - "Total chapters: {}, chapters downloaded : {}", + let download_progress_title = vec![ + format!( + "Total chapters: {}, chapters downloaded : {} ", state.total_chapters, state.download_progress - ))) + ) + .into(), + "Cancel download: ".into(), + "".to_span().style(*INSTRUCTIONS_STYLE), + ]; + + LineGauge::default() + .block(Block::bordered().title(download_progress_title)) .filled_style( Style::default() .fg(Color::Blue) From cbacd7e86440e44299b88350160bc782a139dd71 Mon Sep 17 00:00:00 2001 From: josuebarretogit Date: Tue, 20 Aug 2024 13:45:33 -0500 Subject: [PATCH 14/14] bump version to v0.3.0, add release workflow --- .github/workflows/release.yml | 63 +++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- README.md | 16 ++++----- 3 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d57e004 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release + +on: + push: + tags: + - 'v*' + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + targets: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-14 + - target: aarch64-apple-darwin + os: macos-14 + runs-on: ${{ matrix.targets.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup + run: rustup target add ${{ matrix.targets.target }} + - name: Build + run: cargo build --release --target ${{ matrix.targets.target }} + - name: Set release version + run: echo "RELEASE_VERSION=${GITHUB_REF_NAME#v}" >> ${GITHUB_ENV} + - name: Archive + run: tar -czf manga-tui-${{ env.RELEASE_VERSION }}-${{ matrix.targets.target }}.tar.gz -C target/${{ matrix.targets.target }}/release manga-tui + - name: Checksum + run: shasum -a 256 manga-tui-${{ env.RELEASE_VERSION }}-${{ matrix.targets.target }}.tar.gz + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.targets.target }} + path: manga-tui-${{ env.RELEASE_VERSION }}-${{ matrix.targets.target }}.tar.gz + if-no-files-found: error + release: + permissions: + contents: write + runs-on: ubuntu-latest + needs: build + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + path: releases + pattern: release-* + merge-multiple: true + - name: Checksum + run: sha256sum releases/* > ./releases/checksum.txt + - name: Create Draft Release + uses: softprops/action-gh-release@v2.0.4 + with: + draft: true + generate_release_notes: true + make_latest: true + files: | + releases/* diff --git a/Cargo.toml b/Cargo.toml index 629d938..549ff4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "manga-tui" -version = "0.2.0" +version = "0.3.0" edition = "2021" authors = ["Josue "] readme = "README.md" diff --git a/README.md b/README.md index efe6df6..753531a 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,16 @@ No images will be displayed if the terminal does not have image support (but `ma ## Usage -After installation run the binary +After installation just run the binary ```shell manga-tui ``` + +## Configuration + + Manga downloads and reading history is stored in the `manga-tui` directory, to know where it is run: @@ -115,7 +119,7 @@ On linux it will output something like: `~/.local/share/manga-tui`
On the `manga-tui` directory there will be 4 directories - `history`, which contains a sqlite database to store reading history -- `config`, which contains a TOML file with extra configuration +- `config`, which contains a TOML file with extra configuration (download format and download quality) - `mangaDownloads`, where manga will be downloaded - `errorLogs`, for storing posible errors / bugs @@ -125,14 +129,6 @@ If you want to change the location you can set the environment variable `MANGA_T export MANGA_TUI_DATA_DIR="/home/user/Desktop/mangas" ``` - -## Configuration - -Go to the TOML file located at `config`, there you can change download format and image quality to know where it is run: -```shell -manga-tui --data-dir -``` - By default `manga-tui` will search mangas in english, you can change the language by running: