From 74b98bed98aad3e70e8abe51292767ea8a7d109a Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 12 Feb 2022 04:31:44 +0100 Subject: Local-manga: show if the latest chapter of a manga has been read --- TODO | 7 ++-- include/Path.hpp | 1 + include/QuickMedia.hpp | 1 + plugins/LocalManga.hpp | 5 +++ plugins/Manga.hpp | 2 +- plugins/Page.hpp | 6 ++-- src/QuickMedia.cpp | 38 +++++++++++++++----- src/plugins/LocalManga.cpp | 86 +++++++++++++++++++++++++++++++++++++++++----- src/plugins/Manga.cpp | 2 +- 9 files changed, 124 insertions(+), 24 deletions(-) diff --git a/TODO b/TODO index 609e7dc..fc90b30 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,3 @@ -Give user the option to start where they left off or from the start or from the start (for manga). -Add scrollbar. Somehow deal with youtube banning ip when searching too often. When continuing to read manga from a different page from the first and there is no cache for the chapter, then start downloading from the current page instead of page 1. Show progress of manga in the history tab (current chapter out of total chapters). @@ -214,7 +212,8 @@ Use std::move(string) for all places where text.set_string is called. Use reference for text.get_string() in all places. Cleanup font sizes that are not visible (or not used for a while). Also do the same for character ranges font texture. Show video upload date in video description on youtube. -Add latest read chapter name to manga progress, to easily see from manga list page how we have progressed in the manga and also if the last chapter has been read then mark it as read. +Add latest read chapter name to manga progress, to easily see from manga list page how we have progressed in the manga. Allow changing manga sorting in gui (especially for local manga). Allow using ~ and $HOME in config file. -Save manga "I" mode to file and load it on startup. \ No newline at end of file +Save manga "I" mode to file and load it on startup. +Show in bookmarks and history page if a manga has been read (local-manga). \ No newline at end of file diff --git a/include/Path.hpp b/include/Path.hpp index bb08cc6..67e6942 100644 --- a/include/Path.hpp +++ b/include/Path.hpp @@ -10,6 +10,7 @@ namespace QuickMedia { Path(const char *path) : data(path) {} Path(const std::string &path) : data(path) {} + // TODO: Return a copy instead? makes it easier to use. Do the same for append Path& join(const Path &other) { data += "/"; data += other.data; diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 27c6bb2..c3f06de 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -72,6 +72,7 @@ namespace QuickMedia { mgl::Text search_result_text; AsyncTask fetch_future; AsyncTask next_page_future; + std::string body_item_url_before_refresh; }; class Program { diff --git a/plugins/LocalManga.hpp b/plugins/LocalManga.hpp index d112892..eca5f96 100644 --- a/plugins/LocalManga.hpp +++ b/plugins/LocalManga.hpp @@ -1,6 +1,7 @@ #pragma once #include "Manga.hpp" +#include namespace QuickMedia { struct LocalMangaPage { @@ -29,8 +30,12 @@ namespace QuickMedia { PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override; PluginResult lazy_fetch(BodyItems &result_items) override; const char* get_bookmark_name() const override { return "local-manga"; } + bool reload_on_page_change() override { return true; } + bool reseek_to_body_item_by_url() override { return true; } + std::shared_ptr get_bookmark_body_item(BodyItem *selected_item) override; private: std::vector manga_list; + std::unordered_set finished_reading_manga; bool standalone; }; diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp index bc6b415..fe9f0b4 100644 --- a/plugins/Manga.hpp +++ b/plugins/Manga.hpp @@ -65,7 +65,7 @@ namespace QuickMedia { TrackResult track(const std::string &str) override; void on_navigate_to_page(Body *body) override; bool is_trackable() const override { return true; } - std::shared_ptr get_bookmark_body_item() override; + std::shared_ptr get_bookmark_body_item(BodyItem *selected_item) override; protected: virtual bool extract_id_from_url(const std::string &url, std::string &manga_id) const = 0; virtual const char* get_service_name() const = 0; diff --git a/plugins/Page.hpp b/plugins/Page.hpp index a803725..6864a5d 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -67,8 +67,9 @@ namespace QuickMedia { virtual bool is_trackable() const { return false; } // Return nullptr if bookmark is not supported by this page virtual const char* get_bookmark_name() const { return nullptr; } - // If this returns nullptr then the currently selected body item is used instead - virtual std::shared_ptr get_bookmark_body_item() { return nullptr; } + // If this returns nullptr then the currently selected body item is used instead. + // |selected_item| may be nullptr. + virtual std::shared_ptr get_bookmark_body_item(BodyItem *selected_item) { (void)selected_item; return nullptr; } virtual bool is_bookmark_page() const { return false; } virtual bool is_lazy_fetch_page() const { return false; } // Note: If submit is done without any selection, then the search term is sent as the |title| and |url|. Submit will only be sent if the input text is not empty or if an item is selected @@ -117,6 +118,7 @@ namespace QuickMedia { // If this returns true then |lazy_fetch| is not meant to return results but async background load the page. This can be used to fetch API keys for example virtual bool lazy_fetch_is_loader() { return false; } virtual bool reload_on_page_change() { return false; } + virtual bool reseek_to_body_item_by_url() { return false; } }; class RelatedVideosPage : public Page { diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index cfe4e15..656e196 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1951,6 +1951,8 @@ namespace QuickMedia { if(tabs[i].page->is_lazy_fetch_page() && static_cast(tabs[i].page.get())->reload_on_page_change()) { tab_associated_data[i].lazy_fetch_finished = false; tab_associated_data[i].fetched_page = 0; + const BodyItem *selected_item = tabs[i].body->get_selected(); + tab_associated_data[i].body_item_url_before_refresh = selected_item ? selected_item->url : ""; tabs[i].body->clear_items(); } } @@ -2165,9 +2167,11 @@ namespace QuickMedia { }); } } else if(event.key.code == mgl::Keyboard::B && event.key.control) { - auto bookmark_item = tabs[selected_tab].page->get_bookmark_body_item(); + auto bookmark_item = tabs[selected_tab].page->get_bookmark_body_item(tabs[selected_tab].body->get_selected()); + if(!bookmark_item) bookmark_item = tabs[selected_tab].body->get_selected_shared(); + if(bookmark_item) { const char *bookmark_name = tabs[selected_tab].page->get_bookmark_name(); if(bookmark_name) { @@ -2359,18 +2363,36 @@ namespace QuickMedia { } if(associated_data.fetch_status == FetchStatus::LOADING && associated_data.fetch_type == FetchType::LAZY && associated_data.fetch_future.ready()) { + LazyFetchPage *lazy_fetch_page = static_cast(tabs[i].page.get()); + associated_data.lazy_fetch_finished = true; FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->set_items(std::move(fetch_result.body_items)); - if(tabs[i].search_bar && tabs[i].page->search_is_filter()) tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); - if(tabs[i].body->attach_side == AttachSide::TOP) { - tabs[i].body->select_first_item(); + + if(tabs[i].search_bar && tabs[i].page->search_is_filter()) { + tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); } - if(tabs[i].body->attach_side == AttachSide::BOTTOM) { - tabs[i].body->reverse_items(); - tabs[i].body->select_last_item(); + + if(lazy_fetch_page->reseek_to_body_item_by_url()) { + const auto &tab_ass = tab_associated_data[i]; + const int item_index = tabs[i].body->find_item_index([&tab_ass](const std::shared_ptr &item) { + return item->visible && item->url == tab_ass.body_item_url_before_refresh; + }); + + if(item_index != -1) + tabs[i].body->set_selected_item(item_index); + } else { + if(tabs[i].body->attach_side == AttachSide::TOP) { + tabs[i].body->select_first_item(); + } + if(tabs[i].body->attach_side == AttachSide::BOTTOM) { + tabs[i].body->reverse_items(); + tabs[i].body->select_last_item(); + } } - LazyFetchPage *lazy_fetch_page = static_cast(tabs[i].page.get()); + + tab_associated_data[i].body_item_url_before_refresh.clear(); + if(fetch_result.result != PluginResult::OK) associated_data.search_result_text.set_string("Failed to fetch page!"); else if(tabs[i].body->get_num_items() == 0 && !lazy_fetch_page->lazy_fetch_is_loader()) diff --git a/src/plugins/LocalManga.cpp b/src/plugins/LocalManga.cpp index c39752b..1807ca7 100644 --- a/src/plugins/LocalManga.cpp +++ b/src/plugins/LocalManga.cpp @@ -4,7 +4,8 @@ #include "../../include/Theme.hpp" #include "../../include/StringUtils.hpp" #include "../../include/Storage.hpp" -#include +#include "../../external/cppcodec/base64_url.hpp" +#include namespace QuickMedia { // Pages are sorted from 1.png to n.png @@ -80,8 +81,60 @@ namespace QuickMedia { return manga_list; } - static std::shared_ptr local_manga_to_body_item(const LocalManga &local_manga, time_t time_now) { - auto body_item = BodyItem::create(local_manga.name); + static bool has_finished_reading_latest_chapter(const LocalManga &manga, const Json::Value &chapters_json) { + if(manga.chapters.empty()) + return false; + + const Json::Value &chapter_json = chapters_json[manga.chapters.front().name]; + if(!chapter_json.isObject()) + return false; + + const Json::Value ¤t_json = chapter_json["current"]; + const Json::Value &total_json = chapter_json["total"]; + if(!current_json.isInt() || !total_json.isInt()) + return false; + + return current_json.asInt() >= total_json.asInt(); + } + + // TODO: Check if this is too slow with a lot of manga. + // In that case, only save latest read chapter (or newest chapter read) + // into one file with a list of all manga. + static std::unordered_set get_manga_finished_reading(const std::vector &manga_list) { + Path local_manga_config_dir = get_storage_dir().join("local-manga"); + std::unordered_set finished_reading; + + for(const LocalManga &local_manga : manga_list) { + std::string manga_name_base64_url = cppcodec::base64_url::encode(local_manga.name); + Path manga_progress_filepath = local_manga_config_dir; + manga_progress_filepath.join(manga_name_base64_url); + + Json::Value json_root; + if(!read_file_as_json(manga_progress_filepath, json_root)) + continue; + + if(!json_root.isObject()) + continue; + + const Json::Value &chapters_json = json_root["chapters"]; + if(!chapters_json.isObject()) + continue; + + if(has_finished_reading_latest_chapter(local_manga, chapters_json)) + finished_reading.insert(local_manga.name); + } + return finished_reading; + } + + static std::shared_ptr local_manga_to_body_item(const LocalManga &local_manga, time_t time_now, bool has_finished_reading) { + std::string title; + if(has_finished_reading) + title = "[Finished reading] "; + title += local_manga.name; + + auto body_item = BodyItem::create(std::move(title)); + if(has_finished_reading) + body_item->set_title_color(mgl::Color(43, 255, 47)); body_item->url = local_manga.name; body_item->set_description("Latest chapter: " + local_manga.chapters.front().name + "\nUpdated " + seconds_to_relative_time_str(time_now - local_manga.modified_time_seconds)); body_item->set_description_color(get_theme().faded_text_color); @@ -94,8 +147,10 @@ namespace QuickMedia { SearchResult LocalMangaSearchPage::search(const std::string &str, BodyItems &result_items) { time_t time_now = time(nullptr); for(const LocalManga &local_manga : manga_list) { - if(string_find_fuzzy_case_insensitive(local_manga.name, str)) - result_items.push_back(local_manga_to_body_item(local_manga, time_now)); + if(string_find_fuzzy_case_insensitive(local_manga.name, str)) { + const bool has_finished_reading = finished_reading_manga.find(local_manga.name) != finished_reading_manga.end(); + result_items.push_back(local_manga_to_body_item(local_manga, time_now, has_finished_reading)); + } } return SearchResult::OK; } @@ -128,13 +183,14 @@ namespace QuickMedia { auto chapters_body = create_body(); chapters_body->set_items(std::move(chapters_items)); - auto chapters_page = std::make_unique(program, args.title, args.url, args.thumbnail_url); + auto chapters_page = std::make_unique(program, args.url, args.url, args.thumbnail_url); result_tabs.push_back(Tab{std::move(chapters_body), std::move(chapters_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); return PluginResult::OK; } PluginResult LocalMangaSearchPage::lazy_fetch(BodyItems &result_items) { manga_list.clear(); + finished_reading_manga.clear(); if(get_config().local_manga_directory.empty()) { show_notification("QuickMedia", "local_manga_directory config is not set", Urgency::CRITICAL); @@ -148,14 +204,28 @@ namespace QuickMedia { manga_list = get_manga_in_directory(get_config().local_manga_directory); + if(standalone) + finished_reading_manga = get_manga_finished_reading(manga_list); + const time_t time_now = time(nullptr); for(const LocalManga &local_manga : manga_list) { - result_items.push_back(local_manga_to_body_item(local_manga, time_now)); + const bool has_finished_reading = finished_reading_manga.find(local_manga.name) != finished_reading_manga.end(); + result_items.push_back(local_manga_to_body_item(local_manga, time_now, has_finished_reading)); } return PluginResult::OK; } + std::shared_ptr LocalMangaSearchPage::get_bookmark_body_item(BodyItem *selected_item) { + if(!selected_item) + return nullptr; + + auto body_item = BodyItem::create(selected_item->url); + body_item->url = selected_item->url; + body_item->thumbnail_url = selected_item->thumbnail_url; + return body_item; + } + static std::unordered_set get_lines_in_file(const Path &filepath) { std::unordered_set lines; @@ -208,7 +278,7 @@ namespace QuickMedia { return PluginResult::OK; } - result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, args.title, args.url, thumbnail_url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, args.url, args.url, thumbnail_url), nullptr}); if(is_program_executable_by_name("automedia")) append_seen_manga_to_automedia_seen(content_url + "/" + args.url); diff --git a/src/plugins/Manga.cpp b/src/plugins/Manga.cpp index 4401974..e4269fe 100644 --- a/src/plugins/Manga.cpp +++ b/src/plugins/Manga.cpp @@ -21,7 +21,7 @@ namespace QuickMedia { load_manga_content_storage(get_service_name(), content_title, content_url, manga_id); } - std::shared_ptr MangaChaptersPage::get_bookmark_body_item() { + std::shared_ptr MangaChaptersPage::get_bookmark_body_item(BodyItem*) { auto body_item = BodyItem::create(content_title); body_item->url = content_url; body_item->thumbnail_url = thumbnail_url; -- cgit v1.2.3