From 18467b4cd333ab0b7aa10b1c1acd83942c583e60 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 3 Jul 2020 16:55:19 +0200 Subject: Add tab autocomplete for youtube --- README.md | 3 +- include/SearchBar.hpp | 10 ++++++- plugins/Plugin.hpp | 2 ++ plugins/Youtube.hpp | 4 +-- src/QuickMedia.cpp | 9 +++++- src/SearchBar.cpp | 77 +++++++++++++++++++++++++++++++++++++++++++++++-- src/plugins/Youtube.cpp | 70 ++++++++++++++++++++++++++++++++++---------- 7 files changed, 151 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 2d3873e..f34ad10 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ Press `R` to paste the post number of the selected post into the post field (ima Press `Ctrl + C` to begin writing a post to a thread (image boards).\ Press `1 to 9` or `Numpad 1 to 9` to select google captcha image when posting a comment on 4chan.\ Press `P` to preview the attached item of the selected row in full screen view. Only works for image boards when browsing a thread.\ -Press `I` to switch between single image and scroll image view mode when reading manga. +Press `I` to switch between single image and scroll image view mode when reading manga.\ +Press `Tab` to autocomplete a search when autocomplete is available (currently only available for youtube).\ ## Video controls Press `space` to pause/unpause video. `Double-click` video to fullscreen or leave fullscreen. # Mangadex diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp index 7a6c483..6222da4 100644 --- a/include/SearchBar.hpp +++ b/include/SearchBar.hpp @@ -11,8 +11,8 @@ namespace QuickMedia { using TextUpdateCallback = std::function; // Return true to consume the search (clear the search field) using TextSubmitCallback = std::function; - using TextBeginTypingCallback = std::function; + using AutocompleteRequestCallback = std::function; class SearchBar { public: @@ -24,6 +24,7 @@ namespace QuickMedia { void clear(); void append_text(const std::string &text_to_add); bool is_cursor_at_start_of_line() const; + void set_to_autocomplete(); float getBottom() const; float getBottomWithoutShadow() const; @@ -31,15 +32,22 @@ namespace QuickMedia { TextUpdateCallback onTextUpdateCallback; TextSubmitCallback onTextSubmitCallback; TextBeginTypingCallback onTextBeginTypingCallback; + AutocompleteRequestCallback onAutocompleteRequestCallback; int text_autosearch_delay; + int autocomplete_search_delay; + private: + void clear_autocomplete_if_text_not_substring(); + void clear_autocomplete_if_last_char_not_substr(); private: sf::Text text; + sf::Text autocomplete_text; sf::RectangleShape background; sf::RectangleShape background_shadow; sf::RectangleShape shade; sf::Sprite plugin_logo_sprite; bool show_placeholder; bool updated_search; + bool updated_autocomplete; bool draw_logo; bool needs_update; sf::Clock time_since_search_update; diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index be70684..54ce67d 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -65,6 +65,8 @@ namespace QuickMedia { } virtual bool search_suggestions_has_thumbnails() const = 0; virtual bool search_results_has_thumbnails() const = 0; + virtual std::string autocomplete_search(const std::string &query) { return query; } + virtual int get_autocomplete_delay() const { return 100; } virtual int get_search_delay() const = 0; virtual bool search_suggestion_is_search() const { return false; } virtual Page get_page_after_search() const = 0; diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index 2eea8c2..5a88970 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -10,13 +10,13 @@ namespace QuickMedia { BodyItems get_related_media(const std::string &url) override; bool search_suggestions_has_thumbnails() const override { return true; } bool search_results_has_thumbnails() const override { return false; } + std::string autocomplete_search(const std::string &query) override; int get_search_delay() const override { return 350; } bool search_suggestion_is_search() const override { return true; } Page get_page_after_search() const override { return Page::VIDEO_CONTENT; } private: void search_suggestions_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items); private: - std::string last_related_media_playlist_id; - BodyItems last_playlist_data; + std::string last_autocomplete_result; }; } \ No newline at end of file diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 6ab8c19..0845d43 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -215,7 +215,6 @@ namespace QuickMedia { case Page::SEARCH_SUGGESTION: body->draw_thumbnails = current_plugin->search_suggestions_has_thumbnails(); search_suggestion_page(); - search_bar->onTextBeginTypingCallback = nullptr; break; #if 0 case Page::SEARCH_RESULT: @@ -400,6 +399,11 @@ namespace QuickMedia { typing = true; }; + search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); + search_bar->onAutocompleteRequestCallback = [this](const sf::String &text) { + return current_plugin->autocomplete_search(text); + }; + search_bar->onTextUpdateCallback = [&update_search_text, this, &tabs, &selected_tab, &typing](const std::string &text) { if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) update_search_text = text; @@ -496,6 +500,8 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::Right) { selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); search_bar->clear(); + } else if(event.key.code == sf::Keyboard::Tab) { + search_bar->set_to_autocomplete(); } } } @@ -575,6 +581,7 @@ namespace QuickMedia { } search_bar->onTextBeginTypingCallback = nullptr; + search_bar->onAutocompleteRequestCallback = nullptr; } void Program::search_result_page() { diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index f9b6d0e..9d8a168 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -15,14 +15,19 @@ namespace QuickMedia { onTextUpdateCallback(nullptr), onTextSubmitCallback(nullptr), onTextBeginTypingCallback(nullptr), + onAutocompleteRequestCallback(nullptr), text_autosearch_delay(0), + autocomplete_search_delay(0), text("Search...", font, 18), + autocomplete_text("", font, 18), show_placeholder(true), updated_search(false), + updated_autocomplete(false), draw_logo(false), needs_update(false) { text.setFillColor(text_placeholder_color); + autocomplete_text.setFillColor(text_placeholder_color); background.setFillColor(front_color); background_shadow.setFillColor(sf::Color(23, 25, 27)); //background_shadow.setPosition(background.getPosition() + sf::Vector2f(5.0f, 5.0f)); @@ -43,20 +48,26 @@ namespace QuickMedia { window.draw(background_shadow); window.draw(shade); window.draw(background); + // TODO: Render starting from the character after text length + window.draw(autocomplete_text); window.draw(text); if(draw_logo) window.draw(plugin_logo_sprite); } void SearchBar::update() { - if(updated_search && time_since_search_update.getElapsedTime().asMilliseconds() >= text_autosearch_delay) { - time_since_search_update.restart(); + sf::Int32 elapsed_time = time_since_search_update.getElapsedTime().asMilliseconds(); + if(updated_search && elapsed_time >= text_autosearch_delay) { updated_search = false; sf::String str = text.getString(); if(show_placeholder) str.clear(); if(onTextUpdateCallback) onTextUpdateCallback(str); + } else if(updated_autocomplete && elapsed_time >= autocomplete_search_delay) { + updated_autocomplete = false; + if(!show_placeholder && onAutocompleteRequestCallback) + autocomplete_text.setString(onAutocompleteRequestCallback(text.getString())); } } @@ -89,7 +100,9 @@ namespace QuickMedia { background.setPosition(offset_x, padding_vertical); background_shadow.setPosition(0.0f, std::floor(shade.getSize().y)); - text.setPosition(std::floor(offset_x + background_margin_horizontal), std::floor(padding_vertical + background_margin_vertical)); + sf::Vector2f font_position(std::floor(offset_x + background_margin_horizontal), std::floor(padding_vertical + background_margin_vertical)); + autocomplete_text.setPosition(font_position); + text.setPosition(font_position); } void SearchBar::onTextEntered(sf::Uint32 codepoint) { @@ -105,10 +118,14 @@ namespace QuickMedia { show_placeholder = true; text.setString("Search..."); text.setFillColor(text_placeholder_color); + autocomplete_text.setString(""); + } else { + clear_autocomplete_if_text_not_substring(); } if(!updated_search && onTextBeginTypingCallback) onTextBeginTypingCallback(); updated_search = true; + updated_autocomplete = true; time_since_search_update.restart(); } } else if(codepoint == 13) { // Return @@ -127,9 +144,11 @@ namespace QuickMedia { sf::String str = text.getString(); str += codepoint; text.setString(str); + clear_autocomplete_if_last_char_not_substr(); if(!updated_search && onTextBeginTypingCallback) onTextBeginTypingCallback(); updated_search = true; + updated_autocomplete = true; time_since_search_update.restart(); } else if(codepoint == '\n') needs_update = true; @@ -141,8 +160,10 @@ namespace QuickMedia { show_placeholder = true; text.setString("Search..."); text.setFillColor(text_placeholder_color); + autocomplete_text.setString(""); needs_update = true; updated_search = false; + updated_autocomplete = false; } void SearchBar::append_text(const std::string &text_to_add) { @@ -154,9 +175,11 @@ namespace QuickMedia { sf::String str = text.getString(); str += text_to_add; text.setString(str); + clear_autocomplete_if_text_not_substring(); if(!updated_search && onTextBeginTypingCallback) onTextBeginTypingCallback(); updated_search = true; + updated_autocomplete = true; time_since_search_update.restart(); needs_update = true; } @@ -167,6 +190,54 @@ namespace QuickMedia { return show_placeholder || str.getSize() == 0 || str[str.getSize() - 1] == '\n'; } + void SearchBar::set_to_autocomplete() { + const sf::String &autocomplete_str = autocomplete_text.getString(); + if(!autocomplete_str.isEmpty()) { + if(show_placeholder) { + show_placeholder = false; + text.setString(""); + text.setFillColor(sf::Color::White); + } + text.setString(autocomplete_str); + if(!updated_search && onTextBeginTypingCallback) + onTextBeginTypingCallback(); + updated_search = true; + updated_autocomplete = true; + time_since_search_update.restart(); + needs_update = true; + } + } + + void SearchBar::clear_autocomplete_if_text_not_substring() { + const sf::String &text_str = text.getString(); + const sf::String &autocomplete_str = autocomplete_text.getString(); + if(text_str.getSize() > autocomplete_str.getSize()) { + autocomplete_text.setString(""); + return; + } + + for(size_t i = 0; i < autocomplete_str.getSize(); ++i) { + if(text_str[i] != autocomplete_str[i]) { + autocomplete_text.setString(""); + return; + } + } + } + + void SearchBar::clear_autocomplete_if_last_char_not_substr() { + const sf::String &text_str = text.getString(); + const sf::String &autocomplete_str = autocomplete_text.getString(); + if(text_str.isEmpty() || text_str.getSize() > autocomplete_str.getSize()) { + autocomplete_text.setString(""); + return; + } + + if(autocomplete_str[text_str.getSize() - 1] != text_str[text_str.getSize() - 1]) { + autocomplete_text.setString(""); + return; + } + } + float SearchBar::getBottom() const { return shade.getSize().y + background_shadow.getSize().y; } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index c0180d8..3ab405c 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -3,6 +3,60 @@ #include namespace QuickMedia { + static void iterate_suggestion_result(const Json::Value &value, std::vector &result_items, int &iterate_count) { + ++iterate_count; + if(value.isArray()) { + for(const Json::Value &child : value) { + iterate_suggestion_result(child, result_items, iterate_count); + } + } else if(value.isString() && iterate_count > 2) { + result_items.push_back(value.asString()); + } + } + + std::string Youtube::autocomplete_search(const std::string &query) { + // Return the last result if the query is a substring of the autocomplete result + if(last_autocomplete_result.size() >= query.size() && memcmp(query.data(), last_autocomplete_result.data(), query.size()) == 0) + return last_autocomplete_result; + + std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gs_rn=64&gs_ri=youtube&ds=yt&cp=7&gs_id=x&q="; + url += url_param_encode(query); + + std::string server_response; + if(download_to_string(url, server_response, {}, use_tor, true) != DownloadResult::OK) + return query; + + size_t json_start = server_response.find_first_of('('); + if(json_start == std::string::npos) + return query; + ++json_start; + + size_t json_end = server_response.find_last_of(')'); + if(json_end == std::string::npos) + return query; + + if(json_end == 0 || json_start >= json_end) + return query; + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[json_start], &server_response[json_end], &json_root, &json_errors)) { + fprintf(stderr, "Youtube autocomplete search json error: %s\n", json_errors.c_str()); + return query; + } + + int iterate_count = 0; + std::vector result_items; + iterate_suggestion_result(json_root, result_items, iterate_count); + if(result_items.empty()) + return query; + + last_autocomplete_result = result_items[0]; + return result_items[0]; + } + static size_t find_end_of_json(const std::string &website_data, size_t data_start) { int brace_count = 0; char string_char = '\0'; @@ -228,14 +282,6 @@ namespace QuickMedia { } } - static std::string get_playlist_id_from_url(const std::string &url) { - std::string playlist_id = url; - size_t list_index = playlist_id.find("&list="); - if(list_index == std::string::npos) - return playlist_id; - return playlist_id.substr(list_index); - } - static std::string remove_index_from_playlist_url(const std::string &url) { std::string result = url; size_t index = result.rfind("&index="); @@ -280,14 +326,6 @@ namespace QuickMedia { BodyItems result_items; std::string modified_url = remove_index_from_playlist_url(url); - std::string playlist_id = get_playlist_id_from_url(modified_url); - if(playlist_id == last_related_media_playlist_id) { - result_items.reserve(last_playlist_data.size()); - for(auto &data : last_playlist_data) { - result_items.push_back(std::make_unique(*data)); - } - return result_items; - } std::string website_data; if(download_to_string(modified_url, website_data, {}, use_tor, true) != DownloadResult::OK) -- cgit v1.2.3