aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/QuickMedia.hpp2
-rw-r--r--plugins/Manga.hpp11
-rw-r--r--plugins/Manganelo.hpp4
-rw-r--r--src/QuickMedia.cpp266
-rw-r--r--src/plugins/Manga.cpp7
-rw-r--r--src/plugins/Mangadex.cpp6
-rw-r--r--src/plugins/Manganelo.cpp72
7 files changed, 296 insertions, 72 deletions
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<bool(const std::string &url)>;
+ 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<Creator>& get_creators() const;
+ protected:
+ std::vector<Creator> 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<Manga*>(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<Manga*>(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<BodyItems> 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<BodyItems> 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<Manga*>(current_plugin);
+
+ Json::Value *json_chapters = &content_storage_json["chapters"];
+ std::vector<EpisodeListTab> 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<Creator>& 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<Creator>& 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 <json/reader.h>
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<Creator> *creators = (std::vector<Creator>*)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<BodyItem>(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