Skip to content

Commit

Permalink
feat(ReaderPage): Save reading history from reader page
Browse files Browse the repository at this point in the history
added methods to go to next and previous chapter from within the reader page
  • Loading branch information
josueBarretogit committed Oct 8, 2024
1 parent 7d079b3 commit 7c8e33b
Show file tree
Hide file tree
Showing 9 changed files with 1,029 additions and 307 deletions.
131 changes: 13 additions & 118 deletions src/backend/api_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,6 @@ pub struct AggregateChapterResponse {
pub volumes: HashMap<String, Volumes>,
}

impl AggregateChapterResponse {
pub fn search_chapter(&self, volume: &str, chapter_number: &str) -> Option<Chapters> {
let chapter = self.volumes.get(volume).and_then(|volum| volum.chapters.get(chapter_number)).cloned();
chapter
}

pub fn search_chapter_in_next_volume(&self, volume: &str, chapter_number: &str) -> Option<Chapters> {
let volumen_as_number: u32 = volume.parse().ok()?;
let volume_incremented = volumen_as_number + 1;

self.search_chapter(volume, chapter_number)
.map_or(self.search_chapter(&volume_incremented.to_string(), chapter_number), Some)
}
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Volumes {
Expand Down Expand Up @@ -335,6 +320,19 @@ pub mod authors {
}
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OneChapterResponse {
pub data: OneChapterData,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OneChapterData {
pub id: String,
pub attributes: ChapterAttribute,
}

#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
Expand Down Expand Up @@ -388,107 +386,4 @@ mod tests {
let image_quality = ImageQuality::Low;
assert_eq!(format!("http://some_url/data-saver/{}", response.chapter.hash), response.get_image_url_endpoint(image_quality));
}

#[test]
fn it_searches_chapter_given_a_chapter_number() {
let chapter_to_search = Chapters {
chapter: "2".to_string(),
id: "some_id_other".to_string(),
..Default::default()
};

let chapters_vol_1: HashMap<String, Chapters> = HashMap::from([
("1".to_string(), Chapters {
chapter: "1".to_string(),
id: "some_id".to_string(),
..Default::default()
}),
("2".to_string(), chapter_to_search.clone()),
]);

let chapters_vol_2: HashMap<String, Chapters> = HashMap::from([("3".to_string(), Chapters {
chapter: "3".to_string(),
id: "chapter_3".to_string(),
..Default::default()
})]);

let volumes: HashMap<String, Volumes> = HashMap::from([
("1".to_string(), Volumes {
volume: "1".to_string(),
count: 1,
chapters: chapters_vol_1.clone(),
}),
("2".to_string(), Volumes {
volume: "2".to_string(),
count: 1,
chapters: chapters_vol_2.clone(),
}),
]);

let response = AggregateChapterResponse {
result: "ok".to_string(),
volumes: volumes.clone(),
};

let chapter_found = response.search_chapter("1", "2").expect("should not be none");

let chapter_not_found = response.search_chapter("1", "10");

assert_eq!(chapter_to_search, chapter_found);
assert!(chapter_not_found.is_none());
}

#[test]
fn it_searches_chapter_in_next_volume() {
let chapters_vol_1: HashMap<String, Chapters> = HashMap::from([
("1".to_string(), Chapters {
chapter: "1".to_string(),
id: "some_id".to_string(),
..Default::default()
}),
("2".to_string(), Chapters {
chapter: "2".to_string(),
id: "some_id_other".to_string(),
..Default::default()
}),
]);

let chapter_to_search = Chapters {
chapter: "3".to_string(),
id: "chapter_expected_to_be_found".to_string(),
..Default::default()
};

let chapters_vol_2: HashMap<String, Chapters> = HashMap::from([
("3".to_string(), chapter_to_search.clone()),
("4".to_string(), Chapters {
chapter: "4".to_string(),
..Default::default()
}),
]);

let volumes: HashMap<String, Volumes> = HashMap::from([
("1".to_string(), Volumes {
volume: "1".to_string(),
count: 1,
chapters: chapters_vol_1.clone(),
}),
("2".to_string(), Volumes {
volume: "2".to_string(),
count: 1,
chapters: chapters_vol_2.clone(),
}),
]);

let response = AggregateChapterResponse {
result: "ok".to_string(),
volumes: volumes.clone(),
};

let chapter_found = response.search_chapter_in_next_volume("1", "3").expect("should be found");
let chapter_found_not_found = response.search_chapter_in_next_volume("3", "5");

assert_eq!(chapter_found, chapter_to_search);
assert!(chapter_found_not_found.is_none());
}
}
2 changes: 2 additions & 0 deletions src/backend/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,8 @@ impl Database {
is_read BOOLEAN NOT NULL DEFAULT 0,
is_downloaded BOOLEAN NOT NULL DEFAULT 0,
is_bookmarked BOOLEAN NOT NULL DEFAULT false,
translated_language TEXT NULL,
number_page_bookmarked INT NULL,
FOREIGN KEY (manga_id) REFERENCES mangas (id)
)",
(),
Expand Down
86 changes: 55 additions & 31 deletions src/backend/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ use reqwest::{Client, Response, Url};

use super::api_responses::{AggregateChapterResponse, ChapterPagesResponse};
use super::filter::Languages;
use crate::backend::api_responses::OneChapterResponse;
use crate::backend::filter::{Filters, IntoParam};
use crate::config::ImageQuality;
use crate::view::pages::manga::ChapterOrder;
use crate::view::pages::reader::{Chapter, MangaPanel, SearchChapter, SearchMangaPanel};
use crate::view::pages::reader::{CurrentChapter, MangaPanel, SearchChapter, SearchMangaPanel};

// Todo! this trait should be split 💀💀
pub trait ApiClient: Clone + Send + 'static {
Expand Down Expand Up @@ -141,6 +142,11 @@ impl MangadexClient {
format!("{}/manga/{}/aggregate?translatedLanguage[]={}", self.api_url_base, manga_id, language.as_iso_code());
self.client.get(endpoint).send().await
}

pub async fn search_chapters_by_id(&self, chapter_id: &str) -> Result<Response, reqwest::Error> {
let endpoint = format!("{}/chapter/{chapter_id}", self.api_url_base);
self.client.get(endpoint).send().await
}
}

impl ApiClient for MangadexClient {
Expand Down Expand Up @@ -323,10 +329,7 @@ pub mod fake_api_client {
async fn search_chapter(
&self,
_manga_id: &str,
_volume_number: &str,
_chapter_number: &str,
_language: Languages,
) -> Result<Option<crate::view::pages::reader::Chapter>, Box<dyn std::error::Error>> {
) -> Result<crate::view::pages::reader::CurrentChapter, Box<dyn std::error::Error>> {
unimplemented!()
}
}
Expand Down Expand Up @@ -469,31 +472,22 @@ pub mod fake_api_client {
}

impl SearchChapter for MangadexClient {
async fn search_chapter(
&self,
manga_id: &str,
volume_number: &str,
chapter_number: &str,
language: Languages,
) -> Result<Option<Chapter>, Box<dyn std::error::Error>> {
let chapters_reponse: AggregateChapterResponse = self.search_chapters_aggregate(manga_id, language).await?.json().await?;

let chapter = chapters_reponse.search_chapter_in_next_volume(volume_number, chapter_number);

match chapter {
Some(found) => {
let res: ChapterPagesResponse = self.get_chapter_pages(&found.id).await?.json().await?;

Ok(Some(Chapter {
id: res.chapter.hash.clone(),
number: found.chapter.parse().unwrap_or_default(),
volume_number: Some(volume_number.parse().unwrap_or_default()),
language,
pages_url: res.get_files_based_on_quality_as_url(self.image_quality),
}))
},
None => Ok(None),
}
async fn search_chapter(&self, chapter_id: &str) -> Result<CurrentChapter, Box<dyn std::error::Error>> {
let response: OneChapterResponse = self.search_chapters_by_id(chapter_id).await?.json().await?;
let pages_response: ChapterPagesResponse = self.get_chapter_pages(chapter_id).await?.json().await?;

Ok(CurrentChapter {
id: response.data.id,
title: response.data.attributes.title.unwrap_or_default(),
number: response
.data
.attributes
.chapter
.map(|num| num.parse().unwrap_or_default())
.unwrap_or_default(),
volume_number: response.data.attributes.volume,
pages_url: pages_response.get_files_based_on_quality_as_url(self.image_quality),
})
}
}

Expand Down Expand Up @@ -524,7 +518,8 @@ mod test {
use self::api_responses::feed::OneMangaResponse;
use self::api_responses::tags::TagsResponse;
use self::api_responses::{
AggregateChapterResponse, ChapterPagesResponse, ChapterResponse, MangaStatisticsResponse, SearchMangaResponse,
AggregateChapterResponse, ChapterPagesResponse, ChapterResponse, MangaStatisticsResponse, OneChapterResponse,
SearchMangaResponse,
};
use super::*;
use crate::backend::*;
Expand Down Expand Up @@ -1041,6 +1036,35 @@ mod test {
assert_eq!(expected_response, data_sent);
}

#[tokio::test]
async fn mangadex_client_searches_chapter_by_id() {
let server = MockServer::start_async().await;
let client = MangadexClient::new(server.base_url().parse().unwrap(), server.base_url().parse().unwrap());

let chapter_id = Uuid::new_v4().to_string();

let expected_response = OneChapterResponse::default();

let request = server
.mock_async(|when, then| {
when.method(GET)
.header_exists("User-Agent")
.path_contains("/chapter")
.path_contains(&chapter_id);

then.status(200).json_body_obj(&expected_response);
})
.await;

let response = client.search_chapters_by_id(&chapter_id).await.expect("error sending request");

request.assert_async().await;

let data_sent: OneChapterResponse = response.json().await.expect("error deserializing response");

assert_eq!(expected_response, data_sent);
}

//#[tokio::test]
//async fn test_mangadex() {
// let client = MangadexClient::new(API_URL_BASE.parse().unwrap(), COVER_IMG_URL_BASE.parse().unwrap());
Expand Down
6 changes: 3 additions & 3 deletions src/backend/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use tokio::task::JoinHandle;

use super::fetch::ApiClient;
use crate::common::{Artist, Author};
use crate::view::app::{App, AppState};
use crate::view::pages::reader::{Chapter, SearchChapter, SearchMangaPanel};
use crate::view::app::{App, AppState, MangaToRead};
use crate::view::pages::reader::{CurrentChapter, SearchChapter, SearchMangaPanel};
use crate::view::widgets::search::MangaItem;
use crate::view::widgets::Component;

Expand All @@ -34,7 +34,7 @@ pub enum Events {
GoSearchMangasAuthor(Author),
GoSearchMangasArtist(Artist),
GoFeedPage,
ReadChapter(Chapter, String),
ReadChapter(CurrentChapter, MangaToRead),
}

/// Initialize the terminal
Expand Down
Loading

0 comments on commit 7c8e33b

Please sign in to comment.