From e67b9899feb72027b246e3b63ce5aa0ccae2dd16 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 30 Nov 2019 20:23:07 +0100 Subject: Add image board (4chan) comment navigation --- src/Body.cpp | 6 +- src/QuickMedia.cpp | 186 +++++++++++++++++++++++++++++ src/plugins/Fourchan.cpp | 304 +++++++++++++++++++++++++++++------------------ 3 files changed, 376 insertions(+), 120 deletions(-) (limited to 'src') diff --git a/src/Body.cpp b/src/Body.cpp index 4976a1f..078c4fc 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -104,8 +104,10 @@ namespace QuickMedia { thumbnail_load_thread = std::thread([this, result, url]() { std::string texture_data; if(program->get_current_plugin()->download_to_string(url, texture_data) == DownloadResult::OK) { - if(result->loadFromMemory(texture_data.data(), texture_data.size())) - result->generateMipmap(); + if(result->loadFromMemory(texture_data.data(), texture_data.size())) { + //result->generateMipmap(); + result->setSmooth(true); + } } loading_thumbnail = false; }); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c85f0c0..aea8d60 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -209,7 +209,18 @@ namespace QuickMedia { content_details_page(); break; } + case Page::IMAGE_BOARD_THREAD_LIST: { + body->draw_thumbnails = true; + image_board_thread_list_page(); + break; + } + case Page::IMAGE_BOARD_THREAD: { + body->draw_thumbnails = true; + image_board_thread_page(); + break; + } default: + fprintf(stderr, "Page not implemented: %d\n", current_page); window.close(); break; } @@ -420,6 +431,8 @@ namespace QuickMedia { next_page = Page::SEARCH_RESULT; } 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; @@ -1306,6 +1319,71 @@ namespace QuickMedia { sf::Event event; while (current_page == Page::CONTENT_DETAILS) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::CONTENT_LIST); + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) + redraw = true; + } + + // TODO: This code is duplicated in many places. Handle it in one place. + if(redraw) { + redraw = false; + 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_bottom); + } + + search_bar->update(); + + window.clear(back_color); + body->draw(window, body_pos, body_size); + search_bar->draw(window); + window.display(); + } + } + + void Program::image_board_thread_list_page() { + assert(current_plugin->is_image_board()); + ImageBoard *image_board = static_cast(current_plugin); + if(image_board->get_threads(image_board_thread_list_url, body->items) != PluginResult::OK) { + show_notification("Content list", "Failed to get threads for url: " + image_board_thread_list_url, Urgency::CRITICAL); + current_page = Page::SEARCH_SUGGESTION; + return; + } + + search_bar->onTextUpdateCallback = [this](const std::string &text) { + body->filter_search_fuzzy(text); + body->select_first_item(); + }; + + search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return false; + + content_episode = selected_item->title; + content_url = selected_item->url; + current_page = Page::IMAGE_BOARD_THREAD; + body->clear_items(); + return true; + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool redraw = true; + sf::Event event; + + while (current_page == Page::IMAGE_BOARD_THREAD_LIST) { while (window.pollEvent(event)) { base_event_handler(event, Page::SEARCH_SUGGESTION); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) @@ -1338,4 +1416,112 @@ namespace QuickMedia { window.display(); } } + + void Program::image_board_thread_page() { + assert(current_plugin->is_image_board()); + ImageBoard *image_board = static_cast(current_plugin); + if(image_board->get_thread_comments(image_board_thread_list_url, content_url, body->items) != PluginResult::OK) { + show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); + // TODO: This will return to an empty content list. + // Each page should have its own @Body so we can return to the last page and still have the data loaded + // however the cached images should be cleared. + current_page = Page::IMAGE_BOARD_THREAD_LIST; + return; + } + + // Instead of using search bar to searching, use it for commenting. + // TODO: Have an option for the search bar to be multi-line. + search_bar->onTextUpdateCallback = nullptr; + search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + if(text.empty()) + return false; + + return true; + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool redraw = true; + sf::Event event; + + std::stack comment_navigation_stack; + comment_navigation_stack.push(body); + + while (current_page == Page::IMAGE_BOARD_THREAD) { + while (window.pollEvent(event)) { + 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)); + } + + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) + redraw = true; + else if(event.type == sf::Event::KeyPressed) { + Body *current_body = comment_navigation_stack.top(); + if(event.key.code == sf::Keyboard::Up) { + current_body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + current_body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::IMAGE_BOARD_THREAD_LIST; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } + + BodyItem *selected_item = current_body->get_selected(); + if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.size() == 1 || current_body->selected_item != 0) && !selected_item->replies.empty()) { + // TODO: Optimize this by making body items shared_ptr instead of unique_ptr + Body *new_body = new Body(this, font); + new_body->draw_thumbnails = true; + new_body->items.push_back(std::make_unique(*selected_item)); + for(size_t reply_index : selected_item->replies) { + new_body->items.push_back(std::make_unique(*body->items[reply_index])); + } + comment_navigation_stack.push(new_body); + } else if(event.key.code == sf::Keyboard::BackSpace && comment_navigation_stack.size() > 1) { + delete comment_navigation_stack.top(); + comment_navigation_stack.pop(); + } + } + } + + // TODO: This code is duplicated in many places. Handle it in one place. + if(redraw) { + redraw = false; + 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_bottom); + } + + //search_bar->update(); + + window.clear(back_color); + comment_navigation_stack.top()->draw(window, body_pos, body_size); + //search_bar->draw(window); + window.display(); + } + + // We dont delete the first item since it's a reference to the global body, which we dont want to delete + // as it's also used for other pages. + // TODO: Remove the first item as well when each page has their own body + while(comment_navigation_stack.size() > 1) { + delete comment_navigation_stack.top(); + comment_navigation_stack.pop(); + } + } } \ No newline at end of file diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 01039b9..5a8cada 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Fourchan.hpp" #include #include +#include "../../include/DataView.hpp" #include #include @@ -348,11 +349,115 @@ namespace QuickMedia { } static bool string_ends_with(const std::string &str, const std::string &ends_with_str) { - size_t len = ends_with_str.size(); - return len == 0 || (str.size() >= len && memcmp(&str[str.size() - len], ends_with_str.data(), len) == 0); + size_t ends_len = ends_with_str.size(); + return ends_len == 0 || (str.size() >= ends_len && memcmp(&str[str.size() - ends_len], ends_with_str.data(), ends_len) == 0); } - PluginResult Fourchan::get_content_list(const std::string &url, BodyItems &result_items) { + struct CommentPiece { + enum class Type { + TEXT, + QUOTE, // > + QUOTELINK, // >>POSTNO, + LINEBREAK + }; + + DataView text; // Set when type is TEXT, QUOTE or QUOTELINK + int64_t quote_postnumber; // Set when type is QUOTELINK + Type type; + }; + + static TidyAttr get_attribute_by_name(TidyNode node, const char *name) { + for(TidyAttr attr = tidyAttrFirst(node); attr; attr = tidyAttrNext(attr)) { + const char *attr_name = tidyAttrName(attr); + if(attr_name && strcmp(name, attr_name) == 0) + return attr; + } + return nullptr; + } + + static const char* get_attribute_value(TidyNode node, const char *name) { + TidyAttr attr = get_attribute_by_name(node, name); + if(attr) + return tidyAttrValue(attr); + return nullptr; + } + + using CommentPieceCallback = std::function; + static void extract_comment_pieces(TidyDoc doc, TidyNode node, CommentPieceCallback callback) { + for(TidyNode child = tidyGetChild(node); child; child = tidyGetNext(child)) { + //extract_comment_pieces(doc, child, callback); + const char *node_name = tidyNodeGetName(child); + TidyNodeType node_type = tidyNodeGetType(child); + if(node_type == TidyNode_Start && node_name) { + TidyNode text_node = tidyGetChild(child); + //fprintf(stderr, "Child node name: %s, child text type: %d\n", node_name, tidyNodeGetType(text_node)); + if(tidyNodeGetType(text_node) == TidyNode_Text) { + TidyBuffer tidy_buffer; + tidyBufInit(&tidy_buffer); + if(tidyNodeGetText(doc, text_node, &tidy_buffer)) { + CommentPiece comment_piece; + comment_piece.type = CommentPiece::Type::TEXT; + comment_piece.text = { (char*)tidy_buffer.bp, tidy_buffer.size }; + if(strcmp(node_name, "span") == 0) { + const char *span_class = get_attribute_value(child, "class"); + //fprintf(stderr, "span class: %s\n", span_class); + if(span_class && strcmp(span_class, "quote") == 0) + comment_piece.type = CommentPiece::Type::QUOTE; + } else if(strcmp(node_name, "a") == 0) { + const char *a_class = get_attribute_value(child, "class"); + const char *a_href = get_attribute_value(child, "href"); + //fprintf(stderr, "a class: %s, href: %s\n", a_class, a_href); + if(a_class && a_href && strcmp(a_class, "quotelink") == 0 && strncmp(a_href, "#p", 2) == 0) { + comment_piece.type = CommentPiece::Type::QUOTELINK; + comment_piece.quote_postnumber = strtoll(a_href + 2, nullptr, 10); + } + } + /* + for(size_t i = 0; i < comment_piece.text.size; ++i) { + if(comment_piece.text.data[i] == '\n') + comment_piece.text.data[i] = ' '; + } + */ + callback(comment_piece); + } + tidyBufFree(&tidy_buffer); + } + } else if(node_type == TidyNode_Text) { + TidyBuffer tidy_buffer; + tidyBufInit(&tidy_buffer); + if(tidyNodeGetText(doc, child, &tidy_buffer)) { + CommentPiece comment_piece; + comment_piece.type = CommentPiece::Type::TEXT; + comment_piece.text = { (char*)tidy_buffer.bp, tidy_buffer.size }; + /* + for(size_t i = 0; i < comment_piece.text.size; ++i) { + if(comment_piece.text.data[i] == '\n') + comment_piece.text.data[i] = ' '; + } + */ + callback(comment_piece); + } + tidyBufFree(&tidy_buffer); + } + } + } + + static void extract_comment_pieces(const char *html_source, size_t size, CommentPieceCallback callback) { + TidyDoc doc = tidyCreate(); + tidyOptSetBool(doc, TidyShowWarnings, no); + if(tidyParseString(doc, html_source) < 0) { + CommentPiece comment_piece; + comment_piece.type = CommentPiece::Type::TEXT; + // Warning: Cast from const char* to char* ... + comment_piece.text = { (char*)html_source, size }; + callback(comment_piece); + } else { + extract_comment_pieces(doc, tidyGetBody(doc), std::move(callback)); + } + tidyRelease(doc); + } + + PluginResult Fourchan::get_threads(const std::string &url, BodyItems &result_items) { std::string server_response; if(download_to_string(fourchan_url + url + "/catalog.json", server_response) != DownloadResult::OK) return PluginResult::NET_ERR; @@ -380,14 +485,38 @@ namespace QuickMedia { continue; const Json::Value &com = thread["com"]; - if(!com.isString()) - continue; + const char *comment_begin = ""; + const char *comment_end = comment_begin; + com.getString(&comment_begin, &comment_end); const Json::Value &thread_num = thread["no"]; if(!thread_num.isNumeric()) continue; - auto body_item = std::make_unique(com.asString()); + std::string comment_text; + extract_comment_pieces(comment_begin, comment_end - comment_begin, + [&comment_text](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + break; + } + case CommentPiece::Type::LINEBREAK: + // comment_text += '\n'; + break; + } + } + ); + html_unescape_sequences(comment_text); + auto body_item = std::make_unique(std::move(comment_text)); body_item->url = std::to_string(thread_num.asInt64()); const Json::Value &ext = thread["ext"]; @@ -411,95 +540,7 @@ namespace QuickMedia { return PluginResult::OK; } - struct CommentPiece { - enum class Type { - TEXT, - QUOTE, // > - QUOTELINK, // >>POSTNO, - LINEBREAK - }; - - std::string_view text; // Set when type is TEXT, QUOTE or QUOTELINK - int64_t quote_postnumber; // Set when type is QUOTELINK - Type type; - }; - - static TidyAttr get_attribute_by_name(TidyNode node, const char *name) { - for(TidyAttr attr = tidyAttrFirst(node); attr; attr = tidyAttrNext(attr)) { - const char *attr_name = tidyAttrName(attr); - if(attr_name && strcmp(name, attr_name) == 0) - return attr; - } - return nullptr; - } - - static const char* get_attribute_value(TidyNode node, const char *name) { - TidyAttr attr = get_attribute_by_name(node, name); - if(attr) - return tidyAttrValue(attr); - return nullptr; - } - - using CommentPieceCallback = std::function; - static void extract_comment_pieces(TidyDoc doc, TidyNode node, CommentPieceCallback callback) { - const char *node_name = tidyNodeGetName(node); - printf("node name: %s\n", node_name ? node_name : "N/A"); - - TidyNodeType node_type = tidyNodeGetType(node); - if(node_type == TidyNode_Text) { - TidyBuffer tidy_buffer; - tidyBufInit(&tidy_buffer); - if(tidyNodeGetText(doc, node, &tidy_buffer)) { - CommentPiece comment_piece; - comment_piece.type = CommentPiece::Type::TEXT; - if(node_name) { - if(strncmp("a", (const char*)tidy_buffer.bp, tidy_buffer.size) == 0) { - const char *a_href = get_attribute_value(node, "href"); - if(a_href && strncmp(a_href, "#p", 2) == 0) { - comment_piece.type = CommentPiece::Type::QUOTELINK; - comment_piece.quote_postnumber = strtoll(a_href + 2, nullptr, 10); - } - } else if(strncmp("span", (const char*)tidy_buffer.bp, tidy_buffer.size) == 0) { - const char *a_class = get_attribute_value(node, "class"); - if(a_class && strcmp(a_class, "quote") == 0) { - comment_piece.type = CommentPiece::Type::QUOTE; - comment_piece.text = { (const char*)tidy_buffer.bp, tidy_buffer.size }; - } - } - } - printf("Comment piece value: %.*s\n", tidy_buffer.size, tidy_buffer.bp); - comment_piece.text = { (const char*)tidy_buffer.bp, tidy_buffer.size }; - callback(comment_piece); - } - tidyBufFree(&tidy_buffer); - } else if(node_name) { - if(strcmp("br", node_name) == 0) { - CommentPiece comment_piece; - comment_piece.type = CommentPiece::Type::LINEBREAK; - callback(comment_piece); - } - } - - for(TidyNode child = tidyGetChild(node); child; child = tidyGetNext(child)) { - extract_comment_pieces(doc, child, callback); - } - } - - static void extract_comment_pieces(const char *html_source, size_t size, CommentPieceCallback callback) { - TidyDoc doc = tidyCreate(); - tidyOptSetBool(doc, TidyShowWarnings, no); - if(tidyParseString(doc, html_source) < 0) { - CommentPiece comment_piece; - comment_piece.type = CommentPiece::Type::TEXT; - comment_piece.text = { html_source, size }; - callback(comment_piece); - } else { - extract_comment_pieces(doc, tidyGetBody(doc), std::move(callback)); - } - tidyRelease(doc); - } - - PluginResult Fourchan::get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) { + PluginResult Fourchan::get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) { std::string server_response; if(download_to_string(fourchan_url + list_url + "/thread/" + url + ".json", server_response) != DownloadResult::OK) return PluginResult::NET_ERR; @@ -513,43 +554,70 @@ namespace QuickMedia { return PluginResult::ERR; } + std::unordered_map comment_by_postno; + const Json::Value &posts = json_root["posts"]; if(posts.isArray()) { for(const Json::Value &post : posts) { if(!post.isObject()) continue; - const Json::Value &com = post["com"]; - const char *comment_begin; - const char *comment_end; - if(!com.getString(&comment_begin, &comment_end)) + const Json::Value &post_num = post["no"]; + if(!post_num.isNumeric()) continue; + + comment_by_postno[post_num.asInt64()] = result_items.size(); + result_items.push_back(std::make_unique("")); + } + } + + size_t body_item_index = 0; + if(posts.isArray()) { + for(const Json::Value &post : posts) { + if(!post.isObject()) + continue; + + const Json::Value &com = post["com"]; + const char *comment_begin = ""; + const char *comment_end = comment_begin; + com.getString(&comment_begin, &comment_end); const Json::Value &post_num = post["no"]; if(!post_num.isNumeric()) continue; std::string comment_text; - extract_comment_pieces(comment_begin, comment_end - comment_begin, [&comment_text](const CommentPiece &cp) { - switch(cp.type) { - case CommentPiece::Type::TEXT: - comment_text += cp.text; - break; - case CommentPiece::Type::QUOTE: - comment_text += '>'; - comment_text += cp.text; - break; - case CommentPiece::Type::QUOTELINK: - comment_text += cp.text; - break; - case CommentPiece::Type::LINEBREAK: - // comment_text += '\n'; - break; + extract_comment_pieces(comment_begin, comment_end - comment_begin, + [&comment_text, &comment_by_postno, &result_items, body_item_index](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + auto it = comment_by_postno.find(cp.quote_postnumber); + if(it == comment_by_postno.end()) { + // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) + comment_text += "(dead)"; + } else { + result_items[it->second]->replies.push_back(body_item_index); + } + break; + } + case CommentPiece::Type::LINEBREAK: + // comment_text += '\n'; + break; + } } - }); + ); html_unescape_sequences(comment_text); - auto body_item = std::make_unique(std::move(comment_text)); - body_item->url = std::to_string(post_num.asInt64()); + BodyItem *body_item = result_items[body_item_index].get(); + body_item->title = std::move(comment_text); const Json::Value &ext = post["ext"]; const Json::Value &tim = post["tim"]; @@ -564,7 +632,7 @@ namespace QuickMedia { body_item->thumbnail_url = fourchan_image_url + list_url + "/" + std::to_string(tim.asInt64()) + "s.jpg"; } - result_items.emplace_back(std::move(body_item)); + ++body_item_index; } } -- cgit v1.2.3