From 8d661807e363c973dc6a6d7e6015495e7cc3a2ab Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 15 Sep 2020 21:38:24 +0200 Subject: Add manga creators page to navigate to others works by the creators --- include/QuickMedia.hpp | 2 + plugins/Manga.hpp | 11 ++ plugins/Manganelo.hpp | 4 +- src/QuickMedia.cpp | 266 ++++++++++++++++++++++++++++++++++------------ src/plugins/Manga.cpp | 7 ++ src/plugins/Mangadex.cpp | 6 ++ src/plugins/Manganelo.cpp | 72 ++++++++++++- 7 files changed, 296 insertions(+), 72 deletions(-) create mode 100644 src/plugins/Manga.cpp diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index faee324..b2c499c 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -44,6 +44,8 @@ namespace QuickMedia { void image_board_thread_list_page(); void image_board_thread_page(); + bool on_search_suggestion_submit_text(Body *input_body, Body *output_body); + enum class LoadImageResult { OK, FAILED, diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp index 18ed2f9..0c57d9f 100644 --- a/plugins/Manga.hpp +++ b/plugins/Manga.hpp @@ -8,6 +8,11 @@ namespace QuickMedia { // Return false to stop iteration using PageCallback = std::function; + struct Creator { + std::string name; + std::string url; + }; + class Manga : public Plugin { public: Manga(const std::string &plugin_name) : Plugin(plugin_name) {} @@ -15,5 +20,11 @@ namespace QuickMedia { virtual ImageResult get_number_of_images(const std::string &url, int &num_images) = 0; virtual ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) = 0; virtual bool extract_id_from_url(const std::string &url, std::string &manga_id) = 0; + + virtual PluginResult get_creators_manga_list(const std::string &url, BodyItems &result_items) { return {}; } + + const std::vector& get_creators() const; + protected: + std::vector creators; }; } \ No newline at end of file diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp index ffac830..c2ad693 100644 --- a/plugins/Manganelo.hpp +++ b/plugins/Manganelo.hpp @@ -13,12 +13,14 @@ namespace QuickMedia { ImageResult get_number_of_images(const std::string &url, int &num_images) override; bool search_suggestions_has_thumbnails() const override { return true; } bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 150; } + int get_search_delay() const override { return 200; } Page get_page_after_search() const override { return Page::EPISODE_LIST; } ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override; bool extract_id_from_url(const std::string &url, std::string &manga_id) override; + + PluginResult get_creators_manga_list(const std::string &url, BodyItems &result_items) override; private: // Caches url. If the same url is requested multiple times then the cache is used ImageResult get_image_urls_for_chapter(const std::string &url); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 9adbc22..44e6bd9 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -35,6 +35,8 @@ static const int DOUBLE_CLICK_TIME = 500; static const std::string fourchan_google_captcha_api_key = "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; static const float tab_text_size = 18.0f; static const float tab_height = tab_text_size + 10.0f; +static const sf::Color tab_selected_color(0, 85, 119); +static const sf::Color tab_unselected_color(43, 45, 47); // Prevent writing to broken pipe from exiting the program static void sigpipe_handler(int unused) { @@ -756,6 +758,56 @@ namespace QuickMedia { sf::Text *text; }; + bool Program::on_search_suggestion_submit_text(Body *input_body, Body *output_body) { + if(input_body->no_items_visible()) + return false; + + Page next_page = current_plugin->get_page_after_search(); + bool skip_search = next_page == Page::VIDEO_CONTENT; + // TODO: This shouldn't be done if search_selected_suggestion fails + if(search_selected_suggestion(input_body, output_body, current_plugin, content_title, content_url, skip_search) != SearchResult::OK) { + show_notification("Search", "Search failed!", Urgency::CRITICAL); + return false; + } + + if(next_page == Page::EPISODE_LIST && current_plugin->is_manga()) { + Manga *manga_plugin = static_cast(current_plugin); + if(content_url.empty()) { + show_notification("Manga", "Url is missing for manga!", Urgency::CRITICAL); + return false; + } + + Path content_storage_dir = get_storage_dir().join(current_plugin->name); + + std::string manga_id; + if(!manga_plugin->extract_id_from_url(content_url, manga_id)) + return false; + + manga_id_base64 = base64_encode(manga_id); + content_storage_file = content_storage_dir.join(manga_id_base64); + content_storage_json.clear(); + content_storage_json["name"] = content_title; + FileType file_type = get_file_type(content_storage_file); + if(file_type == FileType::REGULAR) + read_file_as_json(content_storage_file, content_storage_json); + } else if(next_page == Page::VIDEO_CONTENT) { + watched_videos.clear(); + if(content_url.empty()) + next_page = Page::SEARCH_RESULT; + else { + page_stack.push(Page::SEARCH_SUGGESTION); + } + current_page = next_page; + return false; + } else if(next_page == Page::CONTENT_LIST) { + content_list_url = content_url; + } else if(next_page == Page::IMAGE_BOARD_THREAD_LIST) { + image_board_thread_list_url = content_url; + } + current_page = next_page; + return true; + } + void Program::search_suggestion_page() { std::string update_search_text; bool search_running = false; @@ -855,51 +907,7 @@ namespace QuickMedia { if(typing || tabs[selected_tab].body->no_items_visible()) return false; } - - Page next_page = current_plugin->get_page_after_search(); - bool skip_search = next_page == Page::VIDEO_CONTENT; - // TODO: This shouldn't be done if search_selected_suggestion fails - if(search_selected_suggestion(tabs[selected_tab].body, body, current_plugin, content_title, content_url, skip_search) != SearchResult::OK) { - show_notification("Search", "Search failed!", Urgency::CRITICAL); - return false; - } - - if(next_page == Page::EPISODE_LIST && current_plugin->is_manga()) { - Manga *manga_plugin = static_cast(current_plugin); - if(content_url.empty()) { - show_notification("Manga", "Url is missing for manga!", Urgency::CRITICAL); - return false; - } - - Path content_storage_dir = get_storage_dir().join(current_plugin->name); - - std::string manga_id; - if(!manga_plugin->extract_id_from_url(content_url, manga_id)) - return false; - - manga_id_base64 = base64_encode(manga_id); - content_storage_file = content_storage_dir.join(manga_id_base64); - content_storage_json.clear(); - content_storage_json["name"] = content_title; - FileType file_type = get_file_type(content_storage_file); - if(file_type == FileType::REGULAR) - read_file_as_json(content_storage_file, content_storage_json); - } else if(next_page == Page::VIDEO_CONTENT) { - watched_videos.clear(); - if(content_url.empty()) - next_page = Page::SEARCH_RESULT; - else { - page_stack.push(Page::SEARCH_SUGGESTION); - } - current_page = next_page; - return false; - } else if(next_page == Page::CONTENT_LIST) { - content_list_url = content_url; - } else if(next_page == Page::IMAGE_BOARD_THREAD_LIST) { - image_board_thread_list_url = content_url; - } - current_page = next_page; - return true; + return on_search_suggestion_submit_text(tabs[selected_tab].body, body); }; std::future recommended_future; @@ -921,8 +929,6 @@ namespace QuickMedia { bool redraw = true; sf::Event event; - const sf::Color tab_selected_color(0, 85, 119); - const sf::Color tab_unselected_color(43, 45, 47); sf::RectangleShape tab_spacing_rect(sf::Vector2f(0.0f, 0.0f)); tab_spacing_rect.setFillColor(tab_unselected_color); const float tab_spacer_height = 0.0f; @@ -1617,41 +1623,120 @@ namespace QuickMedia { return Page::EXIT; } + enum class EpisodeListTabType { + CHAPTERS, + CREATOR + }; + + struct EpisodeListTab { + EpisodeListTabType type; + Body *body; + const Creator *creator; + std::future creator_page_download_future; + sf::Text text; + }; + void Program::episode_list_page() { - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->select_first_item(); + assert(current_plugin->is_manga()); + Manga *manga = static_cast(current_plugin); + + Json::Value *json_chapters = &content_storage_json["chapters"]; + std::vector tabs; + int selected_tab = 0; + + search_bar->onTextUpdateCallback = [&tabs, &selected_tab](const std::string &text) { + tabs[selected_tab].body->filter_search_fuzzy(text); + tabs[selected_tab].body->clamp_selection(); }; - search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; + search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string &text) -> bool { + if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return false; - select_episode(selected_item, false); - return true; + select_episode(selected_item, false); + return true; + } else { + if(on_search_suggestion_submit_text(tabs[selected_tab].body, body)) { + selected_tab = 0; + json_chapters = &content_storage_json["chapters"]; + return true; + } else { + return false; + } + } }; - const Json::Value &json_chapters = content_storage_json["chapters"]; + auto download_create_page = [manga](std::string url) { + BodyItems body_items; + if(manga->get_creators_manga_list(url, body_items) != PluginResult::OK) + show_notification("Manga", "Failed to download authors page", Urgency::CRITICAL); + return body_items; + }; + EpisodeListTab chapters_tab; + chapters_tab.type = EpisodeListTabType::CHAPTERS; + chapters_tab.body = body; + chapters_tab.creator = nullptr; + chapters_tab.text = sf::Text("Chapters", font, tab_text_size); + tabs.push_back(std::move(chapters_tab)); + + const std::vector& creators = manga->get_creators(); + for(const Creator &creator : creators) { + EpisodeListTab tab; + tab.type = EpisodeListTabType::CREATOR; + tab.body = new Body(this, &font, &bold_font); + tab.body->draw_thumbnails = true; + tab.creator = &creator; + tab.creator_page_download_future = std::async(std::launch::async, download_create_page, creator.url); + tab.text = sf::Text(creator.name, font, tab_text_size); + tabs.push_back(std::move(tab)); + } + + const float tab_spacer_height = 0.0f; sf::Vector2f body_pos; sf::Vector2f body_size; bool redraw = true; sf::Event event; + sf::RectangleShape tab_drop_shadow; + tab_drop_shadow.setFillColor(sf::Color(23, 25, 27)); + while (current_page == Page::EPISODE_LIST) { while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION); + base_event_handler(event, Page::SEARCH_SUGGESTION, false, true); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) redraw = true; - else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::T && sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) { - BodyItem *selected_item = body->get_selected(); - if(selected_item) { - if(track_media(TrackMediaType::HTML, content_title, selected_item->get_title(), content_url) == 0) { - show_notification("Media tracker", "You are now tracking \"" + content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); - } else { - show_notification("Media tracker", "Failed to track media \"" + content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); + else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::T && event.key.control && tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { + BodyItem *selected_item = body->get_selected(); + if(selected_item) { + if(track_media(TrackMediaType::HTML, content_title, selected_item->get_title(), content_url) == 0) { + show_notification("Media tracker", "You are now tracking \"" + content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); + } else { + show_notification("Media tracker", "Failed to track media \"" + content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); + } } + } else if(event.key.code == sf::Keyboard::Up) { + tabs[selected_tab].body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + tabs[selected_tab].body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::SEARCH_SUGGESTION; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } else if(event.key.code == sf::Keyboard::Left) { + tabs[selected_tab].body->filter_search_fuzzy(""); + tabs[selected_tab].body->clamp_selection(); + selected_tab = std::max(0, selected_tab - 1); + search_bar->clear(); + } else if(event.key.code == sf::Keyboard::Right) { + tabs[selected_tab].body->filter_search_fuzzy(""); + tabs[selected_tab].body->clamp_selection(); + selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); + search_bar->clear(); } } } @@ -1660,16 +1745,59 @@ namespace QuickMedia { if(redraw) { redraw = false; search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); + get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); } search_bar->update(); window.clear(back_color); - body->draw(window, body_pos, body_size, json_chapters); - search_bar->draw(window); + + const float width_per_tab = window_size.x / tabs.size(); + sf::RectangleShape tab_background(sf::Vector2f(std::floor(width_per_tab), tab_height)); + + float tab_vertical_offset = search_bar->getBottomWithoutShadow(); + if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) + tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); + else + tabs[selected_tab].body->draw(window, body_pos, body_size); + const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); + + int i = 0; + for(EpisodeListTab &tab : tabs) { + if(tab.type == EpisodeListTabType::CREATOR + && tab.creator_page_download_future.valid() + && tab.creator_page_download_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) + { + tab.body->items = tab.creator_page_download_future.get(); + tab.body->filter_search_fuzzy(search_bar->get_text()); + tab.body->clamp_selection(); + } + + if(i == selected_tab) + tab_background.setFillColor(tab_selected_color); + else + tab_background.setFillColor(tab_unselected_color); + + tab_background.setPosition(std::floor(i * width_per_tab), tab_spacer_height + std::floor(tab_vertical_offset)); + window.draw(tab_background); + const float center = (i * width_per_tab) + (width_per_tab * 0.5f); + tab.text.setPosition(std::floor(center - tab.text.getLocalBounds().width * 0.5f), tab_y); + window.draw(tab.text); + ++i; + } + + tab_drop_shadow.setSize(sf::Vector2f(window_size.x, 5.0f)); + tab_drop_shadow.setPosition(0.0f, std::floor(tab_vertical_offset + tab_height)); + window.draw(tab_drop_shadow); + + search_bar->draw(window, false); window.display(); } + + for(EpisodeListTab &tab : tabs) { + if(tab.type == EpisodeListTabType::CREATOR) + delete tab.body; + } } // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image @@ -2466,7 +2594,7 @@ namespace QuickMedia { body->items[reply_index]->visible = true; } } - } else if(event.key.code == sf::Keyboard::C && sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) && selected_item) { + } else if(event.key.code == sf::Keyboard::C && event.key.control && selected_item) { navigation_stage = NavigationStage::REPLYING; } else if(event.key.code == sf::Keyboard::R && selected_item) { std::string text_to_add = ">>" + selected_item->post_number; diff --git a/src/plugins/Manga.cpp b/src/plugins/Manga.cpp new file mode 100644 index 0000000..6ad11ab --- /dev/null +++ b/src/plugins/Manga.cpp @@ -0,0 +1,7 @@ +#include "../../plugins/Manga.hpp" + +namespace QuickMedia { + const std::vector& Manga::get_creators() const { + return creators; + } +} \ No newline at end of file diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index 4afa89b..705cbc5 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -197,6 +197,9 @@ namespace QuickMedia { } }, &result_items); + if(result != 0) + goto cleanup; + BodyItemImageContext body_item_image_context; body_item_image_context.body_items = &result_items; body_item_image_context.index = 0; @@ -211,6 +214,9 @@ namespace QuickMedia { } }, &body_item_image_context); + if(result != 0) + goto cleanup; + body_item_image_context.index = 0; result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='pl-1']", [](QuickMediaHtmlNode *node, void *userdata) { diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index a4c8809..a772601 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -4,9 +4,16 @@ #include namespace QuickMedia { + struct BodyItemImageContext { + BodyItems *body_items; + size_t index; + }; + SearchResult Manganelo::search(const std::string &url, BodyItems &result_items) { + creators.clear(); + std::string website_data; - if(download_to_string(url, website_data, {}, use_tor) != DownloadResult::OK) + if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) return SearchResult::NET_ERR; QuickMediaHtmlSearch html_search; @@ -26,6 +33,19 @@ namespace QuickMedia { } }, &result_items); + result = quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", + [](QuickMediaHtmlNode *node, void *userdata) { + std::vector *creators = (std::vector*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *text = quickmedia_html_node_get_text(node); + if(href && text && strstr(href, "/author/story/")) { + Creator creator; + creator.name = strip(text); + creator.url = href; + creators->push_back(std::move(creator)); + } + }, &creators); + cleanup: quickmedia_html_search_deinit(&html_search); return result == 0 ? SearchResult::OK : SearchResult::ERR; @@ -112,7 +132,7 @@ namespace QuickMedia { last_chapter_image_urls.clear(); std::string website_data; - if(download_to_string(url, website_data, {}, use_tor) != DownloadResult::OK) + if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) return ImageResult::NET_ERR; QuickMediaHtmlSearch html_search; @@ -191,4 +211,52 @@ namespace QuickMedia { return false; } } + + PluginResult Manganelo::get_creators_manga_list(const std::string &url, BodyItems &result_items) { + std::string website_data; + if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) + return PluginResult::NET_ERR; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); + if(result != 0) + goto cleanup; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *title = quickmedia_html_node_get_attribute_value(node, "title"); + if(href && title && strstr(href, "/manga/")) { + auto body_item = std::make_unique(title); + body_item->url = href; + item_data->push_back(std::move(body_item)); + } + }, &result_items); + + if(result != 0) + goto cleanup; + + BodyItemImageContext body_item_image_context; + body_item_image_context.body_items = &result_items; + body_item_image_context.index = 0; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']//img", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItemImageContext*)userdata; + const char *src = quickmedia_html_node_get_attribute_value(node, "src"); + if(src && item_data->index < item_data->body_items->size()) { + (*item_data->body_items)[item_data->index]->thumbnail_url = src; + item_data->index++; + } + }, &body_item_image_context); + + cleanup: + quickmedia_html_search_deinit(&html_search); + if(result != 0) { + result_items.clear(); + return PluginResult::ERR; + } + return PluginResult::OK; + } } \ No newline at end of file -- cgit v1.2.3