diff options
Diffstat (limited to 'src/QuickMedia.cpp')
-rw-r--r-- | src/QuickMedia.cpp | 2223 |
1 files changed, 833 insertions, 1390 deletions
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 3922ceb..01e244a 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5,7 +5,6 @@ #include "../plugins/Youtube.hpp" #include "../plugins/Pornhub.hpp" #include "../plugins/Fourchan.hpp" -#include "../plugins/Dmenu.hpp" #include "../plugins/NyaaSi.hpp" #include "../plugins/Matrix.hpp" #include "../plugins/FileManager.hpp" @@ -166,13 +165,102 @@ static sf::Color interpolate_colors(sf::Color source, sf::Color target, double p } namespace QuickMedia { + class HistoryPage : public Page { + public: + HistoryPage(Program *program, Page *search_page) : Page(program), search_page(search_page) {} + const char* get_title() const override { return "History"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + return search_page->submit(title, url, result_tabs); + } + private: + Page *search_page; + }; + + class RecommendedPage : public Page { + public: + RecommendedPage(Program *program, Page *search_page) : Page(program), search_page(search_page) {} + const char* get_title() const override { return "Recommended"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + return search_page->submit(title, url, result_tabs); + } + private: + Page *search_page; + }; + + // TODO: Make asynchronous + static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) { + assert(recommended_json.isObject()); + + std::vector<std::pair<std::string, Json::Value>> recommended_items(recommended_json.size()); + /* TODO: Optimize member access */ + for(auto &member_name : recommended_json.getMemberNames()) { + Json::Value recommended_item = recommended_json[member_name]; + if(recommended_item.isObject()) + recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); + } + + /* TODO: Better algorithm for recommendations */ + std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &b) { + Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; + Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; + int64_t a_timestamp = 0; + int64_t b_timestamp = 0; + if(a_timestamp_json.isNumeric()) + a_timestamp = a_timestamp_json.asInt64(); + if(b_timestamp_json.isNumeric()) + b_timestamp = b_timestamp_json.asInt64(); + + Json::Value &a_recommended_count_json = a.second["recommended_count"]; + Json::Value &b_recommended_count_json = b.second["recommended_count"]; + int64_t a_recommended_count = 0; + int64_t b_recommended_count = 0; + if(a_recommended_count_json.isNumeric()) + a_recommended_count = a_recommended_count_json.asInt64(); + if(b_recommended_count_json.isNumeric()) + b_recommended_count = b_recommended_count_json.asInt64(); + + /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ + a_timestamp += (300 * a_recommended_count); + b_timestamp += (300 * b_recommended_count); + + return a_timestamp > b_timestamp; + }); + + for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { + const std::string &recommended_item_id = it->first; + Json::Value &recommended_item = it->second; + + int64_t watched_count = 0; + const Json::Value &watched_count_json = recommended_item["watched_count"]; + if(watched_count_json.isNumeric()) + watched_count = watched_count_json.asInt64(); + + /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ + if(watched_count != 0) + continue; + + const Json::Value &recommended_title_json = recommended_item["title"]; + if(!recommended_title_json.isString()) + continue; + + auto body_item = BodyItem::create(recommended_title_json.asString()); + body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; + body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/hqdefault.jpg"; + body_items.push_back(std::move(body_item)); + + // We dont want more than 150 recommendations + if(body_items.size() == 150) + break; + } + + std::random_shuffle(body_items.begin(), body_items.end()); + } + Program::Program() : disp(nullptr), window(sf::VideoMode(1280, 720), "QuickMedia", sf::Style::Default, sf::ContextSettings(0, 0, 0, 3, 3)), window_size(1280, 720), - body(nullptr), - current_plugin(nullptr), - current_page(Page::SEARCH_SUGGESTION), + current_page(PageType::EXIT), image_index(0) { disp = XOpenDisplay(NULL); @@ -223,10 +311,6 @@ namespace QuickMedia { abort(); } - body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - related_media_body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - related_media_body->draw_thumbnails = true; - struct sigaction action; action.sa_handler = sigpipe_handler; sigemptyset(&action.sa_mask); @@ -262,69 +346,29 @@ namespace QuickMedia { } else { running = false; } - if(related_media_body) - delete related_media_body; - if(body) - delete body; - if(file_manager) - delete file_manager; - if(current_plugin && current_plugin != file_manager) - delete current_plugin; + if(matrix) + delete matrix; if(disp) XCloseDisplay(disp); } - static SearchResult search_selected_suggestion(Body *input_body, Body *output_body, Plugin *plugin, std::string &selected_title, std::string &selected_url, bool skip_search) { - BodyItem *selected_item = input_body->get_selected(); - if(!selected_item) - return SearchResult::ERR; - - selected_title = selected_item->get_title(); - selected_url = selected_item->url; - if(!skip_search) { - output_body->clear_items(); - SearchResult search_result = plugin->search(!selected_url.empty() ? selected_url : selected_title, output_body->items); - output_body->reset_selected(); - return search_result; - } else { - return SearchResult::OK; - } - } - static void usage() { fprintf(stderr, "usage: QuickMedia <plugin> [--tor] [--no-video] [--use-system-mpv-config] [--dir <directory>] [-p <placeholder-text>]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si, matrix, file-manager or dmenu\n"); + fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si, matrix, file-manager\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --tor Use tor. Disabled by default\n"); fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n"); fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n"); fprintf(stderr, " --upscale-images-force Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n"); fprintf(stderr, " --dir Set the start directory when using file-manager\n"); - fprintf(stderr, " -p Change the placeholder text for dmenu\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, "QuickMedia manganelo\n"); fprintf(stderr, "QuickMedia youtube --tor\n"); - fprintf(stderr, "echo \"hello\\nworld\" | QuickMedia dmenu\n"); } - static bool is_program_executable_by_name(const char *name) { - // TODO: Implement for Windows. Windows also uses semicolon instead of colon as a separator - char *env = getenv("PATH"); - std::unordered_set<std::string> paths; - string_split(env, ':', [&paths](const char *str, size_t size) { - paths.insert(std::string(str, size)); - return true; - }); - - for(const std::string &path_str : paths) { - Path path(path_str); - path.join(name); - if(get_file_type(path) == FileType::REGULAR) - return true; - } - - return false; + static bool is_manga_plugin(const char *plugin_name) { + return strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangadex") == 0; } int Program::run(int argc, char **argv) { @@ -333,45 +377,40 @@ namespace QuickMedia { return -1; } - current_plugin = nullptr; std::string plugin_logo_path; - std::string search_placeholder; const char *start_dir = nullptr; + std::vector<Tab> tabs; for(int i = 1; i < argc; ++i) { - if(!current_plugin) { + if(!plugin_name) { if(strcmp(argv[i], "manganelo") == 0) { - current_plugin = new Manganelo(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/manganelo_logo.png"; } else if(strcmp(argv[i], "mangatown") == 0) { - current_plugin = new Mangatown(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/mangatown_logo.png"; } else if(strcmp(argv[i], "mangadex") == 0) { - current_plugin = new Mangadex(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/mangadex_logo.png"; } else if(strcmp(argv[i], "youtube") == 0) { - current_plugin = new Youtube(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/yt_logo_rgb_dark_small.png"; } else if(strcmp(argv[i], "pornhub") == 0) { - current_plugin = new Pornhub(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/pornhub_logo.png"; + plugin_name = argv[i]; } else if(strcmp(argv[i], "4chan") == 0) { - current_plugin = new Fourchan(resources_root); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/4chan_logo.png"; } else if(strcmp(argv[i], "nyaa.si") == 0) { - current_plugin = new NyaaSi(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/nyaa_si_logo.png"; } else if(strcmp(argv[i], "matrix") == 0) { - current_plugin = new Matrix(); + plugin_name = argv[i]; + matrix = new Matrix(); plugin_logo_path = resources_root + "images/matrix_logo.png"; } else if(strcmp(argv[i], "file-manager") == 0) { - current_plugin = new FileManager(); - } else if(strcmp(argv[i], "dmenu") == 0) { - current_plugin = new Dmenu(); - } else { - fprintf(stderr, "Invalid plugin %s\n", argv[i]); - usage(); - return -1; + plugin_name = argv[i]; } } @@ -390,11 +429,6 @@ namespace QuickMedia { start_dir = argv[i + 1]; ++i; } - } else if(strcmp(argv[i], "-p") == 0) { - if(i < argc - 1) { - search_placeholder = argv[i + 1]; - ++i; - } } else if(argv[i][0] == '-') { fprintf(stderr, "Invalid option %s\n", argv[i]); usage(); @@ -402,43 +436,19 @@ namespace QuickMedia { } } - if(!current_plugin) { + if(!plugin_name) { fprintf(stderr, "Missing plugin argument\n"); usage(); return -1; } - if(!search_placeholder.empty() && current_plugin->name == "dmenu") { - fprintf(stderr, "Option -p is only valid with dmenu\n"); - usage(); - return -1; - } - - if(current_plugin->name == "file-manager") { - current_page = Page::FILE_MANAGER; - file_manager = static_cast<FileManager*>(current_plugin); - } else { - if(start_dir) { - fprintf(stderr, "Option --dir is only valid with file-manager\n"); - usage(); - return -1; - } - } - - if(start_dir) { - if(!static_cast<FileManager*>(current_plugin)->set_current_directory(start_dir)) { - fprintf(stderr, "Invalid directory provided with --dir: %s\n", start_dir); - return -3; - } - } - if(use_tor && !is_program_executable_by_name("torsocks")) { fprintf(stderr, "torsocks needs to be installed (and accessible from PATH environment variable) when using the --tor option\n"); return -2; } if(upscale_image_action != UpscaleImageAction::NO) { - if(!current_plugin->is_manga()) { + if(!is_manga_plugin(plugin_name)) { fprintf(stderr, "Option --upscale-images/-upscale-images-force is only valid for manganelo, mangatown and mangadex\n"); return -2; } @@ -485,8 +495,13 @@ namespace QuickMedia { running = true; } - current_plugin->use_tor = use_tor; - window.setTitle("QuickMedia - " + current_plugin->name); + if(strcmp(plugin_name, "file-manager") != 0 && start_dir) { + fprintf(stderr, "Option --dir is only valid with file-manager\n"); + usage(); + return -1; + } + + window.setTitle("QuickMedia - " + std::string(plugin_name)); if(!plugin_logo_path.empty()) { if(!plugin_logo.loadFromFile(plugin_logo_path)) { @@ -497,99 +512,94 @@ namespace QuickMedia { plugin_logo.setSmooth(true); } - if(current_plugin->name == "matrix") { - Matrix *matrix = static_cast<Matrix*>(current_plugin); - if(matrix->load_and_verify_cached_session() == PluginResult::OK) { - current_page = Page::CHAT; - } else { - fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); - current_page = Page::CHAT_LOGIN; + if(strcmp(plugin_name, "manganelo") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<ManganeloSearchPage>(this), create_search_bar("Search...", 200)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mangatown") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<MangatownSearchPage>(this), create_search_bar("Search...", 200)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mangadex") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<MangadexSearchPage>(this), create_search_bar("Search...", 300)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "nyaa.si") == 0) { + auto category_page = std::make_unique<NyaaSiCategoryPage>(this); + auto categories_body = create_body(); + category_page->get_categories(categories_body->items); + tabs.push_back(Tab{std::move(categories_body), std::move(category_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "4chan") == 0) { + auto boards_page = std::make_unique<FourchanBoardsPage>(this, resources_root); + auto boards_body = create_body(); + boards_page->get_boards(boards_body->items); + tabs.push_back(Tab{std::move(boards_body), std::move(boards_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "file-manager") == 0) { + auto file_manager_page = std::make_unique<FileManagerPage>(this); + if(start_dir && !file_manager_page->set_current_directory(start_dir)) { + fprintf(stderr, "Invalid directory provided with --dir: %s\n", start_dir); + return -3; } + auto file_manager_body = create_body(); + file_manager_page->get_files_in_directory(file_manager_body->items); + tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "youtube") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<YoutubeSearchPage>(this), create_search_bar("Search...", 350)}); + + auto history_body = create_body(); + history_body->draw_thumbnails = true; + youtube_get_watch_history(history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + auto recommended_body = create_body(); + recommended_body->draw_thumbnails = true; + fill_recommended_items_from_json(load_recommended_json(), recommended_body->items); + tabs.push_back(Tab{std::move(recommended_body), std::make_unique<RecommendedPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "pornhub") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<PornhubSearchPage>(this), create_search_bar("Search...", 500)}); } - if(search_placeholder.empty()) - search_placeholder = "Search..."; + if(!tabs.empty()) { + page_loop(std::move(tabs)); + return exit_code; + } - search_bar = std::make_unique<SearchBar>(*font, &plugin_logo, search_placeholder); - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); + if(matrix) { + matrix->use_tor = use_tor; + if(matrix->load_and_verify_cached_session() == PluginResult::OK) { + current_page = PageType::CHAT; + } else { + fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); + current_page = PageType::CHAT_LOGIN; + } - while(window.isOpen()) { - switch(current_page) { - case Page::EXIT: - window.close(); - break; - case Page::SEARCH_SUGGESTION: - body->draw_thumbnails = current_plugin->search_suggestions_has_thumbnails(); - search_suggestion_page(); - body->clear_thumbnails(); - break; - case Page::VIDEO_CONTENT: - body->draw_thumbnails = false; - video_content_page(); - break; - case Page::EPISODE_LIST: - body->draw_thumbnails = false; - episode_list_page(); - body->clear_thumbnails(); - break; - case Page::IMAGES: { - body->draw_thumbnails = false; - window.setKeyRepeatEnabled(false); - window.setFramerateLimit(20); - image_page(); - body->filter_search_fuzzy(""); - if(vsync_set) - window.setFramerateLimit(0); - else - window.setFramerateLimit(monitor_hz); - window.setKeyRepeatEnabled(true); - break; - } - case Page::IMAGES_CONTINUOUS: { - body->draw_thumbnails = false; - window.setKeyRepeatEnabled(false); - image_continuous_page(); - body->filter_search_fuzzy(""); - window.setKeyRepeatEnabled(true); - break; - } - case Page::CONTENT_LIST: { - body->draw_thumbnails = true; - content_list_page(); - body->clear_thumbnails(); - break; - } - case Page::CONTENT_DETAILS: { - body->draw_thumbnails = true; - content_details_page(); - body->clear_thumbnails(); - break; - } - case Page::IMAGE_BOARD_THREAD_LIST: { - body->draw_thumbnails = true; - image_board_thread_list_page(); - body->clear_thumbnails(); - break; - } - case Page::IMAGE_BOARD_THREAD: { - body->draw_thumbnails = true; - image_board_thread_page(); - body->clear_thumbnails(); - break; - } - case Page::CHAT_LOGIN: { - chat_login_page(); - break; - } - case Page::CHAT: { - body->draw_thumbnails = true; - chat_page(); - break; - } - case Page::FILE_MANAGER: { - body->draw_thumbnails = true; - file_manager_page(); - break; + while(window.isOpen()) { + switch(current_page) { + case PageType::CHAT_LOGIN: + chat_login_page(); + break; + case PageType::CHAT: + chat_page(); + break; + default: + window.close(); + break; } } } @@ -597,9 +607,10 @@ namespace QuickMedia { return exit_code; } - void Program::base_event_handler(sf::Event &event, Page previous_page, bool handle_keypress, bool clear_on_escape, bool handle_searchbar) { + void Program::base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_keypress, bool handle_searchbar) { if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -620,13 +631,9 @@ namespace QuickMedia { body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { current_page = previous_page; - if(clear_on_escape) { - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } } } else if(handle_searchbar) { + assert(search_bar); if(event.type == sf::Event::TextEntered) search_bar->onTextEntered(event.text.unicode); search_bar->on_event(event); @@ -707,76 +714,7 @@ namespace QuickMedia { } } - // TODO: Make asynchronous - static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) { - assert(recommended_json.isObject()); - - std::vector<std::pair<std::string, Json::Value>> recommended_items(recommended_json.size()); - /* TODO: Optimize member access */ - for(auto &member_name : recommended_json.getMemberNames()) { - Json::Value recommended_item = recommended_json[member_name]; - if(recommended_item.isObject()) - recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); - } - - /* TODO: Better algorithm for recommendations */ - std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &b) { - Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; - Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; - int64_t a_timestamp = 0; - int64_t b_timestamp = 0; - if(a_timestamp_json.isNumeric()) - a_timestamp = a_timestamp_json.asInt64(); - if(b_timestamp_json.isNumeric()) - b_timestamp = b_timestamp_json.asInt64(); - - Json::Value &a_recommended_count_json = a.second["recommended_count"]; - Json::Value &b_recommended_count_json = b.second["recommended_count"]; - int64_t a_recommended_count = 0; - int64_t b_recommended_count = 0; - if(a_recommended_count_json.isNumeric()) - a_recommended_count = a_recommended_count_json.asInt64(); - if(b_recommended_count_json.isNumeric()) - b_recommended_count = b_recommended_count_json.asInt64(); - - /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ - a_timestamp += (300 * a_recommended_count); - b_timestamp += (300 * b_recommended_count); - - return a_timestamp > b_timestamp; - }); - - for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { - const std::string &recommended_item_id = it->first; - Json::Value &recommended_item = it->second; - - int64_t watched_count = 0; - const Json::Value &watched_count_json = recommended_item["watched_count"]; - if(watched_count_json.isNumeric()) - watched_count = watched_count_json.asInt64(); - - /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ - if(watched_count != 0) - continue; - - const Json::Value &recommended_title_json = recommended_item["title"]; - if(!recommended_title_json.isString()) - continue; - - auto body_item = BodyItem::create(recommended_title_json.asString()); - body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; - body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/hqdefault.jpg"; - body_items.push_back(std::move(body_item)); - - // We dont want more than 150 recommendations - if(body_items.size() == 150) - break; - } - - std::random_shuffle(body_items.begin(), body_items.end()); - } - - static Path get_video_history_filepath(Plugin *plugin) { + static Path get_video_history_filepath(const char *plugin_name) { Path video_history_dir = get_storage_dir().join("history"); if(create_directory_recursive(video_history_dir) != 0) { std::string err_msg = "Failed to create video history directory "; @@ -786,10 +724,10 @@ namespace QuickMedia { } Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin->name).append(".json"); + return video_history_filepath.join(plugin_name).append(".json"); } - static Path get_recommended_filepath(Plugin *plugin) { + static Path get_recommended_filepath(const char *plugin_name) { Path video_history_dir = get_storage_dir().join("recommended"); if(create_directory_recursive(video_history_dir) != 0) { std::string err_msg = "Failed to create recommended directory "; @@ -799,13 +737,13 @@ namespace QuickMedia { } Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin->name).append(".json"); + return video_history_filepath.join(plugin_name).append(".json"); } // This is not cached because we could have multiple instances of QuickMedia running the same plugin! // TODO: Find a way to optimize this - Json::Value Program::load_video_history_json(Plugin *plugin) { - Path video_history_filepath = get_video_history_filepath(plugin); + Json::Value Program::load_video_history_json() { + Path video_history_filepath = get_video_history_filepath(plugin_name); Json::Value json_result; if(!read_file_as_json(video_history_filepath, json_result) || !json_result.isArray()) json_result = Json::Value(Json::arrayValue); @@ -814,66 +752,62 @@ namespace QuickMedia { // This is not cached because we could have multiple instances of QuickMedia running the same plugin! // TODO: Find a way to optimize this - Json::Value Program::load_recommended_json(Plugin *plugin) { - Path recommended_filepath = get_recommended_filepath(plugin); + Json::Value Program::load_recommended_json() { + Path recommended_filepath = get_recommended_filepath(plugin_name); Json::Value json_result; if(!read_file_as_json(recommended_filepath, json_result) || !json_result.isObject()) json_result = Json::Value(Json::objectValue); return json_result; } - void Program::plugin_get_watch_history(Plugin *plugin, BodyItems &history_items) { + void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items) { // TOOD: Make generic, instead of checking for plugin - if(plugin->is_manga()) { - Path content_storage_dir = get_storage_dir().join(plugin->name); - if(create_directory_recursive(content_storage_dir) != 0) { - show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); - exit(1); - } - Path credentials_storage_dir = get_storage_dir().join("credentials"); - if(create_directory_recursive(credentials_storage_dir) != 0) { - show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); - exit(1); - } - // TODO: Make asynchronous - for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin](const std::filesystem::path &filepath) { - // This can happen when QuickMedia crashes/is killed while writing to storage. - // In that case, the storage wont be corrupt but there will be .tmp files. - // TODO: Remove these .tmp files if they exist during startup - if(filepath.extension() == ".tmp") - return true; - - Path fullpath(filepath.c_str()); - Json::Value body; - if(!read_file_as_json(fullpath, body)) { - fprintf(stderr, "Failed to read json file: %s\n", fullpath.data.c_str()); - return true; - } + Path content_storage_dir = get_storage_dir().join(plugin_name); + if(create_directory_recursive(content_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); + exit(1); + } + Path credentials_storage_dir = get_storage_dir().join("credentials"); + if(create_directory_recursive(credentials_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); + exit(1); + } + // TODO: Make asynchronous + for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin_name](const std::filesystem::path &filepath) { + // This can happen when QuickMedia crashes/is killed while writing to storage. + // In that case, the storage wont be corrupt but there will be .tmp files. + // TODO: Remove these .tmp files if they exist during startup + if(filepath.extension() == ".tmp") + return true; - auto filename = filepath.filename(); - const Json::Value &manga_name = body["name"]; - if(!filename.empty() && manga_name.isString()) { - // TODO: Add thumbnail - auto body_item = BodyItem::create(manga_name.asString()); - if(plugin->name == "manganelo") - body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); - else if(plugin->name == "mangadex") - body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); - else if(plugin->name == "mangatown") - body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); - else - fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); - history_items.push_back(std::move(body_item)); - } + Path fullpath(filepath.c_str()); + Json::Value body; + if(!read_file_as_json(fullpath, body) || !body.isObject()) { + fprintf(stderr, "Failed to read json file: %s\n", fullpath.data.c_str()); return true; - }); - return; - } + } - if(plugin->name != "youtube") - return; + auto filename = filepath.filename(); + const Json::Value &manga_name = body["name"]; + if(!filename.empty() && manga_name.isString()) { + // TODO: Add thumbnail + auto body_item = BodyItem::create(manga_name.asString()); + if(strcmp(plugin_name, "manganelo") == 0) + body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "mangadex") == 0) + body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "mangatown") == 0) + body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); + else + fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); + history_items.push_back(std::move(body_item)); + } + return true; + }); + } - fill_history_items_from_json(load_video_history_json(plugin), history_items); + void Program::youtube_get_watch_history(BodyItems &history_items) { + fill_history_items_from_json(load_video_history_json(), history_items); } static void get_body_dimensions(const sf::Vector2f &window_size, SearchBar *search_bar, sf::Vector2f &body_pos, sf::Vector2f &body_size, bool has_tabs = false) { @@ -890,7 +824,7 @@ namespace QuickMedia { if(!has_tabs) tab_h = 0.0f; - float search_bottom = search_bar->getBottomWithoutShadow(); + float search_bottom = search_bar ? search_bar->getBottomWithoutShadow() : 0.0f; body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical + tab_h); body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_h); } @@ -922,173 +856,189 @@ namespace QuickMedia { std::unique_ptr<SearchBar> password; }; - struct Tab { - Body *body; - std::unique_ptr<LoginTab> login_tab; - SearchSuggestionTab tab; - 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 || next_page == Page::CONTENT_LIST); - // 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; - } + bool Program::is_tor_enabled() { + return use_tor; + } - 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::unique_ptr<Body> Program::create_body() { + return std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get()); + } - std::string manga_id; - if(!manga_plugin->extract_id_from_url(content_url, manga_id)) - return false; + std::unique_ptr<SearchBar> Program::create_search_bar(const std::string &placeholder, int search_delay) { + auto search_bar = std::make_unique<SearchBar>(*font, &plugin_logo, placeholder); + search_bar->text_autosearch_delay = search_delay; + return search_bar; + } - 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_SUGGESTION; - else { - page_stack.push(Page::SEARCH_SUGGESTION); - } - current_page = next_page; + bool Program::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) { + Path content_storage_dir = get_storage_dir().join(service_name); + 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"] = manga_title; + FileType file_type = get_file_type(content_storage_file); + if(file_type == FileType::REGULAR) { + if(read_file_as_json(content_storage_file, content_storage_json) && content_storage_json.isObject()) + return true; 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; + } else { + return true; } - current_page = next_page; - return true; } - void Program::search_suggestion_page() { - std::string update_search_text; - bool search_text_updated = false; - bool search_running = false; - bool typing = false; - bool is_fourchan = current_plugin->name == "4chan"; - - std::string autocomplete_text; - bool autocomplete_running = false; - - Body history_body(this, font.get(), bold_font.get(), cjk_font.get()); - std::unique_ptr<Body> recommended_body; - sf::Text all_tab_text("All", *font, tab_text_size); - sf::Text history_tab_text("History", *font, tab_text_size); - sf::Text recommended_tab_text("Recommended", *font, tab_text_size); - sf::Text login_tab_text("Login", *font, tab_text_size); - SearchBar *focused_login_input = nullptr; - - if(current_plugin->name == "youtube") { - recommended_body = std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get()); - recommended_body->draw_thumbnails = true; - fill_recommended_items_from_json(load_recommended_json(current_plugin), recommended_body->items); + void Program::select_file(const std::string &filepath) { + puts(filepath.c_str()); + selected_files.push_back(filepath); + } + + void Program::page_loop(std::vector<Tab> tabs) { + if(tabs.empty()) { + show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL); + return; } - std::vector<Tab> tabs; - int selected_tab = 0; + const Json::Value *json_chapters = &Json::Value::nullSingleton(); + if(content_storage_json.isObject()) { + const Json::Value &chapters_json = content_storage_json["chapters"]; + if(chapters_json.isObject()) + json_chapters = &chapters_json; + } - auto login_submit_callback = [this, &tabs, &selected_tab](const std::string&) -> bool { - if(!tabs[selected_tab].body) { - std::string username = tabs[selected_tab].login_tab->username->get_text(); - std::string password = tabs[selected_tab].login_tab->password->get_text(); - if(current_plugin->name == "4chan") { - std::string response_msg; - PluginResult result = static_cast<Fourchan*>(current_plugin)->login(username, password, response_msg); - if(result == PluginResult::NET_ERR) { - show_notification("4chan", "Login failed!", Urgency::CRITICAL); - } else if(result == PluginResult::ERR) { - std::string desc = "Login failed, reason: "; - if(response_msg.empty()) - desc += "Unknown"; - else - desc += response_msg; - show_notification("4chan", desc, Urgency::CRITICAL); - } else if(result == PluginResult::OK) { - show_notification("4chan", "Successfully logged in!", Urgency::LOW); - selected_tab = 0; - } - } - } - return false; + struct TabAssociatedData { + std::string update_search_text; + bool search_text_updated = false; + bool search_running = false; + bool typing = false; + bool fetching_next_page_running = false; + int fetched_page = 0; + sf::Text search_result_text; + std::future<BodyItems> search_future; + std::future<BodyItems> next_page_future; }; - tabs.push_back(Tab{body, nullptr, SearchSuggestionTab::ALL, &all_tab_text}); - tabs.push_back(Tab{&history_body, nullptr, SearchSuggestionTab::HISTORY, &history_tab_text}); - if(recommended_body) - tabs.push_back(Tab{recommended_body.get(), nullptr, SearchSuggestionTab::RECOMMENDED, &recommended_tab_text}); - if(is_fourchan) { - tabs.push_back(Tab{nullptr, std::make_unique<LoginTab>(*font), SearchSuggestionTab::LOGIN, &login_tab_text}); - focused_login_input = tabs.back().login_tab->username.get(); + std::vector<TabAssociatedData> tab_associated_data; + for(size_t i = 0; i < tabs.size(); ++i) { + TabAssociatedData data; + data.search_result_text = sf::Text("", *font, 30); + tab_associated_data.push_back(std::move(data)); + } - tabs.back().login_tab->username->caret_visible = true; - tabs.back().login_tab->password->caret_visible = false; + //std::string autocomplete_text; + //bool autocomplete_running = false; - tabs.back().login_tab->username->onTextSubmitCallback = login_submit_callback; - tabs.back().login_tab->password->onTextSubmitCallback = login_submit_callback; - } + double gradient_inc = 0.0; + const float gradient_height = 5.0f; + sf::Vertex gradient_points[4]; - plugin_get_watch_history(current_plugin, history_body.items); - if(current_plugin->name == "youtube") - history_body.draw_thumbnails = true; + sf::Text tab_text("", *font, tab_text_size); + int selected_tab = 0; - search_bar->onTextBeginTypingCallback = [&typing]() { - typing = true; - }; + bool loop_running = true; - search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); - search_bar->onAutocompleteRequestCallback = [this, &tabs, &selected_tab, &autocomplete_text](const std::string &text) { - if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) - autocomplete_text = text; - }; + auto submit_handler = [this, &tabs, &selected_tab, &loop_running]() { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(!selected_item) + return; + + std::vector<Tab> new_tabs; + PluginResult submit_result = tabs[selected_tab].page->submit(selected_item->get_title(), selected_item->url, new_tabs); + if(submit_result == PluginResult::OK) { + if(tabs[selected_tab].page->is_single_page()) { + tabs[selected_tab].search_bar->clear(); + if(new_tabs.size() == 1) + tabs[selected_tab].body = std::move(new_tabs[0].body); + else + loop_running = false; + return; + } + + if(new_tabs.empty()) + return; - std::string recommended_filter; + for(Tab &tab : tabs) { + tab.body->clear_cache(); + } - search_bar->onTextUpdateCallback = [&update_search_text, &search_text_updated, this, &tabs, &selected_tab, &typing, &recommended_body, &recommended_filter](const std::string &text) { - if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) { - update_search_text = text; - search_text_updated = true; + if(new_tabs.size() == 1 && new_tabs[0].page->is_manga_images_page()) { + select_episode(selected_item, false); + Body *chapters_body = tabs[selected_tab].body.get(); + chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter + MangaImagesPage *manga_images_page = static_cast<MangaImagesPage*>(new_tabs[0].page.get()); + window.setKeyRepeatEnabled(false); + while(true) { + if(current_page == PageType::IMAGES) { + window.setFramerateLimit(20); + while(current_page == PageType::IMAGES) { + int page_navigation = image_page(manga_images_page, chapters_body); + if(page_navigation == -1) { + // TODO: Make this work if the list is sorted differently than from newest to oldest. + chapters_body->select_next_item(); + select_episode(chapters_body->get_selected(), true); + image_index = 99999; // Start at the page that shows we are at the end of the chapter + manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); + } else if(page_navigation == 1) { + // TODO: Make this work if the list is sorted differently than from newest to oldest. + chapters_body->select_previous_item(); + select_episode(chapters_body->get_selected(), true); + manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); + } + } + if(vsync_set) + window.setFramerateLimit(0); + else + window.setFramerateLimit(monitor_hz); + } else if(current_page == PageType::IMAGES_CONTINUOUS) { + image_continuous_page(manga_images_page); + } else { + break; + } + } + window.setKeyRepeatEnabled(true); + } else if(new_tabs.size() == 1 && new_tabs[0].page->is_image_board_thread_page()) { + current_page = PageType::IMAGE_BOARD_THREAD; + image_board_thread_page(static_cast<ImageBoardThreadPage*>(new_tabs[0].page.get()), new_tabs[0].body.get()); + } else if(new_tabs.size() == 1 && new_tabs[0].page->is_video_page()) { + current_page = PageType::VIDEO_CONTENT; + video_content_page(new_tabs[0].page.get(), selected_item->url, selected_item->get_title()); + } else { + page_loop(std::move(new_tabs)); + } } else { - tabs[selected_tab].body->filter_search_fuzzy(text); - tabs[selected_tab].body->select_first_item(); + // TODO: Show the exact cause of error (get error message from curl). + // TODO: Make asynchronous + show_notification("QuickMedia", std::string("Submit failed for page ") + tabs[selected_tab].page->get_title(), Urgency::CRITICAL); } - if(tabs[selected_tab].body == recommended_body.get()) - recommended_filter = text; - typing = false; }; - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string&) -> bool { - if(current_plugin->name != "dmenu") { - if(typing || tabs[selected_tab].body->no_items_visible()) - return false; - } - return on_search_suggestion_submit_text(tabs[selected_tab].body, body); - }; + for(size_t i = 0; i < tabs.size(); ++i) { + Tab &tab = tabs[i]; + TabAssociatedData &associated_data = tab_associated_data[i]; + if(!tab.search_bar) + continue; - if(current_plugin->get_front_page(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "Failed to get front page", Urgency::CRITICAL); - current_page = Page::EXIT; - return; + // tab.search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); + // tab.search_bar->onAutocompleteRequestCallback = [this, &tabs, &selected_tab, &autocomplete_text](const std::string &text) { + // if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) + // autocomplete_text = text; + // }; + + tab.search_bar->onTextUpdateCallback = [&associated_data, &tabs, i](const std::string &text) { + if(!tabs[i].page->search_is_filter()) { + associated_data.update_search_text = text; + associated_data.search_text_updated = true; + } else { + tabs[i].body->filter_search_fuzzy(text); + tabs[i].body->select_first_item(); + } + associated_data.typing = false; + }; + + tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string&) { + if(associated_data.typing) + return; + submit_handler(); + }; } - body->clamp_selection(); sf::Vector2f body_pos; sf::Vector2f body_size; @@ -1103,149 +1053,247 @@ namespace QuickMedia { sf::RoundedRectangleShape tab_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10); tab_background.setFillColor(tab_selected_color); - while (current_page == Page::SEARCH_SUGGESTION) { + sf::Clock frame_timer; + + while (window.isOpen() && loop_running) { + sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); + while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, true, tabs[selected_tab].body != nullptr); + if (event.type == sf::Event::Closed) { + window.close(); + } 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(tabs[selected_tab].search_bar) { + if(event.type == sf::Event::TextEntered) + tabs[selected_tab].search_bar->onTextEntered(event.text.unicode); + tabs[selected_tab].search_bar->on_event(event); + } + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) redraw = true; else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_next_item(); + if(event.key.code == sf::Keyboard::Down || event.key.code == sf::Keyboard::PageDown || event.key.code == sf::Keyboard::End) { + bool hit_bottom = false; + switch(event.key.code) { + case sf::Keyboard::Down: + hit_bottom = !tabs[selected_tab].body->select_next_item(); + break; + case sf::Keyboard::PageDown: + hit_bottom = !tabs[selected_tab].body->select_next_page(); + break; + case sf::Keyboard::End: + tabs[selected_tab].body->select_last_item(); + hit_bottom = true; + break; + default: + hit_bottom = false; + break; + } + if(hit_bottom && !tab_associated_data[selected_tab].search_running && !tab_associated_data[selected_tab].fetching_next_page_running && tabs[selected_tab].page) { + gradient_inc = 0.0; + tab_associated_data[selected_tab].fetching_next_page_running = true; + int next_page = tab_associated_data[selected_tab].fetched_page + 1; + Page *page = tabs[selected_tab].page.get(); + std::string update_search_text = tab_associated_data[selected_tab].update_search_text; + tab_associated_data[selected_tab].next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() { + BodyItems result_items; + if(page->get_page(update_search_text, next_page, result_items) != PluginResult::OK) + fprintf(stderr, "Failed to get next page (page %d)\n", next_page); + return result_items; + }); + } + } else if(event.key.code == sf::Keyboard::Up) { + tabs[selected_tab].body->select_previous_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::PageDown) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_next_page(); + tabs[selected_tab].body->select_previous_page(); } else if(event.key.code == sf::Keyboard::Home) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_first_item(); - } else if(event.key.code == sf::Keyboard::End) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_last_item(); + tabs[selected_tab].body->select_first_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - exit_code = 1; + goto page_end; } else if(event.key.code == sf::Keyboard::Left) { - if(tabs[selected_tab].body) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); + if(selected_tab > 0) { + tabs[selected_tab].body->clear_cache(); + --selected_tab; + redraw = true; } - selected_tab = std::max(0, selected_tab - 1); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Right) { - if(tabs[selected_tab].body) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); + if(selected_tab < (int)tabs.size() - 1) { + tabs[selected_tab].body->clear_cache(); + ++selected_tab; + redraw = true; } - selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Tab) { - if(tabs[selected_tab].body) search_bar->set_to_autocomplete(); - } - } - - if(!tabs[selected_tab].body) { - if(event.type == sf::Event::TextEntered) - focused_login_input->onTextEntered(event.text.unicode); - focused_login_input->on_event(event); - - if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Tab) { - focused_login_input->caret_visible = false; - if(focused_login_input == tabs[selected_tab].login_tab->username.get()) - focused_login_input = tabs[selected_tab].login_tab->password.get(); - else - focused_login_input = tabs[selected_tab].login_tab->username.get(); - focused_login_input->caret_visible = true; + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete(); + } else if(event.key.code == sf::Keyboard::Enter) { + if(!tabs[selected_tab].search_bar) submit_handler(); + } else if(event.key.code == sf::Keyboard::T && event.key.control) { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { + TrackablePage *trackable_page = static_cast<TrackablePage*>(tabs[selected_tab].page.get()); + TrackResult track_result = trackable_page->track(selected_item->get_title()); + // TODO: Show proper error message when this fails. For example if we are already tracking the manga + if(track_result == TrackResult::OK) { + show_notification("Media tracker", "You are now tracking \"" + trackable_page->content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); + } else { + show_notification("Media tracker", "Failed to track media \"" + trackable_page->content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); + } + } } } } if(redraw) { redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->onWindowResize(window_size); + // TODO: Dont show tabs if there is only one tab + get_body_dimensions(window_size, tabs[selected_tab].search_bar.get(), body_pos, body_size, true); + + gradient_points[0].position.x = 0.0f; + gradient_points[0].position.y = window_size.y - gradient_height; + + gradient_points[1].position.x = window_size.x; + gradient_points[1].position.y = window_size.y - gradient_height; + + gradient_points[2].position.x = window_size.x; + gradient_points[2].position.y = window_size.y; + + gradient_points[3].position.x = 0.0f; + gradient_points[3].position.y = window_size.y; } - if(tabs[selected_tab].body) - search_bar->update(); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update(); - if(search_text_updated && !search_running) { - search_suggestion_future = std::async(std::launch::async, [this, update_search_text]() { - BodyItems result; - if(current_plugin->update_search_suggestions(update_search_text, result) != SuggestionResult::OK) { - show_notification("Search", "Search failed!", Urgency::CRITICAL); + for(size_t i = 0; i < tabs.size(); ++i) { + TabAssociatedData &associated_data = tab_associated_data[i]; + + if(associated_data.fetching_next_page_running && associated_data.next_page_future.valid() && associated_data.next_page_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + BodyItems new_body_items = associated_data.next_page_future.get(); + fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", associated_data.fetched_page + 1, new_body_items.size()); + size_t num_new_messages = new_body_items.size(); + if(num_new_messages > 0) { + tabs[i].body->append_items(std::move(new_body_items)); + associated_data.fetched_page++; } - return result; - }); - update_search_text.clear(); - search_text_updated = false; - search_running = true; - } + associated_data.fetching_next_page_running = false; + } - if(search_running && search_suggestion_future.valid() && search_suggestion_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if(!search_text_updated) { - body->items = search_suggestion_future.get(); - body->select_first_item(); - } else { - search_suggestion_future.get(); + if(associated_data.search_text_updated && !associated_data.search_running && !associated_data.fetching_next_page_running) { + Page *page = tabs[i].page.get(); + std::string update_search_text = associated_data.update_search_text; + associated_data.search_future = std::async(std::launch::async, [update_search_text, page]() { + BodyItems result_items; + if(page->search(update_search_text, result_items) != SearchResult::OK) { + show_notification("QuickMedia", "Search failed!", Urgency::CRITICAL); + } + return result_items; + }); + update_search_text.clear(); + associated_data.search_text_updated = false; + associated_data.search_running = true; + associated_data.search_result_text.setString("Searching..."); + } + + if(associated_data.search_running && associated_data.search_future.valid() && associated_data.search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + if(!associated_data.search_text_updated) { + BodyItems result_items = associated_data.search_future.get(); + tabs[i].body->items = std::move(result_items); + tabs[i].body->select_first_item(); + if(tabs[i].body->items.empty()) + associated_data.search_result_text.setString("No results found"); + else + associated_data.search_result_text.setString(""); + } else { + associated_data.search_future.get(); + } + associated_data.search_running = false; } - search_running = false; } - if(!autocomplete_text.empty() && !autocomplete_running) { - autocomplete_future = std::async(std::launch::async, [this, autocomplete_text]() { - return current_plugin->autocomplete_search(autocomplete_text); - }); - autocomplete_text.clear(); - autocomplete_running = true; - } + // if(!autocomplete_text.empty() && !autocomplete_running) { + // autocomplete_future = std::async(std::launch::async, [this, autocomplete_text]() { + // return current_plugin->autocomplete_search(autocomplete_text); + // }); + // autocomplete_text.clear(); + // autocomplete_running = true; + // } - if(autocomplete_running && autocomplete_future.valid() && autocomplete_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - search_bar->set_autocomplete_text(autocomplete_future.get()); - autocomplete_running = false; - } + // if(autocomplete_running && autocomplete_future.valid() && autocomplete_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + // if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_autocomplete_text(autocomplete_future.get()); + // autocomplete_running = false; + // } window.clear(back_color); - if(tabs[selected_tab].body) - search_bar->draw(window, false); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, false); + { + float shade_extra_height = 0.0f; + if(!tabs[selected_tab].search_bar) + shade_extra_height = 10.0f; + const float width_per_tab = window_size.x / tabs.size(); tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - float tab_vertical_offset = search_bar->getBottomWithoutShadow(); - if(tabs[selected_tab].body) { - tabs[selected_tab].body->draw(window, body_pos, body_size); - } else { - tabs[selected_tab].login_tab->username->draw(window, false); - tabs[selected_tab].login_tab->password->draw(window, false); - tabs[selected_tab].login_tab->password->set_vertical_position(tabs[selected_tab].login_tab->username->getBottomWithoutShadow()); - tab_vertical_offset = tabs[selected_tab].login_tab->username->getBottomWithoutShadow() + tabs[selected_tab].login_tab->password->getBottomWithoutShadow(); - } - const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); + float tab_vertical_offset = tabs[selected_tab].search_bar ? tabs[selected_tab].search_bar->getBottomWithoutShadow() : 0.0f; + tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); + const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f) + shade_extra_height; tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); - tab_shade.setSize(sf::Vector2f(window_size.x, tab_height + 10.0f)); + tab_shade.setSize(sf::Vector2f(window_size.x, shade_extra_height + tab_height + 10.0f)); window.draw(tab_shade); int i = 0; + // TODO: Dont show tabs if there is only one tab for(Tab &tab : tabs) { if(i == selected_tab) { - tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset)); + tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset) + shade_extra_height); 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); + // TODO: Optimize. Only set once for each tab! + tab_text.setString(tab.page->get_title()); + tab_text.setPosition(std::floor(center - tab_text.getLocalBounds().width * 0.5f), tab_y); + window.draw(tab_text); ++i; } } + if(tab_associated_data[selected_tab].fetching_next_page_running) { + double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; + gradient_inc += (frame_time_ms * 0.5); + sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); + + gradient_points[0].color = back_color; + gradient_points[1].color = back_color; + gradient_points[2].color = bottom_color; + gradient_points[3].color = bottom_color; + window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl + } + + if(!tab_associated_data[selected_tab].search_result_text.getString().isEmpty()) { + auto search_result_text_bounds = tab_associated_data[selected_tab].search_result_text.getLocalBounds(); + tab_associated_data[selected_tab].search_result_text.setPosition( + std::floor(body_pos.x + body_size.x * 0.5f - search_result_text_bounds.width * 0.5f), + std::floor(body_pos.y + body_size.y * 0.5f - search_result_text_bounds.height * 0.5f)); + window.draw(tab_associated_data[selected_tab].search_result_text); + } + window.display(); } - search_bar->onTextBeginTypingCallback = nullptr; - search_bar->onAutocompleteRequestCallback = nullptr; + page_end: + // TODO: This is needed, because you cant terminate futures without causing an exception to be thrown and its not safe anyways. + // Need a way to solve this, we dont want to wait for a search to finish when navigating backwards + for(TabAssociatedData &associated_data : tab_associated_data) { + if(associated_data.next_page_future.valid()) + associated_data.next_page_future.get(); + if(associated_data.search_future.valid()) + associated_data.search_future.get(); + } } static bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) { @@ -1326,17 +1374,17 @@ namespace QuickMedia { return true; } - void Program::save_recommendations_from_related_videos() { + void Program::save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const Body *related_media_body) { std::string video_id; - if(!youtube_url_extract_id(content_url, video_id)) { + if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += content_url; + err_msg += video_url; err_msg + ", video wont be saved in recommendations"; show_notification("Video player", err_msg.c_str(), Urgency::LOW); return; } - Json::Value recommended_json = load_recommended_json(current_plugin); + Json::Value recommended_json = load_recommended_json(); time_t time_now = time(NULL); Json::Value &existing_recommended_json = recommended_json[video_id]; @@ -1349,7 +1397,7 @@ namespace QuickMedia { existing_recommended_json["watched_timestamp"] = time_now; } else { Json::Value new_content_object(Json::objectValue); - new_content_object["title"] = content_title; + new_content_object["title"] = video_title; new_content_object["recommended_timestamp"] = time_now; new_content_object["recommended_count"] = 1; new_content_object["watched_count"] = 1; @@ -1381,24 +1429,21 @@ namespace QuickMedia { break; } } else { - fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", content_url.c_str()); + fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", video_url.c_str()); } } - save_json_to_file_atomic(get_recommended_filepath(current_plugin), recommended_json); + save_json_to_file_atomic(get_recommended_filepath(plugin_name), recommended_json); } #define CLEANMASK(mask) ((mask) & (ShiftMask|ControlMask|Mod1Mask|Mod4Mask|Mod5Mask)) - void Program::video_content_page() { - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; - + void Program::video_content_page(Page *page, std::string video_url, std::string video_title) { sf::Clock time_watched_timer; bool added_recommendations = false; bool video_loaded = false; - Page previous_page = pop_page_stack(); + PageType previous_page = pop_page_stack(); std::unique_ptr<VideoPlayer> video_player; std::unique_ptr<sf::RenderWindow> related_media_window; @@ -1408,56 +1453,45 @@ namespace QuickMedia { sf::Text related_videos_text("Related videos", *bold_font, 20); const float related_videos_text_height = related_videos_text.getCharacterSize(); + auto related_media_body = create_body(); + related_media_body->draw_thumbnails = true; + sf::WindowHandle video_player_window = None; - auto on_window_create = [this, &video_player_window, &related_media_window, &related_media_window_size](sf::WindowHandle _video_player_window) mutable { + auto on_window_create = [this, &video_player_window](sf::WindowHandle _video_player_window) mutable { video_player_window = _video_player_window; - - if(!current_plugin->is_image_board()) { - related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; - related_media_window_size.y = window_size.y; - related_media_window = std::make_unique<sf::RenderWindow>(sf::VideoMode(related_media_window_size.x, related_media_window_size.y), "", 0, sf::ContextSettings(0, 0, 0, 3, 3)); - related_media_window->setFramerateLimit(0); - if(!enable_vsync(disp, related_media_window->getSystemHandle())) { - fprintf(stderr, "Failed to enable vsync, fallback to frame limiting\n"); - related_media_window->setFramerateLimit(monitor_hz); - } - related_media_window->setVisible(false); - XReparentWindow(disp, related_media_window->getSystemHandle(), video_player_window, window_size.x - related_media_window_size.x, 0); - } - XSelectInput(disp, video_player_window, KeyPressMask | PointerMotionMask); XSync(disp, False); }; - auto load_video_error_check = [this, &video_player, previous_page, &time_watched_timer, &added_recommendations]() mutable { + auto load_video_error_check = [this, &related_media_body, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &added_recommendations, page]() mutable { time_watched_timer.restart(); added_recommendations = false; - watched_videos.insert(content_url); - VideoPlayer::Error err = video_player->load_video(content_url.c_str(), window.getSystemHandle(), current_plugin->name); + watched_videos.insert(video_url); + VideoPlayer::Error err = video_player->load_video(video_url.c_str(), window.getSystemHandle(), plugin_name); if(err != VideoPlayer::Error::OK) { std::string err_msg = "Failed to play url: "; - err_msg += content_url; + err_msg += video_url; show_notification("Video player", err_msg.c_str(), Urgency::CRITICAL); current_page = previous_page; } else { related_media_body->clear_items(); related_media_body->clear_thumbnails(); - related_media_body->items = current_plugin->get_related_media(content_url); + related_media_body->items = page->get_related_media(video_url); // TODO: Make this also work for other video plugins - if(current_plugin->name != "youtube") + if(strcmp(plugin_name, "youtube") != 0) return; std::string video_id; - if(!youtube_url_extract_id(content_url, video_id)) { + if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += content_url; + err_msg += video_url; err_msg + ", video wont be saved in history"; show_notification("Video player", err_msg.c_str(), Urgency::LOW); return; } - Json::Value video_history_json = load_video_history_json(current_plugin); + Json::Value video_history_json = load_video_history_json(); int existing_index = watch_history_get_item_by_id(video_history_json, video_id.c_str()); if(existing_index != -1) { @@ -1470,18 +1504,18 @@ namespace QuickMedia { Json::Value new_content_object(Json::objectValue); new_content_object["id"] = video_id; - new_content_object["title"] = content_title; + new_content_object["title"] = video_title; new_content_object["timestamp"] = time_now; video_history_json.append(std::move(new_content_object)); - Path video_history_filepath = get_video_history_filepath(current_plugin); + Path video_history_filepath = get_video_history_filepath(plugin_name); save_json_to_file_atomic(video_history_filepath, video_history_json); } }; bool has_video_started = true; - auto video_event_callback = [this, &video_player, &load_video_error_check, previous_page, &has_video_started, &time_watched_timer, &video_loaded](const char *event_name) mutable { + auto video_event_callback = [this, &related_media_body, &video_url, &video_title, &video_player, &load_video_error_check, previous_page, &has_video_started, &time_watched_timer, &video_loaded](const char *event_name) mutable { bool end_of_file = false; if(strcmp(event_name, "pause") == 0) { double time_remaining = 9999.0; @@ -1518,8 +1552,8 @@ namespace QuickMedia { return; } - content_url = std::move(new_video_url); - content_title = std::move(new_video_title); + video_url = std::move(new_video_url); + video_title = std::move(new_video_title); load_video_error_check(); } }; @@ -1540,27 +1574,28 @@ namespace QuickMedia { bool cursor_visible = true; sf::Clock cursor_hide_timer; - bool is_youtube = current_plugin->name == "youtube"; - bool is_pornhub = current_plugin->name == "pornhub"; + bool is_youtube = strcmp(plugin_name, "youtube") == 0; + bool is_pornhub = strcmp(plugin_name, "pornhub") == 0; bool supports_url_timestamp = is_youtube || is_pornhub; - auto save_video_url_to_clipboard = [this, &video_player_window, &video_player, &supports_url_timestamp]() { + auto save_video_url_to_clipboard = [&video_url, &video_player_window, &video_player, &supports_url_timestamp]() { if(!video_player_window) return; if(supports_url_timestamp) { + // TODO: Remove timestamp (&t= or ?t=) from video_url double time_in_file; if(video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK) time_in_file = 0.0; - sf::Clipboard::setString(content_url + "&t=" + std::to_string((int)time_in_file)); + sf::Clipboard::setString(video_url + "&t=" + std::to_string((int)time_in_file)); } else { - sf::Clipboard::setString(content_url); + sf::Clipboard::setString(video_url); } }; - while (current_page == Page::VIDEO_CONTENT) { + while (current_page == PageType::VIDEO_CONTENT) { while (window.pollEvent(event)) { - base_event_handler(event, previous_page, true, false, false); + base_event_handler(event, previous_page, related_media_body.get(), nullptr, true, false); if(event.type == sf::Event::Resized && related_media_window) { related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; related_media_window_size.y = window_size.y; @@ -1605,8 +1640,8 @@ namespace QuickMedia { related_media_window->setVisible(false); has_video_started = false; - content_url = selected_item->url; - content_title = selected_item->get_title(); + video_url = selected_item->url; + video_title = selected_item->get_title(); load_video_error_check(); } else if(event.key.code == sf::Keyboard::C && event.key.control) { save_video_url_to_clipboard(); @@ -1624,7 +1659,21 @@ namespace QuickMedia { current_page = previous_page; } else if(pressed_keysym == XK_f && pressing_ctrl) { window_set_fullscreen(disp, window.getSystemHandle(), WindowFullscreenState::TOGGLE); - } else if(pressed_keysym == XK_r && pressing_ctrl && related_media_window) { + } else if(pressed_keysym == XK_r && pressing_ctrl && strcmp(plugin_name, "4chan") != 0) { + if(!related_media_window) { + related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; + related_media_window_size.y = window_size.y; + related_media_window = std::make_unique<sf::RenderWindow>(sf::VideoMode(related_media_window_size.x, related_media_window_size.y), "", 0, sf::ContextSettings(0, 0, 0, 3, 3)); + related_media_window->setFramerateLimit(0); + if(!enable_vsync(disp, related_media_window->getSystemHandle())) { + fprintf(stderr, "Failed to enable vsync, fallback to frame limiting\n"); + related_media_window->setFramerateLimit(monitor_hz); + } + related_media_window->setVisible(false); + XReparentWindow(disp, related_media_window->getSystemHandle(), video_player_window, window_size.x - related_media_window_size.x, 0); + XSync(disp, False); + } + related_media_window_visible = true; related_media_window->setVisible(related_media_window_visible); if(!cursor_visible) @@ -1663,7 +1712,7 @@ namespace QuickMedia { /* Only save recommendations for the video if we have been watching it for 15 seconds */ if(is_youtube && video_loaded && !added_recommendations && time_watched_timer.getElapsedTime().asSeconds() >= 15) { added_recommendations = true; - save_recommendations_from_related_videos(); + save_recommendations_from_related_videos(video_url, video_title, related_media_body.get()); } if(video_player_window) { @@ -1701,37 +1750,14 @@ namespace QuickMedia { window_size.y = window_size_u.y; } - enum class TrackMediaType { - RSS, - HTML - }; - - const char* track_media_type_string(TrackMediaType media_type) { - switch(media_type) { - case TrackMediaType::RSS: - return "rss"; - case TrackMediaType::HTML: - return "html"; - } - assert(false); - return ""; - } - - static int track_media(TrackMediaType media_type, const std::string &manga_title, const std::string &chapter_title, const std::string &url) { - const char *args[] = { "automedia", "add", track_media_type_string(media_type), url.data(), "--start-after", chapter_title.data(), "--name", manga_title.data(), nullptr }; - return exec_program(args, nullptr, nullptr); - } - void Program::select_episode(BodyItem *item, bool start_from_beginning) { - images_url = item->url; - chapter_title = item->get_title(); image_index = 0; switch(image_view_mode) { case ImageViewMode::SINGLE: - current_page = Page::IMAGES; + current_page = PageType::IMAGES; break; case ImageViewMode::SCROLL: - current_page = Page::IMAGES_CONTINUOUS; + current_page = PageType::IMAGES_CONTINUOUS; break; } @@ -1740,7 +1766,7 @@ namespace QuickMedia { const Json::Value &json_chapters = content_storage_json["chapters"]; if(json_chapters.isObject()) { - const Json::Value &json_chapter = json_chapters[chapter_title]; + const Json::Value &json_chapter = json_chapters[item->get_title()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -1749,199 +1775,14 @@ namespace QuickMedia { } } - Page Program::pop_page_stack() { + // TODO: Remove + PageType Program::pop_page_stack() { if(!page_stack.empty()) { - Page previous_page = page_stack.top(); + PageType previous_page = page_stack.top(); page_stack.pop(); return previous_page; } - 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() { - 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->select_first_item(); - }; - - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string&) -> 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; - } 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; - } - } - }; - - auto download_creator_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.get(), bold_font.get(), cjk_font.get()); - tab.body->draw_thumbnails = true; - tab.creator = &creator; - tab.creator_page_download_future = std::async(std::launch::async, download_creator_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_shade; - tab_shade.setFillColor(sf::Color(33, 38, 44)); - - sf::RoundedRectangleShape tab_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10); - tab_background.setFillColor(tab_selected_color); - - while (current_page == Page::EPISODE_LIST) { - while (window.pollEvent(event)) { - 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) { - 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::PageUp) { - tabs[selected_tab].body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::PageDown) { - tabs[selected_tab].body->select_next_page(); - } else if(event.key.code == sf::Keyboard::Home) { - tabs[selected_tab].body->select_first_item(); - } else if(event.key.code == sf::Keyboard::End) { - tabs[selected_tab].body->select_last_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->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); - 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->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); - selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); - } - } - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window, false); - - const float width_per_tab = window_size.x / tabs.size(); - tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), 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); - - tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); - tab_shade.setSize(sf::Vector2f(window_size.x, tab_height + 10.0f)); - window.draw(tab_shade); - - 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->select_first_item(); - } - - if(i == selected_tab) { - tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), 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; - } - - window.display(); - } - - for(EpisodeListTab &tab : tabs) { - if(tab.type == EpisodeListTabType::CREATOR) - delete tab.body; - } + return PageType::EXIT; } // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image @@ -1980,24 +1821,23 @@ namespace QuickMedia { } } - // TODO: Cancel download when navigating to another non-manga page - void Program::download_chapter_images_if_needed(Manga *image_plugin) { - if(downloading_chapter_url == images_url) + void Program::download_chapter_images_if_needed(MangaImagesPage *images_page) { + if(downloading_chapter_url == images_page->get_url()) return; - downloading_chapter_url = images_url; + downloading_chapter_url = images_page->get_url(); if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish image_download_cancel = true; image_download_future.get(); image_download_cancel = false; } - std::string chapter_url = images_url; Path content_cache_dir_ = content_cache_dir; - image_download_future = std::async(std::launch::async, [chapter_url, image_plugin, content_cache_dir_, this]() { + image_download_future = std::async(std::launch::async, [images_page, content_cache_dir_, this]() { // TODO: Download images in parallel int page = 1; - image_plugin->for_each_page_in_chapter(chapter_url, [content_cache_dir_, &page, this](const std::string &url) { + images_page->for_each_page_in_chapter([content_cache_dir_, &page, images_page, this](const std::string &url) { if(image_download_cancel) return false; @@ -2019,7 +1859,7 @@ namespace QuickMedia { return true; std::vector<CommandArg> extra_args; - if(current_plugin->name == "manganelo") { + if(strcmp(images_page->get_service_name(), "manganelo") == 0) { extra_args = { CommandArg { "-H", "accept: image/webp,image/apng,image/*,*/*;q=0.8" }, CommandArg { "-H", "sec-fetch-site: cross-site" }, @@ -2029,9 +1869,10 @@ namespace QuickMedia { }; } + // TODO: Download directly to file instead. TODO: Move to page std::string image_content; - if(download_to_string(url, image_content, extra_args, current_plugin->use_tor, true) != DownloadResult::OK || image_content.size() <= 255) { - if(current_plugin->name == "manganelo") { + if(download_to_string(url, image_content, extra_args, is_tor_enabled(), true) != DownloadResult::OK || image_content.size() <= 255) { + if(strcmp(images_page->get_service_name(), "manganelo") == 0) { bool try_backup_url = false; std::string new_url = url; if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) { @@ -2042,7 +1883,7 @@ namespace QuickMedia { if(try_backup_url) { image_content.clear(); - if(download_to_string(new_url, image_content, extra_args, current_plugin->use_tor, true) != DownloadResult::OK || image_content.size() <= 255) { + if(download_to_string(new_url, image_content, extra_args, is_tor_enabled(), true) != DownloadResult::OK || image_content.size() <= 255) { show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL); return false; } @@ -2109,40 +1950,36 @@ namespace QuickMedia { }); } - void Program::image_page() { + int Program::image_page(MangaImagesPage *images_page, Body *chapters_body) { + int page_navigation = 0; image_download_cancel = false; - 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); - assert(current_plugin->is_manga()); - Manga *image_plugin = static_cast<Manga*>(current_plugin); - std::string image_data; bool download_in_progress = false; - content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; - return; + current_page = pop_page_stack(); + return 0; } int num_images = 0; - if(image_plugin->get_number_of_images(images_url, num_images) != ImageResult::OK) { + if(images_page->get_number_of_images(num_images) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; - return; + current_page = pop_page_stack(); + return 0; } image_index = std::min(image_index, num_images); if(num_images != (int)image_upscale_status.size()) image_upscale_status.resize(num_images); - download_chapter_images_if_needed(image_plugin); + download_chapter_images_if_needed(images_page); if(image_index < num_images) { sf::String error_msg; @@ -2153,14 +1990,15 @@ namespace QuickMedia { download_in_progress = true; error_message.setString(error_msg); } else if(image_index == num_images) { - error_message.setString("End of " + chapter_title); + error_message.setString("End of " + images_page->get_chapter_name()); } + // TODO: Dont do this every time we change page? Json::Value &json_chapters = content_storage_json["chapters"]; Json::Value json_chapter; int latest_read = image_index + 1; if(json_chapters.isObject()) { - json_chapter = json_chapters[chapter_title]; + json_chapter = json_chapters[images_page->get_chapter_name()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -2174,7 +2012,7 @@ namespace QuickMedia { } json_chapter["current"] = std::min(latest_read, num_images); json_chapter["total"] = num_images; - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } @@ -2182,9 +2020,9 @@ namespace QuickMedia { bool error = !error_message.getString().isEmpty(); bool redraw = true; - sf::Text chapter_text(content_title + " | " + chapter_title + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_images), *font, 14); + sf::Text chapter_text(images_page->manga_name + " | " + images_page->get_chapter_name() + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_images), *font, 14); if(image_index == num_images) - chapter_text.setString(content_title + " | " + chapter_title + " | End"); + chapter_text.setString(images_page->manga_name + " | " + images_page->get_chapter_name() + " | End"); chapter_text.setFillColor(sf::Color::White); sf::RectangleShape chapter_text_background; chapter_text_background.setFillColor(sf::Color(0, 0, 0, 150)); @@ -2204,10 +2042,11 @@ namespace QuickMedia { while(window.pollEvent(event)) {} // TODO: Show to user if a certain page is missing (by checking page name (number) and checking if some are skipped) - while (current_page == Page::IMAGES) { + while (current_page == PageType::IMAGES) { while(window.pollEvent(event)) { if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -2221,29 +2060,22 @@ namespace QuickMedia { if(image_index > 0) { --image_index; goto end_of_images_page; - } else if(image_index == 0 && body->get_selected_item() < (int)body->items.size() - 1) { - // TODO: Make this work if the list is sorted differently than from newest to oldest. - body->filter_search_fuzzy(""); - body->select_next_item(); - select_episode(body->items[body->get_selected_item()].get(), true); - image_index = 99999; // Start at the page that shows we are at the end of the chapter + } else if(image_index == 0 && chapters_body->get_selected_item() < (int)chapters_body->items.size() - 1) { + page_navigation = -1; goto end_of_images_page; } } else if(event.key.code == sf::Keyboard::Down) { if(image_index < num_images) { ++image_index; goto end_of_images_page; - } else if(image_index == num_images && body->get_selected_item() > 0) { - // TODO: Make this work if the list is sorted differently than from newest to oldest. - body->filter_search_fuzzy(""); - body->select_previous_item(); - select_episode(body->items[body->get_selected_item()].get(), true); + } else if(image_index == num_images && chapters_body->get_selected_item() > 0) { + page_navigation = 1; goto end_of_images_page; } } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); } else if(event.key.code == sf::Keyboard::I) { - current_page = Page::IMAGES_CONTINUOUS; + current_page = PageType::IMAGES_CONTINUOUS; image_view_mode = ImageViewMode::SCROLL; } else if(event.key.code == sf::Keyboard::F) { fit_image_to_window = !fit_image_to_window; @@ -2317,46 +2149,47 @@ namespace QuickMedia { } end_of_images_page: - if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) { + if(current_page != PageType::IMAGES && current_page != PageType::IMAGES_CONTINUOUS) { image_download_cancel = true; + if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish + image_download_future.get(); + image_download_cancel = false; + } std::unique_lock<std::mutex> lock(image_upscale_mutex); images_to_upscale.clear(); image_upscale_status.clear(); } + return page_navigation; } - void Program::image_continuous_page() { + void Program::image_continuous_page(MangaImagesPage *images_page) { image_download_cancel = false; - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; - - assert(current_plugin->is_manga()); - Manga *image_plugin = static_cast<Manga*>(current_plugin); - content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); return; } int num_images = 0; - if(image_plugin->get_number_of_images(images_url, num_images) != ImageResult::OK) { + if(images_page->get_number_of_images(num_images) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); return; } if(num_images != (int)image_upscale_status.size()) image_upscale_status.resize(num_images); - download_chapter_images_if_needed(image_plugin); + download_chapter_images_if_needed(images_page); Json::Value &json_chapters = content_storage_json["chapters"]; Json::Value json_chapter; int latest_read = 1 + image_index; if(json_chapters.isObject()) { - json_chapter = json_chapters[chapter_title]; + json_chapter = json_chapters[images_page->get_chapter_name()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -2369,27 +2202,27 @@ namespace QuickMedia { json_chapter = Json::Value(Json::objectValue); } - ImageViewer image_viewer(image_plugin, images_url, content_title, chapter_title, image_index, content_cache_dir, font.get()); + ImageViewer image_viewer(images_page, images_page->manga_name, images_page->get_chapter_name(), image_index, content_cache_dir, font.get()); json_chapter["current"] = std::min(latest_read, image_viewer.get_num_pages()); json_chapter["total"] = image_viewer.get_num_pages(); - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } - while(current_page == Page::IMAGES_CONTINUOUS) { + while(current_page == PageType::IMAGES_CONTINUOUS) { window.clear(back_color); ImageViewerAction action = image_viewer.draw(window); switch(action) { case ImageViewerAction::NONE: break; case ImageViewerAction::RETURN: - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); break; case ImageViewerAction::SWITCH_TO_SINGLE_IMAGE_MODE: image_view_mode = ImageViewMode::SINGLE; - current_page = Page::IMAGES; + current_page = PageType::IMAGES; break; } window.display(); @@ -2399,414 +2232,31 @@ namespace QuickMedia { if(focused_page > latest_read) { latest_read = focused_page; json_chapter["current"] = latest_read; - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } } } - if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) { + if(current_page != PageType::IMAGES && current_page != PageType::IMAGES_CONTINUOUS) { image_download_cancel = true; + if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish + image_download_future.get(); + image_download_cancel = false; + } std::unique_lock<std::mutex> lock(image_upscale_mutex); images_to_upscale.clear(); image_upscale_status.clear(); } } - void Program::content_list_page() { - std::string update_search_text; - bool search_text_updated = false; - bool search_running = false; - std::future<BodyItems> search_future; - - if(!current_plugin->content_list_search_is_filter()) - search_bar->text_autosearch_delay = current_plugin->get_content_list_search_delay(); - - body->clear_items(); - body->clear_thumbnails(); - if(current_plugin->get_content_list(content_list_url, body->items) != PluginResult::OK) { - show_notification("Content list", "Failed to get content list for url: " + content_list_url, Urgency::CRITICAL); - current_page = Page::SEARCH_SUGGESTION; - return; - } - - search_bar->onTextUpdateCallback = [this, &update_search_text, &search_text_updated](const std::string &text) { - if(current_plugin->content_list_search_is_filter()) { - body->filter_search_fuzzy(text); - body->select_first_item(); - } else { - update_search_text = text; - search_text_updated = true; - } - }; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - content_episode = selected_item->get_title(); - content_url = selected_item->url; - current_page = Page::CONTENT_DETAILS; - body->clear_items(); - return true; - }; - - int fetched_page = 0; - bool fetching_next_page_running = false; - double gradient_inc = 0; - const float gradient_height = 5.0f; - sf::Vertex gradient_points[4]; - std::future<BodyItems> next_page_future; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - sf::Clock frame_timer; - - while (current_page == Page::CONTENT_LIST) { - sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); - - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION, false); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { - redraw = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Down || event.key.code == sf::Keyboard::PageDown || event.key.code == sf::Keyboard::End) { - bool hit_bottom = false; - switch(event.key.code) { - case sf::Keyboard::Down: - hit_bottom = !body->select_next_item(); - break; - case sf::Keyboard::PageDown: - hit_bottom = !body->select_next_page(); - break; - case sf::Keyboard::End: - body->select_last_item(); - hit_bottom = true; - break; - default: - hit_bottom = false; - break; - } - if(hit_bottom && !search_running && !fetching_next_page_running) { - gradient_inc = 0; - fetching_next_page_running = true; - int next_page = fetched_page + 1; - std::string content_list_url_copy = content_list_url; - std::string update_search_text_copy = update_search_text; - next_page_future = std::async(std::launch::async, [this, content_list_url_copy, update_search_text_copy, next_page]() { - BodyItems result_items; - if(current_plugin->content_list_search_page(content_list_url_copy, update_search_text_copy, next_page, result_items) != SearchResult::OK) - fprintf(stderr, "Failed to get next content list page (page %d)\n", next_page); - return result_items; - }); - } - } else if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::PageUp) { - body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::Home) { - body->select_first_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } - } - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - - gradient_points[0].position.x = 0.0f; - gradient_points[0].position.y = window_size.y - gradient_height; - - gradient_points[1].position.x = window_size.x; - gradient_points[1].position.y = window_size.y - gradient_height; - - gradient_points[2].position.x = window_size.x; - gradient_points[2].position.y = window_size.y; - - gradient_points[3].position.x = 0.0f; - gradient_points[3].position.y = window_size.y; - } - - search_bar->update(); - - if(search_text_updated && !fetching_next_page_running && !search_running) { - std::string search_term = update_search_text; - search_future = std::async(std::launch::async, [this, search_term]() { - BodyItems result; - if(current_plugin->content_list_search(content_list_url, search_term, result) != SearchResult::OK) { - // TODO: Show this? - //show_notification("Search", "Search failed!", Urgency::CRITICAL); - } - return result; - }); - search_text_updated = false; - search_running = true; - } - - if(search_running && search_future.valid() && search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if(!search_text_updated) { - body->items = search_future.get(); - body->select_first_item(); - } else { - search_future.get(); - } - search_running = false; - fetched_page = 0; - } - - if(fetching_next_page_running && next_page_future.valid() && next_page_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - BodyItems new_body_items = next_page_future.get(); - fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", fetched_page + 1, new_body_items.size()); - size_t num_new_messages = new_body_items.size(); - if(num_new_messages > 0) { - body->append_items(std::move(new_body_items)); - fetched_page++; - } - fetching_next_page_running = false; - } - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - if(fetching_next_page_running) { - double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; - gradient_inc += (frame_time_ms * 0.5); - sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); - - gradient_points[0].color = back_color; - gradient_points[1].color = back_color; - gradient_points[2].color = bottom_color; - gradient_points[3].color = bottom_color; - window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl - } - window.display(); - } - - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); - } - - void Program::content_details_page() { - if(current_plugin->get_content_details(content_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::CONTENT_LIST; - return; - } - - search_bar->onTextUpdateCallback = nullptr; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - if(current_plugin->name == "nyaa.si") { - BodyItem *selected_item = body->get_selected(); - if(selected_item && strncmp(selected_item->url.c_str(), "magnet:?", 8) == 0) { - if(!is_program_executable_by_name("xdg-open")) { - show_notification("Nyaa.si", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); - return false; - } - const char *args[] = { "xdg-open", selected_item->url.c_str(), nullptr }; - exec_program_async(args, nullptr); - } - } - return false; - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - 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; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.display(); - } - } - - void Program::file_manager_page() { - selected_files.clear(); - search_bar->clear(); - int prev_autosearch_delay = search_bar->text_autosearch_delay; - search_bar->text_autosearch_delay = file_manager->get_search_delay(); - Page previous_page = pop_page_stack(); - - sf::Text current_dir_text(file_manager->get_current_dir().string(), *bold_font, 18); - - // TODO: Make asynchronous. - // TODO: Automatically go to the parent if this fails (recursively). - body->select_first_item(); - body->items.clear(); - if(file_manager->get_files_in_directory(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "File manager failed to get files in directory: " + file_manager->get_current_dir().string(), Urgency::CRITICAL); - } - - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->reset_selected(); - }; - - search_bar->onTextSubmitCallback = [this, previous_page, ¤t_dir_text](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - if(file_manager->set_child_directory(selected_item->get_title())) { - std::string current_dir_str = file_manager->get_current_dir().string(); - current_dir_text.setString(current_dir_str); - // TODO: Make asynchronous. - // TODO: Automatically go to the parent if this fails (recursively). - body->items.clear(); - if(file_manager->get_files_in_directory(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "File manager failed to get files in directory: " + current_dir_str, Urgency::CRITICAL); - } - body->select_first_item(); - return true; - } else { - std::filesystem::path full_path = file_manager->get_current_dir() / selected_item->get_title(); - selected_files.push_back(full_path.string()); - printf("%s\n", selected_files.back().c_str()); - current_page = previous_page; - return false; - } - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::FILE_MANAGER) { - while (window.pollEvent(event)) { - base_event_handler(event, previous_page); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - const float dir_text_height = std::floor(current_dir_text.getLocalBounds().height + 12.0f); - body_pos.y += dir_text_height; - body_size.y -= dir_text_height; - current_dir_text.setPosition(body_pos.x, body_pos.y - dir_text_height); - } - - search_bar->update(); - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.draw(current_dir_text); - window.display(); - } - - search_bar->text_autosearch_delay = prev_autosearch_delay; - // We want exit code 1 if the file manager was launched and no files were selected, to know when the user didn't select any file(s) - if(selected_files.empty() && current_page == Page::EXIT) - exit_code = 1; - } - - void Program::image_board_thread_list_page() { - assert(current_plugin->is_image_board()); - ImageBoard *image_board = static_cast<ImageBoard*>(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&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - content_episode = selected_item->get_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) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.display(); - } - } - static bool is_url_video(const std::string &url) { return string_ends_with(url, ".webm") || string_ends_with(url, ".mp4") || string_ends_with(url, ".gif"); } - void Program::image_board_thread_page() { - assert(current_plugin->is_image_board()); - // TODO: Support image board other than 4chan. To make this work, the captcha code needs to be changed - // to work with other captcha than google captcha - assert(current_plugin->name == "4chan"); - ImageBoard *image_board = static_cast<ImageBoard*>(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; - } - - const std::string &board = image_board_thread_list_url; - const std::string &thread = content_url; - + void Program::image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body) { // TODO: Instead of using stage here, use different pages for each stage enum class NavigationStage { VIEWING_COMMENTS, @@ -2857,7 +2307,7 @@ namespace QuickMedia { // TODO: Make this work with other sites than 4chan auto request_google_captcha_image = [this, &captcha_texture, &captcha_image_mutex, &navigation_stage, &captcha_sprite, &challenge_description_text](GoogleCaptchaChallengeInfo &challenge_info) { std::string payload_image_data; - DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, current_plugin->use_tor); + DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, is_tor_enabled()); if(download_image_result == DownloadResult::OK) { std::lock_guard<std::mutex> lock(captcha_image_mutex); if(captcha_texture.loadFromMemory(payload_image_data.data(), payload_image_data.size())) { @@ -2893,40 +2343,40 @@ namespace QuickMedia { show_notification("Google captcha", "Failed to get captcha challenge", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } - }, current_plugin->use_tor); + }, is_tor_enabled()); }; Entry comment_input("Press ctrl+m to begin writing a comment...", font.get(), cjk_font.get()); comment_input.draw_background = false; comment_input.set_editable(false); - auto post_comment = [this, &comment_input, &navigation_stage, &image_board, &board, &thread, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { + auto post_comment = [this, &comment_input, &navigation_stage, &thread_page, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { comment_input.set_editable(false); navigation_stage = NavigationStage::POSTING_COMMENT; - PostResult post_result = image_board->post_comment(board, thread, captcha_post_id, comment_to_post); + PostResult post_result = thread_page->post_comment(captcha_post_id, comment_to_post); if(post_result == PostResult::OK) { - show_notification(current_plugin->name, "Comment posted!"); + show_notification("QuickMedia", "Comment posted!"); navigation_stage = NavigationStage::VIEWING_COMMENTS; // TODO: Append posted comment to the thread so the user can see their posted comment. // TODO: Asynchronously update the thread periodically to show new comments. } else if(post_result == PostResult::TRY_AGAIN) { - show_notification(current_plugin->name, "Error while posting, did the captcha expire? Please try again"); + show_notification("QuickMedia", "Error while posting, did the captcha expire? Please try again"); // TODO: Check if the response contains a new captcha instead of requesting a new one manually request_new_google_captcha_challenge(); } else if(post_result == PostResult::BANNED) { - show_notification(current_plugin->name, "Failed to post comment because you are banned", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment because you are banned", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::ERR) { - show_notification(current_plugin->name, "Failed to post comment. Is " + current_plugin->name + " down or is your internet down?", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment. Is " + std::string(plugin_name) + " down or is your internet down?", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else { assert(false && "Unhandled post result"); - show_notification(current_plugin->name, "Failed to post comment. Unknown error", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment. Unknown error", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } }; - comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &image_board](const std::string &text) -> bool { + comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](const std::string &text) -> bool { if(text.empty()) return false; @@ -2937,9 +2387,9 @@ namespace QuickMedia { post_comment(); return true; }); - } else if(image_board->get_pass_id().empty()) { + } else if(thread_page->get_pass_id().empty()) { request_new_google_captcha_challenge(); - } else if(!image_board->get_pass_id().empty()) { + } else if(!thread_page->get_pass_id().empty()) { post_comment_future = std::async(std::launch::async, [&post_comment]() -> bool { post_comment(); return true; @@ -2967,7 +2417,7 @@ namespace QuickMedia { std::stack<int> comment_navigation_stack; std::stack<int> comment_page_scroll_stack; - while (current_page == Page::IMAGE_BOARD_THREAD) { + while (current_page == PageType::IMAGE_BOARD_THREAD) { while (window.pollEvent(event)) { if(navigation_stage == NavigationStage::REPLYING) { comment_input.process_event(event); @@ -2981,8 +2431,9 @@ namespace QuickMedia { } } - if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + if(event.type == sf::Event::Closed) { + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -2994,46 +2445,42 @@ namespace QuickMedia { redraw = true; else if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::VIEWING_COMMENTS) { if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); + thread_body->select_previous_item(); } else if(event.key.code == sf::Keyboard::Down) { - body->select_next_item(); + thread_body->select_next_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - body->select_previous_page(); + thread_body->select_previous_page(); } else if(event.key.code == sf::Keyboard::PageDown) { - body->select_next_page(); + thread_body->select_next_page(); } else if(event.key.code == sf::Keyboard::Home) { - body->select_first_item(); + thread_body->select_first_item(); } else if(event.key.code == sf::Keyboard::End) { - body->select_last_item(); + thread_body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::IMAGE_BOARD_THREAD_LIST; - body->clear_items(); - body->reset_selected(); + current_page = pop_page_stack(); } else if(event.key.code == sf::Keyboard::P) { - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->attached_content_url.empty()) { if(is_url_video(selected_item->attached_content_url)) { - page_stack.push(Page::IMAGE_BOARD_THREAD); - current_page = Page::VIDEO_CONTENT; - std::string prev_content_url = content_url; - content_url = selected_item->attached_content_url; + page_stack.push(PageType::IMAGE_BOARD_THREAD); + current_page = PageType::VIDEO_CONTENT; watched_videos.clear(); - video_content_page(); - content_url = std::move(prev_content_url); + // TODO: Use real title + video_content_page(thread_page, selected_item->attached_content_url, "No title.webm"); redraw = true; } else { if(downloading_image && load_image_future.valid()) load_image_future.get(); downloading_image = true; navigation_stage = NavigationStage::VIEWING_ATTACHED_IMAGE; - load_image_future = std::async(std::launch::async, [this, &image_board]() { + load_image_future = std::async(std::launch::async, [this, &thread_body]() { std::string image_data; - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); if(!selected_item || selected_item->attached_content_url.empty()) { return image_data; } - if(download_to_string_cache(selected_item->attached_content_url, image_data, {}, image_board->use_tor) != DownloadResult::OK) { - show_notification(image_board->name, "Failed to download image: " + selected_item->attached_content_url, Urgency::CRITICAL); + if(download_to_string_cache(selected_item->attached_content_url, image_data, {}, is_tor_enabled()) != DownloadResult::OK) { + show_notification("QuickMedia", "Failed to download image: " + selected_item->attached_content_url, Urgency::CRITICAL); image_data.clear(); } return image_data; @@ -3042,43 +2489,43 @@ namespace QuickMedia { } } - BodyItem *selected_item = body->get_selected(); - if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || body->get_selected_item() != comment_navigation_stack.top()) && !selected_item->replies.empty()) { - for(auto &body_item : body->items) { + BodyItem *selected_item = thread_body->get_selected(); + if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || thread_body->get_selected_item() != comment_navigation_stack.top()) && !selected_item->replies.empty()) { + for(auto &body_item : thread_body->items) { body_item->visible = false; } selected_item->visible = true; for(size_t reply_index : selected_item->replies) { - body->items[reply_index]->visible = true; + thread_body->items[reply_index]->visible = true; } - comment_navigation_stack.push(body->get_selected_item()); - comment_page_scroll_stack.push(body->get_page_scroll()); - body->clamp_selection(); - body->set_page_scroll(0.0f); + comment_navigation_stack.push(thread_body->get_selected_item()); + comment_page_scroll_stack.push(thread_body->get_page_scroll()); + thread_body->clamp_selection(); + thread_body->set_page_scroll(0.0f); } else if(event.key.code == sf::Keyboard::BackSpace && !comment_navigation_stack.empty()) { size_t previous_selected = comment_navigation_stack.top(); float previous_page_scroll = comment_page_scroll_stack.top(); comment_navigation_stack.pop(); comment_page_scroll_stack.pop(); if(comment_navigation_stack.empty()) { - for(auto &body_item : body->items) { + for(auto &body_item : thread_body->items) { body_item->visible = true; } - body->set_selected_item(previous_selected); - body->clamp_selection(); + thread_body->set_selected_item(previous_selected); + thread_body->clamp_selection(); } else { - for(auto &body_item : body->items) { + for(auto &body_item : thread_body->items) { body_item->visible = false; } - body->set_selected_item(previous_selected); - selected_item = body->items[comment_navigation_stack.top()].get(); + thread_body->set_selected_item(previous_selected); + selected_item = thread_body->items[comment_navigation_stack.top()].get(); selected_item->visible = true; for(size_t reply_index : selected_item->replies) { - body->items[reply_index]->visible = true; + thread_body->items[reply_index]->visible = true; } - body->clamp_selection(); + thread_body->clamp_selection(); } - body->set_page_scroll(previous_page_scroll); + thread_body->set_page_scroll(previous_page_scroll); } else if(event.key.code == sf::Keyboard::M && event.key.control && selected_item) { navigation_stage = NavigationStage::REPLYING; comment_input.set_editable(true); @@ -3126,7 +2573,7 @@ namespace QuickMedia { } request_google_captcha_image(challenge_info); } - }, current_plugin->use_tor); + }, is_tor_enabled()); } } @@ -3226,11 +2673,11 @@ namespace QuickMedia { //attached_image_texture->generateMipmap(); attached_image_sprite.setTexture(*attached_image_texture, true); } else { - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); std::string selected_item_attached_url; if(selected_item) selected_item_attached_url = selected_item->attached_content_url; - show_notification(image_board->name, "Failed to load image downloaded from url: " + selected_item->attached_content_url, Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to load image downloaded from url: " + selected_item->attached_content_url, Urgency::CRITICAL); } } @@ -3260,12 +2707,12 @@ namespace QuickMedia { window.draw(comment_input_shade); window.draw(logo_sprite); comment_input.draw(window); - body->draw(window, body_pos, body_size); + thread_body->draw(window, body_pos, body_size); } else if(navigation_stage == NavigationStage::VIEWING_COMMENTS) { window.draw(comment_input_shade); window.draw(logo_sprite); comment_input.draw(window); - body->draw(window, body_pos, body_size); + thread_body->draw(window, body_pos, body_size); } window.display(); } @@ -3279,14 +2726,10 @@ namespace QuickMedia { post_comment_future.get(); if(load_image_future.valid()) load_image_future.get(); - - // Clear post that is still being written. - // TODO: This post should be saved for the thread. Each thread should have its own text edit widget, - // so you dont have to retype a post that was in the middle of being posted when returning. } void Program::chat_login_page() { - assert(current_plugin->name == "matrix"); + assert(strcmp(plugin_name, "matrix") == 0); SearchBar login_input(*font, nullptr, "Username"); SearchBar password_input(*font, nullptr, "Password", true); @@ -3298,23 +2741,22 @@ namespace QuickMedia { SearchBar *inputs[num_inputs] = { &login_input, &password_input, &homeserver_input }; int focused_input = 0; - auto text_submit_callback = [this, inputs, &status_text](const sf::String&) -> bool { - Matrix *matrix = static_cast<Matrix*>(current_plugin); + auto text_submit_callback = [this, inputs, &status_text](const sf::String&) { for(int i = 0; i < num_inputs; ++i) { if(inputs[i]->get_text().empty()) { status_text.setString("All fields need to be filled in"); - return false; + return; } } std::string err_msg; // TODO: Make asynchronous if(matrix->login(inputs[0]->get_text(), inputs[1]->get_text(), inputs[2]->get_text(), err_msg) == PluginResult::OK) { - current_page = Page::CHAT; + current_page = PageType::CHAT; } else { status_text.setString("Failed to login, error: " + err_msg); } - return false; + return; }; for(int i = 0; i < num_inputs; ++i) { @@ -3328,9 +2770,11 @@ namespace QuickMedia { bool redraw = true; sf::Event event; - while (current_page == Page::CHAT_LOGIN) { + auto body = create_body(); + + while (current_page == PageType::CHAT_LOGIN) { while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false, false); + base_event_handler(event, PageType::EXIT, body.get(), nullptr, false, false); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { redraw = true; } else if(event.type == sf::Event::TextEntered) { @@ -3347,8 +2791,7 @@ 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, nullptr, body_pos, body_size); } window.clear(back_color); @@ -3392,8 +2835,9 @@ namespace QuickMedia { } void Program::chat_page() { - assert(current_plugin->name == "matrix"); - Matrix *matrix = static_cast<Matrix*>(current_plugin); + assert(strcmp(plugin_name, "matrix") == 0); + + auto video_page = std::make_unique<MatrixVideoPage>(this); std::vector<ChatTab> tabs; int selected_tab = 0; @@ -3439,7 +2883,7 @@ namespace QuickMedia { bool is_window_focused = window.hasFocus(); - auto process_new_room_messages = [matrix, &body_items_by_room_id, ¤t_room_id, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { + auto process_new_room_messages = [this, &body_items_by_room_id, ¤t_room_id, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { for(auto &[room, messages] : room_sync_messages) { bool was_mentioned = false; for(auto &message : messages) { @@ -3494,7 +2938,7 @@ namespace QuickMedia { URL_SELECTION }; - Page new_page = Page::CHAT; + PageType new_page = PageType::CHAT; ChatState chat_state = ChatState::NAVIGATING; std::shared_ptr<BodyItem> currently_operating_on_item; @@ -3506,7 +2950,7 @@ namespace QuickMedia { chat_input.draw_background = false; chat_input.set_editable(false); - chat_input.on_submit_callback = [matrix, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { + chat_input.on_submit_callback = [this, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { if(tabs[selected_tab].type == ChatTabType::MESSAGES) { if(text.empty()) return false; @@ -3514,12 +2958,12 @@ namespace QuickMedia { if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') { std::string command = strip(text); if(command == "/upload") { - new_page = Page::FILE_MANAGER; + new_page = PageType::FILE_MANAGER; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(command == "/logout") { - new_page = Page::CHAT_LOGIN; + new_page = PageType::CHAT_LOGIN; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; @@ -3622,7 +3066,7 @@ namespace QuickMedia { auto room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); AsyncImageLoader async_image_loader; - auto typing_async_func = [matrix](bool new_state, std::string room_id) { + auto typing_async_func = [this](bool new_state, std::string room_id) { if(new_state) { matrix->on_start_typing(room_id); } else { @@ -3650,17 +3094,17 @@ namespace QuickMedia { std::future<void> set_read_marker_future; bool setting_read_marker = false; - auto launch_url = [this, &redraw](const std::string &url) mutable { + auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable { if(url.empty()) return; std::string video_id; if(youtube_url_extract_id(url, video_id)) { - page_stack.push(Page::CHAT); + page_stack.push(PageType::CHAT); watched_videos.clear(); - content_url = url; - current_page = Page::VIDEO_CONTENT; - video_content_page(); + current_page = PageType::VIDEO_CONTENT; + // TODO: Add title + video_content_page(video_page.get(), url, "No title"); redraw = true; } else { if(!is_program_executable_by_name("xdg-open")) { @@ -3710,10 +3154,10 @@ namespace QuickMedia { return result; }; - while (current_page == Page::CHAT) { + while (current_page == PageType::CHAT) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false, false); + base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); if(event.type == sf::Event::GainedFocus) { is_window_focused = true; redraw = true; @@ -3744,7 +3188,6 @@ namespace QuickMedia { fetching_previous_messages_running = true; previous_messages_future_room_id = current_room_id; previous_messages_future = std::async(std::launch::async, [this, &previous_messages_future_room_id]() { - Matrix *matrix = static_cast<Matrix*>(current_plugin); BodyItems result_items; if(matrix->get_previous_room_messages(previous_messages_future_room_id, result_items) != PluginResult::OK) fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", previous_messages_future_room_id.c_str()); @@ -3758,9 +3201,7 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::End) { tabs[selected_tab].body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - body->clear_items(); - body->reset_selected(); + current_page = PageType::EXIT; } else if(event.key.code == sf::Keyboard::Left && synced) { tabs[selected_tab].body->clear_thumbnails(); selected_tab = std::max(0, selected_tab - 1); @@ -3836,11 +3277,11 @@ namespace QuickMedia { if(!selected->url.empty()) { const char *content_type = link_get_content_type(selected->url); if(content_type && (strcmp(content_type, "audio") == 0 || strcmp(content_type, "video") == 0 || strcmp(content_type, "image") == 0)) { - page_stack.push(Page::CHAT); + page_stack.push(PageType::CHAT); watched_videos.clear(); - content_url = selected->url; - current_page = Page::VIDEO_CONTENT; - video_content_page(); + current_page = PageType::VIDEO_CONTENT; + // TODO: Add title + video_content_page(video_page.get(), selected->url, "No title"); redraw = true; continue; } @@ -3959,15 +3400,19 @@ namespace QuickMedia { } switch(new_page) { - case Page::FILE_MANAGER: { - new_page = Page::CHAT; - if(!file_manager) { - file_manager = new FileManager(); - file_manager->set_current_directory(get_home_dir().data); - } - page_stack.push(Page::CHAT); - current_page = Page::FILE_MANAGER; - file_manager_page(); + case PageType::FILE_MANAGER: { + new_page = PageType::CHAT; + + auto file_manager_page = std::make_unique<FileManagerPage>(this); + file_manager_page->set_current_directory(get_home_dir().data); + auto file_manager_body = create_body(); + file_manager_page->get_files_in_directory(file_manager_body->items); + std::vector<Tab> tabs; + tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + selected_files.clear(); + page_loop(std::move(tabs)); + if(selected_files.empty()) { fprintf(stderr, "No files selected!\n"); } else { @@ -3982,8 +3427,8 @@ namespace QuickMedia { redraw = true; break; } - case Page::CHAT_LOGIN: { - new_page = Page::CHAT; + case PageType::CHAT_LOGIN: { + new_page = PageType::CHAT; matrix->logout(); tabs[MESSAGES_TAB_INDEX].body->clear_thumbnails(); // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. @@ -3991,9 +3436,9 @@ namespace QuickMedia { // and one of them is /sync, which has a timeout of 30 seconds. That timeout has to be killed somehow. //delete current_plugin; //current_plugin = new Matrix(); - current_page = Page::CHAT_LOGIN; + current_page = PageType::CHAT_LOGIN; chat_login_page(); - if(current_page == Page::CHAT) + if(current_page == PageType::CHAT) chat_page(); exit(0); break; @@ -4091,8 +3536,6 @@ namespace QuickMedia { sync_timer.restart(); sync_future_room_id = current_room_id; sync_future = std::async(std::launch::async, [this, &sync_future_room_id, synced]() { - Matrix *matrix = static_cast<Matrix*>(current_plugin); - SyncFutureResult result; if(matrix->sync(result.room_sync_messages) == PluginResult::OK) { fprintf(stderr, "Synced matrix\n"); @@ -4100,7 +3543,7 @@ namespace QuickMedia { if(!synced) { if(matrix->get_joined_rooms(result.rooms_body_items) != PluginResult::OK) { show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL); - current_page = Page::EXIT; + current_page = PageType::EXIT; return result; } } @@ -4311,7 +3754,7 @@ namespace QuickMedia { current_room_body_data->last_read_message_timestamp = message->timestamp; // TODO: What if the message is no longer valid? setting_read_marker = true; - set_read_marker_future = std::async(std::launch::async, [matrix, current_room_id, message]() mutable { + set_read_marker_future = std::async(std::launch::async, [this, current_room_id, message]() mutable { if(matrix->set_read_marker(current_room_id, message) != PluginResult::OK) { fprintf(stderr, "Warning: failed to set read marker to %s\n", message->event_id.c_str()); } |