diff options
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | include/Page.hpp | 4 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 7 | ||||
-rw-r--r-- | plugins/Manganelo.hpp | 8 | ||||
-rw-r--r-- | plugins/Plugin.hpp | 10 | ||||
-rw-r--r-- | plugins/Youtube.hpp | 2 | ||||
-rw-r--r-- | src/Program.c | 10 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 254 | ||||
-rw-r--r-- | src/plugins/Manganelo.cpp (renamed from src/Manganelo.cpp) | 88 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp (renamed from src/Youtube.cpp) | 6 |
10 files changed, 289 insertions, 105 deletions
@@ -4,4 +4,7 @@ Native clients of websites with fast access to what you want to see See project.conf \[dependencies]. youtube-dl also needs to be installed to play videos from youtube. # TODO Fix x11 freeze that sometimes happens when playing video. -If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions.
\ No newline at end of file +If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions. +Keep track of content that has been viewed so the user can return to where they were last. +For manga, view the next chapter when reaching the end of a chapter. +Make network requests asynchronous to not freeze gui when navigating. Also have loading animation.
\ No newline at end of file diff --git a/include/Page.hpp b/include/Page.hpp index ce20971..7b1fc17 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -5,6 +5,8 @@ namespace QuickMedia { EXIT, SEARCH_SUGGESTION, SEARCH_RESULT, - VIDEO_CONTENT + VIDEO_CONTENT, + EPISODE_LIST, + IMAGES }; }
\ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index d5839c7..e5f4f2d 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -17,10 +17,12 @@ namespace QuickMedia { ~Program(); void run(); private: - void base_event_handler(sf::Event &event); + void base_event_handler(sf::Event &event, Page previous_page); void search_suggestion_page(); void search_result_page(); void video_content_page(); + void episode_list_page(); + void image_page(); private: sf::RenderWindow window; sf::Vector2f window_size; @@ -29,6 +31,9 @@ namespace QuickMedia { Plugin *current_plugin; std::unique_ptr<SearchBar> search_bar; Page current_page; + // TODO: Combine these std::string video_url; + std::string images_url; + int image_index; }; }
\ No newline at end of file diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp index 4ed976f..5e9382e 100644 --- a/plugins/Manganelo.hpp +++ b/plugins/Manganelo.hpp @@ -5,7 +5,13 @@ namespace QuickMedia { class Manganelo : public Plugin { public: - SearchResult search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) override; + SearchResult search(const std::string &url, std::vector<std::unique_ptr<BodyItem>> &result_items, Page &next_page) override; SuggestionResult update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) override; + ImageResult get_image_by_index(const std::string &url, int index, std::string &image_data); + private: + ImageResult get_image_urls_for_chapter(const std::string &url, std::vector<std::string> &urls); + private: + std::string last_chapter_url; + std::vector<std::string> last_chapter_image_urls; }; }
\ No newline at end of file diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index 09ce09a..818cc5f 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../include/Page.hpp" #include <string> #include <vector> #include <memory> @@ -35,6 +36,13 @@ namespace QuickMedia { NET_ERR }; + enum class ImageResult { + OK, + END, + ERR, + NET_ERR + }; + struct CommandArg { std::string option; std::string value; @@ -44,7 +52,7 @@ namespace QuickMedia { public: virtual ~Plugin() = default; - virtual SearchResult search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) = 0; + virtual SearchResult search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items, Page &next_page) = 0; virtual SuggestionResult update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items); virtual std::vector<std::unique_ptr<BodyItem>> get_related_media(const std::string &url); protected: diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index eda8a1f..c342a10 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -5,7 +5,7 @@ namespace QuickMedia { class Youtube : public Plugin { public: - SearchResult search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) override; + SearchResult search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items, Page &next_page) override; SuggestionResult update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) override; std::vector<std::unique_ptr<BodyItem>> get_related_media(const std::string &url) override; }; diff --git a/src/Program.c b/src/Program.c index 39957ed..38a602f 100644 --- a/src/Program.c +++ b/src/Program.c @@ -66,7 +66,15 @@ int exec_program(const char **args, ProgramOutputCallback output_callback, void int exit_status = WEXITSTATUS(status); if(exit_status != 0) { - fprintf(stderr, "Failed to execute program, exit status %d\n", exit_status); + fprintf(stderr, "Failed to execute program ("); + const char **arg = args; + while(*arg) { + if(arg != args) + fputc(' ', stderr); + fprintf(stderr, "'%s'", *arg); + ++arg; + } + fprintf(stderr, "), exit status %d\n", exit_status); result = -exit_status; goto cleanup; } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c7d89e2..e6c7077 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -173,7 +173,8 @@ namespace QuickMedia { window_size(800, 600), body(nullptr), current_plugin(nullptr), - current_page(Page::SEARCH_SUGGESTION) + current_page(Page::SEARCH_SUGGESTION), + image_index(0) { window.setVerticalSyncEnabled(true); if(!font.loadFromFile("fonts/Lato-Regular.ttf")) { @@ -181,8 +182,8 @@ namespace QuickMedia { abort(); } body = new Body(font); - //current_plugin = new Manganelo(); - current_plugin = new Youtube(); + current_plugin = new Manganelo(); + //current_plugin = new Youtube(); search_bar = std::make_unique<SearchBar>(font); } @@ -191,14 +192,15 @@ namespace QuickMedia { delete current_plugin; } - static SearchResult search_selected_suggestion(Body *body, Plugin *plugin) { + static SearchResult search_selected_suggestion(Body *body, Plugin *plugin, Page &next_page) { BodyItem *selected_item = body->get_selected(); if(!selected_item) return SearchResult::ERR; std::string selected_item_title = selected_item->title; + std::string selected_item_url = selected_item->url; body->clear_items(); - SearchResult search_result = plugin->search(selected_item_title, body->items); + SearchResult search_result = plugin->search(!selected_item_url.empty() ? selected_item_url : selected_item_title, body->items, next_page); body->reset_selected(); return search_result; } @@ -208,7 +210,6 @@ namespace QuickMedia { if(text.isEmpty()) return; - body->items.push_back(std::make_unique<BodyItem>(text)); SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items); body->clamp_selection(); } @@ -217,7 +218,8 @@ namespace QuickMedia { while(window.isOpen()) { switch(current_page) { case Page::EXIT: - return; + window.close(); + break; case Page::SEARCH_SUGGESTION: search_suggestion_page(); break; @@ -227,14 +229,40 @@ namespace QuickMedia { case Page::VIDEO_CONTENT: video_content_page(); break; + case Page::EPISODE_LIST: + episode_list_page(); + break; + case Page::IMAGES: + image_page(); + break; default: return; } } } - void Program::base_event_handler(sf::Event &event) { - + void Program::base_event_handler(sf::Event &event, Page previous_page) { + if (event.type == sf::Event::Closed) { + current_page = Page::EXIT; + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = previous_page; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } + } else if(event.type == sf::Event::TextEntered) { + search_bar->onTextEntered(event.text.unicode); + } } void Program::search_suggestion_page() { @@ -243,8 +271,9 @@ namespace QuickMedia { }; search_bar->onTextSubmitCallback = [this](const std::string &text) { - if(search_selected_suggestion(body, current_plugin) == SearchResult::OK) - current_page = Page::SEARCH_RESULT; + Page next_page; + if(search_selected_suggestion(body, current_plugin, next_page) == SearchResult::OK) + current_page = next_page; }; sf::Vector2f body_pos; @@ -252,29 +281,11 @@ namespace QuickMedia { bool resized = true; sf::Event event; - while (window.isOpen() && current_page == Page::SEARCH_SUGGESTION) { + while (current_page == Page::SEARCH_SUGGESTION) { while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - window.close(); - current_page = Page::EXIT; - } else if(event.type == sf::Event::Resized) { - window_size.x = event.size.width; - window_size.y = event.size.height; - sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); - window.setView(sf::View(visible_area)); + base_event_handler(event, Page::EXIT); + if(event.type == sf::Event::Resized) resized = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - body->select_next_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - window.close(); - } - } else if(event.type == sf::Event::TextEntered) { - search_bar->onTextEntered(event.text.unicode); - } } if(resized) { @@ -321,31 +332,9 @@ namespace QuickMedia { bool resized = true; sf::Event event; - while (window.isOpen() && current_page == Page::SEARCH_RESULT) { + while (current_page == Page::SEARCH_RESULT) { while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - window.close(); - current_page = Page::EXIT; - } else if(event.type == sf::Event::Resized) { - window_size.x = event.size.width; - window_size.y = event.size.height; - sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); - window.setView(sf::View(visible_area)); - resized = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - 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.type == sf::Event::TextEntered) { - search_bar->onTextEntered(event.text.unicode); - } + base_event_handler(event, Page::SEARCH_SUGGESTION); } if(resized) { @@ -400,31 +389,160 @@ namespace QuickMedia { sf::Clock resize_timer; sf::Event event; - while (window.isOpen() && current_page == Page::VIDEO_CONTENT) { + while (current_page == Page::VIDEO_CONTENT) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::SEARCH_SUGGESTION); + if(event.type == sf::Event::Resized) { + if(video_player) + video_player->resize(sf::Vector2i(window_size.x, window_size.y)); + } + } + + window.clear(); + if(video_player) + video_player->draw(window); + window.display(); + } + } + + void Program::episode_list_page() { + search_bar->onTextUpdateCallback = [this](const std::string &text) { + body->filter_search_fuzzy(text); + body->clamp_selection(); + }; + + search_bar->onTextSubmitCallback = [this](const std::string &text) { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return; + + images_url = selected_item->url; + image_index = 0; + current_page = Page::IMAGES; + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool resized = true; + sf::Event event; + + while (current_page == Page::EPISODE_LIST) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::SEARCH_SUGGESTION); + if(event.type == sf::Event::Resized) + resized = true; + } + + if(resized) { + search_bar->onWindowResize(window_size); + + float body_padding_horizontal = 50.0f; + float body_padding_vertical = 50.0f; + float body_width = window_size.x - body_padding_horizontal * 2.0f; + if(body_width < 400) { + body_width = window_size.x; + body_padding_horizontal = 0.0f; + } + + float search_bottom = search_bar->getBottom(); + body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical); + body_size = sf::Vector2f(body_width, window_size.y); + } + + search_bar->update(); + + window.clear(back_color); + body->draw(window, body_pos, body_size); + search_bar->draw(window); + window.display(); + } + } + + void Program::image_page() { + search_bar->onTextUpdateCallback = nullptr; + search_bar->onTextSubmitCallback = nullptr; + + sf::Texture image_texture; + sf::Sprite image; + sf::Text error_message("", font, 30); + error_message.setFillColor(sf::Color::White); + + Manganelo *image_plugin = static_cast<Manganelo*>(current_plugin); + std::string image_data; + + // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image + // becomes 40mb (private memory, almost 100mb in total!) Unacceptable! + ImageResult image_result = image_plugin->get_image_by_index(images_url, image_index, image_data); + if(image_result == ImageResult::OK) { + if(image_texture.loadFromMemory(image_data.data(), image_data.size())) { + image_texture.setSmooth(true); + image.setTexture(image_texture, true); + } else { + error_message.setString(std::string("Failed to load image for page ") + std::to_string(image_index)); + } + } else if(image_result == ImageResult::END) { + // TODO: Better error message, with chapter name + error_message.setString("End of chapter"); + } else { + // TODO: Convert ImageResult error to a string and show to user + error_message.setString(std::string("Network error, failed to get image for page ") + std::to_string(image_index)); + } + image_data.resize(0); + + bool error = !error_message.getString().isEmpty(); + bool resized = true; + sf::Event event; + + // TODO: Show current page / number of pages. + // TODO: Show to user if a certain page is missing (by checking page name (number) and checking if some is skipped) + while (current_page == Page::IMAGES) { while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) { - window.close(); current_page = Page::EXIT; } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); window.setView(sf::View(visible_area)); - if(video_player) - video_player->resize(sf::Vector2i(window_size.x, window_size.y)); + resized = true; } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); + if(event.key.code == sf::Keyboard::Up) { + if(image_index > 0) { + --image_index; + return; + } + } else if(event.key.code == sf::Keyboard::Down) { + if(!error) { + ++image_index; + return; + } + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::EPISODE_LIST; } } } - window.clear(); - if(video_player) - video_player->draw(window); + if(resized) { + if(error) { + auto bounds = error_message.getLocalBounds(); + error_message.setPosition(window_size.x * 0.5f - bounds.width * 0.5f, window_size.y * 0.5f - bounds.height); + } else { + auto texture_size = image.getTexture()->getSize(); + auto image_scale = image.getScale(); + auto image_size = sf::Vector2f(texture_size.x, texture_size.y); + image_size.x *= image_scale.x; + image_size.y *= image_scale.y; + + image.setPosition(window_size.x * 0.5f - image_size.x * 0.5f, window_size.y * 0.5f - image_size.y * 0.5f); + } + } + + window.clear(back_color); + if(error) { + window.draw(error_message); + } else { + window.draw(image); + } window.display(); } } diff --git a/src/Manganelo.cpp b/src/plugins/Manganelo.cpp index 1d3929e..bc99387 100644 --- a/src/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -1,51 +1,31 @@ -#include "../plugins/Manganelo.hpp" +#include "../../plugins/Manganelo.hpp" #include <quickmedia/HtmlSearch.h> #include <json/reader.h> namespace QuickMedia { - SearchResult Manganelo::search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) { - std::string url = "https://manganelo.com/search/"; - url += url_param_encode(text); + SearchResult Manganelo::search(const std::string &url, std::vector<std::unique_ptr<BodyItem>> &result_items, Page &next_page) { + next_page = Page::EPISODE_LIST; std::string website_data; if(download_to_string(url, website_data) != DownloadResult::OK) return SearchResult::NET_ERR; - struct ItemData { - std::vector<std::unique_ptr<BodyItem>> &result_items; - size_t item_index; - }; - - ItemData item_data = { result_items, 0 }; - 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, "//h3[class=\"story_name\"]/a", + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='chapter-list']/div[class='row']//a", [](QuickMediaHtmlNode *node, void *userdata) { - ItemData *item_data = (ItemData*)userdata; + auto *item_data = (std::vector<std::unique_ptr<BodyItem>>*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); if(href && text) { auto item = std::make_unique<BodyItem>(text); item->url = href; - item_data->result_items.push_back(std::move(item)); - } - }, &item_data); - if (result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class=\"story_item\"]//img", - [](QuickMediaHtmlNode *node, void *userdata) { - ItemData *item_data = (ItemData*)userdata; - const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src && item_data->item_index < item_data->result_items.size()) { - item_data->result_items[item_data->item_index]->thumbnail_url = src; - ++item_data->item_index; + item_data->push_back(std::move(item)); } - }, &item_data); + }, &result_items); cleanup: quickmedia_html_search_deinit(&html_search); @@ -98,11 +78,13 @@ namespace QuickMedia { for(const Json::Value &child : json_root) { if(child.isObject()) { Json::Value name = child.get("name", ""); - if(name.isString() && name.asCString()[0] != '\0') { + Json::Value nameunsigned = child.get("nameunsigned", ""); + if(name.isString() && name.asCString()[0] != '\0' && nameunsigned.isString() && nameunsigned.asCString()[0] != '\0') { std::string name_str = name.asString(); while(remove_html_span(name_str)) {} if(name_str != text) { auto item = std::make_unique<BodyItem>(name_str); + item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); result_items.push_back(std::move(item)); } } @@ -111,4 +93,54 @@ namespace QuickMedia { } return SuggestionResult::OK; } + + ImageResult Manganelo::get_image_by_index(const std::string &url, int index, std::string &image_data) { + if(url != last_chapter_url) { + printf("Get list of image urls for chapter: %s\n", url.c_str()); + last_chapter_image_urls.clear(); + ImageResult image_result = get_image_urls_for_chapter(url, last_chapter_image_urls); + if(image_result != ImageResult::OK) + return image_result; + last_chapter_url = url; + } + + int num_images = last_chapter_image_urls.size(); + if(index < 0 || index >= num_images) + return ImageResult::END; + + // TODO: Cache image in file/memory + switch(download_to_string(last_chapter_image_urls[index], image_data)) { + case DownloadResult::OK: + return ImageResult::OK; + case DownloadResult::ERR: + return ImageResult::ERR; + case DownloadResult::NET_ERR: + return ImageResult::NET_ERR; + default: + return ImageResult::ERR; + } + } + + ImageResult Manganelo::get_image_urls_for_chapter(const std::string &url, std::vector<std::string> &urls) { + std::string website_data; + if(download_to_string(url, website_data) != DownloadResult::OK) + return ImageResult::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[id='vungdoc']/img", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *urls = (std::vector<std::string>*)userdata; + const char *src = quickmedia_html_node_get_attribute_value(node, "src"); + if(src) + urls->push_back(src); + }, &urls); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? ImageResult::OK : ImageResult::ERR; + } }
\ No newline at end of file diff --git a/src/Youtube.cpp b/src/plugins/Youtube.cpp index 780244c..6cc4ac6 100644 --- a/src/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -1,4 +1,4 @@ -#include "../plugins/Youtube.hpp" +#include "../../plugins/Youtube.hpp" #include <quickmedia/HtmlSearch.h> #include <json/reader.h> #include <string.h> @@ -8,7 +8,8 @@ namespace QuickMedia { return strncmp(str, begin_with, strlen(begin_with)) == 0; } - SearchResult Youtube::search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) { + SearchResult Youtube::search(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items, Page &next_page) { + next_page = Page::SEARCH_RESULT; std::string url = "https://youtube.com/results?search_query="; url += url_param_encode(text); @@ -54,6 +55,7 @@ namespace QuickMedia { } SuggestionResult Youtube::update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) { + result_items.push_back(std::make_unique<BodyItem>(text)); std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gl=us&q="; url += url_param_encode(text); |