From 1de2ff02bb746607727900180b6f32ded0cd7856 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 23 Nov 2022 18:54:18 +0100 Subject: Allow opening 4chan post directly --- TODO | 2 +- include/BodyItem.hpp | 5 -- include/StringUtils.hpp | 2 + plugins/Fourchan.hpp | 7 ++- plugins/ImageBoard.hpp | 11 ++++- src/BodyItem.cpp | 3 -- src/QuickMedia.cpp | 123 ++++++++++++++++++++++------------------------- src/StringUtils.cpp | 9 +++- src/Text.cpp | 6 +-- src/plugins/Fourchan.cpp | 49 +++++++++++++++++-- src/plugins/Info.cpp | 10 ++++ 11 files changed, 141 insertions(+), 86 deletions(-) diff --git a/TODO b/TODO index ce73dc8..d91e19f 100644 --- a/TODO +++ b/TODO @@ -247,4 +247,4 @@ Atomic file operations should use a random generated name instead of .tmp, becau TODO: https://github.com/matrix-org/synapse/issues/14444. Use matrix /sync "since" param. Its beneficial even to quickmedia because synapse is written in such a way that using "since" is faster. /encrypt should support formatted text like greentext, custom emoji, mentions etc. -allow opening 4chan thread directly (with jumping to comment), allow navigating to cross-post/dead thread with ctrl+i, fallback to 4chan archive if thread is dead. \ No newline at end of file +allow navigating to cross-post/dead thread with ctrl+i, fallback to 4chan archive if thread is dead, allow navigating to thread post in the same thread as well using archive to bring the post back alive. \ No newline at end of file diff --git a/include/BodyItem.hpp b/include/BodyItem.hpp index 89adc5d..a2dae24 100644 --- a/include/BodyItem.hpp +++ b/include/BodyItem.hpp @@ -188,11 +188,6 @@ namespace QuickMedia { std::unique_ptr description_text; std::unique_ptr author_text; std::unique_ptr timestamp_text; // TODO: Remove - // Used by image boards for example. The elements are indices to other body items - std::vector replies_to; - // Used by image boards for example. The elements are indices to other body items - std::vector replies; - std::string post_number; void *userdata; // Not managed, should be deallocated by whoever sets this float loaded_height = 0.0f; float height = 0.0f; diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index 91de829..002b4c2 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -27,6 +27,8 @@ namespace QuickMedia { bool strncase_equals(const char *str1, const char *str2, size_t length); bool strcase_equals(const char *str1, const char *str2); // Note: does not check for overflow + bool to_num(const char *str, size_t size, int64_t &num); + // Note: does not check for overflow bool to_num(const char *str, size_t size, int &num); // Note: does not check for overflow bool to_num_hex(const char *str, size_t size, int &num); diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp index 51fe880..8622a69 100644 --- a/plugins/Fourchan.hpp +++ b/plugins/Fourchan.hpp @@ -3,6 +3,9 @@ #include "ImageBoard.hpp" namespace QuickMedia { + // |post_id| is optional + bool fourchan_extract_url(const std::string &url, std::string &board_id, std::string &thread_id, std::string &post_id); + class FourchanBoardsPage : public Page { public: FourchanBoardsPage(Program *program, std::string resources_root) : Page(program), resources_root(std::move(resources_root)) {} @@ -57,8 +60,8 @@ namespace QuickMedia { class FourchanThreadPage : public ImageBoardThreadPage { public: - FourchanThreadPage(Program *program, std::string board_id, std::string thread_id, std::string pass_id) : - ImageBoardThreadPage(program, std::move(board_id), std::move(thread_id)), pass_id(std::move(pass_id)) {} + FourchanThreadPage(Program *program, std::string board_id, std::string thread_id, std::string post_id, std::string pass_id) : + ImageBoardThreadPage(program, std::move(board_id), std::move(thread_id), std::move(post_id)), pass_id(std::move(pass_id)) {} PluginResult lazy_fetch(BodyItems &result_items) override; diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index e135532..6341622 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -22,6 +22,13 @@ namespace QuickMedia { std::string err_msg; }; + class ImageBoardBodyItemData : public BodyItemExtra { + public: + std::vector replies_to; + std::vector replies; + int64_t post_id = 0; + }; + // All fields are optional struct ImageBoardCaptchaChallenge { std::string challenge_id; @@ -32,7 +39,8 @@ namespace QuickMedia { class ImageBoardThreadPage : public LazyFetchPage { public: - ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id) : LazyFetchPage(program), board_id(std::move(board_id)), thread_id(std::move(thread_id)) {} + ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id, std::string post_id) : + LazyFetchPage(program), board_id(std::move(board_id)), thread_id(std::move(thread_id)), post_id(std::move(post_id)) {} const char* get_title() const override { return ""; } PageTypez get_type() const override { return PageTypez::IMAGE_BOARD_THREAD; } @@ -46,6 +54,7 @@ namespace QuickMedia { const std::string board_id; const std::string thread_id; + const std::string post_id; }; class ImageBoardVideoPage : public VideoPage { diff --git a/src/BodyItem.cpp b/src/BodyItem.cpp index d62efea..86f0dfb 100644 --- a/src/BodyItem.cpp +++ b/src/BodyItem.cpp @@ -43,9 +43,6 @@ namespace QuickMedia { description_text.reset(); author_text.reset(); timestamp_text.reset(); - replies_to = other.replies_to; - replies = other.replies; - post_number = other.post_number; userdata = other.userdata; loaded_height = 0.0f; loaded_image_size = mgl::vec2f(0.0f, 0.0f); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 2e7feff..ca69248 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -368,44 +368,6 @@ namespace QuickMedia { return true; } - // |comment_id| is optional - static bool fourchan_extract_url(const std::string &url, std::string &board_id, std::string &thread_id, std::string &comment_id) { - size_t len = 10; - size_t index = url.find("4chan.org/"); - if(index == std::string::npos) { - len = 13; - index = url.find("4channel.org/"); - } - - if(index == std::string::npos) - return false; - - index += len; - size_t board_end = url.find('/', index); - if(board_end == std::string::npos) - return false; - - board_id = url.substr(index, board_end - index); - index = board_end + 1; - - const std::string_view remaining(url.data() + index, url.size() - index); - if(remaining.size() <= 7 || remaining.substr(0, 7) != "thread/") - return false; - - index += 7; - size_t thread_id_end = url.find('#', index); - if(thread_id_end == std::string::npos) - thread_id_end = url.size(); - - thread_id = url.substr(index, thread_id_end - index); - if(thread_id.empty()) - return false; - - index = thread_id_end; - comment_id = url.substr(index); - return true; - } - int Program::run(int argc, char **argv) { mgl_init(); @@ -1330,11 +1292,10 @@ namespace QuickMedia { tabs.push_back(Tab{std::move(categories_sukebei_body), std::make_unique(this, true), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "4chan") == 0) { if(launch_url_type == LaunchUrlType::FOURCHAN_THREAD) { - // TODO: Use comment id - std::string board_id, thread_id, comment_id; - fourchan_extract_url(launch_url, board_id, thread_id, comment_id); + std::string board_id, thread_id, post_id; + fourchan_extract_url(launch_url, board_id, thread_id, post_id); auto body = create_body(); - auto thread_page = std::make_unique(this, std::move(board_id), std::move(thread_id), ""); + auto thread_page = std::make_unique(this, std::move(board_id), std::move(thread_id), std::move(post_id), ""); page_stack.push(current_page); current_page = PageType::IMAGE_BOARD_THREAD; image_board_thread_page(thread_page.get(), body.get()); @@ -3461,8 +3422,6 @@ namespace QuickMedia { const bool is_resume_go_back = !start_time.empty(); if(start_time.empty()) start_time = video_page->get_url_timestamp(); - - fprintf(stderr, "start time: %s\n", start_time.c_str()); watched_videos.insert(video_page->get_url()); // TODO: Sync sequences @@ -4553,6 +4512,46 @@ namespace QuickMedia { thread_body->set_items(std::move(result_items)); + std::deque comment_navigation_stack; + std::deque comment_page_scroll_stack; + + auto focus_selected_post = [&]() { + BodyItem *selected_item = thread_body->get_selected(); + if(!selected_item) + return; + + thread_body->for_each_item([](std::shared_ptr &body_item) { + body_item->visible = false; + }); + + selected_item->visible = true; + ImageBoardBodyItemData *image_board_post_data = static_cast(selected_item->extra.get()); + + for(size_t reply_to_index : image_board_post_data->replies_to) { + thread_body->get_item_by_index(reply_to_index)->visible = true; + } + + for(size_t reply_index : image_board_post_data->replies) { + thread_body->get_item_by_index(reply_index)->visible = true; + } + + comment_navigation_stack.push_back(thread_body->get_selected_item()); + comment_page_scroll_stack.push_back(thread_body->get_page_scroll()); + //thread_body->clamp_selection(); + thread_body->set_page_scroll(0.0f); + }; + + int64_t navigate_to_post_id = 0; + if(!thread_page->post_id.empty() && to_num(thread_page->post_id.c_str(), thread_page->post_id.size(), navigate_to_post_id)) { + const int found_body_item_index = thread_body->find_item_index([&](auto &body_item) { + const ImageBoardBodyItemData *image_board_post_data = static_cast(body_item->extra.get()); + return image_board_post_data->post_id == navigate_to_post_id; + }); + + if(found_body_item_index != -1) + thread_body->set_selected_item(found_body_item_index); + } + // TODO: Instead of using stage here, use different pages for each stage enum class NavigationStage { VIEWING_COMMENTS, @@ -4713,9 +4712,6 @@ namespace QuickMedia { mgl::vec2f body_size; mgl::Event event; - std::deque comment_navigation_stack; - std::deque comment_page_scroll_stack; - bool moving_captcha_left = false; bool moving_captcha_right = false; @@ -4866,21 +4862,9 @@ namespace QuickMedia { } BodyItem *selected_item = thread_body->get_selected(); - if(event.key.code == mgl::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || thread_body->get_selected_item() != comment_navigation_stack.back()) && (!selected_item->replies_to.empty() || !selected_item->replies.empty())) { - thread_body->for_each_item([](std::shared_ptr &body_item) { - body_item->visible = false; - }); - selected_item->visible = true; - for(size_t reply_to_index : selected_item->replies_to) { - thread_body->get_item_by_index(reply_to_index)->visible = true; - } - for(size_t reply_index : selected_item->replies) { - thread_body->get_item_by_index(reply_index)->visible = true; - } - comment_navigation_stack.push_back(thread_body->get_selected_item()); - comment_page_scroll_stack.push_back(thread_body->get_page_scroll()); - //thread_body->clamp_selection(); - thread_body->set_page_scroll(0.0f); + ImageBoardBodyItemData *image_board_post_data = static_cast(selected_item->extra.get()); + if(event.key.code == mgl::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || thread_body->get_selected_item() != comment_navigation_stack.back()) && (!image_board_post_data->replies_to.empty() || !image_board_post_data->replies.empty())) { + focus_selected_post(); } else if(event.key.code == mgl::Keyboard::Backspace && !comment_navigation_stack.empty()) { size_t previous_selected = comment_navigation_stack.back(); float previous_page_scroll = comment_page_scroll_stack.back(); @@ -4899,17 +4883,17 @@ namespace QuickMedia { thread_body->set_selected_item(previous_selected); selected_item = thread_body->get_item_by_index(comment_navigation_stack.back()).get(); selected_item->visible = true; - for(size_t reply_to_index : selected_item->replies_to) { + for(size_t reply_to_index : image_board_post_data->replies_to) { thread_body->get_item_by_index(reply_to_index)->visible = true; } - for(size_t reply_index : selected_item->replies) { + for(size_t reply_index : image_board_post_data->replies) { thread_body->get_item_by_index(reply_index)->visible = true; } thread_body->clamp_selection(); } thread_body->set_page_scroll(previous_page_scroll); } else if(event.key.code == mgl::Keyboard::R && selected_item) { - std::string text_to_add = ">>" + selected_item->post_number + "\n"; + std::string text_to_add = ">>" + std::to_string(image_board_post_data->post_id) + "\n"; comment_input.insert_text_at_caret_position(std::move(text_to_add)); comment_input.move_caret_to_end(); } @@ -6578,6 +6562,7 @@ namespace QuickMedia { std::string youtube_channel_id; std::string youtube_channel_url; std::string video_id; + std::string board_id, thread_id, post_id; if(youtube_url_extract_channel_id(url, youtube_channel_id, youtube_channel_url)) { std::vector tabs; YoutubeChannelPage::create_each_type(this, youtube_channel_url, "", "Channel", tabs); @@ -6593,6 +6578,14 @@ namespace QuickMedia { video_content_page(matrix_chat_page, youtube_video_page.get(), "", false, tabs[MESSAGES_TAB_INDEX].body.get(), tabs[MESSAGES_TAB_INDEX].body->get_selected_item()); redraw = true; avatar_applied = false; + } else if(fourchan_extract_url(url, board_id, thread_id, post_id)) { + auto body = create_body(); + auto thread_page = std::make_unique(this, std::move(board_id), std::move(thread_id), std::move(post_id), ""); + page_stack.push(current_page); + current_page = PageType::IMAGE_BOARD_THREAD; + image_board_thread_page(thread_page.get(), body.get()); + redraw = true; + avatar_applied = false; } else { const char *launch_program = "xdg-open"; if(!is_program_executable_by_name("xdg-open")) { diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 564f307..656b511 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -210,7 +210,7 @@ namespace QuickMedia { } } - bool to_num(const char *str, size_t size, int &num) { + bool to_num(const char *str, size_t size, int64_t &num) { if(size == 0) return false; @@ -233,6 +233,13 @@ namespace QuickMedia { return true; } + bool to_num(const char *str, size_t size, int &num) { + int64_t num_i64 = 0; + const bool res = to_num(str, size, num_i64); + num = num_i64; + return res; + } + bool to_num_hex(const char *str, size_t size, int &num) { if(size == 0) return false; diff --git a/src/Text.cpp b/src/Text.cpp index 28247e6..74a0625 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -770,7 +770,7 @@ namespace QuickMedia const float latin_font_width = latin_font->get_glyph(' ').advance; const float vspace = font_get_real_height(latin_font); const float hspace_latin = latin_font_width + characterSpacing; - const float emoji_spacing = 2.0f; + const float emoji_spacing = std::max(1, int(vspace / 10.0f)); int hspace_monospace = 0; const mgl::Color url_color = get_theme().url_text_color; @@ -814,7 +814,7 @@ namespace QuickMedia int vertexStart = vertices[vertices_index].size(); if(prevCodePoint != 0) - glyphPos.x += emoji_spacing; + glyphPos.x += emoji_spacing + characterSpacing; const float font_height_offset = vspace; mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - textElement.size.y); @@ -854,7 +854,7 @@ namespace QuickMedia const mgl::vec2f emoji_size = { vspace, vspace }; if(prevCodePoint != 0) - glyphPos.x += emoji_spacing; + glyphPos.x += emoji_spacing + characterSpacing; const float font_height_offset = floor(vspace * 0.5f); mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - emoji_size.y * 0.5f); diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index cf229d4..1ce97c4 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -19,6 +19,43 @@ static const std::string fourchan_image_url = "https://i.4cdn.org/"; static const char *SERVICE_NAME = "4chan"; namespace QuickMedia { + bool fourchan_extract_url(const std::string &url, std::string &board_id, std::string &thread_id, std::string &post_id) { + size_t len = 10; + size_t index = url.find("4chan.org/"); + if(index == std::string::npos) { + len = 13; + index = url.find("4channel.org/"); + } + + if(index == std::string::npos) + return false; + + index += len; + size_t board_end = url.find('/', index); + if(board_end == std::string::npos) + return false; + + board_id = url.substr(index, board_end - index); + index = board_end + 1; + + const std::string_view remaining(url.data() + index, url.size() - index); + if(remaining.size() <= 7 || remaining.substr(0, 7) != "thread/") + return false; + + index += 7; + size_t thread_id_end = url.find("#p", index); + if(thread_id_end == std::string::npos) + thread_id_end = url.size(); + + thread_id = url.substr(index, thread_id_end - index); + if(thread_id.empty()) + return false; + + index = thread_id_end + 2; + post_id = url.substr(index); + return true; + } + // Returns empty string on failure to read cookie static std::string get_pass_id_from_cookies_file(const Path &cookies_filepath) { std::string file_content; @@ -271,9 +308,9 @@ namespace QuickMedia { // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) comment_text += Text::formatted_text(std::move(cp.text) + " (DEAD)", get_theme().attention_alert_text_color, FORMATTED_TEXT_FLAG_COLOR); } else { - result_items[body_item_index]->replies_to.push_back(it->second); - result_items[it->second]->replies.push_back(body_item_index); - result_items[it->second]->add_reaction(">>" + result_items[body_item_index]->post_number, nullptr, get_theme().replies_text_color); + static_cast(result_items[body_item_index]->extra.get())->replies_to.push_back(it->second); + static_cast(result_items[it->second]->extra.get())->replies.push_back(body_item_index); + result_items[it->second]->add_reaction(">>" + std::to_string(static_cast(result_items[body_item_index]->extra.get())->post_id), nullptr, get_theme().replies_text_color); if(it->second == 0) comment_text += Text::formatted_text(std::move(cp.text) + " (OP)", get_theme().attention_alert_text_color, FORMATTED_TEXT_FLAG_COLOR); else @@ -391,7 +428,7 @@ namespace QuickMedia { } PluginResult FourchanThreadListPage::submit(const SubmitArgs &args, std::vector &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique(program, board_id, args.url, pass_id), nullptr}); + result_tabs.push_back(Tab{create_body(), std::make_unique(program, board_id, args.url, "", pass_id), nullptr}); return PluginResult::OK; } @@ -500,8 +537,10 @@ namespace QuickMedia { int64_t post_num_int = post_num.asInt64(); comment_by_postno[post_num_int] = result_items.size(); + auto image_board_post_data = std::make_shared(); + image_board_post_data->post_id = post_num_int; result_items.push_back(BodyItem::create("")); - result_items.back()->post_number = std::to_string(post_num_int); + result_items.back()->extra = std::move(image_board_post_data); } size_t body_item_index = 0; diff --git a/src/plugins/Info.cpp b/src/plugins/Info.cpp index 1ad7320..d4ee5f1 100644 --- a/src/plugins/Info.cpp +++ b/src/plugins/Info.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Info.hpp" #include "../../plugins/Saucenao.hpp" #include "../../plugins/Youtube.hpp" +#include "../../plugins/Fourchan.hpp" #include "../../include/StringUtils.hpp" #include "../../include/Program.hpp" #include "../../include/Notification.hpp" @@ -40,6 +41,7 @@ namespace QuickMedia { } PluginResult InfoPage::submit(const SubmitArgs &args, std::vector &result_tabs) { + std::string board_id, thread_id, post_id; if(string_starts_with(args.url, REVERSE_IMAGE_SEARCH_URL)) { std::string image_url = args.url.substr(strlen(REVERSE_IMAGE_SEARCH_URL)); result_tabs.push_back(Tab{create_body(), std::make_unique(program, image_url, false), nullptr}); @@ -54,6 +56,9 @@ namespace QuickMedia { } else if(is_youtube_url(args.url)) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url, false), nullptr}); return PluginResult::OK; + } else if(fourchan_extract_url(args.url, board_id, thread_id, post_id)) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, std::move(board_id), std::move(thread_id), std::move(post_id), ""), nullptr}); + return PluginResult::OK; } else { return open_with_browser(args.url); } @@ -75,13 +80,18 @@ namespace QuickMedia { // static std::shared_ptr InfoPage::add_url(const std::string &url) { + std::string board_id, thread_id, comment_id; std::string title; + if(is_youtube_channel_url(url)) title = "Open youtube channel " + url; else if(is_youtube_url(url)) title = "Play " + url; + else if(fourchan_extract_url(url, board_id, thread_id, comment_id)) + title = "Open " + url; else title = "Open " + url + " in a browser"; + auto body_item = BodyItem::create(std::move(title)); body_item->url = url; return body_item; -- cgit v1.2.3