diff options
author | dec05eba <dec05eba@protonmail.com> | 2022-02-12 04:31:44 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2022-02-12 04:33:00 +0100 |
commit | 74b98bed98aad3e70e8abe51292767ea8a7d109a (patch) | |
tree | ea1558431137f8a1e52f4d550c0438e68e676f6f | |
parent | cc445c60d4806fb462a3efc27bf8d727176f77da (diff) |
Local-manga: show if the latest chapter of a manga has been read
-rw-r--r-- | TODO | 7 | ||||
-rw-r--r-- | include/Path.hpp | 1 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 1 | ||||
-rw-r--r-- | plugins/LocalManga.hpp | 5 | ||||
-rw-r--r-- | plugins/Manga.hpp | 2 | ||||
-rw-r--r-- | plugins/Page.hpp | 6 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 38 | ||||
-rw-r--r-- | src/plugins/LocalManga.cpp | 86 | ||||
-rw-r--r-- | src/plugins/Manga.cpp | 2 |
9 files changed, 124 insertions, 24 deletions
@@ -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<FetchResult> fetch_future; AsyncTask<BodyItems> 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 <unordered_set> namespace QuickMedia { struct LocalMangaPage { @@ -29,8 +30,12 @@ namespace QuickMedia { PluginResult submit(const SubmitArgs &args, std::vector<Tab> &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<BodyItem> get_bookmark_body_item(BodyItem *selected_item) override; private: std::vector<LocalManga> manga_list; + std::unordered_set<std::string> 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<BodyItem> get_bookmark_body_item() override; + std::shared_ptr<BodyItem> 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<BodyItem> 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<BodyItem> 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<LazyFetchPage*>(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<LazyFetchPage*>(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<BodyItem> &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<LazyFetchPage*>(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 <unordered_set> +#include "../../external/cppcodec/base64_url.hpp" +#include <json/value.h> namespace QuickMedia { // Pages are sorted from 1.png to n.png @@ -80,8 +81,60 @@ namespace QuickMedia { return manga_list; } - static std::shared_ptr<BodyItem> 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<std::string> get_manga_finished_reading(const std::vector<LocalManga> &manga_list) { + Path local_manga_config_dir = get_storage_dir().join("local-manga"); + std::unordered_set<std::string> finished_reading; + + for(const LocalManga &local_manga : manga_list) { + std::string manga_name_base64_url = cppcodec::base64_url::encode<std::string>(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<BodyItem> 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<LocalMangaChaptersPage>(program, args.title, args.url, args.thumbnail_url); + auto chapters_page = std::make_unique<LocalMangaChaptersPage>(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<BodyItem> 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<std::string> get_lines_in_file(const Path &filepath) { std::unordered_set<std::string> lines; @@ -208,7 +278,7 @@ namespace QuickMedia { return PluginResult::OK; } - result_tabs.push_back(Tab{nullptr, std::make_unique<LocalMangaImagesPage>(program, content_title, args.title, args.url, thumbnail_url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<LocalMangaImagesPage>(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<BodyItem> MangaChaptersPage::get_bookmark_body_item() { + std::shared_ptr<BodyItem> MangaChaptersPage::get_bookmark_body_item(BodyItem*) { auto body_item = BodyItem::create(content_title); body_item->url = content_url; body_item->thumbnail_url = thumbnail_url; |